/* * Copyright (C) 2006-2016 David Robillard * Copyright (C) 2007-2012 Carl Hetherington * Copyright (C) 2007-2018 Paul Davis * Copyright (C) 2008-2012 Hans Baier * Copyright (C) 2013-2017 John Emmas * Copyright (C) 2014-2017 Nick Mainsbridge * Copyright (C) 2014-2018 Ben Loftis * Copyright (C) 2014-2019 Robin Gareus * Copyright (C) 2015-2016 Tim Mayberry * Copyright (C) 2015-2017 André Nusser * * 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 #include #include #include "gtkmm2ext/gtk_ui.h" #include #include "midi++/midnam_patch.h" #include "pbd/stateful_diff_command.h" #include "pbd/unwind.h" #include "ardour/debug.h" #include "ardour/midi_model.h" #include "ardour/midi_playlist.h" #include "ardour/midi_region.h" #include "ardour/midi_source.h" #include "ardour/midi_track.h" #include "ardour/operations.h" #include "ardour/quantize.h" #include "ardour/session.h" #include "evoral/Parameter.h" #include "evoral/Event.h" #include "evoral/Control.h" #include "evoral/midi_util.h" #include "canvas/debug.h" #include "automation_region_view.h" #include "automation_time_axis.h" #include "control_point.h" #include "debug.h" #include "editor.h" #include "editor_drag.h" #include "ghostregion.h" #include "gui_thread.h" #include "item_counts.h" #include "keyboard.h" #include "midi_channel_dialog.h" #include "midi_cut_buffer.h" #include "midi_list_editor.h" #include "midi_region_view.h" #include "midi_streamview.h" #include "midi_time_axis.h" #include "midi_util.h" #include "midi_velocity_dialog.h" #include "note_player.h" #include "paste_context.h" #include "public_editor.h" #include "route_time_axis.h" #include "rgb_macros.h" #include "selection.h" #include "streamview.h" #include "patch_change_dialog.h" #include "velocity_ghost_region.h" #include "verbose_cursor.h" #include "note.h" #include "hit.h" #include "patch_change.h" #include "sys_ex.h" #include "ui_config.h" #include "pbd/i18n.h" using namespace ARDOUR; using namespace PBD; using namespace Editing; using namespace std; using namespace Temporal; using Gtkmm2ext::Keyboard; #define MIDI_BP_ZERO ((Config->get_first_midi_bank_is_zero())?0:1) MidiRegionView::MidiRegionView (ArdourCanvas::Container* parent, EditingContext& ec, RouteTimeAxisView& tv, std::shared_ptr r, double spu, uint32_t basic_color) : RegionView (parent, tv, r, spu, basic_color) , MidiView (std::dynamic_pointer_cast (tv.stripable()), *group, ec, *dynamic_cast(tv.view()), basic_color) { connect_to_diskstream (); } MidiRegionView::MidiRegionView (ArdourCanvas::Container* parent, EditingContext& ec, RouteTimeAxisView& tv, std::shared_ptr r, double spu, uint32_t basic_color, bool recording, TimeAxisViewItem::Visibility visibility) : RegionView (parent, tv, r, spu, basic_color, recording, visibility) , MidiView (std::dynamic_pointer_cast (tv.stripable()), *group, ec, *dynamic_cast(tv.view()), basic_color) { connect_to_diskstream (); } MidiRegionView::MidiRegionView (const MidiRegionView& other) : sigc::trackable(other) , RegionView (other) , MidiView (other) { init (false); } MidiRegionView::MidiRegionView (const MidiRegionView& other, std::shared_ptr region) : RegionView (other, std::shared_ptr (region)) , MidiView (other) { init (true); } void MidiRegionView::init (bool /*wfd*/) { DisplaySuspender ds (*this, true); RegionView::init (false); CANVAS_DEBUG_NAME (_note_group, string_compose ("note group for %1", get_item_name())); set_region (std::dynamic_pointer_cast (_region)); //set_height (trackview.current_height()); region_muted (); region_sync_changed (); region_resized (ARDOUR::bounds_change); //region_locked (); set_colors (); reset_width_dependent_items (_pixel_width); _note_group->parent()->raise_to_top(); Config->ParameterChanged.connect (*this, invalidator (*this), boost::bind (&MidiRegionView::parameter_changed, this, _1), gui_context()); connect_to_diskstream (); } bool MidiRegionView::display_is_enabled () const { return RegionView::display_enabled (); } void MidiRegionView::parameter_changed (std::string const & p) { RegionView::parameter_changed (p); if (p == "display-first-midi-bank-as-zero") { if (display_enabled()) { view_changed (); } } else if (p == "color-regions-using-track-color") { set_colors (); } else if (p == "use-note-color-for-velocity") { color_handler (); } } void MidiRegionView::color_handler () { RegionView::color_handler (); MidiView::color_handler (); } void MidiRegionView::region_resized (PBD::PropertyChange const & change) { RegionView::region_resized (change); MidiView::region_resized (change); } InstrumentInfo& MidiRegionView::instrument_info () const { RouteUI* route_ui = dynamic_cast (&trackview); return route_ui->route()->instrument_info(); } const std::shared_ptr MidiRegionView::midi_region() const { return _midi_region; } void MidiRegionView::connect_to_diskstream () { midi_view()->midi_track()->DataRecorded.connect( *this, invalidator(*this), boost::bind (&MidiRegionView::data_recorded, this, _1), gui_context()); } std::string MidiRegionView::get_modifier_name () const { const bool opaque = _region->opaque() || trackview.layer_display () == Stacked; std::string mod_name; if (_dragging) { mod_name = "dragging region"; } else if (_editing_context.internal_editing()) { if (!opaque || _region->muted ()) { mod_name = "editable region"; } } else { if (!opaque || _region->muted ()) { mod_name = "transparent region base"; } } return mod_name; } GhostRegion* MidiRegionView::add_ghost (TimeAxisView& tv) { double unit_position = _editing_context.time_to_pixel (_region->position ()); MidiTimeAxisView* mtv = dynamic_cast(&tv); MidiGhostRegion* ghost; if (mtv && mtv->midi_view()) { return 0; } else { AutomationTimeAxisView* atv = dynamic_cast(&tv); if (atv && atv->parameter() == Evoral::Parameter (MidiVelocityAutomation)) { ghost = new VelocityGhostRegion (*this, tv, trackview, unit_position); } else { ghost = new MidiGhostRegion (*this, tv, trackview, unit_position); } } ghost->set_colors (); ghost->set_height (); ghost->set_duration (_region->length().samples() / samples_per_pixel); for (auto const & i : _events) { ghost->add_note (i.second); } ghosts.push_back (ghost); return ghost; } bool MidiRegionView::canvas_group_event(GdkEvent* ev) { if (in_destructor || _recregion) { return false; } if (!_editing_context.internal_editing()) { // not in internal edit mode, so just act like a normal region return RegionView::canvas_group_event (ev); } return MidiView::canvas_group_event (ev); } bool MidiRegionView::enter_notify (GdkEventCrossing* ev) { enter_internal (ev->state); _entered = true; return false; } bool MidiRegionView::leave_notify (GdkEventCrossing*) { leave_internal (); _entered = false; return false; } void MidiRegionView::mouse_mode_changed () { // Adjust frame colour (become more transparent for internal tools) set_frame_color(); MidiView::mouse_mode_changed (); } void MidiRegionView::enter_internal (uint32_t state) { if (_editing_context.current_mouse_mode() == MouseDraw && _mouse_state != AddDragging) { // Show ghost note under pencil create_ghost_note(_last_event_x, _last_event_y, state); } // Lower frame handles below notes so they don't steal events if (frame_handle_start) { frame_handle_start->lower_to_bottom(); } if (frame_handle_end) { frame_handle_end->lower_to_bottom(); } } void MidiRegionView::leave_internal() { hide_verbose_cursor (); remove_ghost_note (); _entered_note = 0; // Raise frame handles above notes so they catch events if (frame_handle_start) { frame_handle_start->raise_to_top(); } if (frame_handle_end) { frame_handle_end->raise_to_top(); } } bool MidiRegionView::button_press (GdkEventButton* ev) { if (ev->button != 1) { return false; } MouseMode m = _editing_context.current_mouse_mode(); if (m == MouseContent && Keyboard::modifier_state_contains (ev->state, Keyboard::insert_note_modifier())) { _press_cursor_ctx = CursorContext::create(_editing_context, _editing_context.cursors()->midi_pencil); } if (_mouse_state != SelectTouchDragging) { _pressed_button = ev->button; if (m == MouseDraw || (m == MouseContent && Keyboard::modifier_state_contains (ev->state, Keyboard::insert_note_modifier()))) { if (midi_view()->note_mode() == Percussive) { _editing_context.drags()->set (new HitCreateDrag (_editing_context, group, this), (GdkEvent *) ev); } else { _editing_context.drags()->set (new NoteCreateDrag (_editing_context, group, this), (GdkEvent *) ev); } _mouse_state = AddDragging; remove_ghost_note (); hide_verbose_cursor (); } else { _mouse_state = Pressed; } return true; } _pressed_button = ev->button; _mouse_changed_selection = false; return true; } bool MidiRegionView::button_release (GdkEventButton* ev) { double event_x, event_y; if (ev->button != 1) { return false; } event_x = ev->x; event_y = ev->y; group->canvas_to_item (event_x, event_y); group->ungrab (); _press_cursor_ctx.reset(); switch (_mouse_state) { case Pressed: // Clicked switch (_editing_context.current_mouse_mode()) { case MouseRange: /* no motion occurred - simple click */ clear_selection_internal (); _mouse_changed_selection = true; break; case MouseContent: _editing_context.get_selection().set (this); /* fallthru */ case MouseTimeFX: _mouse_changed_selection = true; clear_selection_internal (); break; case MouseDraw: _editing_context.get_selection().set (this); break; default: break; } _mouse_state = None; break; case AddDragging: /* Don't a ghost note when we added a note - wait until motion to avoid visual confusion. we don't want one when we were drag-selecting either. */ case SelectRectDragging: _editing_context.drags()->end_grab ((GdkEvent *) ev); _mouse_state = None; break; default: break; } if (_mouse_changed_selection) { _editing_context.begin_reversible_selection_op (X_("Mouse Selection Change")); _editing_context.commit_reversible_selection_op (); } return false; } bool MidiRegionView::motion (GdkEventMotion* ev) { MidiView::motion (ev); return RegionView::canvas_group_event ((GdkEvent *) ev); } bool MidiRegionView::scroll (GdkEventScroll* ev) { if (_editing_context.drags()->active()) { return false; } if (!_editing_context.get_selection().selected (this)) { return false; } if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier) || Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) { /* XXX: bit of a hack; allow PrimaryModifier+TertiaryModifier scroll * through so that it still works for navigation and zoom. */ return false; } if (_selection.empty()) { const int step = 1; const bool zoom = Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier); const bool just_one_edge = Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier|Keyboard::PrimaryModifier); switch (ev->direction) { case GDK_SCROLL_UP: if (just_one_edge) { /* make higher notes visible aka expand higher pitch range */ midi_stream_view()->apply_note_range (midi_stream_view()->lowest_note(), min (127, midi_stream_view()->highest_note() + step), true); } else if (zoom) { /* zoom out to show more higher and lower pitches */ midi_stream_view()->apply_note_range (max (0, midi_stream_view()->lowest_note() - step), min (127, midi_stream_view()->highest_note() + step), true); } else { /* scroll towards higher pitches */ midi_stream_view()->apply_note_range (max (0, midi_stream_view()->lowest_note() + step), min (127, midi_stream_view()->highest_note() + step), true); } return true; case GDK_SCROLL_DOWN: if (just_one_edge) { /* make lower notes visible aka expand lower pitch range */ midi_stream_view()->apply_note_range (max (0, midi_stream_view()->lowest_note() - step), midi_stream_view()->highest_note(), true); } else if (zoom) { /* zoom in to show less higher and lower pitches */ midi_stream_view()->apply_note_range (min (127, midi_stream_view()->lowest_note() + step), max (0, midi_stream_view()->highest_note() - step), true); } else { /* scroll towards lower pitches */ midi_stream_view()->apply_note_range (min (127, midi_stream_view()->lowest_note() - step), max (0, midi_stream_view()->highest_note() - step), true); } return true; default: break; } return false; } hide_verbose_cursor (); if (UIConfiguration::instance().get_scroll_velocity_editing()) { bool fine = !Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier); Keyboard::ModifierMask mask_together(Keyboard::PrimaryModifier|Keyboard::TertiaryModifier); bool together = Keyboard::modifier_state_contains (ev->state, mask_together); if (ev->direction == GDK_SCROLL_UP) { change_velocities (true, fine, false, together); } else if (ev->direction == GDK_SCROLL_DOWN) { change_velocities (false, fine, false, together); } else { /* left, right: we don't use them */ return false; } return true; } return false; } void MidiRegionView::ghosts_view_changed () { for (auto & g : ghosts) { MidiGhostRegion* gr = dynamic_cast (g); if (gr && !gr->trackview.hidden()) { gr->view_changed (); } } } MidiRegionView::~MidiRegionView () { in_destructor = true; RegionViewGoingAway (this); /* EMIT_SIGNAL */ } void MidiRegionView::reset_width_dependent_items (double pixel_width) { RegionView::reset_width_dependent_items(pixel_width); view_changed (); bool hide_all = false; PatchChanges::iterator x = _patch_changes.begin(); if (x != _patch_changes.end()) { hide_all = x->second->width() >= _pixel_width; } if (hide_all) { for (; x != _patch_changes.end(); ++x) { x->second->hide(); } } move_step_edit_cursor (_step_edit_cursor_position); set_step_edit_cursor_width (_step_edit_cursor_width); } void MidiRegionView::set_height (double height) { MidiView::set_height (height); RegionView::set_height(height); } void MidiRegionView::set_selected (bool selected) { if (!selected) { clear_selection_internal (); } RegionView::set_selected (selected); } void MidiRegionView::ghost_sync_selection (NoteBase* ev) { for (auto & ghost : ghosts) { MidiGhostRegion* gr; if ((gr = dynamic_cast(ghost)) != 0) { gr->note_selected (ev); } } } uint32_t MidiRegionView::get_fill_color() const { Gtkmm2ext::Color c; if (_selected) { c = UIConfiguration::instance().color ("selected region base"); } else if ((!UIConfiguration::instance().get_show_name_highlight() || high_enough_for_name) && !UIConfiguration::instance().get_color_regions_using_track_color()) { c = UIConfiguration::instance().color (fill_color_name); } else { c = fill_color; } string mod_name = get_modifier_name(); if (mod_name.empty ()) { return c; } else { return UIConfiguration::instance().color_mod (c, mod_name); } } double MidiRegionView::height() const { return TimeAxisViewItem::height(); } void MidiRegionView::redisplay (bool view_only) { MidiView::redisplay (view_only); } ArdourCanvas::Item* MidiRegionView::drag_group () const { return get_canvas_group (); } void MidiRegionView::select_self (bool add) { if (add) { _editing_context.get_selection().add (this); } else { _editing_context.get_selection().set (this); } } void MidiRegionView::unselect_self () { _editing_context.get_selection().remove (this); } void MidiRegionView::begin_drag_edit (std::string const & why) { if (!_selected) { /* unclear why gcc can't understand which version of select_self() to use here, but so be it. */ MidiView::select_self (); } // start_note_diff_command (why); } void MidiRegionView::select_self_uniquely () { _editing_context.set_selected_midi_region_view (*this); } void MidiRegionView::set_visibility_note_range (MidiViewBackground::VisibleNoteRange vnr, bool from_selection) { dynamic_cast(&trackview)->set_visibility_note_range (vnr, from_selection); }