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