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
622 * causes the start-offset to 'walk' because of integer limitations.
623 * To fix this, probably need to maintain float throught the get/set_editor() path.)
626 if ((xn.second - xn.first) < 2)
630 set_overlays_dirty ();
636 EditorSummary::on_motion_notify_event (GdkEventMotion* ev)
638 if (_move_dragging) {
640 /* To avoid accidental zooming, the mouse must move exactly vertical, not diagonal, to trigger a zoom step
641 * we use screen coordinates for this, not canvas-based grab_x */
643 double dx = mx - _last_mx;
645 double dy = my - _last_my;
647 /* do zooming in windowed "steps" so it feels more reversible ? */
648 const int stepsize = 2;
649 int y_delta = _start_mouse_y - my;
650 y_delta = y_delta / stepsize;
653 const float zscale = 3;
654 if ((dx == 0) && (_last_dx == 0) && (y_delta != _last_y_delta)) {
656 summary_zoom_step (dy * zscale);
658 /* after the zoom we must re-calculate x-pos grabs */
659 pair<double, double> xr;
661 _start_editor_x = xr;
662 _start_mouse_x = ev->x;
664 _last_y_delta = y_delta;
667 /* always track horizontal movement, if any */
670 double x = _start_editor_x.first;
671 x += ev->x - _start_mouse_x;
677 /* zoom-behavior-tweaks: protect the right edge from expanding beyond the end */
678 pair<double, double> xr;
680 double w = xr.second - xr.first;
681 if (x + w < get_width()) {
691 } else if (_zoom_trim_dragging) {
693 pair<double, double> xr = _start_editor_x;
695 double const dx = ev->x - _start_mouse_x;
697 if (_zoom_trim_position == LEFT) {
699 } else if (_zoom_trim_position == RIGHT) {
701 /* zoom-behavior-tweaks: protect the right edge from expanding beyond the edge */
702 if ((xr.second + dx) < get_width()) {
708 xr.first = -1; /* do not change */
711 set_overlays_dirty ();
712 set_cursor (_zoom_trim_position);
716 set_cursor (get_position (ev->x, ev->y));
723 EditorSummary::on_button_release_event (GdkEventButton*)
725 bool const was_suspended = suspending_editor_updates ();
727 _move_dragging = false;
728 _zoom_trim_dragging = false;
729 _editor->_dragging_playhead = false;
730 _editor->set_follow_playhead (_old_follow_playhead, false);
732 if (was_suspended && _pending_editor_changed) {
733 set_editor (_pending_editor_x);
740 EditorSummary::on_scroll_event (GdkEventScroll* ev)
743 pair<double, double> xr;
747 switch (ev->direction) {
748 case GDK_SCROLL_UP: {
750 summary_zoom_step (-4);
755 case GDK_SCROLL_DOWN: {
757 summary_zoom_step (4);
762 case GDK_SCROLL_LEFT:
763 if (Keyboard::modifier_state_equals (ev->state, Keyboard::ScrollZoomHorizontalModifier)) {
764 _editor->temporal_zoom_step (false);
765 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
767 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
770 _editor->scroll_left_half_page ();
774 case GDK_SCROLL_RIGHT:
775 if (Keyboard::modifier_state_equals (ev->state, Keyboard::ScrollZoomHorizontalModifier)) {
776 _editor->temporal_zoom_step (true);
777 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
779 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
782 _editor->scroll_right_half_page ();
794 /** Set the editor to display a x range with the left at a given position
795 * and a y range with the top at a given position.
796 * x and y parameters are specified in summary coordinates.
797 * Zoom is not changed in either direction.
800 EditorSummary::set_editor (double const x)
802 if (_editor->pending_visual_change.idle_handler_id >= 0 && _editor->pending_visual_change.being_handled == true) {
804 /* As a side-effect, the Editor's visual change idle handler processes
805 pending GTK events. Hence this motion notify handler can be called
806 in the middle of a visual change idle handler, and if this happens,
807 the queue_visual_change calls below modify the variables that the
808 idle handler is working with. This causes problems. Hence this
809 check. It ensures that we won't modify the pending visual change
810 while a visual change idle handler is in progress. It's not perfect,
811 as it also means that we won't change these variables if an idle handler
812 is merely pending but not executing. But c'est la vie.
821 /** Set the editor to display a given x range and a y range with the top at a given position.
822 * The editor's x zoom is adjusted if necessary, but the y zoom is not changed.
823 * x and y parameters are specified in summary coordinates.
826 EditorSummary::set_editor (pair<double,double> const x)
828 if (_editor->pending_visual_change.idle_handler_id >= 0) {
829 /* see comment in other set_editor () */
838 /** Set the left of the x range visible in the editor.
839 * Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
840 * @param x new x left position in summary coordinates.
843 EditorSummary::set_editor_x (double x)
849 if (suspending_editor_updates ()) {
850 double const w = _pending_editor_x.second - _pending_editor_x.first;
851 _pending_editor_x.first = x;
852 _pending_editor_x.second = x + w;
853 _pending_editor_changed = true;
856 _editor->reset_x_origin (x / _x_scale + _start);
860 /** Set the x range visible in the editor.
861 * Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
862 * @param x new x range in summary coordinates.
865 EditorSummary::set_editor_x (pair<double, double> x)
872 x.second = x.first + 1;
875 if (suspending_editor_updates ()) {
876 _pending_editor_x = x;
877 _pending_editor_changed = true;
880 _editor->reset_x_origin (x.first / _x_scale + _start);
883 ((x.second - x.first) / _x_scale) /
884 _editor->sample_to_pixel (_editor->current_page_samples())
887 if (nx != _editor->get_current_zoom ()) {
888 _editor->reset_zoom (nx);
894 EditorSummary::playhead_position_changed (samplepos_t p)
896 int const o = int (_last_playhead);
897 int const n = int (playhead_sample_to_position (p));
898 if (_session && o != n) {
899 int a = max(2, min (o, n));
901 set_overlays_dirty_rect (a - 2, 0, b + 2, get_height ());
906 EditorSummary::editor_y_to_summary (double y) const
909 for (TrackViewList::const_iterator i = _editor->track_views.begin (); i != _editor->track_views.end(); ++i) {
911 if ((*i)->hidden()) {
915 double const h = (*i)->effective_height ();
918 return sy + y * _track_height / h;
929 EditorSummary::routes_added (list<RouteTimeAxisView*> const & r)
931 for (list<RouteTimeAxisView*>::const_iterator i = r.begin(); i != r.end(); ++i) {
932 /* Connect to the relevant signal for the route so that we know when its colour has changed */
933 (*i)->route()->presentation_info().PropertyChanged.connect (*this, invalidator (*this), boost::bind (&EditorSummary::route_gui_changed, this, _1), gui_context ());
934 boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> ((*i)->route ());
936 tr->PlaylistChanged.connect (*this, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context ());
940 set_background_dirty ();
944 EditorSummary::route_gui_changed (PBD::PropertyChange const& what_changed)
946 if (what_changed.contains (Properties::color)) {
947 set_background_dirty ();
952 EditorSummary::playhead_sample_to_position (samplepos_t t) const
954 return (t - _start) * _x_scale;
958 EditorSummary::position_to_playhead_sample_to_position (double pos) const
960 return _start + (pos * _x_scale);