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::on_motion_notify_event (GdkEventMotion* ev)
550 pair<double, double> xr = _start_editor_x;
551 double x = _start_editor_x.first;
553 if (_move_dragging) {
557 assert (_start_position == INSIDE || _start_position == TO_LEFT_OR_RIGHT);
558 x += ev->x - _start_mouse_x;
566 } else if (_zoom_dragging) {
568 //ToDo: refactor into summary_zoom_in/out(
569 //ToDo: protect the case where the editor position is small, and results in offsetting the position
571 double const dy = ev->y - _zoom_last_y;
573 pair<double, double> xn;
579 set_overlays_dirty ();
582 _zoom_last_y = ev->y;
584 } else if (_zoom_trim_dragging) {
586 double const dx = ev->x - _start_mouse_x;
588 if (_zoom_trim_position == LEFT) {
590 } else if (_zoom_trim_position == RIGHT) {
594 xr.first = -1; /* do not change */
597 set_overlays_dirty ();
598 set_cursor (_zoom_trim_position);
601 } else if (_begin_dragging) {
603 double const dx = ev->x - _start_mouse_x;
604 double const dy = ev->y - _start_mouse_y;
606 if ( fabs(dx) > fabs(dy) ) {
608 /* initiate a move drag */
610 /* get the editor's state in case we are suspending updates */
611 get_editor (&_pending_editor_x, &_pending_editor_y);
612 _pending_editor_changed = false;
614 _move_dragging = true;
616 _editor->_dragging_playhead = true;
617 _editor->set_follow_playhead (false);
619 get_window()->set_cursor (*_editor->_cursors->expand_left_right);
621 _begin_dragging = false;
623 } else if ( fabs(dy) > fabs(dx) ) {
625 /* initiate a zoom drag */
627 /* get the editor's state in case we are suspending updates */
628 get_editor (&_pending_editor_x, &_pending_editor_y);
629 _pending_editor_changed = false;
631 //_zoom_position = get_position (ev->x, ev->y);
632 _zoom_dragging = true;
633 _zoom_last_y = ev->y;
634 _editor->_dragging_playhead = true;
635 _editor->set_follow_playhead (false);
637 get_window()->set_cursor (*_editor->_cursors->expand_up_down);
639 _begin_dragging = false;
643 set_cursor ( INSIDE );
650 EditorSummary::on_button_release_event (GdkEventButton*)
652 bool const was_suspended = suspending_editor_updates ();
654 _begin_dragging = false;
655 _move_dragging = false;
656 _zoom_trim_dragging = false;
657 _zoom_dragging = false;
658 _editor->_dragging_playhead = false;
659 _editor->set_follow_playhead (_old_follow_playhead, false);
661 if (was_suspended && _pending_editor_changed) {
662 set_editor (_pending_editor_x);
669 EditorSummary::on_scroll_event (GdkEventScroll* ev)
672 pair<double, double> xr;
676 switch (ev->direction) {
677 case GDK_SCROLL_UP: {
678 //ToDo: use function summary_zoom_in/out
680 pair<double, double> xn;
686 set_overlays_dirty ();
692 case GDK_SCROLL_DOWN: {
693 pair<double, double> xn;
699 set_overlays_dirty ();
705 case GDK_SCROLL_LEFT:
706 if (Keyboard::modifier_state_equals (ev->state, Keyboard::ScrollZoomHorizontalModifier)) {
707 _editor->temporal_zoom_step (false);
708 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
710 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
713 _editor->scroll_left_half_page ();
717 case GDK_SCROLL_RIGHT:
718 if (Keyboard::modifier_state_equals (ev->state, Keyboard::ScrollZoomHorizontalModifier)) {
719 _editor->temporal_zoom_step (true);
720 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
722 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
725 _editor->scroll_right_half_page ();
737 /** Set the editor to display a x range with the left at a given position
738 * and a y range with the top at a given position.
739 * x and y parameters are specified in summary coordinates.
740 * Zoom is not changed in either direction.
743 EditorSummary::set_editor (double const x)
745 if (_editor->pending_visual_change.idle_handler_id >= 0 && _editor->pending_visual_change.being_handled == true) {
747 /* As a side-effect, the Editor's visual change idle handler processes
748 pending GTK events. Hence this motion notify handler can be called
749 in the middle of a visual change idle handler, and if this happens,
750 the queue_visual_change calls below modify the variables that the
751 idle handler is working with. This causes problems. Hence this
752 check. It ensures that we won't modify the pending visual change
753 while a visual change idle handler is in progress. It's not perfect,
754 as it also means that we won't change these variables if an idle handler
755 is merely pending but not executing. But c'est la vie.
764 /** Set the editor to display a given x range and a y range with the top at a given position.
765 * The editor's x zoom is adjusted if necessary, but the y zoom is not changed.
766 * x and y parameters are specified in summary coordinates.
769 EditorSummary::set_editor (pair<double,double> const x)
771 if (_editor->pending_visual_change.idle_handler_id >= 0) {
772 /* see comment in other set_editor () */
781 /** Set the left of the x range visible in the editor.
782 * Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
783 * @param x new x left position in summary coordinates.
786 EditorSummary::set_editor_x (double x)
792 if (suspending_editor_updates ()) {
793 double const w = _pending_editor_x.second - _pending_editor_x.first;
794 _pending_editor_x.first = x;
795 _pending_editor_x.second = x + w;
796 _pending_editor_changed = true;
799 _editor->reset_x_origin (x / _x_scale + _start);
803 /** Set the x range visible in the editor.
804 * Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
805 * @param x new x range in summary coordinates.
808 EditorSummary::set_editor_x (pair<double, double> x)
815 x.second = x.first + 1;
818 if (suspending_editor_updates ()) {
819 _pending_editor_x = x;
820 _pending_editor_changed = true;
823 _editor->reset_x_origin (x.first / _x_scale + _start);
826 ((x.second - x.first) / _x_scale) /
827 _editor->sample_to_pixel (_editor->current_page_samples())
830 if (nx != _editor->get_current_zoom ()) {
831 _editor->reset_zoom (nx);
837 EditorSummary::playhead_position_changed (framepos_t p)
839 int const o = int (_last_playhead);
840 int const n = int (playhead_frame_to_position (p));
841 if (_session && o != n) {
842 int a = max(2, min (o, n));
844 set_overlays_dirty (a - 2, 0, b + 2, get_height ());
849 EditorSummary::editor_y_to_summary (double y) const
852 for (TrackViewList::const_iterator i = _editor->track_views.begin (); i != _editor->track_views.end(); ++i) {
854 if ((*i)->hidden()) {
858 double const h = (*i)->effective_height ();
861 return sy + y * _track_height / h;
872 EditorSummary::routes_added (list<RouteTimeAxisView*> const & r)
874 for (list<RouteTimeAxisView*>::const_iterator i = r.begin(); i != r.end(); ++i) {
875 /* Connect to the relevant signal for the route so that we know when its colour has changed */
876 (*i)->route()->presentation_info().PropertyChanged.connect (*this, invalidator (*this), boost::bind (&EditorSummary::route_gui_changed, this, _1), gui_context ());
877 boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> ((*i)->route ());
879 tr->PlaylistChanged.connect (*this, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context ());
883 set_background_dirty ();
887 EditorSummary::route_gui_changed (PBD::PropertyChange const& what_changed)
889 if (what_changed.contains (Properties::color)) {
890 set_background_dirty ();
895 EditorSummary::playhead_frame_to_position (framepos_t t) const
897 return (t - _start) * _x_scale;
901 EditorSummary::position_to_playhead_frame_to_position (double pos) const
903 return _start + (pos * _x_scale);