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