2 Copyright (C) 2009 Paul Davis
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.
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.
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.
20 #include "ardour/session.h"
22 #include "canvas/debug.h"
24 #include <gtkmm/menu.h>
25 #include <gtkmm/menuitem.h>
27 #include "time_axis_view.h"
28 #include "streamview.h"
29 #include "editor_summary.h"
30 #include "gui_thread.h"
32 #include "region_view.h"
33 #include "rgb_macros.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"
44 using namespace ARDOUR;
45 using Gtkmm2ext::Keyboard;
47 /** Construct an EditorSummary.
48 * @param e Editor to represent.
50 EditorSummary::EditorSummary (Editor* e)
51 : EditorComponent (e),
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),
63 _background_dirty (true)
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);
69 UIConfiguration::instance().ParameterChanged.connect (sigc::mem_fun (*this, &EditorSummary::parameter_changed));
72 EditorSummary::~EditorSummary ()
74 cairo_surface_destroy (_image);
78 EditorSummary::parameter_changed (string p)
81 if (p == "color-regions-using-track-color") {
82 set_background_dirty ();
86 /** Handle a size allocation.
87 * @param alloc GTK allocation.
90 EditorSummary::on_size_allocate (Gtk::Allocation& alloc)
92 CairoWidget::on_size_allocate (alloc);
93 set_background_dirty ();
97 /** Connect to a session.
101 EditorSummary::set_session (Session* s)
103 SessionHandlePtr::set_session (s);
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.
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));
121 _leftmost = max_samplepos;
126 EditorSummary::render_background_image ()
128 cairo_surface_destroy (_image); // passing NULL is safe
129 _image = cairo_image_surface_create (CAIRO_FORMAT_RGB24, get_width (), get_height ());
131 cairo_t* cr = cairo_create (_image);
133 /* background (really just the dividing lines between tracks */
135 cairo_set_source_rgb (cr, 0, 0, 0);
136 cairo_rectangle (cr, 0, 0, get_width(), get_height());
139 /* compute start and end points for the summary */
141 std::pair<samplepos_t, samplepos_t> ext = _editor->session_gui_extents();
142 double theoretical_start = ext.first;
143 double theoretical_end = ext.second;
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;
152 _start = theoretical_start > 0 ? theoretical_start : 0;
153 _end = theoretical_end < max_samplepos ? theoretical_end : max_samplepos;
155 /* calculate x scale */
156 if (_end != _start) {
157 _x_scale = static_cast<double> (get_width()) / (_end - _start);
162 /* compute track height */
164 for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
165 if (!(*i)->hidden()) {
173 _track_height = (double) get_height() / N;
176 /* render tracks and regions */
179 for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
181 if ((*i)->hidden()) {
185 /* paint a non-bg colored strip to represent the track itself */
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);
195 StreamView* s = (*i)->view ();
198 cairo_set_line_width (cr, _track_height * 0.8);
200 s->foreach_regionview (sigc::bind (
201 sigc::mem_fun (*this, &EditorSummary::render_region),
203 y + _track_height / 2
210 /* start and end markers */
212 cairo_set_line_width (cr, 1);
213 cairo_set_source_rgb (cr, 1, 1, 0);
215 const double p = (_session->current_start_sample() - _start) * _x_scale;
216 cairo_move_to (cr, p, 0);
217 cairo_line_to (cr, p, get_height());
219 double const q = (_session->current_end_sample() - _start) * _x_scale;
220 cairo_move_to (cr, q, 0);
221 cairo_line_to (cr, q, get_height());
227 /** Render the required regions to a cairo context.
231 EditorSummary::render (Cairo::RefPtr<Cairo::Context> const& ctx, cairo_rectangle_t*)
233 cairo_t* cr = ctx->cobj();
239 /* maintain the leftmost and rightmost locations that we've ever reached */
240 samplecnt_t const leftmost = _editor->leftmost_sample ();
241 if ( leftmost < _leftmost) {
242 _leftmost = leftmost;
243 _background_dirty = true;
245 samplecnt_t const rightmost = leftmost + _editor->current_page_samples();
246 if ( rightmost > _rightmost) {
247 _rightmost = rightmost;
248 _background_dirty = true;
251 //draw the background (regions, markers, etc ) if they've changed
252 if (!_image || _background_dirty) {
253 render_background_image ();
254 _background_dirty = false;
257 cairo_push_group (cr);
259 /* Fill with the background image */
261 cairo_rectangle (cr, 0, 0, get_width(), get_height());
262 cairo_set_source_surface (cr, _image, 0, 0);
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.
270 if (_editor->pending_visual_change.idle_handler_id < 0) {
271 get_editor (&_view_rectangle_x, &_view_rectangle_y);
274 int32_t width = _view_rectangle_x.second - _view_rectangle_x.first;
276 cairo_rectangle (cr, _view_rectangle_x.first, 0, width, get_height ());
277 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
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);
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);
292 const double ph= playhead_sample_to_position (_editor->playhead_cursor->current_sample());
293 cairo_move_to (cr, ph, 0);
294 cairo_line_to (cr, ph, get_height());
296 cairo_pop_group_to_source (cr);
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.
308 EditorSummary::render_region (RegionView* r, cairo_t* cr, double y) const
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);
313 if (r->region()->position() > _start) {
314 cairo_move_to (cr, (r->region()->position() - _start) * _x_scale, y);
316 cairo_move_to (cr, 0, y);
319 if ((r->region()->position() + r->region()->length()) > _start) {
320 cairo_line_to (cr, ((r->region()->position() - _start + r->region()->length())) * _x_scale, y);
322 cairo_line_to (cr, 0, y);
329 EditorSummary::set_background_dirty ()
331 if (!_background_dirty) {
332 _background_dirty = true;
337 /** Set the summary so that just the overlays (viewbox, playhead etc.) will be re-rendered */
339 EditorSummary::set_overlays_dirty ()
341 ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty);
345 /** Set the summary so that just the overlays (viewbox, playhead etc.) in a given area will be re-rendered */
347 EditorSummary::set_overlays_dirty_rect (int x, int y, int w, int h)
349 ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty_rect);
350 queue_draw_area (x, y, w, h);
354 /** Handle a size request.
355 * @param req GTK requisition
358 EditorSummary::on_size_request (Gtk::Requisition *req)
360 /* The left/right buttons will determine our height */
367 EditorSummary::centre_on_click (GdkEventButton* ev)
369 pair<double, double> xr;
372 double const w = xr.second - xr.first;
373 double ex = ev->x - w / 2;
376 } else if ((ex + w) > get_width()) {
377 ex = get_width() - w;
384 EditorSummary::on_enter_notify_event (GdkEventCrossing*)
387 Keyboard::magic_widget_grab_focus ();
392 EditorSummary::on_leave_notify_event (GdkEventCrossing*)
394 /* there are no inferior/child windows, so any leave event means that
397 Keyboard::magic_widget_drop_focus ();
402 EditorSummary::on_key_press_event (GdkEventKey* key)
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) {
410 _session->request_locate (_start + (samplepos_t) x / _x_scale, _session->transport_rolling());
420 EditorSummary::on_key_release_event (GdkEventKey* key)
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) {
432 #include "gtkmm2ext/utils.h"
434 /** Handle a button press.
435 * @param ev GTK event.
438 EditorSummary::on_button_press_event (GdkEventButton* ev)
440 _old_follow_playhead = _editor->follow_playhead ();
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);
452 if (ev->button != 1) {
456 pair<double, double> xr;
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);
464 if (_start_position != INSIDE && _start_position != TO_LEFT_OR_RIGHT) {
466 /* start a zoom_trim drag */
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);
473 if (suspending_editor_updates ()) {
474 get_editor (&_pending_editor_x, &_pending_editor_y);
475 _pending_editor_changed = false;
478 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
480 /* secondary-modifier-click: locate playhead */
482 _session->request_locate (ev->x / _x_scale + _start);
485 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
487 centre_on_click (ev);
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);
497 _move_dragging = true;
505 get_window()->set_cursor (*_editor->_cursors->expand_left_right);
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.
516 EditorSummary::suspending_editor_updates () const
518 return (!UIConfiguration::instance().get_update_editor_during_summary_drag () && (_zoom_trim_dragging || _move_dragging));
521 /** Fill in x and y with the editor's current viewable area in summary coordinates */
523 EditorSummary::get_editor (pair<double, double>* x, pair<double, double>* y) const
526 if (suspending_editor_updates ()) {
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.
532 *x = _pending_editor_x;
534 *y = _pending_editor_y;
539 /* Otherwise query the editor for its actual position */
541 x->first = (_editor->leftmost_sample () - _start) * _x_scale;
542 x->second = x->first + _editor->current_page_samples() * _x_scale;
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);
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
554 /* how close the mouse has to be to the edge of the view rectangle to be considered `on it',
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);
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;
567 } else if (near_right) {
569 } else if (within_x) {
572 return TO_LEFT_OR_RIGHT;
577 EditorSummary::reset_to_extents()
579 //reset as if the user never went anywhere outside the extents
580 _leftmost = max_samplepos;
583 _editor->temporal_zoom_extents ();
584 set_background_dirty ();
589 EditorSummary::set_cursor (Position p)
593 get_window()->set_cursor (*_editor->_cursors->resize_left);
596 get_window()->set_cursor (*_editor->_cursors->resize_right);
599 get_window()->set_cursor (*_editor->_cursors->move);
601 case TO_LEFT_OR_RIGHT:
602 get_window()->set_cursor (*_editor->_cursors->move);
606 get_window()->set_cursor ();
612 EditorSummary::summary_zoom_step ( int steps /* positive steps to zoom "out" , negative steps to zoom "in" */ )
614 pair<double, double> xn;
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 )
623 if ( (xn.second-xn.first) < 2)
627 set_overlays_dirty ();
633 EditorSummary::on_motion_notify_event (GdkEventMotion* ev)
635 if (_move_dragging) {
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
640 double dx = mx - _last_mx;
642 double dy = my - _last_my;
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;
650 const float zscale = 3;
651 if ( (dx==0) && (_last_dx ==0) && (y_delta != _last_y_delta) ) {
653 summary_zoom_step( dy * zscale );
655 //after the zoom we must re-calculate x-pos grabs
656 pair<double, double> xr;
658 _start_editor_x = xr;
659 _start_mouse_x = ev->x;
661 _last_y_delta = y_delta;
664 //always track horizontal movement, if any
667 double x = _start_editor_x.first;
668 x += ev->x - _start_mouse_x;
674 //zoom-behavior-tweaks
675 //protect the right edge from expanding beyond the end
676 pair<double, double> xr;
678 double w = xr.second - xr.first;
679 if ( x + w < get_width() ) {
689 } else if (_zoom_trim_dragging) {
691 pair<double, double> xr = _start_editor_x;
693 double const dx = ev->x - _start_mouse_x;
695 if (_zoom_trim_position == LEFT) {
697 } else if (_zoom_trim_position == RIGHT) {
699 //zoom-behavior-tweaks
700 //protect the right edge from expanding beyond the edge
701 if ( (xr.second + dx) < get_width() ) {
707 xr.first = -1; /* do not change */
710 set_overlays_dirty ();
711 set_cursor (_zoom_trim_position);
715 set_cursor ( get_position(ev->x, ev->y) );
722 EditorSummary::on_button_release_event (GdkEventButton*)
724 bool const was_suspended = suspending_editor_updates ();
726 _move_dragging = false;
727 _zoom_trim_dragging = false;
728 _editor->_dragging_playhead = false;
729 _editor->set_follow_playhead (_old_follow_playhead, false);
731 if (was_suspended && _pending_editor_changed) {
732 set_editor (_pending_editor_x);
739 EditorSummary::on_scroll_event (GdkEventScroll* ev)
742 pair<double, double> xr;
746 switch (ev->direction) {
747 case GDK_SCROLL_UP: {
749 summary_zoom_step( -4 );
754 case GDK_SCROLL_DOWN: {
756 summary_zoom_step( 4 );
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)) {
766 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
769 _editor->scroll_left_half_page ();
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)) {
778 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
781 _editor->scroll_right_half_page ();
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.
799 EditorSummary::set_editor (double const x)
801 if (_editor->pending_visual_change.idle_handler_id >= 0 && _editor->pending_visual_change.being_handled == true) {
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.
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.
825 EditorSummary::set_editor (pair<double,double> const x)
827 if (_editor->pending_visual_change.idle_handler_id >= 0) {
828 /* see comment in other set_editor () */
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.
842 EditorSummary::set_editor_x (double x)
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;
855 _editor->reset_x_origin (x / _x_scale + _start);
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.
864 EditorSummary::set_editor_x (pair<double, double> x)
871 x.second = x.first + 1;
874 if (suspending_editor_updates ()) {
875 _pending_editor_x = x;
876 _pending_editor_changed = true;
879 _editor->reset_x_origin (x.first / _x_scale + _start);
882 ((x.second - x.first) / _x_scale) /
883 _editor->sample_to_pixel (_editor->current_page_samples())
886 if (nx != _editor->get_current_zoom ()) {
887 _editor->reset_zoom (nx);
893 EditorSummary::playhead_position_changed (samplepos_t p)
895 int const o = int (_last_playhead);
896 int const n = int (playhead_sample_to_position (p));
897 if (_session && o != n) {
898 int a = max(2, min (o, n));
900 set_overlays_dirty_rect (a - 2, 0, b + 2, get_height ());
905 EditorSummary::editor_y_to_summary (double y) const
908 for (TrackViewList::const_iterator i = _editor->track_views.begin (); i != _editor->track_views.end(); ++i) {
910 if ((*i)->hidden()) {
914 double const h = (*i)->effective_height ();
917 return sy + y * _track_height / h;
928 EditorSummary::routes_added (list<RouteTimeAxisView*> const & r)
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 ());
935 tr->PlaylistChanged.connect (*this, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context ());
939 set_background_dirty ();
943 EditorSummary::route_gui_changed (PBD::PropertyChange const& what_changed)
945 if (what_changed.contains (Properties::color)) {
946 set_background_dirty ();
951 EditorSummary::playhead_sample_to_position (samplepos_t t) const
953 return (t - _start) * _x_scale;
957 EditorSummary::position_to_playhead_sample_to_position (double pos) const
959 return _start + (pos * _x_scale);