An unfortunately large set of timeline-related changes:
[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         for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
93                 (*i)->paint (gc);
94         }
95
96         delete gc;
97 }
98
99 void
100 Timeline::film_changed (Film::Property p)
101 {
102         if (p == Film::CONTENT || p == Film::REEL_TYPE || p == Film::REEL_LENGTH) {
103                 ensure_ui_thread ();
104                 recreate_views ();
105         } else if (p == Film::CONTENT_ORDER) {
106                 Refresh ();
107         }
108 }
109
110 void
111 Timeline::recreate_views ()
112 {
113         shared_ptr<const Film> film = _film.lock ();
114         if (!film) {
115                 return;
116         }
117
118         _views.clear ();
119         _views.push_back (_time_axis_view);
120         _views.push_back (_reels_view);
121
122         BOOST_FOREACH (shared_ptr<Content> i, film->content ()) {
123                 if (dynamic_pointer_cast<VideoContent> (i)) {
124                         _views.push_back (shared_ptr<TimelineView> (new TimelineVideoContentView (*this, i)));
125                 }
126
127                 shared_ptr<AudioContent> ac = dynamic_pointer_cast<AudioContent> (i);
128                 if (ac && !ac->audio_mapping().mapped_output_channels().empty ()) {
129                         _views.push_back (shared_ptr<TimelineView> (new TimelineAudioContentView (*this, i)));
130                 }
131
132                 shared_ptr<SubtitleContent> sc = dynamic_pointer_cast<SubtitleContent> (i);
133                 if (sc && sc->has_subtitles ()) {
134                         _views.push_back (shared_ptr<TimelineView> (new TimelineSubtitleContentView (*this, sc)));
135                 }
136         }
137
138         assign_tracks ();
139         setup_pixels_per_second ();
140         Refresh ();
141 }
142
143 void
144 Timeline::film_content_changed (int property, bool frequent)
145 {
146         ensure_ui_thread ();
147
148         if (property == AudioContentProperty::AUDIO_STREAMS) {
149                 recreate_views ();
150         } else if (!frequent) {
151                 setup_pixels_per_second ();
152                 Refresh ();
153         }
154 }
155
156 void
157 Timeline::assign_tracks ()
158 {
159         for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
160                 shared_ptr<TimelineContentView> c = dynamic_pointer_cast<TimelineContentView> (*i);
161                 if (c) {
162                         c->unset_track ();
163                 }
164         }
165
166         for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
167                 shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
168                 if (!cv) {
169                         continue;
170                 }
171
172                 shared_ptr<Content> content = cv->content();
173                 DCPTimePeriod content_period (content->position(), content->end());
174
175                 int t = 0;
176                 while (true) {
177                         TimelineViewList::iterator j = _views.begin();
178                         while (j != _views.end()) {
179                                 shared_ptr<TimelineContentView> test = dynamic_pointer_cast<TimelineContentView> (*j);
180                                 if (!test) {
181                                         ++j;
182                                         continue;
183                                 }
184
185                                 shared_ptr<Content> test_content = test->content();
186
187                                 if (test && test->track() && test->track().get() == t) {
188                                         if (content_period.overlaps (DCPTimePeriod(test_content->position(), test_content->end()))) {
189                                                 /* we have an overlap on track `t' */
190                                                 ++t;
191                                                 break;
192                                         }
193                                 }
194
195                                 ++j;
196                         }
197
198                         if (j == _views.end ()) {
199                                 /* no overlap on `t' */
200                                 break;
201                         }
202                 }
203
204                 cv->set_track (t);
205                 _tracks = max (_tracks, t + 1);
206         }
207
208         _time_axis_view->set_y (tracks() * track_height() + 64);
209         _reels_view->set_y (tracks() * track_height() + 32);
210 }
211
212 int
213 Timeline::tracks () const
214 {
215         return _tracks;
216 }
217
218 void
219 Timeline::setup_pixels_per_second ()
220 {
221         shared_ptr<const Film> film = _film.lock ();
222         if (!film || film->length() == DCPTime ()) {
223                 return;
224         }
225
226         _pixels_per_second = static_cast<double>(width() - x_offset() * 2) / film->length().seconds ();
227 }
228
229 shared_ptr<TimelineView>
230 Timeline::event_to_view (wxMouseEvent& ev)
231 {
232         TimelineViewList::iterator i = _views.begin();
233         Position<int> const p (ev.GetX(), ev.GetY());
234         while (i != _views.end() && !(*i)->bbox().contains (p)) {
235                 ++i;
236         }
237
238         if (i == _views.end ()) {
239                 return shared_ptr<TimelineView> ();
240         }
241
242         return *i;
243 }
244
245 void
246 Timeline::left_down (wxMouseEvent& ev)
247 {
248         shared_ptr<TimelineView> view = event_to_view (ev);
249         shared_ptr<TimelineContentView> content_view = dynamic_pointer_cast<TimelineContentView> (view);
250
251         _down_view.reset ();
252
253         if (content_view) {
254                 _down_view = content_view;
255                 _down_view_position = content_view->content()->position ();
256         }
257
258         for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
259                 shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
260                 if (!cv) {
261                         continue;
262                 }
263
264                 if (!ev.ShiftDown ()) {
265                         cv->set_selected (view == *i);
266                 }
267         }
268
269         if (content_view && ev.ShiftDown ()) {
270                 content_view->set_selected (!content_view->selected ());
271         }
272
273         _left_down = true;
274         _down_point = ev.GetPosition ();
275         _first_move = false;
276
277         if (_down_view) {
278                 /* Pre-compute the points that we might snap to */
279                 for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
280                         shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
281                         if (!cv || cv == _down_view || cv->content() == _down_view->content()) {
282                                 continue;
283                         }
284
285                         _start_snaps.push_back (cv->content()->position());
286                         _end_snaps.push_back (cv->content()->position());
287                         _start_snaps.push_back (cv->content()->end());
288                         _end_snaps.push_back (cv->content()->end());
289
290                         BOOST_FOREACH (DCPTime i, cv->content()->reel_split_points()) {
291                                 _start_snaps.push_back (i);
292                         }
293                 }
294
295                 /* Tell everyone that things might change frequently during the drag */
296                 _down_view->content()->set_change_signals_frequent (true);
297         }
298 }
299
300 void
301 Timeline::left_up (wxMouseEvent& ev)
302 {
303         _left_down = false;
304
305         if (_down_view) {
306                 _down_view->content()->set_change_signals_frequent (false);
307                 _content_panel->set_selection (_down_view->content ());
308         }
309
310         set_position_from_event (ev);
311
312         /* Clear up up the stuff we don't do during drag */
313         assign_tracks ();
314         setup_pixels_per_second ();
315         Refresh ();
316
317         _start_snaps.clear ();
318         _end_snaps.clear ();
319 }
320
321 void
322 Timeline::mouse_moved (wxMouseEvent& ev)
323 {
324         if (!_left_down) {
325                 return;
326         }
327
328         set_position_from_event (ev);
329 }
330
331 void
332 Timeline::right_down (wxMouseEvent& ev)
333 {
334         shared_ptr<TimelineView> view = event_to_view (ev);
335         shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (view);
336         if (!cv) {
337                 return;
338         }
339
340         if (!cv->selected ()) {
341                 clear_selection ();
342                 cv->set_selected (true);
343         }
344
345         _menu.popup (_film, selected_content (), selected_views (), ev.GetPosition ());
346 }
347
348 void
349 Timeline::maybe_snap (DCPTime a, DCPTime b, optional<DCPTime>& nearest_distance) const
350 {
351         DCPTime const d = a - b;
352         if (!nearest_distance || d.abs() < nearest_distance.get().abs()) {
353                 nearest_distance = d;
354         }
355 }
356
357 void
358 Timeline::set_position_from_event (wxMouseEvent& ev)
359 {
360         if (!_pixels_per_second) {
361                 return;
362         }
363
364         double const pps = _pixels_per_second.get ();
365
366         wxPoint const p = ev.GetPosition();
367
368         if (!_first_move) {
369                 /* We haven't moved yet; in that case, we must move the mouse some reasonable distance
370                    before the drag is considered to have started.
371                 */
372                 int const dist = sqrt (pow (p.x - _down_point.x, 2) + pow (p.y - _down_point.y, 2));
373                 if (dist < 8) {
374                         return;
375                 }
376                 _first_move = true;
377         }
378
379         if (!_down_view) {
380                 return;
381         }
382
383         DCPTime new_position = _down_view_position + DCPTime::from_seconds ((p.x - _down_point.x) / pps);
384
385         if (_snap) {
386
387                 DCPTime const new_end = new_position + _down_view->content()->length_after_trim();
388                 /* Signed `distance' to nearest thing (i.e. negative is left on the timeline,
389                    positive is right).
390                 */
391                 optional<DCPTime> nearest_distance;
392
393                 /* Find the nearest snap point */
394
395                 BOOST_FOREACH (DCPTime i, _start_snaps) {
396                         maybe_snap (i, new_position, nearest_distance);
397                 }
398
399                 BOOST_FOREACH (DCPTime i, _end_snaps) {
400                         maybe_snap (i, new_end, nearest_distance);
401                 }
402
403                 if (nearest_distance) {
404                         /* Snap if it's close; `close' means within a proportion of the time on the timeline */
405                         if (nearest_distance.get().abs() < DCPTime::from_seconds ((width() / pps) / 64)) {
406                                 new_position += nearest_distance.get ();
407                         }
408                 }
409         }
410
411         if (new_position < DCPTime ()) {
412                 new_position = DCPTime ();
413         }
414
415         _down_view->content()->set_position (new_position);
416
417         shared_ptr<Film> film = _film.lock ();
418         DCPOMATIC_ASSERT (film);
419         film->set_sequence (false);
420 }
421
422 void
423 Timeline::force_redraw (dcpomatic::Rect<int> const & r)
424 {
425         RefreshRect (wxRect (r.x, r.y, r.width, r.height), false);
426 }
427
428 shared_ptr<const Film>
429 Timeline::film () const
430 {
431         return _film.lock ();
432 }
433
434 void
435 Timeline::resized ()
436 {
437         setup_pixels_per_second ();
438 }
439
440 void
441 Timeline::clear_selection ()
442 {
443         for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
444                 shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
445                 if (cv) {
446                         cv->set_selected (false);
447                 }
448         }
449 }
450
451 TimelineContentViewList
452 Timeline::selected_views () const
453 {
454         TimelineContentViewList sel;
455
456         for (TimelineViewList::const_iterator i = _views.begin(); i != _views.end(); ++i) {
457                 shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
458                 if (cv && cv->selected()) {
459                         sel.push_back (cv);
460                 }
461         }
462
463         return sel;
464 }
465
466 ContentList
467 Timeline::selected_content () const
468 {
469         ContentList sel;
470         TimelineContentViewList views = selected_views ();
471
472         for (TimelineContentViewList::const_iterator i = views.begin(); i != views.end(); ++i) {
473                 sel.push_back ((*i)->content ());
474         }
475
476         return sel;
477 }
478
479 void
480 Timeline::set_selection (ContentList selection)
481 {
482         for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
483                 shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
484                 if (cv) {
485                         cv->set_selected (find (selection.begin(), selection.end(), cv->content ()) != selection.end ());
486                 }
487         }
488 }