/* * Copyright (C) 2008-2014 David Robillard * Copyright (C) 2009-2011 Carl Hetherington * Copyright (C) 2009-2013 Paul Davis * Copyright (C) 2014-2017 Robin Gareus * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "ardour/midi_track.h" #include "evoral/midi_events.h" #include #include "gtkmm2ext/keyboard.h" #include "editing.h" #include "midi_streamview.h" #include "midi_time_axis.h" #include "piano_roll_header.h" #include "public_editor.h" #include "ui_config.h" using namespace std; using namespace Gtkmm2ext; PianoRollHeader::Color PianoRollHeader::white = PianoRollHeader::Color (0.77f, 0.78f, 0.76f); PianoRollHeader::Color PianoRollHeader::white_highlight = PianoRollHeader::Color (1.00f, 0.40f, 0.40f); PianoRollHeader::Color PianoRollHeader::white_shade_light = PianoRollHeader::Color (0.95f, 0.95f, 0.95f); PianoRollHeader::Color PianoRollHeader::white_shade_dark = PianoRollHeader::Color (0.56f, 0.56f, 0.56f); PianoRollHeader::Color PianoRollHeader::black = PianoRollHeader::Color (0.24f, 0.24f, 0.24f); PianoRollHeader::Color PianoRollHeader::black_highlight = PianoRollHeader::Color (0.60f, 0.10f, 0.10f); PianoRollHeader::Color PianoRollHeader::black_shade_light = PianoRollHeader::Color (0.46f, 0.46f, 0.46f); PianoRollHeader::Color PianoRollHeader::black_shade_dark = PianoRollHeader::Color (0.1f, 0.1f, 0.1f); PianoRollHeader::Color::Color () : r (1.0f) , g (1.0f) , b (1.0f) { } PianoRollHeader::Color::Color (double _r, double _g, double _b) : r (_r) , g (_g) , b (_b) { } inline void PianoRollHeader::Color::set (const PianoRollHeader::Color& c) { r = c.r; g = c.g; b = c.b; } PianoRollHeader::PianoRollHeader (MidiStreamView& v) : _view (v) , _highlighted_note (NO_MIDI_NOTE) , _clicked_note (NO_MIDI_NOTE) , _dragging (false) { add_events (Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | Gdk::POINTER_MOTION_MASK | Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK | Gdk::SCROLL_MASK); for (int i = 0; i < 128; ++i) { _active_notes[i] = false; } _view.NoteRangeChanged.connect (sigc::mem_fun (*this, &PianoRollHeader::note_range_changed)); } inline void create_path (Cairo::RefPtr cr, double x[], double y[], int start, int stop) { cr->move_to (x[start], y[start]); for (int i = start + 1; i <= stop; ++i) { cr->line_to (x[i], y[i]); } } inline void render_rect (Cairo::RefPtr cr, int /*note*/, double x[], double y[], PianoRollHeader::Color& bg, PianoRollHeader::Color& tl_shadow, PianoRollHeader::Color& br_shadow) { cr->set_source_rgb (bg.r, bg.g, bg.b); create_path (cr, x, y, 0, 4); cr->fill (); cr->set_source_rgb (tl_shadow.r, tl_shadow.g, tl_shadow.b); create_path (cr, x, y, 0, 2); cr->stroke (); cr->set_source_rgb (br_shadow.r, br_shadow.g, br_shadow.b); create_path (cr, x, y, 2, 4); cr->stroke (); } inline void render_cf (Cairo::RefPtr cr, int /*note*/, double x[], double y[], PianoRollHeader::Color& bg, PianoRollHeader::Color& tl_shadow, PianoRollHeader::Color& br_shadow) { cr->set_source_rgb (bg.r, bg.g, bg.b); create_path (cr, x, y, 0, 6); cr->fill (); cr->set_source_rgb (tl_shadow.r, tl_shadow.g, tl_shadow.b); create_path (cr, x, y, 0, 4); cr->stroke (); cr->set_source_rgb (br_shadow.r, br_shadow.g, br_shadow.b); create_path (cr, x, y, 4, 6); cr->stroke (); } inline void render_eb (Cairo::RefPtr cr, int /*note*/, double x[], double y[], PianoRollHeader::Color& bg, PianoRollHeader::Color& tl_shadow, PianoRollHeader::Color& br_shadow) { cr->set_source_rgb (bg.r, bg.g, bg.b); create_path (cr, x, y, 0, 6); cr->fill (); cr->set_source_rgb (tl_shadow.r, tl_shadow.g, tl_shadow.b); create_path (cr, x, y, 0, 2); cr->stroke (); create_path (cr, x, y, 4, 5); cr->stroke (); cr->set_source_rgb (br_shadow.r, br_shadow.g, br_shadow.b); create_path (cr, x, y, 2, 4); cr->stroke (); create_path (cr, x, y, 5, 6); cr->stroke (); } inline void render_dga (Cairo::RefPtr cr, int /*note*/, double x[], double y[], PianoRollHeader::Color& bg, PianoRollHeader::Color& tl_shadow, PianoRollHeader::Color& br_shadow) { cr->set_source_rgb (bg.r, bg.g, bg.b); create_path (cr, x, y, 0, 8); cr->fill (); cr->set_source_rgb (tl_shadow.r, tl_shadow.g, tl_shadow.b); create_path (cr, x, y, 0, 4); cr->stroke (); create_path (cr, x, y, 6, 7); cr->stroke (); cr->set_source_rgb (br_shadow.r, br_shadow.g, br_shadow.b); create_path (cr, x, y, 4, 6); cr->stroke (); create_path (cr, x, y, 7, 8); cr->stroke (); } void PianoRollHeader::get_path (PianoRollHeader::ItemType note_type, int note, double x[], double y[]) { double y_pos = floor (_view.note_to_y (note)) + 1.5f; double note_height; double other_y1 = floor (_view.note_to_y (note + 1)) + floor (_note_height / 2.0f) + 2.5f; double other_y2 = floor (_view.note_to_y (note - 1)) + floor (_note_height / 2.0f) + 1.0f; double width = get_width (); if (note == 0) { note_height = floor (_view.contents_height ()) - y_pos + 2.; } else { note_height = floor (_view.note_to_y (note - 1)) - y_pos + 2.; } switch (note_type) { case BLACK_SEPARATOR: x[0] = 1.5f; y[0] = y_pos; x[1] = _black_note_width; y[1] = y_pos; break; case BLACK_MIDDLE_SEPARATOR: x[0] = _black_note_width; y[0] = y_pos + floor (_note_height / 2.0f); x[1] = width - 1.0f; y[1] = y[0]; break; case BLACK: x[0] = 1.5f; y[0] = y_pos + note_height - 0.5f; x[1] = 1.5f; y[1] = y_pos + 1.0f; x[2] = _black_note_width; y[2] = y_pos + 1.0f; x[3] = _black_note_width; y[3] = y_pos + note_height - 0.5f; x[4] = 1.5f; y[4] = y_pos + note_height - 0.5f; return; case WHITE_SEPARATOR: x[0] = 1.5f; y[0] = y_pos; x[1] = width - 1.5f; y[1] = y_pos; break; case WHITE_RECT: x[0] = 1.5f; y[0] = y_pos + note_height - 0.5f; x[1] = 1.5f; y[1] = y_pos + 1.0f; x[2] = width - 1.5f; y[2] = y_pos + 1.0f; x[3] = width - 1.5f; y[3] = y_pos + note_height - 0.5f; x[4] = 1.5f; y[4] = y_pos + note_height - 0.5f; return; case WHITE_CF: x[0] = 1.5f; y[0] = y_pos + note_height - 1.5f; x[1] = 1.5f; y[1] = y_pos + 1.0f; x[2] = _black_note_width + 1.0f; y[2] = y_pos + 1.0f; x[3] = _black_note_width + 1.0f; y[3] = other_y1; x[4] = width - 1.5f; y[4] = other_y1; x[5] = width - 1.5f; y[5] = y_pos + note_height - 1.5f; x[6] = 1.5f; y[6] = y_pos + note_height - 1.5f; return; case WHITE_EB: x[0] = 1.5f; y[0] = y_pos + note_height - 1.5f; x[1] = 1.5f; y[1] = y_pos + 1.0f; x[2] = width - 1.5f; y[2] = y_pos + 1.0f; x[3] = width - 1.5f; y[3] = other_y2; x[4] = _black_note_width + 1.0f; y[4] = other_y2; x[5] = _black_note_width + 1.0f; y[5] = y_pos + note_height - 1.5f; x[6] = 1.5f; y[6] = y_pos + note_height - 1.5f; return; case WHITE_DGA: x[0] = 1.5f; y[0] = y_pos + note_height - 1.5f; x[1] = 1.5f; y[1] = y_pos + 1.0f; x[2] = _black_note_width + 1.0f; y[2] = y_pos + 1.0f; x[3] = _black_note_width + 1.0f; y[3] = other_y1; x[4] = width - 1.5f; y[4] = other_y1; x[5] = width - 1.5f; y[5] = other_y2; x[6] = _black_note_width + 1.0f; y[6] = other_y2; x[7] = _black_note_width + 1.0f; y[7] = y_pos + note_height - 1.5f; x[8] = 1.5f; y[8] = y_pos + note_height - 1.5f; return; default: return; } } bool PianoRollHeader::on_expose_event (GdkEventExpose* ev) { GdkRectangle& rect = ev->area; double font_size; int lowest, highest; double x[9]; double y[9]; Color bg, tl_shadow, br_shadow; int oct_rel; int y1 = max (rect.y, 0); int y2 = min (rect.y + rect.height, (int)floor (_view.contents_height () - 1.0f)); Cairo::RefPtr cr = get_window ()->create_cairo_context (); Cairo::RefPtr pat = Cairo::LinearGradient::create (0, 0, _black_note_width, 0); //Cairo::TextExtents te; lowest = max (_view.lowest_note (), _view.y_to_note (y2)); highest = min (_view.highest_note (), _view.y_to_note (y1)); if (lowest > 127) { lowest = 0; } cr->select_font_face ("Georgia", Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_BOLD); font_size = min ((double)10.0f, _note_height - 4.0f); cr->set_font_size (font_size); /* fill the entire rect with the color for non-highlighted white notes. * then we won't have to draw the background for those notes, * and would only have to draw the background for the one highlighted white note*/ //cr->rectangle(rect.x, rect.y, rect.width, rect.height); //cr->set_source_rgb(white.r, white.g, white.b); //cr->fill(); cr->set_line_width (1.0f); /* draw vertical lines with shade at both ends of the widget */ cr->set_source_rgb (0.0f, 0.0f, 0.0f); cr->move_to (0.5f, rect.y); cr->line_to (0.5f, rect.y + rect.height); cr->stroke (); cr->move_to (get_width () - 0.5f, rect.y); cr->line_to (get_width () - 0.5f, rect.y + rect.height); cr->stroke (); //pat->add_color_stop_rgb(0.0, 0.33, 0.33, 0.33); //pat->add_color_stop_rgb(0.2, 0.39, 0.39, 0.39); //pat->add_color_stop_rgb(1.0, 0.22, 0.22, 0.22); //cr->set_source(pat); for (int i = lowest; i <= highest; ++i) { oct_rel = i % 12; switch (oct_rel) { case 1: case 3: case 6: case 8: case 10: /* black note */ if (i == _highlighted_note) { bg.set (black_highlight); } else { bg.set (black); } if (_active_notes[i]) { tl_shadow.set (black_shade_dark); br_shadow.set (black_shade_light); } else { tl_shadow.set (black_shade_light); br_shadow.set (black_shade_dark); } /* draw black separators */ cr->set_source_rgb (0.0f, 0.0f, 0.0f); get_path (BLACK_SEPARATOR, i, x, y); create_path (cr, x, y, 0, 1); cr->stroke (); get_path (BLACK_MIDDLE_SEPARATOR, i, x, y); create_path (cr, x, y, 0, 1); cr->stroke (); get_path (BLACK, i, x, y); render_rect (cr, i, x, y, bg, tl_shadow, br_shadow); break; default: /* white note */ if (i == _highlighted_note) { bg.set (white_highlight); } else { bg.set (white); } if (_active_notes[i]) { tl_shadow.set (white_shade_dark); br_shadow.set (white_shade_light); } else { tl_shadow.set (white_shade_light); br_shadow.set (white_shade_dark); } switch (oct_rel) { case 0: case 5: if (i == _view.highest_note ()) { get_path (WHITE_RECT, i, x, y); render_rect (cr, i, x, y, bg, tl_shadow, br_shadow); } else { get_path (WHITE_CF, i, x, y); render_cf (cr, i, x, y, bg, tl_shadow, br_shadow); } break; case 2: case 7: case 9: if (i == _view.highest_note ()) { get_path (WHITE_EB, i, x, y); render_eb (cr, i, x, y, bg, tl_shadow, br_shadow); } else if (i == _view.lowest_note ()) { get_path (WHITE_CF, i, x, y); render_cf (cr, i, x, y, bg, tl_shadow, br_shadow); } else { get_path (WHITE_DGA, i, x, y); render_dga (cr, i, x, y, bg, tl_shadow, br_shadow); } break; case 4: case 11: cr->set_source_rgb (0.0f, 0.0f, 0.0f); get_path (WHITE_SEPARATOR, i, x, y); create_path (cr, x, y, 0, 1); cr->stroke (); if (i == _view.lowest_note ()) { get_path (WHITE_RECT, i, x, y); render_rect (cr, i, x, y, bg, tl_shadow, br_shadow); } else { get_path (WHITE_EB, i, x, y); render_eb (cr, i, x, y, bg, tl_shadow, br_shadow); } break; default: break; } break; } /* render the name of which C this is */ if (oct_rel == 0) { std::stringstream s; double y = floor (_view.note_to_y (i)) - 0.5f; double note_height = floor (_view.note_to_y (i - 1)) - y; int cn = i / 12 - 1; s << "C" << cn; //cr->get_text_extents(s.str(), te); cr->set_source_rgb (0.30f, 0.30f, 0.30f); cr->move_to (2.0f, y + note_height - 1.0f - (note_height - font_size) / 2.0f); cr->show_text (s.str ()); } } return true; } bool PianoRollHeader::on_motion_notify_event (GdkEventMotion* ev) { int note = _view.y_to_note (ev->y); set_note_highlight (note); if (_dragging) { if (false /*editor().current_mouse_mode() == Editing::MouseRange*/) { //ToDo: fix this. this mode is buggy, and of questionable utility anyway /* select note range */ if (Keyboard::no_modifiers_active (ev->state)) { AddNoteSelection (note); // EMIT SIGNAL } } else { /* play notes */ /* redraw already taken care of above in set_note_highlight */ if (_clicked_note != NO_MIDI_NOTE && _clicked_note != note) { _active_notes[_clicked_note] = false; send_note_off (_clicked_note); _clicked_note = note; if (!_active_notes[note]) { _active_notes[note] = true; send_note_on (note); } } } } //win->process_updates(false); return true; } bool PianoRollHeader::on_button_press_event (GdkEventButton* ev) { int note = _view.y_to_note (ev->y); bool tertiary = Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier); if (ev->button == 2 && Keyboard::no_modifiers_active (ev->state)) { SetNoteSelection (note); // EMIT SIGNAL return true; } else if (tertiary && (ev->button == 1 || ev->button == 2)) { ExtendNoteSelection (note); // EMIT SIGNAL return true; } else if (ev->button == 1 && note >= 0 && note < 128) { add_modal_grab (); _dragging = true; if (!_active_notes[note]) { _active_notes[note] = true; _clicked_note = note; send_note_on (note); invalidate_note_range (note, note); } else { reset_clicked_note (note); } } return true; } bool PianoRollHeader::on_button_release_event (GdkEventButton* ev) { int note = _view.y_to_note (ev->y); if (false /*editor().current_mouse_mode() == Editing::MouseRange*/) { //Todo: this mode is buggy, and of questionable utility anyway if (Keyboard::no_modifiers_active (ev->state)) { AddNoteSelection (note); // EMIT SIGNAL } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::PrimaryModifier)) { ToggleNoteSelection (note); // EMIT SIGNAL } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::RangeSelectModifier)) { ExtendNoteSelection (note); // EMIT SIGNAL } } else { if (_dragging) { remove_modal_grab (); if (note == _clicked_note) { reset_clicked_note (note); } } } _dragging = false; return true; } void PianoRollHeader::set_note_highlight (uint8_t note) { if (_highlighted_note == note) { return; } if (_highlighted_note != NO_MIDI_NOTE) { if (note > _highlighted_note) { invalidate_note_range (_highlighted_note, note); } else { invalidate_note_range (note, _highlighted_note); } } _highlighted_note = note; if (_highlighted_note != NO_MIDI_NOTE) { invalidate_note_range (_highlighted_note, _highlighted_note); } } bool PianoRollHeader::on_enter_notify_event (GdkEventCrossing* ev) { set_note_highlight (_view.y_to_note (ev->y)); return true; } bool PianoRollHeader::on_leave_notify_event (GdkEventCrossing*) { if (has_grab () && _dragging) { return true; } invalidate_note_range (_highlighted_note, _highlighted_note); if (_clicked_note != NO_MIDI_NOTE) { reset_clicked_note (_clicked_note, _clicked_note != _highlighted_note); } _highlighted_note = NO_MIDI_NOTE; return true; } bool PianoRollHeader::on_scroll_event (GdkEventScroll*) { return true; } void PianoRollHeader::note_range_changed () { _note_height = floor (_view.note_height ()) + 0.5f; queue_draw (); } void PianoRollHeader::invalidate_note_range (int lowest, int highest) { Glib::RefPtr win = get_window (); Gdk::Rectangle rect; /* the non-rectangular geometry of some of the notes requires more * redraws than the notes that actually changed. */ switch (lowest % 12) { case 0: case 5: lowest = max ((int)_view.lowest_note (), lowest); break; default: lowest = max ((int)_view.lowest_note (), lowest - 1); break; } switch (highest % 12) { case 4: case 11: highest = min ((int)_view.highest_note (), highest); break; case 1: case 3: case 6: case 8: case 10: highest = min ((int)_view.highest_note (), highest + 1); break; default: highest = min ((int)_view.highest_note (), highest + 2); break; } double y = _view.note_to_y (highest); double height = _view.note_to_y (lowest - 1) - y; rect.set_x (0); rect.set_width (get_width ()); rect.set_y ((int)floor (y)); rect.set_height ((int)floor (height)); if (win) { win->invalidate_rect (rect, false); } } void PianoRollHeader::on_size_request (Gtk::Requisition* r) { r->width = std::max (20.f, rintf (20.f * UIConfiguration::instance ().get_ui_scale ())); } void PianoRollHeader::on_size_allocate (Gtk::Allocation& a) { DrawingArea::on_size_allocate (a); _black_note_width = floor (0.7 * get_width ()) + 0.5f; } void PianoRollHeader::send_note_on (uint8_t note) { boost::shared_ptr track = _view.trackview ().midi_track (); MidiTimeAxisView* mtv = dynamic_cast (&_view.trackview ()); //cerr << "note on: " << (int) note << endl; if (track) { _event[0] = (MIDI_CMD_NOTE_ON | mtv->get_preferred_midi_channel ()); _event[1] = note; _event[2] = 100; track->write_immediate_event (Evoral::MIDI_EVENT, 3, _event); } } void PianoRollHeader::send_note_off (uint8_t note) { boost::shared_ptr track = _view.trackview ().midi_track (); MidiTimeAxisView* mtv = dynamic_cast (&_view.trackview ()); if (track) { _event[0] = (MIDI_CMD_NOTE_OFF | mtv->get_preferred_midi_channel ()); _event[1] = note; _event[2] = 100; track->write_immediate_event (Evoral::MIDI_EVENT, 3, _event); } } void PianoRollHeader::reset_clicked_note (uint8_t note, bool invalidate) { _active_notes[note] = false; _clicked_note = NO_MIDI_NOTE; send_note_off (note); if (invalidate) { invalidate_note_range (note, note); } } PublicEditor& PianoRollHeader::editor () const { return _view.trackview ().editor (); }