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