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