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