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