use new method in MidiPatchManager to use MIDNAM data when setting a MidiTimeAxisView
[ardour.git] / gtk2_ardour / editor_summary.cc
1 /*
2  * Copyright (C) 2009-2012 Carl Hetherington <carl@carlh.net>
3  * Copyright (C) 2009-2015 David Robillard <d@drobilla.net>
4  * Copyright (C) 2009-2019 Paul Davis <paul@linuxaudiosystems.com>
5  * Copyright (C) 2012-2013 Colin Fletcher <colin.m.fletcher@googlemail.com>
6  * Copyright (C) 2012-2019 Robin Gareus <robin@gareus.org>
7  * Copyright (C) 2014-2016 Nick Mainsbridge <mainsbridge@gmail.com>
8  * Copyright (C) 2015-2016 Tim Mayberry <mojofunk@gmail.com>
9  * Copyright (C) 2017-2019 Ben Loftis <ben@harrisonconsoles.com>
10  *
11  * This program is free software; you can redistribute it and/or modify
12  * it under the terms of the GNU General Public License as published by
13  * the Free Software Foundation; either version 2 of the License, or
14  * (at your option) any later version.
15  *
16  * This program is distributed in the hope that it will be useful,
17  * but WITHOUT ANY WARRANTY; without even the implied warranty of
18  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19  * GNU General Public License for more details.
20  *
21  * You should have received a copy of the GNU General Public License along
22  * with this program; if not, write to the Free Software Foundation, Inc.,
23  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24  */
25
26 #include "ardour/session.h"
27
28 #include "canvas/debug.h"
29
30 #include <gtkmm/menu.h>
31 #include <gtkmm/menuitem.h>
32
33 #include "context_menu_helper.h"
34 #include "time_axis_view.h"
35 #include "streamview.h"
36 #include "editor_summary.h"
37 #include "gui_thread.h"
38 #include "editor.h"
39 #include "region_view.h"
40 #include "rgb_macros.h"
41 #include "keyboard.h"
42 #include "editor_routes.h"
43 #include "editor_cursors.h"
44 #include "mouse_cursors.h"
45 #include "route_time_axis.h"
46 #include "ui_config.h"
47
48 #include "pbd/i18n.h"
49
50 using namespace std;
51 using namespace ARDOUR;
52 using Gtkmm2ext::Keyboard;
53
54 /** Construct an EditorSummary.
55  *  @param e Editor to represent.
56  */
57 EditorSummary::EditorSummary (Editor* e)
58         : EditorComponent (e),
59           _start (0),
60           _end (1),
61           _x_scale (1),
62           _track_height (16),
63           _last_playhead (-1),
64           _move_dragging (false),
65           _view_rectangle_x (0, 0),
66           _view_rectangle_y (0, 0),
67           _zoom_trim_dragging (false),
68           _old_follow_playhead (false),
69           _image (0),
70           _background_dirty (true)
71 {
72         CairoWidget::use_nsglview ();
73         add_events (Gdk::POINTER_MOTION_MASK|Gdk::KEY_PRESS_MASK|Gdk::KEY_RELEASE_MASK|Gdk::ENTER_NOTIFY_MASK|Gdk::LEAVE_NOTIFY_MASK);
74         set_flags (get_flags() | Gtk::CAN_FOCUS);
75
76         UIConfiguration::instance().ParameterChanged.connect (sigc::mem_fun (*this, &EditorSummary::parameter_changed));
77 }
78
79 EditorSummary::~EditorSummary ()
80 {
81         cairo_surface_destroy (_image);
82 }
83
84 void
85 EditorSummary::parameter_changed (string p)
86 {
87
88         if (p == "color-regions-using-track-color") {
89                 set_background_dirty ();
90         }
91 }
92
93 /** Handle a size allocation.
94  *  @param alloc GTK allocation.
95  */
96 void
97 EditorSummary::on_size_allocate (Gtk::Allocation& alloc)
98 {
99         CairoWidget::on_size_allocate (alloc);
100         set_background_dirty ();
101 }
102
103
104 /** Connect to a session.
105  *  @param s Session.
106  */
107 void
108 EditorSummary::set_session (Session* s)
109 {
110         SessionHandlePtr::set_session (s);
111
112         set_dirty ();
113
114         /* Note: the EditorSummary already finds out about new regions from Editor::region_view_added
115          * (which attaches to StreamView::RegionViewAdded), and cut regions by the RegionPropertyChanged
116          * emitted when a cut region is added to the `cutlist' playlist.
117          */
118
119         if (_session) {
120                 Region::RegionPropertyChanged.connect (region_property_connection, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
121                 PresentationInfo::Change.connect (route_ctrl_id_connection, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
122                 _editor->playhead_cursor->PositionChanged.connect (position_connection, invalidator (*this), boost::bind (&EditorSummary::playhead_position_changed, this, _1), gui_context());
123                 _session->StartTimeChanged.connect (_session_connections, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
124                 _session->EndTimeChanged.connect (_session_connections, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
125                 _editor->selection->RegionsChanged.connect (sigc::mem_fun(*this, &EditorSummary::set_background_dirty));
126         }
127
128         UIConfiguration::instance().ColorsChanged.connect (sigc::mem_fun (*this, &EditorSummary::set_colors));
129
130         set_colors();
131
132         _leftmost = max_samplepos;
133         _rightmost = 0;
134 }
135
136 void
137 EditorSummary::render_background_image ()
138 {
139         cairo_surface_destroy (_image); // passing NULL is safe
140         _image = cairo_image_surface_create (CAIRO_FORMAT_RGB24, get_width (), get_height ());
141
142         cairo_t* cr = cairo_create (_image);
143
144         /* background (really just the dividing lines between tracks */
145
146         cairo_set_source_rgb (cr, 0, 0, 0);
147         cairo_rectangle (cr, 0, 0, get_width(), get_height());
148         cairo_fill (cr);
149
150         /* compute start and end points for the summary */
151
152         std::pair<samplepos_t, samplepos_t> ext = _editor->session_gui_extents();
153         double theoretical_start = ext.first;
154         double theoretical_end = ext.second;
155
156         /* the summary should encompass the full extent of everywhere we've visited since the session was opened */
157         if (_leftmost < theoretical_start)
158                 theoretical_start = _leftmost;
159         if (_rightmost > theoretical_end)
160                 theoretical_end = _rightmost;
161
162         /* range-check */
163         _start = theoretical_start > 0 ? theoretical_start : 0;
164         _end = theoretical_end < max_samplepos ? theoretical_end : max_samplepos;
165
166         /* calculate x scale */
167         if (_end != _start) {
168                 _x_scale = static_cast<double> (get_width()) / (_end - _start);
169         } else {
170                 _x_scale = 1;
171         }
172
173         /* compute track height */
174         int N = 0;
175         for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
176                 if (!(*i)->hidden()) {
177                         ++N;
178                 }
179         }
180
181         if (N == 0) {
182                 _track_height = 16;
183         } else {
184                 _track_height = (double) get_height() / N;
185         }
186
187         /* render tracks and regions */
188
189         double y = 0;
190         for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
191
192                 if ((*i)->hidden()) {
193                         continue;
194                 }
195
196                 /* paint a non-bg colored strip to represent the track itself */
197
198                 if (_track_height > 4) {
199                         cairo_set_source_rgb (cr, 0.2, 0.2, 0.2);
200                         cairo_set_line_width (cr, _track_height - 1);
201                         cairo_move_to (cr, 0, y + _track_height / 2);
202                         cairo_line_to (cr, get_width(), y + _track_height / 2);
203                         cairo_stroke (cr);
204                 }
205
206                 StreamView* s = (*i)->view ();
207
208                 if (s) {
209                         cairo_set_line_width (cr, _track_height * 0.8);
210
211                         s->foreach_regionview (sigc::bind (
212                                                            sigc::mem_fun (*this, &EditorSummary::render_region),
213                                                            cr,
214                                                            y + _track_height / 2
215                                                           ));
216                 }
217
218                 y += _track_height;
219         }
220
221         /* start and end markers */
222
223         cairo_set_line_width (cr, 1);
224         cairo_set_source_rgb (cr, 1, 1, 0);
225
226         const double p = (_session->current_start_sample() - _start) * _x_scale;
227         cairo_move_to (cr, p, 0);
228         cairo_line_to (cr, p, get_height());
229
230         double const q = (_session->current_end_sample() - _start) * _x_scale;
231         cairo_move_to (cr, q, 0);
232         cairo_line_to (cr, q, get_height());
233         cairo_stroke (cr);
234
235         cairo_destroy (cr);
236 }
237
238 /** Render the required regions to a cairo context.
239  *  @param cr Context.
240  */
241 void
242 EditorSummary::render (Cairo::RefPtr<Cairo::Context> const& ctx, cairo_rectangle_t*)
243 {
244         cairo_t* cr = ctx->cobj();
245
246         if (_session == 0) {
247                 return;
248         }
249
250         /* maintain the leftmost and rightmost locations that we've ever reached */
251         samplecnt_t const leftmost = _editor->leftmost_sample ();
252         if (leftmost < _leftmost) {
253                 _leftmost = leftmost;
254                 _background_dirty = true;
255         }
256         samplecnt_t const rightmost = leftmost + _editor->current_page_samples();
257         if (rightmost > _rightmost) {
258                 _rightmost = rightmost;
259                 _background_dirty = true;
260         }
261
262         /* draw the background (regions, markers, etc) if they've changed */
263         if (!_image || _background_dirty) {
264                 render_background_image ();
265                 _background_dirty = false;
266         }
267
268         cairo_push_group (cr);
269
270         /* Fill with the background image */
271
272         cairo_rectangle (cr, 0, 0, get_width(), get_height());
273         cairo_set_source_surface (cr, _image, 0, 0);
274         cairo_fill (cr);
275
276         /* Render the view rectangle.  If there is an editor visual pending, don't update
277          * the view rectangle now --- wait until the expose event that we'll get after
278          * the visual change.  This prevents a flicker.
279          */
280
281         if (_editor->pending_visual_change.idle_handler_id < 0) {
282                 get_editor (&_view_rectangle_x, &_view_rectangle_y);
283         }
284
285         int32_t width = _view_rectangle_x.second - _view_rectangle_x.first;
286         std::min(8, width);
287         cairo_rectangle (cr, _view_rectangle_x.first, 0, width, get_height ());
288         cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
289         cairo_fill (cr);
290
291         /* horiz zoom */
292         cairo_rectangle (cr, _view_rectangle_x.first, 0, width, get_height ());
293         cairo_set_line_width (cr, 1);
294         cairo_set_source_rgba (cr, 1, 1, 1, 0.9);
295         cairo_stroke (cr);
296
297         /* Playhead */
298
299         cairo_set_line_width (cr, 1);
300
301         double r,g,b,a;  Gtkmm2ext::color_to_rgba(_phead_color, r,g,b,a);
302         cairo_set_source_rgb (cr, r,g,b); // playhead color
303
304         const double ph= playhead_sample_to_position (_editor->playhead_cursor->current_sample());
305         cairo_move_to (cr, ph, 0);
306         cairo_line_to (cr, ph, get_height());
307         cairo_stroke (cr);
308         cairo_pop_group_to_source (cr);
309         cairo_paint (cr);
310         _last_playhead = ph;
311
312 }
313
314 void
315 EditorSummary::set_colors ()
316 {
317         _phead_color = UIConfiguration::instance().color ("play head");
318 }
319
320
321
322 /** Render a region for the summary.
323  *  @param r Region view.
324  *  @param cr Cairo context.
325  *  @param y y coordinate to render at.
326  */
327 void
328 EditorSummary::render_region (RegionView* r, cairo_t* cr, double y) const
329 {
330         /*NOTE:  you can optimize this operation by coalescing adjacent regions into a single line stroke.
331          * In a session with a single track ~1,000 regions, this reduced render time from 14ms to 11 ms.
332          * However, you lose a lot of visual information.  The current method preserves a sense of separation between regions.
333          * The current method shows the current selection (red regions), which needs to be preserved if this is optimized.
334          * I think it's not worth it for now,  but we might choose to revisit this someday.
335          */ 
336
337         uint32_t const c = r->get_fill_color ();
338         cairo_set_source_rgb (cr, UINT_RGBA_R (c) / 255.0, UINT_RGBA_G (c) / 255.0, UINT_RGBA_B (c) / 255.0);
339
340         if (r->region()->position() > _start) {
341                 cairo_move_to (cr, (r->region()->position() - _start) * _x_scale, y);
342         } else {
343                 cairo_move_to (cr, 0, y);
344         }
345
346         if ((r->region()->position() + r->region()->length()) > _start) {
347                 cairo_line_to (cr, ((r->region()->position() - _start + r->region()->length())) * _x_scale, y);
348         } else {
349                 cairo_line_to (cr, 0, y);
350         }
351
352         cairo_stroke (cr);
353 }
354
355 void
356 EditorSummary::set_background_dirty ()
357 {
358         if (!_background_dirty) {
359                 _background_dirty = true;
360                 set_dirty ();
361         }
362 }
363
364 /** Set the summary so that just the overlays (viewbox, playhead etc.) will be re-rendered */
365 void
366 EditorSummary::set_overlays_dirty ()
367 {
368         ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty);
369         queue_draw ();
370 }
371
372 /** Set the summary so that just the overlays (viewbox, playhead etc.) in a given area will be re-rendered */
373 void
374 EditorSummary::set_overlays_dirty_rect (int x, int y, int w, int h)
375 {
376         ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty_rect);
377         queue_draw_area (x, y, w, h);
378 }
379
380
381 /** Handle a size request.
382  *  @param req GTK requisition
383  */
384 void
385 EditorSummary::on_size_request (Gtk::Requisition *req)
386 {
387         /* The left/right buttons will determine our height */
388         req->width = -1;
389         req->height = -1;
390 }
391
392
393 void
394 EditorSummary::centre_on_click (GdkEventButton* ev)
395 {
396         pair<double, double> xr;
397         get_editor (&xr);
398
399         double const w = xr.second - xr.first;
400         double ex = ev->x - w / 2;
401         if (ex < 0) {
402                 ex = 0;
403         } else if ((ex + w) > get_width()) {
404                 ex = get_width() - w;
405         }
406
407         set_editor (ex);
408 }
409
410 bool
411 EditorSummary::on_enter_notify_event (GdkEventCrossing*)
412 {
413         grab_focus ();
414         Keyboard::magic_widget_grab_focus ();
415         return false;
416 }
417
418 bool
419 EditorSummary::on_leave_notify_event (GdkEventCrossing*)
420 {
421         /* there are no inferior/child windows, so any leave event means that
422            we're gone.
423         */
424         Keyboard::magic_widget_drop_focus ();
425         return false;
426 }
427
428 bool
429 EditorSummary::on_key_press_event (GdkEventKey* key)
430 {
431         gint x, y;
432         GtkAccelKey set_playhead_accel;
433
434         /* XXXX this is really ugly and should be using our own action maps and bindings */
435
436         if (gtk_accel_map_lookup_entry ("<Actions>/Editor/set-playhead", &set_playhead_accel)) {
437                 if (key->keyval == set_playhead_accel.accel_key && (int) key->state == set_playhead_accel.accel_mods) {
438                         if (_session) {
439                                 get_pointer (x, y);
440                                 _session->request_locate (_start + (samplepos_t) x / _x_scale, _session->transport_rolling());
441                                 return true;
442                         }
443                 }
444         }
445
446         return false;
447 }
448
449 bool
450 EditorSummary::on_key_release_event (GdkEventKey* key)
451 {
452
453         GtkAccelKey set_playhead_accel;
454
455         /* XXXX this is really ugly and should be using our own action maps and bindings */
456
457         if (gtk_accel_map_lookup_entry ("<Actions>/Editor/set-playhead", &set_playhead_accel)) {
458                 if (key->keyval == set_playhead_accel.accel_key && (int) key->state == set_playhead_accel.accel_mods) {
459                         return true;
460                 }
461         }
462         return false;
463 }
464
465 #include "gtkmm2ext/utils.h"
466
467 /** Handle a button press.
468  *  @param ev GTK event.
469  */
470 bool
471 EditorSummary::on_button_press_event (GdkEventButton* ev)
472 {
473         _old_follow_playhead = _editor->follow_playhead ();
474
475         if (ev->button == 3) { // right-click:  show the reset menu action
476                 using namespace Gtk::Menu_Helpers;
477                 Gtk::Menu* m = ARDOUR_UI_UTILS::shared_popup_menu ();
478                 MenuList& items = m->items ();
479                 items.push_back(MenuElem(_("Reset Summary to Extents"),
480                         sigc::mem_fun(*this, &EditorSummary::reset_to_extents)));
481                 m->popup (ev->button, ev->time);
482                 return true;
483         }
484
485         if (ev->button != 1) {
486                 return true;
487         }
488
489         pair<double, double> xr;
490         get_editor (&xr);
491
492         _start_editor_x = xr;
493         _start_mouse_x = ev->x;
494         _start_mouse_y = ev->y;
495         _start_position = get_position (ev->x, ev->y);
496
497         if (_start_position != INSIDE && _start_position != TO_LEFT_OR_RIGHT) {
498
499                 /* start a zoom_trim drag */
500
501                 _zoom_trim_position = get_position (ev->x, ev->y);
502                 _zoom_trim_dragging = true;
503                 _editor->_dragging_playhead = true;
504                 _editor->set_follow_playhead (false);
505
506                 if (suspending_editor_updates ()) {
507                         get_editor (&_pending_editor_x, &_pending_editor_y);
508                         _pending_editor_changed = false;
509                 }
510
511         } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
512
513                 /* secondary-modifier-click: locate playhead */
514                 if (_session) {
515                         _session->request_locate (ev->x / _x_scale + _start);
516                 }
517
518         } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
519
520                 centre_on_click (ev);
521
522         } else {
523
524                 /* start a move+zoom drag */
525                 get_editor (&_pending_editor_x, &_pending_editor_y);
526                 _pending_editor_changed = false;
527                 _editor->_dragging_playhead = true;
528                 _editor->set_follow_playhead (false);
529
530                 _move_dragging = true;
531
532                 _last_mx = ev->x;
533                 _last_my = ev->y;
534                 _last_dx = 0;
535                 _last_dy = 0;
536                 _last_y_delta = 0;
537
538                 get_window()->set_cursor (*_editor->_cursors->expand_left_right);
539
540         }
541
542         return true;
543 }
544
545 /** @return true if we are currently suspending updates to the editor's viewport,
546  *  which we do if configured to do so, and if in a drag of some kind.
547  */
548 bool
549 EditorSummary::suspending_editor_updates () const
550 {
551         return (!UIConfiguration::instance().get_update_editor_during_summary_drag () && (_zoom_trim_dragging || _move_dragging));
552 }
553
554 /** Fill in x and y with the editor's current viewable area in summary coordinates */
555 void
556 EditorSummary::get_editor (pair<double, double>* x, pair<double, double>* y) const
557 {
558         assert (x);
559         if (suspending_editor_updates ()) {
560
561                 /* We are dragging, and configured not to update the editor window during drags,
562                  * so just return where the editor will be when the drag finishes.
563                 */
564
565                 *x = _pending_editor_x;
566                 if (y) {
567                         *y = _pending_editor_y;
568                 }
569                 return;
570         }
571
572         /* Otherwise query the editor for its actual position */
573
574         x->first = (_editor->leftmost_sample () - _start) * _x_scale;
575         x->second = x->first + _editor->current_page_samples() * _x_scale;
576
577         if (y) {
578                 y->first = editor_y_to_summary (_editor->vertical_adjustment.get_value ());
579                 y->second = editor_y_to_summary (_editor->vertical_adjustment.get_value () + _editor->visible_canvas_height() - _editor->get_trackview_group()->canvas_origin().y);
580         }
581 }
582
583 /** Get an expression of the position of a point with respect to the view rectangle */
584 EditorSummary::Position
585 EditorSummary::get_position (double x, double y) const
586 {
587         /* how close the mouse has to be to the edge of the view rectangle to be considered `on it',
588            in pixels */
589
590         int x_edge_size = (_view_rectangle_x.second - _view_rectangle_x.first) / 4;
591         x_edge_size = min (x_edge_size, 8);
592         x_edge_size = max (x_edge_size, 1);
593
594         bool const near_left = (std::abs (x - _view_rectangle_x.first) < x_edge_size);
595         bool const near_right = (std::abs (x - _view_rectangle_x.second) < x_edge_size);
596         bool const within_x = _view_rectangle_x.first < x && x < _view_rectangle_x.second;
597
598         if (near_left) {
599                 return LEFT;
600         } else if (near_right) {
601                 return RIGHT;
602         } else if (within_x) {
603                 return INSIDE;
604         } else {
605                 return TO_LEFT_OR_RIGHT;
606         }
607 }
608
609 void
610 EditorSummary::reset_to_extents()
611 {
612         /* reset as if the user never went anywhere outside the extents */
613         _leftmost = max_samplepos;
614         _rightmost = 0;
615
616         _editor->temporal_zoom_extents ();
617         set_background_dirty ();
618 }
619
620
621 void
622 EditorSummary::set_cursor (Position p)
623 {
624         switch (p) {
625         case LEFT:
626                 get_window()->set_cursor (*_editor->_cursors->resize_left);
627                 break;
628         case RIGHT:
629                 get_window()->set_cursor (*_editor->_cursors->resize_right);
630                 break;
631         case INSIDE:
632                 get_window()->set_cursor (*_editor->_cursors->move);
633                 break;
634         case TO_LEFT_OR_RIGHT:
635                 get_window()->set_cursor (*_editor->_cursors->move);
636                 break;
637         default:
638                 assert (0);
639                 get_window()->set_cursor ();
640                 break;
641         }
642 }
643
644 void
645 EditorSummary::summary_zoom_step (int steps /* positive steps to zoom "out" , negative steps to zoom "in" */  )
646 {
647         pair<double, double> xn;
648
649         get_editor (&xn);
650
651         xn.first -= steps;
652         xn.second += steps;
653
654         /* for now, disallow really close zooming-in from the scroomer. (Currently it
655          * causes the start-offset to 'walk' because of integer limitations.
656          * To fix this, probably need to maintain float throught the get/set_editor() path.)
657          */
658         if (steps<0) {
659       if ((xn.second - xn.first) < 2)
660                 return;
661         }
662
663         set_overlays_dirty ();
664         set_editor_x (xn);
665 }
666
667
668 bool
669 EditorSummary::on_motion_notify_event (GdkEventMotion* ev)
670 {
671         if (_move_dragging) {
672
673                 /* To avoid accidental zooming, the mouse must move exactly vertical, not diagonal, to trigger a zoom step
674                  * we use screen coordinates for this, not canvas-based grab_x */
675                 double mx = ev->x;
676                 double dx = mx - _last_mx;
677                 double my = ev->y;
678                 double dy = my - _last_my;
679
680                 /* do zooming in windowed "steps" so it feels more reversible ? */
681                 const int stepsize = 2;
682                 int y_delta = _start_mouse_y - my;
683                 y_delta = y_delta / stepsize;
684
685                 /* do the zoom? */
686                 const float zscale = 3;
687                 if ((dx == 0) && (_last_dx == 0) && (y_delta != _last_y_delta)) {
688
689                         summary_zoom_step (dy * zscale);
690
691                         /* after the zoom we must re-calculate x-pos grabs */
692                         pair<double, double> xr;
693                         get_editor (&xr);
694                         _start_editor_x = xr;
695                         _start_mouse_x = ev->x;
696
697                         _last_y_delta = y_delta;
698                 }
699
700                 /* always track horizontal movement, if any */
701                 if (dx != 0) {
702
703                         double x = _start_editor_x.first;
704                         x += ev->x - _start_mouse_x;
705
706                         if (x < 0) {
707                                 x = 0;
708                         }
709
710                         /* zoom-behavior-tweaks: protect the right edge from expanding beyond the end */
711                         pair<double, double> xr;
712                         get_editor (&xr);
713                         double w = xr.second - xr.first;
714                         if (x + w < get_width()) {
715                                 set_editor (x);
716                         }
717                 }
718
719                 _last_my = my;
720                 _last_mx = mx;
721                 _last_dx = dx;
722                 _last_dy = dy;
723
724         } else if (_zoom_trim_dragging) {
725
726                 pair<double, double> xr = _start_editor_x;
727
728                 double const dx = ev->x - _start_mouse_x;
729
730                 if (_zoom_trim_position == LEFT) {
731                         xr.first += dx;
732                 } else if (_zoom_trim_position == RIGHT) {
733
734                         /* zoom-behavior-tweaks: protect the right edge from expanding beyond the edge */
735                         if ((xr.second + dx) < get_width()) {
736                                 xr.second += dx;
737                         }
738
739                 } else {
740                         assert (0);
741                         xr.first = -1; /* do not change */
742                 }
743
744                 set_overlays_dirty ();
745                 set_cursor (_zoom_trim_position);
746                 set_editor (xr);
747
748         } else {
749                 set_cursor (get_position (ev->x, ev->y));
750         }
751
752         return true;
753 }
754
755 bool
756 EditorSummary::on_button_release_event (GdkEventButton*)
757 {
758         bool const was_suspended = suspending_editor_updates ();
759
760         _move_dragging = false;
761         _zoom_trim_dragging = false;
762         _editor->_dragging_playhead = false;
763         _editor->set_follow_playhead (_old_follow_playhead, false);
764
765         if (was_suspended && _pending_editor_changed) {
766                 set_editor (_pending_editor_x);
767         }
768
769         return true;
770 }
771
772 bool
773 EditorSummary::on_scroll_event (GdkEventScroll* ev)
774 {
775         /* mouse wheel */
776         pair<double, double> xr;
777         get_editor (&xr);
778         double x = xr.first;
779
780         switch (ev->direction) {
781                 case GDK_SCROLL_UP: {
782
783                         summary_zoom_step (-4);
784
785                         return true;
786                 } break;
787
788                 case GDK_SCROLL_DOWN: {
789
790                         summary_zoom_step (4);
791
792                         return true;
793                 } break;
794
795                 case GDK_SCROLL_LEFT:
796                         if (Keyboard::modifier_state_equals (ev->state, Keyboard::ScrollZoomHorizontalModifier)) {
797                                 _editor->temporal_zoom_step (false);
798                         } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
799                                 x -= 64;
800                         } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
801                                 x -= 1;
802                         } else {
803                                 _editor->scroll_left_half_page ();
804                                 return true;
805                         }
806                         break;
807                 case GDK_SCROLL_RIGHT:
808                         if (Keyboard::modifier_state_equals (ev->state, Keyboard::ScrollZoomHorizontalModifier)) {
809                                 _editor->temporal_zoom_step (true);
810                         } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
811                                 x += 64;
812                         } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
813                                 x += 1;
814                         } else {
815                                 _editor->scroll_right_half_page ();
816                                 return true;
817                         }
818                         break;
819                 default:
820                         break;
821         }
822
823         set_editor (x);
824         return true;
825 }
826
827 /** Set the editor to display a x range with the left at a given position
828  *  and a y range with the top at a given position.
829  *  x and y parameters are specified in summary coordinates.
830  *  Zoom is not changed in either direction.
831  */
832 void
833 EditorSummary::set_editor (double const x)
834 {
835         if (_editor->pending_visual_change.idle_handler_id >= 0 && _editor->pending_visual_change.being_handled == true) {
836
837                 /* As a side-effect, the Editor's visual change idle handler processes
838                    pending GTK events.  Hence this motion notify handler can be called
839                    in the middle of a visual change idle handler, and if this happens,
840                    the queue_visual_change calls below modify the variables that the
841                    idle handler is working with.  This causes problems.  Hence this
842                    check.  It ensures that we won't modify the pending visual change
843                    while a visual change idle handler is in progress.  It's not perfect,
844                    as it also means that we won't change these variables if an idle handler
845                    is merely pending but not executing.  But c'est la vie.
846                 */
847
848                 return;
849         }
850
851         set_editor_x (x);
852 }
853
854 /** Set the editor to display a given x range and a y range with the top at a given position.
855  *  The editor's x zoom is adjusted if necessary, but the y zoom is not changed.
856  *  x and y parameters are specified in summary coordinates.
857  */
858 void
859 EditorSummary::set_editor (pair<double,double> const x)
860 {
861         if (_editor->pending_visual_change.idle_handler_id >= 0) {
862                 /* see comment in other set_editor () */
863                 return;
864         }
865
866         if (x.first >= 0) {
867                 set_editor_x (x);
868         }
869 }
870
871 /** Set the left of the x range visible in the editor.
872  *  Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
873  *  @param x new x left position in summary coordinates.
874  */
875 void
876 EditorSummary::set_editor_x (double x)
877 {
878         if (x < 0) {
879                 x = 0;
880         }
881
882         if (suspending_editor_updates ()) {
883                 double const w = _pending_editor_x.second - _pending_editor_x.first;
884                 _pending_editor_x.first = x;
885                 _pending_editor_x.second = x + w;
886                 _pending_editor_changed = true;
887                 set_dirty ();
888         } else {
889                 _editor->reset_x_origin (x / _x_scale + _start);
890         }
891 }
892
893 /** Set the x range visible in the editor.
894  *  Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
895  *  @param x new x range in summary coordinates.
896  */
897 void
898 EditorSummary::set_editor_x (pair<double, double> x)
899 {
900         if (x.first < 0) {
901                 x.first = 0;
902         }
903
904         if (x.second < 0) {
905                 x.second = x.first + 1;
906         }
907
908         if (suspending_editor_updates ()) {
909                 _pending_editor_x = x;
910                 _pending_editor_changed = true;
911                 set_dirty ();
912         } else {
913                 _editor->reset_x_origin (x.first / _x_scale + _start);
914
915                 double const nx = (
916                         ((x.second - x.first) / _x_scale) /
917                         _editor->sample_to_pixel (_editor->current_page_samples())
918                         );
919
920                 if (nx != _editor->get_current_zoom ()) {
921                         _editor->reset_zoom (nx);
922                 }
923         }
924 }
925
926 void
927 EditorSummary::playhead_position_changed (samplepos_t p)
928 {
929         int const o = int (_last_playhead);
930         int const n = int (playhead_sample_to_position (p));
931         if (_session && o != n) {
932                 int a = max(2, min (o, n));
933                 int b = max (o, n);
934                 set_overlays_dirty_rect (a - 2, 0, b + 2, get_height ());
935         }
936 }
937
938 double
939 EditorSummary::editor_y_to_summary (double y) const
940 {
941         double sy = 0;
942         for (TrackViewList::const_iterator i = _editor->track_views.begin (); i != _editor->track_views.end(); ++i) {
943
944                 if ((*i)->hidden()) {
945                         continue;
946                 }
947
948                 double const h = (*i)->effective_height ();
949                 if (y < h) {
950                         /* in this track */
951                         return sy + y * _track_height / h;
952                 }
953
954                 sy += _track_height;
955                 y -= h;
956         }
957
958         return sy;
959 }
960
961 void
962 EditorSummary::routes_added (list<RouteTimeAxisView*> const & r)
963 {
964         for (list<RouteTimeAxisView*>::const_iterator i = r.begin(); i != r.end(); ++i) {
965                 /* Connect to the relevant signal for the route so that we know when its colour has changed */
966                 (*i)->route()->presentation_info().PropertyChanged.connect (*this, invalidator (*this), boost::bind (&EditorSummary::route_gui_changed, this, _1), gui_context ());
967                 boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> ((*i)->route ());
968                 if (tr) {
969                         tr->PlaylistChanged.connect (*this, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context ());
970                 }
971         }
972
973         set_background_dirty ();
974 }
975
976 void
977 EditorSummary::route_gui_changed (PBD::PropertyChange const& what_changed)
978 {
979         if (what_changed.contains (Properties::color)) {
980                 set_background_dirty ();
981         }
982 }
983
984 double
985 EditorSummary::playhead_sample_to_position (samplepos_t t) const
986 {
987         return (t - _start) * _x_scale;
988 }
989
990 samplepos_t
991 EditorSummary::position_to_playhead_sample_to_position (double pos) const
992 {
993         return _start  + (pos * _x_scale);
994 }