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 "time_axis_view.h"
25 #include "streamview.h"
26 #include "editor_summary.h"
27 #include "gui_thread.h"
29 #include "region_view.h"
30 #include "rgb_macros.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"
39 using namespace ARDOUR;
40 using Gtkmm2ext::Keyboard;
42 /** Construct an EditorSummary.
43 * @param e Editor to represent.
45 EditorSummary::EditorSummary (Editor* e)
46 : EditorComponent (e),
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),
58 _background_dirty (true)
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);
64 UIConfiguration::instance().ParameterChanged.connect (sigc::mem_fun (*this, &EditorSummary::parameter_changed));
67 EditorSummary::~EditorSummary ()
69 cairo_surface_destroy (_image);
73 EditorSummary::parameter_changed (string p)
76 if (p == "color-regions-using-track-color") {
77 set_background_dirty ();
81 /** Handle a size allocation.
82 * @param alloc GTK allocation.
85 EditorSummary::on_size_allocate (Gtk::Allocation& alloc)
87 CairoWidget::on_size_allocate (alloc);
88 set_background_dirty ();
92 /** Connect to a session.
96 EditorSummary::set_session (Session* s)
98 SessionHandlePtr::set_session (s);
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.
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));
116 _leftmost = max_framepos;
121 EditorSummary::render_background_image ()
123 cairo_surface_destroy (_image); // passing NULL is safe
124 _image = cairo_image_surface_create (CAIRO_FORMAT_RGB24, get_width (), get_height ());
126 cairo_t* cr = cairo_create (_image);
128 /* background (really just the dividing lines between tracks */
130 cairo_set_source_rgb (cr, 0, 0, 0);
131 cairo_rectangle (cr, 0, 0, get_width(), get_height());
134 /* compute start and end points for the summary */
136 std::pair<framepos_t, framepos_t> ext = _editor->session_gui_extents();
137 double theoretical_start = ext.first;
138 double theoretical_end = ext.second;
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;
147 _start = theoretical_start > 0 ? theoretical_start : 0;
148 _end = theoretical_end < max_framepos ? theoretical_end : max_framepos;
150 /* calculate x scale */
151 if (_end != _start) {
152 _x_scale = static_cast<double> (get_width()) / (_end - _start);
157 /* compute track height */
159 for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
160 if (!(*i)->hidden()) {
168 _track_height = (double) get_height() / N;
171 /* render tracks and regions */
174 for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
176 if ((*i)->hidden()) {
180 /* paint a non-bg colored strip to represent the track itself */
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);
190 StreamView* s = (*i)->view ();
193 cairo_set_line_width (cr, _track_height * 0.8);
195 s->foreach_regionview (sigc::bind (
196 sigc::mem_fun (*this, &EditorSummary::render_region),
198 y + _track_height / 2
205 /* start and end markers */
207 cairo_set_line_width (cr, 1);
208 cairo_set_source_rgb (cr, 1, 1, 0);
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());
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());
222 /** Render the required regions to a cairo context.
226 EditorSummary::render (Cairo::RefPtr<Cairo::Context> const& ctx, cairo_rectangle_t*)
228 cairo_t* cr = ctx->cobj();
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;
240 framecnt_t const rightmost = leftmost + _editor->current_page_samples();
241 if ( rightmost > _rightmost) {
242 _rightmost = rightmost;
243 _background_dirty = true;
246 //draw the background (regions, markers, etc ) if they've changed
247 if (!_image || _background_dirty) {
248 render_background_image ();
249 _background_dirty = false;
252 cairo_push_group (cr);
254 /* Fill with the background image */
256 cairo_rectangle (cr, 0, 0, get_width(), get_height());
257 cairo_set_source_surface (cr, _image, 0, 0);
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.
265 if (_editor->pending_visual_change.idle_handler_id < 0) {
266 get_editor (&_view_rectangle_x, &_view_rectangle_y);
269 int32_t width = _view_rectangle_x.second - _view_rectangle_x.first;
271 cairo_rectangle (cr, _view_rectangle_x.first, 0, width, get_height ());
272 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
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);
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);
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());
291 cairo_pop_group_to_source (cr);
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.
303 EditorSummary::render_region (RegionView* r, cairo_t* cr, double y) const
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);
308 if (r->region()->position() > _start) {
309 cairo_move_to (cr, (r->region()->position() - _start) * _x_scale, y);
311 cairo_move_to (cr, 0, y);
314 if ((r->region()->position() + r->region()->length()) > _start) {
315 cairo_line_to (cr, ((r->region()->position() - _start + r->region()->length())) * _x_scale, y);
317 cairo_line_to (cr, 0, y);
324 EditorSummary::set_background_dirty ()
326 if (!_background_dirty) {
327 _background_dirty = true;
332 /** Set the summary so that just the overlays (viewbox, playhead etc.) will be re-rendered */
334 EditorSummary::set_overlays_dirty ()
336 ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty);
340 /** Set the summary so that just the overlays (viewbox, playhead etc.) in a given area will be re-rendered */
342 EditorSummary::set_overlays_dirty_rect (int x, int y, int w, int h)
344 ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty_rect);
345 queue_draw_area (x, y, w, h);
349 /** Handle a size request.
350 * @param req GTK requisition
353 EditorSummary::on_size_request (Gtk::Requisition *req)
355 /* The left/right buttons will determine our height */
362 EditorSummary::centre_on_click (GdkEventButton* ev)
364 pair<double, double> xr;
367 double const w = xr.second - xr.first;
368 double ex = ev->x - w / 2;
371 } else if ((ex + w) > get_width()) {
372 ex = get_width() - w;
379 EditorSummary::on_enter_notify_event (GdkEventCrossing*)
382 Keyboard::magic_widget_grab_focus ();
387 EditorSummary::on_leave_notify_event (GdkEventCrossing*)
389 /* there are no inferior/child windows, so any leave event means that
392 Keyboard::magic_widget_drop_focus ();
397 EditorSummary::on_key_press_event (GdkEventKey* key)
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) {
405 _session->request_locate (_start + (framepos_t) x / _x_scale, _session->transport_rolling());
415 EditorSummary::on_key_release_event (GdkEventKey* key)
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) {
427 /** Handle a button press.
428 * @param ev GTK event.
431 EditorSummary::on_button_press_event (GdkEventButton* ev)
433 _old_follow_playhead = _editor->follow_playhead ();
435 if (ev->button != 1) {
439 pair<double, double> xr;
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);
447 if (_start_position != INSIDE && _start_position != TO_LEFT_OR_RIGHT) {
449 /* start a zoom_trim drag */
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);
456 if (suspending_editor_updates ()) {
457 get_editor (&_pending_editor_x, &_pending_editor_y);
458 _pending_editor_changed = false;
461 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
463 /* secondary-modifier-click: locate playhead */
465 _session->request_locate (ev->x / _x_scale + _start);
468 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
470 centre_on_click (ev);
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);
480 _move_dragging = true;
488 get_window()->set_cursor (*_editor->_cursors->expand_left_right);
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.
499 EditorSummary::suspending_editor_updates () const
501 return (!UIConfiguration::instance().get_update_editor_during_summary_drag () && (_zoom_trim_dragging || _move_dragging));
504 /** Fill in x and y with the editor's current viewable area in summary coordinates */
506 EditorSummary::get_editor (pair<double, double>* x, pair<double, double>* y) const
509 if (suspending_editor_updates ()) {
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.
515 *x = _pending_editor_x;
517 *y = _pending_editor_y;
522 /* Otherwise query the editor for its actual position */
524 x->first = (_editor->leftmost_sample () - _start) * _x_scale;
525 x->second = x->first + _editor->current_page_samples() * _x_scale;
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);
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
537 /* how close the mouse has to be to the edge of the view rectangle to be considered `on it',
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);
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;
550 } else if (near_right) {
552 } else if (within_x) {
555 return TO_LEFT_OR_RIGHT;
560 EditorSummary::set_cursor (Position p)
564 get_window()->set_cursor (*_editor->_cursors->resize_left);
567 get_window()->set_cursor (*_editor->_cursors->resize_right);
570 get_window()->set_cursor (*_editor->_cursors->move);
572 case TO_LEFT_OR_RIGHT:
573 get_window()->set_cursor (*_editor->_cursors->move);
577 get_window()->set_cursor ();
583 EditorSummary::summary_zoom_step ( int steps /* positive steps to zoom "out" , negative steps to zoom "in" */ )
585 pair<double, double> xn;
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 )
594 if ( (xn.second-xn.first) < 2)
598 set_overlays_dirty ();
604 EditorSummary::on_motion_notify_event (GdkEventMotion* ev)
606 if (_move_dragging) {
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
611 double dx = mx - _last_mx;
613 double dy = my - _last_my;
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;
621 const float zscale = 3;
622 if ( (dx==0) && (_last_dx ==0) && (y_delta != _last_y_delta) ) {
624 summary_zoom_step( dy * zscale );
626 //after the zoom we must re-calculate x-pos grabs
627 pair<double, double> xr;
629 _start_editor_x = xr;
630 _start_mouse_x = ev->x;
632 _last_y_delta = y_delta;
635 //always track horizontal movement, if any
638 double x = _start_editor_x.first;
639 x += ev->x - _start_mouse_x;
645 //zoom-behavior-tweaks
646 //protect the right edge from expanding beyond the end
647 pair<double, double> xr;
649 double w = xr.second - xr.first;
650 if ( x + w < get_width() ) {
660 } else if (_zoom_trim_dragging) {
662 pair<double, double> xr = _start_editor_x;
664 double const dx = ev->x - _start_mouse_x;
666 if (_zoom_trim_position == LEFT) {
668 } else if (_zoom_trim_position == RIGHT) {
670 //zoom-behavior-tweaks
671 //protect the right edge from expanding beyond the edge
672 if ( (xr.second + dx) < get_width() ) {
678 xr.first = -1; /* do not change */
681 set_overlays_dirty ();
682 set_cursor (_zoom_trim_position);
686 set_cursor ( get_position(ev->x, ev->y) );
693 EditorSummary::on_button_release_event (GdkEventButton*)
695 bool const was_suspended = suspending_editor_updates ();
697 _move_dragging = false;
698 _zoom_trim_dragging = false;
699 _editor->_dragging_playhead = false;
700 _editor->set_follow_playhead (_old_follow_playhead, false);
702 if (was_suspended && _pending_editor_changed) {
703 set_editor (_pending_editor_x);
710 EditorSummary::on_scroll_event (GdkEventScroll* ev)
713 pair<double, double> xr;
717 switch (ev->direction) {
718 case GDK_SCROLL_UP: {
720 summary_zoom_step( -4 );
725 case GDK_SCROLL_DOWN: {
727 summary_zoom_step( 4 );
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)) {
737 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
740 _editor->scroll_left_half_page ();
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)) {
749 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
752 _editor->scroll_right_half_page ();
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.
770 EditorSummary::set_editor (double const x)
772 if (_editor->pending_visual_change.idle_handler_id >= 0 && _editor->pending_visual_change.being_handled == true) {
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.
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.
796 EditorSummary::set_editor (pair<double,double> const x)
798 if (_editor->pending_visual_change.idle_handler_id >= 0) {
799 /* see comment in other set_editor () */
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.
813 EditorSummary::set_editor_x (double x)
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;
826 _editor->reset_x_origin (x / _x_scale + _start);
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.
835 EditorSummary::set_editor_x (pair<double, double> x)
842 x.second = x.first + 1;
845 if (suspending_editor_updates ()) {
846 _pending_editor_x = x;
847 _pending_editor_changed = true;
850 _editor->reset_x_origin (x.first / _x_scale + _start);
853 ((x.second - x.first) / _x_scale) /
854 _editor->sample_to_pixel (_editor->current_page_samples())
857 if (nx != _editor->get_current_zoom ()) {
858 _editor->reset_zoom (nx);
864 EditorSummary::playhead_position_changed (framepos_t p)
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));
871 set_overlays_dirty_rect (a - 2, 0, b + 2, get_height ());
876 EditorSummary::editor_y_to_summary (double y) const
879 for (TrackViewList::const_iterator i = _editor->track_views.begin (); i != _editor->track_views.end(); ++i) {
881 if ((*i)->hidden()) {
885 double const h = (*i)->effective_height ();
888 return sy + y * _track_height / h;
899 EditorSummary::routes_added (list<RouteTimeAxisView*> const & r)
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 ());
906 tr->PlaylistChanged.connect (*this, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context ());
910 set_background_dirty ();
914 EditorSummary::route_gui_changed (PBD::PropertyChange const& what_changed)
916 if (what_changed.contains (Properties::color)) {
917 set_background_dirty ();
922 EditorSummary::playhead_frame_to_position (framepos_t t) const
924 return (t - _start) * _x_scale;
928 EditorSummary::position_to_playhead_frame_to_position (double pos) const
930 return _start + (pos * _x_scale);