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