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