Add a PianoKeyboard to GenericUI (on a MIDI track)
[ardour.git] / gtk2_ardour / editor_summary.cc
index aae814d8e564df3945e4a0c2be4d3a95076ae25c..95b54cdf02a3b0981176143dfafb9e8e57335d77 100644 (file)
 */
 
 #include "ardour/session.h"
+
+#include "canvas/debug.h"
+
+#include <gtkmm/menu.h>
+#include <gtkmm/menuitem.h>
+
 #include "time_axis_view.h"
 #include "streamview.h"
 #include "editor_summary.h"
 #include "region_view.h"
 #include "rgb_macros.h"
 #include "keyboard.h"
+#include "editor_routes.h"
+#include "editor_cursors.h"
+#include "mouse_cursors.h"
+#include "route_time_axis.h"
+#include "ui_config.h"
+
+#include "pbd/i18n.h"
 
 using namespace std;
 using namespace ARDOUR;
@@ -38,145 +51,160 @@ EditorSummary::EditorSummary (Editor* e)
        : EditorComponent (e),
          _start (0),
          _end (1),
-         _overhang_fraction (0.1),
          _x_scale (1),
-         _y_scale (1),
+         _track_height (16),
          _last_playhead (-1),
          _move_dragging (false),
-         _moved (false),
-         _zoom_dragging (false)
+         _view_rectangle_x (0, 0),
+         _view_rectangle_y (0, 0),
+         _zoom_trim_dragging (false),
+         _old_follow_playhead (false),
+         _image (0),
+         _background_dirty (true)
+{
+       CairoWidget::use_nsglview ();
+       add_events (Gdk::POINTER_MOTION_MASK|Gdk::KEY_PRESS_MASK|Gdk::KEY_RELEASE_MASK|Gdk::ENTER_NOTIFY_MASK|Gdk::LEAVE_NOTIFY_MASK);
+       set_flags (get_flags() | Gtk::CAN_FOCUS);
 
+       UIConfiguration::instance().ParameterChanged.connect (sigc::mem_fun (*this, &EditorSummary::parameter_changed));
+}
+
+EditorSummary::~EditorSummary ()
 {
-       Region::RegionPropertyChanged.connect (region_property_connection, boost::bind (&CairoWidget::set_dirty, this), gui_context());
-       _editor->playhead_cursor->PositionChanged.connect (position_connection, ui_bind (&EditorSummary::playhead_position_changed, this, _1), gui_context());
+       cairo_surface_destroy (_image);
 }
 
-/** Connect to a session.
- *  @param s Session.
- */
 void
-EditorSummary::set_session (Session* s)
+EditorSummary::parameter_changed (string p)
 {
-       EditorComponent::set_session (s);
 
-       set_dirty ();
-
-       if (_session) {
-               _session->RegionRemoved.connect (_session_connections, boost::bind (&EditorSummary::set_dirty, this), gui_context());
-               _session->StartTimeChanged.connect (_session_connections, boost::bind (&EditorSummary::set_dirty, this), gui_context());
-               _session->EndTimeChanged.connect (_session_connections, boost::bind (&EditorSummary::set_dirty, this), gui_context());
+       if (p == "color-regions-using-track-color") {
+               set_background_dirty ();
        }
 }
 
-/** Handle an expose event.
- *  @param event Event from GTK.
+/** Handle a size allocation.
+ *  @param alloc GTK allocation.
  */
-bool
-EditorSummary::on_expose_event (GdkEventExpose* event)
+void
+EditorSummary::on_size_allocate (Gtk::Allocation& alloc)
 {
-       CairoWidget::on_expose_event (event);
-
-       if (_session == 0) {
-               return false;
-       }
-
-       cairo_t* cr = gdk_cairo_create (get_window()->gobj());
-
-       /* Render the view rectangle */
+       CairoWidget::on_size_allocate (alloc);
+       set_background_dirty ();
+}
 
-       pair<double, double> x;
-       pair<double, double> y;
-       get_editor (&x, &y);
 
-       cairo_move_to (cr, x.first, y.first);
-       cairo_line_to (cr, x.second, y.first);
-       cairo_line_to (cr, x.second, y.second);
-       cairo_line_to (cr, x.first, y.second);
-       cairo_line_to (cr, x.first, y.first);
-       cairo_set_source_rgba (cr, 1, 1, 1, 0.25);
-       cairo_fill_preserve (cr);
-       cairo_set_line_width (cr, 1);
-       cairo_set_source_rgba (cr, 1, 1, 1, 0.5);
-       cairo_stroke (cr);
-
-       /* Playhead */
+/** Connect to a session.
+ *  @param s Session.
+ */
+void
+EditorSummary::set_session (Session* s)
+{
+       SessionHandlePtr::set_session (s);
 
-       cairo_set_line_width (cr, 1);
-       /* XXX: colour should be set from configuration file */
-       cairo_set_source_rgba (cr, 1, 0, 0, 1);
+       set_dirty ();
 
-       double const p = (_editor->playhead_cursor->current_frame - _start) * _x_scale;
-       cairo_move_to (cr, p, 0);
-       cairo_line_to (cr, p, _height);
-       cairo_stroke (cr);
-       _last_playhead = p;
+       /* Note: the EditorSummary already finds out about new regions from Editor::region_view_added
+        * (which attaches to StreamView::RegionViewAdded), and cut regions by the RegionPropertyChanged
+        * emitted when a cut region is added to the `cutlist' playlist.
+        */
 
-       cairo_destroy (cr);
+       if (_session) {
+               Region::RegionPropertyChanged.connect (region_property_connection, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
+               PresentationInfo::Change.connect (route_ctrl_id_connection, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
+               _editor->playhead_cursor->PositionChanged.connect (position_connection, invalidator (*this), boost::bind (&EditorSummary::playhead_position_changed, this, _1), gui_context());
+               _session->StartTimeChanged.connect (_session_connections, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
+               _session->EndTimeChanged.connect (_session_connections, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
+               _editor->selection->RegionsChanged.connect (sigc::mem_fun(*this, &EditorSummary::set_background_dirty));
+       }
 
-       return true;
+       _leftmost = max_framepos;
+       _rightmost = 0;
 }
 
-/** Render the required regions to a cairo context.
- *  @param cr Context.
- */
 void
-EditorSummary::render (cairo_t* cr)
+EditorSummary::render_background_image ()
 {
-       /* background */
+       cairo_surface_destroy (_image); // passing NULL is safe
+       _image = cairo_image_surface_create (CAIRO_FORMAT_RGB24, get_width (), get_height ());
+
+       cairo_t* cr = cairo_create (_image);
+
+       /* background (really just the dividing lines between tracks */
 
        cairo_set_source_rgb (cr, 0, 0, 0);
-       cairo_rectangle (cr, 0, 0, _width, _height);
+       cairo_rectangle (cr, 0, 0, get_width(), get_height());
        cairo_fill (cr);
 
-       if (_session == 0) {
-               return;
-       }
-
        /* compute start and end points for the summary */
-       
-       nframes_t const session_length = _session->current_end_frame() - _session->current_start_frame ();
-       double const theoretical_start = _session->current_start_frame() - session_length * _overhang_fraction;
-       _start = theoretical_start > 0 ? theoretical_start : 0;
-       _end = _session->current_end_frame() + session_length * _overhang_fraction;
 
-       /* compute total height of all tracks */
+       std::pair<framepos_t, framepos_t> ext = _editor->session_gui_extents();
+       double theoretical_start = ext.first;
+       double theoretical_end = ext.second;
 
-       int h = 0;
-       int max_height = 0;
-       for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
-               int const t = (*i)->effective_height ();
-               h += t;
-               max_height = max (max_height, t);
-       }
+       /* the summary should encompass the full extent of everywhere we've visited since the session was opened */
+       if ( _leftmost < theoretical_start)
+               theoretical_start = _leftmost;
+       if ( _rightmost > theoretical_end )
+               theoretical_end = _rightmost;
 
-       _x_scale = static_cast<double> (_width) / (_end - _start);
-       _y_scale = static_cast<double> (_height) / h;
+       /* range-check */
+       _start = theoretical_start > 0 ? theoretical_start : 0;
+       _end = theoretical_end < max_framepos ? theoretical_end : max_framepos;
 
-       /* tallest a region should ever be in the summary, in pixels */
-       int const tallest_region_pixels = 4;
+       /* calculate x scale */
+       if (_end != _start) {
+               _x_scale = static_cast<double> (get_width()) / (_end - _start);
+       } else {
+               _x_scale = 1;
+       }
 
-       if (max_height * _y_scale > tallest_region_pixels) {
-               _y_scale = static_cast<double> (tallest_region_pixels) / max_height;
+       /* compute track height */
+       int N = 0;
+       for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
+               if (!(*i)->hidden()) {
+                       ++N;
+               }
+       }
 
+       if (N == 0) {
+               _track_height = 16;
+       } else {
+               _track_height = (double) get_height() / N;
        }
 
-       /* render regions */
+       /* render tracks and regions */
 
        double y = 0;
        for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
+
+               if ((*i)->hidden()) {
+                       continue;
+               }
+
+               /* paint a non-bg colored strip to represent the track itself */
+
+               if ( _track_height > 4 ) {
+                       cairo_set_source_rgb (cr, 0.2, 0.2, 0.2);
+                       cairo_set_line_width (cr, _track_height - 1);
+                       cairo_move_to (cr, 0, y + _track_height / 2);
+                       cairo_line_to (cr, get_width(), y + _track_height / 2);
+                       cairo_stroke (cr);
+               }
+               
                StreamView* s = (*i)->view ();
 
                if (s) {
-                       double const h = (*i)->effective_height () * _y_scale;
-                       cairo_set_line_width (cr, h);
+                       cairo_set_line_width (cr, _track_height * 0.8);
 
                        s->foreach_regionview (sigc::bind (
                                                       sigc::mem_fun (*this, &EditorSummary::render_region),
                                                       cr,
-                                                      y + h / 2
+                                                      y + _track_height / 2
                                                       ));
-                       y += h;
                }
+
+               y += _track_height;
        }
 
        /* start and end markers */
@@ -184,15 +212,91 @@ EditorSummary::render (cairo_t* cr)
        cairo_set_line_width (cr, 1);
        cairo_set_source_rgb (cr, 1, 1, 0);
 
-       double const p = (_session->current_start_frame() - _start) * _x_scale;
+       const double p = (_session->current_start_frame() - _start) * _x_scale;
        cairo_move_to (cr, p, 0);
-       cairo_line_to (cr, p, _height);
-       cairo_stroke (cr);
+       cairo_line_to (cr, p, get_height());
 
        double const q = (_session->current_end_frame() - _start) * _x_scale;
        cairo_move_to (cr, q, 0);
-       cairo_line_to (cr, q, _height);
+       cairo_line_to (cr, q, get_height());
+       cairo_stroke (cr);
+
+       cairo_destroy (cr);
+}
+
+/** Render the required regions to a cairo context.
+ *  @param cr Context.
+ */
+void
+EditorSummary::render (Cairo::RefPtr<Cairo::Context> const& ctx, cairo_rectangle_t*)
+{
+       cairo_t* cr = ctx->cobj();
+
+       if (_session == 0) {
+               return;
+       }
+
+       /* maintain the leftmost and rightmost locations that we've ever reached */
+       framecnt_t const leftmost = _editor->leftmost_sample ();
+       if ( leftmost < _leftmost) {
+               _leftmost = leftmost;
+               _background_dirty = true;
+       }
+       framecnt_t const rightmost = leftmost + _editor->current_page_samples();
+       if ( rightmost > _rightmost) {
+               _rightmost = rightmost;
+               _background_dirty = true;
+       }
+
+       //draw the background (regions, markers, etc ) if they've changed
+       if (!_image || _background_dirty) {
+               render_background_image ();
+               _background_dirty = false;
+       }
+
+       cairo_push_group (cr);
+
+       /* Fill with the background image */
+
+       cairo_rectangle (cr, 0, 0, get_width(), get_height());
+       cairo_set_source_surface (cr, _image, 0, 0);
+       cairo_fill (cr);
+
+       /* Render the view rectangle.  If there is an editor visual pending, don't update
+        * the view rectangle now --- wait until the expose event that we'll get after
+        * the visual change.  This prevents a flicker.
+        */
+
+       if (_editor->pending_visual_change.idle_handler_id < 0) {
+               get_editor (&_view_rectangle_x, &_view_rectangle_y);
+       }
+
+       int32_t width = _view_rectangle_x.second - _view_rectangle_x.first;
+       std::min(8, width);
+       cairo_rectangle (cr, _view_rectangle_x.first, 0, width, get_height ());
+       cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
+       cairo_fill (cr);
+
+       /* horiz zoom */
+       cairo_rectangle (cr, _view_rectangle_x.first, 0, width, get_height ());
+       cairo_set_line_width (cr, 1);
+       cairo_set_source_rgba (cr, 1, 1, 1, 0.9);
+       cairo_stroke (cr);
+
+       /* Playhead */
+
+       cairo_set_line_width (cr, 1);
+       /* XXX: colour should be set from configuration file */
+       cairo_set_source_rgba (cr, 1, 0, 0, 1);
+
+       const double ph= playhead_frame_to_position (_editor->playhead_cursor->current_frame());
+       cairo_move_to (cr, ph, 0);
+       cairo_line_to (cr, ph, get_height());
        cairo_stroke (cr);
+       cairo_pop_group_to_source (cr);
+       cairo_paint (cr);
+       _last_playhead = ph;
+
 }
 
 /** Render a region for the summary.
@@ -221,23 +325,41 @@ EditorSummary::render_region (RegionView* r, cairo_t* cr, double y) const
        cairo_stroke (cr);
 }
 
+void
+EditorSummary::set_background_dirty ()
+{
+       if (!_background_dirty) {
+               _background_dirty = true;
+               set_dirty ();
+       }
+}
+
 /** Set the summary so that just the overlays (viewbox, playhead etc.) will be re-rendered */
 void
 EditorSummary::set_overlays_dirty ()
 {
-       ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty)
+       ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty);
        queue_draw ();
 }
 
+/** Set the summary so that just the overlays (viewbox, playhead etc.) in a given area will be re-rendered */
+void
+EditorSummary::set_overlays_dirty_rect (int x, int y, int w, int h)
+{
+       ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty_rect);
+       queue_draw_area (x, y, w, h);
+}
+
+
 /** Handle a size request.
  *  @param req GTK requisition
  */
 void
 EditorSummary::on_size_request (Gtk::Requisition *req)
 {
-       /* Use a dummy, small width and the actual height that we want */
-       req->width = 64;
-       req->height = 32;
+       /* The left/right buttons will determine our height */
+       req->width = -1;
+       req->height = -1;
 }
 
 
@@ -245,147 +367,352 @@ void
 EditorSummary::centre_on_click (GdkEventButton* ev)
 {
        pair<double, double> xr;
-       pair<double, double> yr;
-       get_editor (&xr, &yr);
+       get_editor (&xr);
 
        double const w = xr.second - xr.first;
-       double const h = yr.second - yr.first;
+       double ex = ev->x - w / 2;
+       if (ex < 0) {
+               ex = 0;
+       } else if ((ex + w) > get_width()) {
+               ex = get_width() - w;
+       }
 
-       xr.first = ev->x - w / 2;
-       xr.second = ev->x + w / 2;
-       yr.first = ev->y - h / 2;
-       yr.second = ev->y + h / 2;
+       set_editor (ex);
+}
 
-       if (xr.first < 0) {
-               xr.first = 0;
-               xr.second = w;
-       } else if (xr.second > _width) {
-               xr.second = _width;
-               xr.first = _width - w;
-       }
+bool
+EditorSummary::on_enter_notify_event (GdkEventCrossing*)
+{
+       grab_focus ();
+       Keyboard::magic_widget_grab_focus ();
+       return false;
+}
+
+bool
+EditorSummary::on_leave_notify_event (GdkEventCrossing*)
+{
+       /* there are no inferior/child windows, so any leave event means that
+          we're gone.
+       */
+       Keyboard::magic_widget_drop_focus ();
+       return false;
+}
 
-       if (yr.first < 0) {
-               yr.first = 0;
-               yr.second = h;
-       } else if (yr.second > _height) {
-               yr.second = _height;
-               yr.first = _height - h;
+bool
+EditorSummary::on_key_press_event (GdkEventKey* key)
+{
+       gint x, y;
+       GtkAccelKey set_playhead_accel;
+       if (gtk_accel_map_lookup_entry ("<Actions>/Editor/set-playhead", &set_playhead_accel)) {
+               if (key->keyval == set_playhead_accel.accel_key && (int) key->state == set_playhead_accel.accel_mods) {
+                       if (_session) {
+                               get_pointer (x, y);
+                               _session->request_locate (_start + (framepos_t) x / _x_scale, _session->transport_rolling());
+                               return true;
+                       }
+               }
        }
 
-       set_editor (xr, yr);
+       return false;
+}
+
+bool
+EditorSummary::on_key_release_event (GdkEventKey* key)
+{
+
+       GtkAccelKey set_playhead_accel;
+       if (gtk_accel_map_lookup_entry ("<Actions>/Editor/set-playhead", &set_playhead_accel)) {
+               if (key->keyval == set_playhead_accel.accel_key && (int) key->state == set_playhead_accel.accel_mods) {
+                       return true;
+               }
+       }
+       return false;
 }
 
+#include "gtkmm2ext/utils.h"
+
 /** Handle a button press.
  *  @param ev GTK event.
  */
 bool
 EditorSummary::on_button_press_event (GdkEventButton* ev)
 {
-       if (ev->button == 1) {
+       _old_follow_playhead = _editor->follow_playhead ();
+
+       if (ev->button == 3) {  //right-click:  show the reset menu action
+               using namespace Gtk::Menu_Helpers;
+               Gtk::Menu* m = manage (new Gtk::Menu);
+               MenuList& items = m->items ();
+               items.push_back(MenuElem(_("Reset Summary to Extents"),
+                       sigc::mem_fun(*this, &EditorSummary::reset_to_extents)));
+               m->popup (ev->button, ev->time);
+               return true;
+       }
 
-               pair<double, double> xr;
-               pair<double, double> yr;
-               get_editor (&xr, &yr);
+       if (ev->button != 1) {
+               return true;
+       }
 
-               _start_editor_x = xr;
-               _start_editor_y = yr;
-               _start_mouse_x = ev->x;
-               _start_mouse_y = ev->y;
+       pair<double, double> xr;
+       get_editor (&xr);
 
-               if (Keyboard::modifier_state_equals (ev->state, Keyboard::PrimaryModifier)) {
+       _start_editor_x = xr;
+       _start_mouse_x = ev->x;
+       _start_mouse_y = ev->y;
+       _start_position = get_position (ev->x, ev->y);
 
-                       /* primary-modifier-click: start a zoom drag */
+       if (_start_position != INSIDE && _start_position != TO_LEFT_OR_RIGHT) {
 
-                       double const hx = (xr.first + xr.second) * 0.5;
-                       _zoom_left = ev->x < hx;
-                       _zoom_dragging = true;
-                       _editor->_dragging_playhead = true;
+               /* start a zoom_trim drag */
 
+               _zoom_trim_position = get_position (ev->x, ev->y);
+               _zoom_trim_dragging = true;
+               _editor->_dragging_playhead = true;
+               _editor->set_follow_playhead (false);
 
-                       /* In theory, we could support vertical dragging, which logically
-                          might scale track heights in order to make the editor reflect
-                          the dragged viewbox.  However, having tried this:
-                          a) it's hard to do
-                          b) it's quite slow
-                          c) it doesn't seem particularly useful, especially with the
-                          limited height of the summary
+               if (suspending_editor_updates ()) {
+                       get_editor (&_pending_editor_x, &_pending_editor_y);
+                       _pending_editor_changed = false;
+               }
 
-                          So at the moment we don't support that...
-                       */
+       } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
 
+               /* secondary-modifier-click: locate playhead */
+               if (_session) {
+                       _session->request_locate (ev->x / _x_scale + _start);
+               }
 
-               } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
+       } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
 
-                       /* secondary-modifier-click: locate playhead */
-                       if (_session) {
-                               _session->request_locate (ev->x / _x_scale + _start);
-                       }
+               centre_on_click (ev);
 
-               } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
+       } else {
 
-                       centre_on_click (ev);
+               /* start a move+zoom drag */
+               get_editor (&_pending_editor_x, &_pending_editor_y);
+               _pending_editor_changed = false;
+               _editor->_dragging_playhead = true;
+               _editor->set_follow_playhead (false);
+
+               _move_dragging = true;
+               
+               _last_mx = ev->x;
+               _last_my = ev->y;
+               _last_dx = 0;
+               _last_dy = 0;
+               _last_y_delta = 0;
+
+               get_window()->set_cursor (*_editor->_cursors->expand_left_right);
+       
+       }
 
-               } else {
+       return true;
+}
 
-                       /* ordinary click: start a move drag */
+/** @return true if we are currently suspending updates to the editor's viewport,
+ *  which we do if configured to do so, and if in a drag of some kind.
+ */
+bool
+EditorSummary::suspending_editor_updates () const
+{
+       return (!UIConfiguration::instance().get_update_editor_during_summary_drag () && (_zoom_trim_dragging || _move_dragging));
+}
 
-                       _move_dragging = true;
-                       _moved = false;
-                       _editor->_dragging_playhead = true;
+/** Fill in x and y with the editor's current viewable area in summary coordinates */
+void
+EditorSummary::get_editor (pair<double, double>* x, pair<double, double>* y) const
+{
+       assert (x);
+       if (suspending_editor_updates ()) {
+
+               /* We are dragging, and configured not to update the editor window during drags,
+                * so just return where the editor will be when the drag finishes.
+               */
+
+               *x = _pending_editor_x;
+               if (y) {
+                       *y = _pending_editor_y;
                }
+               return;
        }
 
-       return true;
+       /* Otherwise query the editor for its actual position */
+
+       x->first = (_editor->leftmost_sample () - _start) * _x_scale;
+       x->second = x->first + _editor->current_page_samples() * _x_scale;
+
+       if (y) {
+               y->first = editor_y_to_summary (_editor->vertical_adjustment.get_value ());
+               y->second = editor_y_to_summary (_editor->vertical_adjustment.get_value () + _editor->visible_canvas_height() - _editor->get_trackview_group()->canvas_origin().y);
+       }
+}
+
+/** Get an expression of the position of a point with respect to the view rectangle */
+EditorSummary::Position
+EditorSummary::get_position (double x, double y) const
+{
+       /* how close the mouse has to be to the edge of the view rectangle to be considered `on it',
+          in pixels */
+
+       int x_edge_size = (_view_rectangle_x.second - _view_rectangle_x.first) / 4;
+       x_edge_size = min (x_edge_size, 8);
+       x_edge_size = max (x_edge_size, 1);
+
+       bool const near_left = (std::abs (x - _view_rectangle_x.first) < x_edge_size);
+       bool const near_right = (std::abs (x - _view_rectangle_x.second) < x_edge_size);
+       bool const within_x = _view_rectangle_x.first < x && x < _view_rectangle_x.second;
+
+       if (near_left) {
+               return LEFT;
+       } else if (near_right) {
+               return RIGHT;
+       } else if (within_x) {
+               return INSIDE;
+       } else {
+               return TO_LEFT_OR_RIGHT;
+       }
 }
 
 void
-EditorSummary::get_editor (pair<double, double>* x, pair<double, double>* y) const
+EditorSummary::reset_to_extents()
 {
-       x->first = (_editor->leftmost_position () - _start) * _x_scale;
-       x->second = x->first + _editor->current_page_frames() * _x_scale;
+       //reset as if the user never went anywhere outside the extents
+       _leftmost = max_framepos;
+       _rightmost = 0;
 
-       y->first = _editor->vertical_adjustment.get_value() * _y_scale;
-       y->second = y->first + (_editor->canvas_height () - _editor->get_canvas_timebars_vsize()) * _y_scale;
+       _editor->temporal_zoom_extents ();
+       set_background_dirty ();
 }
 
-bool
-EditorSummary::on_motion_notify_event (GdkEventMotion* ev)
+
+void
+EditorSummary::set_cursor (Position p)
 {
-       pair<double, double> xr = _start_editor_x;
-       pair<double, double> yr = _start_editor_y;
+       switch (p) {
+       case LEFT:
+               get_window()->set_cursor (*_editor->_cursors->resize_left);
+               break;
+       case RIGHT:
+               get_window()->set_cursor (*_editor->_cursors->resize_right);
+               break;
+       case INSIDE:
+               get_window()->set_cursor (*_editor->_cursors->move);
+               break;
+       case TO_LEFT_OR_RIGHT:
+               get_window()->set_cursor (*_editor->_cursors->move);
+               break;
+       default:
+               assert (0);
+               get_window()->set_cursor ();
+               break;
+       }
+}
 
-       if (_move_dragging) {
+void
+EditorSummary::summary_zoom_step ( int steps /* positive steps to zoom "out" , negative steps to zoom "in" */  )
+{
+       pair<double, double> xn;
 
-               _moved = true;
+       get_editor (&xn);
 
-               xr.first += ev->x - _start_mouse_x;
-               xr.second += ev->x - _start_mouse_x;
-               yr.first += ev->y - _start_mouse_y;
-               yr.second += ev->y - _start_mouse_y;
+       xn.first -= steps;
+       xn.second += steps;
 
-               if (xr.first < 0) {
-                       xr.second -= xr.first;
-                       xr.first = 0;
+       //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 )
+       if (steps<0) {
+      if ( (xn.second-xn.first) < 2)
+               return;
+       }
+
+       set_overlays_dirty ();
+       set_editor_x (xn);
+}
+
+
+bool
+EditorSummary::on_motion_notify_event (GdkEventMotion* ev)
+{
+       if (_move_dragging) {
+
+               //To avoid accidental zooming, the mouse must move exactly vertical, not diagonal, to trigger a zoom step
+               //we use screen coordinates for this, not canvas-based grab_x
+               double mx = ev->x;
+               double dx = mx - _last_mx;
+               double my = ev->y;
+               double dy = my - _last_my;
+
+               //do zooming in windowed "steps" so it feels more reversible ?
+               const int stepsize = 2;
+               int y_delta = _start_mouse_y - my;
+               y_delta = y_delta / stepsize;
+
+               //do the zoom?
+               const float zscale = 3;
+               if ( (dx==0) && (_last_dx ==0) && (y_delta != _last_y_delta) ) {
+
+                       summary_zoom_step( dy * zscale );
+
+                       //after the zoom we must re-calculate x-pos grabs
+                       pair<double, double> xr;
+                       get_editor (&xr);
+                       _start_editor_x = xr;
+                       _start_mouse_x = ev->x;
+                       
+                       _last_y_delta = y_delta;
                }
+               
+               //always track horizontal movement, if any
+               if ( dx != 0 ) {
+
+                       double x = _start_editor_x.first;
+                       x += ev->x - _start_mouse_x;
+                       
+                       if (x < 0) {
+                               x = 0;
+                       }
 
-               if (yr.first < 0) {
-                       yr.second -= yr.first;
-                       yr.first = 0;
+                       //zoom-behavior-tweaks
+                       //protect the right edge from expanding beyond the end
+                       pair<double, double> xr;
+                       get_editor (&xr);
+                       double w = xr.second - xr.first;
+                       if ( x + w < get_width() ) {
+                               set_editor (x);
+                       }
                }
 
-               set_editor (xr, yr);
+               _last_my = my;
+               _last_mx = mx;
+               _last_dx = dx;
+               _last_dy = dy;
+
+       } else if (_zoom_trim_dragging) {
 
-       } else if (_zoom_dragging) {
+               pair<double, double> xr = _start_editor_x;
 
                double const dx = ev->x - _start_mouse_x;
 
-               if (_zoom_left) {
+               if (_zoom_trim_position == LEFT) {
                        xr.first += dx;
+               } else if (_zoom_trim_position == RIGHT) {
+
+                       //zoom-behavior-tweaks
+                       //protect the right edge from expanding beyond the edge
+                       if ( (xr.second + dx) < get_width() ) {
+                               xr.second += dx;
+                       }
+
                } else {
-                       xr.second += dx;
+                       assert (0);
+                       xr.first = -1; /* do not change */
                }
 
-               set_editor (xr, yr);
+               set_overlays_dirty ();
+               set_cursor (_zoom_trim_position);
+               set_editor (xr);
+
+       } else {
+               set_cursor ( get_position(ev->x, ev->y) );
        }
 
        return true;
@@ -394,9 +721,17 @@ EditorSummary::on_motion_notify_event (GdkEventMotion* ev)
 bool
 EditorSummary::on_button_release_event (GdkEventButton*)
 {
+       bool const was_suspended = suspending_editor_updates ();
+
        _move_dragging = false;
-       _zoom_dragging = false;
+       _zoom_trim_dragging = false;
        _editor->_dragging_playhead = false;
+       _editor->set_follow_playhead (_old_follow_playhead, false);
+
+       if (was_suspended && _pending_editor_changed) {
+               set_editor (_pending_editor_x);
+       }
+
        return true;
 }
 
@@ -404,92 +739,148 @@ bool
 EditorSummary::on_scroll_event (GdkEventScroll* ev)
 {
        /* mouse wheel */
-
        pair<double, double> xr;
-       pair<double, double> yr;
-       get_editor (&xr, &yr);
-
-       double amount = 8;
-
-       if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
-               amount = 64;
-       } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
-               amount = 1;
-       }
-
-       if (Keyboard::modifier_state_equals (ev->state, Keyboard::PrimaryModifier)) {
-
-               /* primary-wheel == left-right scrolling */
-
-               if (ev->direction == GDK_SCROLL_UP) {
-                       xr.first += amount;
-                       xr.second += amount;
-               } else if (ev->direction == GDK_SCROLL_DOWN) {
-                       xr.first -= amount;
-                       xr.second -= amount;
-               }
-
-       } else {
-
-               if (ev->direction == GDK_SCROLL_DOWN) {
-                       yr.first += amount;
-                       yr.second += amount;
-               } else if (ev->direction == GDK_SCROLL_UP) {
-                       yr.first -= amount;
-                       yr.second -= amount;
-               } else if (ev->direction == GDK_SCROLL_LEFT) {
-                       xr.first -= amount;
-                       xr.second -= amount;
-               } else if (ev->direction == GDK_SCROLL_RIGHT) {
-                       xr.first += amount;
-                       xr.second += amount;
-               }
+       get_editor (&xr);
+       double x = xr.first;
+
+       switch (ev->direction) {
+               case GDK_SCROLL_UP: {
+                       
+                       summary_zoom_step( -4 );
+               
+                       return true;
+               } break;
+               
+               case GDK_SCROLL_DOWN: {
+                       
+                       summary_zoom_step( 4 );
+               
+                       return true;
+               } break;
+               
+               case GDK_SCROLL_LEFT:
+                       if (Keyboard::modifier_state_equals (ev->state, Keyboard::ScrollZoomHorizontalModifier)) {
+                               _editor->temporal_zoom_step (false);
+                       } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
+                               x -= 64;
+                       } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
+                               x -= 1;
+                       } else {
+                               _editor->scroll_left_half_page ();
+                               return true;
+                       }
+                       break;
+               case GDK_SCROLL_RIGHT:
+                       if (Keyboard::modifier_state_equals (ev->state, Keyboard::ScrollZoomHorizontalModifier)) {
+                               _editor->temporal_zoom_step (true);
+                       } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
+                               x += 64;
+                       } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
+                               x += 1;
+                       } else {
+                               _editor->scroll_right_half_page ();
+                               return true;
+                       }
+                       break;
+               default:
+                       break;
        }
 
-       set_editor (xr, yr);
+       set_editor (x);
        return true;
 }
 
+/** Set the editor to display a x range with the left at a given position
+ *  and a y range with the top at a given position.
+ *  x and y parameters are specified in summary coordinates.
+ *  Zoom is not changed in either direction.
+ */
 void
-EditorSummary::set_editor (pair<double,double> const & x, pair<double, double> const & y)
+EditorSummary::set_editor (double const x)
 {
-       if (_editor->pending_visual_change.idle_handler_id < 0) {
+       if (_editor->pending_visual_change.idle_handler_id >= 0 && _editor->pending_visual_change.being_handled == true) {
 
                /* As a side-effect, the Editor's visual change idle handler processes
                   pending GTK events.  Hence this motion notify handler can be called
                   in the middle of a visual change idle handler, and if this happens,
                   the queue_visual_change calls below modify the variables that the
-                  idle handler is working with.  This causes problems.  Hence the
-                  check above.  It ensures that we won't modify the pending visual change
+                  idle handler is working with.  This causes problems.  Hence this
+                  check.  It ensures that we won't modify the pending visual change
                   while a visual change idle handler is in progress.  It's not perfect,
                   as it also means that we won't change these variables if an idle handler
                   is merely pending but not executing.  But c'est la vie.
                */
 
-               /* proposed bottom of the editor with the requested position */
-               double const pb = y.second / _y_scale;
+               return;
+       }
 
-               /* bottom of the canvas */
-               double const ch = _editor->full_canvas_height - _editor->canvas_timebars_vsize;
+       set_editor_x (x);
+}
 
-               /* requested y position */
-               double ly = y.first / _y_scale;
+/** Set the editor to display a given x range and a y range with the top at a given position.
+ *  The editor's x zoom is adjusted if necessary, but the y zoom is not changed.
+ *  x and y parameters are specified in summary coordinates.
+ */
+void
+EditorSummary::set_editor (pair<double,double> const x)
+{
+       if (_editor->pending_visual_change.idle_handler_id >= 0) {
+               /* see comment in other set_editor () */
+               return;
+       }
 
-               /* clamp y position so as not to go off the bottom */
-               if (pb > ch) {
-                       ly -= (pb - ch);
-               }
+       if (x.first >= 0) {
+               set_editor_x (x);
+       }
+}
 
-               if (ly < 0) {
-                       ly = 0;
-               }
+/** Set the left of the x range visible in the editor.
+ *  Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
+ *  @param x new x left position in summary coordinates.
+ */
+void
+EditorSummary::set_editor_x (double x)
+{
+       if (x < 0) {
+               x = 0;
+       }
+
+       if (suspending_editor_updates ()) {
+               double const w = _pending_editor_x.second - _pending_editor_x.first;
+               _pending_editor_x.first = x;
+               _pending_editor_x.second = x + w;
+               _pending_editor_changed = true;
+               set_dirty ();
+       } else {
+               _editor->reset_x_origin (x / _x_scale + _start);
+       }
+}
 
+/** Set the x range visible in the editor.
+ *  Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
+ *  @param x new x range in summary coordinates.
+ */
+void
+EditorSummary::set_editor_x (pair<double, double> x)
+{
+       if (x.first < 0) {
+               x.first = 0;
+       }
+
+       if (x.second < 0) {
+               x.second = x.first + 1;
+       }
+
+       if (suspending_editor_updates ()) {
+               _pending_editor_x = x;
+               _pending_editor_changed = true;
+               set_dirty ();
+       } else {
                _editor->reset_x_origin (x.first / _x_scale + _start);
-               _editor->reset_y_origin (ly);
 
                double const nx = (
                        ((x.second - x.first) / _x_scale) /
-                       _editor->frame_to_unit (_editor->current_page_frames())
+                       _editor->sample_to_pixel (_editor->current_page_samples())
                        );
 
                if (nx != _editor->get_current_zoom ()) {
@@ -499,11 +890,71 @@ EditorSummary::set_editor (pair<double,double> const & x, pair<double, double> c
 }
 
 void
-EditorSummary::playhead_position_changed (nframes64_t p)
+EditorSummary::playhead_position_changed (framepos_t p)
 {
-       if (_session && int (p * _x_scale) != int (_last_playhead)) {
-               set_overlays_dirty ();
+       int const o = int (_last_playhead);
+       int const n = int (playhead_frame_to_position (p));
+       if (_session && o != n) {
+               int a = max(2, min (o, n));
+               int b = max (o, n);
+               set_overlays_dirty_rect (a - 2, 0, b + 2, get_height ());
+       }
+}
+
+double
+EditorSummary::editor_y_to_summary (double y) const
+{
+       double sy = 0;
+       for (TrackViewList::const_iterator i = _editor->track_views.begin (); i != _editor->track_views.end(); ++i) {
+
+               if ((*i)->hidden()) {
+                       continue;
+               }
+
+               double const h = (*i)->effective_height ();
+               if (y < h) {
+                       /* in this track */
+                       return sy + y * _track_height / h;
+               }
+
+               sy += _track_height;
+               y -= h;
        }
+
+       return sy;
 }
 
+void
+EditorSummary::routes_added (list<RouteTimeAxisView*> const & r)
+{
+       for (list<RouteTimeAxisView*>::const_iterator i = r.begin(); i != r.end(); ++i) {
+               /* Connect to the relevant signal for the route so that we know when its colour has changed */
+               (*i)->route()->presentation_info().PropertyChanged.connect (*this, invalidator (*this), boost::bind (&EditorSummary::route_gui_changed, this, _1), gui_context ());
+               boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> ((*i)->route ());
+               if (tr) {
+                       tr->PlaylistChanged.connect (*this, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context ());
+               }
+       }
 
+       set_background_dirty ();
+}
+
+void
+EditorSummary::route_gui_changed (PBD::PropertyChange const& what_changed)
+{
+       if (what_changed.contains (Properties::color)) {
+               set_background_dirty ();
+       }
+}
+
+double
+EditorSummary::playhead_frame_to_position (framepos_t t) const
+{
+       return (t - _start) * _x_scale;
+}
+
+framepos_t
+EditorSummary::position_to_playhead_frame_to_position (double pos) const
+{
+       return _start  + (pos * _x_scale);
+}