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