Hand-apply a2f81da6d9afc5d3b5e647e1e05ca5d4507af42c from master;
[dcpomatic.git] / src / wx / timeline.cc
1 /*
2     Copyright (C) 2013-2015 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 <list>
21 #include <wx/graphics.h>
22 #include <boost/weak_ptr.hpp>
23 #include "lib/film.h"
24 #include "lib/playlist.h"
25 #include "lib/image_content.h"
26 #include "film_editor.h"
27 #include "timeline.h"
28 #include "timeline_time_axis_view.h"
29 #include "timeline_video_content_view.h"
30 #include "timeline_audio_content_view.h"
31 #include "timeline_subtitle_content_view.h"
32 #include "content_panel.h"
33 #include "wx_util.h"
34
35 using std::list;
36 using std::cout;
37 using std::max;
38 using boost::shared_ptr;
39 using boost::weak_ptr;
40 using boost::dynamic_pointer_cast;
41 using boost::bind;
42 using boost::optional;
43
44 Timeline::Timeline (wxWindow* parent, ContentPanel* cp, shared_ptr<Film> film)
45         : wxPanel (parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE)
46         , _content_panel (cp)
47         , _film (film)
48         , _time_axis_view (new TimelineTimeAxisView (*this, 32))
49         , _tracks (0)
50         , _left_down (false)
51         , _down_view_position (0)
52         , _first_move (false)
53         , _menu (this)
54         , _snap (true)
55 {
56 #ifndef __WXOSX__
57         SetDoubleBuffered (true);
58 #endif  
59
60         Bind (wxEVT_PAINT,      boost::bind (&Timeline::paint,       this));
61         Bind (wxEVT_LEFT_DOWN,  boost::bind (&Timeline::left_down,   this, _1));
62         Bind (wxEVT_LEFT_UP,    boost::bind (&Timeline::left_up,     this, _1));
63         Bind (wxEVT_RIGHT_DOWN, boost::bind (&Timeline::right_down,  this, _1));
64         Bind (wxEVT_MOTION,     boost::bind (&Timeline::mouse_moved, this, _1));
65         Bind (wxEVT_SIZE,       boost::bind (&Timeline::resized,     this));
66
67         playlist_changed ();
68
69         SetMinSize (wxSize (640, tracks() * track_height() + 96));
70
71         _playlist_changed_connection = film->playlist()->Changed.connect (bind (&Timeline::playlist_changed, this));
72         _playlist_content_changed_connection = film->playlist()->ContentChanged.connect (bind (&Timeline::playlist_content_changed, this, _2));
73 }
74
75 void
76 Timeline::paint ()
77 {
78         wxPaintDC dc (this);
79
80         wxGraphicsContext* gc = wxGraphicsContext::Create (dc);
81         if (!gc) {
82                 return;
83         }
84
85         for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
86                 (*i)->paint (gc);
87         }
88
89         delete gc;
90 }
91
92 void
93 Timeline::playlist_changed ()
94 {
95         ensure_ui_thread ();
96         recreate_views ();
97 }
98
99 void
100 Timeline::recreate_views ()
101 {
102         shared_ptr<const Film> fl = _film.lock ();
103         if (!fl) {
104                 return;
105         }
106
107         _views.clear ();
108         _views.push_back (_time_axis_view);
109
110         ContentList content = fl->playlist()->content ();
111
112         for (ContentList::iterator i = content.begin(); i != content.end(); ++i) {
113                 if (dynamic_pointer_cast<VideoContent> (*i)) {
114                         _views.push_back (shared_ptr<TimelineView> (new TimelineVideoContentView (*this, *i)));
115                 }
116
117                 shared_ptr<AudioContent> ac = dynamic_pointer_cast<AudioContent> (*i);
118                 if (ac && !ac->audio_mapping().mapped_dcp_channels().empty ()) {
119                         _views.push_back (shared_ptr<TimelineView> (new TimelineAudioContentView (*this, *i)));
120                 }
121
122                 shared_ptr<SubtitleContent> sc = dynamic_pointer_cast<SubtitleContent> (*i);
123                 if (sc && sc->has_subtitles ()) {
124                         _views.push_back (shared_ptr<TimelineView> (new TimelineSubtitleContentView (*this, sc)));
125                 }
126         }
127
128         assign_tracks ();
129         setup_pixels_per_second ();
130         Refresh ();
131 }
132
133 void
134 Timeline::playlist_content_changed (int property)
135 {
136         ensure_ui_thread ();
137
138         if (property == ContentProperty::POSITION) {
139                 assign_tracks ();
140                 setup_pixels_per_second ();
141                 Refresh ();
142         } else if (property == AudioContentProperty::AUDIO_MAPPING) {
143                 recreate_views ();
144         }
145 }
146
147 void
148 Timeline::assign_tracks ()
149 {
150         for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
151                 shared_ptr<TimelineContentView> c = dynamic_pointer_cast<TimelineContentView> (*i);
152                 if (c) {
153                         c->unset_track ();
154                 }
155         }
156
157         for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
158                 shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
159                 if (!cv) {
160                         continue;
161                 }
162
163                 shared_ptr<Content> content = cv->content();
164
165                 int t = 0;
166                 while (true) {
167                         TimelineViewList::iterator j = _views.begin();
168                         while (j != _views.end()) {
169                                 shared_ptr<TimelineContentView> test = dynamic_pointer_cast<TimelineContentView> (*j);
170                                 if (!test) {
171                                         ++j;
172                                         continue;
173                                 }
174                                 
175                                 shared_ptr<Content> test_content = test->content();
176                                         
177                                 if (test && test->track() && test->track().get() == t) {
178                                         bool const no_overlap =
179                                                 (content->position() < test_content->position() && content->end() < test_content->position()) ||
180                                                 (content->position() > test_content->end()      && content->end() > test_content->end());
181                                         
182                                         if (!no_overlap) {
183                                                 /* we have an overlap on track `t' */
184                                                 ++t;
185                                                 break;
186                                         }
187                                 }
188                                 
189                                 ++j;
190                         }
191
192                         if (j == _views.end ()) {
193                                 /* no overlap on `t' */
194                                 break;
195                         }
196                 }
197
198                 cv->set_track (t);
199                 _tracks = max (_tracks, t + 1);
200         }
201
202         _time_axis_view->set_y (tracks() * track_height() + 32);
203 }
204
205 int
206 Timeline::tracks () const
207 {
208         return _tracks;
209 }
210
211 void
212 Timeline::setup_pixels_per_second ()
213 {
214         shared_ptr<const Film> film = _film.lock ();
215         if (!film || film->length() == DCPTime ()) {
216                 return;
217         }
218
219         _pixels_per_second = static_cast<double>(width() - x_offset() * 2) / film->length().seconds ();
220 }
221
222 shared_ptr<TimelineView>
223 Timeline::event_to_view (wxMouseEvent& ev)
224 {
225         TimelineViewList::iterator i = _views.begin();
226         Position<int> const p (ev.GetX(), ev.GetY());
227         while (i != _views.end() && !(*i)->bbox().contains (p)) {
228                 ++i;
229         }
230
231         if (i == _views.end ()) {
232                 return shared_ptr<TimelineView> ();
233         }
234
235         return *i;
236 }
237
238 void
239 Timeline::left_down (wxMouseEvent& ev)
240 {
241         shared_ptr<TimelineView> view = event_to_view (ev);
242         shared_ptr<TimelineContentView> content_view = dynamic_pointer_cast<TimelineContentView> (view);
243
244         _down_view.reset ();
245
246         if (content_view) {
247                 _down_view = content_view;
248                 _down_view_position = content_view->content()->position ();
249         }
250
251         for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
252                 shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
253                 if (!cv) {
254                         continue;
255                 }
256                 
257                 if (!ev.ShiftDown ()) {
258                         cv->set_selected (view == *i);
259                 }
260                 
261                 if (view == *i) {
262                         _content_panel->set_selection (cv->content ());
263                 }
264         }
265
266         if (content_view && ev.ShiftDown ()) {
267                 content_view->set_selected (!content_view->selected ());
268         }
269
270         _left_down = true;
271         _down_point = ev.GetPosition ();
272         _first_move = false;
273
274         if (_down_view) {
275                 _down_view->content()->set_change_signals_frequent (true);
276         }
277 }
278
279 void
280 Timeline::left_up (wxMouseEvent& ev)
281 {
282         _left_down = false;
283
284         if (_down_view) {
285                 _down_view->content()->set_change_signals_frequent (false);
286         }
287
288         set_position_from_event (ev);
289 }
290
291 void
292 Timeline::mouse_moved (wxMouseEvent& ev)
293 {
294         if (!_left_down) {
295                 return;
296         }
297
298         set_position_from_event (ev);
299 }
300
301 void
302 Timeline::right_down (wxMouseEvent& ev)
303 {
304         shared_ptr<TimelineView> view = event_to_view (ev);
305         shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (view);
306         if (!cv) {
307                 return;
308         }
309
310         if (!cv->selected ()) {
311                 clear_selection ();
312                 cv->set_selected (true);
313         }
314
315         _menu.popup (_film, selected_content (), selected_views (), ev.GetPosition ());
316 }
317
318 void
319 Timeline::maybe_snap (DCPTime a, DCPTime b, optional<DCPTime>& nearest_distance) const
320 {
321         DCPTime const d = a - b;
322         if (!nearest_distance || d.abs() < nearest_distance.get().abs()) {
323                 nearest_distance = d;
324         }
325 }
326
327 void
328 Timeline::set_position_from_event (wxMouseEvent& ev)
329 {
330         if (!_pixels_per_second) {
331                 return;
332         }
333
334         double const pps = _pixels_per_second.get ();
335
336         wxPoint const p = ev.GetPosition();
337
338         if (!_first_move) {
339                 /* We haven't moved yet; in that case, we must move the mouse some reasonable distance
340                    before the drag is considered to have started.
341                 */
342                 int const dist = sqrt (pow (p.x - _down_point.x, 2) + pow (p.y - _down_point.y, 2));
343                 if (dist < 8) {
344                         return;
345                 }
346                 _first_move = true;
347         }
348
349         if (!_down_view) {
350                 return;
351         }
352         
353         DCPTime new_position = _down_view_position + DCPTime::from_seconds ((p.x - _down_point.x) / pps);
354         
355         if (_snap) {
356
357                 DCPTime const new_end = new_position + _down_view->content()->length_after_trim ();
358                 /* Signed `distance' to nearest thing (i.e. negative is left on the timeline,
359                    positive is right).
360                 */
361                 optional<DCPTime> nearest_distance;
362                 
363                 /* Find the nearest content edge; this is inefficient */
364                 for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
365                         shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
366                         if (!cv || cv == _down_view) {
367                                 continue;
368                         }
369
370                         maybe_snap (cv->content()->position(), new_position, nearest_distance);
371                         maybe_snap (cv->content()->position(), new_end, nearest_distance);
372                         maybe_snap (cv->content()->end(), new_position, nearest_distance);
373                         maybe_snap (cv->content()->end(), new_end, nearest_distance);
374                 }
375                 
376                 if (nearest_distance) {
377                         /* Snap if it's close; `close' means within a proportion of the time on the timeline */
378                         if (nearest_distance.get().abs() < DCPTime::from_seconds ((width() / pps) / 64)) {
379                                 new_position += nearest_distance.get ();
380                         }
381                 }
382         }
383         
384         if (new_position < DCPTime ()) {
385                 new_position = DCPTime ();
386         }
387
388         _down_view->content()->set_position (new_position);
389         
390         shared_ptr<Film> film = _film.lock ();
391         DCPOMATIC_ASSERT (film);
392         film->set_sequence_video (false);
393 }
394
395 void
396 Timeline::force_redraw (dcpomatic::Rect<int> const & r)
397 {
398         RefreshRect (wxRect (r.x, r.y, r.width, r.height), false);
399 }
400
401 shared_ptr<const Film>
402 Timeline::film () const
403 {
404         return _film.lock ();
405 }
406
407 void
408 Timeline::resized ()
409 {
410         setup_pixels_per_second ();
411 }
412
413 void
414 Timeline::clear_selection ()
415 {
416         for (TimelineViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
417                 shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
418                 if (cv) {
419                         cv->set_selected (false);
420                 }
421         }
422 }
423
424 TimelineContentViewList
425 Timeline::selected_views () const
426 {
427         TimelineContentViewList sel;
428         
429         for (TimelineViewList::const_iterator i = _views.begin(); i != _views.end(); ++i) {
430                 shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView> (*i);
431                 if (cv && cv->selected()) {
432                         sel.push_back (cv);
433                 }
434         }
435
436         return sel;
437 }
438
439 ContentList
440 Timeline::selected_content () const
441 {
442         ContentList sel;
443         TimelineContentViewList views = selected_views ();
444         
445         for (TimelineContentViewList::const_iterator i = views.begin(); i != views.end(); ++i) {
446                 sel.push_back ((*i)->content ());
447         }
448
449         return sel;
450 }