From 15258b9aa4073110a604bcf2834dbc5ecb5ec161 Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Wed, 11 Sep 2024 19:36:54 -0600 Subject: [PATCH] new base class for automation lines --- gtk2_ardour/automation_line_base.cc | 1522 +++++++++++++++++++++++++++ gtk2_ardour/automation_line_base.h | 261 +++++ 2 files changed, 1783 insertions(+) create mode 100644 gtk2_ardour/automation_line_base.cc create mode 100644 gtk2_ardour/automation_line_base.h diff --git a/gtk2_ardour/automation_line_base.cc b/gtk2_ardour/automation_line_base.cc new file mode 100644 index 0000000000..861c4454a7 --- /dev/null +++ b/gtk2_ardour/automation_line_base.cc @@ -0,0 +1,1522 @@ +/* + * Copyright (C) 2005-2017 Paul Davis + * Copyright (C) 2005 Karsten Wiese + * Copyright (C) 2005 Taybin Rutkin + * Copyright (C) 2006 Hans Fugal + * Copyright (C) 2007-2012 Carl Hetherington + * Copyright (C) 2007-2015 David Robillard + * Copyright (C) 2007 Doug McLain + * Copyright (C) 2013-2017 Robin Gareus + * Copyright (C) 2014-2016 Nick Mainsbridge + * + * 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 + +#ifdef COMPILER_MSVC +#include + +// 'std::isnan()' is not available in MSVC. +#define isnan_local(val) (bool)_isnan((double)val) +#else +#define isnan_local std::isnan +#endif + +#include +#include + +#include "boost/shared_ptr.hpp" + +#include "pbd/floating.h" +#include "pbd/memento_command.h" +#include "pbd/stl_delete.h" + +#include "ardour/automation_list.h" +#include "ardour/dB.h" +#include "ardour/debug.h" +#include "ardour/parameter_types.h" +#include "ardour/tempo.h" + +#include "temporal/range.h" + +#include "evoral/Curve.h" + +#include "canvas/debug.h" + +#include "automation_line_base.h" +#include "control_point.h" +#include "editing_context.h" +#include "gui_thread.h" +#include "rgb_macros.h" +#include "public_editor.h" +#include "selection.h" +#include "time_axis_view.h" +#include "point_selection.h" +#include "automation_time_axis.h" +#include "ui_config.h" + +#include "ardour/event_type_map.h" +#include "ardour/session.h" +#include "ardour/value_as_string.h" + +#include "pbd/i18n.h" + +using namespace std; +using namespace ARDOUR; +using namespace PBD; +using namespace Editing; +using namespace Temporal; + +#define SAMPLES_TO_TIME(x) (get_origin().distance (x)) + +/** @param converter A TimeConverter whose origin_b is the start time of the AutomationList in session samples. + * This will not be deleted by AutomationLine. + */ +AutomationLineBase::AutomationLineBase (const string& name, + EditingContext& ec, + ArdourCanvas::Item& parent, + ArdourCanvas::Rectangle* drag_base, + std::shared_ptr al, + const ParameterDescriptor& desc) + :_name (name) + , _height (0) + , _line_color ("automation line") + , _view_index_offset (0) + , alist (al) + , _visible (Line) + , terminal_points_can_slide (true) + , update_pending (false) + , have_reset_timeout (false) + , no_draw (false) + , _is_boolean (false) + , editing_context (ec) + , _parent_group (parent) + , _drag_base (drag_base) + , _offset (0) + , _maximum_time (timepos_t::max (al->time_domain())) + , _fill (false) + , _desc (desc) +{ + group = new ArdourCanvas::Container (&parent, ArdourCanvas::Duple(0, 1.5)); + CANVAS_DEBUG_NAME (group, "region gain envelope group"); + + line = new ArdourCanvas::PolyLine (group); + CANVAS_DEBUG_NAME (line, "region gain envelope line"); + line->set_data ("line", this); + line->set_data ("drag-base", _drag_base); + line->set_outline_width (2.0); + line->set_covers_threshold (4.0); + + line->Event.connect (sigc::mem_fun (*this, &AutomationLineBase::event_handler)); + + editing_context.session()->register_with_memento_command_factory(alist->id(), this); + + interpolation_changed (alist->interpolation ()); + + connect_to_list (); +} + +AutomationLineBase::~AutomationLineBase () +{ + delete group; // deletes child items + + for (std::vector::iterator i = control_points.begin(); i != control_points.end(); i++) { + (*i)->unset_item (); + delete *i; + } + control_points.clear (); +} + +timepos_t +AutomationLineBase::get_origin() const +{ + /* this is the default for all non-derived AutomationLine classes: the + origin is zero, in whatever time domain the list we represent uses. + */ + return timepos_t (the_list()->time_domain()); +} + +bool +AutomationLineBase::is_stepped() const +{ + return (_desc.toggled || (alist && alist->interpolation() == AutomationList::Discrete)); +} + +void +AutomationLineBase::update_visibility () +{ + if (_visible & Line) { + /* Only show the line when there are some points, otherwise we may show an out-of-date line + when automation points have been removed (the line will still follow the shape of the + old points). + */ + if (line_points.size() >= 2) { + line->show(); + } else { + line->hide (); + } + + if (_visible & ControlPoints) { + for (auto & cp : control_points) { + cp->show (); + } + } else if (_visible & SelectedControlPoints) { + for (auto & cp : control_points) { + if (cp->selected()) { + cp->show (); + } else { + cp->hide (); + } + } + } else { + for (auto & cp : control_points) { + cp->hide (); + } + } + + } else { + line->hide (); + for (auto & cp : control_points) { + if (_visible & ControlPoints) { + cp->show (); + } else { + cp->hide (); + } + } + } +} + +bool +AutomationLineBase::get_uses_gain_mapping () const +{ + switch (_desc.type) { + case GainAutomation: + case BusSendLevel: + case EnvelopeAutomation: + case TrimAutomation: + case SurroundSendLevel: + case InsertReturnLevel: + return true; + default: + return false; + } +} + +void +AutomationLineBase::hide () +{ + /* leave control points setting unchanged, we are just hiding the + overall line + */ + + set_visibility (AutomationLineBase::VisibleAspects (_visible & ~Line)); +} + +double +AutomationLineBase::control_point_box_size () +{ + float uiscale = UIConfiguration::instance().get_ui_scale(); + uiscale = std::max (1.f, powf (uiscale, 1.71)); + + if (_height > TimeAxisView::preset_height (HeightLarger)) { + return rint (8.0 * uiscale); + } else if (_height > (guint32) TimeAxisView::preset_height (HeightNormal)) { + return rint (6.0 * uiscale); + } + return rint (4.0 * uiscale); +} + +void +AutomationLineBase::set_height (guint32 h) +{ + if (h != _height) { + _height = h; + + double bsz = control_point_box_size(); + + for (vector::iterator i = control_points.begin(); i != control_points.end(); ++i) { + (*i)->set_size (bsz); + } + + if (_fill) { + line->set_fill_y1 (_height); + } else { + line->set_fill_y1 (0); + } + reset (); + } +} + +void +AutomationLineBase::set_line_color (string color_name, std::string color_mod) +{ + _line_color = color_name; + _line_color_mod = color_mod; + + uint32_t color = UIConfiguration::instance().color (color_name); + line->set_outline_color (color); + + Gtkmm2ext::SVAModifier mod = UIConfiguration::instance().modifier (color_mod.empty () ? "automation line fill" : color_mod); + + line->set_fill_color ((color & 0xffffff00) + mod.a() * 255); +} + +uint32_t +AutomationLineBase::get_line_color() const +{ + return UIConfiguration::instance().color (_line_color); +} + +ControlPoint* +AutomationLineBase::nth (uint32_t n) +{ + if (n < control_points.size()) { + return control_points[n]; + } else { + return 0; + } +} + +ControlPoint const * +AutomationLineBase::nth (uint32_t n) const +{ + if (n < control_points.size()) { + return control_points[n]; + } else { + return 0; + } +} + +void +AutomationLineBase::modify_points_y (std::vector const& cps, double y) +{ + /* clamp y-coord appropriately. y is supposed to be a normalized fraction (0.0-1.0), + and needs to be converted to a canvas unit distance. + */ + + y = max (0.0, y); + y = min (1.0, y); + y = _height - (y * _height); + + editing_context.begin_reversible_command (_("automation event move")); + editing_context.session()->add_command ( + new MementoCommand (memento_command_binder(), &get_state(), 0)); + + alist->freeze (); + for (auto const& cp : cps) { + cp->move_to (cp->get_x(), y, ControlPoint::Full); + sync_model_with_view_point (*cp); + } + alist->thaw (); + + for (auto const& cp : cps) { + reset_line_coords (*cp); + } + + if (line_points.size() > 1) { + line->set_steps (line_points, is_stepped()); + } + + update_pending = false; + + editing_context.session()->add_command ( + new MementoCommand (memento_command_binder(), 0, &alist->get_state())); + + editing_context.commit_reversible_command (); + editing_context.session()->set_dirty (); +} + +void +AutomationLineBase::reset_line_coords (ControlPoint& cp) +{ + if (cp.view_index() < line_points.size()) { + line_points[cp.view_index() + _view_index_offset].x = cp.get_x (); + line_points[cp.view_index() + _view_index_offset].y = cp.get_y (); + } +} + +bool +AutomationLineBase::sync_model_with_view_points (list cp) +{ + update_pending = true; + + bool moved = false; + for (auto const & vp : cp) { + moved = sync_model_with_view_point (*vp) || moved; + } + + return moved; +} + +string +AutomationLineBase::get_verbose_cursor_string (double fraction) const +{ + return fraction_to_string (fraction); +} + +string +AutomationLineBase::get_verbose_cursor_relative_string (double fraction, double delta) const +{ + std::string s = fraction_to_string (fraction); + std::string d = delta_to_string (delta); + return s + " (" + d + ")"; +} + +/** + * @param fraction y fraction + * @return string representation of this value, using dB if appropriate. + */ +string +AutomationLineBase::fraction_to_string (double fraction) const +{ + view_to_model_coord_y (fraction); + return ARDOUR::value_as_string (_desc, fraction); +} + +string +AutomationLineBase::delta_to_string (double delta) const +{ + if (!get_uses_gain_mapping () && _desc.logarithmic) { + return "x " + ARDOUR::value_as_string (_desc, delta); + } else { + return u8"\u0394 " + ARDOUR::value_as_string (_desc, delta); + } +} + +/** + * @param s Value string in the form as returned by fraction_to_string. + * @return Corresponding y fraction. + */ +double +AutomationLineBase::string_to_fraction (string const & s) const +{ + double v; + sscanf (s.c_str(), "%lf", &v); + + switch (_desc.type) { + case GainAutomation: + case BusSendLevel: + case EnvelopeAutomation: + case TrimAutomation: + case SurroundSendLevel: + case InsertReturnLevel: + if (s == "-inf") { /* translation */ + v = 0; + } else { + v = dB_to_coefficient (v); + } + break; + default: + break; + } + return model_to_view_coord_y (v); +} + +/** Start dragging a single point, possibly adding others if the supplied point is selected and there + * are other selected points. + * + * @param cp Point to drag. + * @param x Initial x position (units). + * @param fraction Initial y position (as a fraction of the track height, where 0 is the bottom and 1 the top) + */ +void +AutomationLineBase::start_drag_single (ControlPoint* cp, double x, float fraction) +{ + editing_context.session()->add_command ( + new MementoCommand (memento_command_binder(), &get_state(), 0)); + + _drag_points.clear (); + _drag_points.push_back (cp); + + if (cp->selected ()) { + for (vector::iterator i = control_points.begin(); i != control_points.end(); ++i) { + if (*i != cp && (*i)->selected()) { + _drag_points.push_back (*i); + } + } + } + + start_drag_common (x, fraction); +} + +/** Start dragging a line vertically (with no change in x) + * @param i1 Control point index of the `left' point on the line. + * @param i2 Control point index of the `right' point on the line. + * @param fraction Initial y position (as a fraction of the track height, where 0 is the bottom and 1 the top) + */ +void +AutomationLineBase::start_drag_line (uint32_t i1, uint32_t i2, float fraction) +{ + editing_context.session()->add_command ( + new MementoCommand (memento_command_binder (), &get_state(), 0)); + + _drag_points.clear (); + + for (uint32_t i = i1; i <= i2; i++) { + _drag_points.push_back (nth (i)); + } + + start_drag_common (0, fraction); +} + +/** Start dragging multiple points (with no change in x) + * @param cp Points to drag. + * @param fraction Initial y position (as a fraction of the track height, where 0 is the bottom and 1 the top) + */ +void +AutomationLineBase::start_drag_multiple (list cp, float fraction, XMLNode* state) +{ + editing_context.session()->add_command ( + new MementoCommand (memento_command_binder(), state, 0)); + + _drag_points = cp; + start_drag_common (0, fraction); +} + +struct ControlPointSorter +{ + bool operator() (ControlPoint const * a, ControlPoint const * b) const { + if (floateq (a->get_x(), b->get_x(), 1)) { + return a->view_index() < b->view_index(); + } + return a->get_x() < b->get_x(); + } +}; + +AutomationLineBase::ContiguousControlPoints::ContiguousControlPoints (AutomationLineBase& al) + : line (al), before_x (timepos_t (line.the_list()->time_domain())), after_x (timepos_t::max (line.the_list()->time_domain())) +{ +} + +void +AutomationLineBase::ContiguousControlPoints::compute_x_bounds () +{ + uint32_t sz = size(); + + if (sz > 0 && sz < line.npoints()) { + const TempoMap::SharedPtr map (TempoMap::use()); + + /* determine the limits on x-axis motion for this + contiguous range of control points + */ + + if (front()->view_index() > 0) { + before_x = (*line.nth (front()->view_index() - 1)->model())->when; + before_x += timepos_t (64); + } + + /* if our last point has a point after it in the line, + we have an "after" bound + */ + + if (back()->view_index() < (line.npoints() - 1)) { + after_x = (*line.nth (back()->view_index() + 1)->model())->when; + after_x.shift_earlier (timepos_t (64)); + } + } +} + +Temporal::timecnt_t +AutomationLineBase::ContiguousControlPoints::clamp_dt (timecnt_t const & dt, timepos_t const & line_limit) +{ + if (empty()) { + return dt; + } + + /* get the maximum distance we can move any of these points along the x-axis + */ + + ControlPoint* reference_point; + + if (dt.magnitude() > 0) { + /* check the last point, since we're moving later in time */ + reference_point = back(); + } else { + /* check the first point, since we're moving earlier in time */ + reference_point = front(); + } + + /* possible position the "reference" point would move to, given dx */ + Temporal::timepos_t possible_pos = (*reference_point->model())->when + dt; // new possible position if we just add the motion + + /* Now clamp that position so that: + * + * - it is not before the origin (zero) + * - it is not beyond the line's own limit (e.g. for region automation) + * - it is not before the preceding point + * - it is not after the following point + */ + + possible_pos = max (possible_pos, Temporal::timepos_t (possible_pos.time_domain())); + possible_pos = min (possible_pos, line_limit); + + possible_pos = max (possible_pos, before_x); // can't move later than following point + possible_pos = min (possible_pos, after_x); // can't move earlier than preceding point + + return (*reference_point->model())->when.distance (possible_pos); +} + +void +AutomationLineBase::ContiguousControlPoints::move (timecnt_t const & dt, double dvalue) +{ + for (auto & cp : *this) { + // compute y-axis delta + double view_y = 1.0 - cp->get_y() / line.height(); + line.view_to_model_coord_y (view_y); + line.apply_delta (view_y, dvalue); + view_y = line.model_to_view_coord_y (view_y); + view_y = (1.0 - view_y) * line.height(); + + cp->move_to (line.dt_to_dx ((*cp->model())->when, dt), view_y, ControlPoint::Full); + line.reset_line_coords (*cp); + } +} + +/** Common parts of starting a drag. + * @param x Starting x position in units, or 0 if x is being ignored. + * @param fraction Starting y position (as a fraction of the track height, where 0 is the bottom and 1 the top) + */ +void +AutomationLineBase::start_drag_common (double x, float fraction) +{ + _last_drag_fraction = fraction; + _drag_had_movement = false; + did_push = false; + + /* they are probably ordered already, but we have to make sure */ + + _drag_points.sort (ControlPointSorter()); +} + +/** Takes a relative-to-origin position, moves it by dt, and returns a + * relative-to-origin pixel count. + */ +double +AutomationLineBase::dt_to_dx (timepos_t const & pos, timecnt_t const & dt) +{ + /* convert a shift of pos by dt into an absolute timepos */ + timepos_t const new_pos ((pos + dt + get_origin()).shift_earlier (offset())); + /* convert to pixels */ + double px = editing_context.time_to_pixel_unrounded (new_pos); + /* convert back to pixels-relative-to-origin */ + px -= editing_context.time_to_pixel_unrounded (get_origin()); + return px; +} + +/** Should be called to indicate motion during a drag. + * @param x New x position of the drag in canvas units relative to origin, or undefined if ignore_x == true. + * @param fraction New y fraction. + * @return x position and y fraction that were actually used (once clamped). + */ +pair +AutomationLineBase::drag_motion (timecnt_t const & pdt, float fraction, bool ignore_x, bool with_push, uint32_t& final_index) +{ + if (_drag_points.empty()) { + return pair (fraction, _desc.is_linear () ? 0 : 1); + } + + timecnt_t dt (pdt); + + if (ignore_x) { + dt = timecnt_t (pdt.time_domain()); + } + + double dy = fraction - _last_drag_fraction; + + if (!_drag_had_movement) { + + /* "first move" ... do some stuff that we don't want to do if + no motion ever took place, but need to do before we handle + motion. + */ + + /* partition the points we are dragging into (potentially several) + * set(s) of contiguous points. this will not happen with a normal + * drag, but if the user does a discontiguous selection, it can. + */ + + uint32_t expected_view_index = 0; + CCP contig; + + for (list::iterator i = _drag_points.begin(); i != _drag_points.end(); ++i) { + if (i == _drag_points.begin() || (*i)->view_index() != expected_view_index) { + contig.reset (new ContiguousControlPoints (*this)); + contiguous_points.push_back (contig); + } + contig->push_back (*i); + expected_view_index = (*i)->view_index() + 1; + } + + if (contiguous_points.back()->empty()) { + contiguous_points.pop_back (); + } + + for (auto const & ccp : contiguous_points) { + ccp->compute_x_bounds (); + } + _drag_had_movement = true; + } + + /* OK, now on to the stuff related to *this* motion event. First, for + * each contiguous range, figure out the maximum x-axis motion we are + * allowed (because of neighbouring points that are not moving. + * + * if we are moving forwards with push, we don't need to do this, + * since all later points will move too. + */ + + if (dt.is_negative() || (dt.is_positive() && !with_push)) { + const timepos_t line_limit = get_origin() + maximum_time() + _offset; + for (auto const & ccp : contiguous_points){ + dt = ccp->clamp_dt (dt, line_limit); + } + } + + /* compute deflection */ + double delta_value; + { + double value0 = _last_drag_fraction; + double value1 = _last_drag_fraction + dy; + view_to_model_coord_y (value0); + view_to_model_coord_y (value1); + delta_value = compute_delta (value0, value1); + } + + /* special case -inf */ + if (delta_value == 0 && dy > 0 && !_desc.is_linear ()) { + assert (_desc.lower == 0); + delta_value = 1.0; + } + + /* clamp y */ + for (list::iterator i = _drag_points.begin(); i != _drag_points.end(); ++i) { + double vy = 1.0 - (*i)->get_y() / _height; + view_to_model_coord_y (vy); + const double orig = vy; + apply_delta (vy, delta_value); + if (vy < _desc.lower) { + delta_value = compute_delta (orig, _desc.lower); + } + if (vy > _desc.upper) { + delta_value = compute_delta (orig, _desc.upper); + } + } + + if (!dt.is_zero() || dy) { + /* and now move each section */ + + + for (vector::iterator ccp = contiguous_points.begin(); ccp != contiguous_points.end(); ++ccp) { + (*ccp)->move (dt, delta_value); + } + + if (with_push) { + final_index = contiguous_points.back()->back()->view_index () + 1; + ControlPoint* p; + uint32_t i = final_index; + + while ((p = nth (i)) != 0 && p->can_slide()) { + + p->move_to (dt_to_dx ((*p->model())->when, dt), p->get_y(), ControlPoint::Full); + reset_line_coords (*p); + ++i; + } + } + + /* update actual line coordinates (will queue a redraw) */ + + if (line_points.size() > 1) { + line->set_steps (line_points, is_stepped()); + } + } + + /* calculate effective delta */ + ControlPoint* cp = _drag_points.front(); + double vy = 1.0 - cp->get_y() / (double)_height; + view_to_model_coord_y (vy); + float val = (*(cp->model ()))->value; + float effective_delta = _desc.compute_delta (val, vy); + /* special case recovery from -inf */ + if (val == 0 && effective_delta == 0 && vy > 0) { + assert (!_desc.is_linear ()); + effective_delta = HUGE_VAL; // +Infinity + } + + double const result_frac = _last_drag_fraction + dy; + _last_drag_fraction = result_frac; + did_push = with_push; + + return pair (result_frac, effective_delta); +} + +/** Should be called to indicate the end of a drag */ +void +AutomationLineBase::end_drag (bool with_push, uint32_t final_index) +{ + if (!_drag_had_movement) { + return; + } + + alist->freeze (); + bool moved = sync_model_with_view_points (_drag_points); + + if (with_push) { + ControlPoint* p; + uint32_t i = final_index; + while ((p = nth (i)) != 0 && p->can_slide()) { + moved = sync_model_with_view_point (*p) || moved; + ++i; + } + } + + alist->thaw (); + + update_pending = false; + + if (moved) { + /* A point has moved as a result of sync (clamped to integer or boolean + value), update line accordingly. */ + line->set_steps (line_points, is_stepped()); + } + + editing_context.session()->add_command ( + new MementoCommand(memento_command_binder (), 0, &alist->get_state())); + + editing_context.session()->set_dirty (); + did_push = false; + + contiguous_points.clear (); +} + +/** + * + * get model coordinates synced with (possibly changed) view coordinates. + * + * For example, we call this in ::end_drag(), when we have probably moved a + * point in the view, and now want to "push" that change back into the + * corresponding model point. + */ +bool +AutomationLineBase::sync_model_with_view_point (ControlPoint& cp) +{ + /* find out where the visual control point is. + * ControlPoint uses canvas-units. The origin + * is the RegionView's top-left corner. + */ + double view_x = cp.get_x(); + + /* model time is relative to the Region (regardless of region->start offset) */ + timepos_t model_time = (*cp.model())->when; + + const timepos_t origin (get_origin()); + + /* convert to absolute time on timeline */ + const timepos_t absolute_time = model_time + origin; + + /* now convert to pixels relative to start of region, which matches view_x */ + const double model_x = editing_context.time_to_pixel_unrounded (absolute_time) - editing_context.time_to_pixel_unrounded (origin); + + if (view_x != model_x) { + + /* convert the current position in the view (units: + * region-relative pixels) into samples, then use that to + * create a timecnt_t that measures the distance from the + * origin for this line. + * + * Note that the offset and origin is irrelevant here, + * pixel_to_sample() islinear only depending on zoom level. + */ + + const timepos_t view_samples (editing_context.pixel_to_sample (view_x)); + + /* measure distance from RegionView origin (this preserves time domain) */ + + if (model_time.time_domain() == Temporal::AudioTime) { + model_time = timepos_t (timecnt_t (view_samples, origin).samples()); + } else { + model_time = timepos_t (timecnt_t (view_samples, origin).beats()); + } + + /* convert RegionView to Region position (account for region->start() _offset) */ + model_time += _offset; + } + + update_pending = true; + + double view_y = 1.0 - cp.get_y() / (double)_height; + view_to_model_coord_y (view_y); + + alist->modify (cp.model(), model_time, view_y); + + /* convert back from model to view y for clamping position (for integer/boolean/etc) */ + view_y = model_to_view_coord_y (view_y); + const double point_y = _height - (view_y * _height); + if (point_y != cp.get_y()) { + cp.move_to (cp.get_x(), point_y, ControlPoint::Full); + reset_line_coords (cp); + return true; + } + + return false; +} + +bool +AutomationLineBase::control_points_adjacent (double xval, uint32_t & before, uint32_t& after) +{ + ControlPoint *bcp = 0; + ControlPoint *acp = 0; + double unit_xval; + + unit_xval = editing_context.sample_to_pixel_unrounded (xval); + + for (vector::iterator i = control_points.begin(); i != control_points.end(); ++i) { + + if ((*i)->get_x() <= unit_xval) { + + if (!bcp || (*i)->get_x() > bcp->get_x()) { + bcp = *i; + before = bcp->view_index(); + } + + } else if ((*i)->get_x() > unit_xval) { + acp = *i; + after = acp->view_index(); + break; + } + } + + return bcp && acp; +} + +bool +AutomationLineBase::is_last_point (ControlPoint& cp) +{ + // If the list is not empty, and the point is the last point in the list + + if (alist->empty()) { + return false; + } + + AutomationList::const_iterator i = alist->end(); + --i; + + if (cp.model() == i) { + return true; + } + + return false; +} + +bool +AutomationLineBase::is_first_point (ControlPoint& cp) +{ + // If the list is not empty, and the point is the first point in the list + + if (!alist->empty() && cp.model() == alist->begin()) { + return true; + } + + return false; +} + +// This is copied into AudioRegionGainLine +void +AutomationLineBase::remove_point (ControlPoint& cp) +{ + editing_context.begin_reversible_command (_("remove control point")); + XMLNode &before = alist->get_state(); + + editing_context.get_selection ().clear_points (); + alist->erase (cp.model()); + + editing_context.session()->add_command( + new MementoCommand (memento_command_binder (), &before, &alist->get_state())); + + editing_context.commit_reversible_command (); + editing_context.session()->set_dirty (); +} + +/** Get selectable points within an area. + * @param start Start position in session samples. + * @param end End position in session samples. + * @param bot Bottom y range, as a fraction of line height, where 0 is the bottom of the line. + * @param top Top y range, as a fraction of line height, where 0 is the bottom of the line. + * @param result Filled in with selectable things; in this case, ControlPoints. + */ +void +AutomationLineBase::get_selectables (timepos_t const & start, timepos_t const & end, double botfrac, double topfrac, list& results) +{ + /* convert fractions to display coordinates with 0 at the top of the track */ + double const bot_track = (1 - topfrac) * _height; // this should StreamView::child_height () for RegionGain + double const top_track = (1 - botfrac) * _height; // --"-- + + for (auto const & cp : control_points) { + + const timepos_t w = session_position ((*cp->model())->when); + + if (w >= start && w <= end && cp->get_y() >= bot_track && cp->get_y() <= top_track) { + results.push_back (cp); + } + } +} + +void +AutomationLineBase::get_inverted_selectables (Selection&, list& /*results*/) +{ + // hmmm .... +} + +void +AutomationLineBase::set_selected_points (PointSelection const & points) +{ + for (vector::iterator i = control_points.begin(); i != control_points.end(); ++i) { + (*i)->set_selected (false); + } + + for (PointSelection::const_iterator i = points.begin(); i != points.end(); ++i) { + (*i)->set_selected (true); + } + + if (points.empty()) { + remove_visibility (SelectedControlPoints); + } else { + add_visibility (SelectedControlPoints); + } + + set_colors (); +} + +void +AutomationLineBase::set_colors () +{ + set_line_color (_line_color, _line_color_mod); + for (vector::iterator i = control_points.begin(); i != control_points.end(); ++i) { + (*i)->set_color (); + } +} + +void +AutomationLineBase::list_changed () +{ + DEBUG_TRACE (DEBUG::Automation, string_compose ("\tline changed, existing update pending? %1\n", update_pending)); + + if (!update_pending) { + update_pending = true; + Gtkmm2ext::UI::instance()->call_slot (invalidator (*this), boost::bind (&AutomationLineBase::queue_reset, this)); + } +} + +void +AutomationLineBase::tempo_map_changed () +{ + if (alist->time_domain() != Temporal::BeatTime) { + return; + } + + reset (); +} + +void +AutomationLineBase::reset_callback (const Evoral::ControlList& events) +{ + uint32_t vp = 0; + uint32_t pi = 0; + uint32_t np; + + if (events.empty()) { + for (vector::iterator i = control_points.begin(); i != control_points.end(); ++i) { + delete *i; + } + control_points.clear (); + line->hide(); + line_points.clear (); + return; + } + + /* hide all existing points, and the line */ + + for (vector::iterator i = control_points.begin(); i != control_points.end(); ++i) { + (*i)->hide(); + } + + line->hide (); + np = events.size(); + + Evoral::ControlList& e (const_cast (events)); + AutomationList::iterator preceding (e.end()); + AutomationList::iterator following (e.end()); + + for (AutomationList::iterator ai = e.begin(); ai != e.end(); ++ai, ++pi) { + + /* drop points outside our range */ + + if (((*ai)->when < _offset)) { + preceding = ai; + continue; + } + + if ((*ai)->when >= _offset + _maximum_time) { + following = ai; + break; + } + + double ty = model_to_view_coord_y ((*ai)->value); + + if (isnan_local (ty)) { + warning << string_compose (_("Ignoring illegal points on AutomationLine \"%1\""), _name) << endmsg; + continue; + } + + /* convert from canonical view height (0..1.0) to actual + * height coordinates (using X11's top-left rooted system) + */ + + ty = _height - (ty * _height); + + /* tx is currently the distance of this point from + * _offset, which may be either: + * + * a) zero, for an automation line not connected to a + * region + * + * b) some non-zero value, corresponding to the start + * of the region within its source(s). Remember that + * this start is an offset within the source, not a + * position on the timeline. + * + * We need to convert tx to a global position, and to + * do that we need to measure the distance from the + * result of get_origin(), which tells ut the timeline + * position of _offset + */ + + timecnt_t tx = model_to_view_coord_x ((*ai)->when); + + /* convert x-coordinate to a canvas unit coordinate (this takes + * zoom and scroll into account). + */ + + double px = editing_context.duration_to_pixels_unrounded (tx); + add_visible_control_point (vp, pi, px, ty, ai, np); + vp++; + } + + /* discard extra CP's to avoid confusing ourselves */ + + while (control_points.size() > vp) { + ControlPoint* cp = control_points.back(); + control_points.pop_back (); + delete cp; + } + + if (!terminal_points_can_slide && !control_points.empty()) { + control_points.back()->set_can_slide(false); + } + + if (vp) { + + /* reset the line coordinates given to the CanvasLine */ + + /* 2 extra in case we need hidden points for line start and end */ + + line_points.resize (vp + 2, ArdourCanvas::Duple (0, 0)); + + ArdourCanvas::Points::size_type n = 0; + + /* potentially insert front hidden (line) point to make the line draw from + * zero to the first actual point + */ + + _view_index_offset = 0; + + if (control_points[0]->get_x() != 0 && preceding != e.end()) { + double ty = model_to_view_coord_y (e.unlocked_eval (_offset)); + + if (isnan_local (ty)) { + warning << string_compose (_("Ignoring illegal points on AutomationLine \"%1\""), _name) << endmsg; + + + } else { + line_points[n].y = _height - (ty * _height); + line_points[n].x = 0; + _view_index_offset = 1; + ++n; + } + } + + for (auto const & cp : control_points) { + line_points[n].x = cp->get_x(); + line_points[n].y = cp->get_y(); + ++n; + } + + /* potentially insert final hidden (line) point to make the line draw + * from the last point to the very end + */ + + double px = editing_context.duration_to_pixels_unrounded (model_to_view_coord_x (_offset + _maximum_time)); + + if (control_points[control_points.size() - 1]->get_x() != px && following != e.end()) { + double ty = model_to_view_coord_y (e.unlocked_eval (_offset + _maximum_time)); + + if (isnan_local (ty)) { + warning << string_compose (_("Ignoring illegal points on AutomationLine \"%1\""), _name) << endmsg; + + + } else { + line_points[n].y = _height - (ty * _height); + line_points[n].x = px; + ++n; + } + } + + line_points.resize (n); + line->set_steps (line_points, is_stepped()); + + update_visibility (); + } + + set_selected_points (editing_context.get_selection().points); +} + +void +AutomationLineBase::reset () +{ + DEBUG_TRACE (DEBUG::Automation, "\t\tLINE RESET\n"); + update_pending = false; + have_reset_timeout = false; + + if (no_draw) { + return; + } + + /* TODO: abort any drags in progress, e.g. dragging points while writing automation + * (the control-point model, used by AutomationLineBase::drag_motion, will be invalid). + * + * Note: reset() may also be called from an aborted drag (LineDrag::aborted) + * maybe abort in list_changed(), interpolation_changed() and ... ? + * XXX + */ + + alist->apply_to_points (*this, &AutomationLineBase::reset_callback); +} + +void +AutomationLineBase::queue_reset () +{ + /* this must be called from the GUI thread */ + + if (editing_context.session()->transport_rolling() && alist->automation_write()) { + /* automation write pass ... defer to a timeout */ + /* redraw in 1/4 second */ + if (!have_reset_timeout) { + DEBUG_TRACE (DEBUG::Automation, "\tqueue timeout\n"); + Glib::signal_timeout().connect (sigc::bind_return (sigc::mem_fun (*this, &AutomationLineBase::reset), false), 250); + have_reset_timeout = true; + } else { + DEBUG_TRACE (DEBUG::Automation, "\ttimeout already queued, change ignored\n"); + } + } else { + reset (); + } +} + +void +AutomationLineBase::clear () +{ + /* parent must create and commit command */ + XMLNode &before = alist->get_state(); + alist->clear(); + + editing_context.session()->add_command ( + new MementoCommand (memento_command_binder (), &before, &alist->get_state())); +} + +void +AutomationLineBase::set_list (std::shared_ptr list) +{ + alist = list; + queue_reset (); + connect_to_list (); +} + +void +AutomationLineBase::add_visibility (VisibleAspects va) +{ + VisibleAspects old = _visible; + + _visible = VisibleAspects (_visible | va); + + if (old != _visible) { + update_visibility (); + } +} + +void +AutomationLineBase::set_visibility (VisibleAspects va) +{ + if (_visible != va) { + _visible = va; + update_visibility (); + } +} + +void +AutomationLineBase::remove_visibility (VisibleAspects va) +{ + VisibleAspects old = _visible; + + _visible = VisibleAspects (_visible & ~va); + + if (old != _visible) { + update_visibility (); + } +} + +void +AutomationLineBase::track_entered() +{ + add_visibility (ControlPoints); +} + +void +AutomationLineBase::track_exited() +{ + remove_visibility (ControlPoints); +} + +XMLNode & +AutomationLineBase::get_state () const +{ + /* function as a proxy for the model */ + return alist->get_state(); +} + +int +AutomationLineBase::set_state (const XMLNode &node, int version) +{ + /* function as a proxy for the model */ + return alist->set_state (node, version); +} + +void +AutomationLineBase::view_to_model_coord_y (double& y) const +{ + if (alist->default_interpolation () != alist->interpolation()) { + switch (alist->interpolation()) { + case AutomationList::Discrete: + /* toggles and MIDI only -- see is_stepped() */ + assert (alist->default_interpolation () == AutomationList::Linear); + break; + case AutomationList::Linear: + y = y * (_desc.upper - _desc.lower) + _desc.lower; + return; + default: + /* types that default to linear, can't be use + * Logarithmic or Exponential interpolation. + * "Curved" is invalid for automation (only x-fads) + */ + assert (0); + break; + } + } + y = _desc.from_interface (y); +} + +double +AutomationLineBase::compute_delta (double from, double to) const +{ + return _desc.compute_delta (from, to); +} + +void +AutomationLineBase::apply_delta (double& val, double delta) const +{ + if (val == 0 && !_desc.is_linear () && delta >= 1.0) { + /* recover from -inf */ + val = 1.0 / _height; + view_to_model_coord_y (val); + return; + } + val = _desc.apply_delta (val, delta); +} + +double +AutomationLineBase::model_to_view_coord_y (double y) const +{ + if (alist->default_interpolation () != alist->interpolation()) { + switch (alist->interpolation()) { + case AutomationList::Discrete: + /* toggles and MIDI only -- see is_stepped */ + assert (alist->default_interpolation () == AutomationList::Linear); + break; + case AutomationList::Linear: + return (y - _desc.lower) / (_desc.upper - _desc.lower); + default: + /* types that default to linear, can't be use + * Logarithmic or Exponential interpolation. + * "Curved" is invalid for automation (only x-fads) + */ + assert (0); + break; + } + } + return _desc.to_interface (y); +} + +timecnt_t +AutomationLineBase::model_to_view_coord_x (timepos_t const & when) const +{ + /* @param when is a distance (with implicit origin) from the start of the + * source. So we subtract the offset (from the region if this is + * related to a region; zero otherwise) to get the distance (again, + * implicit origin) from the start of the line. + * + * Then we construct a timecnt_t from this duration, and the origin of + * the line on the timeline. + */ + + return timecnt_t (when.earlier (_offset), get_origin()); +} + +/** Called when our list has announced that its interpolation style has changed */ +void +AutomationLineBase::interpolation_changed (AutomationList::InterpolationStyle style) +{ + if (line_points.size() > 1) { + reset (); + line->set_steps(line_points, is_stepped()); + } +} + +void +AutomationLineBase::add_visible_control_point (uint32_t view_index, uint32_t pi, double tx, double ty, + AutomationList::iterator model, uint32_t npoints) +{ + ControlPoint::ShapeType shape; + + if (view_index >= control_points.size()) { + + /* make sure we have enough control points */ + + ControlPoint* ncp = new ControlPoint (*this); + ncp->set_size (control_point_box_size ()); + + control_points.push_back (ncp); + } + + if (!terminal_points_can_slide) { + if (pi == 0) { + control_points[view_index]->set_can_slide (false); + if (tx == 0) { + shape = ControlPoint::Start; + } else { + shape = ControlPoint::Full; + } + } else if (pi == npoints - 1) { + control_points[view_index]->set_can_slide (false); + shape = ControlPoint::End; + } else { + control_points[view_index]->set_can_slide (true); + shape = ControlPoint::Full; + } + } else { + control_points[view_index]->set_can_slide (true); + shape = ControlPoint::Full; + } + + control_points[view_index]->reset (tx, ty, model, view_index, shape); + + /* finally, control visibility */ + + if (_visible & ControlPoints) { + control_points[view_index]->show (); + } else { + control_points[view_index]->hide (); + } +} + +void +AutomationLineBase::dump (std::ostream& ostr) const +{ + for (auto const & cp : control_points) { + if (cp->model() != alist->end()) { + ostr << '#' << cp->view_index() << " @ " << cp->get_x() << ", " << cp->get_y() << " for " << (*cp->model())->value << " @ " << (*(cp->model()))->when << std::endl; + } else { + ostr << "dead point\n"; + } + } +} + +void +AutomationLineBase::connect_to_list () +{ + _list_connections.drop_connections (); + + alist->StateChanged.connect (_list_connections, invalidator (*this), boost::bind (&AutomationLineBase::list_changed, this), gui_context()); + + alist->InterpolationChanged.connect ( + _list_connections, invalidator (*this), boost::bind (&AutomationLineBase::interpolation_changed, this, _1), gui_context()); +} + +MementoCommandBinder* +AutomationLineBase::memento_command_binder () +{ + return new SimpleMementoCommandBinder (*alist.get()); +} + +/** Set the maximum time that points on this line can be at, relative + * to the start of the track or region that it is on. + */ +void +AutomationLineBase::set_maximum_time (Temporal::timepos_t const & t) +{ + if (_maximum_time == t) { + return; + } + + _maximum_time = t; + reset (); +} + + +/** @return min and max x positions of points that are in the list, in session samples */ +pair +AutomationLineBase::get_point_x_range () const +{ + pair r (timepos_t::max (the_list()->time_domain()), timepos_t::zero (the_list()->time_domain())); + + for (auto const & cp : *the_list()) { + const timepos_t w (session_position (cp->when)); + r.first = min (r.first, w); + r.second = max (r.second, w); + } + + return r; +} + +timepos_t +AutomationLineBase::session_position (timepos_t const & when) const +{ + return when + get_origin(); +} + +void +AutomationLineBase::set_offset (timepos_t const & off) +{ + _offset = off; + reset (); +} diff --git a/gtk2_ardour/automation_line_base.h b/gtk2_ardour/automation_line_base.h new file mode 100644 index 0000000000..084d9326f3 --- /dev/null +++ b/gtk2_ardour/automation_line_base.h @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2005-2017 Paul Davis + * Copyright (C) 2005 Karsten Wiese + * Copyright (C) 2005 Nick Mainsbridge + * Copyright (C) 2005 Taybin Rutkin + * Copyright (C) 2006 Hans Fugal + * Copyright (C) 2007-2012 Carl Hetherington + * Copyright (C) 2007-2015 David Robillard + * 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. + */ + +#ifndef __ardour_automation_line_base_h__ +#define __ardour_automation_line_base_h__ + +#include +#include +#include +#include + +#include + +#include "pbd/undo.h" +#include "pbd/statefuldestructible.h" +#include "pbd/memento_command.h" + +#include "ardour/automation_list.h" +#include "ardour/parameter_descriptor.h" +#include "ardour/types.h" + +#include "canvas/types.h" +#include "canvas/container.h" +#include "canvas/poly_line.h" + +namespace ArdourCanvas { + class Rectangle; +} + +class AutomationLine; +class ControlPoint; +class PointSelection; +class TimeAxisView; +class AutomationTimeAxisView; +class Selectable; +class Selection; +class EditingContext; + +/** A GUI representation of an ARDOUR::AutomationList */ +class AutomationLineBase : public sigc::trackable, public PBD::StatefulDestructible +{ +public: + enum VisibleAspects { + Line = 0x1, + ControlPoints = 0x2, + SelectedControlPoints = 0x4 + }; + + AutomationLineBase (const std::string& name, + EditingContext& ec, + ArdourCanvas::Item& parent, + ArdourCanvas::Rectangle* drag_base, + std::shared_ptr al, + const ARDOUR::ParameterDescriptor& desc); + + virtual ~AutomationLineBase (); + + virtual Temporal::timepos_t get_origin () const; + + ArdourCanvas::Rectangle* drag_base() const { return _drag_base; } + + void queue_reset (); + void reset (); + void clear (); + void set_fill (bool f) { _fill = f; } // owner needs to call set_height + + void set_selected_points (PointSelection const &); + void get_selectables (Temporal::timepos_t const &, Temporal::timepos_t const &, double, double, std::list&); + void get_inverted_selectables (Selection&, std::list& results); + + virtual void remove_point (ControlPoint&); + bool control_points_adjacent (double xval, uint32_t& before, uint32_t& after); + + /* dragging API */ + virtual void start_drag_single (ControlPoint*, double, float); + virtual void start_drag_line (uint32_t, uint32_t, float); + virtual void start_drag_multiple (std::list, float, XMLNode *); + virtual std::pair drag_motion (Temporal::timecnt_t const &, float, bool, bool with_push, uint32_t& final_index); + virtual void end_drag (bool with_push, uint32_t final_index); + virtual void end_draw_merge () {} + + ControlPoint* nth (uint32_t); + ControlPoint const * nth (uint32_t) const; + uint32_t npoints() const { return control_points.size(); } + + std::string name() const { return _name; } + bool visible() const { return _visible != VisibleAspects(0); } + guint32 height() const { return _height; } + + void set_line_color (std::string color, std::string mod = ""); + uint32_t get_line_color() const; + + void set_visibility (VisibleAspects); + void add_visibility (VisibleAspects); + void remove_visibility (VisibleAspects); + + void hide (); + void set_height (guint32); + + bool get_uses_gain_mapping () const; + void tempo_map_changed (); + + ArdourCanvas::Container& canvas_group() const { return *group; } + ArdourCanvas::Item& parent_group() const { return _parent_group; } + ArdourCanvas::Item& grab_item() const { return *line; } + + virtual std::string get_verbose_cursor_string (double) const; + std::string get_verbose_cursor_relative_string (double, double) const; + std::string fraction_to_string (double) const; + std::string delta_to_string (double) const; + double string_to_fraction (std::string const &) const; + + void view_to_model_coord_y (double &) const; + + double model_to_view_coord_y (double) const; + Temporal::timecnt_t model_to_view_coord_x (Temporal::timepos_t const &) const; + + double compute_delta (double from, double to) const; + void apply_delta (double& val, double delta) const; + + void set_list(std::shared_ptr list); + std::shared_ptr the_list() const { return alist; } + + void track_entered(); + void track_exited(); + + bool is_last_point (ControlPoint &); + bool is_first_point (ControlPoint &); + + XMLNode& get_state () const; + int set_state (const XMLNode&, int version); + void set_colors(); + + void modify_points_y (std::vector const&, double); + + virtual MementoCommandBinder* memento_command_binder (); + + std::pair get_point_x_range () const; + + void set_maximum_time (Temporal::timepos_t const &); + Temporal::timepos_t maximum_time () const { + return _maximum_time; + } + + void set_offset (Temporal::timepos_t const &); + Temporal::timepos_t offset () { return _offset; } + void set_width (Temporal::timecnt_t const &); + + Temporal::timepos_t session_position (Temporal::timepos_t const &) const; + void dump (std::ostream&) const; + + double dt_to_dx (Temporal::timepos_t const &, Temporal::timecnt_t const &); + +protected: + + std::string _name; + guint32 _height; + std::string _line_color; + std::string _line_color_mod; + uint32_t _view_index_offset; + std::shared_ptr alist; + + VisibleAspects _visible; + + bool terminal_points_can_slide; + bool update_pending; + bool have_reset_timeout; + bool no_draw; + bool _is_boolean; + /** true if we did a push at any point during the current drag */ + bool did_push; + + EditingContext& editing_context; + ArdourCanvas::Item& _parent_group; + ArdourCanvas::Rectangle* _drag_base; + ArdourCanvas::Container* group; + ArdourCanvas::PolyLine* line; /* line */ + ArdourCanvas::Points line_points; /* coordinates for canvas line */ + std::vector control_points; /* visible control points */ + + class ContiguousControlPoints : public std::list { + public: + ContiguousControlPoints (AutomationLineBase& al); + Temporal::timecnt_t clamp_dt (Temporal::timecnt_t const & dx, Temporal::timepos_t const & region_limit); + void move (Temporal::timecnt_t const &, double dvalue); + void compute_x_bounds (); + private: + AutomationLineBase& line; + Temporal::timepos_t before_x; + Temporal::timepos_t after_x; + }; + + friend class ContiguousControlPoints; + + typedef std::shared_ptr CCP; + std::vector contiguous_points; + + bool sync_model_with_view_point (ControlPoint&); + bool sync_model_with_view_points (std::list); + void start_drag_common (double, float); + + void reset_callback (const Evoral::ControlList&); + void list_changed (); + + virtual bool event_handler (GdkEvent*) = 0; + +private: + std::list _drag_points; ///< points we are dragging + std::list _push_points; ///< additional points we are dragging if "push" is enabled + bool _drag_had_movement; ///< true if the drag has seen movement, otherwise false + double _last_drag_fraction; ///< last y position of the drag, as a fraction + /** offset from the start of the automation list to the start of the line, so that + * a +ve offset means that the 0 on the line is at _offset in the list + */ + Temporal::timepos_t _offset; + + bool is_stepped() const; + void update_visibility (); + void reset_line_coords (ControlPoint&); + void add_visible_control_point (uint32_t, uint32_t, double, double, ARDOUR::AutomationList::iterator, uint32_t); + double control_point_box_size (); + void connect_to_list (); + void interpolation_changed (ARDOUR::AutomationList::InterpolationStyle); + + PBD::ScopedConnectionList _list_connections; + + /** maximum time that a point on this line can be at, relative to the position of its region or start of its track */ + Temporal::timepos_t _maximum_time; + + bool _fill; + + const ARDOUR::ParameterDescriptor _desc; + + friend class AudioRegionGainLine; + friend class RegionFxLine; +}; + +#endif /* __ardour_automation_line_base_h__ */ +