3fa379a9b950407e7607856edf2244cfcef018a6
[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           _track_height (16),
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 track height */
147         size_t const N = _editor->track_views.size ();
148         if (N == 0) {
149                 _track_height = 16;
150         } else {
151                 _track_height = (double) _height / N;
152         }
153
154         /* calculate x scale */
155         if (_end != _start) {
156                 _x_scale = static_cast<double> (_width) / (_end - _start);
157         } else {
158                 _x_scale = 1;
159         }
160
161         /* render tracks and regions */
162
163         double y = 0;
164         for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
165
166                 cairo_set_source_rgb (cr, 0.2, 0.2, 0.2);
167                 cairo_set_line_width (cr, _track_height - 2);
168                 cairo_move_to (cr, 0, y + _track_height / 2);
169                 cairo_line_to (cr, _width, y + _track_height / 2);
170                 cairo_stroke (cr);
171                 
172                 StreamView* s = (*i)->view ();
173
174                 if (s) {
175                         cairo_set_line_width (cr, _track_height * 0.6);
176
177                         s->foreach_regionview (sigc::bind (
178                                                        sigc::mem_fun (*this, &EditorSummary::render_region),
179                                                        cr,
180                                                        y + _track_height / 2
181                                                        ));
182                 }
183                 
184                 y += _track_height;
185         }
186
187         /* start and end markers */
188
189         cairo_set_line_width (cr, 1);
190         cairo_set_source_rgb (cr, 1, 1, 0);
191
192         double const p = (_session->current_start_frame() - _start) * _x_scale;
193         cairo_move_to (cr, p, 0);
194         cairo_line_to (cr, p, _height);
195         cairo_stroke (cr);
196
197         double const q = (_session->current_end_frame() - _start) * _x_scale;
198         cairo_move_to (cr, q, 0);
199         cairo_line_to (cr, q, _height);
200         cairo_stroke (cr);
201 }
202
203 /** Render a region for the summary.
204  *  @param r Region view.
205  *  @param cr Cairo context.
206  *  @param y y coordinate to render at.
207  */
208 void
209 EditorSummary::render_region (RegionView* r, cairo_t* cr, double y) const
210 {
211         uint32_t const c = r->get_fill_color ();
212         cairo_set_source_rgb (cr, UINT_RGBA_R (c) / 255.0, UINT_RGBA_G (c) / 255.0, UINT_RGBA_B (c) / 255.0);
213
214         if (r->region()->position() > _start) {
215                 cairo_move_to (cr, (r->region()->position() - _start) * _x_scale, y);
216         } else {
217                 cairo_move_to (cr, 0, y);
218         }
219
220         if ((r->region()->position() + r->region()->length()) > _start) {
221                 cairo_line_to (cr, ((r->region()->position() - _start + r->region()->length())) * _x_scale, y);
222         } else {
223                 cairo_line_to (cr, 0, y);
224         }
225
226         cairo_stroke (cr);
227 }
228
229 /** Set the summary so that just the overlays (viewbox, playhead etc.) will be re-rendered */
230 void
231 EditorSummary::set_overlays_dirty ()
232 {
233         ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty)
234         queue_draw ();
235 }
236
237 /** Handle a size request.
238  *  @param req GTK requisition
239  */
240 void
241 EditorSummary::on_size_request (Gtk::Requisition *req)
242 {
243         /* Use a dummy, small width and the actual height that we want */
244         req->width = 64;
245         req->height = 32;
246 }
247
248
249 void
250 EditorSummary::centre_on_click (GdkEventButton* ev)
251 {
252         pair<double, double> xr;
253         pair<double, double> yr;
254         get_editor (&xr, &yr);
255
256         double const w = xr.second - xr.first;
257
258         xr.first = ev->x - w / 2;
259         xr.second = ev->x + w / 2;
260
261         if (xr.first < 0) {
262                 xr.first = 0;
263                 xr.second = w;
264         } else if (xr.second > _width) {
265                 xr.second = _width;
266                 xr.first = _width - w;
267         }
268
269         double ey = summary_y_to_editor (ev->y);
270         ey -= (_editor->canvas_height() - _editor->get_canvas_timebars_vsize ()) / 2;
271         if (ey < 0) {
272                 ey = 0;
273         }
274         
275         set_editor (xr, editor_y_to_summary (ey));
276 }
277
278 /** Handle a button press.
279  *  @param ev GTK event.
280  */
281 bool
282 EditorSummary::on_button_press_event (GdkEventButton* ev)
283 {
284         if (ev->button == 1) {
285
286                 pair<double, double> xr;
287                 pair<double, double> yr;
288                 get_editor (&xr, &yr);
289
290                 _start_editor_x = xr;
291                 _start_editor_y = yr;
292                 _start_mouse_x = ev->x;
293                 _start_mouse_y = ev->y;
294
295                 if (
296                         _start_editor_x.first <= _start_mouse_x && _start_mouse_x <= _start_editor_x.second &&
297                         _start_editor_y.first <= _start_mouse_y && _start_mouse_y <= _start_editor_y.second
298                         ) {
299
300                         _start_position = IN_VIEWBOX;
301
302                 } else if (_start_editor_x.first <= _start_mouse_x && _start_mouse_x <= _start_editor_x.second) {
303
304                         _start_position = BELOW_OR_ABOVE_VIEWBOX;
305
306                 } else {
307
308                         _start_position = TO_LEFT_OR_RIGHT_OF_VIEWBOX;
309                 }
310
311                 if (Keyboard::modifier_state_equals (ev->state, Keyboard::PrimaryModifier)) {
312
313                         /* primary-modifier-click: start a zoom drag */
314
315                         double const hx = (xr.first + xr.second) * 0.5;
316                         _zoom_left = ev->x < hx;
317                         _zoom_dragging = true;
318                         _editor->_dragging_playhead = true;
319
320
321                         /* In theory, we could support vertical dragging, which logically
322                            might scale track heights in order to make the editor reflect
323                            the dragged viewbox.  However, having tried this:
324                            a) it's hard to do
325                            b) it's quite slow
326                            c) it doesn't seem particularly useful, especially with the
327                            limited height of the summary
328
329                            So at the moment we don't support that...
330                         */
331
332
333                 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
334
335                         /* secondary-modifier-click: locate playhead */
336                         if (_session) {
337                                 _session->request_locate (ev->x / _x_scale + _start);
338                         }
339
340                 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
341
342                         centre_on_click (ev);
343
344                 } else {
345
346                         /* ordinary click: start a move drag */
347
348                         _move_dragging = true;
349                         _moved = false;
350                         _editor->_dragging_playhead = true;
351                 }
352         }
353
354         return true;
355 }
356
357 void
358 EditorSummary::get_editor (pair<double, double>* x, pair<double, double>* y) const
359 {
360         x->first = (_editor->leftmost_position () - _start) * _x_scale;
361         x->second = x->first + _editor->current_page_frames() * _x_scale;
362
363         y->first = editor_y_to_summary (_editor->vertical_adjustment.get_value ());
364         y->second = editor_y_to_summary (_editor->vertical_adjustment.get_value () + _editor->canvas_height() - _editor->get_canvas_timebars_vsize());
365 }
366
367 bool
368 EditorSummary::on_motion_notify_event (GdkEventMotion* ev)
369 {
370         pair<double, double> xr = _start_editor_x;
371         pair<double, double> yr = _start_editor_y;
372         double y = _start_editor_y.first;
373
374         if (_move_dragging) {
375
376                 _moved = true;
377
378                 /* don't alter x if we clicked outside and above or below the viewbox */
379                 if (_start_position == IN_VIEWBOX || _start_position == TO_LEFT_OR_RIGHT_OF_VIEWBOX) {
380                         xr.first += ev->x - _start_mouse_x;
381                         xr.second += ev->x - _start_mouse_x;
382                 }
383
384                 /* don't alter y if we clicked outside and to the left or right of the viewbox */
385                 if (_start_position == IN_VIEWBOX || _start_position == BELOW_OR_ABOVE_VIEWBOX) {
386                         y += ev->y - _start_mouse_y;
387                 }
388
389                 if (xr.first < 0) {
390                         xr.second -= xr.first;
391                         xr.first = 0;
392                 }
393
394                 if (y < 0) {
395                         y = 0;
396                 }
397
398                 set_editor (xr, y);
399
400         } else if (_zoom_dragging) {
401
402                 double const dx = ev->x - _start_mouse_x;
403
404                 if (_zoom_left) {
405                         xr.first += dx;
406                 } else {
407                         xr.second += dx;
408                 }
409
410                 set_editor (xr, y);
411         }
412
413         return true;
414 }
415
416 bool
417 EditorSummary::on_button_release_event (GdkEventButton*)
418 {
419         _move_dragging = false;
420         _zoom_dragging = false;
421         _editor->_dragging_playhead = false;
422         return true;
423 }
424
425 bool
426 EditorSummary::on_scroll_event (GdkEventScroll* ev)
427 {
428         /* mouse wheel */
429
430         pair<double, double> xr;
431         pair<double, double> yr;
432         get_editor (&xr, &yr);
433         double y = yr.first;
434
435         double amount = 8;
436
437         if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
438                 amount = 64;
439         } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
440                 amount = 1;
441         }
442
443         if (Keyboard::modifier_state_equals (ev->state, Keyboard::PrimaryModifier)) {
444
445                 /* primary-wheel == left-right scrolling */
446
447                 if (ev->direction == GDK_SCROLL_UP) {
448                         xr.first += amount;
449                         xr.second += amount;
450                 } else if (ev->direction == GDK_SCROLL_DOWN) {
451                         xr.first -= amount;
452                         xr.second -= amount;
453                 }
454
455         } else {
456
457                 if (ev->direction == GDK_SCROLL_DOWN) {
458                         y += amount;
459                 } else if (ev->direction == GDK_SCROLL_UP) {
460                         y -= amount;
461                 } else if (ev->direction == GDK_SCROLL_LEFT) {
462                         xr.first -= amount;
463                         xr.second -= amount;
464                 } else if (ev->direction == GDK_SCROLL_RIGHT) {
465                         xr.first += amount;
466                         xr.second += amount;
467                 }
468         }
469
470         set_editor (xr, y);
471         return true;
472 }
473
474 /** Set the editor to display a given x range and a y range with the top at a given position.
475  *  The editor's x zoom is adjusted if necessary, but the y zoom is not changed.
476  *  x and y parameters are specified in summary coordinates.
477  */
478 void
479 EditorSummary::set_editor (pair<double,double> const & x, double const y)
480 {
481         if (_editor->pending_visual_change.idle_handler_id >= 0) {
482
483                 /* As a side-effect, the Editor's visual change idle handler processes
484                    pending GTK events.  Hence this motion notify handler can be called
485                    in the middle of a visual change idle handler, and if this happens,
486                    the queue_visual_change calls below modify the variables that the
487                    idle handler is working with.  This causes problems.  Hence this
488                    check.  It ensures that we won't modify the pending visual change
489                    while a visual change idle handler is in progress.  It's not perfect,
490                    as it also means that we won't change these variables if an idle handler
491                    is merely pending but not executing.  But c'est la vie.
492                 */
493
494                 return;
495         }
496         
497         double y1 = summary_y_to_editor (y);
498         double const eh = _editor->canvas_height() - _editor->get_canvas_timebars_vsize ();
499         double y2 = y1 + eh;
500         
501         double const full_editor_height = _editor->full_canvas_height - _editor->get_canvas_timebars_vsize();
502
503         if (y2 > full_editor_height) {
504                 y1 -= y2 - full_editor_height;
505         }
506         
507         if (y1 < 0) {
508                 y1 = 0;
509         }
510
511         _editor->reset_x_origin (x.first / _x_scale + _start);
512         _editor->reset_y_origin (y1);
513         
514         double const nx = (
515                 ((x.second - x.first) / _x_scale) /
516                 _editor->frame_to_unit (_editor->current_page_frames())
517                 );
518         
519         if (nx != _editor->get_current_zoom ()) {
520                 _editor->reset_zoom (nx);
521         }       
522 }
523
524 void
525 EditorSummary::playhead_position_changed (nframes64_t p)
526 {
527         if (_session && int (p * _x_scale) != int (_last_playhead)) {
528                 set_overlays_dirty ();
529         }
530 }
531
532 double
533 EditorSummary::summary_y_to_editor (double y) const
534 {
535         double ey = 0;
536         TrackViewList::const_iterator i = _editor->track_views.begin ();
537         while (i != _editor->track_views.end()) {
538                 double const h = (*i)->effective_height ();
539                 if (y < _track_height) {
540                         /* in this track */
541                         return ey + y * h / _track_height;
542                 }
543
544                 ey += h;
545                 y -= _track_height;
546                 ++i;
547         }
548
549         return ey;
550 }
551
552 double
553 EditorSummary::editor_y_to_summary (double y) const
554 {
555         double sy = 0;
556         TrackViewList::const_iterator i = _editor->track_views.begin ();
557         while (i != _editor->track_views.end()) {
558                 double const h = (*i)->effective_height ();
559                 if (y < h) {
560                         /* in this track */
561                         return sy + y * _track_height / h;
562                 }
563
564                 sy += _track_height;
565                 y -= h;
566                 ++i;
567         }
568
569         return sy;
570 }