Merge branch '1.0' of /home/carl/git/dvdomatic into 1.0
[dcpomatic.git] / src / wx / timeline.cc
1 /*
2     Copyright (C) 2013 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
37 class View : public boost::noncopyable
38 {
39 public:
40         View (Timeline& t)
41                 : _timeline (t)
42         {
43
44         }
45                 
46         void paint (wxGraphicsContext* g)
47         {
48                 _last_paint_bbox = bbox ();
49                 do_paint (g);
50         }
51         
52         void force_redraw ()
53         {
54                 _timeline.force_redraw (_last_paint_bbox);
55                 _timeline.force_redraw (bbox ());
56         }
57
58         virtual dcpomatic::Rect<int> bbox () const = 0;
59
60 protected:
61         virtual void do_paint (wxGraphicsContext *) = 0;
62         
63         int time_x (Time t) const
64         {
65                 return _timeline.tracks_position().x + t * _timeline.pixels_per_time_unit();
66         }
67         
68         Timeline& _timeline;
69
70 private:
71         dcpomatic::Rect<int> _last_paint_bbox;
72 };
73
74 class ContentView : public View
75 {
76 public:
77         ContentView (Timeline& tl, shared_ptr<Content> c, int t)
78                 : View (tl)
79                 , _content (c)
80                 , _track (t)
81                 , _selected (false)
82         {
83                 _content_connection = c->Changed.connect (bind (&ContentView::content_changed, this, _2));
84         }
85
86         dcpomatic::Rect<int> bbox () const
87         {
88                 shared_ptr<const Film> film = _timeline.film ();
89                 if (!film) {
90                         return dcpomatic::Rect<int> ();
91                 }
92                 
93                 return dcpomatic::Rect<int> (
94                         time_x (_content->start ()) - 8,
95                         y_pos (_track) - 8,
96                         _content->length () * _timeline.pixels_per_time_unit() + 16,
97                         _timeline.track_height() + 16
98                         );
99         }
100
101         void set_selected (bool s) {
102                 _selected = s;
103                 force_redraw ();
104         }
105         
106         bool selected () const {
107                 return _selected;
108         }
109
110         shared_ptr<Content> content () const {
111                 return _content;
112         }
113
114         void set_track (int t) {
115                 _track = t;
116         }
117
118         int track () const {
119                 return _track;
120         }
121
122         virtual wxString type () const = 0;
123         virtual wxColour colour () const = 0;
124         
125 private:
126
127         void do_paint (wxGraphicsContext* gc)
128         {
129                 shared_ptr<const Film> film = _timeline.film ();
130                 if (!film) {
131                         return;
132                 }
133
134                 Time const start = _content->start ();
135                 Time const len = _content->length ();
136
137                 wxColour selected (colour().Red() / 2, colour().Green() / 2, colour().Blue() / 2);
138
139                 gc->SetPen (*wxBLACK_PEN);
140                 
141                 gc->SetPen (*wxThePenList->FindOrCreatePen (wxColour (0, 0, 0), 4, wxPENSTYLE_SOLID));
142                 if (_selected) {
143                         gc->SetBrush (*wxTheBrushList->FindOrCreateBrush (selected, wxBRUSHSTYLE_SOLID));
144                 } else {
145                         gc->SetBrush (*wxTheBrushList->FindOrCreateBrush (colour(), wxBRUSHSTYLE_SOLID));
146                 }
147
148                 wxGraphicsPath path = gc->CreatePath ();
149                 path.MoveToPoint    (time_x (start),       y_pos (_track) + 4);
150                 path.AddLineToPoint (time_x (start + len), y_pos (_track) + 4);
151                 path.AddLineToPoint (time_x (start + len), y_pos (_track + 1) - 4);
152                 path.AddLineToPoint (time_x (start),       y_pos (_track + 1) - 4);
153                 path.AddLineToPoint (time_x (start),       y_pos (_track) + 4);
154                 gc->StrokePath (path);
155                 gc->FillPath (path);
156
157                 wxString name = wxString::Format (wxT ("%s [%s]"), std_to_wx (_content->file().filename().string()).data(), type().data());
158                 wxDouble name_width;
159                 wxDouble name_height;
160                 wxDouble name_descent;
161                 wxDouble name_leading;
162                 gc->GetTextExtent (name, &name_width, &name_height, &name_descent, &name_leading);
163                 
164                 gc->Clip (wxRegion (time_x (start), y_pos (_track), len * _timeline.pixels_per_time_unit(), _timeline.track_height()));
165                 gc->DrawText (name, time_x (start) + 12, y_pos (_track + 1) - name_height - 4);
166                 gc->ResetClip ();
167         }
168
169         int y_pos (int t) const
170         {
171                 return _timeline.tracks_position().y + t * _timeline.track_height();
172         }
173
174         void content_changed (int p)
175         {
176                 if (p == ContentProperty::START || p == ContentProperty::LENGTH) {
177                         force_redraw ();
178                 }
179         }
180
181         /* This must be a shared_ptr, not a weak_ptr, as in the looped case this
182            will be the only remaining pointer to the looped content that we get
183            from the playlist.
184         */
185         boost::shared_ptr<Content> _content;
186         int _track;
187         bool _selected;
188
189         boost::signals2::scoped_connection _content_connection;
190 };
191
192 class AudioContentView : public ContentView
193 {
194 public:
195         AudioContentView (Timeline& tl, shared_ptr<Content> c, int t)
196                 : ContentView (tl, c, t)
197         {}
198         
199 private:
200         wxString type () const
201         {
202                 return _("audio");
203         }
204
205         wxColour colour () const
206         {
207                 return wxColour (149, 121, 232, 255);
208         }
209 };
210
211 class VideoContentView : public ContentView
212 {
213 public:
214         VideoContentView (Timeline& tl, shared_ptr<Content> c, int t)
215                 : ContentView (tl, c, t)
216         {}
217
218 private:        
219
220         wxString type () const
221         {
222                 if (dynamic_pointer_cast<FFmpegContent> (content ())) {
223                         return _("video");
224                 } else {
225                         return _("still");
226                 }
227         }
228
229         wxColour colour () const
230         {
231                 return wxColour (242, 92, 120, 255);
232         }
233 };
234
235 class TimeAxisView : public View
236 {
237 public:
238         TimeAxisView (Timeline& tl, int y)
239                 : View (tl)
240                 , _y (y)
241         {}
242         
243         dcpomatic::Rect<int> bbox () const
244         {
245                 return dcpomatic::Rect<int> (0, _y - 4, _timeline.width(), 24);
246         }
247
248         void set_y (int y)
249         {
250                 _y = y;
251                 force_redraw ();
252         }
253
254 private:        
255
256         void do_paint (wxGraphicsContext* gc)
257         {
258                 gc->SetPen (*wxThePenList->FindOrCreatePen (wxColour (0, 0, 0), 1, wxPENSTYLE_SOLID));
259                 
260                 int mark_interval = rint (128 / (TIME_HZ * _timeline.pixels_per_time_unit ()));
261                 if (mark_interval > 5) {
262                         mark_interval -= mark_interval % 5;
263                 }
264                 if (mark_interval > 10) {
265                         mark_interval -= mark_interval % 10;
266                 }
267                 if (mark_interval > 60) {
268                         mark_interval -= mark_interval % 60;
269                 }
270                 if (mark_interval > 3600) {
271                         mark_interval -= mark_interval % 3600;
272                 }
273                 
274                 if (mark_interval < 1) {
275                         mark_interval = 1;
276                 }
277
278                 wxGraphicsPath path = gc->CreatePath ();
279                 path.MoveToPoint (_timeline.x_offset(), _y);
280                 path.AddLineToPoint (_timeline.width(), _y);
281                 gc->StrokePath (path);
282
283                 Time t = 0;
284                 while ((t * _timeline.pixels_per_time_unit()) < _timeline.width()) {
285                         wxGraphicsPath path = gc->CreatePath ();
286                         path.MoveToPoint (time_x (t), _y - 4);
287                         path.AddLineToPoint (time_x (t), _y + 4);
288                         gc->StrokePath (path);
289
290                         int tc = t / TIME_HZ;
291                         int const h = tc / 3600;
292                         tc -= h * 3600;
293                         int const m = tc / 60;
294                         tc -= m * 60;
295                         int const s = tc;
296                         
297                         wxString str = wxString::Format (wxT ("%02d:%02d:%02d"), h, m, s);
298                         wxDouble str_width;
299                         wxDouble str_height;
300                         wxDouble str_descent;
301                         wxDouble str_leading;
302                         gc->GetTextExtent (str, &str_width, &str_height, &str_descent, &str_leading);
303                         
304                         int const tx = _timeline.x_offset() + t * _timeline.pixels_per_time_unit();
305                         if ((tx + str_width) < _timeline.width()) {
306                                 gc->DrawText (str, time_x (t), _y + 16);
307                         }
308                         
309                         t += mark_interval * TIME_HZ;
310                 }
311         }
312
313 private:
314         int _y;
315 };
316
317 Timeline::Timeline (wxWindow* parent, FilmEditor* ed, shared_ptr<Film> film)
318         : wxPanel (parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE)
319         , _film_editor (ed)
320         , _film (film)
321         , _time_axis_view (new TimeAxisView (*this, 32))
322         , _tracks (0)
323         , _pixels_per_time_unit (0)
324         , _left_down (false)
325         , _down_view_start (0)
326         , _first_move (false)
327 {
328 #ifndef __WXOSX__
329         SetDoubleBuffered (true);
330 #endif  
331
332         setup_pixels_per_time_unit ();
333         
334         Connect (wxID_ANY, wxEVT_PAINT, wxPaintEventHandler (Timeline::paint), 0, this);
335         Connect (wxID_ANY, wxEVT_LEFT_DOWN, wxMouseEventHandler (Timeline::left_down), 0, this);
336         Connect (wxID_ANY, wxEVT_LEFT_UP, wxMouseEventHandler (Timeline::left_up), 0, this);
337         Connect (wxID_ANY, wxEVT_MOTION, wxMouseEventHandler (Timeline::mouse_moved), 0, this);
338         Connect (wxID_ANY, wxEVT_SIZE, wxSizeEventHandler (Timeline::resized), 0, this);
339
340         playlist_changed ();
341
342         SetMinSize (wxSize (640, tracks() * track_height() + 96));
343
344         _playlist_connection = film->playlist()->Changed.connect (bind (&Timeline::playlist_changed, this));
345
346         _views.push_back (_time_axis_view);
347 }
348
349 void
350 Timeline::paint (wxPaintEvent &)
351 {
352         wxPaintDC dc (this);
353
354         wxGraphicsContext* gc = wxGraphicsContext::Create (dc);
355         if (!gc) {
356                 return;
357         }
358
359         gc->SetFont (gc->CreateFont (*wxNORMAL_FONT));
360
361         for (list<shared_ptr<View> >::iterator i = _views.begin(); i != _views.end(); ++i) {
362                 (*i)->paint (gc);
363         }
364
365         delete gc;
366 }
367
368 void
369 Timeline::playlist_changed ()
370 {
371         shared_ptr<const Film> fl = _film.lock ();
372         if (!fl) {
373                 return;
374         }
375
376         _views.clear ();
377
378         Playlist::ContentList content = fl->playlist()->content_with_loop ();
379
380         for (Playlist::ContentList::iterator i = content.begin(); i != content.end(); ++i) {
381                 if (dynamic_pointer_cast<VideoContent> (*i)) {
382                         _views.push_back (shared_ptr<View> (new VideoContentView (*this, *i, 0)));
383                 }
384                 if (dynamic_pointer_cast<AudioContent> (*i)) {
385                         _views.push_back (shared_ptr<View> (new AudioContentView (*this, *i, 0)));
386                 }
387         }
388
389         assign_tracks ();
390         Refresh ();
391 }
392
393 void
394 Timeline::assign_tracks ()
395 {
396         for (list<shared_ptr<View> >::iterator i = _views.begin(); i != _views.end(); ++i) {
397                 shared_ptr<ContentView> cv = dynamic_pointer_cast<ContentView> (*i);
398                 if (cv) {
399                         cv->set_track (0);
400                         _tracks = 1;
401                 }
402         }
403
404         for (list<shared_ptr<View> >::iterator i = _views.begin(); i != _views.end(); ++i) {
405                 shared_ptr<AudioContentView> acv = dynamic_pointer_cast<AudioContentView> (*i);
406                 if (!acv) {
407                         continue;
408                 }
409         
410                 shared_ptr<Content> acv_content = acv->content();
411                 
412                 int t = 1;
413                 while (1) {
414                         list<shared_ptr<View> >::iterator j = _views.begin();
415                         while (j != _views.end()) {
416                                 shared_ptr<AudioContentView> test = dynamic_pointer_cast<AudioContentView> (*j);
417                                 if (!test) {
418                                         ++j;
419                                         continue;
420                                 }
421                                 
422                                 shared_ptr<Content> test_content = test->content();
423                                         
424                                 if (test && test->track() == t) {
425                                         if ((acv_content->start() < test_content->start() && test_content->start() < acv_content->end()) ||
426                                             (acv_content->start() < test_content->end()   && test_content->end()   < acv_content->end())) {
427                                                 /* we have an overlap on track `t' */
428                                                 ++t;
429                                                 break;
430                                         }
431                                 }
432                                 
433                                 ++j;
434                         }
435
436                         if (j == _views.end ()) {
437                                 /* no overlap on `t' */
438                                 break;
439                         }
440                 }
441
442                 acv->set_track (t);
443                 _tracks = max (_tracks, t + 1);
444         }
445
446         _time_axis_view->set_y (tracks() * track_height() + 32);
447 }
448
449 int
450 Timeline::tracks () const
451 {
452         return _tracks;
453 }
454
455 void
456 Timeline::setup_pixels_per_time_unit ()
457 {
458         shared_ptr<const Film> film = _film.lock ();
459         if (!film) {
460                 return;
461         }
462
463         _pixels_per_time_unit = static_cast<double>(width() - x_offset() * 2) / film->length_with_loop();
464 }
465
466 void
467 Timeline::left_down (wxMouseEvent& ev)
468 {
469         list<shared_ptr<View> >::iterator i = _views.begin();
470         Position<int> const p (ev.GetX(), ev.GetY());
471         while (i != _views.end() && !(*i)->bbox().contains (p)) {
472                 ++i;
473         }
474
475         _down_view.reset ();
476
477         if (i != _views.end ()) {
478                 shared_ptr<ContentView> cv = dynamic_pointer_cast<ContentView> (*i);
479                 if (cv) {
480                         _down_view = cv;
481                         _down_view_start = cv->content()->start ();
482                 }
483         }
484
485         for (list<shared_ptr<View> >::iterator j = _views.begin(); j != _views.end(); ++j) {
486                 shared_ptr<ContentView> cv = dynamic_pointer_cast<ContentView> (*j);
487                 if (cv) {
488                         cv->set_selected (i == j);
489                         if (i == j) {
490                                 _film_editor->set_selection (cv->content ());
491                         }
492                 }
493         }
494
495         _left_down = true;
496         _down_point = ev.GetPosition ();
497         _first_move = false;
498
499         if (_down_view) {
500                 _down_view->content()->set_change_signals_frequent (true);
501         }
502 }
503
504 void
505 Timeline::left_up (wxMouseEvent& ev)
506 {
507         _left_down = false;
508
509         if (_down_view) {
510                 _down_view->content()->set_change_signals_frequent (false);
511         }
512
513         set_start_from_event (ev);
514 }
515
516 void
517 Timeline::mouse_moved (wxMouseEvent& ev)
518 {
519         if (!_left_down) {
520                 return;
521         }
522
523         set_start_from_event (ev);
524 }
525
526 void
527 Timeline::set_start_from_event (wxMouseEvent& ev)
528 {
529         wxPoint const p = ev.GetPosition();
530
531         if (!_first_move) {
532                 int const dist = sqrt (pow (p.x - _down_point.x, 2) + pow (p.y - _down_point.y, 2));
533                 if (dist < 8) {
534                         return;
535                 }
536                 _first_move = true;
537         }
538
539         Time const time_diff = (p.x - _down_point.x) / _pixels_per_time_unit;
540         if (_down_view) {
541                 _down_view->content()->set_start (max (static_cast<Time> (0), _down_view_start + time_diff));
542
543                 shared_ptr<Film> film = _film.lock ();
544                 assert (film);
545                 film->set_sequence_video (false);
546         }
547 }
548
549 void
550 Timeline::force_redraw (dcpomatic::Rect<int> const & r)
551 {
552         RefreshRect (wxRect (r.x, r.y, r.width, r.height), false);
553 }
554
555 shared_ptr<const Film>
556 Timeline::film () const
557 {
558         return _film.lock ();
559 }
560
561 void
562 Timeline::resized (wxSizeEvent &)
563 {
564         setup_pixels_per_time_unit ();
565 }