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