Merge master.
[dcpomatic.git] / src / wx / timeline.cc
1 /*
2     Copyright (C) 2013-2014 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 "film_editor.h"
26 #include "timeline.h"
27 #include "wx_util.h"
28
29 using std::list;
30 using std::cout;
31 using std::max;
32 using boost::shared_ptr;
33 using boost::weak_ptr;
34 using boost::dynamic_pointer_cast;
35 using boost::bind;
36 using boost::optional;
37
38 /** Parent class for components of the timeline (e.g. a piece of content or an axis) */
39 class View : public boost::noncopyable
40 {
41 public:
42         View (Timeline& t)
43                 : _timeline (t)
44         {
45
46         }
47
48         virtual ~View () {}
49                 
50         void paint (wxGraphicsContext* g)
51         {
52                 _last_paint_bbox = bbox ();
53                 do_paint (g);
54         }
55         
56         void force_redraw ()
57         {
58                 _timeline.force_redraw (_last_paint_bbox);
59                 _timeline.force_redraw (bbox ());
60         }
61
62         virtual dcpomatic::Rect<int> bbox () const = 0;
63
64 protected:
65         virtual void do_paint (wxGraphicsContext *) = 0;
66         
67         int time_x (DCPTime t) const
68         {
69                 return _timeline.tracks_position().x + t.seconds() * _timeline.pixels_per_second ();
70         }
71         
72         Timeline& _timeline;
73
74 private:
75         dcpomatic::Rect<int> _last_paint_bbox;
76 };
77
78
79 /** Parent class for views of pieces of content */
80 class ContentView : public View
81 {
82 public:
83         ContentView (Timeline& tl, shared_ptr<Content> c)
84                 : View (tl)
85                 , _content (c)
86                 , _selected (false)
87         {
88                 _content_connection = c->Changed.connect (bind (&ContentView::content_changed, this, _2, _3));
89         }
90
91         dcpomatic::Rect<int> bbox () const
92         {
93                 assert (_track);
94
95                 shared_ptr<const Film> film = _timeline.film ();
96                 shared_ptr<const Content> content = _content.lock ();
97                 if (!film || !content) {
98                         return dcpomatic::Rect<int> ();
99                 }
100                 
101                 return dcpomatic::Rect<int> (
102                         time_x (content->position ()) - 8,
103                         y_pos (_track.get()) - 8,
104                         content->length_after_trim().seconds() * _timeline.pixels_per_second() + 16,
105                         _timeline.track_height() + 16
106                         );
107         }
108
109         void set_selected (bool s) {
110                 _selected = s;
111                 force_redraw ();
112         }
113         
114         bool selected () const {
115                 return _selected;
116         }
117
118         shared_ptr<Content> content () const {
119                 return _content.lock ();
120         }
121
122         void set_track (int t) {
123                 _track = t;
124         }
125
126         optional<int> track () const {
127                 return _track;
128         }
129
130         virtual wxString type () const = 0;
131         virtual wxColour colour () const = 0;
132         
133 private:
134
135         void do_paint (wxGraphicsContext* gc)
136         {
137                 assert (_track);
138
139                 shared_ptr<const Film> film = _timeline.film ();
140                 shared_ptr<const Content> cont = content ();
141                 if (!film || !cont) {
142                         return;
143                 }
144
145                 DCPTime const position = cont->position ();
146                 DCPTime const len = cont->length_after_trim ();
147
148                 wxColour selected (colour().Red() / 2, colour().Green() / 2, colour().Blue() / 2);
149
150                 gc->SetPen (*wxBLACK_PEN);
151                 
152                 gc->SetPen (*wxThePenList->FindOrCreatePen (wxColour (0, 0, 0), 4, wxPENSTYLE_SOLID));
153                 if (_selected) {
154                         gc->SetBrush (*wxTheBrushList->FindOrCreateBrush (selected, wxBRUSHSTYLE_SOLID));
155                 } else {
156                         gc->SetBrush (*wxTheBrushList->FindOrCreateBrush (colour(), wxBRUSHSTYLE_SOLID));
157                 }
158
159                 wxGraphicsPath path = gc->CreatePath ();
160                 path.MoveToPoint    (time_x (position),       y_pos (_track.get()) + 4);
161                 path.AddLineToPoint (time_x (position + len), y_pos (_track.get()) + 4);
162                 path.AddLineToPoint (time_x (position + len), y_pos (_track.get() + 1) - 4);
163                 path.AddLineToPoint (time_x (position),       y_pos (_track.get() + 1) - 4);
164                 path.AddLineToPoint (time_x (position),       y_pos (_track.get()) + 4);
165                 gc->StrokePath (path);
166                 gc->FillPath (path);
167
168                 wxString name = wxString::Format (wxT ("%s [%s]"), std_to_wx (cont->path_summary()).data(), type().data());
169                 wxDouble name_width;
170                 wxDouble name_height;
171                 wxDouble name_descent;
172                 wxDouble name_leading;
173                 gc->GetTextExtent (name, &name_width, &name_height, &name_descent, &name_leading);
174                 
175                 gc->Clip (wxRegion (time_x (position), y_pos (_track.get()), len.seconds() * _timeline.pixels_per_second(), _timeline.track_height()));
176                 gc->DrawText (name, time_x (position) + 12, y_pos (_track.get() + 1) - name_height - 4);
177                 gc->ResetClip ();
178         }
179
180         int y_pos (int t) const
181         {
182                 return _timeline.tracks_position().y + t * _timeline.track_height();
183         }
184
185         void content_changed (int p, bool frequent)
186         {
187                 ensure_ui_thread ();
188                 
189                 if (p == ContentProperty::POSITION || p == ContentProperty::LENGTH) {
190                         force_redraw ();
191                 }
192
193                 if (!frequent) {
194                         _timeline.setup_pixels_per_second ();
195                         _timeline.Refresh ();
196                 }
197         }
198
199         boost::weak_ptr<Content> _content;
200         optional<int> _track;
201         bool _selected;
202
203         boost::signals2::scoped_connection _content_connection;
204 };
205
206 class AudioContentView : public ContentView
207 {
208 public:
209         AudioContentView (Timeline& tl, shared_ptr<Content> c)
210                 : ContentView (tl, c)
211         {}
212         
213 private:
214         wxString type () const
215         {
216                 return _("audio");
217         }
218
219         wxColour colour () const
220         {
221                 return wxColour (149, 121, 232, 255);
222         }
223 };
224
225 class VideoContentView : public ContentView
226 {
227 public:
228         VideoContentView (Timeline& tl, shared_ptr<Content> c)
229                 : ContentView (tl, c)
230         {}
231
232 private:        
233
234         wxString type () const
235         {
236                 if (dynamic_pointer_cast<FFmpegContent> (content ())) {
237                         return _("video");
238                 } else {
239                         return _("still");
240                 }
241         }
242
243         wxColour colour () const
244         {
245                 return wxColour (242, 92, 120, 255);
246         }
247 };
248
249 class SubtitleContentView : public ContentView
250 {
251 public:
252         SubtitleContentView (Timeline& tl, shared_ptr<Content> c)
253                 : ContentView (tl, c)
254         {}
255
256 private:
257         wxString type () const
258         {
259                 return _("subtitles");
260         }
261
262         wxColour colour () const
263         {
264                 return wxColour (163, 255, 154, 255);
265         }
266 };
267
268 class TimeAxisView : public View
269 {
270 public:
271         TimeAxisView (Timeline& tl, int y)
272                 : View (tl)
273                 , _y (y)
274         {}
275         
276         dcpomatic::Rect<int> bbox () const
277         {
278                 return dcpomatic::Rect<int> (0, _y - 4, _timeline.width(), 24);
279         }
280
281         void set_y (int y)
282         {
283                 _y = y;
284                 force_redraw ();
285         }
286
287 private:        
288
289         void do_paint (wxGraphicsContext* gc)
290         {
291                 gc->SetPen (*wxThePenList->FindOrCreatePen (wxColour (0, 0, 0), 1, wxPENSTYLE_SOLID));
292                 
293                 double mark_interval = rint (128 / _timeline.pixels_per_second ());
294                 if (mark_interval > 5) {
295                         mark_interval -= int (rint (mark_interval)) % 5;
296                 }
297                 if (mark_interval > 10) {
298                         mark_interval -= int (rint (mark_interval)) % 10;
299                 }
300                 if (mark_interval > 60) {
301                         mark_interval -= int (rint (mark_interval)) % 60;
302                 }
303                 if (mark_interval > 3600) {
304                         mark_interval -= int (rint (mark_interval)) % 3600;
305                 }
306                 
307                 if (mark_interval < 1) {
308                         mark_interval = 1;
309                 }
310
311                 wxGraphicsPath path = gc->CreatePath ();
312                 path.MoveToPoint (_timeline.x_offset(), _y);
313                 path.AddLineToPoint (_timeline.width(), _y);
314                 gc->StrokePath (path);
315
316                 /* Time in seconds */
317                 DCPTime t;
318                 while ((t.seconds() * _timeline.pixels_per_second()) < _timeline.width()) {
319                         wxGraphicsPath path = gc->CreatePath ();
320                         path.MoveToPoint (time_x (t), _y - 4);
321                         path.AddLineToPoint (time_x (t), _y + 4);
322                         gc->StrokePath (path);
323
324                         double tc = t.seconds ();
325                         int const h = tc / 3600;
326                         tc -= h * 3600;
327                         int const m = tc / 60;
328                         tc -= m * 60;
329                         int const s = tc;
330                         
331                         wxString str = wxString::Format (wxT ("%02d:%02d:%02d"), h, m, s);
332                         wxDouble str_width;
333                         wxDouble str_height;
334                         wxDouble str_descent;
335                         wxDouble str_leading;
336                         gc->GetTextExtent (str, &str_width, &str_height, &str_descent, &str_leading);
337                         
338                         int const tx = _timeline.x_offset() + t.seconds() * _timeline.pixels_per_second();
339                         if ((tx + str_width) < _timeline.width()) {
340                                 gc->DrawText (str, time_x (t), _y + 16);
341                         }
342                         
343                         t += DCPTime::from_seconds (mark_interval);
344                 }
345         }
346
347 private:
348         int _y;
349 };
350
351
352 Timeline::Timeline (wxWindow* parent, FilmEditor* ed, shared_ptr<Film> film)
353         : wxPanel (parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE)
354         , _film_editor (ed)
355         , _film (film)
356         , _time_axis_view (new TimeAxisView (*this, 32))
357         , _tracks (0)
358         , _pixels_per_second (0)
359         , _left_down (false)
360         , _down_view_position (0)
361         , _first_move (false)
362         , _menu (this)
363         , _snap (true)
364 {
365 #ifndef __WXOSX__
366         SetDoubleBuffered (true);
367 #endif  
368
369         Bind (wxEVT_PAINT,      boost::bind (&Timeline::paint,       this));
370         Bind (wxEVT_LEFT_DOWN,  boost::bind (&Timeline::left_down,   this, _1));
371         Bind (wxEVT_LEFT_UP,    boost::bind (&Timeline::left_up,     this, _1));
372         Bind (wxEVT_RIGHT_DOWN, boost::bind (&Timeline::right_down,  this, _1));
373         Bind (wxEVT_MOTION,     boost::bind (&Timeline::mouse_moved, this, _1));
374         Bind (wxEVT_SIZE,       boost::bind (&Timeline::resized,     this));
375
376         playlist_changed ();
377
378         SetMinSize (wxSize (640, tracks() * track_height() + 96));
379
380         _playlist_connection = film->playlist()->Changed.connect (bind (&Timeline::playlist_changed, this));
381 }
382
383 void
384 Timeline::paint ()
385 {
386         wxPaintDC dc (this);
387
388         wxGraphicsContext* gc = wxGraphicsContext::Create (dc);
389         if (!gc) {
390                 return;
391         }
392
393         gc->SetFont (gc->CreateFont (*wxNORMAL_FONT));
394
395         for (ViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
396                 (*i)->paint (gc);
397         }
398
399         delete gc;
400 }
401
402 void
403 Timeline::playlist_changed ()
404 {
405         ensure_ui_thread ();
406         
407         shared_ptr<const Film> fl = _film.lock ();
408         if (!fl) {
409                 return;
410         }
411
412         _views.clear ();
413         _views.push_back (_time_axis_view);
414
415         ContentList content = fl->playlist()->content ();
416
417         for (ContentList::iterator i = content.begin(); i != content.end(); ++i) {
418                 if (dynamic_pointer_cast<VideoContent> (*i)) {
419                         _views.push_back (shared_ptr<View> (new VideoContentView (*this, *i)));
420                 }
421                 if (dynamic_pointer_cast<AudioContent> (*i)) {
422                         _views.push_back (shared_ptr<View> (new AudioContentView (*this, *i)));
423                 }
424                 if (dynamic_pointer_cast<SubtitleContent> (*i)) {
425                         _views.push_back (shared_ptr<View> (new SubtitleContentView (*this, *i)));
426                 }
427         }
428
429         assign_tracks ();
430         setup_pixels_per_second ();
431         Refresh ();
432 }
433
434 void
435 Timeline::assign_tracks ()
436 {
437         for (ViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
438                 shared_ptr<ContentView> cv = dynamic_pointer_cast<ContentView> (*i);
439                 if (!cv) {
440                         continue;
441                 }
442
443                 shared_ptr<Content> content = cv->content();
444
445                 int t = 0;
446                 while (true) {
447                         ViewList::iterator j = _views.begin();
448                         while (j != _views.end()) {
449                                 shared_ptr<ContentView> test = dynamic_pointer_cast<ContentView> (*j);
450                                 if (!test) {
451                                         ++j;
452                                         continue;
453                                 }
454                                 
455                                 shared_ptr<Content> test_content = test->content();
456                                         
457                                 if (test && test->track() && test->track().get() == t) {
458                                         bool const no_overlap =
459                                                 (content->position() < test_content->position() && content->end() < test_content->position()) ||
460                                                 (content->position() > test_content->end()      && content->end() > test_content->end());
461                                         
462                                         if (!no_overlap) {
463                                                 /* we have an overlap on track `t' */
464                                                 ++t;
465                                                 break;
466                                         }
467                                 }
468                                 
469                                 ++j;
470                         }
471
472                         if (j == _views.end ()) {
473                                 /* no overlap on `t' */
474                                 break;
475                         }
476                 }
477
478                 cv->set_track (t);
479                 _tracks = max (_tracks, t + 1);
480         }
481
482         _time_axis_view->set_y (tracks() * track_height() + 32);
483 }
484
485 int
486 Timeline::tracks () const
487 {
488         return _tracks;
489 }
490
491 void
492 Timeline::setup_pixels_per_second ()
493 {
494         shared_ptr<const Film> film = _film.lock ();
495         if (!film || film->length() == DCPTime ()) {
496                 return;
497         }
498
499         _pixels_per_second = static_cast<double>(width() - x_offset() * 2) / film->length().seconds ();
500 }
501
502 shared_ptr<View>
503 Timeline::event_to_view (wxMouseEvent& ev)
504 {
505         ViewList::iterator i = _views.begin();
506         Position<int> const p (ev.GetX(), ev.GetY());
507         while (i != _views.end() && !(*i)->bbox().contains (p)) {
508                 ++i;
509         }
510
511         if (i == _views.end ()) {
512                 return shared_ptr<View> ();
513         }
514
515         return *i;
516 }
517
518 void
519 Timeline::left_down (wxMouseEvent& ev)
520 {
521         shared_ptr<View> view = event_to_view (ev);
522         shared_ptr<ContentView> content_view = dynamic_pointer_cast<ContentView> (view);
523
524         _down_view.reset ();
525
526         if (content_view) {
527                 _down_view = content_view;
528                 _down_view_position = content_view->content()->position ();
529         }
530
531         for (ViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
532                 shared_ptr<ContentView> cv = dynamic_pointer_cast<ContentView> (*i);
533                 if (!cv) {
534                         continue;
535                 }
536                 
537                 if (!ev.ShiftDown ()) {
538                         cv->set_selected (view == *i);
539                 }
540                 
541                 if (view == *i) {
542                         _film_editor->set_selection (cv->content ());
543                 }
544         }
545
546         if (content_view && ev.ShiftDown ()) {
547                 content_view->set_selected (!content_view->selected ());
548         }
549
550         _left_down = true;
551         _down_point = ev.GetPosition ();
552         _first_move = false;
553
554         if (_down_view) {
555                 _down_view->content()->set_change_signals_frequent (true);
556         }
557 }
558
559 void
560 Timeline::left_up (wxMouseEvent& ev)
561 {
562         _left_down = false;
563
564         if (_down_view) {
565                 _down_view->content()->set_change_signals_frequent (false);
566         }
567
568         set_position_from_event (ev);
569 }
570
571 void
572 Timeline::mouse_moved (wxMouseEvent& ev)
573 {
574         if (!_left_down) {
575                 return;
576         }
577
578         set_position_from_event (ev);
579 }
580
581 void
582 Timeline::right_down (wxMouseEvent& ev)
583 {
584         shared_ptr<View> view = event_to_view (ev);
585         shared_ptr<ContentView> cv = dynamic_pointer_cast<ContentView> (view);
586         if (!cv) {
587                 return;
588         }
589
590         if (!cv->selected ()) {
591                 clear_selection ();
592                 cv->set_selected (true);
593         }
594
595         _menu.popup (_film, selected_content (), ev.GetPosition ());
596 }
597
598 void
599 Timeline::set_position_from_event (wxMouseEvent& ev)
600 {
601         wxPoint const p = ev.GetPosition();
602
603         if (!_first_move) {
604                 /* We haven't moved yet; in that case, we must move the mouse some reasonable distance
605                    before the drag is considered to have started.
606                 */
607                 int const dist = sqrt (pow (p.x - _down_point.x, 2) + pow (p.y - _down_point.y, 2));
608                 if (dist < 8) {
609                         return;
610                 }
611                 _first_move = true;
612         }
613
614         if (!_down_view) {
615                 return;
616         }
617         
618         DCPTime new_position = _down_view_position + DCPTime::from_seconds ((p.x - _down_point.x) / _pixels_per_second);
619         
620         if (_snap) {
621                 
622                 bool first = true;
623                 DCPTime nearest_distance = DCPTime::max ();
624                 DCPTime nearest_new_position = DCPTime::max ();
625                 
626                 /* Find the nearest content edge; this is inefficient */
627                 for (ViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
628                         shared_ptr<ContentView> cv = dynamic_pointer_cast<ContentView> (*i);
629                         if (!cv || cv == _down_view) {
630                                 continue;
631                         }
632                         
633                         {
634                                 /* Snap starts to ends */
635                                 DCPTime const d = DCPTime (cv->content()->end() - new_position).abs ();
636                                 if (first || d < nearest_distance) {
637                                         nearest_distance = d;
638                                         nearest_new_position = cv->content()->end();
639                                 }
640                         }
641                         
642                         {
643                                 /* Snap ends to starts */
644                                 DCPTime const d = DCPTime (
645                                         cv->content()->position() - (new_position + _down_view->content()->length_after_trim())
646                                         ).abs ();
647                                 
648                                 if (d < nearest_distance) {
649                                         nearest_distance = d;
650                                         nearest_new_position = cv->content()->position() - _down_view->content()->length_after_trim ();
651                                 }
652                         }
653                         
654                         first = false;
655                 }
656                 
657                 if (!first) {
658                         /* Snap if it's close; `close' means within a proportion of the time on the timeline */
659                         if (nearest_distance < DCPTime::from_seconds ((width() / pixels_per_second()) / 32)) {
660                                 new_position = nearest_new_position;
661                         }
662                 }
663         }
664         
665         if (new_position < DCPTime ()) {
666                 new_position = DCPTime ();
667         }
668         
669         _down_view->content()->set_position (new_position);
670         
671         shared_ptr<Film> film = _film.lock ();
672         assert (film);
673         film->set_sequence_video (false);
674 }
675
676 void
677 Timeline::force_redraw (dcpomatic::Rect<int> const & r)
678 {
679         RefreshRect (wxRect (r.x, r.y, r.width, r.height), false);
680 }
681
682 shared_ptr<const Film>
683 Timeline::film () const
684 {
685         return _film.lock ();
686 }
687
688 void
689 Timeline::resized ()
690 {
691         setup_pixels_per_second ();
692 }
693
694 void
695 Timeline::clear_selection ()
696 {
697         for (ViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
698                 shared_ptr<ContentView> cv = dynamic_pointer_cast<ContentView> (*i);
699                 if (cv) {
700                         cv->set_selected (false);
701                 }
702         }
703 }
704
705 Timeline::ContentViewList
706 Timeline::selected_views () const
707 {
708         ContentViewList sel;
709         
710         for (ViewList::const_iterator i = _views.begin(); i != _views.end(); ++i) {
711                 shared_ptr<ContentView> cv = dynamic_pointer_cast<ContentView> (*i);
712                 if (cv && cv->selected()) {
713                         sel.push_back (cv);
714                 }
715         }
716
717         return sel;
718 }
719
720 ContentList
721 Timeline::selected_content () const
722 {
723         ContentList sel;
724         ContentViewList views = selected_views ();
725         
726         for (ContentViewList::const_iterator i = views.begin(); i != views.end(); ++i) {
727                 sel.push_back ((*i)->content ());
728         }
729
730         return sel;
731 }