1 /*
2 * Copyright (C) 2009-2012 Carl Hetherington <carl@carlh.net>
3 * Copyright (C) 2009-2015 David Robillard <d@drobilla.net>
4 * Copyright (C) 2009-2019 Paul Davis <paul@linuxaudiosystems.com>
5 * Copyright (C) 2012-2013 Colin Fletcher <colin.m.fletcher@googlemail.com>
6 * Copyright (C) 2012-2019 Robin Gareus <robin@gareus.org>
7 * Copyright (C) 2014-2016 Nick Mainsbridge <mainsbridge@gmail.com>
8 * Copyright (C) 2015-2016 Tim Mayberry <mojofunk@gmail.com>
9 * Copyright (C) 2017-2019 Ben Loftis <ben@harrisonconsoles.com>
10 *
11 * This program is free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * This program is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 * GNU General Public License for more details.
20 *
21 * You should have received a copy of the GNU General Public License along
22 * with this program; if not, write to the Free Software Foundation, Inc.,
23 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24 */
25
26 #include "ardour/session.h"
27
28 #include "canvas/debug.h"
29
30 #include <gtkmm/menu.h>
31 #include <gtkmm/menuitem.h>
32
33 #include "context_menu_helper.h"
34 #include "time_axis_view.h"
35 #include "streamview.h"
36 #include "editor_summary.h"
37 #include "gui_thread.h"
38 #include "editor.h"
39 #include "region_view.h"
40 #include "rgb_macros.h"
41 #include "keyboard.h"
42 #include "editor_routes.h"
43 #include "editor_cursors.h"
44 #include "mouse_cursors.h"
45 #include "route_time_axis.h"
46 #include "ui_config.h"
47
48 #include "pbd/i18n.h"
49
50 using namespace std;
51 using namespace ARDOUR;
52 using Gtkmm2ext::Keyboard;
53
54 /** Construct an EditorSummary.
55 * @param e Editor to represent.
56 */
EditorSummary(Editor * e)57 EditorSummary::EditorSummary (Editor* e)
58 : EditorComponent (e),
59 _start (0),
60 _end (1),
61 _x_scale (1),
62 _track_height (16),
63 _last_playhead (-1),
64 _move_dragging (false),
65 _view_rectangle_x (0, 0),
66 _view_rectangle_y (0, 0),
67 _zoom_trim_dragging (false),
68 _old_follow_playhead (false),
69 _image (0),
70 _background_dirty (true)
71 {
72 CairoWidget::use_nsglview ();
73 add_events (Gdk::POINTER_MOTION_MASK|Gdk::KEY_PRESS_MASK|Gdk::KEY_RELEASE_MASK|Gdk::ENTER_NOTIFY_MASK|Gdk::LEAVE_NOTIFY_MASK);
74 set_flags (get_flags() | Gtk::CAN_FOCUS);
75
76 UIConfiguration::instance().ParameterChanged.connect (sigc::mem_fun (*this, &EditorSummary::parameter_changed));
77 }
78
~EditorSummary()79 EditorSummary::~EditorSummary ()
80 {
81 cairo_surface_destroy (_image);
82 }
83
84 void
parameter_changed(string p)85 EditorSummary::parameter_changed (string p)
86 {
87
88 if (p == "color-regions-using-track-color") {
89 set_background_dirty ();
90 }
91 }
92
93 /** Handle a size allocation.
94 * @param alloc GTK allocation.
95 */
96 void
on_size_allocate(Gtk::Allocation & alloc)97 EditorSummary::on_size_allocate (Gtk::Allocation& alloc)
98 {
99 CairoWidget::on_size_allocate (alloc);
100 set_background_dirty ();
101 }
102
103
104 /** Connect to a session.
105 * @param s Session.
106 */
107 void
set_session(Session * s)108 EditorSummary::set_session (Session* s)
109 {
110 SessionHandlePtr::set_session (s);
111
112 set_dirty ();
113
114 /* Note: the EditorSummary already finds out about new regions from Editor::region_view_added
115 * (which attaches to StreamView::RegionViewAdded), and cut regions by the RegionPropertyChanged
116 * emitted when a cut region is added to the `cutlist' playlist.
117 */
118
119 if (_session) {
120 Region::RegionsPropertyChanged.connect (region_property_connection, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
121 PresentationInfo::Change.connect (route_ctrl_id_connection, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
122 _editor->playhead_cursor()->PositionChanged.connect (position_connection, invalidator (*this), boost::bind (&EditorSummary::playhead_position_changed, this, _1), gui_context());
123 _session->StartTimeChanged.connect (_session_connections, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
124 _session->EndTimeChanged.connect (_session_connections, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
125 _editor->selection->RegionsChanged.connect (sigc::mem_fun(*this, &EditorSummary::set_background_dirty));
126 }
127
128 UIConfiguration::instance().ColorsChanged.connect (sigc::mem_fun (*this, &EditorSummary::set_colors));
129
130 set_colors();
131
132 _leftmost = max_samplepos;
133 _rightmost = 0;
134 }
135
136 void
render_background_image()137 EditorSummary::render_background_image ()
138 {
139 cairo_surface_destroy (_image); // passing NULL is safe
140 _image = cairo_image_surface_create (CAIRO_FORMAT_RGB24, get_width (), get_height ());
141
142 cairo_t* cr = cairo_create (_image);
143
144 /* background (really just the dividing lines between tracks */
145
146 cairo_set_source_rgb (cr, 0, 0, 0);
147 cairo_rectangle (cr, 0, 0, get_width(), get_height());
148 cairo_fill (cr);
149
150 /* compute start and end points for the summary */
151
152 std::pair<samplepos_t, samplepos_t> ext = _editor->session_gui_extents();
153 double theoretical_start = ext.first;
154 double theoretical_end = ext.second;
155
156 /* the summary should encompass the full extent of everywhere we've visited since the session was opened */
157 if (_leftmost < theoretical_start)
158 theoretical_start = _leftmost;
159 if (_rightmost > theoretical_end)
160 theoretical_end = _rightmost;
161
162 /* range-check */
163 _start = theoretical_start > 0 ? theoretical_start : 0;
164 _end = theoretical_end < max_samplepos ? theoretical_end : max_samplepos;
165
166 /* calculate x scale */
167 if (_end != _start) {
168 _x_scale = static_cast<double> (get_width()) / (_end - _start);
169 } else {
170 _x_scale = 1;
171 }
172
173 /* compute track height */
174 int N = 0;
175 for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
176 if (!(*i)->hidden()) {
177 ++N;
178 }
179 }
180
181 if (N == 0) {
182 _track_height = 16;
183 } else {
184 _track_height = (double) get_height() / N;
185 }
186
187 /* render tracks and regions */
188
189 double y = 0;
190 for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
191
192 if ((*i)->hidden()) {
193 continue;
194 }
195
196 /* paint a non-bg colored strip to represent the track itself */
197
198 if (_track_height > 4) {
199 cairo_set_source_rgb (cr, 0.2, 0.2, 0.2);
200 cairo_set_line_width (cr, _track_height - 1);
201 cairo_move_to (cr, 0, y + _track_height / 2);
202 cairo_line_to (cr, get_width(), y + _track_height / 2);
203 cairo_stroke (cr);
204 }
205
206 StreamView* s = (*i)->view ();
207
208 if (s) {
209 cairo_set_line_width (cr, _track_height * 0.8);
210
211 s->foreach_regionview (sigc::bind (
212 sigc::mem_fun (*this, &EditorSummary::render_region),
213 cr,
214 y + _track_height / 2
215 ));
216 }
217
218 y += _track_height;
219 }
220
221 /* start and end markers */
222
223 cairo_set_line_width (cr, 1);
224 cairo_set_source_rgb (cr, 1, 1, 0);
225
226 const double p = (_session->current_start_sample() - _start) * _x_scale;
227 cairo_move_to (cr, p, 0);
228 cairo_line_to (cr, p, get_height());
229
230 double const q = (_session->current_end_sample() - _start) * _x_scale;
231 cairo_move_to (cr, q, 0);
232 cairo_line_to (cr, q, get_height());
233 cairo_stroke (cr);
234
235 cairo_destroy (cr);
236 }
237
238 /** Render the required regions to a cairo context.
239 * @param cr Context.
240 */
241 void
render(Cairo::RefPtr<Cairo::Context> const & ctx,cairo_rectangle_t *)242 EditorSummary::render (Cairo::RefPtr<Cairo::Context> const& ctx, cairo_rectangle_t*)
243 {
244 cairo_t* cr = ctx->cobj();
245
246 if (_session == 0) {
247 return;
248 }
249
250 /* maintain the leftmost and rightmost locations that we've ever reached */
251 samplecnt_t const leftmost = _editor->leftmost_sample ();
252 if (leftmost < _leftmost) {
253 _leftmost = leftmost;
254 _background_dirty = true;
255 }
256 samplecnt_t const rightmost = leftmost + _editor->current_page_samples();
257 if (rightmost > _rightmost) {
258 _rightmost = rightmost;
259 _background_dirty = true;
260 }
261
262 /* draw the background (regions, markers, etc) if they've changed */
263 if (!_image || _background_dirty) {
264 render_background_image ();
265 _background_dirty = false;
266 }
267
268 cairo_push_group (cr);
269
270 /* Fill with the background image */
271
272 cairo_rectangle (cr, 0, 0, get_width(), get_height());
273 cairo_set_source_surface (cr, _image, 0, 0);
274 cairo_fill (cr);
275
276 /* Render the view rectangle. If there is an editor visual pending, don't update
277 * the view rectangle now --- wait until the expose event that we'll get after
278 * the visual change. This prevents a flicker.
279 */
280
281 if (_editor->pending_visual_change.idle_handler_id < 0) {
282 get_editor (&_view_rectangle_x, &_view_rectangle_y);
283 }
284
285 int32_t width = _view_rectangle_x.second - _view_rectangle_x.first;
286 std::min(8, width);
287 cairo_rectangle (cr, _view_rectangle_x.first, 0, width, get_height ());
288 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
289 cairo_fill (cr);
290
291 /* horiz zoom */
292 cairo_rectangle (cr, _view_rectangle_x.first, 0, width, get_height ());
293 cairo_set_line_width (cr, 1);
294 cairo_set_source_rgba (cr, 1, 1, 1, 0.9);
295 cairo_stroke (cr);
296
297 /* Playhead */
298
299 cairo_set_line_width (cr, 1);
300
301 double r,g,b,a; Gtkmm2ext::color_to_rgba(_phead_color, r,g,b,a);
302 cairo_set_source_rgb (cr, r,g,b); // playhead color
303
304 const double ph= playhead_sample_to_position (_editor->playhead_cursor ()->current_sample());
305 cairo_move_to (cr, ph, 0);
306 cairo_line_to (cr, ph, get_height());
307 cairo_stroke (cr);
308 cairo_pop_group_to_source (cr);
309 cairo_paint (cr);
310 _last_playhead = ph;
311
312 }
313
314 void
set_colors()315 EditorSummary::set_colors ()
316 {
317 _phead_color = UIConfiguration::instance().color ("play head");
318 }
319
320
321
322 /** Render a region for the summary.
323 * @param r Region view.
324 * @param cr Cairo context.
325 * @param y y coordinate to render at.
326 */
327 void
render_region(RegionView * r,cairo_t * cr,double y) const328 EditorSummary::render_region (RegionView* r, cairo_t* cr, double y) const
329 {
330 /*NOTE: you can optimize this operation by coalescing adjacent regions into a single line stroke.
331 * In a session with a single track ~1,000 regions, this reduced render time from 14ms to 11 ms.
332 * However, you lose a lot of visual information. The current method preserves a sense of separation between regions.
333 * The current method shows the current selection (red regions), which needs to be preserved if this is optimized.
334 * I think it's not worth it for now, but we might choose to revisit this someday.
335 */
336
337 uint32_t const c = r->get_fill_color ();
338 cairo_set_source_rgb (cr, UINT_RGBA_R (c) / 255.0, UINT_RGBA_G (c) / 255.0, UINT_RGBA_B (c) / 255.0);
339
340 if (r->region()->position() > _start) {
341 cairo_move_to (cr, (r->region()->position() - _start) * _x_scale, y);
342 } else {
343 cairo_move_to (cr, 0, y);
344 }
345
346 if ((r->region()->position() + r->region()->length()) > _start) {
347 cairo_line_to (cr, ((r->region()->position() - _start + r->region()->length())) * _x_scale, y);
348 } else {
349 cairo_line_to (cr, 0, y);
350 }
351
352 cairo_stroke (cr);
353 }
354
355 void
set_background_dirty()356 EditorSummary::set_background_dirty ()
357 {
358 if (!_background_dirty) {
359 _background_dirty = true;
360 set_dirty ();
361 }
362 }
363
364 /** Set the summary so that just the overlays (viewbox, playhead etc.) will be re-rendered */
365 void
set_overlays_dirty()366 EditorSummary::set_overlays_dirty ()
367 {
368 ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty);
369 queue_draw ();
370 }
371
372 /** Set the summary so that just the overlays (viewbox, playhead etc.) in a given area will be re-rendered */
373 void
set_overlays_dirty_rect(int x,int y,int w,int h)374 EditorSummary::set_overlays_dirty_rect (int x, int y, int w, int h)
375 {
376 ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty_rect);
377 queue_draw_area (x, y, w, h);
378 }
379
380
381 /** Handle a size request.
382 * @param req GTK requisition
383 */
384 void
on_size_request(Gtk::Requisition * req)385 EditorSummary::on_size_request (Gtk::Requisition *req)
386 {
387 /* The left/right buttons will determine our height */
388 req->width = -1;
389 req->height = -1;
390 }
391
392
393 void
centre_on_click(GdkEventButton * ev)394 EditorSummary::centre_on_click (GdkEventButton* ev)
395 {
396 pair<double, double> xr;
397 get_editor (&xr);
398
399 double const w = xr.second - xr.first;
400 double ex = ev->x - w / 2;
401 if (ex < 0) {
402 ex = 0;
403 } else if ((ex + w) > get_width()) {
404 ex = get_width() - w;
405 }
406
407 set_editor (ex);
408 }
409
410 bool
on_enter_notify_event(GdkEventCrossing *)411 EditorSummary::on_enter_notify_event (GdkEventCrossing*)
412 {
413 grab_focus ();
414 Keyboard::magic_widget_grab_focus ();
415 return false;
416 }
417
418 bool
on_leave_notify_event(GdkEventCrossing *)419 EditorSummary::on_leave_notify_event (GdkEventCrossing*)
420 {
421 /* there are no inferior/child windows, so any leave event means that
422 we're gone.
423 */
424 Keyboard::magic_widget_drop_focus ();
425 return false;
426 }
427
428 bool
on_key_press_event(GdkEventKey * key)429 EditorSummary::on_key_press_event (GdkEventKey* key)
430 {
431 gint x, y;
432 GtkAccelKey set_playhead_accel;
433
434 /* XXXX this is really ugly and should be using our own action maps and bindings */
435
436 if (gtk_accel_map_lookup_entry ("<Actions>/Editor/set-playhead", &set_playhead_accel)) {
437 if (key->keyval == set_playhead_accel.accel_key && (int) key->state == set_playhead_accel.accel_mods) {
438 if (_session) {
439 get_pointer (x, y);
440 _session->request_locate (_start + (samplepos_t) x / _x_scale);
441 return true;
442 }
443 }
444 }
445
446 return false;
447 }
448
449 bool
on_key_release_event(GdkEventKey * key)450 EditorSummary::on_key_release_event (GdkEventKey* key)
451 {
452
453 GtkAccelKey set_playhead_accel;
454
455 /* XXXX this is really ugly and should be using our own action maps and bindings */
456
457 if (gtk_accel_map_lookup_entry ("<Actions>/Editor/set-playhead", &set_playhead_accel)) {
458 if (key->keyval == set_playhead_accel.accel_key && (int) key->state == set_playhead_accel.accel_mods) {
459 return true;
460 }
461 }
462 return false;
463 }
464
465 #include "gtkmm2ext/utils.h"
466
467 /** Handle a button press.
468 * @param ev GTK event.
469 */
470 bool
on_button_press_event(GdkEventButton * ev)471 EditorSummary::on_button_press_event (GdkEventButton* ev)
472 {
473 _old_follow_playhead = _editor->follow_playhead ();
474
475 if (ev->button == 3) { // right-click: show the reset menu action
476 using namespace Gtk::Menu_Helpers;
477 Gtk::Menu* m = ARDOUR_UI_UTILS::shared_popup_menu ();
478 MenuList& items = m->items ();
479 items.push_back(MenuElem(_("Reset Summary to Extents"),
480 sigc::mem_fun(*this, &EditorSummary::reset_to_extents)));
481 m->popup (ev->button, ev->time);
482 return true;
483 }
484
485 if (ev->button != 1) {
486 return true;
487 }
488
489 pair<double, double> xr;
490 get_editor (&xr);
491
492 _start_editor_x = xr;
493 _start_mouse_x = ev->x;
494 _start_mouse_y = ev->y;
495 _start_position = get_position (ev->x, ev->y);
496
497 if (_start_position != INSIDE && _start_position != TO_LEFT_OR_RIGHT) {
498
499 /* start a zoom_trim drag */
500
501 _zoom_trim_position = get_position (ev->x, ev->y);
502 _zoom_trim_dragging = true;
503 _editor->_dragging_playhead = true;
504 _editor->set_follow_playhead (false);
505
506 if (suspending_editor_updates ()) {
507 get_editor (&_pending_editor_x, &_pending_editor_y);
508 _pending_editor_changed = false;
509 }
510
511 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
512
513 /* secondary-modifier-click: locate playhead */
514 if (_session) {
515 _session->request_locate (ev->x / _x_scale + _start);
516 }
517
518 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
519
520 centre_on_click (ev);
521
522 } else {
523
524 /* start a move+zoom drag */
525 get_editor (&_pending_editor_x, &_pending_editor_y);
526 _pending_editor_changed = false;
527 _editor->_dragging_playhead = true;
528 _editor->set_follow_playhead (false);
529
530 _move_dragging = true;
531
532 _last_mx = ev->x;
533 _last_my = ev->y;
534 _last_dx = 0;
535 _last_dy = 0;
536 _last_y_delta = 0;
537
538 get_window()->set_cursor (*_editor->_cursors->expand_left_right);
539
540 }
541
542 return true;
543 }
544
545 /** @return true if we are currently suspending updates to the editor's viewport,
546 * which we do if configured to do so, and if in a drag of some kind.
547 */
548 bool
suspending_editor_updates() const549 EditorSummary::suspending_editor_updates () const
550 {
551 return (!UIConfiguration::instance().get_update_editor_during_summary_drag () && (_zoom_trim_dragging || _move_dragging));
552 }
553
554 /** Fill in x and y with the editor's current viewable area in summary coordinates */
555 void
get_editor(pair<double,double> * x,pair<double,double> * y) const556 EditorSummary::get_editor (pair<double, double>* x, pair<double, double>* y) const
557 {
558 assert (x);
559 if (suspending_editor_updates ()) {
560
561 /* We are dragging, and configured not to update the editor window during drags,
562 * so just return where the editor will be when the drag finishes.
563 */
564
565 *x = _pending_editor_x;
566 if (y) {
567 *y = _pending_editor_y;
568 }
569 return;
570 }
571
572 /* Otherwise query the editor for its actual position */
573
574 x->first = (_editor->leftmost_sample () - _start) * _x_scale;
575 x->second = x->first + _editor->current_page_samples() * _x_scale;
576
577 if (y) {
578 y->first = editor_y_to_summary (_editor->vertical_adjustment.get_value ());
579 y->second = editor_y_to_summary (_editor->vertical_adjustment.get_value () + _editor->visible_canvas_height() - _editor->get_trackview_group()->canvas_origin().y);
580 }
581 }
582
583 /** Get an expression of the position of a point with respect to the view rectangle */
584 EditorSummary::Position
get_position(double x,double y) const585 EditorSummary::get_position (double x, double y) const
586 {
587 /* how close the mouse has to be to the edge of the view rectangle to be considered `on it',
588 in pixels */
589
590 int x_edge_size = (_view_rectangle_x.second - _view_rectangle_x.first) / 4;
591 x_edge_size = min (x_edge_size, 8);
592 x_edge_size = max (x_edge_size, 1);
593
594 bool const near_left = (std::abs (x - _view_rectangle_x.first) < x_edge_size);
595 bool const near_right = (std::abs (x - _view_rectangle_x.second) < x_edge_size);
596 bool const within_x = _view_rectangle_x.first < x && x < _view_rectangle_x.second;
597
598 if (near_left) {
599 return LEFT;
600 } else if (near_right) {
601 return RIGHT;
602 } else if (within_x) {
603 return INSIDE;
604 } else {
605 return TO_LEFT_OR_RIGHT;
606 }
607 }
608
609 void
reset_to_extents()610 EditorSummary::reset_to_extents()
611 {
612 /* reset as if the user never went anywhere outside the extents */
613 _leftmost = max_samplepos;
614 _rightmost = 0;
615
616 _editor->temporal_zoom_extents ();
617 set_background_dirty ();
618 }
619
620
621 void
set_cursor(Position p)622 EditorSummary::set_cursor (Position p)
623 {
624 switch (p) {
625 case LEFT:
626 get_window()->set_cursor (*_editor->_cursors->resize_left);
627 break;
628 case RIGHT:
629 get_window()->set_cursor (*_editor->_cursors->resize_right);
630 break;
631 case INSIDE:
632 get_window()->set_cursor (*_editor->_cursors->move);
633 break;
634 case TO_LEFT_OR_RIGHT:
635 get_window()->set_cursor (*_editor->_cursors->move);
636 break;
637 default:
638 assert (0);
639 get_window()->set_cursor ();
640 break;
641 }
642 }
643
644 void
summary_zoom_step(int steps)645 EditorSummary::summary_zoom_step (int steps /* positive steps to zoom "out" , negative steps to zoom "in" */ )
646 {
647 pair<double, double> xn;
648
649 get_editor (&xn);
650
651 xn.first -= steps;
652 xn.second += steps;
653
654 /* for now, disallow really close zooming-in from the scroomer. (Currently it
655 * causes the start-offset to 'walk' because of integer limitations.
656 * To fix this, probably need to maintain float throught the get/set_editor() path.)
657 */
658 if (steps<0) {
659 if ((xn.second - xn.first) < 2)
660 return;
661 }
662
663 set_overlays_dirty ();
664 set_editor_x (xn);
665 }
666
667
668 bool
on_motion_notify_event(GdkEventMotion * ev)669 EditorSummary::on_motion_notify_event (GdkEventMotion* ev)
670 {
671 if (_move_dragging) {
672
673 /* To avoid accidental zooming, the mouse must move exactly vertical, not diagonal, to trigger a zoom step
674 * we use screen coordinates for this, not canvas-based grab_x */
675 double mx = ev->x;
676 double dx = mx - _last_mx;
677 double my = ev->y;
678 double dy = my - _last_my;
679
680 /* do zooming in windowed "steps" so it feels more reversible ? */
681 const int stepsize = 2;
682 int y_delta = _start_mouse_y - my;
683 y_delta = y_delta / stepsize;
684
685 /* do the zoom? */
686 const float zscale = 3;
687 if ((dx == 0) && (_last_dx == 0) && (y_delta != _last_y_delta)) {
688
689 summary_zoom_step (dy * zscale);
690
691 /* after the zoom we must re-calculate x-pos grabs */
692 pair<double, double> xr;
693 get_editor (&xr);
694 _start_editor_x = xr;
695 _start_mouse_x = ev->x;
696
697 _last_y_delta = y_delta;
698 }
699
700 /* always track horizontal movement, if any */
701 if (dx != 0) {
702
703 double x = _start_editor_x.first;
704 x += ev->x - _start_mouse_x;
705
706 if (x < 0) {
707 x = 0;
708 }
709
710 /* zoom-behavior-tweaks: protect the right edge from expanding beyond the end */
711 pair<double, double> xr;
712 get_editor (&xr);
713 double w = xr.second - xr.first;
714 if (x + w < get_width()) {
715 set_editor (x);
716 }
717 }
718
719 _last_my = my;
720 _last_mx = mx;
721 _last_dx = dx;
722 _last_dy = dy;
723
724 } else if (_zoom_trim_dragging) {
725
726 pair<double, double> xr = _start_editor_x;
727
728 double const dx = ev->x - _start_mouse_x;
729
730 if (_zoom_trim_position == LEFT) {
731 xr.first += dx;
732 } else if (_zoom_trim_position == RIGHT) {
733
734 /* zoom-behavior-tweaks: protect the right edge from expanding beyond the edge */
735 if ((xr.second + dx) < get_width()) {
736 xr.second += dx;
737 }
738
739 } else {
740 assert (0);
741 xr.first = -1; /* do not change */
742 }
743
744 set_overlays_dirty ();
745 set_cursor (_zoom_trim_position);
746 set_editor (xr);
747
748 } else {
749 set_cursor (get_position (ev->x, ev->y));
750 }
751
752 return true;
753 }
754
755 bool
on_button_release_event(GdkEventButton *)756 EditorSummary::on_button_release_event (GdkEventButton*)
757 {
758 bool const was_suspended = suspending_editor_updates ();
759
760 _move_dragging = false;
761 _zoom_trim_dragging = false;
762 _editor->_dragging_playhead = false;
763 _editor->set_follow_playhead (_old_follow_playhead, false);
764
765 if (was_suspended && _pending_editor_changed) {
766 set_editor (_pending_editor_x);
767 }
768
769 return true;
770 }
771
772 bool
on_scroll_event(GdkEventScroll * ev)773 EditorSummary::on_scroll_event (GdkEventScroll* ev)
774 {
775 /* mouse wheel */
776 pair<double, double> xr;
777 get_editor (&xr);
778 double x = xr.first;
779
780 switch (ev->direction) {
781 case GDK_SCROLL_UP: {
782
783 summary_zoom_step (-4);
784
785 return true;
786 } break;
787
788 case GDK_SCROLL_DOWN: {
789
790 summary_zoom_step (4);
791
792 return true;
793 } break;
794
795 case GDK_SCROLL_LEFT:
796 if (Keyboard::modifier_state_equals (ev->state, Keyboard::ScrollZoomHorizontalModifier)) {
797 _editor->temporal_zoom_step (false);
798 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
799 x -= 64;
800 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
801 x -= 1;
802 } else {
803 _editor->scroll_left_half_page ();
804 return true;
805 }
806 break;
807 case GDK_SCROLL_RIGHT:
808 if (Keyboard::modifier_state_equals (ev->state, Keyboard::ScrollZoomHorizontalModifier)) {
809 _editor->temporal_zoom_step (true);
810 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
811 x += 64;
812 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
813 x += 1;
814 } else {
815 _editor->scroll_right_half_page ();
816 return true;
817 }
818 break;
819 default:
820 break;
821 }
822
823 set_editor (x);
824 return true;
825 }
826
827 /** Set the editor to display a x range with the left at a given position
828 * and a y range with the top at a given position.
829 * x and y parameters are specified in summary coordinates.
830 * Zoom is not changed in either direction.
831 */
832 void
set_editor(double const x)833 EditorSummary::set_editor (double const x)
834 {
835 if (_editor->pending_visual_change.idle_handler_id >= 0 && _editor->pending_visual_change.being_handled == true) {
836
837 /* As a side-effect, the Editor's visual change idle handler processes
838 pending GTK events. Hence this motion notify handler can be called
839 in the middle of a visual change idle handler, and if this happens,
840 the queue_visual_change calls below modify the variables that the
841 idle handler is working with. This causes problems. Hence this
842 check. It ensures that we won't modify the pending visual change
843 while a visual change idle handler is in progress. It's not perfect,
844 as it also means that we won't change these variables if an idle handler
845 is merely pending but not executing. But c'est la vie.
846 */
847
848 return;
849 }
850
851 set_editor_x (x);
852 }
853
854 /** Set the editor to display a given x range and a y range with the top at a given position.
855 * The editor's x zoom is adjusted if necessary, but the y zoom is not changed.
856 * x and y parameters are specified in summary coordinates.
857 */
858 void
set_editor(pair<double,double> const x)859 EditorSummary::set_editor (pair<double,double> const x)
860 {
861 if (_editor->pending_visual_change.idle_handler_id >= 0) {
862 /* see comment in other set_editor () */
863 return;
864 }
865
866 if (x.first >= 0) {
867 set_editor_x (x);
868 }
869 }
870
871 /** Set the left of the x range visible in the editor.
872 * Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
873 * @param x new x left position in summary coordinates.
874 */
875 void
set_editor_x(double x)876 EditorSummary::set_editor_x (double x)
877 {
878 if (x < 0) {
879 x = 0;
880 }
881
882 if (suspending_editor_updates ()) {
883 double const w = _pending_editor_x.second - _pending_editor_x.first;
884 _pending_editor_x.first = x;
885 _pending_editor_x.second = x + w;
886 _pending_editor_changed = true;
887 set_dirty ();
888 } else {
889 _editor->reset_x_origin (x / _x_scale + _start);
890 }
891 }
892
893 /** Set the x range visible in the editor.
894 * Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
895 * @param x new x range in summary coordinates.
896 */
897 void
set_editor_x(pair<double,double> x)898 EditorSummary::set_editor_x (pair<double, double> x)
899 {
900 if (x.first < 0) {
901 x.first = 0;
902 }
903
904 if (x.second < 0) {
905 x.second = x.first + 1;
906 }
907
908 if (suspending_editor_updates ()) {
909 _pending_editor_x = x;
910 _pending_editor_changed = true;
911 set_dirty ();
912 } else {
913 _editor->reset_x_origin (x.first / _x_scale + _start);
914
915 double const nx = (
916 ((x.second - x.first) / _x_scale) /
917 _editor->sample_to_pixel (_editor->current_page_samples())
918 );
919
920 if (nx != _editor->get_current_zoom ()) {
921 _editor->reset_zoom (nx);
922 }
923 }
924 }
925
926 void
playhead_position_changed(samplepos_t p)927 EditorSummary::playhead_position_changed (samplepos_t p)
928 {
929 int const o = int (_last_playhead);
930 int const n = int (playhead_sample_to_position (p));
931 if (_session && o != n) {
932 int a = max(2, min (o, n));
933 int b = max (o, n);
934 set_overlays_dirty_rect (a - 2, 0, b - a + 4, get_height ());
935 }
936 }
937
938 double
editor_y_to_summary(double y) const939 EditorSummary::editor_y_to_summary (double y) const
940 {
941 double sy = 0;
942 for (TrackViewList::const_iterator i = _editor->track_views.begin (); i != _editor->track_views.end(); ++i) {
943
944 if ((*i)->hidden()) {
945 continue;
946 }
947
948 double const h = (*i)->effective_height ();
949 if (y < h) {
950 /* in this track */
951 return sy + y * _track_height / h;
952 }
953
954 sy += _track_height;
955 y -= h;
956 }
957
958 return sy;
959 }
960
961 void
routes_added(list<RouteTimeAxisView * > const & r)962 EditorSummary::routes_added (list<RouteTimeAxisView*> const & r)
963 {
964 for (list<RouteTimeAxisView*>::const_iterator i = r.begin(); i != r.end(); ++i) {
965 /* Connect to the relevant signal for the route so that we know when its colour has changed */
966 (*i)->route()->presentation_info().PropertyChanged.connect (*this, invalidator (*this), boost::bind (&EditorSummary::route_gui_changed, this, _1), gui_context ());
967 boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> ((*i)->route ());
968 if (tr) {
969 tr->PlaylistChanged.connect (*this, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context ());
970 }
971 }
972
973 set_background_dirty ();
974 }
975
976 void
route_gui_changed(PBD::PropertyChange const & what_changed)977 EditorSummary::route_gui_changed (PBD::PropertyChange const& what_changed)
978 {
979 if (what_changed.contains (Properties::color)) {
980 set_background_dirty ();
981 }
982 }
983
984 double
playhead_sample_to_position(samplepos_t t) const985 EditorSummary::playhead_sample_to_position (samplepos_t t) const
986 {
987 return (t - _start) * _x_scale;
988 }
989
990 samplepos_t
position_to_playhead_sample_to_position(double pos) const991 EditorSummary::position_to_playhead_sample_to_position (double pos) const
992 {
993 return _start + (pos * _x_scale);
994 }
995