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 cairo_rectangle (cr, _view_rectangle_x.first, 0, width, get_height ());
273 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
277 cairo_rectangle (cr, _view_rectangle_x.first, 0, width, get_height ());
278 cairo_set_line_width (cr, 1);
279 cairo_set_source_rgba (cr, 1, 1, 1, 0.9);
284 cairo_set_line_width (cr, 1);
285 /* XXX: colour should be set from configuration file */
286 cairo_set_source_rgba (cr, 1, 0, 0, 1);
288 const double ph= playhead_frame_to_position (_editor->playhead_cursor->current_frame());
289 cairo_move_to (cr, ph, 0);
290 cairo_line_to (cr, ph, get_height());
292 cairo_pop_group_to_source (cr);
298 /** Render a region for the summary.
299 * @param r Region view.
300 * @param cr Cairo context.
301 * @param y y coordinate to render at.
304 EditorSummary::render_region (RegionView* r, cairo_t* cr, double y) const
306 uint32_t const c = r->get_fill_color ();
307 cairo_set_source_rgb (cr, UINT_RGBA_R (c) / 255.0, UINT_RGBA_G (c) / 255.0, UINT_RGBA_B (c) / 255.0);
309 if (r->region()->position() > _start) {
310 cairo_move_to (cr, (r->region()->position() - _start) * _x_scale, y);
312 cairo_move_to (cr, 0, y);
315 if ((r->region()->position() + r->region()->length()) > _start) {
316 cairo_line_to (cr, ((r->region()->position() - _start + r->region()->length())) * _x_scale, y);
318 cairo_line_to (cr, 0, y);
325 EditorSummary::set_background_dirty ()
327 if (!_background_dirty) {
328 _background_dirty = true;
333 /** Set the summary so that just the overlays (viewbox, playhead etc.) will be re-rendered */
335 EditorSummary::set_overlays_dirty ()
337 ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty);
341 /** Set the summary so that just the overlays (viewbox, playhead etc.) in a given area will be re-rendered */
343 EditorSummary::set_overlays_dirty (int x, int y, int w, int h)
345 ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty);
346 queue_draw_area (x, y, w, h);
350 /** Handle a size request.
351 * @param req GTK requisition
354 EditorSummary::on_size_request (Gtk::Requisition *req)
356 /* The left/right buttons will determine our height */
363 EditorSummary::centre_on_click (GdkEventButton* ev)
365 pair<double, double> xr;
368 double const w = xr.second - xr.first;
369 double ex = ev->x - w / 2;
372 } else if ((ex + w) > get_width()) {
373 ex = get_width() - w;
380 EditorSummary::on_enter_notify_event (GdkEventCrossing*)
383 Keyboard::magic_widget_grab_focus ();
388 EditorSummary::on_leave_notify_event (GdkEventCrossing*)
390 /* there are no inferior/child windows, so any leave event means that
393 Keyboard::magic_widget_drop_focus ();
398 EditorSummary::on_key_press_event (GdkEventKey* key)
401 GtkAccelKey set_playhead_accel;
402 if (gtk_accel_map_lookup_entry ("<Actions>/Editor/set-playhead", &set_playhead_accel)) {
403 if (key->keyval == set_playhead_accel.accel_key && (int) key->state == set_playhead_accel.accel_mods) {
406 _session->request_locate (_start + (framepos_t) x / _x_scale, _session->transport_rolling());
416 EditorSummary::on_key_release_event (GdkEventKey* key)
419 GtkAccelKey set_playhead_accel;
420 if (gtk_accel_map_lookup_entry ("<Actions>/Editor/set-playhead", &set_playhead_accel)) {
421 if (key->keyval == set_playhead_accel.accel_key && (int) key->state == set_playhead_accel.accel_mods) {
428 /** Handle a button press.
429 * @param ev GTK event.
432 EditorSummary::on_button_press_event (GdkEventButton* ev)
434 _old_follow_playhead = _editor->follow_playhead ();
436 if (ev->button != 1) {
440 pair<double, double> xr;
443 _start_editor_x = xr;
444 _start_mouse_x = ev->x;
445 _start_mouse_y = ev->y;
446 _start_position = get_position (ev->x, ev->y);
448 if (_start_position != INSIDE && _start_position != TO_LEFT_OR_RIGHT) {
450 /* start a zoom_trim drag */
452 _zoom_trim_position = get_position (ev->x, ev->y);
453 _zoom_trim_dragging = true;
454 _editor->_dragging_playhead = true;
455 _editor->set_follow_playhead (false);
457 if (suspending_editor_updates ()) {
458 get_editor (&_pending_editor_x, &_pending_editor_y);
459 _pending_editor_changed = false;
462 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
464 /* secondary-modifier-click: locate playhead */
466 _session->request_locate (ev->x / _x_scale + _start);
469 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
471 centre_on_click (ev);
475 /* start a move+zoom drag */
476 get_editor (&_pending_editor_x, &_pending_editor_y);
477 _pending_editor_changed = false;
478 _editor->_dragging_playhead = true;
479 _editor->set_follow_playhead (false);
481 _move_dragging = true;
489 get_window()->set_cursor (*_editor->_cursors->expand_left_right);
496 /** @return true if we are currently suspending updates to the editor's viewport,
497 * which we do if configured to do so, and if in a drag of some kind.
500 EditorSummary::suspending_editor_updates () const
502 return (!UIConfiguration::instance().get_update_editor_during_summary_drag () && (_zoom_trim_dragging || _move_dragging));
505 /** Fill in x and y with the editor's current viewable area in summary coordinates */
507 EditorSummary::get_editor (pair<double, double>* x, pair<double, double>* y) const
510 if (suspending_editor_updates ()) {
512 /* We are dragging, and configured not to update the editor window during drags,
513 * so just return where the editor will be when the drag finishes.
516 *x = _pending_editor_x;
518 *y = _pending_editor_y;
523 /* Otherwise query the editor for its actual position */
525 x->first = (_editor->leftmost_sample () - _start) * _x_scale;
526 x->second = x->first + _editor->current_page_samples() * _x_scale;
529 y->first = editor_y_to_summary (_editor->vertical_adjustment.get_value ());
530 y->second = editor_y_to_summary (_editor->vertical_adjustment.get_value () + _editor->visible_canvas_height() - _editor->get_trackview_group()->canvas_origin().y);
534 /** Get an expression of the position of a point with respect to the view rectangle */
535 EditorSummary::Position
536 EditorSummary::get_position (double x, double y) const
538 /* how close the mouse has to be to the edge of the view rectangle to be considered `on it',
541 int x_edge_size = (_view_rectangle_x.second - _view_rectangle_x.first) / 4;
542 x_edge_size = min (x_edge_size, 8);
543 x_edge_size = max (x_edge_size, 1);
545 bool const near_left = (std::abs (x - _view_rectangle_x.first) < x_edge_size);
546 bool const near_right = (std::abs (x - _view_rectangle_x.second) < x_edge_size);
547 bool const within_x = _view_rectangle_x.first < x && x < _view_rectangle_x.second;
551 } else if (near_right) {
553 } else if (within_x) {
556 return TO_LEFT_OR_RIGHT;
561 EditorSummary::set_cursor (Position p)
565 get_window()->set_cursor (*_editor->_cursors->resize_left);
568 get_window()->set_cursor (*_editor->_cursors->resize_right);
571 get_window()->set_cursor (*_editor->_cursors->move);
573 case TO_LEFT_OR_RIGHT:
574 get_window()->set_cursor (*_editor->_cursors->move);
578 get_window()->set_cursor ();
584 EditorSummary::summary_zoom_step ( int steps /* positive steps to zoom "out" , negative steps to zoom "in" */ )
586 pair<double, double> xn;
593 //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 )
595 if ( (xn.second-xn.first) < 2)
599 set_overlays_dirty ();
605 EditorSummary::on_motion_notify_event (GdkEventMotion* ev)
607 if (_move_dragging) {
609 //To avoid accidental zooming, the mouse must move exactly vertical, not diagonal, to trigger a zoom step
610 //we use screen coordinates for this, not canvas-based grab_x
612 double dx = mx - _last_mx;
614 double dy = my - _last_my;
616 //do zooming in windowed "steps" so it feels more reversible ?
617 const int stepsize = 2;
618 int y_delta = _start_mouse_y - my;
619 y_delta = y_delta / stepsize;
622 const float zscale = 3;
623 if ( (dx==0) && (_last_dx ==0) && (y_delta != _last_y_delta) ) {
625 summary_zoom_step( dy * zscale );
627 //after the zoom we must re-calculate x-pos grabs
628 pair<double, double> xr;
630 _start_editor_x = xr;
631 _start_mouse_x = ev->x;
633 _last_y_delta = y_delta;
636 //always track horizontal movement, if any
639 double x = _start_editor_x.first;
640 x += ev->x - _start_mouse_x;
652 } else if (_zoom_trim_dragging) {
654 pair<double, double> xr = _start_editor_x;
656 double const dx = ev->x - _start_mouse_x;
658 if (_zoom_trim_position == LEFT) {
660 } else if (_zoom_trim_position == RIGHT) {
664 xr.first = -1; /* do not change */
667 set_overlays_dirty ();
668 set_cursor (_zoom_trim_position);
672 set_cursor ( get_position(ev->x, ev->y) );
679 EditorSummary::on_button_release_event (GdkEventButton*)
681 bool const was_suspended = suspending_editor_updates ();
683 _move_dragging = false;
684 _zoom_trim_dragging = false;
685 _editor->_dragging_playhead = false;
686 _editor->set_follow_playhead (_old_follow_playhead, false);
688 if (was_suspended && _pending_editor_changed) {
689 set_editor (_pending_editor_x);
696 EditorSummary::on_scroll_event (GdkEventScroll* ev)
699 pair<double, double> xr;
703 switch (ev->direction) {
704 case GDK_SCROLL_UP: {
706 summary_zoom_step( -4 );
711 case GDK_SCROLL_DOWN: {
713 summary_zoom_step( 4 );
718 case GDK_SCROLL_LEFT:
719 if (Keyboard::modifier_state_equals (ev->state, Keyboard::ScrollZoomHorizontalModifier)) {
720 _editor->temporal_zoom_step (false);
721 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
723 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
726 _editor->scroll_left_half_page ();
730 case GDK_SCROLL_RIGHT:
731 if (Keyboard::modifier_state_equals (ev->state, Keyboard::ScrollZoomHorizontalModifier)) {
732 _editor->temporal_zoom_step (true);
733 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
735 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
738 _editor->scroll_right_half_page ();
750 /** Set the editor to display a x range with the left at a given position
751 * and a y range with the top at a given position.
752 * x and y parameters are specified in summary coordinates.
753 * Zoom is not changed in either direction.
756 EditorSummary::set_editor (double const x)
758 if (_editor->pending_visual_change.idle_handler_id >= 0 && _editor->pending_visual_change.being_handled == true) {
760 /* As a side-effect, the Editor's visual change idle handler processes
761 pending GTK events. Hence this motion notify handler can be called
762 in the middle of a visual change idle handler, and if this happens,
763 the queue_visual_change calls below modify the variables that the
764 idle handler is working with. This causes problems. Hence this
765 check. It ensures that we won't modify the pending visual change
766 while a visual change idle handler is in progress. It's not perfect,
767 as it also means that we won't change these variables if an idle handler
768 is merely pending but not executing. But c'est la vie.
777 /** Set the editor to display a given x range and a y range with the top at a given position.
778 * The editor's x zoom is adjusted if necessary, but the y zoom is not changed.
779 * x and y parameters are specified in summary coordinates.
782 EditorSummary::set_editor (pair<double,double> const x)
784 if (_editor->pending_visual_change.idle_handler_id >= 0) {
785 /* see comment in other set_editor () */
794 /** Set the left of the x range visible in the editor.
795 * Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
796 * @param x new x left position in summary coordinates.
799 EditorSummary::set_editor_x (double x)
805 if (suspending_editor_updates ()) {
806 double const w = _pending_editor_x.second - _pending_editor_x.first;
807 _pending_editor_x.first = x;
808 _pending_editor_x.second = x + w;
809 _pending_editor_changed = true;
812 _editor->reset_x_origin (x / _x_scale + _start);
816 /** Set the x range visible in the editor.
817 * Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
818 * @param x new x range in summary coordinates.
821 EditorSummary::set_editor_x (pair<double, double> x)
828 x.second = x.first + 1;
831 if (suspending_editor_updates ()) {
832 _pending_editor_x = x;
833 _pending_editor_changed = true;
836 _editor->reset_x_origin (x.first / _x_scale + _start);
839 ((x.second - x.first) / _x_scale) /
840 _editor->sample_to_pixel (_editor->current_page_samples())
843 if (nx != _editor->get_current_zoom ()) {
844 _editor->reset_zoom (nx);
850 EditorSummary::playhead_position_changed (framepos_t p)
852 int const o = int (_last_playhead);
853 int const n = int (playhead_frame_to_position (p));
854 if (_session && o != n) {
855 int a = max(2, min (o, n));
857 set_overlays_dirty (a - 2, 0, b + 2, get_height ());
862 EditorSummary::editor_y_to_summary (double y) const
865 for (TrackViewList::const_iterator i = _editor->track_views.begin (); i != _editor->track_views.end(); ++i) {
867 if ((*i)->hidden()) {
871 double const h = (*i)->effective_height ();
874 return sy + y * _track_height / h;
885 EditorSummary::routes_added (list<RouteTimeAxisView*> const & r)
887 for (list<RouteTimeAxisView*>::const_iterator i = r.begin(); i != r.end(); ++i) {
888 /* Connect to the relevant signal for the route so that we know when its colour has changed */
889 (*i)->route()->presentation_info().PropertyChanged.connect (*this, invalidator (*this), boost::bind (&EditorSummary::route_gui_changed, this, _1), gui_context ());
890 boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> ((*i)->route ());
892 tr->PlaylistChanged.connect (*this, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context ());
896 set_background_dirty ();
900 EditorSummary::route_gui_changed (PBD::PropertyChange const& what_changed)
902 if (what_changed.contains (Properties::color)) {
903 set_background_dirty ();
908 EditorSummary::playhead_frame_to_position (framepos_t t) const
910 return (t - _start) * _x_scale;
914 EditorSummary::position_to_playhead_frame_to_position (double pos) const
916 return _start + (pos * _x_scale);