Add zoom-all button.
[dcpomatic.git] / src / wx / timeline.cc
1 /*
2     Copyright (C) 2013-2018 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21 #include "film_editor.h"
22 #include "timeline.h"
23 #include "timeline_time_axis_view.h"
24 #include "timeline_reels_view.h"
25 #include "timeline_labels_view.h"
26 #include "timeline_video_content_view.h"
27 #include "timeline_audio_content_view.h"
28 #include "timeline_subtitle_content_view.h"
29 #include "timeline_atmos_content_view.h"
30 #include "content_panel.h"
31 #include "wx_util.h"
32 #include "lib/film.h"
33 #include "lib/playlist.h"
34 #include "lib/image_content.h"
35 #include "lib/timer.h"
36 #include "lib/audio_content.h"
37 #include "lib/subtitle_content.h"
38 #include "lib/video_content.h"
39 #include "lib/atmos_mxf_content.h"
40 #include <wx/graphics.h>
41 #include <boost/weak_ptr.hpp>
42 #include <boost/foreach.hpp>
43 #include <list>
44 #include <iostream>
45
46 using std::list;
47 using std::cout;
48 using std::min;
49 using std::max;
50 using boost::shared_ptr;
51 using boost::weak_ptr;
52 using boost::dynamic_pointer_cast;
53 using boost::bind;
54 using boost::optional;
55
56 /* 3 hours in 640 pixels */
57 double const Timeline::_minimum_pixels_per_second = 640.0 / (60 * 60 * 3);
58 int const Timeline::_minimum_pixels_per_track = 16;
59
60 Timeline::Timeline (wxWindow* parent, ContentPanel* cp, shared_ptr<Film> film)
61         : wxPanel (parent, wxID_ANY)
62         , _labels_canvas (new wxScrolledCanvas (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE))
63         , _main_canvas (new wxScrolledCanvas (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE))
64         , _content_panel (cp)
65         , _film (film)
66         , _time_axis_view (new TimelineTimeAxisView (*this, 64))
67         , _reels_view (new TimelineReelsView (*this, 32))
68         , _labels_view (new TimelineLabelsView (*this))
69         , _tracks (0)
70         , _left_down (false)
71         , _down_view_position (0)
72         , _first_move (false)
73         , _menu (this)
74         , _snap (true)
75         , _tool (SELECT)
76         , _x_scroll_rate (16)
77         , _y_scroll_rate (16)
78         , _pixels_per_track (48)
79 {
80 #ifndef __WXOSX__
81         _labels_canvas->SetDoubleBuffered (true);
82         _main_canvas->SetDoubleBuffered (true);
83 #endif
84
85         wxSizer* sizer = new wxBoxSizer (wxHORIZONTAL);
86         sizer->Add (_labels_canvas, 0, wxEXPAND);
87         _labels_canvas->SetMinSize (wxSize (_labels_view->bbox().width, -1));
88         sizer->Add (_main_canvas, 1, wxEXPAND);
89         SetSizer (sizer);
90
91         _labels_canvas->Bind (wxEVT_PAINT,      boost::bind (&Timeline::paint_labels, this));
92         _main_canvas->Bind   (wxEVT_PAINT,      boost::bind (&Timeline::paint_main,   this));
93         _main_canvas->Bind   (wxEVT_LEFT_DOWN,  boost::bind (&Timeline::left_down,    this, _1));
94         _main_canvas->Bind   (wxEVT_LEFT_UP,    boost::bind (&Timeline::left_up,      this, _1));
95         _main_canvas->Bind   (wxEVT_RIGHT_DOWN, boost::bind (&Timeline::right_down,   this, _1));
96         _main_canvas->Bind   (wxEVT_MOTION,     boost::bind (&Timeline::mouse_moved,  this, _1));
97         _main_canvas->Bind   (wxEVT_SIZE,       boost::bind (&Timeline::resized,      this));
98         _main_canvas->Bind   (wxEVT_SCROLLWIN_THUMBTRACK,  boost::bind (&Timeline::scrolled,     this, _1));
99
100         film_changed (Film::CONTENT);
101
102         SetMinSize (wxSize (640, 4 * pixels_per_track() + 96));
103
104         _film_changed_connection = film->Changed.connect (bind (&Timeline::film_changed, this, _1));
105         _film_content_changed_connection = film->ContentChanged.connect (bind (&Timeline::film_content_changed, this, _2, _3));
106
107         set_pixels_per_second (static_cast<double>(640) / film->length().seconds ());
108
109         setup_scrollbars ();
110         _labels_canvas->ShowScrollbars (wxSHOW_SB_NEVER, wxSHOW_SB_NEVER);
111 }
112
113 void
114 Timeline::set_pixels_per_second (double pps)
115 {
116         _pixels_per_second = max (_minimum_pixels_per_second, pps);
117 }
118
119 void
120 Timeline::paint_labels ()
121 {
122         wxPaintDC dc (_labels_canvas);
123
124         wxGraphicsContext* gc = wxGraphicsContext::Create (dc);
125         if (!gc) {
126                 return;
127         }
128
129         int vsx, vsy;
130         _labels_canvas->GetViewStart (&vsx, &vsy);
131         gc->Translate (-vsx * _x_scroll_rate, -vsy * _y_scroll_rate + tracks_y_offset());
132
133         _labels_view->paint (gc, list<dcpomatic::Rect<int> >());
134
135         delete gc;
136 }
137
138 void
139 Timeline::paint_main ()
140 {
141         wxPaintDC dc (_main_canvas);
142         _main_canvas->DoPrepareDC (dc);
143
144         wxGraphicsContext* gc = wxGraphicsContext::Create (dc);
145         if (!gc) {
146                 return;
147         }
148
149         int vsx, vsy;
150         _main_canvas->GetViewStart (&vsx, &vsy);
151         gc->Translate (-vsx * _x_scroll_rate, -vsy * _y_scroll_rate);
152
153         gc->SetAntialiasMode (wxANTIALIAS_DEFAULT);
154
155         BOOST_FOREACH (shared_ptr<TimelineView> i, _views) {
156
157                 shared_ptr<TimelineContentView> ic = dynamic_pointer_cast<TimelineContentView> (i);
158
159                 /* Find areas of overlap with other content views, so that we can plot them */
160                 list<dcpomatic::Rect<int> > overlaps;
161                 BOOST_FOREACH (shared_ptr<TimelineView> j, _views) {
162                         shared_ptr<TimelineContentView> jc = dynamic_pointer_cast<TimelineContentView> (j);
163                         /* No overlap with non-content views, views no different tracks, audio views or non-active views */
164                         if (!ic || !jc || i == j || ic->track() != jc->track() || ic->track().get_value_or(2) >= 2 || !ic->active() || !jc->active()) {
165                                 continue;
166                         }
167
168                         optional<dcpomatic::Rect<int> > r = j->bbox().intersection (i->bbox());
169                         if (r) {
170                                 overlaps.push_back (r.get ());
171                         }
172                 }
173
174                 i->paint (gc, overlaps);
175         }
176
177         if (_zoom_point) {
178                 /* Translate back as _down_point and _zoom_point do not take scroll into account */
179                 gc->Translate (vsx * _x_scroll_rate, vsy * _y_scroll_rate);
180                 gc->SetPen (*wxBLACK_PEN);
181                 gc->SetBrush (*wxTRANSPARENT_BRUSH);
182                 gc->DrawRectangle (
183                         min (_down_point.x, _zoom_point->x),
184                         min (_down_point.y, _zoom_point->y),
185                         fabs (_down_point.x - _zoom_point->x),
186                         fabs (_down_point.y - _zoom_point->y)
187                         );
188         }
189
190         delete gc;
191 }
192
193 void
194 Timeline::film_changed (Film::Property p)
195 {
196         if (p == Film::CONTENT || p == Film::REEL_TYPE || p == Film::REEL_LENGTH) {
197                 ensure_ui_thread ();
198                 recreate_views ();
199         } else if (p == Film::CONTENT_ORDER) {
200                 Refresh ();
201         }
202 }
203
204 void
205 Timeline::recreate_views ()
206 {
207         shared_ptr<const Film> film = _film.lock ();
208         if (!film) {
209                 return;
210         }
211
212         _views.clear ();
213         _views.push_back (_time_axis_view);
214         _views.push_back (_reels_view);
215
216         BOOST_FOREACH (shared_ptr<Content> i, film->content ()) {
217                 if (i->video) {
218                         _views.push_back (shared_ptr<TimelineView> (new TimelineVideoContentView (*this, i)));
219                 }
220
221                 if (i->audio && !i->audio->mapping().mapped_output_channels().empty ()) {
222                         _views.push_back (shared_ptr<TimelineView> (new TimelineAudioContentView (*this, i)));
223                 }
224
225                 if (i->subtitle) {
226                         _views.push_back (shared_ptr<TimelineView> (new TimelineSubtitleContentView (*this, i)));
227                 }
228
229                 if (dynamic_pointer_cast<AtmosMXFContent> (i)) {
230                         _views.push_back (shared_ptr<TimelineView> (new TimelineAtmosContentView (*this, i)));
231                 }
232         }
233
234         assign_tracks ();
235         setup_scrollbars ();
236         Refresh ();
237 }
238
239 void
240 Timeline::film_content_changed (int property, bool frequent)
241 {
242         ensure_ui_thread ();
243
244         if (property == AudioContentProperty::STREAMS) {
245                 recreate_views ();
246         } else if (!frequent) {
247                 setup_scrollbars ();
248                 Refresh ();
249         }
250 }
251
252 template <class T>
253 int
254 place (TimelineViewList& views, int& tracks)
255 {
256         int const base = tracks;
257
258         BOOST_FOREACH (shared_ptr<TimelineView> i, views) {
259                 if (!dynamic_pointer_cast<T>(i)) {
260                         continue;
261                 }
262
263                 shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (i);
264
265                 int t = base;
266
267                 shared_ptr<Content> content = cv->content();
268                 DCPTimePeriod const content_period (content->position(), content->end());
269
270                 while (true) {
271                         TimelineViewList::iterator j = views.begin();
272                         while (j != views.end()) {
273                                 shared_ptr<T> test = dynamic_pointer_cast<T> (*j);
274                                 if (!test) {
275                                         ++j;
276                                         continue;
277                                 }
278
279                                 shared_ptr<Content> test_content = test->content();
280                                 if (
281                                         test->track() && test->track().get() == t &&
282                                         content_period.overlap(DCPTimePeriod(test_content->position(), test_content->end()))) {
283                                         /* we have an overlap on track `t' */
284                                         ++t;
285                                         break;
286                                 }
287
288                                 ++j;
289                         }
290
291                         if (j == views.end ()) {
292                                 /* no overlap on `t' */
293                                 break;
294                         }
295                 }
296
297                 cv->set_track (t);
298                 tracks = max (tracks, t + 1);
299         }
300
301         return tracks - base;
302 }
303
304 void
305 Timeline::assign_tracks ()
306 {
307         /* Tracks are:
308            Video (mono or left-eye)
309            Video (right-eye)
310            Subtitle 1
311            Subtitle 2
312            Subtitle N
313            Atmos
314            Audio 1
315            Audio 2
316            Audio N
317         */
318
319         _tracks = 0;
320
321         for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
322                 shared_ptr<TimelineContentView> c = dynamic_pointer_cast<TimelineContentView> (*i);
323                 if (c) {
324                         c->unset_track ();
325                 }
326         }
327
328         /* Video */
329
330         bool have_3d = false;
331         BOOST_FOREACH (shared_ptr<TimelineView> i, _views) {
332                 shared_ptr<TimelineVideoContentView> cv = dynamic_pointer_cast<TimelineVideoContentView> (i);
333                 if (!cv) {
334                         continue;
335                 }
336
337                 /* Video on tracks 0 and maybe 1 (left and right eye) */
338                 if (cv->content()->video->frame_type() == VIDEO_FRAME_TYPE_3D_RIGHT) {
339                         cv->set_track (1);
340                         _tracks = max (_tracks, 2);
341                         have_3d = true;
342                 } else {
343                         cv->set_track (0);
344                 }
345         }
346
347         _tracks = max (_tracks, 1);
348
349         /* Subtitle */
350
351         int const subtitle_tracks = place<TimelineSubtitleContentView> (_views, _tracks);
352
353         /* Atmos */
354
355         bool have_atmos = false;
356         BOOST_FOREACH (shared_ptr<TimelineView> i, _views) {
357                 shared_ptr<TimelineVideoContentView> cv = dynamic_pointer_cast<TimelineVideoContentView> (i);
358                 if (!cv) {
359                         continue;
360                 }
361                 if (dynamic_pointer_cast<TimelineAtmosContentView> (i)) {
362                         cv->set_track (_tracks - 1);
363                         have_atmos = true;
364                 }
365         }
366
367         if (have_atmos) {
368                 ++_tracks;
369         }
370
371         /* Audio */
372
373         place<TimelineAudioContentView> (_views, _tracks);
374
375         _labels_view->set_3d (have_3d);
376         _labels_view->set_subtitle_tracks (subtitle_tracks);
377         _labels_view->set_atmos (have_atmos);
378
379         _time_axis_view->set_y (tracks());
380         _reels_view->set_y (8);
381 }
382
383 int
384 Timeline::tracks () const
385 {
386         return _tracks;
387 }
388
389 void
390 Timeline::setup_scrollbars ()
391 {
392         shared_ptr<const Film> film = _film.lock ();
393         if (!film || !_pixels_per_second) {
394                 return;
395         }
396         _labels_canvas->SetVirtualSize (_labels_view->bbox().width, tracks() * pixels_per_track() + 96);
397         _labels_canvas->SetScrollRate (_x_scroll_rate, _y_scroll_rate);
398         _main_canvas->SetVirtualSize (*_pixels_per_second * film->length().seconds(), tracks() * pixels_per_track() + 96);
399         _main_canvas->SetScrollRate (_x_scroll_rate, _y_scroll_rate);
400 }
401
402 shared_ptr<TimelineView>
403 Timeline::event_to_view (wxMouseEvent& ev)
404 {
405         /* Search backwards through views so that we find the uppermost one first */
406         TimelineViewList::reverse_iterator i = _views.rbegin();
407         Position<int> const p (ev.GetX(), ev.GetY());
408         while (i != _views.rend() && !(*i)->bbox().contains (p)) {
409                 shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
410                 ++i;
411         }
412
413         if (i == _views.rend ()) {
414                 return shared_ptr<TimelineView> ();
415         }
416
417         return *i;
418 }
419
420 void
421 Timeline::left_down (wxMouseEvent& ev)
422 {
423         _left_down = true;
424         _down_point = ev.GetPosition ();
425
426         switch (_tool) {
427         case SELECT:
428                 left_down_select (ev);
429                 break;
430         case ZOOM:
431         case ZOOM_ALL:
432                 /* Nothing to do */
433                 break;
434         }
435 }
436
437 void
438 Timeline::left_down_select (wxMouseEvent& ev)
439 {
440         shared_ptr<TimelineView> view = event_to_view (ev);
441         shared_ptr<TimelineContentView> content_view = dynamic_pointer_cast<TimelineContentView> (view);
442
443         _down_view.reset ();
444
445         if (content_view) {
446                 _down_view = content_view;
447                 _down_view_position = content_view->content()->position ();
448         }
449
450         for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
451                 shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
452                 if (!cv) {
453                         continue;
454                 }
455
456                 if (!ev.ShiftDown ()) {
457                         cv->set_selected (view == *i);
458                 }
459         }
460
461         if (content_view && ev.ShiftDown ()) {
462                 content_view->set_selected (!content_view->selected ());
463         }
464
465         _first_move = false;
466
467         if (_down_view) {
468                 /* Pre-compute the points that we might snap to */
469                 for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
470                         shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
471                         if (!cv || cv == _down_view || cv->content() == _down_view->content()) {
472                                 continue;
473                         }
474
475                         _start_snaps.push_back (cv->content()->position());
476                         _end_snaps.push_back (cv->content()->position());
477                         _start_snaps.push_back (cv->content()->end());
478                         _end_snaps.push_back (cv->content()->end());
479
480                         BOOST_FOREACH (DCPTime i, cv->content()->reel_split_points()) {
481                                 _start_snaps.push_back (i);
482                         }
483                 }
484
485                 /* Tell everyone that things might change frequently during the drag */
486                 _down_view->content()->set_change_signals_frequent (true);
487         }
488 }
489
490 void
491 Timeline::left_up (wxMouseEvent& ev)
492 {
493         _left_down = false;
494
495         switch (_tool) {
496         case SELECT:
497                 left_up_select (ev);
498                 break;
499         case ZOOM:
500                 left_up_zoom (ev);
501                 break;
502         case ZOOM_ALL:
503                 break;
504         }
505 }
506
507 void
508 Timeline::left_up_select (wxMouseEvent& ev)
509 {
510         if (_down_view) {
511                 _down_view->content()->set_change_signals_frequent (false);
512         }
513
514         _content_panel->set_selection (selected_content ());
515         set_position_from_event (ev);
516
517         /* Clear up up the stuff we don't do during drag */
518         assign_tracks ();
519         setup_scrollbars ();
520         Refresh ();
521
522         _start_snaps.clear ();
523         _end_snaps.clear ();
524 }
525
526 void
527 Timeline::left_up_zoom (wxMouseEvent& ev)
528 {
529         _zoom_point = ev.GetPosition ();
530
531         int vsx, vsy;
532         _main_canvas->GetViewStart (&vsx, &vsy);
533
534         wxPoint top_left(min(_down_point.x, _zoom_point->x), min(_down_point.y, _zoom_point->y));
535         wxPoint bottom_right(max(_down_point.x, _zoom_point->x), max(_down_point.y, _zoom_point->y));
536
537         if ((bottom_right.x - top_left.x) < 8 || (bottom_right.y - top_left.y) < 8) {
538                 /* Very small zoom rectangle: we assume it wasn't intentional */
539                 return;
540         }
541
542         DCPTime const time_left = DCPTime::from_seconds((top_left.x + vsx) / *_pixels_per_second);
543         DCPTime const time_right = DCPTime::from_seconds((bottom_right.x + vsx) / *_pixels_per_second);
544         set_pixels_per_second (double(GetSize().GetWidth()) / (time_right.seconds() - time_left.seconds()));
545
546         double const tracks_top = double(top_left.y) / _pixels_per_track;
547         double const tracks_bottom = double(bottom_right.y) / _pixels_per_track;
548         set_pixels_per_track (lrint(GetSize().GetHeight() / (tracks_bottom - tracks_top)));
549
550         setup_scrollbars ();
551         _main_canvas->Scroll (time_left.seconds() * *_pixels_per_second / _x_scroll_rate, tracks_top * _pixels_per_track / _y_scroll_rate);
552         _labels_canvas->Scroll (0, tracks_top * _pixels_per_track / _y_scroll_rate);
553
554         _zoom_point = optional<wxPoint> ();
555         Refresh ();
556 }
557
558 void
559 Timeline::set_pixels_per_track (int h)
560 {
561         _pixels_per_track = max(_minimum_pixels_per_track, h);
562 }
563
564 void
565 Timeline::mouse_moved (wxMouseEvent& ev)
566 {
567         switch (_tool) {
568         case SELECT:
569                 mouse_moved_select (ev);
570                 break;
571         case ZOOM:
572                 mouse_moved_zoom (ev);
573                 break;
574         case ZOOM_ALL:
575                 break;
576         }
577 }
578
579 void
580 Timeline::mouse_moved_select (wxMouseEvent& ev)
581 {
582         if (!_left_down) {
583                 return;
584         }
585
586         set_position_from_event (ev);
587 }
588
589 void
590 Timeline::mouse_moved_zoom (wxMouseEvent& ev)
591 {
592         if (!_left_down) {
593                 return;
594         }
595
596         _zoom_point = ev.GetPosition ();
597         Refresh ();
598 }
599
600 void
601 Timeline::right_down (wxMouseEvent& ev)
602 {
603         switch (_tool) {
604         case SELECT:
605                 right_down_select (ev);
606                 break;
607         case ZOOM:
608                 /* Zoom out */
609                 set_pixels_per_second (*_pixels_per_second / 2);
610                 set_pixels_per_track (_pixels_per_track / 2);
611                 setup_scrollbars ();
612                 Refresh ();
613                 break;
614         case ZOOM_ALL:
615                 break;
616         }
617 }
618
619 void
620 Timeline::right_down_select (wxMouseEvent& ev)
621 {
622         shared_ptr<TimelineView> view = event_to_view (ev);
623         shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (view);
624         if (!cv) {
625                 return;
626         }
627
628         if (!cv->selected ()) {
629                 clear_selection ();
630                 cv->set_selected (true);
631         }
632
633         _menu.popup (_film, selected_content (), selected_views (), ev.GetPosition ());
634 }
635
636 void
637 Timeline::maybe_snap (DCPTime a, DCPTime b, optional<DCPTime>& nearest_distance) const
638 {
639         DCPTime const d = a - b;
640         if (!nearest_distance || d.abs() < nearest_distance.get().abs()) {
641                 nearest_distance = d;
642         }
643 }
644
645 void
646 Timeline::set_position_from_event (wxMouseEvent& ev)
647 {
648         if (!_pixels_per_second) {
649                 return;
650         }
651
652         double const pps = _pixels_per_second.get ();
653
654         wxPoint const p = ev.GetPosition();
655
656         if (!_first_move) {
657                 /* We haven't moved yet; in that case, we must move the mouse some reasonable distance
658                    before the drag is considered to have started.
659                 */
660                 int const dist = sqrt (pow (p.x - _down_point.x, 2) + pow (p.y - _down_point.y, 2));
661                 if (dist < 8) {
662                         return;
663                 }
664                 _first_move = true;
665         }
666
667         if (!_down_view) {
668                 return;
669         }
670
671         DCPTime new_position = _down_view_position + DCPTime::from_seconds ((p.x - _down_point.x) / pps);
672
673         if (_snap) {
674
675                 DCPTime const new_end = new_position + _down_view->content()->length_after_trim();
676                 /* Signed `distance' to nearest thing (i.e. negative is left on the timeline,
677                    positive is right).
678                 */
679                 optional<DCPTime> nearest_distance;
680
681                 /* Find the nearest snap point */
682
683                 BOOST_FOREACH (DCPTime i, _start_snaps) {
684                         maybe_snap (i, new_position, nearest_distance);
685                 }
686
687                 BOOST_FOREACH (DCPTime i, _end_snaps) {
688                         maybe_snap (i, new_end, nearest_distance);
689                 }
690
691                 if (nearest_distance) {
692                         /* Snap if it's close; `close' means within a proportion of the time on the timeline */
693                         if (nearest_distance.get().abs() < DCPTime::from_seconds ((width() / pps) / 64)) {
694                                 new_position += nearest_distance.get ();
695                         }
696                 }
697         }
698
699         if (new_position < DCPTime ()) {
700                 new_position = DCPTime ();
701         }
702
703         _down_view->content()->set_position (new_position);
704
705         shared_ptr<Film> film = _film.lock ();
706         DCPOMATIC_ASSERT (film);
707         film->set_sequence (false);
708 }
709
710 void
711 Timeline::force_redraw (dcpomatic::Rect<int> const & r)
712 {
713         RefreshRect (wxRect (r.x, r.y, r.width, r.height), false);
714 }
715
716 shared_ptr<const Film>
717 Timeline::film () const
718 {
719         return _film.lock ();
720 }
721
722 void
723 Timeline::resized ()
724 {
725         setup_scrollbars ();
726 }
727
728 void
729 Timeline::clear_selection ()
730 {
731         for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
732                 shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
733                 if (cv) {
734                         cv->set_selected (false);
735                 }
736         }
737 }
738
739 TimelineContentViewList
740 Timeline::selected_views () const
741 {
742         TimelineContentViewList sel;
743
744         for (TimelineViewList::const_iterator i = _views.begin(); i != _views.end(); ++i) {
745                 shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
746                 if (cv && cv->selected()) {
747                         sel.push_back (cv);
748                 }
749         }
750
751         return sel;
752 }
753
754 ContentList
755 Timeline::selected_content () const
756 {
757         ContentList sel;
758         TimelineContentViewList views = selected_views ();
759
760         for (TimelineContentViewList::const_iterator i = views.begin(); i != views.end(); ++i) {
761                 sel.push_back ((*i)->content ());
762         }
763
764         return sel;
765 }
766
767 void
768 Timeline::set_selection (ContentList selection)
769 {
770         for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
771                 shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
772                 if (cv) {
773                         cv->set_selected (find (selection.begin(), selection.end(), cv->content ()) != selection.end ());
774                 }
775         }
776 }
777
778 int
779 Timeline::tracks_y_offset () const
780 {
781         return _reels_view->bbox().height + 4;
782 }
783
784 int
785 Timeline::width () const
786 {
787         return _main_canvas->GetVirtualSize().GetWidth();
788 }
789
790 void
791 Timeline::scrolled (wxScrollWinEvent& ev)
792 {
793         if (ev.GetOrientation() == wxVERTICAL) {
794                 _labels_canvas->Scroll (0, ev.GetPosition ());
795         }
796         ev.Skip ();
797 }
798
799 void
800 Timeline::tool_clicked (Tool t)
801 {
802         switch (t) {
803         case ZOOM:
804         case SELECT:
805                 _tool = t;
806                 break;
807         case ZOOM_ALL:
808                 shared_ptr<Film> film = _film.lock ();
809                 DCPOMATIC_ASSERT (film);
810                 set_pixels_per_second ((_main_canvas->GetSize().GetWidth() - 32) / film->length().seconds());
811                 set_pixels_per_track ((_main_canvas->GetSize().GetHeight() - tracks_y_offset() - _time_axis_view->bbox().height - 32) / _tracks);
812                 Refresh ();
813                 break;
814         }
815 }