5c2135ac43ac45d8dbedae624f3f29ba8956720c
[ardour.git] / gtk2_ardour / editor_summary.cc
1 /*
2     Copyright (C) 2009 Paul Davis
3
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.
8
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.
13
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.
17
18 */
19
20 #include "ardour/session.h"
21 #include "time_axis_view.h"
22 #include "streamview.h"
23 #include "editor_summary.h"
24 #include "gui_thread.h"
25 #include "editor.h"
26 #include "region_view.h"
27 #include "rgb_macros.h"
28 #include "keyboard.h"
29
30 using namespace std;
31 using namespace ARDOUR;
32 using Gtkmm2ext::Keyboard;
33
34 /** Construct an EditorSummary.
35  *  @param e Editor to represent.
36  */
37 EditorSummary::EditorSummary (Editor* e)
38         : EditorComponent (e),
39           _start (0),
40           _end (1),
41           _overhang_fraction (0.1),
42           _x_scale (1),
43           _y_scale (1),
44           _last_playhead (-1),
45           _move_dragging (false),
46           _moved (false),
47           _zoom_dragging (false)
48
49 {
50         Region::RegionPropertyChanged.connect (region_property_connection, invalidator (*this), boost::bind (&CairoWidget::set_dirty, this), gui_context());
51         _editor->playhead_cursor->PositionChanged.connect (position_connection, invalidator (*this), ui_bind (&EditorSummary::playhead_position_changed, this, _1), gui_context());
52 }
53
54 /** Connect to a session.
55  *  @param s Session.
56  */
57 void
58 EditorSummary::set_session (Session* s)
59 {
60         EditorComponent::set_session (s);
61
62         set_dirty ();
63
64         /* Note: the EditorSummary already finds out about new regions from Editor::region_view_added
65          * (which attaches to StreamView::RegionViewAdded), and cut regions by the RegionPropertyChanged
66          * emitted when a cut region is added to the `cutlist' playlist.
67          */
68
69         if (_session) {
70                 _session->StartTimeChanged.connect (_session_connections, invalidator (*this), boost::bind (&EditorSummary::set_dirty, this), gui_context());
71                 _session->EndTimeChanged.connect (_session_connections, invalidator (*this), boost::bind (&EditorSummary::set_dirty, this), gui_context());
72         }
73 }
74
75 /** Handle an expose event.
76  *  @param event Event from GTK.
77  */
78 bool
79 EditorSummary::on_expose_event (GdkEventExpose* event)
80 {
81         CairoWidget::on_expose_event (event);
82
83         if (_session == 0) {
84                 return false;
85         }
86
87         cairo_t* cr = gdk_cairo_create (get_window()->gobj());
88
89         /* Render the view rectangle */
90
91         pair<double, double> x;
92         pair<double, double> y;
93         get_editor (&x, &y);
94
95         cairo_move_to (cr, x.first, y.first);
96         cairo_line_to (cr, x.second, y.first);
97         cairo_line_to (cr, x.second, y.second);
98         cairo_line_to (cr, x.first, y.second);
99         cairo_line_to (cr, x.first, y.first);
100         cairo_set_source_rgba (cr, 1, 1, 1, 0.25);
101         cairo_fill_preserve (cr);
102         cairo_set_line_width (cr, 1);
103         cairo_set_source_rgba (cr, 1, 1, 1, 0.5);
104         cairo_stroke (cr);
105
106         /* Playhead */
107
108         cairo_set_line_width (cr, 1);
109         /* XXX: colour should be set from configuration file */
110         cairo_set_source_rgba (cr, 1, 0, 0, 1);
111
112         double const p = (_editor->playhead_cursor->current_frame - _start) * _x_scale;
113         cairo_move_to (cr, p, 0);
114         cairo_line_to (cr, p, _height);
115         cairo_stroke (cr);
116         _last_playhead = p;
117
118         cairo_destroy (cr);
119
120         return true;
121 }
122
123 /** Render the required regions to a cairo context.
124  *  @param cr Context.
125  */
126 void
127 EditorSummary::render (cairo_t* cr)
128 {
129         /* background */
130
131         cairo_set_source_rgb (cr, 0, 0, 0);
132         cairo_rectangle (cr, 0, 0, _width, _height);
133         cairo_fill (cr);
134
135         if (_session == 0) {
136                 return;
137         }
138
139         /* compute start and end points for the summary */
140         
141         nframes_t const session_length = _session->current_end_frame() - _session->current_start_frame ();
142         double const theoretical_start = _session->current_start_frame() - session_length * _overhang_fraction;
143         _start = theoretical_start > 0 ? theoretical_start : 0;
144         _end = _session->current_end_frame() + session_length * _overhang_fraction;
145
146         /* compute total height of all tracks */
147
148         int h = 0;
149         int max_height = 0;
150         for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
151                 int const t = (*i)->effective_height ();
152                 h += t;
153                 max_height = max (max_height, t);
154         }
155
156         if (_end != _start) {
157                 _x_scale = static_cast<double> (_width) / (_end - _start);
158         } else {
159                 _x_scale = 1;
160         }
161         _y_scale = static_cast<double> (_height) / h;
162
163         /* tallest a region should ever be in the summary, in pixels */
164         int const tallest_region_pixels = _height / 16;
165
166         if (max_height * _y_scale > tallest_region_pixels) {
167                 _y_scale = static_cast<double> (tallest_region_pixels) / max_height;
168         }
169
170         /* render regions */
171
172         double y = 0;
173         for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
174                 StreamView* s = (*i)->view ();
175
176                 if (s) {
177                         double const h = (*i)->effective_height () * _y_scale;
178                         cairo_set_line_width (cr, h);
179
180                         s->foreach_regionview (sigc::bind (
181                                                        sigc::mem_fun (*this, &EditorSummary::render_region),
182                                                        cr,
183                                                        y + h / 2
184                                                        ));
185                         y += h;
186                 }
187         }
188
189         /* start and end markers */
190
191         cairo_set_line_width (cr, 1);
192         cairo_set_source_rgb (cr, 1, 1, 0);
193
194         double const p = (_session->current_start_frame() - _start) * _x_scale;
195         cairo_move_to (cr, p, 0);
196         cairo_line_to (cr, p, _height);
197         cairo_stroke (cr);
198
199         double const q = (_session->current_end_frame() - _start) * _x_scale;
200         cairo_move_to (cr, q, 0);
201         cairo_line_to (cr, q, _height);
202         cairo_stroke (cr);
203 }
204
205 /** Render a region for the summary.
206  *  @param r Region view.
207  *  @param cr Cairo context.
208  *  @param y y coordinate to render at.
209  */
210 void
211 EditorSummary::render_region (RegionView* r, cairo_t* cr, double y) const
212 {
213         uint32_t const c = r->get_fill_color ();
214         cairo_set_source_rgb (cr, UINT_RGBA_R (c) / 255.0, UINT_RGBA_G (c) / 255.0, UINT_RGBA_B (c) / 255.0);
215
216         if (r->region()->position() > _start) {
217                 cairo_move_to (cr, (r->region()->position() - _start) * _x_scale, y);
218         } else {
219                 cairo_move_to (cr, 0, y);
220         }
221
222         if ((r->region()->position() + r->region()->length()) > _start) {
223                 cairo_line_to (cr, ((r->region()->position() - _start + r->region()->length())) * _x_scale, y);
224         } else {
225                 cairo_line_to (cr, 0, y);
226         }
227
228         cairo_stroke (cr);
229 }
230
231 /** Set the summary so that just the overlays (viewbox, playhead etc.) will be re-rendered */
232 void
233 EditorSummary::set_overlays_dirty ()
234 {
235         ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty)
236         queue_draw ();
237 }
238
239 /** Handle a size request.
240  *  @param req GTK requisition
241  */
242 void
243 EditorSummary::on_size_request (Gtk::Requisition *req)
244 {
245         /* Use a dummy, small width and the actual height that we want */
246         req->width = 64;
247         req->height = 32;
248 }
249
250
251 void
252 EditorSummary::centre_on_click (GdkEventButton* ev)
253 {
254         pair<double, double> xr;
255         pair<double, double> yr;
256         get_editor (&xr, &yr);
257
258         double const w = xr.second - xr.first;
259         double const h = yr.second - yr.first;
260
261         xr.first = ev->x - w / 2;
262         xr.second = ev->x + w / 2;
263         yr.first = ev->y - h / 2;
264         yr.second = ev->y + h / 2;
265
266         if (xr.first < 0) {
267                 xr.first = 0;
268                 xr.second = w;
269         } else if (xr.second > _width) {
270                 xr.second = _width;
271                 xr.first = _width - w;
272         }
273
274         if (yr.first < 0) {
275                 yr.first = 0;
276                 yr.second = h;
277         } else if (yr.second > _height) {
278                 yr.second = _height;
279                 yr.first = _height - h;
280         }
281
282         set_editor (xr, yr);
283 }
284
285 /** Handle a button press.
286  *  @param ev GTK event.
287  */
288 bool
289 EditorSummary::on_button_press_event (GdkEventButton* ev)
290 {
291         if (ev->button == 1) {
292
293                 pair<double, double> xr;
294                 pair<double, double> yr;
295                 get_editor (&xr, &yr);
296
297                 _start_editor_x = xr;
298                 _start_editor_y = yr;
299                 _start_mouse_x = ev->x;
300                 _start_mouse_y = ev->y;
301
302                 if (
303                         _start_editor_x.first <= _start_mouse_x && _start_mouse_x <= _start_editor_x.second &&
304                         _start_editor_y.first <= _start_mouse_y && _start_mouse_y <= _start_editor_y.second
305                         ) {
306
307                         _start_position = IN_VIEWBOX;
308
309                 } else if (_start_editor_x.first <= _start_mouse_x && _start_mouse_x <= _start_editor_x.second) {
310
311                         _start_position = BELOW_OR_ABOVE_VIEWBOX;
312
313                 } else {
314
315                         _start_position = TO_LEFT_OR_RIGHT_OF_VIEWBOX;
316                 }
317
318                 if (Keyboard::modifier_state_equals (ev->state, Keyboard::PrimaryModifier)) {
319
320                         /* primary-modifier-click: start a zoom drag */
321
322                         double const hx = (xr.first + xr.second) * 0.5;
323                         _zoom_left = ev->x < hx;
324                         _zoom_dragging = true;
325                         _editor->_dragging_playhead = true;
326
327
328                         /* In theory, we could support vertical dragging, which logically
329                            might scale track heights in order to make the editor reflect
330                            the dragged viewbox.  However, having tried this:
331                            a) it's hard to do
332                            b) it's quite slow
333                            c) it doesn't seem particularly useful, especially with the
334                            limited height of the summary
335
336                            So at the moment we don't support that...
337                         */
338
339
340                 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
341
342                         /* secondary-modifier-click: locate playhead */
343                         if (_session) {
344                                 _session->request_locate (ev->x / _x_scale + _start);
345                         }
346
347                 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
348
349                         centre_on_click (ev);
350
351                 } else {
352
353                         /* ordinary click: start a move drag */
354
355                         _move_dragging = true;
356                         _moved = false;
357                         _editor->_dragging_playhead = true;
358                 }
359         }
360
361         return true;
362 }
363
364 void
365 EditorSummary::get_editor (pair<double, double>* x, pair<double, double>* y) const
366 {
367         x->first = (_editor->leftmost_position () - _start) * _x_scale;
368         x->second = x->first + _editor->current_page_frames() * _x_scale;
369
370         y->first = _editor->vertical_adjustment.get_value() * _y_scale;
371         y->second = y->first + (_editor->canvas_height () - _editor->get_canvas_timebars_vsize()) * _y_scale;
372 }
373
374 bool
375 EditorSummary::on_motion_notify_event (GdkEventMotion* ev)
376 {
377         pair<double, double> xr = _start_editor_x;
378         pair<double, double> yr = _start_editor_y;
379
380         if (_move_dragging) {
381
382                 _moved = true;
383
384                 /* don't alter x if we clicked outside and above or below the viewbox */
385                 if (_start_position == IN_VIEWBOX || _start_position == TO_LEFT_OR_RIGHT_OF_VIEWBOX) {
386                         xr.first += ev->x - _start_mouse_x;
387                         xr.second += ev->x - _start_mouse_x;
388                 }
389
390                 /* don't alter y if we clicked outside and to the left or right of the viewbox */
391                 if (_start_position == IN_VIEWBOX || _start_position == BELOW_OR_ABOVE_VIEWBOX) {
392                         yr.first += ev->y - _start_mouse_y;
393                         yr.second += ev->y - _start_mouse_y;
394                 }
395
396                 if (xr.first < 0) {
397                         xr.second -= xr.first;
398                         xr.first = 0;
399                 }
400
401                 if (yr.first < 0) {
402                         yr.second -= yr.first;
403                         yr.first = 0;
404                 }
405
406                 set_editor (xr, yr);
407
408         } else if (_zoom_dragging) {
409
410                 double const dx = ev->x - _start_mouse_x;
411
412                 if (_zoom_left) {
413                         xr.first += dx;
414                 } else {
415                         xr.second += dx;
416                 }
417
418                 set_editor (xr, yr);
419         }
420
421         return true;
422 }
423
424 bool
425 EditorSummary::on_button_release_event (GdkEventButton*)
426 {
427         _move_dragging = false;
428         _zoom_dragging = false;
429         _editor->_dragging_playhead = false;
430         return true;
431 }
432
433 bool
434 EditorSummary::on_scroll_event (GdkEventScroll* ev)
435 {
436         /* mouse wheel */
437
438         pair<double, double> xr;
439         pair<double, double> yr;
440         get_editor (&xr, &yr);
441
442         double amount = 8;
443
444         if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
445                 amount = 64;
446         } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
447                 amount = 1;
448         }
449
450         if (Keyboard::modifier_state_equals (ev->state, Keyboard::PrimaryModifier)) {
451
452                 /* primary-wheel == left-right scrolling */
453
454                 if (ev->direction == GDK_SCROLL_UP) {
455                         xr.first += amount;
456                         xr.second += amount;
457                 } else if (ev->direction == GDK_SCROLL_DOWN) {
458                         xr.first -= amount;
459                         xr.second -= amount;
460                 }
461
462         } else {
463
464                 if (ev->direction == GDK_SCROLL_DOWN) {
465                         yr.first += amount;
466                         yr.second += amount;
467                 } else if (ev->direction == GDK_SCROLL_UP) {
468                         yr.first -= amount;
469                         yr.second -= amount;
470                 } else if (ev->direction == GDK_SCROLL_LEFT) {
471                         xr.first -= amount;
472                         xr.second -= amount;
473                 } else if (ev->direction == GDK_SCROLL_RIGHT) {
474                         xr.first += amount;
475                         xr.second += amount;
476                 }
477         }
478
479         set_editor (xr, yr);
480         return true;
481 }
482
483 void
484 EditorSummary::set_editor (pair<double,double> const & x, pair<double, double> const & y)
485 {
486         if (_editor->pending_visual_change.idle_handler_id < 0) {
487
488                 /* As a side-effect, the Editor's visual change idle handler processes
489                    pending GTK events.  Hence this motion notify handler can be called
490                    in the middle of a visual change idle handler, and if this happens,
491                    the queue_visual_change calls below modify the variables that the
492                    idle handler is working with.  This causes problems.  Hence the
493                    check above.  It ensures that we won't modify the pending visual change
494                    while a visual change idle handler is in progress.  It's not perfect,
495                    as it also means that we won't change these variables if an idle handler
496                    is merely pending but not executing.  But c'est la vie.
497                 */
498
499                 /* proposed bottom of the editor with the requested position */
500                 double const pb = y.second / _y_scale;
501
502                 /* bottom of the canvas */
503                 double const ch = _editor->full_canvas_height - _editor->canvas_timebars_vsize;
504
505                 /* requested y position */
506                 double ly = y.first / _y_scale;
507
508                 /* clamp y position so as not to go off the bottom */
509                 if (pb > ch) {
510                         ly -= (pb - ch);
511                 }
512
513                 if (ly < 0) {
514                         ly = 0;
515                 }
516
517                 _editor->reset_x_origin (x.first / _x_scale + _start);
518                 _editor->reset_y_origin (ly);
519
520                 double const nx = (
521                         ((x.second - x.first) / _x_scale) /
522                         _editor->frame_to_unit (_editor->current_page_frames())
523                         );
524
525                 if (nx != _editor->get_current_zoom ()) {
526                         _editor->reset_zoom (nx);
527                 }
528         }
529 }
530
531 void
532 EditorSummary::playhead_position_changed (nframes64_t p)
533 {
534         if (_session && int (p * _x_scale) != int (_last_playhead)) {
535                 set_overlays_dirty ();
536         }
537 }
538
539