From fa970718728070bf1e297ae3de4e354fbd1e01f0 Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Tue, 12 Aug 2025 21:44:37 -0600 Subject: [PATCH] mamy improvements to kbd-driven automation editing Really needs a short video to demo/explain --- gtk2_ardour/automation.bindings | 2 + gtk2_ardour/automation_line.cc | 175 +++++++++++++--- gtk2_ardour/automation_line.h | 10 +- gtk2_ardour/automation_text_entry.cc | 290 +++++++++++++++++++++++++++ gtk2_ardour/automation_text_entry.h | 69 +++++++ gtk2_ardour/editing_context.cc | 2 + gtk2_ardour/editing_context.h | 2 + gtk2_ardour/editor.h | 2 + gtk2_ardour/editor_actions.cc | 32 +++ gtk2_ardour/wscript | 1 + 10 files changed, 553 insertions(+), 32 deletions(-) create mode 100644 gtk2_ardour/automation_text_entry.cc create mode 100644 gtk2_ardour/automation_text_entry.h diff --git a/gtk2_ardour/automation.bindings b/gtk2_ardour/automation.bindings index dd3139a40c..e5ba356cca 100644 --- a/gtk2_ardour/automation.bindings +++ b/gtk2_ardour/automation.bindings @@ -5,5 +5,7 @@ + + diff --git a/gtk2_ardour/automation_line.cc b/gtk2_ardour/automation_line.cc index 52939cb4b5..cad2517841 100644 --- a/gtk2_ardour/automation_line.cc +++ b/gtk2_ardour/automation_line.cc @@ -57,10 +57,12 @@ #include "canvas/canvas.h" #include "canvas/debug.h" +#include "gtkmm2ext/doi.h" + #include "automation_line.h" +#include "automation_text_entry.h" #include "control_point.h" #include "editing_context.h" -#include "floating_text_entry.h" #include "gui_thread.h" #include "rgb_macros.h" #include "public_editor.h" @@ -1089,36 +1091,16 @@ AutomationLine::set_selected_points (PointSelection const & points) set_colors (); + if (points.empty() || !one_of_ours) { + return; + } + if (one_of_ours && entry_required_post_add && points.size() == 1) { - ControlPoint* cp (points.front()); - std::stringstream str; - str << (*cp->model())->value << ' ' << "Hz"; - - ArdourCanvas::GtkCanvas* cvp = dynamic_cast (cp->item().canvas()); - Gtk::Window* toplevel = static_cast (cvp->get_toplevel()); - if (!toplevel) { - entry_required_post_add = false; - return; + text_edit_control_point (*points.front(), false); + } else { + if (points.size() > 1 && automation_entry) { + delete_when_idle (automation_entry); } - - automation_entry = new FloatingTextEntry (toplevel, str.str()); - automation_entry->set_name (X_("LargeTextEntry")); - automation_entry->delete_on_focus_out (); - ArdourCanvas::Duple d (cp->get_x(), cp->get_y()); - d = cp->item().item_to_window (d); - - int wx, wy; - - cvp->translate_coordinates (*toplevel, d.x, d.y, wx, wy); - - /* Shift the text entry a bit to the right */ - wx += 30 * UIConfiguration::instance().get_ui_scale(); - - gint rwx, rwy; - - toplevel->get_position (rwx, rwy); - automation_entry->move (rwx + wx, rwy + wy); - automation_entry->show (); } if (one_of_ours) { @@ -1126,6 +1108,137 @@ AutomationLine::set_selected_points (PointSelection const & points) } } +void +AutomationLine::text_edit_control_point (ControlPoint& cp, bool grab_focus) +{ + switch (_desc.type) { + case GainAutomation: + case PanAzimuthAutomation: + case PanElevationAutomation: + case PanWidthAutomation: + case PanFrontBackAutomation: + case PanLFEAutomation: + case MidiCCAutomation: + case MidiPitchBenderAutomation: + case MidiChannelPressureAutomation: + case MidiNotePressureAutomation: + case FadeInAutomation: + case FadeOutAutomation: + case EnvelopeAutomation: + case TrimAutomation: + case PhaseAutomation: + case MonitoringAutomation: + case BusSendLevel: + case BusSendEnable: + case SurroundSendLevel: + case InsertReturnLevel: + case MainOutVolume: + case MidiVelocityAutomation: + case PanSurroundX: + case PanSurroundY: + case PanSurroundZ: + case PanSurroundSize: + case PanSurroundSnap: + case BinauralRenderMode: + case PanSurroundElevationEnable: + case PanSurroundZones: + case PanSurroundRamp: + case SendLevelAutomation: + case SendEnableAutomation: + case SendAzimuthAutomation: + break; + default: + /* No text editing values for this type of automation */ + entry_required_post_add = false; + return; + } + + std::string txt = ARDOUR::value_as_string (_desc, (*cp.model())->value); + + ArdourCanvas::GtkCanvas* cvp = dynamic_cast (cp.item().canvas()); + Gtk::Window* toplevel = static_cast (cvp->get_toplevel()); + if (!toplevel) { + entry_required_post_add = false; + return; + } + + if (automation_entry) { + delete_when_idle (automation_entry); + automation_entry = nullptr; + } + + automation_entry = new AutomationTextEntry (toplevel, txt); + automation_entry->set_name (X_("BigTextEntry")); + automation_entry->use_text.connect (sigc::bind (sigc::mem_fun (*this, &AutomationLine::value_edited), &cp)); + automation_entry->going_away.connect (sigc::mem_fun (*this, &AutomationLine::automation_text_deleted)); + + ArdourCanvas::Duple d (cp.get_x(), cp.get_y()); + d = cp.item().item_to_window (d); + + int wx, wy; + + cvp->translate_coordinates (*toplevel, d.x, d.y, wx, wy); + + /* Shift the text entry a bit to the right */ + wx += 30 * UIConfiguration::instance().get_ui_scale(); + + gint rwx, rwy; + + toplevel->get_position (rwx, rwy); + automation_entry->move (rwx + wx, rwy + wy); + automation_entry->show (); + + if (grab_focus) { + automation_entry->activate_entry (); + } +} + +void +AutomationLine::begin_edit () +{ + if (automation_entry) { + return; + } + + PointSelection& points (_editing_context.get_selection().points); + + if (points.size() != 1) { + return; + } + + if (&(points.front()->line()) == this) { + text_edit_control_point (*points.front(), true); + } +} + +void +AutomationLine::end_edit () +{ + if (automation_entry) { + delete_when_idle (automation_entry); + } +} + +void +AutomationLine::automation_text_deleted (AutomationTextEntry* ate) +{ + if (ate == automation_entry) { + automation_entry = nullptr; + } +} + +void +AutomationLine::value_edited (std::string str, int /* what_next*/, ControlPoint* cp) +{ + bool legal; + double val = ARDOUR::string_as_value (_desc, str, legal); + + if (legal) { + alist->modify (cp->model(), (*cp->model())->when, val); + automation_entry->hide (); + } +} + void AutomationLine::list_changed () { @@ -1307,7 +1420,9 @@ AutomationLine::reset_callback (const Evoral::ControlList& events) update_visibility (); } - set_selected_points (_editing_context.get_selection().points); + if (!entry_required_post_add) { + set_selected_points (_editing_context.get_selection().points); + } } void diff --git a/gtk2_ardour/automation_line.h b/gtk2_ardour/automation_line.h index 99bf699b8f..d29d133c0f 100644 --- a/gtk2_ardour/automation_line.h +++ b/gtk2_ardour/automation_line.h @@ -58,7 +58,7 @@ class TimeAxisView; class AutomationTimeAxisView; class Selection; class EditingContext; -class FloatingTextEntry; +class AutomationTextEntry; /** A GUI representation of an ARDOUR::AutomationList */ class AutomationLine : public sigc::trackable, public PBD::StatefulDestructible, public SelectableOwner @@ -192,6 +192,8 @@ public: EditingContext& editing_context() const { return _editing_context; } void add (std::shared_ptr, GdkEvent*, Temporal::timepos_t const &, double y, bool with_guard_points, bool from_kbd = false); + void end_edit (); + void begin_edit (); protected: @@ -277,7 +279,11 @@ private: bool _sensitive; AutomationTimeAxisView* atv; bool entry_required_post_add; - FloatingTextEntry* automation_entry; + AutomationTextEntry* automation_entry; + + void value_edited (std::string, int, ControlPoint*); + void automation_text_deleted (AutomationTextEntry*); + void text_edit_control_point (ControlPoint& cp, bool grab_focus); friend class AudioRegionGainLine; friend class RegionFxLine; diff --git a/gtk2_ardour/automation_text_entry.cc b/gtk2_ardour/automation_text_entry.cc new file mode 100644 index 0000000000..16b16a70ad --- /dev/null +++ b/gtk2_ardour/automation_text_entry.cc @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2014-2016 Paul Davis + * Copyright (C) 2015-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 "gtkmm2ext/doi.h" +#include "gtkmm2ext/utils.h" + +#include "automation_text_entry.h" +#include "public_editor.h" +#include "utils.h" + +#include "pbd/i18n.h" + +AutomationTextEntry::AutomationTextEntry (Gtk::Window* parent, std::string const & initial_contents) + : Gtk::Window () + , entry_changed (false) +{ + //set_name (X_("AutomationTextEntry")); + set_position (Gtk::WIN_POS_MOUSE); + set_border_width (0); + set_type_hint(Gdk::WINDOW_TYPE_HINT_POPUP_MENU); + set_resizable (false); + set_accept_focus (false); + + _connections.push_back (entry.signal_changed().connect (sigc::mem_fun (*this, &AutomationTextEntry::changed))); + _connections.push_back (entry.signal_activate().connect (sigc::mem_fun (*this, &AutomationTextEntry::activated))); + _connections.push_back (entry.signal_key_press_event().connect (sigc::mem_fun (*this, &AutomationTextEntry::key_press), false)); + _connections.push_back (entry.signal_key_release_event().connect (sigc::mem_fun (*this, &AutomationTextEntry::key_release), false)); + _connections.push_back (entry.signal_button_press_event().connect (sigc::mem_fun (*this, &AutomationTextEntry::button_press), false)); + _connections.push_back (entry.signal_focus_in_event().connect (sigc::mem_fun (*this, &AutomationTextEntry::entry_focus_in))); + + if (parent) { + set_transient_for (*parent); + } + + std::string unit_text; + std::string n; + + split_units (initial_contents, n, unit_text); + + entry.set_text (n); + entry.show (); + entry.set_can_focus (false); + + if (!unit_text.empty()) { + Gtk::HBox* hbox (manage (new Gtk::HBox)); + hbox->set_spacing (6); + + hbox->pack_start (entry); + hbox->pack_start (units); + units.set_text (unit_text); + units.show (); + hbox->show (); + add (*hbox); + } else { + add (entry); + } +} + +AutomationTextEntry::~AutomationTextEntry () +{ + going_away (this); /* EMIT SIGNAL */ +} + +void +AutomationTextEntry::split_units (std::string const & str, std::string & numeric, std::string & units) +{ + if (str.empty()) { + return; + } + + std::regex units_regex ("( *[^0-9.,]+)$"); + std::smatch matches; + + std::regex_search (str, matches, units_regex); + + if (!matches.empty()) { + units = matches[matches.size() - 1]; + + numeric = str.substr (0, units.length()); + + while (units.length() > 1 && std::isspace (units[0])) { + units = units.substr (1); + } + + } else { + + numeric = str; + } +} + +void +AutomationTextEntry::changed () +{ + entry_changed = true; +} + +void +AutomationTextEntry::delete_on_focus_out () +{ + _connections.push_back (signal_focus_out_event().connect (sigc::mem_fun (*this, &AutomationTextEntry::entry_focus_out))); +} + +void +AutomationTextEntry::on_realize () +{ + Gtk::Window::on_realize (); + get_window()->set_decorations (Gdk::WMDecoration (0)); + set_keep_above (true); +} + +bool +AutomationTextEntry::entry_focus_in (GdkEventFocus* ev) +{ + std::cerr << "focus in\n"; + entry.add_modal_grab (); + entry.select_region (0, -1); + return false; +} + +bool +AutomationTextEntry::entry_focus_out (GdkEventFocus* ev) +{ + std::cerr << "focus out\n"; + entry.remove_modal_grab (); + if (entry_changed) { + disconect_signals (); + use_text (entry.get_text (), 0); /* EMIT SIGNAL */ + entry_changed = false; + } + + idle_delete_self (); + return false; +} + +void +AutomationTextEntry::activate_entry () +{ + entry.add_modal_grab (); + entry.select_region (0, -1); +} + +bool +AutomationTextEntry::button_press (GdkEventButton* ev) +{ + if (Gtkmm2ext::event_inside_widget_window (*this, (GdkEvent*) ev)) { + activate_entry (); + return true; + } + + /* Clicked outside widget window - edit is done */ + entry.remove_modal_grab (); + + /* arrange re-propagation of the event once we go idle */ + Glib::signal_idle().connect (sigc::bind_return (sigc::bind (sigc::ptr_fun (gtk_main_do_event), gdk_event_copy ((GdkEvent*) ev)), false)); + + if (entry_changed) { + disconect_signals (); + use_text (entry.get_text (), 0); /* EMIT SIGNAL */ + entry_changed = false; + } + + idle_delete_self (); + + return false; +} + +void +AutomationTextEntry::activated () +{ + disconect_signals (); + use_text (entry.get_text(), 0); // EMIT SIGNAL + entry_changed = false; + idle_delete_self (); +} + +bool +AutomationTextEntry::key_press (GdkEventKey* ev) +{ + /* steal escape, tabs from GTK */ + + switch (ev->keyval) { + case GDK_Escape: + case GDK_ISO_Left_Tab: + case GDK_Tab: + return true; + } + + if (!ARDOUR_UI_UTILS::key_is_legal_for_numeric_entry (ev->keyval)) { + return true; + } + + return false; +} + +bool +AutomationTextEntry::key_release (GdkEventKey* ev) +{ + switch (ev->keyval) { + case GDK_Escape: + /* cancel edit */ + idle_delete_self (); + return true; + + case GDK_ISO_Left_Tab: + /* Shift+Tab Keys Pressed. Note that for Shift+Tab, GDK actually + * generates a different ev->keyval, rather than setting + * ev->state. + */ + disconect_signals (); + use_text (entry.get_text(), -1); // EMIT SIGNAL, move to prev + entry_changed = false; + idle_delete_self (); + return true; + + case GDK_Tab: + disconect_signals (); + use_text (entry.get_text(), 1); // EMIT SIGNAL, move to next + entry_changed = false; + idle_delete_self (); + return true; + + default: + break; + } + + return false; +} + +void +AutomationTextEntry::on_hide () +{ + entry.remove_modal_grab (); + + /* No hide button is shown (no decoration on the window), + * so being hidden is equivalent to the Escape key or any other + * method of cancelling the edit. + * + * This is also used during disconect_signals() before calling + * use_text (). see note below. + * + * If signals are already disconnected, idle-delete must be + * in progress already. + */ + if (!_connections.empty ()) { + idle_delete_self (); + } + Gtk::Window::on_hide (); +} + +void +AutomationTextEntry::disconect_signals () +{ + for (std::list::iterator i = _connections.begin(); i != _connections.end(); ++i) { + i->disconnect (); + } + _connections.clear (); + /* the entry is floating on-top, emitting use_text() + * may result in another dialog being shown (cannot rename track) + * which would + * - be stacked below the floating text entry + * - return focus to the entry when closedA + * so we hide the entry here. + */ + hide (); +} + +void +AutomationTextEntry::idle_delete_self () +{ + disconect_signals (); + delete_when_idle (this); +} diff --git a/gtk2_ardour/automation_text_entry.h b/gtk2_ardour/automation_text_entry.h new file mode 100644 index 0000000000..29d14e0780 --- /dev/null +++ b/gtk2_ardour/automation_text_entry.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2014-2016 Paul Davis + * Copyright (C) 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. + */ + +#pragma once + +#include +#include + +class AutomationTextEntry : public Gtk::Window +{ +public: + AutomationTextEntry (Gtk::Window* parent, std::string const& initial_contents); + ~AutomationTextEntry(); + + /* 1st argument to handler is the new text + * 2nd argument is 0, 1 or -1 to indicate: + * - do not move to next editable field + * - move to next editable field + * - move to previous editable field. + */ + sigc::signal2 use_text; + void delete_on_focus_out (); + sigc::signal1 going_away; + + /* grabs focus to be setup for editing */ + void activate_entry (); + +private: + Gtk::Entry entry; + Gtk::Label units; + bool entry_changed; + + /* handlers for Entry events */ + bool entry_focus_in (GdkEventFocus*); + bool entry_focus_out (GdkEventFocus*); + bool key_press (GdkEventKey*); + bool key_release (GdkEventKey*); + void activated (); + bool button_press (GdkEventButton*); + void changed (); + void idle_delete_self (); + void disconect_signals (); + + std::list _connections; + + /* handlers for window events */ + + void on_realize (); + void on_hide (); + + void split_units (std::string const &, std::string & numeric_part, std::string & units_part); +}; + diff --git a/gtk2_ardour/editing_context.cc b/gtk2_ardour/editing_context.cc index 0e5d735dca..116a5a92ce 100644 --- a/gtk2_ardour/editing_context.cc +++ b/gtk2_ardour/editing_context.cc @@ -316,6 +316,8 @@ EditingContext::register_automation_actions (Bindings* automation_bindings, std: reg_sens (_automation_actions, "move-points-earlier", _("Create Automation Point (at Playhead)"), sigc::mem_fun (*this, &EditingContext::automation_move_points_earlier)); reg_sens (_automation_actions, "raise-points", _("Create Automation Point (at Playhead)"), sigc::mem_fun (*this, &EditingContext::automation_raise_points)); reg_sens (_automation_actions, "lower-points", _("Create Automation Point (at Playhead)"), sigc::mem_fun (*this, &EditingContext::automation_lower_points)); + reg_sens (_automation_actions, "begin-edit", _("Open value entry window for automation editing"), sigc::mem_fun (*this, &EditingContext::automation_begin_edit)); + reg_sens (_automation_actions, "end-edit", _("Close value entry window for automation editing"), sigc::mem_fun (*this, &EditingContext::automation_end_edit)); disable_automation_bindings (); } diff --git a/gtk2_ardour/editing_context.h b/gtk2_ardour/editing_context.h index 558c504238..fc64ed2813 100644 --- a/gtk2_ardour/editing_context.h +++ b/gtk2_ardour/editing_context.h @@ -814,6 +814,8 @@ class EditingContext : public ARDOUR::SessionHandlePtr, public AxisViewProvider, virtual void automation_lower_points () {}; virtual void automation_move_points_later () {}; virtual void automation_move_points_earlier () {}; + virtual void automation_begin_edit () {}; + virtual void automation_end_edit () {}; bool temporary_zoom_focus_change; bool _dragging_playhead; diff --git a/gtk2_ardour/editor.h b/gtk2_ardour/editor.h index 3729088504..e4805dfae6 100644 --- a/gtk2_ardour/editor.h +++ b/gtk2_ardour/editor.h @@ -1599,6 +1599,8 @@ protected: void automation_lower_points (); void automation_move_points_later (); void automation_move_points_earlier (); + void automation_begin_edit (); + void automation_end_edit (); private: friend class DragManager; diff --git a/gtk2_ardour/editor_actions.cc b/gtk2_ardour/editor_actions.cc index 025007873d..048957bf8d 100644 --- a/gtk2_ardour/editor_actions.cc +++ b/gtk2_ardour/editor_actions.cc @@ -1350,6 +1350,30 @@ Editor::automation_create_point_at_edit_point () atv->line()->add (atv->control(), &event, where, atv->line()->the_list()->eval (where), false, true); } +void +Editor::automation_begin_edit () +{ + AutomationTimeAxisView* atv = dynamic_cast (entered_track); + + if (!atv) { + return; + } + + atv->line()->begin_edit (); +} + +void +Editor::automation_end_edit () +{ + AutomationTimeAxisView* atv = dynamic_cast (entered_track); + + if (!atv) { + return; + } + + atv->line()->end_edit (); +} + void Editor::automation_lower_points () { @@ -1365,6 +1389,8 @@ Editor::automation_lower_points () return; } + atv->line()->end_edit (); + begin_reversible_command (_("automation event lower")); add_command (new MementoCommand (atv->line()->memento_command_binder(), &atv->line()->the_list()->get_state(), 0)); atv->line()->the_list()->freeze (); @@ -1391,6 +1417,8 @@ Editor::automation_raise_points () return; } + atv->line()->end_edit (); + begin_reversible_command (_("automation event raise")); add_command (new MementoCommand (atv->line()->memento_command_binder(), &atv->line()->the_list()->get_state(), 0)); atv->line()->the_list()->freeze (); @@ -1417,6 +1445,8 @@ Editor::automation_move_points_later () return; } + atv->line()->end_edit (); + begin_reversible_command (_("automation points move later")); add_command (new MementoCommand (atv->line()->memento_command_binder(), &atv->line()->the_list()->get_state(), 0)); atv->line()->the_list()->freeze (); @@ -1445,6 +1475,8 @@ Editor::automation_move_points_earlier () return; } + atv->line()->end_edit (); + begin_reversible_command (_("automation points move earlier")); add_command (new MementoCommand (atv->line()->memento_command_binder(), &atv->line()->the_list()->get_state(), 0)); atv->line()->the_list()->freeze (); diff --git a/gtk2_ardour/wscript b/gtk2_ardour/wscript index 291c31d75c..7d4b854cc0 100644 --- a/gtk2_ardour/wscript +++ b/gtk2_ardour/wscript @@ -50,6 +50,7 @@ gtk2_ardour_sources = [ 'automation_line.cc', 'automation_region_view.cc', 'automation_streamview.cc', + 'automation_text_entry.cc', 'automation_time_axis.cc', 'axis_view.cc', # 'beatbox_gui.cc',