Optimize automation-event process splitting
[ardour.git] / gtk2_ardour / automation_line.cc
1 /*
2     Copyright (C) 2002-2003 Paul Davis
3
4     This program is free software; you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation; either version 2 of the License, or
7     (at your option) any later version.
8
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU General Public License
15     along with this program; if not, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
18 */
19
20 #include <cmath>
21
22 #ifdef COMPILER_MSVC
23 #include <float.h>
24
25 // 'std::isnan()' is not available in MSVC.
26 #define isnan_local(val) (bool)_isnan((double)val)
27 #else
28 #define isnan_local std::isnan
29 #endif
30
31 #include <climits>
32 #include <vector>
33
34 #include "boost/shared_ptr.hpp"
35
36 #include "pbd/floating.h"
37 #include "pbd/memento_command.h"
38 #include "pbd/stl_delete.h"
39 #include "pbd/stacktrace.h"
40
41 #include "ardour/automation_list.h"
42 #include "ardour/dB.h"
43 #include "ardour/debug.h"
44 #include "ardour/parameter_types.h"
45 #include "ardour/tempo.h"
46
47 #include "evoral/Curve.hpp"
48
49 #include "canvas/debug.h"
50
51 #include "automation_line.h"
52 #include "control_point.h"
53 #include "gui_thread.h"
54 #include "rgb_macros.h"
55 #include "public_editor.h"
56 #include "selection.h"
57 #include "time_axis_view.h"
58 #include "point_selection.h"
59 #include "automation_time_axis.h"
60 #include "ui_config.h"
61
62 #include "ardour/event_type_map.h"
63 #include "ardour/session.h"
64 #include "ardour/value_as_string.h"
65
66 #include "pbd/i18n.h"
67
68 using namespace std;
69 using namespace ARDOUR;
70 using namespace PBD;
71 using namespace Editing;
72
73 /** @param converter A TimeConverter whose origin_b is the start time of the AutomationList in session samples.
74  *  This will not be deleted by AutomationLine.
75  */
76 AutomationLine::AutomationLine (const string&                              name,
77                                 TimeAxisView&                              tv,
78                                 ArdourCanvas::Item&                        parent,
79                                 boost::shared_ptr<AutomationList>          al,
80                                 const ParameterDescriptor&                 desc,
81                                 Evoral::TimeConverter<double, samplepos_t>* converter)
82         : trackview (tv)
83         , _name (name)
84         , alist (al)
85         , _time_converter (converter ? converter : new Evoral::IdentityConverter<double, samplepos_t>)
86         , _parent_group (parent)
87         , _offset (0)
88         , _maximum_time (max_samplepos)
89         , _fill (false)
90         , _desc (desc)
91 {
92         if (converter) {
93                 _our_time_converter = false;
94         } else {
95                 _our_time_converter = true;
96         }
97
98         _visible = Line;
99
100         update_pending = false;
101         have_timeout = false;
102         no_draw = false;
103         _is_boolean = false;
104         terminal_points_can_slide = true;
105         _height = 0;
106
107         group = new ArdourCanvas::Container (&parent, ArdourCanvas::Duple(0, 1.5));
108         CANVAS_DEBUG_NAME (group, "region gain envelope group");
109
110         line = new ArdourCanvas::PolyLine (group);
111         CANVAS_DEBUG_NAME (line, "region gain envelope line");
112         line->set_data ("line", this);
113         line->set_outline_width (2.0);
114         line->set_covers_threshold (4.0);
115
116         line->Event.connect (sigc::mem_fun (*this, &AutomationLine::event_handler));
117
118         trackview.session()->register_with_memento_command_factory(alist->id(), this);
119
120         interpolation_changed (alist->interpolation ());
121
122         connect_to_list ();
123 }
124
125 AutomationLine::~AutomationLine ()
126 {
127         vector_delete (&control_points);
128         delete group;
129
130         if (_our_time_converter) {
131                 delete _time_converter;
132         }
133 }
134
135 bool
136 AutomationLine::event_handler (GdkEvent* event)
137 {
138         return PublicEditor::instance().canvas_line_event (event, line, this);
139 }
140
141 bool
142 AutomationLine::is_stepped() const
143 {
144         return (_desc.toggled ||
145                 (alist && alist->interpolation() == AutomationList::Discrete));
146 }
147
148 void
149 AutomationLine::update_visibility ()
150 {
151         if (_visible & Line) {
152                 /* Only show the line when there are some points, otherwise we may show an out-of-date line
153                    when automation points have been removed (the line will still follow the shape of the
154                    old points).
155                 */
156                 if (control_points.size() >= 2) {
157                         line->show();
158                 } else {
159                         line->hide ();
160                 }
161
162                 if (_visible & ControlPoints) {
163                         for (vector<ControlPoint*>::iterator i = control_points.begin(); i != control_points.end(); ++i) {
164                                 (*i)->show ();
165                         }
166                 } else if (_visible & SelectedControlPoints) {
167                         for (vector<ControlPoint*>::iterator i = control_points.begin(); i != control_points.end(); ++i) {
168                                 if ((*i)->selected()) {
169                                         (*i)->show ();
170                                 } else {
171                                         (*i)->hide ();
172                                 }
173                         }
174                 } else {
175                         for (vector<ControlPoint*>::iterator i = control_points.begin(); i != control_points.end(); ++i) {
176                                 (*i)->hide ();
177                         }
178                 }
179
180         } else {
181                 line->hide ();
182                 for (vector<ControlPoint*>::iterator i = control_points.begin(); i != control_points.end(); ++i) {
183                         if (_visible & ControlPoints) {
184                                 (*i)->show ();
185                         } else {
186                                 (*i)->hide ();
187                         }
188                 }
189         }
190 }
191
192 bool
193 AutomationLine::get_uses_gain_mapping () const
194 {
195         switch (_desc.type) {
196                 case GainAutomation:
197                 case EnvelopeAutomation:
198                 case TrimAutomation:
199                         return true;
200                 default:
201                         return false;
202         }
203 }
204
205 void
206 AutomationLine::hide ()
207 {
208         /* leave control points setting unchanged, we are just hiding the
209            overall line
210         */
211
212         set_visibility (AutomationLine::VisibleAspects (_visible & ~Line));
213 }
214
215 double
216 AutomationLine::control_point_box_size ()
217 {
218         if (_height > TimeAxisView::preset_height (HeightLarger)) {
219                 return 8.0;
220         } else if (_height > (guint32) TimeAxisView::preset_height (HeightNormal)) {
221                 return 6.0;
222         }
223         return 4.0;
224 }
225
226 void
227 AutomationLine::set_height (guint32 h)
228 {
229         if (h != _height) {
230                 _height = h;
231
232                 double bsz = control_point_box_size();
233
234                 for (vector<ControlPoint*>::iterator i = control_points.begin(); i != control_points.end(); ++i) {
235                         (*i)->set_size (bsz);
236                 }
237
238                 if (_fill) {
239                         line->set_fill_y1 (_height);
240                 } else {
241                         line->set_fill_y1 (0);
242                 }
243                 reset ();
244         }
245 }
246
247 void
248 AutomationLine::set_line_color (uint32_t color)
249 {
250         _line_color = color;
251         line->set_outline_color (color);
252
253         Gtkmm2ext::SVAModifier mod = UIConfiguration::instance().modifier ("automation line fill");
254
255         line->set_fill_color ((color & 0xffffff00) + mod.a()*255);
256 }
257
258 ControlPoint*
259 AutomationLine::nth (uint32_t n)
260 {
261         if (n < control_points.size()) {
262                 return control_points[n];
263         } else {
264                 return 0;
265         }
266 }
267
268 ControlPoint const *
269 AutomationLine::nth (uint32_t n) const
270 {
271         if (n < control_points.size()) {
272                 return control_points[n];
273         } else {
274                 return 0;
275         }
276 }
277
278 void
279 AutomationLine::modify_point_y (ControlPoint& cp, double y)
280 {
281         /* clamp y-coord appropriately. y is supposed to be a normalized fraction (0.0-1.0),
282            and needs to be converted to a canvas unit distance.
283         */
284
285         y = max (0.0, y);
286         y = min (1.0, y);
287         y = _height - (y * _height);
288
289         double const x = trackview.editor().sample_to_pixel_unrounded (_time_converter->to((*cp.model())->when) - _offset);
290
291         trackview.editor().begin_reversible_command (_("automation event move"));
292         trackview.editor().session()->add_command (
293                 new MementoCommand<AutomationList> (memento_command_binder(), &get_state(), 0));
294
295         cp.move_to (x, y, ControlPoint::Full);
296
297         alist->freeze ();
298         sync_model_with_view_point (cp);
299         alist->thaw ();
300
301         reset_line_coords (cp);
302
303         if (line_points.size() > 1) {
304                 line->set_steps (line_points, is_stepped());
305         }
306
307         update_pending = false;
308
309         trackview.editor().session()->add_command (
310                 new MementoCommand<AutomationList> (memento_command_binder(), 0, &alist->get_state()));
311
312         trackview.editor().commit_reversible_command ();
313         trackview.editor().session()->set_dirty ();
314 }
315
316 void
317 AutomationLine::reset_line_coords (ControlPoint& cp)
318 {
319         if (cp.view_index() < line_points.size()) {
320                 line_points[cp.view_index()].x = cp.get_x ();
321                 line_points[cp.view_index()].y = cp.get_y ();
322         }
323 }
324
325 bool
326 AutomationLine::sync_model_with_view_points (list<ControlPoint*> cp)
327 {
328         update_pending = true;
329
330         bool moved = false;
331         for (list<ControlPoint*>::iterator i = cp.begin(); i != cp.end(); ++i) {
332                 moved = sync_model_with_view_point (**i) || moved;
333         }
334
335         return moved;
336 }
337
338 string
339 AutomationLine::get_verbose_cursor_string (double fraction) const
340 {
341         return fraction_to_string (fraction);
342 }
343
344 string
345 AutomationLine::get_verbose_cursor_relative_string (double fraction, double delta) const
346 {
347         std::string s = fraction_to_string (fraction);
348         std::string d = delta_to_string (delta);
349         return s + " (" + d + ")";
350 }
351
352 /**
353  *  @param fraction y fraction
354  *  @return string representation of this value, using dB if appropriate.
355  */
356 string
357 AutomationLine::fraction_to_string (double fraction) const
358 {
359         view_to_model_coord_y (fraction);
360         return ARDOUR::value_as_string (_desc, fraction);
361 }
362
363 string
364 AutomationLine::delta_to_string (double delta) const
365 {
366         if (!get_uses_gain_mapping () && _desc.logarithmic) {
367                 return "x " + ARDOUR::value_as_string (_desc, delta);
368         } else {
369                 return "\u0394 " + ARDOUR::value_as_string (_desc, delta);
370         }
371 }
372
373 /**
374  *  @param s Value string in the form as returned by fraction_to_string.
375  *  @return Corresponding y fraction.
376  */
377 double
378 AutomationLine::string_to_fraction (string const & s) const
379 {
380         double v;
381         sscanf (s.c_str(), "%lf", &v);
382
383         switch (_desc.type) {
384                 case GainAutomation:
385                 case EnvelopeAutomation:
386                 case TrimAutomation:
387                         if (s == "-inf") { /* translation */
388                                 v = 0;
389                         } else {
390                                 v = dB_to_coefficient (v);
391                         }
392                         break;
393                 default:
394                         break;
395         }
396         model_to_view_coord_y (v);
397         return v;
398 }
399
400 /** Start dragging a single point, possibly adding others if the supplied point is selected and there
401  *  are other selected points.
402  *
403  *  @param cp Point to drag.
404  *  @param x Initial x position (units).
405  *  @param fraction Initial y position (as a fraction of the track height, where 0 is the bottom and 1 the top)
406  */
407 void
408 AutomationLine::start_drag_single (ControlPoint* cp, double x, float fraction)
409 {
410         trackview.editor().session()->add_command (
411                 new MementoCommand<AutomationList> (memento_command_binder(), &get_state(), 0));
412
413         _drag_points.clear ();
414         _drag_points.push_back (cp);
415
416         if (cp->selected ()) {
417                 for (vector<ControlPoint*>::iterator i = control_points.begin(); i != control_points.end(); ++i) {
418                         if (*i != cp && (*i)->selected()) {
419                                 _drag_points.push_back (*i);
420                         }
421                 }
422         }
423
424         start_drag_common (x, fraction);
425 }
426
427 /** Start dragging a line vertically (with no change in x)
428  *  @param i1 Control point index of the `left' point on the line.
429  *  @param i2 Control point index of the `right' point on the line.
430  *  @param fraction Initial y position (as a fraction of the track height, where 0 is the bottom and 1 the top)
431  */
432 void
433 AutomationLine::start_drag_line (uint32_t i1, uint32_t i2, float fraction)
434 {
435         trackview.editor().session()->add_command (
436                 new MementoCommand<AutomationList> (memento_command_binder (), &get_state(), 0));
437
438         _drag_points.clear ();
439
440         for (uint32_t i = i1; i <= i2; i++) {
441                 _drag_points.push_back (nth (i));
442         }
443
444         start_drag_common (0, fraction);
445 }
446
447 /** Start dragging multiple points (with no change in x)
448  *  @param cp Points to drag.
449  *  @param fraction Initial y position (as a fraction of the track height, where 0 is the bottom and 1 the top)
450  */
451 void
452 AutomationLine::start_drag_multiple (list<ControlPoint*> cp, float fraction, XMLNode* state)
453 {
454         trackview.editor().session()->add_command (
455                 new MementoCommand<AutomationList> (memento_command_binder(), state, 0));
456
457         _drag_points = cp;
458         start_drag_common (0, fraction);
459 }
460
461 struct ControlPointSorter
462 {
463         bool operator() (ControlPoint const * a, ControlPoint const * b) const {
464                 if (floateq (a->get_x(), b->get_x(), 1)) {
465                         return a->view_index() < b->view_index();
466                 }
467                 return a->get_x() < b->get_x();
468         }
469 };
470
471 AutomationLine::ContiguousControlPoints::ContiguousControlPoints (AutomationLine& al)
472         : line (al), before_x (0), after_x (DBL_MAX)
473 {
474 }
475
476 void
477 AutomationLine::ContiguousControlPoints::compute_x_bounds (PublicEditor& e)
478 {
479         uint32_t sz = size();
480
481         if (sz > 0 && sz < line.npoints()) {
482                 const TempoMap& map (e.session()->tempo_map());
483
484                 /* determine the limits on x-axis motion for this
485                    contiguous range of control points
486                 */
487
488                 if (front()->view_index() > 0) {
489                         before_x = line.nth (front()->view_index() - 1)->get_x();
490
491                         const samplepos_t pos = e.pixel_to_sample(before_x);
492                         const Meter& meter = map.meter_at_sample (pos);
493                         const samplecnt_t len = ceil (meter.samples_per_bar (map.tempo_at_sample (pos), e.session()->sample_rate())
494                                         / (Timecode::BBT_Time::ticks_per_beat * meter.divisions_per_bar()) );
495                         const double one_tick_in_pixels = e.sample_to_pixel_unrounded (len);
496
497                         before_x += one_tick_in_pixels;
498                 }
499
500                 /* if our last point has a point after it in the line,
501                    we have an "after" bound
502                 */
503
504                 if (back()->view_index() < (line.npoints() - 1)) {
505                         after_x = line.nth (back()->view_index() + 1)->get_x();
506
507                         const samplepos_t pos = e.pixel_to_sample(after_x);
508                         const Meter& meter = map.meter_at_sample (pos);
509                         const samplecnt_t len = ceil (meter.samples_per_bar (map.tempo_at_sample (pos), e.session()->sample_rate())
510                                         / (Timecode::BBT_Time::ticks_per_beat * meter.divisions_per_bar()));
511                         const double one_tick_in_pixels = e.sample_to_pixel_unrounded (len);
512
513                         after_x -= one_tick_in_pixels;
514                 }
515         }
516 }
517
518 double
519 AutomationLine::ContiguousControlPoints::clamp_dx (double dx)
520 {
521         if (empty()) {
522                 return dx;
523         }
524
525         /* get the maximum distance we can move any of these points along the x-axis
526          */
527
528         double tx; /* possible position a point would move to, given dx */
529         ControlPoint* cp;
530
531         if (dx > 0) {
532                 /* check the last point, since we're moving later in time */
533                 cp = back();
534         } else {
535                 /* check the first point, since we're moving earlier in time */
536                 cp = front();
537         }
538
539         tx = cp->get_x() + dx; // new possible position if we just add the motion
540         tx = max (tx, before_x); // can't move later than following point
541         tx = min (tx, after_x);  // can't move earlier than preceeding point
542         return  tx - cp->get_x ();
543 }
544
545 void
546 AutomationLine::ContiguousControlPoints::move (double dx, double dvalue)
547 {
548         for (std::list<ControlPoint*>::iterator i = begin(); i != end(); ++i) {
549                 // compute y-axis delta
550                 double view_y = 1.0 - (*i)->get_y() / line.height();
551                 line.view_to_model_coord_y (view_y);
552                 line.apply_delta (view_y, dvalue);
553                 line.model_to_view_coord_y (view_y);
554                 view_y = (1.0 - view_y) * line.height();
555
556                 (*i)->move_to ((*i)->get_x() + dx, view_y, ControlPoint::Full);
557                 line.reset_line_coords (**i);
558         }
559 }
560
561 /** Common parts of starting a drag.
562  *  @param x Starting x position in units, or 0 if x is being ignored.
563  *  @param fraction Starting y position (as a fraction of the track height, where 0 is the bottom and 1 the top)
564  */
565 void
566 AutomationLine::start_drag_common (double x, float fraction)
567 {
568         _drag_x = x;
569         _drag_distance = 0;
570         _last_drag_fraction = fraction;
571         _drag_had_movement = false;
572         did_push = false;
573
574         /* they are probably ordered already, but we have to make sure */
575
576         _drag_points.sort (ControlPointSorter());
577 }
578
579
580 /** Should be called to indicate motion during a drag.
581  *  @param x New x position of the drag in canvas units, or undefined if ignore_x == true.
582  *  @param fraction New y fraction.
583  *  @return x position and y fraction that were actually used (once clamped).
584  */
585 pair<float, float>
586 AutomationLine::drag_motion (double const x, float fraction, bool ignore_x, bool with_push, uint32_t& final_index)
587 {
588         if (_drag_points.empty()) {
589                 return pair<double,float> (fraction, _desc.is_linear () ? 0 : 1);
590         }
591
592         double dx = ignore_x ? 0 : (x - _drag_x);
593         double dy = fraction - _last_drag_fraction;
594
595         if (!_drag_had_movement) {
596
597                 /* "first move" ... do some stuff that we don't want to do if
598                    no motion ever took place, but need to do before we handle
599                    motion.
600                 */
601
602                 /* partition the points we are dragging into (potentially several)
603                  * set(s) of contiguous points. this will not happen with a normal
604                  * drag, but if the user does a discontiguous selection, it can.
605                  */
606
607                 uint32_t expected_view_index = 0;
608                 CCP contig;
609
610                 for (list<ControlPoint*>::iterator i = _drag_points.begin(); i != _drag_points.end(); ++i) {
611                         if (i == _drag_points.begin() || (*i)->view_index() != expected_view_index) {
612                                 contig.reset (new ContiguousControlPoints (*this));
613                                 contiguous_points.push_back (contig);
614                         }
615                         contig->push_back (*i);
616                         expected_view_index = (*i)->view_index() + 1;
617                 }
618
619                 if (contiguous_points.back()->empty()) {
620                         contiguous_points.pop_back ();
621                 }
622
623                 for (vector<CCP>::iterator ccp = contiguous_points.begin(); ccp != contiguous_points.end(); ++ccp) {
624                         (*ccp)->compute_x_bounds (trackview.editor());
625                 }
626                 _drag_had_movement = true;
627         }
628
629         /* OK, now on to the stuff related to *this* motion event. First, for
630          * each contiguous range, figure out the maximum x-axis motion we are
631          * allowed (because of neighbouring points that are not moving.
632          *
633          * if we are moving forwards with push, we don't need to do this,
634          * since all later points will move too.
635          */
636
637         if (dx < 0 || ((dx > 0) && !with_push)) {
638                 for (vector<CCP>::iterator ccp = contiguous_points.begin(); ccp != contiguous_points.end(); ++ccp) {
639                         double dxt = (*ccp)->clamp_dx (dx);
640                         if (fabs (dxt) < fabs (dx)) {
641                                 dx = dxt;
642                         }
643                 }
644         }
645
646         /* compute deflection */
647         double delta_value;
648         {
649                 double value0 = _last_drag_fraction;
650                 double value1 = _last_drag_fraction + dy;
651                 view_to_model_coord_y (value0);
652                 view_to_model_coord_y (value1);
653                 delta_value = compute_delta (value0, value1);
654         }
655
656         /* special case -inf */
657         if (delta_value == 0 && dy > 0 && !_desc.is_linear ()) {
658                 assert (_desc.lower == 0);
659                 delta_value = 1.0;
660         }
661
662         /* clamp y */
663         for (list<ControlPoint*>::iterator i = _drag_points.begin(); i != _drag_points.end(); ++i) {
664                 double vy = 1.0 - (*i)->get_y() / _height;
665                 view_to_model_coord_y (vy);
666                 const double orig = vy;
667                 apply_delta (vy, delta_value);
668                 if (vy < _desc.lower) {
669                         delta_value = compute_delta (orig, _desc.lower);
670                 }
671                 if (vy > _desc.upper) {
672                         delta_value = compute_delta (orig, _desc.upper);
673                 }
674         }
675
676         if (dx || dy) {
677                 /* and now move each section */
678                 for (vector<CCP>::iterator ccp = contiguous_points.begin(); ccp != contiguous_points.end(); ++ccp) {
679                         (*ccp)->move (dx, delta_value);
680                 }
681
682                 if (with_push) {
683                         final_index = contiguous_points.back()->back()->view_index () + 1;
684                         ControlPoint* p;
685                         uint32_t i = final_index;
686                         while ((p = nth (i)) != 0 && p->can_slide()) {
687                                 p->move_to (p->get_x() + dx, p->get_y(), ControlPoint::Full);
688                                 reset_line_coords (*p);
689                                 ++i;
690                         }
691                 }
692
693                 /* update actual line coordinates (will queue a redraw) */
694
695                 if (line_points.size() > 1) {
696                         line->set_steps (line_points, is_stepped());
697                 }
698         }
699
700         /* calculate effective delta */
701         ControlPoint* cp = _drag_points.front();
702         double vy = 1.0 - cp->get_y() / (double)_height;
703         view_to_model_coord_y (vy);
704         float val = (*(cp->model ()))->value;
705         float effective_delta = _desc.compute_delta (val, vy);
706         /* special case recovery from -inf */
707         if (val == 0 && effective_delta == 0 && vy > 0) {
708                 assert (!_desc.is_linear ());
709                 effective_delta = HUGE_VAL; // +Infinity
710         }
711
712         double const result_frac = _last_drag_fraction + dy;
713         _drag_distance += dx;
714         _drag_x += dx;
715         _last_drag_fraction = result_frac;
716         did_push = with_push;
717
718         return pair<float, float> (result_frac, effective_delta);
719 }
720
721 /** Should be called to indicate the end of a drag */
722 void
723 AutomationLine::end_drag (bool with_push, uint32_t final_index)
724 {
725         if (!_drag_had_movement) {
726                 return;
727         }
728
729         alist->freeze ();
730         bool moved = sync_model_with_view_points (_drag_points);
731
732         if (with_push) {
733                 ControlPoint* p;
734                 uint32_t i = final_index;
735                 while ((p = nth (i)) != 0 && p->can_slide()) {
736                         moved = sync_model_with_view_point (*p) || moved;
737                         ++i;
738                 }
739         }
740
741         alist->thaw ();
742
743         update_pending = false;
744
745         if (moved) {
746                 /* A point has moved as a result of sync (clamped to integer or boolean
747                    value), update line accordingly. */
748                 line->set_steps (line_points, is_stepped());
749         }
750
751         trackview.editor().session()->add_command (
752                 new MementoCommand<AutomationList>(memento_command_binder (), 0, &alist->get_state()));
753
754         trackview.editor().session()->set_dirty ();
755         did_push = false;
756
757         contiguous_points.clear ();
758 }
759
760 bool
761 AutomationLine::sync_model_with_view_point (ControlPoint& cp)
762 {
763         /* find out where the visual control point is.
764            initial results are in canvas units. ask the
765            line to convert them to something relevant.
766         */
767
768         double view_x = cp.get_x();
769         double view_y = 1.0 - cp.get_y() / (double)_height;
770
771         /* if xval has not changed, set it directly from the model to avoid rounding errors */
772
773         if (view_x == trackview.editor().sample_to_pixel_unrounded (_time_converter->to ((*cp.model())->when)) - _offset) {
774                 view_x = (*cp.model())->when - _offset;
775         } else {
776                 view_x = trackview.editor().pixel_to_sample (view_x);
777                 view_x = _time_converter->from (view_x + _offset);
778         }
779
780         update_pending = true;
781
782         view_to_model_coord_y (view_y);
783
784         alist->modify (cp.model(), view_x, view_y);
785
786         /* convert back from model to view y for clamping position (for integer/boolean/etc) */
787         model_to_view_coord_y (view_y);
788         const double point_y = _height - (view_y * _height);
789         if (point_y != cp.get_y()) {
790                 cp.move_to (cp.get_x(), point_y, ControlPoint::Full);
791                 reset_line_coords (cp);
792                 return true;
793         }
794
795         return false;
796 }
797
798 bool
799 AutomationLine::control_points_adjacent (double xval, uint32_t & before, uint32_t& after)
800 {
801         ControlPoint *bcp = 0;
802         ControlPoint *acp = 0;
803         double unit_xval;
804
805         unit_xval = trackview.editor().sample_to_pixel_unrounded (xval);
806
807         for (vector<ControlPoint*>::iterator i = control_points.begin(); i != control_points.end(); ++i) {
808
809                 if ((*i)->get_x() <= unit_xval) {
810
811                         if (!bcp || (*i)->get_x() > bcp->get_x()) {
812                                 bcp = *i;
813                                 before = bcp->view_index();
814                         }
815
816                 } else if ((*i)->get_x() > unit_xval) {
817                         acp = *i;
818                         after = acp->view_index();
819                         break;
820                 }
821         }
822
823         return bcp && acp;
824 }
825
826 bool
827 AutomationLine::is_last_point (ControlPoint& cp)
828 {
829         // If the list is not empty, and the point is the last point in the list
830
831         if (alist->empty()) {
832                 return false;
833         }
834
835         AutomationList::const_iterator i = alist->end();
836         --i;
837
838         if (cp.model() == i) {
839                 return true;
840         }
841
842         return false;
843 }
844
845 bool
846 AutomationLine::is_first_point (ControlPoint& cp)
847 {
848         // If the list is not empty, and the point is the first point in the list
849
850         if (!alist->empty() && cp.model() == alist->begin()) {
851                 return true;
852         }
853
854         return false;
855 }
856
857 // This is copied into AudioRegionGainLine
858 void
859 AutomationLine::remove_point (ControlPoint& cp)
860 {
861         trackview.editor().begin_reversible_command (_("remove control point"));
862         XMLNode &before = alist->get_state();
863
864         trackview.editor ().get_selection ().clear_points ();
865         alist->erase (cp.model());
866
867         trackview.editor().session()->add_command(
868                 new MementoCommand<AutomationList> (memento_command_binder (), &before, &alist->get_state()));
869
870         trackview.editor().commit_reversible_command ();
871         trackview.editor().session()->set_dirty ();
872 }
873
874 /** Get selectable points within an area.
875  *  @param start Start position in session samples.
876  *  @param end End position in session samples.
877  *  @param bot Bottom y range, as a fraction of line height, where 0 is the bottom of the line.
878  *  @param top Top y range, as a fraction of line height, where 0 is the bottom of the line.
879  *  @param result Filled in with selectable things; in this case, ControlPoints.
880  */
881 void
882 AutomationLine::get_selectables (samplepos_t start, samplepos_t end, double botfrac, double topfrac, list<Selectable*>& results)
883 {
884         /* convert fractions to display coordinates with 0 at the top of the track */
885         double const bot_track = (1 - topfrac) * trackview.current_height ();
886         double const top_track = (1 - botfrac) * trackview.current_height ();
887
888         for (vector<ControlPoint*>::iterator i = control_points.begin(); i != control_points.end(); ++i) {
889                 double const model_when = (*(*i)->model())->when;
890
891                 /* model_when is relative to the start of the source, so we just need to add on the origin_b here
892                    (as it is the session sample position of the start of the source)
893                 */
894
895                 samplepos_t const session_samples_when = _time_converter->to (model_when) + _time_converter->origin_b ();
896
897                 if (session_samples_when >= start && session_samples_when <= end && (*i)->get_y() >= bot_track && (*i)->get_y() <= top_track) {
898                         results.push_back (*i);
899                 }
900         }
901 }
902
903 void
904 AutomationLine::get_inverted_selectables (Selection&, list<Selectable*>& /*results*/)
905 {
906         // hmmm ....
907 }
908
909 void
910 AutomationLine::set_selected_points (PointSelection const & points)
911 {
912         for (vector<ControlPoint*>::iterator i = control_points.begin(); i != control_points.end(); ++i) {
913                 (*i)->set_selected (false);
914         }
915
916         for (PointSelection::const_iterator i = points.begin(); i != points.end(); ++i) {
917                 (*i)->set_selected (true);
918         }
919
920         if (points.empty()) {
921                 remove_visibility (SelectedControlPoints);
922         } else {
923                 add_visibility (SelectedControlPoints);
924         }
925
926         set_colors ();
927 }
928
929 void
930 AutomationLine::set_colors ()
931 {
932         set_line_color (UIConfiguration::instance().color ("automation line"));
933         for (vector<ControlPoint*>::iterator i = control_points.begin(); i != control_points.end(); ++i) {
934                 (*i)->set_color ();
935         }
936 }
937
938 void
939 AutomationLine::list_changed ()
940 {
941         DEBUG_TRACE (DEBUG::Automation, string_compose ("\tline changed, existing update pending? %1\n", update_pending));
942
943         if (!update_pending) {
944                 update_pending = true;
945                 Gtkmm2ext::UI::instance()->call_slot (invalidator (*this), boost::bind (&AutomationLine::queue_reset, this));
946         }
947 }
948
949 void
950 AutomationLine::reset_callback (const Evoral::ControlList& events)
951 {
952         uint32_t vp = 0;
953         uint32_t pi = 0;
954         uint32_t np;
955
956         if (events.empty()) {
957                 for (vector<ControlPoint*>::iterator i = control_points.begin(); i != control_points.end(); ++i) {
958                         delete *i;
959                 }
960                 control_points.clear ();
961                 line->hide();
962                 return;
963         }
964
965         /* hide all existing points, and the line */
966
967         for (vector<ControlPoint*>::iterator i = control_points.begin(); i != control_points.end(); ++i) {
968                 (*i)->hide();
969         }
970
971         line->hide ();
972         np = events.size();
973
974         Evoral::ControlList& e = const_cast<Evoral::ControlList&> (events);
975
976         for (AutomationList::iterator ai = e.begin(); ai != e.end(); ++ai, ++pi) {
977
978                 double tx = (*ai)->when;
979                 double ty = (*ai)->value;
980
981                 /* convert from model coordinates to canonical view coordinates */
982
983                 model_to_view_coord (tx, ty);
984
985                 if (isnan_local (tx) || isnan_local (ty)) {
986                         warning << string_compose (_("Ignoring illegal points on AutomationLine \"%1\""),
987                                                    _name) << endmsg;
988                         continue;
989                 }
990
991                 if (tx >= max_samplepos || tx < 0 || tx >= _maximum_time) {
992                         continue;
993                 }
994
995                 /* convert x-coordinate to a canvas unit coordinate (this takes
996                  * zoom and scroll into account).
997                  */
998
999                 tx = trackview.editor().sample_to_pixel_unrounded (tx);
1000
1001                 /* convert from canonical view height (0..1.0) to actual
1002                  * height coordinates (using X11's top-left rooted system)
1003                  */
1004
1005                 ty = _height - (ty * _height);
1006
1007                 add_visible_control_point (vp, pi, tx, ty, ai, np);
1008                 vp++;
1009         }
1010
1011         /* discard extra CP's to avoid confusing ourselves */
1012
1013         while (control_points.size() > vp) {
1014                 ControlPoint* cp = control_points.back();
1015                 control_points.pop_back ();
1016                 delete cp;
1017         }
1018
1019         if (!terminal_points_can_slide) {
1020                 control_points.back()->set_can_slide(false);
1021         }
1022
1023         if (vp > 1) {
1024
1025                 /* reset the line coordinates given to the CanvasLine */
1026
1027                 while (line_points.size() < vp) {
1028                         line_points.push_back (ArdourCanvas::Duple (0,0));
1029                 }
1030
1031                 while (line_points.size() > vp) {
1032                         line_points.pop_back ();
1033                 }
1034
1035                 for (uint32_t n = 0; n < vp; ++n) {
1036                         line_points[n].x = control_points[n]->get_x();
1037                         line_points[n].y = control_points[n]->get_y();
1038                 }
1039
1040                 line->set_steps (line_points, is_stepped());
1041
1042                 update_visibility ();
1043         }
1044
1045         set_selected_points (trackview.editor().get_selection().points);
1046 }
1047
1048 void
1049 AutomationLine::reset ()
1050 {
1051         DEBUG_TRACE (DEBUG::Automation, "\t\tLINE RESET\n");
1052         update_pending = false;
1053         have_timeout = false;
1054
1055         if (no_draw) {
1056                 return;
1057         }
1058
1059         /* TODO: abort any drags in progress, e.g. draging points while writing automation
1060          * (the control-point model, used by AutomationLine::drag_motion, will be invalid).
1061          *
1062          * Note: reset() may also be called from an aborted drag (LineDrag::aborted)
1063          * maybe abort in list_changed(), interpolation_changed() and ... ?
1064          * XXX
1065          */
1066
1067         alist->apply_to_points (*this, &AutomationLine::reset_callback);
1068 }
1069
1070 void
1071 AutomationLine::queue_reset ()
1072 {
1073         /* this must be called from the GUI thread */
1074
1075         if (trackview.editor().session()->transport_rolling() && alist->automation_write()) {
1076                 /* automation write pass ... defer to a timeout */
1077                 /* redraw in 1/4 second */
1078                 if (!have_timeout) {
1079                         DEBUG_TRACE (DEBUG::Automation, "\tqueue timeout\n");
1080                         Glib::signal_timeout().connect (sigc::bind_return (sigc::mem_fun (*this, &AutomationLine::reset), false), 250);
1081                         have_timeout = true;
1082                 } else {
1083                         DEBUG_TRACE (DEBUG::Automation, "\ttimeout already queued, change ignored\n");
1084                 }
1085         } else {
1086                 reset ();
1087         }
1088 }
1089
1090 void
1091 AutomationLine::clear ()
1092 {
1093         /* parent must create and commit command */
1094         XMLNode &before = alist->get_state();
1095         alist->clear();
1096
1097         trackview.editor().session()->add_command (
1098                 new MementoCommand<AutomationList> (memento_command_binder (), &before, &alist->get_state()));
1099 }
1100
1101 void
1102 AutomationLine::set_list (boost::shared_ptr<ARDOUR::AutomationList> list)
1103 {
1104         alist = list;
1105         queue_reset ();
1106         connect_to_list ();
1107 }
1108
1109 void
1110 AutomationLine::add_visibility (VisibleAspects va)
1111 {
1112         VisibleAspects old = _visible;
1113
1114         _visible = VisibleAspects (_visible | va);
1115
1116         if (old != _visible) {
1117                 update_visibility ();
1118         }
1119 }
1120
1121 void
1122 AutomationLine::set_visibility (VisibleAspects va)
1123 {
1124         if (_visible != va) {
1125                 _visible = va;
1126                 update_visibility ();
1127         }
1128 }
1129
1130 void
1131 AutomationLine::remove_visibility (VisibleAspects va)
1132 {
1133         VisibleAspects old = _visible;
1134
1135         _visible = VisibleAspects (_visible & ~va);
1136
1137         if (old != _visible) {
1138                 update_visibility ();
1139         }
1140 }
1141
1142 void
1143 AutomationLine::track_entered()
1144 {
1145         add_visibility (ControlPoints);
1146 }
1147
1148 void
1149 AutomationLine::track_exited()
1150 {
1151         remove_visibility (ControlPoints);
1152 }
1153
1154 XMLNode &
1155 AutomationLine::get_state (void)
1156 {
1157         /* function as a proxy for the model */
1158         return alist->get_state();
1159 }
1160
1161 int
1162 AutomationLine::set_state (const XMLNode &node, int version)
1163 {
1164         /* function as a proxy for the model */
1165         return alist->set_state (node, version);
1166 }
1167
1168 void
1169 AutomationLine::view_to_model_coord (double& x, double& y) const
1170 {
1171         x = _time_converter->from (x);
1172         view_to_model_coord_y (y);
1173 }
1174
1175 void
1176 AutomationLine::view_to_model_coord_y (double& y) const
1177 {
1178         if (alist->default_interpolation () != alist->interpolation()) {
1179                 switch (alist->interpolation()) {
1180                         case AutomationList::Discrete:
1181                                 /* toggles and MIDI only -- see is_stepped() */
1182                                 assert (alist->default_interpolation () == AutomationList::Linear);
1183                                 break;
1184                         case AutomationList::Linear:
1185                                 y = y * (_desc.upper - _desc.lower) + _desc.lower;
1186                                 return;
1187                         default:
1188                                 /* types that default to linear, can't be use
1189                                  * Logarithmic or Exponential interpolation.
1190                                  * "Curved" is invalid for automation (only x-fads)
1191                                  */
1192                                 assert (0);
1193                                 break;
1194                 }
1195         }
1196         y = _desc.from_interface (y);
1197 }
1198
1199 double
1200 AutomationLine::compute_delta (double from, double to) const
1201 {
1202         return _desc.compute_delta (from, to);
1203 }
1204
1205 void
1206 AutomationLine::apply_delta (double& val, double delta) const
1207 {
1208         if (val == 0 && !_desc.is_linear () && delta >= 1.0) {
1209                 /* recover from -inf */
1210                 val = 1.0 / _height;
1211                 view_to_model_coord_y (val);
1212                 return;
1213         }
1214         val = _desc.apply_delta (val, delta);
1215 }
1216
1217 void
1218 AutomationLine::model_to_view_coord_y (double& y) const
1219 {
1220         if (alist->default_interpolation () != alist->interpolation()) {
1221                 switch (alist->interpolation()) {
1222                         case AutomationList::Discrete:
1223                                 /* toggles and MIDI only -- see is_stepped */
1224                                 assert (alist->default_interpolation () == AutomationList::Linear);
1225                                 break;
1226                         case AutomationList::Linear:
1227                                 y = (y - _desc.lower) / (_desc.upper - _desc.lower);
1228                                 return;
1229                         default:
1230                                 /* types that default to linear, can't be use
1231                                  * Logarithmic or Exponential interpolation.
1232                                  * "Curved" is invalid for automation (only x-fads)
1233                                  */
1234                                 assert (0);
1235                                 break;
1236                 }
1237         }
1238         y = _desc.to_interface (y);
1239 }
1240
1241 void
1242 AutomationLine::model_to_view_coord (double& x, double& y) const
1243 {
1244         model_to_view_coord_y (y);
1245         x = _time_converter->to (x) - _offset;
1246 }
1247
1248 /** Called when our list has announced that its interpolation style has changed */
1249 void
1250 AutomationLine::interpolation_changed (AutomationList::InterpolationStyle style)
1251 {
1252         if (line_points.size() > 1) {
1253                 reset ();
1254                 line->set_steps(line_points, is_stepped());
1255         }
1256 }
1257
1258 void
1259 AutomationLine::add_visible_control_point (uint32_t view_index, uint32_t pi, double tx, double ty,
1260                                            AutomationList::iterator model, uint32_t npoints)
1261 {
1262         ControlPoint::ShapeType shape;
1263
1264         if (view_index >= control_points.size()) {
1265
1266                 /* make sure we have enough control points */
1267
1268                 ControlPoint* ncp = new ControlPoint (*this);
1269                 ncp->set_size (control_point_box_size ());
1270
1271                 control_points.push_back (ncp);
1272         }
1273
1274         if (!terminal_points_can_slide) {
1275                 if (pi == 0) {
1276                         control_points[view_index]->set_can_slide (false);
1277                         if (tx == 0) {
1278                                 shape = ControlPoint::Start;
1279                         } else {
1280                                 shape = ControlPoint::Full;
1281                         }
1282                 } else if (pi == npoints - 1) {
1283                         control_points[view_index]->set_can_slide (false);
1284                         shape = ControlPoint::End;
1285                 } else {
1286                         control_points[view_index]->set_can_slide (true);
1287                         shape = ControlPoint::Full;
1288                 }
1289         } else {
1290                 control_points[view_index]->set_can_slide (true);
1291                 shape = ControlPoint::Full;
1292         }
1293
1294         control_points[view_index]->reset (tx, ty, model, view_index, shape);
1295
1296         /* finally, control visibility */
1297
1298         if (_visible & ControlPoints) {
1299                 control_points[view_index]->show ();
1300         } else {
1301                 control_points[view_index]->hide ();
1302         }
1303 }
1304
1305 void
1306 AutomationLine::connect_to_list ()
1307 {
1308         _list_connections.drop_connections ();
1309
1310         alist->StateChanged.connect (_list_connections, invalidator (*this), boost::bind (&AutomationLine::list_changed, this), gui_context());
1311
1312         alist->InterpolationChanged.connect (
1313                 _list_connections, invalidator (*this), boost::bind (&AutomationLine::interpolation_changed, this, _1), gui_context());
1314 }
1315
1316 MementoCommandBinder<AutomationList>*
1317 AutomationLine::memento_command_binder ()
1318 {
1319         return new SimpleMementoCommandBinder<AutomationList> (*alist.get());
1320 }
1321
1322 /** Set the maximum time that points on this line can be at, relative
1323  *  to the start of the track or region that it is on.
1324  */
1325 void
1326 AutomationLine::set_maximum_time (samplecnt_t t)
1327 {
1328         if (_maximum_time == t) {
1329                 return;
1330         }
1331
1332         _maximum_time = t;
1333         reset ();
1334 }
1335
1336
1337 /** @return min and max x positions of points that are in the list, in session samples */
1338 pair<samplepos_t, samplepos_t>
1339 AutomationLine::get_point_x_range () const
1340 {
1341         pair<samplepos_t, samplepos_t> r (max_samplepos, 0);
1342
1343         for (AutomationList::const_iterator i = the_list()->begin(); i != the_list()->end(); ++i) {
1344                 r.first = min (r.first, session_position (i));
1345                 r.second = max (r.second, session_position (i));
1346         }
1347
1348         return r;
1349 }
1350
1351 samplepos_t
1352 AutomationLine::session_position (AutomationList::const_iterator p) const
1353 {
1354         return _time_converter->to ((*p)->when) + _offset + _time_converter->origin_b ();
1355 }
1356
1357 void
1358 AutomationLine::set_offset (samplepos_t off)
1359 {
1360         if (_offset == off) {
1361                 return;
1362         }
1363
1364         _offset = off;
1365         reset ();
1366 }