Supporters update.
[dcpomatic.git] / src / wx / timeline.cc
1 /*
2     Copyright (C) 2013-2021 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 "content_panel.h"
22 #include "film_editor.h"
23 #include "film_viewer.h"
24 #include "timeline.h"
25 #include "timeline_atmos_content_view.h"
26 #include "timeline_audio_content_view.h"
27 #include "timeline_labels_view.h"
28 #include "timeline_reels_view.h"
29 #include "timeline_text_content_view.h"
30 #include "timeline_time_axis_view.h"
31 #include "timeline_video_content_view.h"
32 #include "wx_util.h"
33 #include "lib/atmos_mxf_content.h"
34 #include "lib/audio_content.h"
35 #include "lib/film.h"
36 #include "lib/image_content.h"
37 #include "lib/playlist.h"
38 #include "lib/text_content.h"
39 #include "lib/timer.h"
40 #include "lib/video_content.h"
41 #include <dcp/scope_guard.h>
42 #include <dcp/warnings.h>
43 LIBDCP_DISABLE_WARNINGS
44 #include <wx/graphics.h>
45 LIBDCP_ENABLE_WARNINGS
46 #include <iterator>
47 #include <list>
48
49
50 using std::abs;
51 using std::dynamic_pointer_cast;
52 using std::list;
53 using std::make_shared;
54 using std::max;
55 using std::min;
56 using std::shared_ptr;
57 using std::weak_ptr;
58 using boost::bind;
59 using boost::optional;
60 using namespace dcpomatic;
61 #if BOOST_VERSION >= 106100
62 using namespace boost::placeholders;
63 #endif
64
65
66 /* 3 hours in 640 pixels */
67 double const Timeline::_minimum_pixels_per_second = 640.0 / (60 * 60 * 3);
68 int const Timeline::_minimum_pixels_per_track = 16;
69
70
71 Timeline::Timeline(wxWindow* parent, ContentPanel* cp, shared_ptr<Film> film, FilmViewer& viewer)
72         : wxPanel (parent, wxID_ANY)
73         , _labels_canvas (new wxScrolledCanvas (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE))
74         , _main_canvas (new wxScrolledCanvas (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE))
75         , _content_panel (cp)
76         , _film (film)
77         , _viewer (viewer)
78         , _time_axis_view (new TimelineTimeAxisView (*this, 64))
79         , _reels_view (new TimelineReelsView (*this, 32))
80         , _labels_view (new TimelineLabelsView (*this))
81         , _tracks (0)
82         , _left_down (false)
83         , _down_view_position (0)
84         , _first_move (false)
85         , _menu (this, viewer)
86         , _snap (true)
87         , _tool (SELECT)
88         , _x_scroll_rate (16)
89         , _y_scroll_rate (16)
90         , _pixels_per_track (48)
91         , _first_resize (true)
92         , _timer (this)
93 {
94 #ifndef __WXOSX__
95         _labels_canvas->SetDoubleBuffered (true);
96         _main_canvas->SetDoubleBuffered (true);
97 #endif
98
99         auto sizer = new wxBoxSizer (wxHORIZONTAL);
100         sizer->Add (_labels_canvas, 0, wxEXPAND);
101         _labels_canvas->SetMinSize (wxSize (_labels_view->bbox().width, -1));
102         sizer->Add (_main_canvas, 1, wxEXPAND);
103         SetSizer (sizer);
104
105         _labels_canvas->Bind (wxEVT_PAINT,      boost::bind (&Timeline::paint_labels, this));
106         _main_canvas->Bind   (wxEVT_PAINT,      boost::bind (&Timeline::paint_main,   this));
107         _main_canvas->Bind   (wxEVT_LEFT_DOWN,  boost::bind (&Timeline::left_down,    this, _1));
108         _main_canvas->Bind   (wxEVT_LEFT_UP,    boost::bind (&Timeline::left_up,      this, _1));
109         _main_canvas->Bind   (wxEVT_RIGHT_DOWN, boost::bind (&Timeline::right_down,   this, _1));
110         _main_canvas->Bind   (wxEVT_MOTION,     boost::bind (&Timeline::mouse_moved,  this, _1));
111         _main_canvas->Bind   (wxEVT_SIZE,       boost::bind (&Timeline::resized,      this));
112         _main_canvas->Bind   (wxEVT_MOUSEWHEEL, boost::bind(&Timeline::mouse_wheel_turned, this, _1));
113         _main_canvas->Bind   (wxEVT_SCROLLWIN_TOP,        boost::bind (&Timeline::scrolled,     this, _1));
114         _main_canvas->Bind   (wxEVT_SCROLLWIN_BOTTOM,     boost::bind (&Timeline::scrolled,     this, _1));
115         _main_canvas->Bind   (wxEVT_SCROLLWIN_LINEUP,     boost::bind (&Timeline::scrolled,     this, _1));
116         _main_canvas->Bind   (wxEVT_SCROLLWIN_LINEDOWN,   boost::bind (&Timeline::scrolled,     this, _1));
117         _main_canvas->Bind   (wxEVT_SCROLLWIN_PAGEUP,     boost::bind (&Timeline::scrolled,     this, _1));
118         _main_canvas->Bind   (wxEVT_SCROLLWIN_PAGEDOWN,   boost::bind (&Timeline::scrolled,     this, _1));
119         _main_canvas->Bind   (wxEVT_SCROLLWIN_THUMBTRACK, boost::bind (&Timeline::scrolled,     this, _1));
120
121         film_change(ChangeType::DONE, FilmProperty::CONTENT);
122
123         SetMinSize (wxSize (640, 4 * pixels_per_track() + 96));
124
125         _film_changed_connection = film->Change.connect (bind (&Timeline::film_change, this, _1, _2));
126         _film_content_change_connection = film->ContentChange.connect (bind (&Timeline::film_content_change, this, _1, _3, _4));
127
128         Bind (wxEVT_TIMER, boost::bind(&Timeline::update_playhead, this));
129         _timer.Start (200, wxTIMER_CONTINUOUS);
130
131         setup_scrollbars ();
132         _labels_canvas->ShowScrollbars (wxSHOW_SB_NEVER, wxSHOW_SB_NEVER);
133 }
134
135
136 void
137 Timeline::mouse_wheel_turned(wxMouseEvent& event)
138 {
139         auto const rotation = event.GetWheelRotation();
140
141         if (event.ControlDown()) {
142                 /* On my mouse one click of the scroll wheel is 120, and it's -ve when
143                  * scrolling the wheel towards me.
144                  */
145                 auto const scale = rotation > 0 ?
146                         (1.0 / (rotation / 90.0)) :
147                         (-rotation / 90.0);
148
149                 int before_start_x;
150                 int before_start_y;
151                 _main_canvas->GetViewStart(&before_start_x, &before_start_y);
152
153                 auto const before_pps = _pixels_per_second.get_value_or(1);
154                 auto const before_pos = _last_mouse_wheel_x && *_last_mouse_wheel_x == event.GetX() ?
155                         *_last_mouse_wheel_time :
156                         (before_start_x * _x_scroll_rate + event.GetX()) / before_pps;
157
158                 set_pixels_per_second(before_pps * scale);
159                 setup_scrollbars();
160
161                 auto after_left = std::max(0.0, before_pos * _pixels_per_second.get_value_or(1) - event.GetX());
162                 _main_canvas->Scroll(after_left / _x_scroll_rate, before_start_y);
163                 _labels_canvas->Scroll(0, before_start_y);
164                 Refresh();
165
166                 if (!_last_mouse_wheel_x || *_last_mouse_wheel_x != event.GetX()) {
167                         _last_mouse_wheel_x = event.GetX();
168                         _last_mouse_wheel_time = before_pos;
169                 }
170         } else if (event.ShiftDown()) {
171                 int before_start_x;
172                 int before_start_y;
173                 _main_canvas->GetViewStart(&before_start_x, &before_start_y);
174                 auto const width = _main_canvas->GetSize().GetWidth();
175                 _main_canvas->Scroll(std::max(0.0, before_start_x - rotation * 100.0 / width), before_start_y);
176         }
177 }
178
179
180 void
181 Timeline::update_playhead ()
182 {
183         Refresh ();
184 }
185
186
187 void
188 Timeline::set_pixels_per_second (double pps)
189 {
190         _pixels_per_second = max (_minimum_pixels_per_second, pps);
191 }
192
193
194 void
195 Timeline::paint_labels ()
196 {
197         wxPaintDC dc (_labels_canvas);
198
199         auto film = _film.lock();
200         if (film->content().empty()) {
201                 return;
202         }
203
204         auto gc = wxGraphicsContext::Create (dc);
205         if (!gc) {
206                 return;
207         }
208
209         dcp::ScopeGuard sg = [gc]() { delete gc; };
210
211         int vsx, vsy;
212         _labels_canvas->GetViewStart (&vsx, &vsy);
213         gc->Translate (-vsx * _x_scroll_rate, -vsy * _y_scroll_rate + tracks_y_offset());
214
215         _labels_view->paint (gc, {});
216 }
217
218
219 void
220 Timeline::paint_main ()
221 {
222         wxPaintDC dc (_main_canvas);
223         dc.Clear();
224
225         auto film = _film.lock();
226         if (film->content().empty()) {
227                 return;
228         }
229
230         _main_canvas->DoPrepareDC (dc);
231
232         auto gc = wxGraphicsContext::Create (dc);
233         if (!gc) {
234                 return;
235         }
236
237         dcp::ScopeGuard sg = [gc]() { delete gc; };
238
239         gc->SetAntialiasMode (wxANTIALIAS_DEFAULT);
240
241         for (auto i: _views) {
242
243                 auto ic = dynamic_pointer_cast<TimelineContentView> (i);
244
245                 /* Find areas of overlap with other content views, so that we can plot them */
246                 list<dcpomatic::Rect<int>> overlaps;
247                 for (auto j: _views) {
248                         auto jc = dynamic_pointer_cast<TimelineContentView> (j);
249                         /* No overlap with non-content views, views on different tracks, audio views or non-active views */
250                         if (!ic || !jc || i == j || ic->track() != jc->track() || ic->track().get_value_or(2) >= 2 || !ic->active() || !jc->active()) {
251                                 continue;
252                         }
253
254                         auto r = j->bbox().intersection(i->bbox());
255                         if (r) {
256                                 overlaps.push_back (r.get ());
257                         }
258                 }
259
260                 i->paint (gc, overlaps);
261         }
262
263         if (_zoom_point) {
264                 gc->SetPen(gui_is_dark() ? *wxWHITE_PEN : *wxBLACK_PEN);
265                 gc->SetBrush (*wxTRANSPARENT_BRUSH);
266                 gc->DrawRectangle (
267                         min (_down_point.x, _zoom_point->x),
268                         min (_down_point.y, _zoom_point->y),
269                         abs (_down_point.x - _zoom_point->x),
270                         abs (_down_point.y - _zoom_point->y)
271                         );
272         }
273
274         /* Playhead */
275
276         gc->SetPen (*wxRED_PEN);
277         auto path = gc->CreatePath ();
278         double const ph = _viewer.position().seconds() * pixels_per_second().get_value_or(0);
279         path.MoveToPoint (ph, 0);
280         path.AddLineToPoint (ph, pixels_per_track() * _tracks + 32);
281         gc->StrokePath (path);
282 }
283
284
285 void
286 Timeline::film_change(ChangeType type, FilmProperty p)
287 {
288         if (type != ChangeType::DONE) {
289                 return;
290         }
291
292         if (p == FilmProperty::CONTENT || p == FilmProperty::REEL_TYPE || p == FilmProperty::REEL_LENGTH) {
293                 ensure_ui_thread ();
294                 recreate_views ();
295         } else if (p == FilmProperty::CONTENT_ORDER) {
296                 Refresh ();
297         }
298 }
299
300
301 void
302 Timeline::recreate_views ()
303 {
304         auto film = _film.lock ();
305         if (!film) {
306                 return;
307         }
308
309         _views.clear ();
310         _views.push_back (_time_axis_view);
311         _views.push_back (_reels_view);
312
313         for (auto i: film->content ()) {
314                 if (i->video) {
315                         _views.push_back (make_shared<TimelineVideoContentView>(*this, i));
316                 }
317
318                 if (i->audio && !i->audio->mapping().mapped_output_channels().empty ()) {
319                         _views.push_back (make_shared<TimelineAudioContentView>(*this, i));
320                 }
321
322                 for (auto j: i->text) {
323                         _views.push_back (make_shared<TimelineTextContentView>(*this, i, j));
324                 }
325
326                 if (i->atmos) {
327                         _views.push_back (make_shared<TimelineAtmosContentView>(*this, i));
328                 }
329         }
330
331         assign_tracks ();
332         setup_scrollbars ();
333         Refresh ();
334 }
335
336
337 void
338 Timeline::film_content_change (ChangeType type, int property, bool frequent)
339 {
340         if (type != ChangeType::DONE) {
341                 return;
342         }
343
344         ensure_ui_thread ();
345
346         if (property == AudioContentProperty::STREAMS || property == VideoContentProperty::FRAME_TYPE) {
347                 recreate_views ();
348         } else if (property == ContentProperty::POSITION || property == ContentProperty::LENGTH) {
349                 _reels_view->force_redraw ();
350         } else if (!frequent) {
351                 setup_scrollbars ();
352                 Refresh ();
353         }
354 }
355
356
357 template <class T>
358 int
359 place (shared_ptr<const Film> film, TimelineViewList& views, int& tracks)
360 {
361         int const base = tracks;
362
363         for (auto i: views) {
364                 if (!dynamic_pointer_cast<T>(i)) {
365                         continue;
366                 }
367
368                 auto cv = dynamic_pointer_cast<TimelineContentView> (i);
369                 DCPOMATIC_ASSERT(cv);
370
371                 int t = base;
372
373                 auto content = cv->content();
374                 DCPTimePeriod const content_period = content->period(film);
375
376                 while (true) {
377                         auto j = views.begin();
378                         while (j != views.end()) {
379                                 auto test = dynamic_pointer_cast<T> (*j);
380                                 if (!test) {
381                                         ++j;
382                                         continue;
383                                 }
384
385                                 auto test_content = test->content();
386                                 if (
387                                         test->track() && test->track().get() == t &&
388                                         content_period.overlap(test_content->period(film))
389                                    ) {
390                                         /* we have an overlap on track `t' */
391                                         ++t;
392                                         break;
393                                 }
394
395                                 ++j;
396                         }
397
398                         if (j == views.end ()) {
399                                 /* no overlap on `t' */
400                                 break;
401                         }
402                 }
403
404                 cv->set_track (t);
405                 tracks = max (tracks, t + 1);
406         }
407
408         return tracks - base;
409 }
410
411
412 /** Compare the mapped output channels of two TimelineViews, so we can into
413  *  order of first mapped DCP channel.
414  */
415 struct AudioMappingComparator {
416         bool operator()(shared_ptr<TimelineView> a, shared_ptr<TimelineView> b) {
417                 int la = -1;
418                 auto cva = dynamic_pointer_cast<TimelineAudioContentView>(a);
419                 if (cva) {
420                         auto oc = cva->content()->audio->mapping().mapped_output_channels();
421                         la = *min_element(boost::begin(oc), boost::end(oc));
422                 }
423                 int lb = -1;
424                 auto cvb = dynamic_pointer_cast<TimelineAudioContentView>(b);
425                 if (cvb) {
426                         auto oc = cvb->content()->audio->mapping().mapped_output_channels();
427                         lb = *min_element(boost::begin(oc), boost::end(oc));
428                 }
429                 return la < lb;
430         }
431 };
432
433
434 void
435 Timeline::assign_tracks ()
436 {
437         /* Tracks are:
438            Video 1
439            Video 2
440            Video N
441            Text 1
442            Text 2
443            Text N
444            Atmos
445            Audio 1
446            Audio 2
447            Audio N
448         */
449
450         auto film = _film.lock ();
451         DCPOMATIC_ASSERT (film);
452
453         _tracks = 0;
454
455         for (auto i: _views) {
456                 auto c = dynamic_pointer_cast<TimelineContentView>(i);
457                 if (c) {
458                         c->unset_track ();
459                 }
460         }
461
462         int const video_tracks = place<TimelineVideoContentView> (film, _views, _tracks);
463         int const text_tracks = place<TimelineTextContentView> (film, _views, _tracks);
464
465         /* Atmos */
466
467         bool have_atmos = false;
468         for (auto i: _views) {
469                 auto cv = dynamic_pointer_cast<TimelineAtmosContentView>(i);
470                 if (cv) {
471                         cv->set_track (_tracks);
472                         have_atmos = true;
473                 }
474         }
475
476         if (have_atmos) {
477                 ++_tracks;
478         }
479
480         /* Audio.  We're sorting the views so that we get the audio views in order of increasing
481            DCP channel index.
482         */
483
484         auto views = _views;
485         sort(views.begin(), views.end(), AudioMappingComparator());
486         int const audio_tracks = place<TimelineAudioContentView> (film, views, _tracks);
487
488         _labels_view->set_video_tracks (video_tracks);
489         _labels_view->set_audio_tracks (audio_tracks);
490         _labels_view->set_text_tracks (text_tracks);
491         _labels_view->set_atmos (have_atmos);
492
493         _time_axis_view->set_y (tracks());
494         _reels_view->set_y (8);
495 }
496
497
498 int
499 Timeline::tracks () const
500 {
501         return _tracks;
502 }
503
504
505 void
506 Timeline::setup_scrollbars ()
507 {
508         auto film = _film.lock ();
509         if (!film || !_pixels_per_second) {
510                 return;
511         }
512
513         int const h = tracks() * pixels_per_track() + tracks_y_offset() + _time_axis_view->bbox().height;
514
515         _labels_canvas->SetVirtualSize (_labels_view->bbox().width, h);
516         _labels_canvas->SetScrollRate (_x_scroll_rate, _y_scroll_rate);
517         _main_canvas->SetVirtualSize (*_pixels_per_second * film->length().seconds(), h);
518         _main_canvas->SetScrollRate (_x_scroll_rate, _y_scroll_rate);
519 }
520
521
522 shared_ptr<TimelineView>
523 Timeline::event_to_view (wxMouseEvent& ev)
524 {
525         /* Search backwards through views so that we find the uppermost one first */
526         auto i = _views.rbegin();
527
528         int vsx, vsy;
529         _main_canvas->GetViewStart (&vsx, &vsy);
530         Position<int> const p (ev.GetX() + vsx * _x_scroll_rate, ev.GetY() + vsy * _y_scroll_rate);
531
532         while (i != _views.rend() && !(*i)->bbox().contains (p)) {
533                 ++i;
534         }
535
536         if (i == _views.rend ()) {
537                 return {};
538         }
539
540         return *i;
541 }
542
543
544 void
545 Timeline::left_down (wxMouseEvent& ev)
546 {
547         _left_down = true;
548         _down_point = ev.GetPosition ();
549
550         switch (_tool) {
551         case SELECT:
552                 left_down_select (ev);
553                 break;
554         case ZOOM:
555         case ZOOM_ALL:
556         case SNAP:
557         case SEQUENCE:
558                 /* Nothing to do */
559                 break;
560         }
561 }
562
563
564 void
565 Timeline::left_down_select (wxMouseEvent& ev)
566 {
567         auto view = event_to_view (ev);
568         auto content_view = dynamic_pointer_cast<TimelineContentView>(view);
569
570         _down_view.reset ();
571
572         if (content_view) {
573                 _down_view = content_view;
574                 _down_view_position = content_view->content()->position ();
575         }
576
577         if (dynamic_pointer_cast<TimelineTimeAxisView>(view)) {
578                 int vsx, vsy;
579                 _main_canvas->GetViewStart(&vsx, &vsy);
580                 _viewer.seek(DCPTime::from_seconds((ev.GetPosition().x + vsx * _x_scroll_rate) / _pixels_per_second.get_value_or(1)), true);
581         }
582
583         for (auto i: _views) {
584                 auto cv = dynamic_pointer_cast<TimelineContentView>(i);
585                 if (!cv) {
586                         continue;
587                 }
588
589                 if (!ev.ShiftDown ()) {
590                         cv->set_selected (view == i);
591                 }
592         }
593
594         if (content_view && ev.ShiftDown ()) {
595                 content_view->set_selected (!content_view->selected ());
596         }
597
598         _first_move = false;
599
600         if (_down_view) {
601                 /* Pre-compute the points that we might snap to */
602                 for (auto i: _views) {
603                         auto cv = dynamic_pointer_cast<TimelineContentView>(i);
604                         if (!cv || cv == _down_view || cv->content() == _down_view->content()) {
605                                 continue;
606                         }
607
608                         auto film = _film.lock ();
609                         DCPOMATIC_ASSERT (film);
610
611                         _start_snaps.push_back (cv->content()->position());
612                         _end_snaps.push_back (cv->content()->position());
613                         _start_snaps.push_back (cv->content()->end(film));
614                         _end_snaps.push_back (cv->content()->end(film));
615
616                         for (auto i: cv->content()->reel_split_points(film)) {
617                                 _start_snaps.push_back (i);
618                         }
619                 }
620
621                 /* Tell everyone that things might change frequently during the drag */
622                 _down_view->content()->set_change_signals_frequent (true);
623         }
624 }
625
626
627 void
628 Timeline::left_up (wxMouseEvent& ev)
629 {
630         _left_down = false;
631
632         switch (_tool) {
633         case SELECT:
634                 left_up_select (ev);
635                 break;
636         case ZOOM:
637                 left_up_zoom (ev);
638                 break;
639         case ZOOM_ALL:
640         case SNAP:
641         case SEQUENCE:
642                 break;
643         }
644 }
645
646
647 void
648 Timeline::left_up_select (wxMouseEvent& ev)
649 {
650         if (_down_view) {
651                 _down_view->content()->set_change_signals_frequent (false);
652         }
653
654         _content_panel->set_selection (selected_content ());
655         /* Since we may have just set change signals back to `not-frequent', we have to
656            make sure this position change is signalled, even if the position value has
657            not changed since the last time it was set (with frequent=true).  This is
658            a bit of a hack.
659         */
660         set_position_from_event (ev, true);
661
662         /* Clear up up the stuff we don't do during drag */
663         assign_tracks ();
664         setup_scrollbars ();
665         Refresh ();
666
667         _start_snaps.clear ();
668         _end_snaps.clear ();
669 }
670
671
672 void
673 Timeline::left_up_zoom (wxMouseEvent& ev)
674 {
675         _zoom_point = ev.GetPosition ();
676
677         int vsx, vsy;
678         _main_canvas->GetViewStart (&vsx, &vsy);
679
680         wxPoint top_left(min(_down_point.x, _zoom_point->x), min(_down_point.y, _zoom_point->y));
681         wxPoint bottom_right(max(_down_point.x, _zoom_point->x), max(_down_point.y, _zoom_point->y));
682
683         if ((bottom_right.x - top_left.x) < 8 || (bottom_right.y - top_left.y) < 8) {
684                 /* Very small zoom rectangle: we assume it wasn't intentional */
685                 _zoom_point = optional<wxPoint> ();
686                 Refresh ();
687                 return;
688         }
689
690         auto const time_left = DCPTime::from_seconds((top_left.x + vsx) / *_pixels_per_second);
691         auto const time_right = DCPTime::from_seconds((bottom_right.x + vsx) / *_pixels_per_second);
692         set_pixels_per_second (double(GetSize().GetWidth()) / (time_right.seconds() - time_left.seconds()));
693
694         double const tracks_top = double(top_left.y - tracks_y_offset()) / _pixels_per_track;
695         double const tracks_bottom = double(bottom_right.y - tracks_y_offset()) / _pixels_per_track;
696         set_pixels_per_track (lrint(GetSize().GetHeight() / (tracks_bottom - tracks_top)));
697
698         setup_scrollbars ();
699         int const y = (tracks_top * _pixels_per_track + tracks_y_offset()) / _y_scroll_rate;
700         _main_canvas->Scroll (time_left.seconds() * *_pixels_per_second / _x_scroll_rate, y);
701         _labels_canvas->Scroll (0, y);
702
703         _zoom_point = optional<wxPoint> ();
704         Refresh ();
705 }
706
707
708 void
709 Timeline::set_pixels_per_track (int h)
710 {
711         _pixels_per_track = max(_minimum_pixels_per_track, h);
712 }
713
714
715 void
716 Timeline::mouse_moved (wxMouseEvent& ev)
717 {
718         switch (_tool) {
719         case SELECT:
720                 mouse_moved_select (ev);
721                 break;
722         case ZOOM:
723                 mouse_moved_zoom (ev);
724                 break;
725         case ZOOM_ALL:
726         case SNAP:
727         case SEQUENCE:
728                 break;
729         }
730 }
731
732
733 void
734 Timeline::mouse_moved_select (wxMouseEvent& ev)
735 {
736         if (!_left_down) {
737                 return;
738         }
739
740         set_position_from_event (ev);
741 }
742
743
744 void
745 Timeline::mouse_moved_zoom (wxMouseEvent& ev)
746 {
747         if (!_left_down) {
748                 return;
749         }
750
751         _zoom_point = ev.GetPosition ();
752         setup_scrollbars();
753         Refresh ();
754 }
755
756
757 void
758 Timeline::right_down (wxMouseEvent& ev)
759 {
760         switch (_tool) {
761         case SELECT:
762                 right_down_select (ev);
763                 break;
764         case ZOOM:
765                 /* Zoom out */
766                 set_pixels_per_second (*_pixels_per_second / 2);
767                 set_pixels_per_track (_pixels_per_track / 2);
768                 setup_scrollbars ();
769                 Refresh ();
770                 break;
771         case ZOOM_ALL:
772         case SNAP:
773         case SEQUENCE:
774                 break;
775         }
776 }
777
778
779 void
780 Timeline::right_down_select (wxMouseEvent& ev)
781 {
782         auto view = event_to_view (ev);
783         auto cv = dynamic_pointer_cast<TimelineContentView> (view);
784         if (!cv) {
785                 return;
786         }
787
788         if (!cv->selected ()) {
789                 clear_selection ();
790                 cv->set_selected (true);
791         }
792
793         _menu.popup (_film, selected_content (), selected_views (), ev.GetPosition ());
794 }
795
796
797 void
798 Timeline::maybe_snap (DCPTime a, DCPTime b, optional<DCPTime>& nearest_distance) const
799 {
800         auto const d = a - b;
801         if (!nearest_distance || d.abs() < nearest_distance.get().abs()) {
802                 nearest_distance = d;
803         }
804 }
805
806
807 void
808 Timeline::set_position_from_event (wxMouseEvent& ev, bool force_emit)
809 {
810         if (!_pixels_per_second) {
811                 return;
812         }
813
814         double const pps = _pixels_per_second.get ();
815
816         auto const p = ev.GetPosition();
817
818         if (!_first_move) {
819                 /* We haven't moved yet; in that case, we must move the mouse some reasonable distance
820                    before the drag is considered to have started.
821                 */
822                 int const dist = sqrt (pow (p.x - _down_point.x, 2) + pow (p.y - _down_point.y, 2));
823                 if (dist < 8) {
824                         return;
825                 }
826                 _first_move = true;
827         }
828
829         if (!_down_view) {
830                 return;
831         }
832
833         auto new_position = _down_view_position + DCPTime::from_seconds ((p.x - _down_point.x) / pps);
834
835         auto film = _film.lock ();
836         DCPOMATIC_ASSERT (film);
837
838         if (_snap) {
839                 auto const new_end = new_position + _down_view->content()->length_after_trim(film);
840                 /* Signed `distance' to nearest thing (i.e. negative is left on the timeline,
841                    positive is right).
842                 */
843                 optional<DCPTime> nearest_distance;
844
845                 /* Find the nearest snap point */
846
847                 for (auto i: _start_snaps) {
848                         maybe_snap (i, new_position, nearest_distance);
849                 }
850
851                 for (auto i: _end_snaps) {
852                         maybe_snap (i, new_end, nearest_distance);
853                 }
854
855                 if (nearest_distance) {
856                         /* Snap if it's close; `close' means within a proportion of the time on the timeline */
857                         if (nearest_distance.get().abs() < DCPTime::from_seconds ((width() / pps) / 64)) {
858                                 new_position += nearest_distance.get ();
859                         }
860                 }
861         }
862
863         if (new_position < DCPTime ()) {
864                 new_position = DCPTime ();
865         }
866
867         _down_view->content()->set_position (film, new_position, force_emit);
868
869         film->set_sequence (false);
870 }
871
872
873 void
874 Timeline::force_redraw (dcpomatic::Rect<int> const & r)
875 {
876         _main_canvas->RefreshRect (wxRect (r.x, r.y, r.width, r.height), false);
877 }
878
879
880 shared_ptr<const Film>
881 Timeline::film () const
882 {
883         return _film.lock ();
884 }
885
886
887 void
888 Timeline::resized ()
889 {
890         if (_main_canvas->GetSize().GetWidth() > 0 && _first_resize) {
891                 zoom_all ();
892                 _first_resize = false;
893         }
894         setup_scrollbars ();
895 }
896
897
898 void
899 Timeline::clear_selection ()
900 {
901         for (auto i: _views) {
902                 shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView>(i);
903                 if (cv) {
904                         cv->set_selected (false);
905                 }
906         }
907 }
908
909
910 TimelineContentViewList
911 Timeline::selected_views () const
912 {
913         TimelineContentViewList sel;
914
915         for (auto i: _views) {
916                 auto cv = dynamic_pointer_cast<TimelineContentView>(i);
917                 if (cv && cv->selected()) {
918                         sel.push_back (cv);
919                 }
920         }
921
922         return sel;
923 }
924
925
926 ContentList
927 Timeline::selected_content () const
928 {
929         ContentList sel;
930
931         for (auto i: selected_views()) {
932                 sel.push_back(i->content());
933         }
934
935         return sel;
936 }
937
938
939 void
940 Timeline::set_selection (ContentList selection)
941 {
942         for (auto i: _views) {
943                 auto cv = dynamic_pointer_cast<TimelineContentView> (i);
944                 if (cv) {
945                         cv->set_selected (find (selection.begin(), selection.end(), cv->content ()) != selection.end ());
946                 }
947         }
948 }
949
950
951 int
952 Timeline::tracks_y_offset () const
953 {
954         return _reels_view->bbox().height + 4;
955 }
956
957
958 int
959 Timeline::width () const
960 {
961         return _main_canvas->GetVirtualSize().GetWidth();
962 }
963
964
965 void
966 Timeline::scrolled (wxScrollWinEvent& ev)
967 {
968         if (ev.GetOrientation() == wxVERTICAL) {
969                 int x, y;
970                 _main_canvas->GetViewStart (&x, &y);
971                 _labels_canvas->Scroll (0, y);
972         }
973         ev.Skip ();
974 }
975
976
977 void
978 Timeline::tool_clicked (Tool t)
979 {
980         switch (t) {
981         case ZOOM:
982         case SELECT:
983                 _tool = t;
984                 break;
985         case ZOOM_ALL:
986                 zoom_all ();
987                 break;
988         case SNAP:
989         case SEQUENCE:
990                 break;
991         }
992 }
993
994
995 void
996 Timeline::zoom_all ()
997 {
998         auto film = _film.lock ();
999         DCPOMATIC_ASSERT (film);
1000         set_pixels_per_second((_main_canvas->GetSize().GetWidth() - 32) / std::max(1.0, film->length().seconds()));
1001         set_pixels_per_track((_main_canvas->GetSize().GetHeight() - tracks_y_offset() - _time_axis_view->bbox().height - 32) / std::max(1, _tracks));
1002         setup_scrollbars ();
1003         _main_canvas->Scroll (0, 0);
1004         _labels_canvas->Scroll (0, 0);
1005         Refresh ();
1006 }
1007
1008
1009 void
1010 Timeline::keypress(wxKeyEvent const& event)
1011 {
1012         if (event.GetKeyCode() == WXK_DELETE) {
1013                 auto film = _film.lock();
1014                 DCPOMATIC_ASSERT(film);
1015                 film->remove_content(selected_content());
1016         } else {
1017                 switch (event.GetRawKeyCode()) {
1018                 case '+':
1019                         set_pixels_per_second(_pixels_per_second.get_value_or(1) * 2);
1020                         setup_scrollbars();
1021                         break;
1022                 case '-':
1023                         set_pixels_per_second(_pixels_per_second.get_value_or(1) / 2);
1024                         setup_scrollbars();
1025                         break;
1026                 }
1027         }
1028 }
1029