diff --git a/gtk2_ardour/piano_roll_header.cc b/gtk2_ardour/piano_roll_header.cc index d58f95e099..4d9dc9c4cc 100644 --- a/gtk2_ardour/piano_roll_header.cc +++ b/gtk2_ardour/piano_roll_header.cc @@ -41,32 +41,10 @@ using namespace std; using namespace Gtkmm2ext; -PianoRollHeader::PianoRollHeader(MidiStreamView& v) - : have_note_names (false) - , _adj(v.note_range_adjustment) - , _view(v) - , _font_descript ("Sans Bold") - , _font_descript_big_c ("Sans") - , _font_descript_midnam ("Sans") - , _highlighted_note (NO_MIDI_NOTE) - , _clicked_note (NO_MIDI_NOTE) - , _dragging (false) - , _scroomer_size (63.f) - , _scroomer_drag (false) - , _old_y (0.0) - , _fract (0.0) - , _scroomer_state (NONE) - , _scroomer_button_state (NONE) - , _saved_top_val (0.0) - , _saved_bottom_val (127.0) - , _mini_map_display (false) - , entered (false) -{ - _layout = Pango::Layout::create (get_pango_context()); - _big_c_layout = Pango::Layout::create (get_pango_context()); - _font_descript_big_c.set_absolute_size (10.0 * Pango::SCALE); - _big_c_layout->set_font_description(_font_descript_big_c); - _midnam_layout = Pango::Layout::create (get_pango_context()); +PianoRollHeader::PianoRollHeader (MidiViewBackground& bg) + : PianoRollHeaderBase (bg) + { + alloc_layouts (get_pango_context ()); _adj.set_lower(0); _adj.set_upper(127); @@ -85,757 +63,65 @@ PianoRollHeader::PianoRollHeader(MidiStreamView& v) 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[], - Gtkmm2ext::Color& bg) -{ - set_source_rgba(cr, bg); - create_path(cr, x, y, 0, 4); - cr->fill(); -} - -void -PianoRollHeader::render_scroomer(Cairo::RefPtr cr) -{ - double scroomer_top = max (1.0, (1.0 - ((_adj.get_value()+_adj.get_page_size()) / 127.0)) * get_height () ); - double scroomer_bottom = (1.0 - (_adj.get_value () / 127.0)) * get_height (); - double scroomer_width = _scroomer_size; - - Gtkmm2ext::Color c = UIConfiguration::instance().color_mod (X_("scroomer"), X_("scroomer alpha")); - Gtkmm2ext::Color save_color (c); - - if (entered) { - c = HSV (c).lighter (0.25).color(); - } - - set_source_rgba (cr, c); - cr->move_to (1.f, scroomer_top); - cr->line_to (scroomer_width - 1.f, scroomer_top); - cr->line_to (scroomer_width - 1.f, scroomer_bottom); - cr->line_to (1.f, scroomer_bottom); - cr->line_to (1.f, scroomer_top); - cr->fill(); - - if (entered) { - cr->save (); - c = HSV (save_color).lighter (0.9).color(); - set_source_rgba (cr, c); - cr->set_line_width (4.); - cr->move_to (1.f, scroomer_top + 2.); - cr->line_to (scroomer_width - 1.f, scroomer_top + 2.); - cr->stroke (); - cr->line_to (scroomer_width - 1.f, scroomer_bottom - 2.); - cr->line_to (2.f, scroomer_bottom - 2.); - cr->stroke (); - cr->restore (); - } } bool PianoRollHeader::on_scroll_event (GdkEventScroll* ev) { - int note_range = _adj.get_page_size (); - int note_lower = _adj.get_value (); - - if(ev->state == GDK_SHIFT_MASK){ - switch (ev->direction) { - case GDK_SCROLL_UP: //ZOOM IN - _view.apply_note_range (min(note_lower + 1, 127), max(note_lower + note_range - 1,0), true); - break; - case GDK_SCROLL_DOWN: //ZOOM OUT - _view.apply_note_range (max(note_lower - 1,0), min(note_lower + note_range + 1, 127), true); - break; - default: - return false; - } - }else{ - switch (ev->direction) { - case GDK_SCROLL_UP: - _adj.set_value (min (note_lower + 1, 127 - note_range)); - break; - case GDK_SCROLL_DOWN: - _adj.set_value (note_lower - 1.0); - break; - default: - return false; - } - } - - set_note_highlight (_view.y_to_note (ev->y)); - - _adj.value_changed (); - queue_draw (); - return true; + return scroll_handler (ev); } +void +PianoRollHeader::redraw () +{ + queue_draw (); +} void -PianoRollHeader::get_path (int note, double x[], double y[]) +PianoRollHeader::redraw (double x, double y, double w, double h) { - double scroomer_size = _scroomer_size; - double y_pos = floor(_view.note_to_y(note)); - double note_height; - _raw_note_height = floor(_view.note_to_y(note - 1)) - y_pos; - double width = get_width() - 1.0f; - - if (note == 0) { - note_height = floor(_view.contents_height()) - y_pos; - } else { - note_height = _raw_note_height <= 3 ? _raw_note_height : _raw_note_height - 1.f; - } - - x[0] = scroomer_size; - y[0] = y_pos + note_height; - - x[1] = scroomer_size; - y[1] = y_pos; - - x[2] = width; - y[2] = y_pos; - - x[3] = width; - y[3] = y_pos + note_height; - - x[4] = scroomer_size; - y[4] = y_pos + note_height; - return; + queue_draw_area (x, y, w, h); } bool PianoRollHeader::on_expose_event (GdkEventExpose* ev) { - GdkRectangle& rect = ev->area; - int lowest, highest; - Gtkmm2ext::Color bg; - Cairo::RefPtr cr = get_window()->create_cairo_context(); - double x[9]; - double y[9]; - int oct_rel; - int y1 = max(rect.y, 0); - int y2 = min(rect.y + rect.height, (int) floor(_view.contents_height())); - double av_note_height = get_height () / _adj.get_page_size (); - int bc_height, bc_width; + ArdourCanvas::Rect rect (ev->area.x, ev->area.y, ev->area.x + ev->area.width, ev->area.y + ev->area.height); + ArdourCanvas::Rect self (0., 0., get_width(), get_height()); - //Reduce the frequency of Pango layout resizing - //if (int(_old_av_note_height) != int(av_note_height)) { - //Set Pango layout keyboard c's size - _font_descript.set_absolute_size (av_note_height * 0.7 * Pango::SCALE); - _layout->set_font_description(_font_descript); - - //change mode of midnam display - if (av_note_height >= 8.0) { - _mini_map_display = false; - } else { - _mini_map_display = true; - } - - //Set Pango layout midnam size - _font_descript_midnam.set_absolute_size (max(8.0 * 0.7 * Pango::SCALE, (int)av_note_height * 0.7 * Pango::SCALE)); - - _midnam_layout->set_font_description(_font_descript_midnam); - - 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; - } - - /* 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); - //r->set_source_rgb(1, 0,0); - //cr->fill(); - - cr->set_line_width (1.0f); - - Gtkmm2ext::Color white = UIConfiguration::instance().color (X_("piano key white")); - Gtkmm2ext::Color white_highlight = UIConfiguration::instance().color (X_("piano key highlight")); - Gtkmm2ext::Color black = UIConfiguration::instance().color (X_("piano key black")); - Gtkmm2ext::Color black_highlight = UIConfiguration::instance().color (X_("piano key highlight")); - Gtkmm2ext::Color textc = UIConfiguration::instance().color (X_("gtk_foreground")); - - /* draw vertical lines on both sides of the widget */ - cr->set_source_rgb(0.0f, 0.0f, 0.0f); - cr->move_to(0.f, rect.y); - cr->line_to(0.f, rect.y + rect.height); - cr->stroke(); - cr->move_to(get_width(),rect.y); - cr->line_to(get_width(), rect.y + get_height ()); - cr->stroke(); - - // Render the MIDNAM text or its equivalent. First, set up a clip - // region so that the text doesn't spill, regardless of its length. - - cr->save(); - - cr->rectangle (0,0,_scroomer_size, get_height () ); - cr->clip(); - - if (show_scroomer()) { - - /* Draw the actual text */ - - for (int i = lowest; i <= highest; ++i) { - int size_x, size_y; - double y = floor(_view.note_to_y(i)) - 0.5f; - NoteName & note (note_names[i]); - - _midnam_layout->set_text (note.name); - - set_source_rgba(cr, textc); - cr->move_to(2.f, y); - - if (!_mini_map_display) { - _midnam_layout->show_in_cairo_context (cr); - } else { - /* Too small for text, just show a thing rect where the - text would have been. - */ - if (!note.from_midnam) { - set_source_rgba(cr, textc); - } - pango_layout_get_pixel_size (_midnam_layout->gobj (), &size_x, &size_y); - cr->rectangle (2.f, y + (av_note_height * 0.5), size_x, av_note_height * 0.2); - cr->fill (); - } - } - - /* Add a gradient over the text, to act as a sort of "visual - elision". This avoids using text elision with "..." which takes up too - much space. - */ - Gtkmm2ext::Color bg = UIConfiguration::instance().color (X_("gtk_background")); - double r,g,b,a; - Gtkmm2ext::color_to_rgba(bg,r,g,b,a); - double fade_width = 30.; - auto gradient_ptr = Cairo::LinearGradient::create (_scroomer_size - fade_width, 0, _scroomer_size, 0); - gradient_ptr->add_color_stop_rgba (0,r,g,b,0); - gradient_ptr->add_color_stop_rgba (1,r,g,b,1); - cr->set_source (gradient_ptr); - cr->rectangle (_scroomer_size - fade_width, 0, _scroomer_size, get_height () ); - cr->fill(); - } - - /* Now draw the semi-transparent scroomer over the top */ - - render_scroomer(cr); - - /* Done with clip region */ - - cr->restore(); - - /* Now draw black/white rects for each note, following standard piano - layout, but without a setback/offset for the black keys - */ - - 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 = black_highlight; - } else { - bg = black; - } - - /* draw black separators */ - cr->set_source_rgb (0.0f, 0.0f, 0.0f); - get_path (i, x, y); - create_path (cr, x, y, 0, 1); - cr->stroke(); - - get_path (i, x, y); - create_path (cr, x, y, 0, 1); - cr->stroke(); - - get_path (i, x, y); - render_rect (cr, i, x, y, bg); - break; - } - - switch(oct_rel) { - case 0: - case 2: - case 4: - case 5: - case 7: - case 9: - case 11: - if (i == _highlighted_note) { - bg = white_highlight; - } else { - bg = white; - } - get_path (i, x, y); - render_rect (cr, i, x, y, bg); - break; - default: - break; - - } - } - - /* render the C of the key, when key is too small to contain text we - place the C on the midnam scroomer area. - - we render an additional 5 notes below the lowest note displayed - so that the top of the C is shown to maintain visual context - */ - for (int i = lowest - 5; i <= highest; ++i) { - double y = floor(_view.note_to_y(i)) - 0.5f; - double note_height = i == 0? av_note_height : floor(_view.note_to_y(i - 1)) - y; - oct_rel = i % 12; - - if (oct_rel == 0 || (oct_rel == 7 && _adj.get_page_size() <=10) ) { - std::stringstream s; - - int cn = i / 12 - 1; - - if (oct_rel == 0){ - s << "C" << cn; - }else{ - s << "G" << cn; - } - - if (av_note_height > 12.0){ - set_source_rgba(cr, black); - _layout->set_text (s.str()); - cr->move_to(_scroomer_size, ceil(y+1.)); - _layout->show_in_cairo_context (cr); - }else{ - set_source_rgba(cr, textc); - _big_c_layout->set_text (s.str()); - pango_layout_get_pixel_size (_big_c_layout->gobj(), &bc_width, &bc_height); - cr->move_to(_scroomer_size - 18, y - bc_height + av_note_height); - _big_c_layout->show_in_cairo_context (cr); - cr->move_to(_scroomer_size - 18, y + note_height); - cr->line_to(_scroomer_size, y + note_height); - cr->stroke(); - } - } - } + render (self, rect, get_window()->create_cairo_context()); return true; } - -void -PianoRollHeader::instrument_info_change () -{ - have_note_names = false; - - for (int i = 0; i < 128; ++i) { - note_names[i] = get_note_name (i); - - if (note_names[i].from_midnam) { - have_note_names = true; - } - } - - queue_resize (); - - /* need this to get editor to potentially sync all - track header widths if our piano roll header changes - width. - */ - - _view.trackview().stripable()->gui_changed ("visible_tracks", (void *) 0); /* EMIT SIGNAL */ -} - -PianoRollHeader::NoteName -PianoRollHeader::get_note_name (int note) -{ - using namespace MIDI::Name; - std::string name; - std::string note_n; - NoteName rtn; - - MidiTimeAxisView* mtv = dynamic_cast(&_view.trackview()); - - if (mtv) { - string chn = mtv->gui_property (X_("midnam-channel")); - - if (!chn.empty()) { - - int midnam_channel; - - sscanf (chn.c_str(), "%*s %d", &midnam_channel); - midnam_channel--; - - name = mtv->route()->instrument_info ().get_note_name ( - 0, //bank - 0, //program - midnam_channel, //channel - note); //note - } - } - - int oct_rel = note % 12; - switch(oct_rel) { - case 0: - note_n = "C"; - break; - case 1: - note_n = "C♯"; - break; - case 2: - note_n = "D"; - break; - case 3: - note_n = "D♯"; - break; - case 4: - note_n = "E"; - break; - case 5: - note_n = "F"; - break; - case 6: - note_n = "F♯"; - break; - case 7: - note_n = "G"; - break; - case 8: - note_n = "G♯"; - break; - case 9: - note_n = "A"; - break; - case 10: - note_n = "A♯"; - break; - case 11: - note_n = "B"; - break; - default: - break; - } - - std::string new_string = std::string(3 - std::to_string(note).length(), '0') + std::to_string(note); - rtn.name = name.empty()? new_string + " " + note_n : name; - rtn.from_midnam = !name.empty(); - return rtn; -} - bool PianoRollHeader::on_motion_notify_event (GdkEventMotion* ev) { - if (!_scroomer_drag && ev->x < _scroomer_size){ - Gdk::Cursor m_Cursor; - double scroomer_top = max(1.0, (1.0 - ((_adj.get_value()+_adj.get_page_size()) / 127.0)) * get_height () ); - double scroomer_bottom = (1.0 - (_adj.get_value () / 127.0)) * get_height (); - if (ev->y > scroomer_top - 5 && ev->y < scroomer_top + 5){ - m_Cursor = Gdk::Cursor (Gdk::TOP_SIDE); - get_window()->set_cursor(m_Cursor); - _scroomer_state = TOP; - }else if (ev->y > scroomer_bottom - 5 && ev->y < scroomer_bottom + 5){ - m_Cursor = Gdk::Cursor (Gdk::BOTTOM_SIDE); - get_window()->set_cursor(m_Cursor); - _scroomer_state = BOTTOM; - }else { - _scroomer_state = MOVE; - get_window()->set_cursor(); - } - } - - if (_scroomer_drag){ - double pixel2val = 127.0 / get_height(); - double delta = _old_y - ev->y; - double val_at_pointer = (delta * pixel2val); - double real_val_at_pointer = 127.0 - (ev->y * pixel2val); - double note_range = _adj.get_page_size (); - - switch (_scroomer_button_state){ - case MOVE: - _fract += val_at_pointer; - _fract = (_fract + note_range > 127.0)? 127.0 - note_range : _fract; - _fract = max(0.0, _fract); - _adj.set_value (min(_fract, 127.0 - note_range)); - break; - case TOP: - real_val_at_pointer = real_val_at_pointer <= _saved_top_val? _adj.get_value() + _adj.get_page_size() : real_val_at_pointer; - real_val_at_pointer = min(127.0, real_val_at_pointer); - if (_note_height >= UIConfiguration::instance().get_max_note_height()){ - _saved_top_val = min(_adj.get_value() + _adj.get_page_size (), 127.0); - } else { - _saved_top_val = 0.0; - } - //if we are at largest note size & the user is moving down don't do anything - //FIXME we are using a heuristic of 18.5 for max note size, but this changes when track size is small to 19.5? - _view.apply_note_range (_adj.get_value (), real_val_at_pointer, true); - break; - case BOTTOM: - real_val_at_pointer = max(0.0, real_val_at_pointer); - real_val_at_pointer = real_val_at_pointer >= _saved_bottom_val? _adj.get_value() : real_val_at_pointer; - if (_note_height >= UIConfiguration::instance().get_max_note_height()){ - _saved_bottom_val = _adj.get_value(); - } else { - _saved_bottom_val = 127.0; - } - _view.apply_note_range (real_val_at_pointer, _adj.get_value () + _adj.get_page_size (), true); - break; - default: - break; - } - }else{ - 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); - } - } - } - } - } - _adj.value_changed (); - queue_draw (); - _old_y = ev->y; - //win->process_updates(false); - - return true; + return motion_handler (ev); } bool PianoRollHeader::on_button_press_event (GdkEventButton* ev) { - _scroomer_button_state = _scroomer_state; - - if (ev->button == 1 && ev->x <= _scroomer_size){ - - if (ev->type == GDK_2BUTTON_PRESS) { - MidiTimeAxisView* mtv = dynamic_cast(&_view.trackview()); - if (mtv) { - mtv->set_visibility_note_range (MidiStreamView::ContentsRange, false); - } - return true; - } - - _scroomer_drag = true; - _old_y = ev->y; - _fract = _adj.get_value(); - _fract_top = _adj.get_value() + _adj.get_page_size(); - return true; - - } else { - int note = _view.y_to_note(ev->y); - bool tertiary = Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier); - bool primary = Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier); - - if (ev->button == 1 && ev->type == GDK_2BUTTON_PRESS) { - if (primary) { - _adj.set_value (0.0); - _adj.set_page_size (127.0); - _adj.value_changed (); - queue_draw (); - } - return true; - } else 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 (primary && (ev->button == 1 || ev->button == 2)) { - ToggleNoteSelection (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; + return button_press_handler (ev); } + bool PianoRollHeader::on_button_release_event (GdkEventButton* ev) { - if (_scroomer_drag){ - _scroomer_drag = false; - } - 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); - } + return button_release_handler (ev); } bool PianoRollHeader::on_enter_notify_event (GdkEventCrossing* ev) { - set_note_highlight (_view.y_to_note (ev->y)); - entered = true; - queue_draw (); - return true; + return enter_handler (ev); } bool -PianoRollHeader::on_leave_notify_event (GdkEventCrossing*) +PianoRollHeader::on_leave_notify_event (GdkEventCrossing* ev) { - if (!_scroomer_drag){ - get_window()->set_cursor(); - } - 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; - entered = false; - queue_draw (); - - 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; - - lowest = max((int) _view.lowest_note(), lowest - 1); - highest = min((int) _view.highest_note(), highest + 2); - - 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); - } - queue_draw (); -} - -bool -PianoRollHeader::show_scroomer () const -{ - Editing::NoteNameDisplay nnd = UIConfiguration::instance().get_note_name_display(); - - if (nnd == Editing::Never) { - return false; - } - - switch (editor().current_mouse_mode()) { - case Editing::MouseDraw: - case Editing::MouseContent: - if (nnd == Editing::WithMIDNAM) { - return have_note_names; - } else { - return true; - } - default: - break; - } - return false; + return leave_handler (ev); } void @@ -850,57 +136,20 @@ PianoRollHeader::on_size_request (Gtk::Requisition* r) r->width = _scroomer_size + 20.f; } -void -PianoRollHeader::send_note_on (uint8_t note) +double +PianoRollHeader::height() const { - std::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_user_immediate_event (Evoral::MIDI_EVENT, 3, _event); - } + return get_height(); } -void -PianoRollHeader::send_note_off (uint8_t note) +double +PianoRollHeader::width() const { - std::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_user_immediate_event (Evoral::MIDI_EVENT, 3, _event); - } + return get_width(); } -void -PianoRollHeader::reset_clicked_note (uint8_t note, bool invalidate) +Glib::RefPtr +PianoRollHeader::cursor_window() { - _active_notes[note] = false; - _clicked_note = NO_MIDI_NOTE; - send_note_off (note); - if (invalidate) { - invalidate_note_range (note, note); - } + return get_window (); } - -PublicEditor& -PianoRollHeader::editor () const -{ - return _view.trackview ().editor (); -} - -void -PianoRollHeader::set_min_page_size(double page_size) -{ - _min_page_size = page_size; -}; diff --git a/gtk2_ardour/piano_roll_header.h b/gtk2_ardour/piano_roll_header.h index a6c2aa4cd0..1a405c3485 100644 --- a/gtk2_ardour/piano_roll_header.h +++ b/gtk2_ardour/piano_roll_header.h @@ -19,21 +19,13 @@ #pragma once -#include "ardour/types.h" - #include -namespace ARDOUR { - class MidiTrack; -} +#include "prh_base.h" -class MidiTimeAxisView; -class MidiStreamView; -class PublicEditor; - -class PianoRollHeader : public Gtk::DrawingArea { -public: - PianoRollHeader(MidiStreamView&); +class PianoRollHeader : public Gtk::DrawingArea, public PianoRollHeaderBase { + public: + PianoRollHeader(MidiViewBackground&); bool on_expose_event (GdkEventExpose*); bool on_motion_notify_event (GdkEventMotion*); @@ -44,87 +36,16 @@ public: bool on_leave_notify_event (GdkEventCrossing*); void on_size_request(Gtk::Requisition*); - - void instrument_info_change (); - - void note_range_changed(); - void set_note_highlight (uint8_t note); - - sigc::signal SetNoteSelection; - sigc::signal AddNoteSelection; - sigc::signal ToggleNoteSelection; - sigc::signal ExtendNoteSelection; - -private: - struct NoteName { - std::string name; - bool from_midnam; - }; - NoteName note_names[128]; - bool have_note_names; - void set_min_page_size(double page_size); - void render_scroomer(Cairo::RefPtr); - NoteName get_note_name (int note); - - Gtk::Adjustment& _adj; - - PianoRollHeader(const PianoRollHeader&); - - enum ItemType { - BLACK_SEPARATOR, - BLACK_MIDDLE_SEPARATOR, - BLACK, - WHITE_SEPARATOR, - WHITE_RECT, - WHITE_CF, - WHITE_EB, - WHITE_DGA - }; - - void invalidate_note_range(int lowest, int highest); - - void get_path(int note, double x[], double y[]); - - void send_note_on(uint8_t note); - void send_note_off(uint8_t note); - - void reset_clicked_note(uint8_t, bool invalidate = true); - - MidiStreamView& _view; - - uint8_t _event[3]; - - Glib::RefPtr _layout; - Glib::RefPtr _big_c_layout; - Glib::RefPtr _midnam_layout; - - Pango::FontDescription _font_descript; - Pango::FontDescription _font_descript_big_c; - Pango::FontDescription _font_descript_midnam; - bool _active_notes[128]; - uint8_t _highlighted_note; - uint8_t _clicked_note; - double _grab_y; - bool _dragging; - double _scroomer_size; - bool _scroomer_drag; - double _old_y; - double _fract; - double _fract_top; - double _raw_note_height; - double _min_page_size; - enum scr_pos {TOP, BOTTOM, MOVE, NONE}; - scr_pos _scroomer_state; - scr_pos _scroomer_button_state; - double _saved_top_val; - double _saved_bottom_val; - bool _mini_map_display; - bool entered; - - double _note_height; - double _old_av_note_height; - - PublicEditor& editor() const; - bool show_scroomer () const; + void redraw (); + void redraw (double x, double y, double w, double h); + double height() const; + double width() const; + double event_y_to_y (double evy) const { return evy; } + void draw_transform (double& x, double& y) const {} + void event_transform (double& x, double& y) const {} + void _queue_resize () { queue_resize(); } + void do_grab() { add_modal_grab(); } + void do_ungrab() { remove_modal_grab(); } + Glib::RefPtr cursor_window(); }; diff --git a/gtk2_ardour/prh.cc b/gtk2_ardour/prh.cc index d2e5b4d2b0..de1678bff2 100644 --- a/gtk2_ardour/prh.cc +++ b/gtk2_ardour/prh.cc @@ -52,49 +52,31 @@ namespace ArdourCanvas { PianoRollHeader::PianoRollHeader (Item* parent, MidiViewBackground& bg) : Rectangle (parent) - , _midi_context (bg) - , _adj (_midi_context.note_range_adjustment) - , _view (nullptr) - , _font_descript ("Sans Bold") - , _font_descript_big_c ("Sans") - , _font_descript_midnam ("Sans") - , _highlighted_note (NO_MIDI_NOTE) - , _clicked_note (NO_MIDI_NOTE) - , _dragging (false) - , _scroomer_size (63.f) - , _scroomer_drag (false) - , _old_y (0.0) - , _fract (0.0) - , _scroomer_state (NONE) - , _scroomer_button_state (NONE) - , _saved_top_val (0.0) - , _saved_bottom_val (127.0) - , _mini_map_display (false) - , entered (false) - , have_note_names (false) + , PianoRollHeaderBase (bg) { - Glib::RefPtr context = _canvas->get_pango_context(); + Event.connect (sigc::mem_fun (*this, &PianoRollHeader::event_handler)); - _layout = Pango::Layout::create (context); - _big_c_layout = Pango::Layout::create (context); - _font_descript_big_c.set_absolute_size (10.0 * Pango::SCALE); - _big_c_layout->set_font_description(_font_descript_big_c); - _midnam_layout = Pango::Layout::create (context); - - for (int i = 0; i < 128; ++i) { - _active_notes[i] = false; - } - - resize (); - bg.HeightChanged.connect (height_connection, MISSING_INVALIDATOR, std::bind (&PianoRollHeader::resize, this), gui_context()); + alloc_layouts (_canvas->get_pango_context()); /* draw vertical lines on both sides of the rectangle */ set_fill (false); - set_fill (true); set_outline_color (0x000000ff); /* XXX theme me */ set_outline_what (Rectangle::What (Rectangle::LEFT|Rectangle::RIGHT)); - Event.connect (sigc::mem_fun (*this, &PianoRollHeader::event_handler)); + _midi_context.HeightChanged.connect (height_connection, MISSING_INVALIDATOR, std::bind (&PianoRollHeader::resize, this), gui_context()); + resize (); +} + +void +PianoRollHeader::redraw () +{ + ArdourCanvas::Rectangle::redraw (); +} + +void +PianoRollHeader::redraw (double x, double y, double w, double h) +{ + dynamic_cast(_canvas)->queue_draw_area (x, y, w, h); } void @@ -105,15 +87,6 @@ PianoRollHeader::resize () set (Rect (0., 0., w, h)); } -void -PianoRollHeader::set_view (MidiView* v) -{ - _view = v; - if (_view) { - _view->midi_context().NoteRangeChanged.connect (sigc::mem_fun (*this, &PianoRollHeader::note_range_changed)); - } -} - void PianoRollHeader::size_request (double& w, double& h) const { @@ -131,852 +104,111 @@ PianoRollHeader::size_request (double& w, double& h) const bool PianoRollHeader::event_handler (GdkEvent* ev) { + GdkEvent* copy = gdk_event_copy (ev); + Duple evd; + /* Remember that ev uses canvas coordinates, not item */ switch (ev->type) { case GDK_BUTTON_PRESS: case GDK_2BUTTON_PRESS: case GDK_3BUTTON_PRESS: - return button_press_handler (&ev->button); + evd = (canvas_to_item (Duple (ev->button.x, ev->button.y))); + copy->button.x = evd.x; + copy->button.y = evd.y; + return button_press_handler (©->button); case GDK_BUTTON_RELEASE: - return button_release_handler (&ev->button); + evd = (canvas_to_item (Duple (ev->button.x, ev->button.y))); + copy->button.x = evd.x; + copy->button.y = evd.y; + return button_release_handler (©->button); case GDK_ENTER_NOTIFY: - return enter_handler (&ev->crossing); + evd = (canvas_to_item (Duple (ev->crossing.x, ev->crossing.y))); + copy->crossing.x = evd.x; + copy->crossing.y = evd.y; + return enter_handler (©->crossing); case GDK_LEAVE_NOTIFY: - return leave_handler (&ev->crossing); + evd = (canvas_to_item (Duple (ev->crossing.x, ev->crossing.y))); + copy->crossing.x = evd.x; + copy->crossing.y = evd.y; + return leave_handler (©->crossing); case GDK_SCROLL: - return scroll_handler (&ev->scroll); + evd = (canvas_to_item (Duple (ev->scroll.x, ev->scroll.y))); + copy->scroll.x = evd.x; + copy->scroll.y = evd.y; + return scroll_handler (©->scroll); case GDK_MOTION_NOTIFY: - return motion_handler (&ev->motion); + evd = (canvas_to_item (Duple (ev->motion.x, ev->motion.y))); + copy->motion.x = evd.x; + copy->motion.y = evd.y; + return motion_handler (©->motion); default: break; } + gdk_event_free (copy); + return false; } -inline void -create_path (Cairo::RefPtr cr, double x[], double y[], int start, int stop) +double +PianoRollHeader::height() const { - cr->move_to (x[start], y[start]); - - for (int i = start + 1; i <= stop; ++i) { - cr->line_to (x[i], y[i]); - } + return get().height(); } -inline void -render_rect(Cairo::RefPtr cr, int note, double x[], double y[], Gtkmm2ext::Color& bg) +double +PianoRollHeader::width() const { - set_source_rgba (cr, bg); - create_path (cr, x, y, 0, 4); - cr->fill (); + return get().width(); } void -PianoRollHeader::render_scroomer (Cairo::RefPtr cr) const +PianoRollHeader::render (ArdourCanvas::Rect const & area, Cairo::RefPtr context) const { - double scroomer_top = max (1.0, (1.0 - ((_adj.get_value()+_adj.get_page_size()) / 127.0)) * get().height () ); - double scroomer_bottom = (1.0 - (_adj.get_value () / 127.0)) * get().height (); - - Gtkmm2ext::Color c = UIConfiguration::instance().color_mod (X_("scroomer"), X_("scroomer alpha")); - Gtkmm2ext::Color save_color (c); - - if (entered) { - c = HSV (c).lighter (0.25).color(); - } - - set_source_rgba (cr, c); - cr->move_to (1.f, scroomer_top); - cr->line_to (_scroomer_size - 1.f, scroomer_top); - cr->line_to (_scroomer_size - 1.f, scroomer_bottom); - cr->line_to (1.f, scroomer_bottom); - cr->line_to (1.f, scroomer_top); - cr->fill(); - - if (entered) { - cr->save (); - c = HSV (save_color).lighter (0.9).color(); - set_source_rgba (cr, c); - cr->set_line_width (4.); - cr->move_to (1.f, scroomer_top + 2.); - cr->line_to (_scroomer_size - 1.f, scroomer_top + 2.); - cr->stroke (); - cr->line_to (_scroomer_size - 1.f, scroomer_bottom - 2.); - cr->line_to (2.f, scroomer_bottom - 2.); - cr->stroke (); - cr->restore (); - } -} - -bool -PianoRollHeader::scroll_handler (GdkEventScroll* ev) -{ - if (!_view) { - return false; - } - - int note_range = _adj.get_page_size (); - int note_lower = _adj.get_value (); - - if(ev->state == GDK_SHIFT_MASK){ - switch (ev->direction) { - case GDK_SCROLL_UP: //ZOOM IN - _view->apply_note_range (min(note_lower + 1, 127), max(note_lower + note_range - 1,0), true); - break; - case GDK_SCROLL_DOWN: //ZOOM OUT - _view->apply_note_range (max(note_lower - 1,0), min(note_lower + note_range + 1, 127), true); - break; - default: - return false; - } - }else{ - switch (ev->direction) { - case GDK_SCROLL_UP: - _adj.set_value (min (note_lower + 1, 127 - note_range)); - break; - case GDK_SCROLL_DOWN: - _adj.set_value (note_lower - 1.0); - break; - default: - return false; - } - } - - Duple evd (canvas_to_item (Duple (ev->x, ev->y))); - set_note_highlight (_view->midi_context().y_to_note (evd.y)); - - _adj.value_changed (); - redraw (); - return true; -} - - -void -PianoRollHeader::get_path (int note, double x[], double y[]) const -{ - double y_pos = floor(_midi_context.note_to_y(note)); - double note_height; - double width = get().width() - 1.0f; - - if (note == 0) { - note_height = floor(_midi_context.contents_height()) - y_pos; - } else { - note_height = _midi_context.note_height() <= 3 ? _midi_context.note_height() : _midi_context.note_height() - 1.f; - } - - x[0] = _scroomer_size; - y[0] = y_pos + note_height; - - x[1] = _scroomer_size; - y[1] = y_pos; - - x[2] = width; - y[2] = y_pos; - - x[3] = width; - y[3] = y_pos + note_height; - - x[4] = x[0]; - y[4] = y[0]; -} - -void -PianoRollHeader::render (ArdourCanvas::Rect const & area, Cairo::RefPtr cr) const -{ - int lowest, highest; - Gtkmm2ext::Color bg; - double x[9]; - double y[9]; - int oct_rel; - - Rectangle::render (area, cr); - - /* Setup a cairo translation so that all drawing can be done using item - * coordinate - */ - - Duple origin (item_to_window (Duple (0., 0.))); - - cr->save (); - cr->translate (origin.x, origin.y); - Rect self (get()); + Rectangle::render (area, context); + PianoRollHeaderBase::render (self, area, context); +} - double y1 = max (self.y0, 0.); - double y2 = min (self.y1, (ArdourCanvas::Coord) floor(_midi_context.contents_height())); - double av_note_height = _midi_context.note_height(); - int bc_height, bc_width; - - //Reduce the frequency of Pango layout resizing - //if (int(_old_av_note_height) != int(av_note_height)) { - //Set Pango layout keyboard c's size - _font_descript.set_absolute_size (av_note_height * 0.7 * Pango::SCALE); - _layout->set_font_description(_font_descript); - - //change mode of midnam display - if (av_note_height >= 8.0) { - _mini_map_display = false; - } else { - _mini_map_display = true; - } - - //Set Pango layout midnam size - _font_descript_midnam.set_absolute_size (max(8.0 * 0.7 * Pango::SCALE, (int)av_note_height * 0.7 * Pango::SCALE)); - - _midnam_layout->set_font_description(_font_descript_midnam); - - lowest = max(_midi_context.lowest_note(), _midi_context.y_to_note(y2)); - highest = min(_midi_context.highest_note(), _midi_context.y_to_note(y1)); - - if (lowest > 127) { - lowest = 0; - } - - /* 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); - //r->set_source_rgb(1, 0,0); - //cr->fill(); - - cr->set_line_width (1.0f); - - Gtkmm2ext::Color white = UIConfiguration::instance().color (X_("piano key white")); - Gtkmm2ext::Color white_highlight = UIConfiguration::instance().color (X_("piano key highlight")); - Gtkmm2ext::Color black = UIConfiguration::instance().color (X_("piano key black")); - Gtkmm2ext::Color black_highlight = UIConfiguration::instance().color (X_("piano key highlight")); - Gtkmm2ext::Color textc = UIConfiguration::instance().color (X_("gtk_foreground")); - - - // Render the MIDNAM text or its equivalent. First, set up a clip - // region so that the text doesn't spill, regardless of its length. - - cr->save(); - - cr->rectangle (0,0,_scroomer_size, get().height () ); - cr->clip(); - - if (show_scroomer()) { - - /* Draw the actual text */ - - for (int i = lowest; i <= highest; ++i) { - int size_x, size_y; - double y = floor(_midi_context.note_to_y(i)) - 0.5f; - NoteName const & note (note_names[i]); - - _midnam_layout->set_text (note.name); - - set_source_rgba (cr, textc); - cr->move_to (2.f, y); - - if (!_mini_map_display) { - _midnam_layout->show_in_cairo_context (cr); - } else { - /* Too small for text, just show a thing rect where the - text would have been. - */ - if (!note.from_midnam) { - set_source_rgba(cr, textc); - } - pango_layout_get_pixel_size (_midnam_layout->gobj (), &size_x, &size_y); - cr->rectangle (2.f, y + (av_note_height * 0.5), size_x, av_note_height * 0.2); - cr->fill (); - } - } - - /* Add a gradient over the text, to act as a sort of "visual - elision". This avoids using text elision with "..." which takes up too - much space. - */ - Gtkmm2ext::Color bg = UIConfiguration::instance().color (X_("gtk_background")); - double r,g,b,a; - Gtkmm2ext::color_to_rgba(bg,r,g,b,a); - double fade_width = 30.; - auto gradient_ptr = Cairo::LinearGradient::create (_scroomer_size - fade_width, 0, _scroomer_size, 0); - gradient_ptr->add_color_stop_rgba (0,r,g,b,0); - gradient_ptr->add_color_stop_rgba (1,r,g,b,1); - cr->set_source (gradient_ptr); - cr->rectangle (_scroomer_size - fade_width, 0, _scroomer_size, get().height () ); - cr->fill(); - } - - /* Now draw the semi-transparent scroomer over the top */ - - render_scroomer (cr); - - /* Done with clip region */ - - cr->restore(); - - /* Now draw black/white rects for each note, following standard piano - layout, but without a setback/offset for the black keys - */ - - 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 = black_highlight; - } else { - bg = black; - } - - /* draw black separators */ - cr->set_source_rgb (0.0f, 0.0f, 0.0f); - get_path (i, x, y); - create_path (cr, x, y, 0, 1); - cr->stroke(); - - get_path (i, x, y); - create_path (cr, x, y, 0, 1); - cr->stroke(); - - get_path (i, x, y); - render_rect (cr, i, x, y, bg); - break; - } - - switch(oct_rel) { - case 0: - case 2: - case 4: - case 5: - case 7: - case 9: - case 11: - if (i == _highlighted_note) { - bg = white_highlight; - } else { - bg = white; - } - get_path (i, x, y); - render_rect (cr, i, x, y, bg); - break; - default: - break; - - } - } - - /* render the C of the key, when key is too small to contain text we - place the C on the midnam scroomer area. - - we render an additional 5 notes below the lowest note displayed - so that the top of the C is shown to maintain visual context - */ - for (int i = lowest - 5; i <= highest; ++i) { - double y = floor(_midi_context.note_to_y(i)) - 0.5f; - double note_height = i == 0? av_note_height : floor(_midi_context.note_to_y(i - 1)) - y; - oct_rel = i % 12; - - if (oct_rel == 0 || (oct_rel == 7 && _adj.get_page_size() <=10)) { - std::stringstream s; - - int cn = i / 12 - 1; - - if (oct_rel == 0){ - s << 'C' << cn; - } else { - s << 'G' << cn; - } - - if (av_note_height > 12.0){ - set_source_rgba(cr, black); - _layout->set_text (s.str()); - cr->move_to(_scroomer_size, ceil(y+1.)); - _layout->show_in_cairo_context (cr); - }else{ - set_source_rgba(cr, textc); - _big_c_layout->set_text (s.str()); - pango_layout_get_pixel_size (_big_c_layout->gobj(), &bc_width, &bc_height); - cr->move_to(_scroomer_size - 18, y - bc_height + av_note_height); - _big_c_layout->show_in_cairo_context (cr); - cr->move_to(_scroomer_size - 18, y + note_height); - cr->line_to(_scroomer_size, y + note_height); - cr->stroke(); - } - } - } - - /* Done with translation for item->window */ - cr->restore (); +double +PianoRollHeader::event_y_to_y (double evy) const +{ + Duple evd (canvas_to_item (Duple (0., evy))); + return evd.y; } void -PianoRollHeader::instrument_info_change () +PianoRollHeader::draw_transform (double& x, double& y) const { - have_note_names = false; - - for (int i = 0; i < 128; ++i) { - note_names[i] = get_note_name (i); - - if (note_names[i].from_midnam) { - have_note_names = true; - } - } - - queue_resize (); - - /* need this to get editor to potentially sync all - track header widths if our piano roll header changes - width. - */ - - if (_view) { - _view->midi_track()->gui_changed ("visible_tracks", (void *) 0); /* EMIT SIGNAL */ - } - -} - -PianoRollHeader::NoteName -PianoRollHeader::get_note_name (int note) -{ - using namespace MIDI::Name; - std::string name; - std::string note_n; - NoteName rtn; - - ARDOUR::InstrumentInfo* ii = _midi_context.instrument_info(); - - if (!ii) { - return rtn; - } - - int midnam_channel = _midi_context.get_preferred_midi_channel (); - - name = ii->get_note_name ( - 0, //bank - 0, //program - midnam_channel, //channel - note); //note - - int oct_rel = note % 12; - switch (oct_rel) { - case 0: - note_n = "C"; - break; - case 1: - note_n = "C♯"; - break; - case 2: - note_n = "D"; - break; - case 3: - note_n = "D♯"; - break; - case 4: - note_n = "E"; - break; - case 5: - note_n = "F"; - break; - case 6: - note_n = "F♯"; - break; - case 7: - note_n = "G"; - break; - case 8: - note_n = "G♯"; - break; - case 9: - note_n = "A"; - break; - case 10: - note_n = "A♯"; - break; - case 11: - note_n = "B"; - break; - default: - break; - } - - std::string new_string = std::string(3 - std::to_string(note).length(), '0') + std::to_string(note); - rtn.name = name.empty()? new_string + " " + note_n : name; - rtn.from_midnam = !name.empty(); - return rtn; -} - -bool -PianoRollHeader::motion_handler (GdkEventMotion* ev) -{ - if (!_view) { - return false; - } - - Duple evd (canvas_to_item (Duple (ev->x, ev->y))); - - if (!_scroomer_drag && ev->x < _scroomer_size){ - - double scroomer_top = max (1.0, (1.0 - ((_adj.get_value()+_adj.get_page_size()) / 127.0)) * get().height()); - double scroomer_bottom = (1.0 - (_adj.get_value () / 127.0)) * get().height(); - double edge = 5. * UIConfiguration::instance().get_ui_scale(); - - if (evd.y > scroomer_top - 5 && evd.y < scroomer_top + edge){ - if (_scroomer_state != TOP) { - _view->editing_context().set_canvas_cursor (_view->editing_context().cursors()->resize_top); - _scroomer_state = TOP; - } - } else if (evd.y > scroomer_bottom - edge && evd.y < scroomer_bottom + edge){ - if (_scroomer_state != BOTTOM) { - _view->editing_context().set_canvas_cursor (_view->editing_context().cursors()->resize_bottom); - _scroomer_state = BOTTOM; - } - } else { - if (_scroomer_state != MOVE) { - _view->editing_context().set_canvas_cursor (_view->editing_context().cursors()->grabber); - _scroomer_state = MOVE; - } - } - } - - if (_scroomer_drag){ - - double pixel2val = 127.0 / get().height(); - double delta = _old_y - evd.y; - double val_at_pointer = (delta * pixel2val); - double real_val_at_pointer = 127.0 - (evd.y * pixel2val); - double note_range = _adj.get_page_size (); - - switch (_scroomer_button_state){ - case MOVE: - _fract += val_at_pointer; - _fract = (_fract + note_range > 127.0)? 127.0 - note_range : _fract; - _fract = max(0.0, _fract); - _adj.set_value (min(_fract, 127.0 - note_range)); - break; - case TOP: - real_val_at_pointer = real_val_at_pointer <= _saved_top_val? _adj.get_value() + _adj.get_page_size() : real_val_at_pointer; - real_val_at_pointer = min(127.0, real_val_at_pointer); - if (_midi_context.note_height() >= UIConfiguration::instance().get_max_note_height()){ - _saved_top_val = min(_adj.get_value() + _adj.get_page_size (), 127.0); - } else { - _saved_top_val = 0.0; - } - //if we are at largest note size & the user is moving down don't do anything - //FIXME we are using a heuristic of 18.5 for max note size, but this changes when track size is small to 19.5? - _midi_context.apply_note_range (_adj.get_value (), real_val_at_pointer, true); - break; - case BOTTOM: - real_val_at_pointer = max(0.0, real_val_at_pointer); - real_val_at_pointer = real_val_at_pointer >= _saved_bottom_val? _adj.get_value() : real_val_at_pointer; - if (_midi_context.note_height() >= UIConfiguration::instance().get_max_note_height()){ - _saved_bottom_val = _adj.get_value(); - } else { - _saved_bottom_val = 127.0; - } - _midi_context.apply_note_range (real_val_at_pointer, _adj.get_value () + _adj.get_page_size (), true); - break; - default: - break; - } - - } else { - - int note = _midi_context.y_to_note(evd.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); - } - } - } - } - } - - redraw (); - _old_y = evd.y; - //win->process_updates(false); - - return true; -} - -bool -PianoRollHeader::button_press_handler (GdkEventButton* ev) -{ - if (!_view) { - return false; - } - - /* Convert canvas-coordinates to item coordinates */ - Duple evd (canvas_to_item (Duple (ev->x, ev->y))); - - _scroomer_button_state = _scroomer_state; - - if (ev->button == 1 && ev->x <= _scroomer_size){ - - if (ev->type == GDK_2BUTTON_PRESS) { - _view->set_visibility_note_range (MidiStreamView::ContentsRange, false); - return true; - } - - _scroomer_drag = true; - _old_y = evd.y; - _fract = _adj.get_value(); - _fract_top = _adj.get_value() + _adj.get_page_size(); - return true; - - } else { - int note = _midi_context.y_to_note(evd.y); - bool tertiary = Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier); - bool primary = Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier); - - if (ev->button == 1 && ev->type == GDK_2BUTTON_PRESS) { - if (primary) { - _adj.set_value (0.0); - _adj.set_page_size (127.0); - _adj.value_changed (); - redraw (); - return false; - } - return false; - } else 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 (primary && (ev->button == 1 || ev->button == 2)) { - ToggleNoteSelection (note); // EMIT SIGNAL - return true; - } else if (ev->button == 1 && note >= 0 && note < 128) { - 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::button_release_handler (GdkEventButton* ev) -{ - Duple evd (canvas_to_item (Duple (ev->x, ev->y))); - - _scroomer_drag = false; - - int note = _midi_context.y_to_note(evd.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) { - ungrab (); - - if (note == _clicked_note) { - reset_clicked_note (note); - } - } - } - - _dragging = false; - return true; + Duple d (x, y); + d = item_to_window (d); + x = d.x; + y = d.y; } void -PianoRollHeader::set_note_highlight (uint8_t note) +PianoRollHeader::event_transform (double& x, double& y) const { - 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); - } + Duple d (x, y); + d = canvas_to_item (d); + x = d.x; + y = d.y; } -bool -PianoRollHeader::enter_handler (GdkEventCrossing* ev) +Glib::RefPtr +PianoRollHeader::cursor_window() { - Duple evd (canvas_to_item (Duple (ev->x, ev->y))); - set_note_highlight (_midi_context.y_to_note (evd.y)); - entered = true; - redraw (); - return true; + ArdourCanvas::GtkCanvas* gc (_midi_context.editing_context().get_canvas()); + assert (gc); + return gc->get_window (); } -bool -PianoRollHeader::leave_handler (GdkEventCrossing*) -{ - if (!_scroomer_drag){ - if (_view) { - /* XXX we used to pop the cursor stack here */ - } - } - 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; - entered = false; - redraw (); - - return true; -} - -void -PianoRollHeader::note_range_changed () -{ - redraw (); -} - -void -PianoRollHeader::invalidate_note_range (int lowest, int highest) -{ - lowest = max((int) _midi_context.lowest_note(), lowest - 1); - highest = min((int) _midi_context.highest_note(), highest + 2); - - double y = _midi_context.note_to_y (highest); - double height = _midi_context.note_to_y (lowest - 1) - y; - - dynamic_cast(_canvas)->queue_draw_area (0., floor (y), get().width(), floor (height)); -} - -bool -PianoRollHeader::show_scroomer () const -{ - if (!_view) { - return false; - } - - Editing::NoteNameDisplay nnd = UIConfiguration::instance().get_note_name_display(); - - if (nnd == Editing::Never) { - return false; - } - - switch (_view->editing_context().current_mouse_mode()) { - case Editing::MouseDraw: - case Editing::MouseContent: - if (nnd == Editing::WithMIDNAM) { - return have_note_names; - } else { - return true; - } - default: - break; - } - return false; -} - -void -PianoRollHeader::send_note_on (uint8_t note) -{ - if (!_view) { - return; - } - - std::shared_ptr track = _view->midi_track (); - - //cerr << "note on: " << (int) note << endl; - - if (track) { - _event[0] = (MIDI_CMD_NOTE_ON | _midi_context.get_preferred_midi_channel ()); - _event[1] = note; - _event[2] = 100; - - track->write_user_immediate_event (Evoral::MIDI_EVENT, 3, _event); - } -} - -void -PianoRollHeader::send_note_off (uint8_t note) -{ - if (!_view) { - return; - } - - std::shared_ptr track = _view->midi_track (); - - if (track) { - _event[0] = (MIDI_CMD_NOTE_OFF | _midi_context.get_preferred_midi_channel ()); - _event[1] = note; - _event[2] = 100; - - track->write_user_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); - } -} - -void -PianoRollHeader::set_min_page_size(double page_size) -{ - _min_page_size = page_size; -}; - -} +} // namespace diff --git a/gtk2_ardour/prh.h b/gtk2_ardour/prh.h index ad349092c5..2df84a3f02 100644 --- a/gtk2_ardour/prh.h +++ b/gtk2_ardour/prh.h @@ -19,116 +19,36 @@ #pragma once -#include -#include - -#include "ardour/types.h" - #include "canvas/rectangle.h" -#include - -namespace ARDOUR { - class MidiTrack; -} - -class MidiView; -class MidiViewBackground; -class EditingContext; +#include "prh_base.h" namespace ArdourCanvas { -class PianoRollHeader : public ArdourCanvas::Rectangle { -public: +class PianoRollHeader : public ArdourCanvas::Rectangle, public PianoRollHeaderBase { + public: PianoRollHeader (ArdourCanvas::Item* parent, MidiViewBackground&); void size_request (double& w, double& h) const; + void resize (); + void redraw (); + void redraw (double x, double y, double w, double h); + double height() const; + double width() const; + double event_y_to_y (double evy) const; + void draw_transform (double& x, double& y) const; + void event_transform (double& x, double& y) const; + void _queue_resize () { queue_resize(); } + void do_grab() { ArdourCanvas::Rectangle::grab(); } + void do_ungrab() { ArdourCanvas::Rectangle::ungrab(); } + Glib::RefPtr cursor_window(); + void render (ArdourCanvas::Rect const & area, Cairo::RefPtr) const; - void instrument_info_change (); - void note_range_changed(); - void set_note_highlight (uint8_t note); - - sigc::signal SetNoteSelection; - sigc::signal AddNoteSelection; - sigc::signal ToggleNoteSelection; - sigc::signal ExtendNoteSelection; - - void set_view (MidiView*); - -private: - MidiViewBackground& _midi_context; - Gtk::Adjustment& _adj; - MidiView* _view; - - uint8_t _event[3]; - - mutable Glib::RefPtr _layout; - mutable Glib::RefPtr _big_c_layout; - mutable Glib::RefPtr _midnam_layout; - mutable Pango::FontDescription _font_descript; - Pango::FontDescription _font_descript_big_c; - mutable Pango::FontDescription _font_descript_midnam; - bool _active_notes[128]; - uint8_t _highlighted_note; - uint8_t _clicked_note; - double _grab_y; - bool _dragging; - mutable double _scroomer_size; - bool _scroomer_drag; - double _old_y; - double _fract; - double _fract_top; - double _min_page_size; - enum scr_pos {TOP, BOTTOM, MOVE, NONE}; - scr_pos _scroomer_state; - scr_pos _scroomer_button_state; - double _saved_top_val; - double _saved_bottom_val; - mutable bool _mini_map_display; - bool entered; - - // void on_size_request(Gtk::Requisition*); - - struct NoteName { - std::string name; - bool from_midnam; - }; - NoteName note_names[128]; - bool have_note_names; + private: PBD::ScopedConnection height_connection; - - void set_min_page_size (double page_size); - void render_scroomer (Cairo::RefPtr) const; - NoteName get_note_name (int note); - bool event_handler (GdkEvent*); - bool motion_handler (GdkEventMotion*); - bool button_press_handler (GdkEventButton*); - bool button_release_handler (GdkEventButton*); - bool scroll_handler (GdkEventScroll*); - bool enter_handler (GdkEventCrossing*); - bool leave_handler (GdkEventCrossing*); - - enum ItemType { - BLACK_SEPARATOR, - BLACK_MIDDLE_SEPARATOR, - BLACK, - WHITE_SEPARATOR, - WHITE_RECT, - WHITE_CF, - WHITE_EB, - WHITE_DGA - }; - - void invalidate_note_range (int lowest, int highest); - void get_path (int note, double x[], double y[]) const; - void send_note_on (uint8_t note); - void send_note_off (uint8_t note); - void reset_clicked_note (uint8_t, bool invalidate = true); - bool show_scroomer () const; - void resize (); }; } diff --git a/gtk2_ardour/prh_base.cc b/gtk2_ardour/prh_base.cc new file mode 100644 index 0000000000..e66d1d8c9b --- /dev/null +++ b/gtk2_ardour/prh_base.cc @@ -0,0 +1,880 @@ +/* + * 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 + +#include "evoral/midi_events.h" + +#include "canvas/canvas.h" + +#include "ardour/instrument_info.h" +#include "ardour/midi_track.h" +#include "ardour/parameter_descriptor.h" + +#include "gtkmm2ext/colors.h" +#include "gtkmm2ext/keyboard.h" +#include "gtkmm2ext/rgb_macros.h" + +#include "midi++/midnam_patch.h" + +#include "editing.h" +#include "gui_thread.h" +#include "midi_view.h" +#include "midi_view_background.h" +#include "mouse_cursors.h" +#include "prh_base.h" +#include "editing_context.h" +#include "ui_config.h" + +#include "pbd/i18n.h" + +using namespace std; +using namespace Gtkmm2ext; + +PianoRollHeaderBase::PianoRollHeaderBase (MidiViewBackground& bg) + : _midi_context (bg) + , _adj (_midi_context.note_range_adjustment) + , _view (nullptr) + , _font_descript ("Sans Bold") + , _font_descript_big_c ("Sans") + , _font_descript_midnam ("Sans") + , _highlighted_note (NO_MIDI_NOTE) + , _clicked_note (NO_MIDI_NOTE) + , _dragging (false) + , _scroomer_size (63.f) + , _scroomer_drag (false) + , _old_y (0.0) + , _fract (0.0) + , _scroomer_state (NONE) + , _scroomer_button_state (NONE) + , _saved_top_val (0.0) + , _saved_bottom_val (127.0) + , _mini_map_display (false) + , entered (false) + , have_note_names (false) +{ + for (int i = 0; i < 128; ++i) { + _active_notes[i] = false; + } + + _midi_context.NoteRangeChanged.connect (sigc::mem_fun (*this, &PianoRollHeaderBase::note_range_changed)); +} + +void +PianoRollHeaderBase::alloc_layouts (Glib::RefPtr context) +{ + _layout = Pango::Layout::create (context); + _big_c_layout = Pango::Layout::create (context); + _font_descript_big_c.set_absolute_size (10.0 * Pango::SCALE); + _big_c_layout->set_font_description(_font_descript_big_c); + _midnam_layout = Pango::Layout::create (context); +} + +void +PianoRollHeaderBase::set_view (MidiView* v) +{ + _view = v; + if (_view) { + _view->midi_context().NoteRangeChanged.connect (sigc::mem_fun (*this, &PianoRollHeaderBase::note_range_changed)); + } +} + +bool +PianoRollHeaderBase::event_handler (GdkEvent* ev) +{ + /* Remember that ev uses canvas coordinates, not item */ + + switch (ev->type) { + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + return button_press_handler (&ev->button); + + case GDK_BUTTON_RELEASE: + return button_release_handler (&ev->button); + + case GDK_ENTER_NOTIFY: + return enter_handler (&ev->crossing); + + case GDK_LEAVE_NOTIFY: + return leave_handler (&ev->crossing); + + case GDK_SCROLL: + return scroll_handler (&ev->scroll); + + case GDK_MOTION_NOTIFY: + return motion_handler (&ev->motion); + + default: + break; + } + + return false; +} + +bool +PianoRollHeaderBase::scroll_handler (GdkEventScroll* ev) +{ + double evy = ev->y; + double ignore; + event_transform (ignore, evy); + + int note_range = _adj.get_page_size (); + int note_lower = _adj.get_value (); + + if(ev->state == GDK_SHIFT_MASK){ + switch (ev->direction) { + case GDK_SCROLL_UP: //ZOOM IN + _midi_context.apply_note_range (min(note_lower + 1, 127), max(note_lower + note_range - 1,0), true); + break; + case GDK_SCROLL_DOWN: //ZOOM OUT + _midi_context.apply_note_range (max(note_lower - 1,0), min(note_lower + note_range + 1, 127), true); + break; + default: + return false; + } + }else{ + switch (ev->direction) { + case GDK_SCROLL_UP: + _adj.set_value (min (note_lower + 1, 127 - note_range)); + break; + case GDK_SCROLL_DOWN: + _adj.set_value (note_lower - 1.0); + break; + default: + return false; + } + } + + set_note_highlight (_midi_context.y_to_note (event_y_to_y (evy))); + + _adj.value_changed (); + redraw (); + return true; +} + +void +PianoRollHeaderBase::render (ArdourCanvas::Rect const & self, ArdourCanvas::Rect const & area, Cairo::RefPtr cr) const +{ + int lowest; + Gtkmm2ext::Color bg; + + double y2 = min (self.y1, (ArdourCanvas::Coord) _midi_context.contents_height()); + double context_note_height = _midi_context.note_height(); + int bc_height, bc_width; + + //Reduce the frequency of Pango layout resizing + //if (int(_old_context_note_height) != int(context_note_height)) { + //Set Pango layout keyboard c's size + _font_descript.set_absolute_size (context_note_height * 0.7 * Pango::SCALE); + _layout->set_font_description(_font_descript); + + //change mode of midnam display + if (context_note_height >= 8.0) { + _mini_map_display = false; + } else { + _mini_map_display = true; + } + + //Set Pango layout midnam size + _font_descript_midnam.set_absolute_size (max(8.0 * 0.7 * Pango::SCALE, (int)context_note_height * 0.7 * Pango::SCALE)); + + _midnam_layout->set_font_description(_font_descript_midnam); + + lowest = max(_midi_context.lowest_note(), _midi_context.y_to_note(y2)); + + if (lowest > 127) { + lowest = 0; + } + + /* 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); + //r->set_source_rgb(1, 0,0); + //cr->fill(); + + cr->set_line_width (1.0f); + + Gtkmm2ext::Color white = UIConfiguration::instance().color (X_("piano key white")); + Gtkmm2ext::Color white_highlight = UIConfiguration::instance().color (X_("piano key highlight")); + Gtkmm2ext::Color black = UIConfiguration::instance().color (X_("piano key black")); + Gtkmm2ext::Color black_highlight = UIConfiguration::instance().color (X_("piano key highlight")); + Gtkmm2ext::Color textc = UIConfiguration::instance().color (X_("gtk_foreground")); + + std::vector numbers;; + std::vector positions; + std::vector heights; + + _midi_context.get_note_positions (numbers, positions, heights); + + /* Apply translation so we can use our natural coordinates to draw */ + + double origin_x = 0.; + double origin_y = 0.; + draw_transform (origin_x, origin_y); + + cr->save (); + cr->translate (origin_x, origin_y); + + // Render the MIDNAM text or its equivalent. First, set up a clip + // region so that the text doesn't spill, regardless of its length. + + cr->save (); + cr->rectangle (0,0,_scroomer_size, height ()); + cr->clip(); + + if (show_scroomer()) { + + /* Draw the actual text */ + + for (std::vector::size_type n = 0; n < numbers.size(); ++n) { + + int size_x, size_y; + int y = positions[n]; + NoteName const & note (note_names[numbers[n]]); + + _midnam_layout->set_text (note.name); + + set_source_rgba (cr, textc); + cr->move_to (2.f, y); + + if (!_mini_map_display) { + _midnam_layout->show_in_cairo_context (cr); + } else { + /* Too small for text, just show a thing rect where the + text would have been. + */ + if (!note.from_midnam) { + set_source_rgba (cr, textc); + } + pango_layout_get_pixel_size (_midnam_layout->gobj (), &size_x, &size_y); + cr->rectangle (2.f, y + (context_note_height * 0.5), size_x, context_note_height * 0.2); + cr->fill (); + } + } + + /* Add a gradient over the text, to act as a sort of "visual + elision". This avoids using text elision with "..." which takes up too + much space. + */ + Gtkmm2ext::Color bg = UIConfiguration::instance().color (X_("gtk_background")); + double r,g,b,a; + Gtkmm2ext::color_to_rgba(bg,r,g,b,a); + double fade_width = 30.; + auto gradient_ptr = Cairo::LinearGradient::create (_scroomer_size - fade_width, 0, _scroomer_size, 0); + gradient_ptr->add_color_stop_rgba (0,r,g,b,0); + gradient_ptr->add_color_stop_rgba (1,r,g,b,1); + cr->set_source (gradient_ptr); + cr->rectangle (_scroomer_size - fade_width, 0, fade_width, height ()); + cr->fill(); + } + + /* Now draw the semi-transparent scroomer over the top */ + + render_scroomer (cr); + + /* Done with clip region */ + + cr->restore(); + + /* Setup a cairo translation so that all drawing can be done using item + * coordinate + */ + + /* Now draw black/white rects for each note, following standard piano + layout, but without a setback/offset for the black keys + */ + + for (std::vector::size_type n = 0; n < numbers.size(); ++n) { + + int i = numbers[n]; + int oct_rel = i % 12; + + switch (oct_rel) { + case 1: + case 3: + case 6: + case 8: + case 10: + /* black note */ + if (i == _highlighted_note) { + bg = black_highlight; + } else { + bg = black; + } + break; + + case 0: + case 2: + case 4: + case 5: + case 7: + case 9: + case 11: + /* white note */ + if (i == _highlighted_note) { + bg = white_highlight; + } else { + bg = white; + } + break; + default: + break; + + } + + + Gtkmm2ext::set_source_rgba (cr, bg); + + double x = _scroomer_size;; + double y = positions[n]; + + cr->rectangle (x, y, width() - 1., heights[n]); + cr->fill (); + + if (oct_rel == 4 || oct_rel == 11) { + /* draw black separators between B/C and E/F */ + cr->set_source_rgb (0.0f, 0.0f, 0.0f); + cr->move_to (x, y); + cr->line_to (x + width(), y); + cr->set_line_width (2.0); + cr->stroke (); + } + } + + /* render the C of the key, when key is too small to contain text we + place the C on the midnam scroomer area. + + we render an additional 5 notes below the lowest note displayed + so that the top of the C is shown to maintain visual context + */ + + for (std::vector::size_type n = 0; n < numbers.size(); ++n) { + + double x = 0.; + double y = positions[n]; + int oct_rel = numbers[n] % 12; + + draw_transform (x, y); + + if (oct_rel == 0 || (oct_rel == 7 && _adj.get_page_size() <=10)) { + + std::stringstream str; + int cn = numbers[n] / 12 - 1; + + if (oct_rel == 0){ + str << 'C' << cn; + } else { + str << 'G' << cn; + } + + if (context_note_height > 12.0){ + set_source_rgba(cr, black); + _layout->set_text (str.str()); + cr->move_to(_scroomer_size, ceil (y+1.)); + _layout->show_in_cairo_context (cr); + } else { + set_source_rgba(cr, textc); + _big_c_layout->set_text (str.str()); + pango_layout_get_pixel_size (_big_c_layout->gobj(), &bc_width, &bc_height); + cr->move_to (_scroomer_size - 18, y - bc_height + context_note_height); + _big_c_layout->show_in_cairo_context (cr); + cr->move_to (_scroomer_size - 18, y + context_note_height); + cr->line_to (_scroomer_size, y + context_note_height); + cr->stroke(); + } + } + } + + cr->restore (); +} + +void +PianoRollHeaderBase::render_scroomer (Cairo::RefPtr cr) const +{ + double scroomer_top = max (1.0, (1.0 - ((_adj.get_value()+_adj.get_page_size()) / 127.0)) * height()); + double scroomer_bottom = (1.0 - (_adj.get_value () / 127.0)) * height (); + + Gtkmm2ext::Color c = UIConfiguration::instance().color_mod (X_("scroomer"), X_("scroomer alpha")); + Gtkmm2ext::Color save_color (c); + + if (entered) { + c = HSV (c).lighter (0.25).color(); + } + + double x = 0.; + double y = 0.; + draw_transform (x, y); + + cr->save (); + cr->translate (x, y); + + set_source_rgba (cr, c); + cr->move_to (1.f, scroomer_top); + cr->line_to (_scroomer_size - 1.f, scroomer_top); + cr->line_to (_scroomer_size - 1.f, scroomer_bottom); + cr->line_to (1.f, scroomer_bottom); + cr->line_to (1.f, scroomer_top); + cr->fill(); + + if (entered) { + cr->save (); + c = HSV (save_color).lighter (0.9).color(); + set_source_rgba (cr, c); + cr->set_line_width (4.); + cr->move_to (1.f, scroomer_top + 2.); + cr->line_to (_scroomer_size - 1.f, scroomer_top + 2.); + cr->stroke (); + cr->line_to (_scroomer_size - 1.f, scroomer_bottom - 2.); + cr->line_to (2.f, scroomer_bottom - 2.); + cr->stroke (); + cr->restore (); + } + + cr->restore (); +} + +void +PianoRollHeaderBase::instrument_info_change () +{ + have_note_names = false; + + for (int i = 0; i < 128; ++i) { + note_names[i] = get_note_name (i); + + if (note_names[i].from_midnam) { + have_note_names = true; + } + } + + _queue_resize (); + + /* need this to get editor to potentially sync all + track header widths if our piano roll header changes + width. + */ + + if (_view) { + _view->midi_track()->gui_changed ("visible_tracks", (void *) 0); /* EMIT SIGNAL */ + } + +} + +PianoRollHeaderBase::NoteName +PianoRollHeaderBase::get_note_name (int note) +{ + using namespace MIDI::Name; + std::string name; + NoteName rtn; + + ARDOUR::InstrumentInfo* ii = _midi_context.instrument_info(); + + if (!ii) { + return rtn; + } + + int midnam_channel = _midi_context.get_preferred_midi_channel (); + + name = ii->get_note_name ( + 0, //bank + 0, //program + midnam_channel, //channel + note); //note + + rtn.name = name.empty() ? ARDOUR::ParameterDescriptor::midi_note_name (note) : name; + rtn.from_midnam = !name.empty(); + return rtn; +} + +bool +PianoRollHeaderBase::motion_handler (GdkEventMotion* ev) +{ + /* event coordinates are in canvas/window space */ + + double evy = ev->y; + double ignore; + event_transform (ignore, evy); + + if (!_scroomer_drag && ev->x < _scroomer_size){ + + double scroomer_top = max (1.0, (1.0 - ((_adj.get_value()+_adj.get_page_size()) / 127.0)) * height()); + double scroomer_bottom = (1.0 - (_adj.get_value () / 127.0)) * height(); + double edge = 5. * UIConfiguration::instance().get_ui_scale(); + + if (evy > scroomer_top - edge && evy < scroomer_top + edge){ + if (_scroomer_state != TOP) { + set_cursor (_midi_context.editing_context().cursors()->resize_top); + _scroomer_state = TOP; + } + } else if (evy > scroomer_bottom - edge && evy < scroomer_bottom + edge){ + if (_scroomer_state != BOTTOM) { + set_cursor (_midi_context.editing_context().cursors()->resize_bottom); + _scroomer_state = BOTTOM; + } + } else { + if (_scroomer_state != MOVE) { + set_cursor (_midi_context.editing_context().cursors()->grabber); + _scroomer_state = MOVE; + } + } + } + + if (_scroomer_drag){ + + double pixel2val = 127.0 / height(); + double delta = _old_y - evy; + double val_at_pointer = (delta * pixel2val); + double real_val_at_pointer = 127.0 - (evy * pixel2val); + double note_range = _adj.get_page_size (); + + switch (_scroomer_button_state){ + case MOVE: + _fract += val_at_pointer; + _fract = (_fract + note_range > 127.0)? 127.0 - note_range : _fract; + _fract = max(0.0, _fract); + _adj.set_value (min(_fract, 127.0 - note_range)); + break; + case TOP: + real_val_at_pointer = real_val_at_pointer <= _saved_top_val? _adj.get_value() + _adj.get_page_size() : real_val_at_pointer; + real_val_at_pointer = min(127.0, real_val_at_pointer); + if (_midi_context.note_height() >= UIConfiguration::instance().get_max_note_height()){ + _saved_top_val = min(_adj.get_value() + _adj.get_page_size (), 127.0); + } else { + _saved_top_val = 0.0; + } + //if we are at largest note size & the user is moving down don't do anything + //FIXME we are using a heuristic of 18.5 for max note size, but this changes when track size is small to 19.5? + _midi_context.apply_note_range (_adj.get_value (), real_val_at_pointer, true); + break; + case BOTTOM: + real_val_at_pointer = max(0.0, real_val_at_pointer); + real_val_at_pointer = real_val_at_pointer >= _saved_bottom_val? _adj.get_value() : real_val_at_pointer; + if (_midi_context.note_height() >= UIConfiguration::instance().get_max_note_height()){ + _saved_bottom_val = _adj.get_value(); + } else { + _saved_bottom_val = 127.0; + } + _midi_context.apply_note_range (real_val_at_pointer, _adj.get_value () + _adj.get_page_size (), true); + break; + default: + break; + } + + redraw (); + + } else { + + int note = _midi_context.y_to_note(evy); + 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); + } + } + } + } + + } + + _old_y = evy; + + return true; +} + +bool +PianoRollHeaderBase::button_press_handler (GdkEventButton* ev) +{ + double evy = ev->y; + double ignore; + event_transform (ignore, evy); + + if (!_view) { + return false; + } + + /* Convert canvas-coordinates to item coordinates */ + + _scroomer_button_state = _scroomer_state; + + if (ev->button == 1 && ev->x <= _scroomer_size){ + + if (ev->type == GDK_2BUTTON_PRESS) { + _view->set_visibility_note_range (MidiStreamView::ContentsRange, false); + return true; + } + + _scroomer_drag = true; + _old_y = evy; + _fract = _adj.get_value(); + _fract_top = _adj.get_value() + _adj.get_page_size(); + return true; + + } else { + int note = _midi_context.y_to_note(evy); + bool tertiary = Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier); + bool primary = Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier); + + if (ev->button == 1 && ev->type == GDK_2BUTTON_PRESS) { + if (primary) { + _adj.set_value (0.0); + _adj.set_page_size (127.0); + _adj.value_changed (); + redraw (); + return false; + } + return false; + } else 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 (primary && (ev->button == 1 || ev->button == 2)) { + ToggleNoteSelection (note); // EMIT SIGNAL + return true; + } else if (ev->button == 1 && note >= 0 && note < 128) { + do_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 +PianoRollHeaderBase::button_release_handler (GdkEventButton* ev) +{ + double evy = ev->y; + double ignore; + event_transform (ignore, evy); + + _scroomer_drag = false; + + int note = _midi_context.y_to_note(evy); + + 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) { + do_ungrab (); + + if (note == _clicked_note) { + reset_clicked_note (note); + } + } + } + + _dragging = false; + return true; +} + +void +PianoRollHeaderBase::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 +PianoRollHeaderBase::enter_handler (GdkEventCrossing* ev) +{ + double evy = ev->y; + double ignore; + event_transform (ignore, evy); + + set_note_highlight (_midi_context.y_to_note (evy)); + entered = true; + redraw (); + return true; +} + +bool +PianoRollHeaderBase::leave_handler (GdkEventCrossing*) +{ + if (!_scroomer_drag){ + if (_view) { + /* XXX we used to pop the cursor stack here */ + } + } + 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; + entered = false; + redraw (); + + return true; +} + +void +PianoRollHeaderBase::note_range_changed () +{ + redraw (); +} + +void +PianoRollHeaderBase::invalidate_note_range (int lowest, int highest) +{ + lowest = max ((int) _midi_context.lowest_note(), lowest - 1); + highest = min ((int) _midi_context.highest_note(), highest + 2); + + int y = _midi_context.note_to_y (highest); + int h = _midi_context.note_to_y (lowest - 1) - y; + + redraw (0., y, width(), h); +} + +bool +PianoRollHeaderBase::show_scroomer () const +{ + Editing::NoteNameDisplay nnd = UIConfiguration::instance().get_note_name_display(); + + if (nnd == Editing::Never) { + return false; + } + + switch (_midi_context.editing_context().current_mouse_mode()) { + case Editing::MouseDraw: + case Editing::MouseContent: + if (nnd == Editing::WithMIDNAM) { + return have_note_names; + } else { + return true; + } + default: + break; + } + return false; +} + +void +PianoRollHeaderBase::send_note_on (uint8_t note) +{ + std::shared_ptr track = _view->midi_track (); + + //cerr << "note on: " << (int) note << endl; + + if (track) { + _event[0] = (MIDI_CMD_NOTE_ON | _midi_context.get_preferred_midi_channel ()); + _event[1] = note; + _event[2] = 100; + + track->write_user_immediate_event (Evoral::MIDI_EVENT, 3, _event); + } +} + +void +PianoRollHeaderBase::send_note_off (uint8_t note) +{ + std::shared_ptr track = _view->midi_track (); + + if (track) { + _event[0] = (MIDI_CMD_NOTE_OFF | _midi_context.get_preferred_midi_channel ()); + _event[1] = note; + _event[2] = 100; + + track->write_user_immediate_event (Evoral::MIDI_EVENT, 3, _event); + } +} + +void +PianoRollHeaderBase::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); + } +} + +void +PianoRollHeaderBase::set_min_page_size(double page_size) +{ + _min_page_size = page_size; +} + +void +PianoRollHeaderBase::set_cursor (Gdk::Cursor* cursor) +{ + Glib::RefPtr win = cursor_window (); + + if (win && !_midi_context.editing_context().cursors()->is_invalid (cursor)) { + gdk_window_set_cursor (win->gobj(), cursor ? cursor->gobj() : nullptr); + gdk_flush (); + } +} diff --git a/gtk2_ardour/prh_base.h b/gtk2_ardour/prh_base.h new file mode 100644 index 0000000000..395f99155a --- /dev/null +++ b/gtk2_ardour/prh_base.h @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2008-2025 David Robillard + * Copyright (C) 2010-2012 Paul Davis + * + * 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. + */ + +#pragma once + +#include +#include + +#include "ardour/types.h" + +#include "canvas/rectangle.h" + +#include + +namespace Gdk { + class Window; +} + +namespace ARDOUR { + class MidiTrack; +} + +class MidiView; +class MidiViewBackground; +class EditingContext; + +class PianoRollHeaderBase : virtual public sigc::trackable { + public: + PianoRollHeaderBase (MidiViewBackground&); + virtual ~PianoRollHeaderBase() {} + + void render (ArdourCanvas::Rect const & self, ArdourCanvas::Rect const & area, Cairo::RefPtr) const; + + void instrument_info_change (); + + void note_range_changed(); + void set_note_highlight (uint8_t note); + + sigc::signal SetNoteSelection; + sigc::signal AddNoteSelection; + sigc::signal ToggleNoteSelection; + sigc::signal ExtendNoteSelection; + + void set_view (MidiView*); + + virtual void redraw () = 0; + virtual void redraw (double x, double y, double w, double h) = 0; + virtual double height() const = 0; + virtual double width() const = 0; + virtual double event_y_to_y (double evy) const = 0; + virtual void draw_transform (double& x, double& y) const = 0; + virtual void event_transform (double& x, double& y) const = 0; + virtual void _queue_resize () = 0; + virtual void do_grab() = 0; + virtual void do_ungrab() = 0; + virtual Glib::RefPtr cursor_window() = 0; + + protected: + MidiViewBackground& _midi_context; + Gtk::Adjustment& _adj; + MidiView* _view; + + uint8_t _event[3]; + + mutable Glib::RefPtr _layout; + mutable Glib::RefPtr _big_c_layout; + mutable Glib::RefPtr _midnam_layout; + mutable Pango::FontDescription _font_descript; + Pango::FontDescription _font_descript_big_c; + mutable Pango::FontDescription _font_descript_midnam; + bool _active_notes[128]; + uint8_t _highlighted_note; + uint8_t _clicked_note; + double _grab_y; + bool _dragging; + mutable double _scroomer_size; + bool _scroomer_drag; + double _old_y; + double _fract; + double _fract_top; + double _min_page_size; + enum scr_pos {TOP, BOTTOM, MOVE, NONE}; + scr_pos _scroomer_state; + scr_pos _scroomer_button_state; + double _saved_top_val; + double _saved_bottom_val; + mutable bool _mini_map_display; + bool entered; + + // void on_size_request(Gtk::Requisition*); + + struct NoteName { + std::string name; + bool from_midnam; + }; + NoteName note_names[128]; + bool have_note_names; + + void set_min_page_size (double page_size); + void render_scroomer (Cairo::RefPtr) const; + NoteName get_note_name (int note); + + bool event_handler (GdkEvent*); + bool motion_handler (GdkEventMotion*); + bool button_press_handler (GdkEventButton*); + bool button_release_handler (GdkEventButton*); + bool scroll_handler (GdkEventScroll*); + bool enter_handler (GdkEventCrossing*); + bool leave_handler (GdkEventCrossing*); + + enum ItemType { + BLACK_SEPARATOR, + BLACK_MIDDLE_SEPARATOR, + BLACK, + WHITE_SEPARATOR, + WHITE_RECT, + WHITE_CF, + WHITE_EB, + WHITE_DGA + }; + + void invalidate_note_range (int lowest, int highest); + void send_note_on (uint8_t note); + void send_note_off (uint8_t note); + void reset_clicked_note (uint8_t, bool invalidate = true); + bool show_scroomer () const; + void alloc_layouts (Glib::RefPtr); + void set_cursor (Gdk::Cursor*); +}; + diff --git a/gtk2_ardour/wscript b/gtk2_ardour/wscript index 8bf1e1e817..bc7290387e 100644 --- a/gtk2_ardour/wscript +++ b/gtk2_ardour/wscript @@ -236,6 +236,7 @@ gtk2_ardour_sources = [ 'port_matrix_labels.cc', 'port_matrix_row_labels.cc', 'prh.cc', + 'prh_base.cc', 'processor_box.cc', 'processor_selection.cc', 'patch_change_dialog.cc',