13
0
livetrax/gtk2_ardour/midi_region_view.cc
Paul Davis b8fb779fa7 allow dragging in MIDI automation line in cue editor
Note: once this is cleaned up and checked with respect to snap and more,
the same should be done for MidiRegionView
2024-10-17 07:44:32 -06:00

765 lines
18 KiB
C++

/*
* Copyright (C) 2006-2016 David Robillard <d@drobilla.net>
* Copyright (C) 2007-2012 Carl Hetherington <carl@carlh.net>
* Copyright (C) 2007-2018 Paul Davis <paul@linuxaudiosystems.com>
* Copyright (C) 2008-2012 Hans Baier <hansfbaier@googlemail.com>
* Copyright (C) 2013-2017 John Emmas <john@creativepost.co.uk>
* Copyright (C) 2014-2017 Nick Mainsbridge <mainsbridge@gmail.com>
* Copyright (C) 2014-2018 Ben Loftis <ben@harrisonconsoles.com>
* Copyright (C) 2014-2019 Robin Gareus <robin@gareus.org>
* Copyright (C) 2015-2016 Tim Mayberry <mojofunk@gmail.com>
* Copyright (C) 2015-2017 André Nusser <andre.nusser@googlemail.com>
*
* 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 <cmath>
#include <algorithm>
#include <ostream>
#include <gtkmm.h>
#include "gtkmm2ext/gtk_ui.h"
#include <sigc++/signal.h>
#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<MidiRegion> r,
double spu,
uint32_t basic_color)
: RegionView (parent, tv, r, spu, basic_color)
, MidiView (std::dynamic_pointer_cast<MidiTrack> (tv.stripable()), *group, ec, *dynamic_cast<MidiStreamView*>(tv.view()), basic_color)
{
connect_to_diskstream ();
}
MidiRegionView::MidiRegionView (ArdourCanvas::Container* parent,
EditingContext& ec,
RouteTimeAxisView& tv,
std::shared_ptr<MidiRegion> 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<MidiTrack> (tv.stripable()), *group, ec, *dynamic_cast<MidiStreamView*>(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<MidiRegion> region)
: RegionView (other, std::shared_ptr<Region> (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<MidiRegion> (_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<RouteUI*> (&trackview);
return route_ui->route()->instrument_info();
}
const std::shared_ptr<ARDOUR::MidiRegion>
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<MidiTimeAxisView*>(&tv);
MidiGhostRegion* ghost;
if (mtv && mtv->midi_view()) {
return 0;
} else {
AutomationTimeAxisView* atv = dynamic_cast<AutomationTimeAxisView*>(&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);
std::cerr << "Adding " << _events.size() << " notes to ghost\n";
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<MidiGhostRegion*> (g);
if (gr && !gr->trackview.hidden()) {
gr->view_changed ();
}
}
}
void
MidiRegionView::clear_ghost_events()
{
for (auto & ghost : ghosts) {
MidiGhostRegion* gr;
if ((gr = dynamic_cast<MidiGhostRegion*>(ghost)) != 0) {
gr->clear_events ();
}
}
}
void
MidiRegionView::ghosts_model_changed()
{
for (auto & ghost : ghosts) {
MidiGhostRegion* gr;
if ((gr = dynamic_cast<MidiGhostRegion*>(ghost)) != 0) {
if (!gr->trackview.hidden()) {
gr->model_changed ();
}
}
}
}
void
MidiRegionView::ghost_remove_note (NoteBase* nb)
{
for (auto & ghost : ghosts) {
MidiGhostRegion* gr;
if ((gr = dynamic_cast<MidiGhostRegion*>(ghost)) != 0) {
gr->remove_note (nb);
}
}
}
void
MidiRegionView::ghost_add_note (NoteBase* nb)
{
for (auto & ghost : ghosts) {
MidiGhostRegion* gr;
std::cerr << "GAN on " << ghost << std::endl;
if ((gr = dynamic_cast<MidiGhostRegion*>(ghost)) != 0) {
gr->add_note (nb);
}
}
}
void
MidiRegionView::ghost_sync_selection (NoteBase* nb)
{
for (auto & ghost : ghosts) {
MidiGhostRegion* gr;
if ((gr = dynamic_cast<MidiGhostRegion*>(ghost)) != 0) {
gr->note_selected (nb);
}
}
}
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);
}
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<MidiTimeAxisView*>(&trackview)->set_visibility_note_range (vnr, from_selection);
}
MergeableLine*
MidiRegionView::make_merger ()
{
return nullptr;
}