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