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