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