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 _move_dragging (false),
55 _view_rectangle_x (0, 0),
56 _view_rectangle_y (0, 0),
57 _zoom_dragging (false),
58 _old_follow_playhead (false),
60 _background_dirty (true)
62 CairoWidget::use_nsglview ();
63 add_events (Gdk::POINTER_MOTION_MASK|Gdk::KEY_PRESS_MASK|Gdk::KEY_RELEASE_MASK|Gdk::ENTER_NOTIFY_MASK|Gdk::LEAVE_NOTIFY_MASK);
64 set_flags (get_flags() | Gtk::CAN_FOCUS);
66 UIConfiguration::instance().ParameterChanged.connect (sigc::mem_fun (*this, &EditorSummary::parameter_changed));
69 EditorSummary::~EditorSummary ()
71 cairo_surface_destroy (_image);
75 EditorSummary::parameter_changed (string p)
78 if (p == "color-regions-using-track-color") {
79 set_background_dirty ();
83 /** Handle a size allocation.
84 * @param alloc GTK allocation.
87 EditorSummary::on_size_allocate (Gtk::Allocation& alloc)
89 CairoWidget::on_size_allocate (alloc);
90 set_background_dirty ();
94 /** Connect to a session.
98 EditorSummary::set_session (Session* s)
100 SessionHandlePtr::set_session (s);
104 /* Note: the EditorSummary already finds out about new regions from Editor::region_view_added
105 * (which attaches to StreamView::RegionViewAdded), and cut regions by the RegionPropertyChanged
106 * emitted when a cut region is added to the `cutlist' playlist.
110 Region::RegionPropertyChanged.connect (region_property_connection, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
111 PresentationInfo::Change.connect (route_ctrl_id_connection, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
112 _editor->playhead_cursor->PositionChanged.connect (position_connection, invalidator (*this), boost::bind (&EditorSummary::playhead_position_changed, this, _1), gui_context());
113 _session->StartTimeChanged.connect (_session_connections, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
114 _session->EndTimeChanged.connect (_session_connections, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
115 _editor->selection->RegionsChanged.connect (sigc::mem_fun(*this, &EditorSummary::set_background_dirty));
120 EditorSummary::render_background_image ()
122 cairo_surface_destroy (_image); // passing NULL is safe
123 _image = cairo_image_surface_create (CAIRO_FORMAT_RGB24, get_width (), get_height ());
125 cairo_t* cr = cairo_create (_image);
127 /* background (really just the dividing lines between tracks */
129 cairo_set_source_rgb (cr, 0, 0, 0);
130 cairo_rectangle (cr, 0, 0, get_width(), get_height());
133 /* compute start and end points for the summary */
135 framecnt_t const session_length = _session->current_end_frame() - _session->current_start_frame ();
136 double const theoretical_start = _session->current_start_frame() - session_length * _overhang_fraction;
137 _start = theoretical_start > 0 ? theoretical_start : 0;
138 _end = _session->current_end_frame() + session_length * _overhang_fraction;
140 /* compute track height */
142 for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
143 if (!(*i)->hidden()) {
151 _track_height = (double) get_height() / N;
154 /* calculate x scale */
155 if (_end != _start) {
156 _x_scale = static_cast<double> (get_width()) / (_end - _start);
161 /* render tracks and regions */
164 for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
166 if ((*i)->hidden()) {
170 /* paint a non-bg colored strip to represent the track itself */
172 cairo_set_source_rgb (cr, 0.2, 0.2, 0.2);
173 cairo_set_line_width (cr, _track_height - 1);
174 cairo_move_to (cr, 0, y + _track_height / 2);
175 cairo_line_to (cr, get_width(), y + _track_height / 2);
178 StreamView* s = (*i)->view ();
181 cairo_set_line_width (cr, _track_height * 0.8);
183 s->foreach_regionview (sigc::bind (
184 sigc::mem_fun (*this, &EditorSummary::render_region),
186 y + _track_height / 2
193 /* start and end markers */
195 cairo_set_line_width (cr, 1);
196 cairo_set_source_rgb (cr, 1, 1, 0);
198 const double p = (_session->current_start_frame() - _start) * _x_scale;
199 cairo_move_to (cr, p, 0);
200 cairo_line_to (cr, p, get_height());
202 double const q = (_session->current_end_frame() - _start) * _x_scale;
203 cairo_move_to (cr, q, 0);
204 cairo_line_to (cr, q, get_height());
210 /** Render the required regions to a cairo context.
214 EditorSummary::render (Cairo::RefPtr<Cairo::Context> const& ctx, cairo_rectangle_t*)
216 cairo_t* cr = ctx->cobj();
222 if (!_image || _background_dirty) {
223 render_background_image ();
224 _background_dirty = false;
227 cairo_push_group (cr);
229 /* Fill with the background image */
231 cairo_rectangle (cr, 0, 0, get_width(), get_height());
232 cairo_set_source_surface (cr, _image, 0, 0);
235 /* Render the view rectangle. If there is an editor visual pending, don't update
236 * the view rectangle now --- wait until the expose event that we'll get after
237 * the visual change. This prevents a flicker.
240 if (_editor->pending_visual_change.idle_handler_id < 0) {
241 get_editor (&_view_rectangle_x, &_view_rectangle_y);
244 int32_t width = _view_rectangle_x.second - _view_rectangle_x.first;
245 int32_t height = _view_rectangle_y.second - _view_rectangle_y.first;
246 cairo_rectangle (cr, _view_rectangle_x.first, _view_rectangle_y.first, width, height);
247 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
251 cairo_rectangle (cr, _view_rectangle_x.first, 0, width, get_height ());
252 cairo_set_line_width (cr, 1);
253 cairo_set_source_rgba (cr, 1, 1, 1, 0.9);
258 cairo_set_line_width (cr, 1);
259 /* XXX: colour should be set from configuration file */
260 cairo_set_source_rgba (cr, 1, 0, 0, 1);
262 const double ph= playhead_frame_to_position (_editor->playhead_cursor->current_frame());
263 cairo_move_to (cr, ph, 0);
264 cairo_line_to (cr, ph, get_height());
266 cairo_pop_group_to_source (cr);
272 /** Render a region for the summary.
273 * @param r Region view.
274 * @param cr Cairo context.
275 * @param y y coordinate to render at.
278 EditorSummary::render_region (RegionView* r, cairo_t* cr, double y) const
280 uint32_t const c = r->get_fill_color ();
281 cairo_set_source_rgb (cr, UINT_RGBA_R (c) / 255.0, UINT_RGBA_G (c) / 255.0, UINT_RGBA_B (c) / 255.0);
283 if (r->region()->position() > _start) {
284 cairo_move_to (cr, (r->region()->position() - _start) * _x_scale, y);
286 cairo_move_to (cr, 0, y);
289 if ((r->region()->position() + r->region()->length()) > _start) {
290 cairo_line_to (cr, ((r->region()->position() - _start + r->region()->length())) * _x_scale, y);
292 cairo_line_to (cr, 0, y);
299 EditorSummary::set_background_dirty ()
301 if (!_background_dirty) {
302 _background_dirty = true;
307 /** Set the summary so that just the overlays (viewbox, playhead etc.) will be re-rendered */
309 EditorSummary::set_overlays_dirty ()
311 ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty);
315 /** Set the summary so that just the overlays (viewbox, playhead etc.) in a given area will be re-rendered */
317 EditorSummary::set_overlays_dirty (int x, int y, int w, int h)
319 ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty);
320 queue_draw_area (x, y, w, h);
324 /** Handle a size request.
325 * @param req GTK requisition
328 EditorSummary::on_size_request (Gtk::Requisition *req)
330 /* Use a dummy, small width and the actual height that we want */
337 EditorSummary::centre_on_click (GdkEventButton* ev)
339 pair<double, double> xr;
342 double const w = xr.second - xr.first;
343 double ex = ev->x - w / 2;
346 } else if ((ex + w) > get_width()) {
347 ex = get_width() - w;
354 EditorSummary::on_enter_notify_event (GdkEventCrossing*)
357 Keyboard::magic_widget_grab_focus ();
362 EditorSummary::on_leave_notify_event (GdkEventCrossing*)
364 /* there are no inferior/child windows, so any leave event means that
367 Keyboard::magic_widget_drop_focus ();
372 EditorSummary::on_key_press_event (GdkEventKey* key)
375 GtkAccelKey set_playhead_accel;
376 if (gtk_accel_map_lookup_entry ("<Actions>/Editor/set-playhead", &set_playhead_accel)) {
377 if (key->keyval == set_playhead_accel.accel_key && (int) key->state == set_playhead_accel.accel_mods) {
380 _session->request_locate (_start + (framepos_t) x / _x_scale, _session->transport_rolling());
390 EditorSummary::on_key_release_event (GdkEventKey* key)
393 GtkAccelKey set_playhead_accel;
394 if (gtk_accel_map_lookup_entry ("<Actions>/Editor/set-playhead", &set_playhead_accel)) {
395 if (key->keyval == set_playhead_accel.accel_key && (int) key->state == set_playhead_accel.accel_mods) {
402 /** Handle a button press.
403 * @param ev GTK event.
406 EditorSummary::on_button_press_event (GdkEventButton* ev)
408 _old_follow_playhead = _editor->follow_playhead ();
410 if (ev->button != 1) {
414 pair<double, double> xr;
417 _start_editor_x = xr;
418 _start_mouse_x = ev->x;
419 _start_position = get_position (ev->x, ev->y);
421 if (_start_position != INSIDE && _start_position != TO_LEFT_OR_RIGHT) {
423 /* start a zoom drag */
425 _zoom_position = get_position (ev->x, ev->y);
426 _zoom_dragging = true;
427 _editor->_dragging_playhead = true;
428 _editor->set_follow_playhead (false);
430 if (suspending_editor_updates ()) {
431 get_editor (&_pending_editor_x, &_pending_editor_y);
432 _pending_editor_changed = false;
435 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
437 /* secondary-modifier-click: locate playhead */
439 _session->request_locate (ev->x / _x_scale + _start);
442 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
444 centre_on_click (ev);
448 /* start a move drag */
450 /* get the editor's state in case we are suspending updates */
451 get_editor (&_pending_editor_x, &_pending_editor_y);
452 _pending_editor_changed = false;
454 _move_dragging = true;
456 _editor->_dragging_playhead = true;
457 _editor->set_follow_playhead (false);
463 /** @return true if we are currently suspending updates to the editor's viewport,
464 * which we do if configured to do so, and if in a drag of some kind.
467 EditorSummary::suspending_editor_updates () const
469 return (!UIConfiguration::instance().get_update_editor_during_summary_drag () && (_zoom_dragging || _move_dragging));
472 /** Fill in x and y with the editor's current viewable area in summary coordinates */
474 EditorSummary::get_editor (pair<double, double>* x, pair<double, double>* y) const
477 if (suspending_editor_updates ()) {
479 /* We are dragging, and configured not to update the editor window during drags,
480 * so just return where the editor will be when the drag finishes.
483 *x = _pending_editor_x;
485 *y = _pending_editor_y;
490 /* Otherwise query the editor for its actual position */
492 x->first = (_editor->leftmost_sample () - _start) * _x_scale;
493 x->second = x->first + _editor->current_page_samples() * _x_scale;
496 y->first = editor_y_to_summary (_editor->vertical_adjustment.get_value ());
497 y->second = editor_y_to_summary (_editor->vertical_adjustment.get_value () + _editor->visible_canvas_height() - _editor->get_trackview_group()->canvas_origin().y);
501 /** Get an expression of the position of a point with respect to the view rectangle */
502 EditorSummary::Position
503 EditorSummary::get_position (double x, double y) const
505 /* how close the mouse has to be to the edge of the view rectangle to be considered `on it',
508 int x_edge_size = (_view_rectangle_x.second - _view_rectangle_x.first) / 4;
509 x_edge_size = min (x_edge_size, 8);
510 x_edge_size = max (x_edge_size, 1);
512 bool const near_left = (std::abs (x - _view_rectangle_x.first) < x_edge_size);
513 bool const near_right = (std::abs (x - _view_rectangle_x.second) < x_edge_size);
514 bool const within_x = _view_rectangle_x.first < x && x < _view_rectangle_x.second;
518 } else if (near_right) {
520 } else if (within_x) {
523 return TO_LEFT_OR_RIGHT;
528 EditorSummary::set_cursor (Position p)
532 get_window()->set_cursor (*_editor->_cursors->resize_left);
535 get_window()->set_cursor (*_editor->_cursors->resize_right);
538 get_window()->set_cursor (*_editor->_cursors->move);
540 case TO_LEFT_OR_RIGHT:
541 get_window()->set_cursor (*_editor->_cursors->expand_left_right);
545 get_window()->set_cursor ();
551 EditorSummary::on_motion_notify_event (GdkEventMotion* ev)
553 pair<double, double> xr = _start_editor_x;
554 double x = _start_editor_x.first;
556 if (_move_dragging) {
560 assert (_start_position == INSIDE || _start_position == TO_LEFT_OR_RIGHT);
561 x += ev->x - _start_mouse_x;
569 } else if (_zoom_dragging) {
571 double const dx = ev->x - _start_mouse_x;
573 if (_zoom_position == LEFT) {
575 } else if (_zoom_position == RIGHT) {
579 xr.first = -1; /* do not change */
582 set_overlays_dirty ();
583 set_cursor (_zoom_position);
587 set_cursor (get_position (ev->x, ev->y));
594 EditorSummary::on_button_release_event (GdkEventButton*)
596 bool const was_suspended = suspending_editor_updates ();
598 _move_dragging = false;
599 _zoom_dragging = false;
600 _editor->_dragging_playhead = false;
601 _editor->set_follow_playhead (_old_follow_playhead, false);
603 if (was_suspended && _pending_editor_changed) {
604 set_editor (_pending_editor_x);
611 EditorSummary::on_scroll_event (GdkEventScroll* ev)
614 pair<double, double> xr;
618 switch (ev->direction) {
620 case GDK_SCROLL_LEFT:
621 if (Keyboard::modifier_state_equals (ev->state, Keyboard::ScrollZoomHorizontalModifier)) {
622 _editor->temporal_zoom_step (false);
623 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
625 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
628 _editor->scroll_left_half_page ();
632 case GDK_SCROLL_DOWN:
633 case GDK_SCROLL_RIGHT:
634 if (Keyboard::modifier_state_equals (ev->state, Keyboard::ScrollZoomHorizontalModifier)) {
635 _editor->temporal_zoom_step (true);
636 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
638 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
641 _editor->scroll_right_half_page ();
653 /** Set the editor to display a x range with the left at a given position
654 * and a y range with the top at a given position.
655 * x and y parameters are specified in summary coordinates.
656 * Zoom is not changed in either direction.
659 EditorSummary::set_editor (double const x)
661 if (_editor->pending_visual_change.idle_handler_id >= 0 && _editor->pending_visual_change.being_handled == true) {
663 /* As a side-effect, the Editor's visual change idle handler processes
664 pending GTK events. Hence this motion notify handler can be called
665 in the middle of a visual change idle handler, and if this happens,
666 the queue_visual_change calls below modify the variables that the
667 idle handler is working with. This causes problems. Hence this
668 check. It ensures that we won't modify the pending visual change
669 while a visual change idle handler is in progress. It's not perfect,
670 as it also means that we won't change these variables if an idle handler
671 is merely pending but not executing. But c'est la vie.
680 /** Set the editor to display a given x range and a y range with the top at a given position.
681 * The editor's x zoom is adjusted if necessary, but the y zoom is not changed.
682 * x and y parameters are specified in summary coordinates.
685 EditorSummary::set_editor (pair<double,double> const x)
687 if (_editor->pending_visual_change.idle_handler_id >= 0) {
688 /* see comment in other set_editor () */
697 /** Set the left of the x range visible in the editor.
698 * Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
699 * @param x new x left position in summary coordinates.
702 EditorSummary::set_editor_x (double x)
708 if (suspending_editor_updates ()) {
709 double const w = _pending_editor_x.second - _pending_editor_x.first;
710 _pending_editor_x.first = x;
711 _pending_editor_x.second = x + w;
712 _pending_editor_changed = true;
715 _editor->reset_x_origin (x / _x_scale + _start);
719 /** Set the x range visible in the editor.
720 * Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
721 * @param x new x range in summary coordinates.
724 EditorSummary::set_editor_x (pair<double, double> x)
731 x.second = x.first + 1;
734 if (suspending_editor_updates ()) {
735 _pending_editor_x = x;
736 _pending_editor_changed = true;
739 _editor->reset_x_origin (x.first / _x_scale + _start);
742 ((x.second - x.first) / _x_scale) /
743 _editor->sample_to_pixel (_editor->current_page_samples())
746 if (nx != _editor->get_current_zoom ()) {
747 _editor->reset_zoom (nx);
753 EditorSummary::playhead_position_changed (framepos_t p)
755 int const o = int (_last_playhead);
756 int const n = int (playhead_frame_to_position (p));
757 if (_session && o != n) {
758 int a = max(2, min (o, n));
760 set_overlays_dirty (a - 2, 0, b + 2, get_height ());
765 EditorSummary::editor_y_to_summary (double y) const
768 for (TrackViewList::const_iterator i = _editor->track_views.begin (); i != _editor->track_views.end(); ++i) {
770 if ((*i)->hidden()) {
774 double const h = (*i)->effective_height ();
777 return sy + y * _track_height / h;
788 EditorSummary::routes_added (list<RouteTimeAxisView*> const & r)
790 for (list<RouteTimeAxisView*>::const_iterator i = r.begin(); i != r.end(); ++i) {
791 /* Connect to the relevant signal for the route so that we know when its colour has changed */
792 (*i)->route()->presentation_info().PropertyChanged.connect (*this, invalidator (*this), boost::bind (&EditorSummary::route_gui_changed, this, _1), gui_context ());
793 boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> ((*i)->route ());
795 tr->PlaylistChanged.connect (*this, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context ());
799 set_background_dirty ();
803 EditorSummary::route_gui_changed (PBD::PropertyChange const& what_changed)
805 if (what_changed.contains (Properties::color)) {
806 set_background_dirty ();
811 EditorSummary::playhead_frame_to_position (framepos_t t) const
813 return (t - _start) * _x_scale;
817 EditorSummary::position_to_playhead_frame_to_position (double pos) const
819 return _start + (pos * _x_scale);