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