Fix rebase onto master.
[dcpomatic.git] / src / wx / timeline.cc
1 /*
2     Copyright (C) 2013-2016 Carl Hetherington <cth@carlh.net>
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 "film_editor.h"
21 #include "timeline.h"
22 #include "timeline_time_axis_view.h"
23 #include "timeline_reels_view.h"
24 #include "timeline_labels_view.h"
25 #include "timeline_video_content_view.h"
26 #include "timeline_audio_content_view.h"
27 #include "timeline_subtitle_content_view.h"
28 #include "content_panel.h"
29 #include "wx_util.h"
30 #include "lib/film.h"
31 #include "lib/playlist.h"
32 #include "lib/image_content.h"
33 #include "lib/timer.h"
34 #include "lib/audio_content.h"
35 #include "lib/subtitle_content.h"
36 #include "lib/video_content.h"
37 #include <wx/graphics.h>
38 #include <boost/weak_ptr.hpp>
39 #include <boost/foreach.hpp>
40 #include <list>
41 #include <iostream>
42
43 using std::list;
44 using std::cout;
45 using std::max;
46 using boost::shared_ptr;
47 using boost::weak_ptr;
48 using boost::dynamic_pointer_cast;
49 using boost::bind;
50 using boost::optional;
51
52 Timeline::Timeline (wxWindow* parent, ContentPanel* cp, shared_ptr<Film> film)
53         : wxPanel (parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE)
54         , _content_panel (cp)
55         , _film (film)
56         , _time_axis_view (new TimelineTimeAxisView (*this, 64))
57         , _reels_view (new TimelineReelsView (*this, 32))
58         , _labels_view (new TimelineLabelsView (*this))
59         , _tracks (0)
60         , _left_down (false)
61         , _down_view_position (0)
62         , _first_move (false)
63         , _menu (this)
64         , _snap (true)
65 {
66 #ifndef __WXOSX__
67         SetDoubleBuffered (true);
68 #endif
69
70         Bind (wxEVT_PAINT,      boost::bind (&Timeline::paint,       this));
71         Bind (wxEVT_LEFT_DOWN,  boost::bind (&Timeline::left_down,   this, _1));
72         Bind (wxEVT_LEFT_UP,    boost::bind (&Timeline::left_up,     this, _1));
73         Bind (wxEVT_RIGHT_DOWN, boost::bind (&Timeline::right_down,  this, _1));
74         Bind (wxEVT_MOTION,     boost::bind (&Timeline::mouse_moved, this, _1));
75         Bind (wxEVT_SIZE,       boost::bind (&Timeline::resized,     this));
76
77         film_changed (Film::CONTENT);
78
79         SetMinSize (wxSize (640, tracks() * track_height() + 96));
80
81         _tracks_position = Position<int> (_labels_view->bbox().width, 32);
82
83         _film_changed_connection = film->Changed.connect (bind (&Timeline::film_changed, this, _1));
84         _film_content_changed_connection = film->ContentChanged.connect (bind (&Timeline::film_content_changed, this, _2, _3));
85 }
86
87 void
88 Timeline::paint ()
89 {
90         wxPaintDC dc (this);
91
92         wxGraphicsContext* gc = wxGraphicsContext::Create (dc);
93         if (!gc) {
94                 return;
95         }
96
97         gc->SetAntialiasMode (wxANTIALIAS_DEFAULT);
98
99         BOOST_FOREACH (shared_ptr<TimelineView> i, _views) {
100
101                 shared_ptr<TimelineContentView> ic = dynamic_pointer_cast<TimelineContentView> (i);
102
103                 /* Find areas of overlap */
104                 list<dcpomatic::Rect<int> > overlaps;
105                 BOOST_FOREACH (shared_ptr<TimelineView> j, _views) {
106                         shared_ptr<TimelineContentView> jc = dynamic_pointer_cast<TimelineContentView> (j);
107                         /* No overlap with non-content views, views no different tracks, audio views or non-active views */
108                         if (!ic || !jc || i == j || ic->track() != jc->track() || ic->track().get_value_or(2) >= 2 || !ic->active() || !jc->active()) {
109                                 continue;
110                         }
111
112                         optional<dcpomatic::Rect<int> > r = j->bbox().intersection (i->bbox());
113                         if (r) {
114                                 overlaps.push_back (r.get ());
115                         }
116                 }
117
118                 i->paint (gc, overlaps);
119         }
120
121         delete gc;
122 }
123
124 void
125 Timeline::film_changed (Film::Property p)
126 {
127         if (p == Film::CONTENT || p == Film::REEL_TYPE || p == Film::REEL_LENGTH) {
128                 ensure_ui_thread ();
129                 recreate_views ();
130         } else if (p == Film::CONTENT_ORDER) {
131                 Refresh ();
132         }
133 }
134
135 void
136 Timeline::recreate_views ()
137 {
138         shared_ptr<const Film> film = _film.lock ();
139         if (!film) {
140                 return;
141         }
142
143         _views.clear ();
144         _views.push_back (_time_axis_view);
145         _views.push_back (_reels_view);
146         _views.push_back (_labels_view);
147
148         BOOST_FOREACH (shared_ptr<Content> i, film->content ()) {
149                 if (i->video) {
150                         _views.push_back (shared_ptr<TimelineView> (new TimelineVideoContentView (*this, i)));
151                 }
152
153                 if (i->audio && !i->audio->mapping().mapped_output_channels().empty ()) {
154                         _views.push_back (shared_ptr<TimelineView> (new TimelineAudioContentView (*this, i)));
155                 }
156
157                 if (i->subtitle) {
158                         _views.push_back (shared_ptr<TimelineView> (new TimelineSubtitleContentView (*this, i)));
159                 }
160         }
161
162         assign_tracks ();
163         setup_pixels_per_second ();
164         Refresh ();
165 }
166
167 void
168 Timeline::film_content_changed (int property, bool frequent)
169 {
170         ensure_ui_thread ();
171
172         if (property == AudioContentProperty::STREAMS) {
173                 recreate_views ();
174         } else if (!frequent) {
175                 setup_pixels_per_second ();
176                 Refresh ();
177         }
178 }
179
180 void
181 Timeline::assign_tracks ()
182 {
183         for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
184                 shared_ptr<TimelineContentView> c = dynamic_pointer_cast<TimelineContentView> (*i);
185                 if (c) {
186                         c->unset_track ();
187                 }
188         }
189
190         for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
191                 shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
192                 if (!cv) {
193                         continue;
194                 }
195
196                 if (dynamic_pointer_cast<TimelineVideoContentView> (*i)) {
197                         /* Video on tracks 0 and 1 (left and right eye) */
198                         cv->set_track (cv->content()->video->frame_type() == VIDEO_FRAME_TYPE_3D_RIGHT ? 1 : 0);
199                         _tracks = max (_tracks, 2);
200                         continue;
201                 } else if (dynamic_pointer_cast<TimelineSubtitleContentView> (*i)) {
202                         /* Subtitles on track 2 */
203                         cv->set_track (2);
204                         _tracks = max (_tracks, 3);
205                         continue;
206                 }
207
208                 /* Audio on tracks 3 and up */
209                 int t = 3;
210
211                 shared_ptr<Content> content = cv->content();
212                 DCPTimePeriod content_period (content->position(), content->end());
213
214                 while (true) {
215                         TimelineViewList::iterator j = _views.begin();
216                         while (j != _views.end()) {
217                                 shared_ptr<TimelineAudioContentView> test = dynamic_pointer_cast<TimelineAudioContentView> (*j);
218                                 if (!test) {
219                                         ++j;
220                                         continue;
221                                 }
222
223                                 shared_ptr<Content> test_content = test->content();
224
225                                 if (test && test->track() && test->track().get() == t) {
226                                         if (content_period.overlaps (DCPTimePeriod(test_content->position(), test_content->end()))) {
227                                                 /* we have an overlap on track `t' */
228                                                 ++t;
229                                                 break;
230                                         }
231                                 }
232
233                                 ++j;
234                         }
235
236                         if (j == _views.end ()) {
237                                 /* no overlap on `t' */
238                                 break;
239                         }
240                 }
241
242                 cv->set_track (t);
243                 _tracks = max (_tracks, t + 1);
244         }
245
246         _time_axis_view->set_y (tracks() * track_height() + 64);
247         _reels_view->set_y (8);
248 }
249
250 int
251 Timeline::tracks () const
252 {
253         return _tracks;
254 }
255
256 void
257 Timeline::setup_pixels_per_second ()
258 {
259         shared_ptr<const Film> film = _film.lock ();
260         if (!film || film->length() == DCPTime ()) {
261                 return;
262         }
263
264         _pixels_per_second = static_cast<double>(width() - tracks_position().x * 2) / film->length().seconds ();
265 }
266
267 shared_ptr<TimelineView>
268 Timeline::event_to_view (wxMouseEvent& ev)
269 {
270         /* Search backwards through views so that we find the uppermost one first */
271         TimelineViewList::reverse_iterator i = _views.rbegin();
272         Position<int> const p (ev.GetX(), ev.GetY());
273         while (i != _views.rend() && !(*i)->bbox().contains (p)) {
274                 shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
275                 ++i;
276         }
277
278         if (i == _views.rend ()) {
279                 return shared_ptr<TimelineView> ();
280         }
281
282         return *i;
283 }
284
285 void
286 Timeline::left_down (wxMouseEvent& ev)
287 {
288         shared_ptr<TimelineView> view = event_to_view (ev);
289         shared_ptr<TimelineContentView> content_view = dynamic_pointer_cast<TimelineContentView> (view);
290
291         _down_view.reset ();
292
293         if (content_view) {
294                 _down_view = content_view;
295                 _down_view_position = content_view->content()->position ();
296         }
297
298         for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
299                 shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
300                 if (!cv) {
301                         continue;
302                 }
303
304                 if (!ev.ShiftDown ()) {
305                         cv->set_selected (view == *i);
306                 }
307         }
308
309         if (content_view && ev.ShiftDown ()) {
310                 content_view->set_selected (!content_view->selected ());
311         }
312
313         _left_down = true;
314         _down_point = ev.GetPosition ();
315         _first_move = false;
316
317         if (_down_view) {
318                 /* Pre-compute the points that we might snap to */
319                 for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
320                         shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
321                         if (!cv || cv == _down_view || cv->content() == _down_view->content()) {
322                                 continue;
323                         }
324
325                         _start_snaps.push_back (cv->content()->position());
326                         _end_snaps.push_back (cv->content()->position());
327                         _start_snaps.push_back (cv->content()->end());
328                         _end_snaps.push_back (cv->content()->end());
329
330                         BOOST_FOREACH (DCPTime i, cv->content()->reel_split_points()) {
331                                 _start_snaps.push_back (i);
332                         }
333                 }
334
335                 /* Tell everyone that things might change frequently during the drag */
336                 _down_view->content()->set_change_signals_frequent (true);
337         }
338 }
339
340 void
341 Timeline::left_up (wxMouseEvent& ev)
342 {
343         _left_down = false;
344
345         if (_down_view) {
346                 _down_view->content()->set_change_signals_frequent (false);
347                 _content_panel->set_selection (_down_view->content ());
348         }
349
350         set_position_from_event (ev);
351
352         /* Clear up up the stuff we don't do during drag */
353         assign_tracks ();
354         setup_pixels_per_second ();
355         Refresh ();
356
357         _start_snaps.clear ();
358         _end_snaps.clear ();
359 }
360
361 void
362 Timeline::mouse_moved (wxMouseEvent& ev)
363 {
364         if (!_left_down) {
365                 return;
366         }
367
368         set_position_from_event (ev);
369 }
370
371 void
372 Timeline::right_down (wxMouseEvent& ev)
373 {
374         shared_ptr<TimelineView> view = event_to_view (ev);
375         shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (view);
376         if (!cv) {
377                 return;
378         }
379
380         if (!cv->selected ()) {
381                 clear_selection ();
382                 cv->set_selected (true);
383         }
384
385         _menu.popup (_film, selected_content (), selected_views (), ev.GetPosition ());
386 }
387
388 void
389 Timeline::maybe_snap (DCPTime a, DCPTime b, optional<DCPTime>& nearest_distance) const
390 {
391         DCPTime const d = a - b;
392         if (!nearest_distance || d.abs() < nearest_distance.get().abs()) {
393                 nearest_distance = d;
394         }
395 }
396
397 void
398 Timeline::set_position_from_event (wxMouseEvent& ev)
399 {
400         if (!_pixels_per_second) {
401                 return;
402         }
403
404         double const pps = _pixels_per_second.get ();
405
406         wxPoint const p = ev.GetPosition();
407
408         if (!_first_move) {
409                 /* We haven't moved yet; in that case, we must move the mouse some reasonable distance
410                    before the drag is considered to have started.
411                 */
412                 int const dist = sqrt (pow (p.x - _down_point.x, 2) + pow (p.y - _down_point.y, 2));
413                 if (dist < 8) {
414                         return;
415                 }
416                 _first_move = true;
417         }
418
419         if (!_down_view) {
420                 return;
421         }
422
423         DCPTime new_position = _down_view_position + DCPTime::from_seconds ((p.x - _down_point.x) / pps);
424
425         if (_snap) {
426
427                 DCPTime const new_end = new_position + _down_view->content()->length_after_trim();
428                 /* Signed `distance' to nearest thing (i.e. negative is left on the timeline,
429                    positive is right).
430                 */
431                 optional<DCPTime> nearest_distance;
432
433                 /* Find the nearest snap point */
434
435                 BOOST_FOREACH (DCPTime i, _start_snaps) {
436                         maybe_snap (i, new_position, nearest_distance);
437                 }
438
439                 BOOST_FOREACH (DCPTime i, _end_snaps) {
440                         maybe_snap (i, new_end, nearest_distance);
441                 }
442
443                 if (nearest_distance) {
444                         /* Snap if it's close; `close' means within a proportion of the time on the timeline */
445                         if (nearest_distance.get().abs() < DCPTime::from_seconds ((width() / pps) / 64)) {
446                                 new_position += nearest_distance.get ();
447                         }
448                 }
449         }
450
451         if (new_position < DCPTime ()) {
452                 new_position = DCPTime ();
453         }
454
455         _down_view->content()->set_position (new_position);
456
457         shared_ptr<Film> film = _film.lock ();
458         DCPOMATIC_ASSERT (film);
459         film->set_sequence (false);
460 }
461
462 void
463 Timeline::force_redraw (dcpomatic::Rect<int> const & r)
464 {
465         RefreshRect (wxRect (r.x, r.y, r.width, r.height), false);
466 }
467
468 shared_ptr<const Film>
469 Timeline::film () const
470 {
471         return _film.lock ();
472 }
473
474 void
475 Timeline::resized ()
476 {
477         setup_pixels_per_second ();
478 }
479
480 void
481 Timeline::clear_selection ()
482 {
483         for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
484                 shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
485                 if (cv) {
486                         cv->set_selected (false);
487                 }
488         }
489 }
490
491 TimelineContentViewList
492 Timeline::selected_views () const
493 {
494         TimelineContentViewList sel;
495
496         for (TimelineViewList::const_iterator i = _views.begin(); i != _views.end(); ++i) {
497                 shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
498                 if (cv && cv->selected()) {
499                         sel.push_back (cv);
500                 }
501         }
502
503         return sel;
504 }
505
506 ContentList
507 Timeline::selected_content () const
508 {
509         ContentList sel;
510         TimelineContentViewList views = selected_views ();
511
512         for (TimelineContentViewList::const_iterator i = views.begin(); i != views.end(); ++i) {
513                 sel.push_back ((*i)->content ());
514         }
515
516         return sel;
517 }
518
519 void
520 Timeline::set_selection (ContentList selection)
521 {
522         for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
523                 shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
524                 if (cv) {
525                         cv->set_selected (find (selection.begin(), selection.end(), cv->content ()) != selection.end ());
526                 }
527         }
528 }