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.1),
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));
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 const theoretical_start = _session->current_start_frame() - session_length * _overhang_fraction;
139 _start = theoretical_start > 0 ? theoretical_start : 0;
140 _end = _session->current_end_frame() + session_length * _overhang_fraction;
142 /* compute track height */
144 for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
145 if (!(*i)->hidden()) {
153 _track_height = (double) get_height() / N;
156 /* calculate x scale */
157 if (_end != _start) {
158 _x_scale = static_cast<double> (get_width()) / (_end - _start);
163 /* render tracks and regions */
166 for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
168 if ((*i)->hidden()) {
172 /* paint a non-bg colored strip to represent the track itself */
174 cairo_set_source_rgb (cr, 0.2, 0.2, 0.2);
175 cairo_set_line_width (cr, _track_height - 1);
176 cairo_move_to (cr, 0, y + _track_height / 2);
177 cairo_line_to (cr, get_width(), y + _track_height / 2);
180 StreamView* s = (*i)->view ();
183 cairo_set_line_width (cr, _track_height * 0.8);
185 s->foreach_regionview (sigc::bind (
186 sigc::mem_fun (*this, &EditorSummary::render_region),
188 y + _track_height / 2
195 /* start and end markers */
197 cairo_set_line_width (cr, 1);
198 cairo_set_source_rgb (cr, 1, 1, 0);
200 const double p = (_session->current_start_frame() - _start) * _x_scale;
201 cairo_move_to (cr, p, 0);
202 cairo_line_to (cr, p, get_height());
204 double const q = (_session->current_end_frame() - _start) * _x_scale;
205 cairo_move_to (cr, q, 0);
206 cairo_line_to (cr, q, get_height());
212 /** Render the required regions to a cairo context.
216 EditorSummary::render (Cairo::RefPtr<Cairo::Context> const& ctx, cairo_rectangle_t*)
218 cairo_t* cr = ctx->cobj();
224 if (!_image || _background_dirty) {
225 render_background_image ();
226 _background_dirty = false;
229 cairo_push_group (cr);
231 /* Fill with the background image */
233 cairo_rectangle (cr, 0, 0, get_width(), get_height());
234 cairo_set_source_surface (cr, _image, 0, 0);
237 /* Render the view rectangle. If there is an editor visual pending, don't update
238 * the view rectangle now --- wait until the expose event that we'll get after
239 * the visual change. This prevents a flicker.
242 if (_editor->pending_visual_change.idle_handler_id < 0) {
243 get_editor (&_view_rectangle_x, &_view_rectangle_y);
246 int32_t width = _view_rectangle_x.second - _view_rectangle_x.first;
248 int32_t height = _view_rectangle_y.second - _view_rectangle_y.first;
249 cairo_rectangle (cr, _view_rectangle_x.first, 0, width, get_height ());
250 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
254 cairo_rectangle (cr, _view_rectangle_x.first, 0, width, get_height ());
255 cairo_set_line_width (cr, 1);
256 cairo_set_source_rgba (cr, 1, 1, 1, 0.9);
261 cairo_set_line_width (cr, 1);
262 /* XXX: colour should be set from configuration file */
263 cairo_set_source_rgba (cr, 1, 0, 0, 1);
265 const double ph= playhead_frame_to_position (_editor->playhead_cursor->current_frame());
266 cairo_move_to (cr, ph, 0);
267 cairo_line_to (cr, ph, get_height());
269 cairo_pop_group_to_source (cr);
275 /** Render a region for the summary.
276 * @param r Region view.
277 * @param cr Cairo context.
278 * @param y y coordinate to render at.
281 EditorSummary::render_region (RegionView* r, cairo_t* cr, double y) const
283 uint32_t const c = r->get_fill_color ();
284 cairo_set_source_rgb (cr, UINT_RGBA_R (c) / 255.0, UINT_RGBA_G (c) / 255.0, UINT_RGBA_B (c) / 255.0);
286 if (r->region()->position() > _start) {
287 cairo_move_to (cr, (r->region()->position() - _start) * _x_scale, y);
289 cairo_move_to (cr, 0, y);
292 if ((r->region()->position() + r->region()->length()) > _start) {
293 cairo_line_to (cr, ((r->region()->position() - _start + r->region()->length())) * _x_scale, y);
295 cairo_line_to (cr, 0, y);
302 EditorSummary::set_background_dirty ()
304 if (!_background_dirty) {
305 _background_dirty = true;
310 /** Set the summary so that just the overlays (viewbox, playhead etc.) will be re-rendered */
312 EditorSummary::set_overlays_dirty ()
314 ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty);
318 /** Set the summary so that just the overlays (viewbox, playhead etc.) in a given area will be re-rendered */
320 EditorSummary::set_overlays_dirty (int x, int y, int w, int h)
322 ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty);
323 queue_draw_area (x, y, w, h);
327 /** Handle a size request.
328 * @param req GTK requisition
331 EditorSummary::on_size_request (Gtk::Requisition *req)
333 /* The left/right buttons will determine our height */
340 EditorSummary::centre_on_click (GdkEventButton* ev)
342 pair<double, double> xr;
345 double const w = xr.second - xr.first;
346 double ex = ev->x - w / 2;
349 } else if ((ex + w) > get_width()) {
350 ex = get_width() - w;
357 EditorSummary::on_enter_notify_event (GdkEventCrossing*)
360 Keyboard::magic_widget_grab_focus ();
365 EditorSummary::on_leave_notify_event (GdkEventCrossing*)
367 /* there are no inferior/child windows, so any leave event means that
370 Keyboard::magic_widget_drop_focus ();
375 EditorSummary::on_key_press_event (GdkEventKey* key)
378 GtkAccelKey set_playhead_accel;
379 if (gtk_accel_map_lookup_entry ("<Actions>/Editor/set-playhead", &set_playhead_accel)) {
380 if (key->keyval == set_playhead_accel.accel_key && (int) key->state == set_playhead_accel.accel_mods) {
383 _session->request_locate (_start + (framepos_t) x / _x_scale, _session->transport_rolling());
393 EditorSummary::on_key_release_event (GdkEventKey* key)
396 GtkAccelKey set_playhead_accel;
397 if (gtk_accel_map_lookup_entry ("<Actions>/Editor/set-playhead", &set_playhead_accel)) {
398 if (key->keyval == set_playhead_accel.accel_key && (int) key->state == set_playhead_accel.accel_mods) {
405 /** Handle a button press.
406 * @param ev GTK event.
409 EditorSummary::on_button_press_event (GdkEventButton* ev)
411 _old_follow_playhead = _editor->follow_playhead ();
413 if (ev->button != 1) {
417 pair<double, double> xr;
420 _start_editor_x = xr;
421 _start_mouse_x = ev->x;
422 _start_mouse_y = ev->y;
423 _start_position = get_position (ev->x, ev->y);
425 if (_start_position != INSIDE && _start_position != TO_LEFT_OR_RIGHT) {
427 /* start a zoom_trim drag */
429 _zoom_trim_position = get_position (ev->x, ev->y);
430 _zoom_trim_dragging = true;
431 _editor->_dragging_playhead = true;
432 _editor->set_follow_playhead (false);
434 if (suspending_editor_updates ()) {
435 get_editor (&_pending_editor_x, &_pending_editor_y);
436 _pending_editor_changed = false;
439 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
441 /* secondary-modifier-click: locate playhead */
443 _session->request_locate (ev->x / _x_scale + _start);
446 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
448 centre_on_click (ev);
452 /* start a move or zoom drag */
453 /* won't know which one until the mouse moves */
454 _begin_dragging = true;
460 /** @return true if we are currently suspending updates to the editor's viewport,
461 * which we do if configured to do so, and if in a drag of some kind.
464 EditorSummary::suspending_editor_updates () const
466 return (!UIConfiguration::instance().get_update_editor_during_summary_drag () && (_zoom_dragging || _zoom_trim_dragging || _move_dragging));
469 /** Fill in x and y with the editor's current viewable area in summary coordinates */
471 EditorSummary::get_editor (pair<double, double>* x, pair<double, double>* y) const
474 if (suspending_editor_updates ()) {
476 /* We are dragging, and configured not to update the editor window during drags,
477 * so just return where the editor will be when the drag finishes.
480 *x = _pending_editor_x;
482 *y = _pending_editor_y;
487 /* Otherwise query the editor for its actual position */
489 x->first = (_editor->leftmost_sample () - _start) * _x_scale;
490 x->second = x->first + _editor->current_page_samples() * _x_scale;
493 y->first = editor_y_to_summary (_editor->vertical_adjustment.get_value ());
494 y->second = editor_y_to_summary (_editor->vertical_adjustment.get_value () + _editor->visible_canvas_height() - _editor->get_trackview_group()->canvas_origin().y);
498 /** Get an expression of the position of a point with respect to the view rectangle */
499 EditorSummary::Position
500 EditorSummary::get_position (double x, double y) const
502 /* how close the mouse has to be to the edge of the view rectangle to be considered `on it',
505 int x_edge_size = (_view_rectangle_x.second - _view_rectangle_x.first) / 4;
506 x_edge_size = min (x_edge_size, 8);
507 x_edge_size = max (x_edge_size, 1);
509 bool const near_left = (std::abs (x - _view_rectangle_x.first) < x_edge_size);
510 bool const near_right = (std::abs (x - _view_rectangle_x.second) < x_edge_size);
511 bool const within_x = _view_rectangle_x.first < x && x < _view_rectangle_x.second;
515 } else if (near_right) {
517 } else if (within_x) {
520 return TO_LEFT_OR_RIGHT;
525 EditorSummary::set_cursor (Position p)
529 get_window()->set_cursor (*_editor->_cursors->resize_left);
532 get_window()->set_cursor (*_editor->_cursors->resize_right);
535 get_window()->set_cursor (*_editor->_cursors->move);
537 case TO_LEFT_OR_RIGHT:
538 get_window()->set_cursor (*_editor->_cursors->expand_left_right);
542 get_window()->set_cursor ();
548 EditorSummary::summary_zoom_step ( int steps /* negative steps to zoom "out" , positive steps to zoom "in" */ )
550 pair<double, double> xn;
554 // xn.first = (_editor->leftmost_sample () - _start) * _x_scale;
555 // xn.second = xn.first + _editor->current_page_samples() * _x_scale;
561 set_overlays_dirty ();
567 EditorSummary::on_motion_notify_event (GdkEventMotion* ev)
569 pair<double, double> xr = _start_editor_x;
570 double x = _start_editor_x.first;
572 if (_move_dragging) {
576 assert (_start_position == INSIDE || _start_position == TO_LEFT_OR_RIGHT);
577 x += ev->x - _start_mouse_x;
585 } else if (_zoom_dragging) {
587 //ToDo: refactor into summary_zoom_in/out(
588 //ToDo: protect the case where the editor position is small, and results in offsetting the position
590 double const dy = ev->y - _zoom_last_y;
592 summary_zoom_step( dy );
594 _zoom_last_y = ev->y;
596 } else if (_zoom_trim_dragging) {
598 double const dx = ev->x - _start_mouse_x;
600 if (_zoom_trim_position == LEFT) {
602 } else if (_zoom_trim_position == RIGHT) {
606 xr.first = -1; /* do not change */
609 set_overlays_dirty ();
610 set_cursor (_zoom_trim_position);
613 } else if (_begin_dragging) {
615 double const dx = ev->x - _start_mouse_x;
616 double const dy = ev->y - _start_mouse_y;
618 if ( fabs(dx) > fabs(dy) ) {
620 /* initiate a move drag */
622 /* get the editor's state in case we are suspending updates */
623 get_editor (&_pending_editor_x, &_pending_editor_y);
624 _pending_editor_changed = false;
626 _move_dragging = true;
628 _editor->_dragging_playhead = true;
629 _editor->set_follow_playhead (false);
631 get_window()->set_cursor (*_editor->_cursors->expand_left_right);
633 _begin_dragging = false;
635 } else if ( fabs(dy) > fabs(dx) ) {
637 /* initiate a zoom drag */
639 /* get the editor's state in case we are suspending updates */
640 get_editor (&_pending_editor_x, &_pending_editor_y);
641 _pending_editor_changed = false;
643 //_zoom_position = get_position (ev->x, ev->y);
644 _zoom_dragging = true;
645 _zoom_last_y = ev->y;
646 _editor->_dragging_playhead = true;
647 _editor->set_follow_playhead (false);
649 get_window()->set_cursor (*_editor->_cursors->expand_up_down);
651 _begin_dragging = false;
655 set_cursor ( INSIDE );
662 EditorSummary::on_button_release_event (GdkEventButton*)
664 bool const was_suspended = suspending_editor_updates ();
666 _begin_dragging = false;
667 _move_dragging = false;
668 _zoom_trim_dragging = false;
669 _zoom_dragging = false;
670 _editor->_dragging_playhead = false;
671 _editor->set_follow_playhead (_old_follow_playhead, false);
673 if (was_suspended && _pending_editor_changed) {
674 set_editor (_pending_editor_x);
681 EditorSummary::on_scroll_event (GdkEventScroll* ev)
684 pair<double, double> xr;
688 switch (ev->direction) {
689 case GDK_SCROLL_UP: {
691 summary_zoom_step( -2 );
696 case GDK_SCROLL_DOWN: {
698 summary_zoom_step( 2 );
703 case GDK_SCROLL_LEFT:
704 if (Keyboard::modifier_state_equals (ev->state, Keyboard::ScrollZoomHorizontalModifier)) {
705 _editor->temporal_zoom_step (false);
706 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
708 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
711 _editor->scroll_left_half_page ();
715 case GDK_SCROLL_RIGHT:
716 if (Keyboard::modifier_state_equals (ev->state, Keyboard::ScrollZoomHorizontalModifier)) {
717 _editor->temporal_zoom_step (true);
718 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
720 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
723 _editor->scroll_right_half_page ();
735 /** Set the editor to display a x range with the left at a given position
736 * and a y range with the top at a given position.
737 * x and y parameters are specified in summary coordinates.
738 * Zoom is not changed in either direction.
741 EditorSummary::set_editor (double const x)
743 if (_editor->pending_visual_change.idle_handler_id >= 0 && _editor->pending_visual_change.being_handled == true) {
745 /* As a side-effect, the Editor's visual change idle handler processes
746 pending GTK events. Hence this motion notify handler can be called
747 in the middle of a visual change idle handler, and if this happens,
748 the queue_visual_change calls below modify the variables that the
749 idle handler is working with. This causes problems. Hence this
750 check. It ensures that we won't modify the pending visual change
751 while a visual change idle handler is in progress. It's not perfect,
752 as it also means that we won't change these variables if an idle handler
753 is merely pending but not executing. But c'est la vie.
762 /** Set the editor to display a given x range and a y range with the top at a given position.
763 * The editor's x zoom is adjusted if necessary, but the y zoom is not changed.
764 * x and y parameters are specified in summary coordinates.
767 EditorSummary::set_editor (pair<double,double> const x)
769 if (_editor->pending_visual_change.idle_handler_id >= 0) {
770 /* see comment in other set_editor () */
779 /** Set the left of the x range visible in the editor.
780 * Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
781 * @param x new x left position in summary coordinates.
784 EditorSummary::set_editor_x (double x)
790 if (suspending_editor_updates ()) {
791 double const w = _pending_editor_x.second - _pending_editor_x.first;
792 _pending_editor_x.first = x;
793 _pending_editor_x.second = x + w;
794 _pending_editor_changed = true;
797 _editor->reset_x_origin (x / _x_scale + _start);
801 /** Set the x range visible in the editor.
802 * Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
803 * @param x new x range in summary coordinates.
806 EditorSummary::set_editor_x (pair<double, double> x)
813 x.second = x.first + 1;
816 if (suspending_editor_updates ()) {
817 _pending_editor_x = x;
818 _pending_editor_changed = true;
821 _editor->reset_x_origin (x.first / _x_scale + _start);
824 ((x.second - x.first) / _x_scale) /
825 _editor->sample_to_pixel (_editor->current_page_samples())
828 if (nx != _editor->get_current_zoom ()) {
829 _editor->reset_zoom (nx);
835 EditorSummary::playhead_position_changed (framepos_t p)
837 int const o = int (_last_playhead);
838 int const n = int (playhead_frame_to_position (p));
839 if (_session && o != n) {
840 int a = max(2, min (o, n));
842 set_overlays_dirty (a - 2, 0, b + 2, get_height ());
847 EditorSummary::editor_y_to_summary (double y) const
850 for (TrackViewList::const_iterator i = _editor->track_views.begin (); i != _editor->track_views.end(); ++i) {
852 if ((*i)->hidden()) {
856 double const h = (*i)->effective_height ();
859 return sy + y * _track_height / h;
870 EditorSummary::routes_added (list<RouteTimeAxisView*> const & r)
872 for (list<RouteTimeAxisView*>::const_iterator i = r.begin(); i != r.end(); ++i) {
873 /* Connect to the relevant signal for the route so that we know when its colour has changed */
874 (*i)->route()->presentation_info().PropertyChanged.connect (*this, invalidator (*this), boost::bind (&EditorSummary::route_gui_changed, this, _1), gui_context ());
875 boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> ((*i)->route ());
877 tr->PlaylistChanged.connect (*this, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context ());
881 set_background_dirty ();
885 EditorSummary::route_gui_changed (PBD::PropertyChange const& what_changed)
887 if (what_changed.contains (Properties::color)) {
888 set_background_dirty ();
893 EditorSummary::playhead_frame_to_position (framepos_t t) const
895 return (t - _start) * _x_scale;
899 EditorSummary::position_to_playhead_frame_to_position (double pos) const
901 return _start + (pos * _x_scale);