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 _begin_dragging (false),
54 _move_dragging (false),
56 _view_rectangle_x (0, 0),
57 _view_rectangle_y (0, 0),
58 _zoom_trim_dragging (false),
59 _zoom_dragging (false),
60 _old_follow_playhead (false),
62 _background_dirty (true)
64 CairoWidget::use_nsglview ();
65 add_events (Gdk::POINTER_MOTION_MASK|Gdk::KEY_PRESS_MASK|Gdk::KEY_RELEASE_MASK|Gdk::ENTER_NOTIFY_MASK|Gdk::LEAVE_NOTIFY_MASK);
66 set_flags (get_flags() | Gtk::CAN_FOCUS);
68 UIConfiguration::instance().ParameterChanged.connect (sigc::mem_fun (*this, &EditorSummary::parameter_changed));
71 EditorSummary::~EditorSummary ()
73 cairo_surface_destroy (_image);
77 EditorSummary::parameter_changed (string p)
80 if (p == "color-regions-using-track-color") {
81 set_background_dirty ();
85 /** Handle a size allocation.
86 * @param alloc GTK allocation.
89 EditorSummary::on_size_allocate (Gtk::Allocation& alloc)
91 CairoWidget::on_size_allocate (alloc);
92 set_background_dirty ();
96 /** Connect to a session.
100 EditorSummary::set_session (Session* s)
102 SessionHandlePtr::set_session (s);
106 /* Note: the EditorSummary already finds out about new regions from Editor::region_view_added
107 * (which attaches to StreamView::RegionViewAdded), and cut regions by the RegionPropertyChanged
108 * emitted when a cut region is added to the `cutlist' playlist.
112 Region::RegionPropertyChanged.connect (region_property_connection, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
113 PresentationInfo::Change.connect (route_ctrl_id_connection, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
114 _editor->playhead_cursor->PositionChanged.connect (position_connection, invalidator (*this), boost::bind (&EditorSummary::playhead_position_changed, this, _1), gui_context());
115 _session->StartTimeChanged.connect (_session_connections, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
116 _session->EndTimeChanged.connect (_session_connections, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
117 _editor->selection->RegionsChanged.connect (sigc::mem_fun(*this, &EditorSummary::set_background_dirty));
119 _leftmost = _session->current_start_frame();
120 _rightmost = min (_session->nominal_frame_rate()*60*2, _session->current_end_frame() ); //always show at least 2 minutes
125 EditorSummary::render_background_image ()
127 cairo_surface_destroy (_image); // passing NULL is safe
128 _image = cairo_image_surface_create (CAIRO_FORMAT_RGB24, get_width (), get_height ());
130 cairo_t* cr = cairo_create (_image);
132 /* background (really just the dividing lines between tracks */
134 cairo_set_source_rgb (cr, 0, 0, 0);
135 cairo_rectangle (cr, 0, 0, get_width(), get_height());
138 /* compute start and end points for the summary */
140 framecnt_t const session_length = _session->current_end_frame() - _session->current_start_frame ();
141 double theoretical_start = _session->current_start_frame() - session_length * _overhang_fraction;
142 double theoretical_end = _session->current_end_frame();
144 /* the summary should encompass the full extent of everywhere we've visited since the session was opened */
145 if ( _leftmost < theoretical_start)
146 theoretical_start = _leftmost;
147 if ( _rightmost > theoretical_end )
148 theoretical_end = _rightmost;
151 _start = theoretical_start > 0 ? theoretical_start : 0;
152 _end = theoretical_end + session_length * _overhang_fraction;
154 /* calculate x scale */
155 if (_end != _start) {
156 _x_scale = static_cast<double> (get_width()) / (_end - _start);
161 /* compute track height */
163 for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
164 if (!(*i)->hidden()) {
172 _track_height = (double) get_height() / N;
175 /* render tracks and regions */
178 for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
180 if ((*i)->hidden()) {
184 /* paint a non-bg colored strip to represent the track itself */
186 if ( _track_height > 4 ) {
187 cairo_set_source_rgb (cr, 0.2, 0.2, 0.2);
188 cairo_set_line_width (cr, _track_height - 1);
189 cairo_move_to (cr, 0, y + _track_height / 2);
190 cairo_line_to (cr, get_width(), y + _track_height / 2);
194 StreamView* s = (*i)->view ();
197 cairo_set_line_width (cr, _track_height * 0.8);
199 s->foreach_regionview (sigc::bind (
200 sigc::mem_fun (*this, &EditorSummary::render_region),
202 y + _track_height / 2
209 /* start and end markers */
211 cairo_set_line_width (cr, 1);
212 cairo_set_source_rgb (cr, 1, 1, 0);
214 const double p = (_session->current_start_frame() - _start) * _x_scale;
215 cairo_move_to (cr, p, 0);
216 cairo_line_to (cr, p, get_height());
218 double const q = (_session->current_end_frame() - _start) * _x_scale;
219 cairo_move_to (cr, q, 0);
220 cairo_line_to (cr, q, get_height());
226 /** Render the required regions to a cairo context.
230 EditorSummary::render (Cairo::RefPtr<Cairo::Context> const& ctx, cairo_rectangle_t*)
232 cairo_t* cr = ctx->cobj();
238 /* maintain the leftmost and rightmost locations that we've ever reached */
239 framecnt_t const leftmost = _editor->leftmost_sample ();
240 if ( leftmost < _leftmost) {
241 _leftmost = leftmost;
242 _background_dirty = true;
244 framecnt_t const rightmost = leftmost + _editor->current_page_samples();
245 if ( rightmost > _rightmost) {
246 _rightmost = rightmost;
247 _background_dirty = true;
250 //draw the background (regions, markers, etc ) if they've changed
251 if (!_image || _background_dirty) {
252 render_background_image ();
253 _background_dirty = false;
256 cairo_push_group (cr);
258 /* Fill with the background image */
260 cairo_rectangle (cr, 0, 0, get_width(), get_height());
261 cairo_set_source_surface (cr, _image, 0, 0);
264 /* Render the view rectangle. If there is an editor visual pending, don't update
265 * the view rectangle now --- wait until the expose event that we'll get after
266 * the visual change. This prevents a flicker.
269 if (_editor->pending_visual_change.idle_handler_id < 0) {
270 get_editor (&_view_rectangle_x, &_view_rectangle_y);
273 int32_t width = _view_rectangle_x.second - _view_rectangle_x.first;
275 int32_t height = _view_rectangle_y.second - _view_rectangle_y.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_frame_to_position (_editor->playhead_cursor->current_frame());
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 (int x, int y, int w, int h)
349 ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty);
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 + (framepos_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 /** Handle a button press.
433 * @param ev GTK event.
436 EditorSummary::on_button_press_event (GdkEventButton* ev)
438 _old_follow_playhead = _editor->follow_playhead ();
440 if (ev->button != 1) {
444 pair<double, double> xr;
447 _start_editor_x = xr;
448 _start_mouse_x = ev->x;
449 _start_mouse_y = ev->y;
450 _start_position = get_position (ev->x, ev->y);
452 if (_start_position != INSIDE && _start_position != TO_LEFT_OR_RIGHT) {
454 /* start a zoom_trim drag */
456 _zoom_trim_position = get_position (ev->x, ev->y);
457 _zoom_trim_dragging = true;
458 _editor->_dragging_playhead = true;
459 _editor->set_follow_playhead (false);
461 if (suspending_editor_updates ()) {
462 get_editor (&_pending_editor_x, &_pending_editor_y);
463 _pending_editor_changed = false;
466 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
468 /* secondary-modifier-click: locate playhead */
470 _session->request_locate (ev->x / _x_scale + _start);
473 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
475 centre_on_click (ev);
479 /* start a move or zoom drag */
480 /* won't know which one until the mouse moves */
481 _begin_dragging = true;
487 /** @return true if we are currently suspending updates to the editor's viewport,
488 * which we do if configured to do so, and if in a drag of some kind.
491 EditorSummary::suspending_editor_updates () const
493 return (!UIConfiguration::instance().get_update_editor_during_summary_drag () && (_zoom_dragging || _zoom_trim_dragging || _move_dragging));
496 /** Fill in x and y with the editor's current viewable area in summary coordinates */
498 EditorSummary::get_editor (pair<double, double>* x, pair<double, double>* y) const
501 if (suspending_editor_updates ()) {
503 /* We are dragging, and configured not to update the editor window during drags,
504 * so just return where the editor will be when the drag finishes.
507 *x = _pending_editor_x;
509 *y = _pending_editor_y;
514 /* Otherwise query the editor for its actual position */
516 x->first = (_editor->leftmost_sample () - _start) * _x_scale;
517 x->second = x->first + _editor->current_page_samples() * _x_scale;
520 y->first = editor_y_to_summary (_editor->vertical_adjustment.get_value ());
521 y->second = editor_y_to_summary (_editor->vertical_adjustment.get_value () + _editor->visible_canvas_height() - _editor->get_trackview_group()->canvas_origin().y);
525 /** Get an expression of the position of a point with respect to the view rectangle */
526 EditorSummary::Position
527 EditorSummary::get_position (double x, double y) const
529 /* how close the mouse has to be to the edge of the view rectangle to be considered `on it',
532 int x_edge_size = (_view_rectangle_x.second - _view_rectangle_x.first) / 4;
533 x_edge_size = min (x_edge_size, 8);
534 x_edge_size = max (x_edge_size, 1);
536 bool const near_left = (std::abs (x - _view_rectangle_x.first) < x_edge_size);
537 bool const near_right = (std::abs (x - _view_rectangle_x.second) < x_edge_size);
538 bool const within_x = _view_rectangle_x.first < x && x < _view_rectangle_x.second;
542 } else if (near_right) {
544 } else if (within_x) {
547 return TO_LEFT_OR_RIGHT;
552 EditorSummary::set_cursor (Position p)
556 get_window()->set_cursor (*_editor->_cursors->resize_left);
559 get_window()->set_cursor (*_editor->_cursors->resize_right);
562 get_window()->set_cursor (*_editor->_cursors->move);
564 case TO_LEFT_OR_RIGHT:
565 get_window()->set_cursor (*_editor->_cursors->move);
569 get_window()->set_cursor ();
575 EditorSummary::summary_zoom_step ( int steps /* positive steps to zoom "out" , negative steps to zoom "in" */ )
577 pair<double, double> xn;
584 //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 )
586 if ( (xn.second-xn.first) < 2)
590 set_overlays_dirty ();
596 EditorSummary::on_motion_notify_event (GdkEventMotion* ev)
598 pair<double, double> xr = _start_editor_x;
599 double x = _start_editor_x.first;
601 if (_move_dragging) {
605 assert (_start_position == INSIDE || _start_position == TO_LEFT_OR_RIGHT);
606 x += ev->x - _start_mouse_x;
614 } else if (_zoom_dragging) {
616 //ToDo: refactor into summary_zoom_in/out(
617 //ToDo: protect the case where the editor position is small, and results in offsetting the position
619 double const dy = ev->y - _zoom_last_y;
621 summary_zoom_step( dy );
623 _zoom_last_y = ev->y;
625 } else if (_zoom_trim_dragging) {
627 double const dx = ev->x - _start_mouse_x;
629 if (_zoom_trim_position == LEFT) {
631 } else if (_zoom_trim_position == RIGHT) {
635 xr.first = -1; /* do not change */
638 set_overlays_dirty ();
639 set_cursor (_zoom_trim_position);
642 } else if (_begin_dragging) {
644 double const dx = ev->x - _start_mouse_x;
645 double const dy = ev->y - _start_mouse_y;
647 if ( fabs(dx) > fabs(dy) ) {
649 /* initiate a move drag */
651 /* get the editor's state in case we are suspending updates */
652 get_editor (&_pending_editor_x, &_pending_editor_y);
653 _pending_editor_changed = false;
655 _move_dragging = true;
657 _editor->_dragging_playhead = true;
658 _editor->set_follow_playhead (false);
660 get_window()->set_cursor (*_editor->_cursors->expand_left_right);
662 _begin_dragging = false;
664 } else if ( fabs(dy) > fabs(dx) ) {
666 /* initiate a zoom drag */
668 /* get the editor's state in case we are suspending updates */
669 get_editor (&_pending_editor_x, &_pending_editor_y);
670 _pending_editor_changed = false;
672 //_zoom_position = get_position (ev->x, ev->y);
673 _zoom_dragging = true;
674 _zoom_last_y = ev->y;
675 _editor->_dragging_playhead = true;
676 _editor->set_follow_playhead (false);
678 get_window()->set_cursor (*_editor->_cursors->expand_up_down);
680 _begin_dragging = false;
684 set_cursor ( get_position(ev->x, ev->y) );
691 EditorSummary::on_button_release_event (GdkEventButton*)
693 bool const was_suspended = suspending_editor_updates ();
695 _begin_dragging = false;
696 _move_dragging = false;
697 _zoom_trim_dragging = false;
698 _zoom_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 (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);