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),
49 _overhang_fraction (0.02),
53 _move_dragging (false),
54 _view_rectangle_x (0, 0),
55 _view_rectangle_y (0, 0),
56 _zoom_trim_dragging (false),
57 _old_follow_playhead (false),
59 _background_dirty (true)
61 CairoWidget::use_nsglview ();
62 add_events (Gdk::POINTER_MOTION_MASK|Gdk::KEY_PRESS_MASK|Gdk::KEY_RELEASE_MASK|Gdk::ENTER_NOTIFY_MASK|Gdk::LEAVE_NOTIFY_MASK);
63 set_flags (get_flags() | Gtk::CAN_FOCUS);
65 UIConfiguration::instance().ParameterChanged.connect (sigc::mem_fun (*this, &EditorSummary::parameter_changed));
68 EditorSummary::~EditorSummary ()
70 cairo_surface_destroy (_image);
74 EditorSummary::parameter_changed (string p)
77 if (p == "color-regions-using-track-color") {
78 set_background_dirty ();
82 /** Handle a size allocation.
83 * @param alloc GTK allocation.
86 EditorSummary::on_size_allocate (Gtk::Allocation& alloc)
88 CairoWidget::on_size_allocate (alloc);
89 set_background_dirty ();
93 /** Connect to a session.
97 EditorSummary::set_session (Session* s)
99 SessionHandlePtr::set_session (s);
103 /* Note: the EditorSummary already finds out about new regions from Editor::region_view_added
104 * (which attaches to StreamView::RegionViewAdded), and cut regions by the RegionPropertyChanged
105 * emitted when a cut region is added to the `cutlist' playlist.
109 Region::RegionPropertyChanged.connect (region_property_connection, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
110 PresentationInfo::Change.connect (route_ctrl_id_connection, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
111 _editor->playhead_cursor->PositionChanged.connect (position_connection, invalidator (*this), boost::bind (&EditorSummary::playhead_position_changed, this, _1), gui_context());
112 _session->StartTimeChanged.connect (_session_connections, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
113 _session->EndTimeChanged.connect (_session_connections, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
114 _editor->selection->RegionsChanged.connect (sigc::mem_fun(*this, &EditorSummary::set_background_dirty));
116 _leftmost = _session->current_start_frame();
117 _rightmost = min (_session->nominal_frame_rate()*60*2, _session->current_end_frame() ); //always show at least 2 minutes
122 EditorSummary::render_background_image ()
124 cairo_surface_destroy (_image); // passing NULL is safe
125 _image = cairo_image_surface_create (CAIRO_FORMAT_RGB24, get_width (), get_height ());
127 cairo_t* cr = cairo_create (_image);
129 /* background (really just the dividing lines between tracks */
131 cairo_set_source_rgb (cr, 0, 0, 0);
132 cairo_rectangle (cr, 0, 0, get_width(), get_height());
135 /* compute start and end points for the summary */
137 framecnt_t const session_length = _session->current_end_frame() - _session->current_start_frame ();
138 double theoretical_start = _session->current_start_frame() - session_length * _overhang_fraction;
139 double theoretical_end = _session->current_end_frame();
141 /* the summary should encompass the full extent of everywhere we've visited since the session was opened */
142 if ( _leftmost < theoretical_start)
143 theoretical_start = _leftmost;
144 if ( _rightmost > theoretical_end )
145 theoretical_end = _rightmost;
148 _start = theoretical_start > 0 ? theoretical_start : 0;
149 _end = theoretical_end + session_length * _overhang_fraction;
151 /* calculate x scale */
152 if (_end != _start) {
153 _x_scale = static_cast<double> (get_width()) / (_end - _start);
158 /* compute track height */
160 for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
161 if (!(*i)->hidden()) {
169 _track_height = (double) get_height() / N;
172 /* render tracks and regions */
175 for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
177 if ((*i)->hidden()) {
181 /* paint a non-bg colored strip to represent the track itself */
183 if ( _track_height > 4 ) {
184 cairo_set_source_rgb (cr, 0.2, 0.2, 0.2);
185 cairo_set_line_width (cr, _track_height - 1);
186 cairo_move_to (cr, 0, y + _track_height / 2);
187 cairo_line_to (cr, get_width(), y + _track_height / 2);
191 StreamView* s = (*i)->view ();
194 cairo_set_line_width (cr, _track_height * 0.8);
196 s->foreach_regionview (sigc::bind (
197 sigc::mem_fun (*this, &EditorSummary::render_region),
199 y + _track_height / 2
206 /* start and end markers */
208 cairo_set_line_width (cr, 1);
209 cairo_set_source_rgb (cr, 1, 1, 0);
211 const double p = (_session->current_start_frame() - _start) * _x_scale;
212 cairo_move_to (cr, p, 0);
213 cairo_line_to (cr, p, get_height());
215 double const q = (_session->current_end_frame() - _start) * _x_scale;
216 cairo_move_to (cr, q, 0);
217 cairo_line_to (cr, q, get_height());
223 /** Render the required regions to a cairo context.
227 EditorSummary::render (Cairo::RefPtr<Cairo::Context> const& ctx, cairo_rectangle_t*)
229 cairo_t* cr = ctx->cobj();
235 /* maintain the leftmost and rightmost locations that we've ever reached */
236 framecnt_t const leftmost = _editor->leftmost_sample ();
237 if ( leftmost < _leftmost) {
238 _leftmost = leftmost;
239 _background_dirty = true;
241 framecnt_t const rightmost = leftmost + _editor->current_page_samples();
242 if ( rightmost > _rightmost) {
243 _rightmost = rightmost;
244 _background_dirty = true;
247 //draw the background (regions, markers, etc ) if they've changed
248 if (!_image || _background_dirty) {
249 render_background_image ();
250 _background_dirty = false;
253 cairo_push_group (cr);
255 /* Fill with the background image */
257 cairo_rectangle (cr, 0, 0, get_width(), get_height());
258 cairo_set_source_surface (cr, _image, 0, 0);
261 /* Render the view rectangle. If there is an editor visual pending, don't update
262 * the view rectangle now --- wait until the expose event that we'll get after
263 * the visual change. This prevents a flicker.
266 if (_editor->pending_visual_change.idle_handler_id < 0) {
267 get_editor (&_view_rectangle_x, &_view_rectangle_y);
270 int32_t width = _view_rectangle_x.second - _view_rectangle_x.first;
272 int32_t height = _view_rectangle_y.second - _view_rectangle_y.first;
273 cairo_rectangle (cr, _view_rectangle_x.first, 0, width, get_height ());
274 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
278 cairo_rectangle (cr, _view_rectangle_x.first, 0, width, get_height ());
279 cairo_set_line_width (cr, 1);
280 cairo_set_source_rgba (cr, 1, 1, 1, 0.9);
285 cairo_set_line_width (cr, 1);
286 /* XXX: colour should be set from configuration file */
287 cairo_set_source_rgba (cr, 1, 0, 0, 1);
289 const double ph= playhead_frame_to_position (_editor->playhead_cursor->current_frame());
290 cairo_move_to (cr, ph, 0);
291 cairo_line_to (cr, ph, get_height());
293 cairo_pop_group_to_source (cr);
299 /** Render a region for the summary.
300 * @param r Region view.
301 * @param cr Cairo context.
302 * @param y y coordinate to render at.
305 EditorSummary::render_region (RegionView* r, cairo_t* cr, double y) const
307 uint32_t const c = r->get_fill_color ();
308 cairo_set_source_rgb (cr, UINT_RGBA_R (c) / 255.0, UINT_RGBA_G (c) / 255.0, UINT_RGBA_B (c) / 255.0);
310 if (r->region()->position() > _start) {
311 cairo_move_to (cr, (r->region()->position() - _start) * _x_scale, y);
313 cairo_move_to (cr, 0, y);
316 if ((r->region()->position() + r->region()->length()) > _start) {
317 cairo_line_to (cr, ((r->region()->position() - _start + r->region()->length())) * _x_scale, y);
319 cairo_line_to (cr, 0, y);
326 EditorSummary::set_background_dirty ()
328 if (!_background_dirty) {
329 _background_dirty = true;
334 /** Set the summary so that just the overlays (viewbox, playhead etc.) will be re-rendered */
336 EditorSummary::set_overlays_dirty ()
338 ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty);
342 /** Set the summary so that just the overlays (viewbox, playhead etc.) in a given area will be re-rendered */
344 EditorSummary::set_overlays_dirty (int x, int y, int w, int h)
346 ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty);
347 queue_draw_area (x, y, w, h);
351 /** Handle a size request.
352 * @param req GTK requisition
355 EditorSummary::on_size_request (Gtk::Requisition *req)
357 /* The left/right buttons will determine our height */
364 EditorSummary::centre_on_click (GdkEventButton* ev)
366 pair<double, double> xr;
369 double const w = xr.second - xr.first;
370 double ex = ev->x - w / 2;
373 } else if ((ex + w) > get_width()) {
374 ex = get_width() - w;
381 EditorSummary::on_enter_notify_event (GdkEventCrossing*)
384 Keyboard::magic_widget_grab_focus ();
389 EditorSummary::on_leave_notify_event (GdkEventCrossing*)
391 /* there are no inferior/child windows, so any leave event means that
394 Keyboard::magic_widget_drop_focus ();
399 EditorSummary::on_key_press_event (GdkEventKey* key)
402 GtkAccelKey set_playhead_accel;
403 if (gtk_accel_map_lookup_entry ("<Actions>/Editor/set-playhead", &set_playhead_accel)) {
404 if (key->keyval == set_playhead_accel.accel_key && (int) key->state == set_playhead_accel.accel_mods) {
407 _session->request_locate (_start + (framepos_t) x / _x_scale, _session->transport_rolling());
417 EditorSummary::on_key_release_event (GdkEventKey* key)
420 GtkAccelKey set_playhead_accel;
421 if (gtk_accel_map_lookup_entry ("<Actions>/Editor/set-playhead", &set_playhead_accel)) {
422 if (key->keyval == set_playhead_accel.accel_key && (int) key->state == set_playhead_accel.accel_mods) {
429 /** Handle a button press.
430 * @param ev GTK event.
433 EditorSummary::on_button_press_event (GdkEventButton* ev)
435 _old_follow_playhead = _editor->follow_playhead ();
437 if (ev->button != 1) {
441 pair<double, double> xr;
444 _start_editor_x = xr;
445 _start_mouse_x = ev->x;
446 _start_mouse_y = ev->y;
447 _start_position = get_position (ev->x, ev->y);
449 if (_start_position != INSIDE && _start_position != TO_LEFT_OR_RIGHT) {
451 /* start a zoom_trim drag */
453 _zoom_trim_position = get_position (ev->x, ev->y);
454 _zoom_trim_dragging = true;
455 _editor->_dragging_playhead = true;
456 _editor->set_follow_playhead (false);
458 if (suspending_editor_updates ()) {
459 get_editor (&_pending_editor_x, &_pending_editor_y);
460 _pending_editor_changed = false;
463 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
465 /* secondary-modifier-click: locate playhead */
467 _session->request_locate (ev->x / _x_scale + _start);
470 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
472 centre_on_click (ev);
476 /* start a move+zoom drag */
477 get_editor (&_pending_editor_x, &_pending_editor_y);
478 _pending_editor_changed = false;
479 _editor->_dragging_playhead = true;
480 _editor->set_follow_playhead (false);
482 _move_dragging = true;
490 get_window()->set_cursor (*_editor->_cursors->expand_left_right);
497 /** @return true if we are currently suspending updates to the editor's viewport,
498 * which we do if configured to do so, and if in a drag of some kind.
501 EditorSummary::suspending_editor_updates () const
503 return (!UIConfiguration::instance().get_update_editor_during_summary_drag () && (_zoom_trim_dragging || _move_dragging));
506 /** Fill in x and y with the editor's current viewable area in summary coordinates */
508 EditorSummary::get_editor (pair<double, double>* x, pair<double, double>* y) const
511 if (suspending_editor_updates ()) {
513 /* We are dragging, and configured not to update the editor window during drags,
514 * so just return where the editor will be when the drag finishes.
517 *x = _pending_editor_x;
519 *y = _pending_editor_y;
524 /* Otherwise query the editor for its actual position */
526 x->first = (_editor->leftmost_sample () - _start) * _x_scale;
527 x->second = x->first + _editor->current_page_samples() * _x_scale;
530 y->first = editor_y_to_summary (_editor->vertical_adjustment.get_value ());
531 y->second = editor_y_to_summary (_editor->vertical_adjustment.get_value () + _editor->visible_canvas_height() - _editor->get_trackview_group()->canvas_origin().y);
535 /** Get an expression of the position of a point with respect to the view rectangle */
536 EditorSummary::Position
537 EditorSummary::get_position (double x, double y) const
539 /* how close the mouse has to be to the edge of the view rectangle to be considered `on it',
542 int x_edge_size = (_view_rectangle_x.second - _view_rectangle_x.first) / 4;
543 x_edge_size = min (x_edge_size, 8);
544 x_edge_size = max (x_edge_size, 1);
546 bool const near_left = (std::abs (x - _view_rectangle_x.first) < x_edge_size);
547 bool const near_right = (std::abs (x - _view_rectangle_x.second) < x_edge_size);
548 bool const within_x = _view_rectangle_x.first < x && x < _view_rectangle_x.second;
552 } else if (near_right) {
554 } else if (within_x) {
557 return TO_LEFT_OR_RIGHT;
562 EditorSummary::set_cursor (Position p)
566 get_window()->set_cursor (*_editor->_cursors->resize_left);
569 get_window()->set_cursor (*_editor->_cursors->resize_right);
572 get_window()->set_cursor (*_editor->_cursors->move);
574 case TO_LEFT_OR_RIGHT:
575 get_window()->set_cursor (*_editor->_cursors->move);
579 get_window()->set_cursor ();
585 EditorSummary::summary_zoom_step ( int steps /* positive steps to zoom "out" , negative steps to zoom "in" */ )
587 pair<double, double> xn;
594 //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 )
596 if ( (xn.second-xn.first) < 2)
600 set_overlays_dirty ();
606 EditorSummary::on_motion_notify_event (GdkEventMotion* ev)
608 if (_move_dragging) {
610 //To avoid accidental zooming, the mouse must move exactly vertical, not diagonal, to trigger a zoom step
611 //we use screen coordinates for this, not canvas-based grab_x
613 double dx = mx - _last_mx;
615 double dy = my - _last_my;
617 //do zooming in windowed "steps" so it feels more reversible ?
618 const int stepsize = 2;
619 int y_delta = _start_mouse_y - my;
620 y_delta = y_delta / stepsize;
623 const float zscale = 3;
624 if ( (dx==0) && (_last_dx ==0) && (y_delta != _last_y_delta) ) {
626 summary_zoom_step( dy * zscale );
628 //after the zoom we must re-calculate x-pos grabs
629 pair<double, double> xr;
631 _start_editor_x = xr;
632 _start_mouse_x = ev->x;
634 _last_y_delta = y_delta;
637 //always track horizontal movement, if any
640 double x = _start_editor_x.first;
641 x += ev->x - _start_mouse_x;
653 } else if (_zoom_trim_dragging) {
655 pair<double, double> xr = _start_editor_x;
657 double const dx = ev->x - _start_mouse_x;
659 if (_zoom_trim_position == LEFT) {
661 } else if (_zoom_trim_position == RIGHT) {
665 xr.first = -1; /* do not change */
668 set_overlays_dirty ();
669 set_cursor (_zoom_trim_position);
673 set_cursor ( get_position(ev->x, ev->y) );
680 EditorSummary::on_button_release_event (GdkEventButton*)
682 bool const was_suspended = suspending_editor_updates ();
684 _move_dragging = false;
685 _zoom_trim_dragging = false;
686 _editor->_dragging_playhead = false;
687 _editor->set_follow_playhead (_old_follow_playhead, false);
689 if (was_suspended && _pending_editor_changed) {
690 set_editor (_pending_editor_x);
697 EditorSummary::on_scroll_event (GdkEventScroll* ev)
700 pair<double, double> xr;
704 switch (ev->direction) {
705 case GDK_SCROLL_UP: {
707 summary_zoom_step( -4 );
712 case GDK_SCROLL_DOWN: {
714 summary_zoom_step( 4 );
719 case GDK_SCROLL_LEFT:
720 if (Keyboard::modifier_state_equals (ev->state, Keyboard::ScrollZoomHorizontalModifier)) {
721 _editor->temporal_zoom_step (false);
722 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
724 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
727 _editor->scroll_left_half_page ();
731 case GDK_SCROLL_RIGHT:
732 if (Keyboard::modifier_state_equals (ev->state, Keyboard::ScrollZoomHorizontalModifier)) {
733 _editor->temporal_zoom_step (true);
734 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
736 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
739 _editor->scroll_right_half_page ();
751 /** Set the editor to display a x range with the left at a given position
752 * and a y range with the top at a given position.
753 * x and y parameters are specified in summary coordinates.
754 * Zoom is not changed in either direction.
757 EditorSummary::set_editor (double const x)
759 if (_editor->pending_visual_change.idle_handler_id >= 0 && _editor->pending_visual_change.being_handled == true) {
761 /* As a side-effect, the Editor's visual change idle handler processes
762 pending GTK events. Hence this motion notify handler can be called
763 in the middle of a visual change idle handler, and if this happens,
764 the queue_visual_change calls below modify the variables that the
765 idle handler is working with. This causes problems. Hence this
766 check. It ensures that we won't modify the pending visual change
767 while a visual change idle handler is in progress. It's not perfect,
768 as it also means that we won't change these variables if an idle handler
769 is merely pending but not executing. But c'est la vie.
778 /** Set the editor to display a given x range and a y range with the top at a given position.
779 * The editor's x zoom is adjusted if necessary, but the y zoom is not changed.
780 * x and y parameters are specified in summary coordinates.
783 EditorSummary::set_editor (pair<double,double> const x)
785 if (_editor->pending_visual_change.idle_handler_id >= 0) {
786 /* see comment in other set_editor () */
795 /** Set the left of the x range visible in the editor.
796 * Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
797 * @param x new x left position in summary coordinates.
800 EditorSummary::set_editor_x (double x)
806 if (suspending_editor_updates ()) {
807 double const w = _pending_editor_x.second - _pending_editor_x.first;
808 _pending_editor_x.first = x;
809 _pending_editor_x.second = x + w;
810 _pending_editor_changed = true;
813 _editor->reset_x_origin (x / _x_scale + _start);
817 /** Set the x range visible in the editor.
818 * Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
819 * @param x new x range in summary coordinates.
822 EditorSummary::set_editor_x (pair<double, double> x)
829 x.second = x.first + 1;
832 if (suspending_editor_updates ()) {
833 _pending_editor_x = x;
834 _pending_editor_changed = true;
837 _editor->reset_x_origin (x.first / _x_scale + _start);
840 ((x.second - x.first) / _x_scale) /
841 _editor->sample_to_pixel (_editor->current_page_samples())
844 if (nx != _editor->get_current_zoom ()) {
845 _editor->reset_zoom (nx);
851 EditorSummary::playhead_position_changed (framepos_t p)
853 int const o = int (_last_playhead);
854 int const n = int (playhead_frame_to_position (p));
855 if (_session && o != n) {
856 int a = max(2, min (o, n));
858 set_overlays_dirty (a - 2, 0, b + 2, get_height ());
863 EditorSummary::editor_y_to_summary (double y) const
866 for (TrackViewList::const_iterator i = _editor->track_views.begin (); i != _editor->track_views.end(); ++i) {
868 if ((*i)->hidden()) {
872 double const h = (*i)->effective_height ();
875 return sy + y * _track_height / h;
886 EditorSummary::routes_added (list<RouteTimeAxisView*> const & r)
888 for (list<RouteTimeAxisView*>::const_iterator i = r.begin(); i != r.end(); ++i) {
889 /* Connect to the relevant signal for the route so that we know when its colour has changed */
890 (*i)->route()->presentation_info().PropertyChanged.connect (*this, invalidator (*this), boost::bind (&EditorSummary::route_gui_changed, this, _1), gui_context ());
891 boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> ((*i)->route ());
893 tr->PlaylistChanged.connect (*this, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context ());
897 set_background_dirty ();
901 EditorSummary::route_gui_changed (PBD::PropertyChange const& what_changed)
903 if (what_changed.contains (Properties::color)) {
904 set_background_dirty ();
909 EditorSummary::playhead_frame_to_position (framepos_t t) const
911 return (t - _start) * _x_scale;
915 EditorSummary::position_to_playhead_frame_to_position (double pos) const
917 return _start + (pos * _x_scale);