Partial split of film viewer.
[dcpomatic.git] / src / wx / timing_panel.cc
1 /*
2     Copyright (C) 2012-2016 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21 #include "timing_panel.h"
22 #include "wx_util.h"
23 #include "film_viewer.h"
24 #include "timecode.h"
25 #include "content_panel.h"
26 #include "move_to_dialog.h"
27 #include "lib/content.h"
28 #include "lib/image_content.h"
29 #include "lib/text_content.h"
30 #include "lib/dcp_subtitle_content.h"
31 #include "lib/audio_content.h"
32 #include "lib/string_text_file_content.h"
33 #include "lib/video_content.h"
34 #include <dcp/locale_convert.h>
35 #include <boost/foreach.hpp>
36 #include <set>
37 #include <iostream>
38
39 using std::cout;
40 using std::string;
41 using std::set;
42 using boost::shared_ptr;
43 using boost::dynamic_pointer_cast;
44 using boost::optional;
45 using dcp::locale_convert;
46
47 TimingPanel::TimingPanel (ContentPanel* p, ControlFilmViewer* viewer)
48         /* horrid hack for apparent lack of context support with wxWidgets i18n code */
49         /// TRANSLATORS: translate the word "Timing" here; do not include the "Timing|" prefix
50         : ContentSubPanel (p, S_("Timing|Timing"))
51         , _viewer (viewer)
52 {
53         wxSize size = TimecodeBase::size (this);
54
55         for (int i = 0; i < 3; ++i) {
56                 _colon[i] = create_label (this, wxT(":"), false);
57         }
58
59         //// TRANSLATORS: this is an abbreviation for "hours"
60         _h_label = new wxStaticText (this, wxID_ANY, _("h"), wxDefaultPosition, size, wxALIGN_CENTRE_HORIZONTAL);
61 #ifdef DCPOMATIC_LINUX
62         /* Hack to work around failure to centre text on GTK */
63         gtk_label_set_line_wrap (GTK_LABEL(_h_label->GetHandle()), FALSE);
64 #endif
65         //// TRANSLATORS: this is an abbreviation for "minutes"
66         _m_label = new wxStaticText (this, wxID_ANY, _("m"), wxDefaultPosition, size, wxALIGN_CENTRE_HORIZONTAL);
67 #ifdef DCPOMATIC_LINUX
68         gtk_label_set_line_wrap (GTK_LABEL (_m_label->GetHandle()), FALSE);
69 #endif
70         //// TRANSLATORS: this is an abbreviation for "seconds"
71         _s_label = new wxStaticText (this, wxID_ANY, _("s"), wxDefaultPosition, size, wxALIGN_CENTRE_HORIZONTAL);
72 #ifdef DCPOMATIC_LINUX
73         gtk_label_set_line_wrap (GTK_LABEL(_s_label->GetHandle()), FALSE);
74 #endif
75         //// TRANSLATORS: this is an abbreviation for "frames"
76         _f_label = new wxStaticText (this, wxID_ANY, _("f"), wxDefaultPosition, size, wxALIGN_CENTRE_HORIZONTAL);
77 #ifdef DCPOMATIC_LINUX
78         gtk_label_set_line_wrap (GTK_LABEL(_f_label->GetHandle()), FALSE);
79 #endif
80
81         _position_label = create_label (this, _("Position"), true);
82         _position = new Timecode<DCPTime> (this);
83         _move_to_start_of_reel = new wxButton (this, wxID_ANY, _("Move to start of reel"));
84         _full_length_label = create_label (this, _("Full length"), true);
85         _full_length = new Timecode<DCPTime> (this);
86         _trim_start_label = create_label (this, _("Trim from start"), true);
87         _trim_start = new Timecode<ContentTime> (this);
88         _trim_start_to_playhead = new wxButton (this, wxID_ANY, _("Trim up to current position"));
89         _trim_end_label = create_label (this, _("Trim from end"), true);
90         _trim_end = new Timecode<ContentTime> (this);
91         _trim_end_to_playhead = new wxButton (this, wxID_ANY, _("Trim after current position"));
92         _play_length_label = create_label (this, _("Play length"), true);
93         _play_length = new Timecode<DCPTime> (this);
94
95         _video_frame_rate_label = create_label (this, _("Video frame rate"), true);
96         _video_frame_rate = new wxTextCtrl (this, wxID_ANY);
97         _set_video_frame_rate = new wxButton (this, wxID_ANY, _("Set"));
98         _set_video_frame_rate->Enable (false);
99
100         /* We can't use Wrap() here as it doesn't work with markup:
101          * http://trac.wxwidgets.org/ticket/13389
102          */
103
104         wxString in = _("<i>Only change this if the content's frame rate has been read incorrectly.</i>");
105         wxString out;
106         int const width = 20;
107         int current = 0;
108         for (size_t i = 0; i < in.Length(); ++i) {
109                 if (in[i] == ' ' && current >= width) {
110                         out += '\n';
111                         current = 0;
112                 } else {
113                         out += in[i];
114                         ++current;
115                 }
116         }
117
118         _tip = new wxStaticText (this, wxID_ANY, wxT (""));
119         _tip->SetLabelMarkup (out);
120 #ifdef DCPOMATIC_OSX
121         /* Hack to stop hidden text on some versions of OS X */
122         _tip->SetMinSize (wxSize (-1, 256));
123 #endif
124
125         _position->Changed.connect    (boost::bind (&TimingPanel::position_changed, this));
126         _move_to_start_of_reel->Bind  (wxEVT_BUTTON, boost::bind (&TimingPanel::move_to_start_of_reel_clicked, this));
127         _full_length->Changed.connect (boost::bind (&TimingPanel::full_length_changed, this));
128         _trim_start->Changed.connect  (boost::bind (&TimingPanel::trim_start_changed, this));
129         _trim_start_to_playhead->Bind (wxEVT_BUTTON, boost::bind (&TimingPanel::trim_start_to_playhead_clicked, this));
130         _trim_end->Changed.connect    (boost::bind (&TimingPanel::trim_end_changed, this));
131         _trim_end_to_playhead->Bind   (wxEVT_BUTTON, boost::bind (&TimingPanel::trim_end_to_playhead_clicked, this));
132         _play_length->Changed.connect (boost::bind (&TimingPanel::play_length_changed, this));
133         _video_frame_rate->Bind       (wxEVT_TEXT, boost::bind (&TimingPanel::video_frame_rate_changed, this));
134         _set_video_frame_rate->Bind   (wxEVT_BUTTON, boost::bind (&TimingPanel::set_video_frame_rate, this));
135
136         _viewer->ImageChanged.connect (boost::bind (&TimingPanel::setup_sensitivity, this));
137
138         setup_sensitivity ();
139         add_to_grid ();
140 }
141
142 void
143 TimingPanel::add_to_grid ()
144 {
145         bool const full = Config::instance()->interface_complexity() == Config::INTERFACE_FULL;
146
147         int r = 0;
148
149         wxSizer* labels = new wxBoxSizer (wxHORIZONTAL);
150         labels->Add (_h_label, 1, wxEXPAND);
151         add_label_to_sizer (labels, _colon[0], false);
152         labels->Add (_m_label, 1, wxEXPAND);
153         add_label_to_sizer (labels, _colon[1], false);
154         labels->Add (_s_label, 1, wxEXPAND);
155         add_label_to_sizer (labels, _colon[2], false);
156         labels->Add (_f_label, 1, wxEXPAND);
157         _grid->Add (labels, wxGBPosition(r, 1));
158         ++r;
159
160         add_label_to_sizer (_grid, _position_label, true, wxGBPosition(r, 0));
161         _grid->Add (_position, wxGBPosition(r, 1));
162         ++r;
163
164         _move_to_start_of_reel->Show (full);
165         _full_length_label->Show (full);
166         _full_length->Show (full);
167         _trim_start_label->Show (full);
168         _trim_start->Show (full);
169         _trim_start_to_playhead->Show (full);
170         _trim_end_label->Show (full);
171         _trim_end->Show (full);
172         _trim_end_to_playhead->Show (full);
173         _play_length_label->Show (full);
174         _play_length->Show (full);
175         _video_frame_rate_label->Show (full);
176         _video_frame_rate->Show (full);
177         _set_video_frame_rate->Show (full);
178         _tip->Show (full);
179
180         if (full) {
181                 _grid->Add (_move_to_start_of_reel, wxGBPosition(r, 1));
182                 ++r;
183
184                 add_label_to_sizer (_grid, _full_length_label, true, wxGBPosition(r, 0));
185                 _grid->Add (_full_length, wxGBPosition(r, 1));
186                 ++r;
187
188                 add_label_to_sizer (_grid, _trim_start_label, true, wxGBPosition(r, 0));
189                 _grid->Add (_trim_start, wxGBPosition(r, 1));
190                 ++r;
191
192                 _grid->Add (_trim_start_to_playhead, wxGBPosition(r, 1));
193                 ++r;
194
195                 add_label_to_sizer (_grid, _trim_end_label, true, wxGBPosition(r, 0));
196                 _grid->Add (_trim_end, wxGBPosition(r, 1));
197                 ++r;
198
199                 _grid->Add (_trim_end_to_playhead, wxGBPosition(r, 1));
200                 ++r;
201
202                 add_label_to_sizer (_grid, _play_length_label, true, wxGBPosition(r, 0));
203                 _grid->Add (_play_length, wxGBPosition(r, 1));
204                 ++r;
205
206                 {
207                         add_label_to_sizer (_grid, _video_frame_rate_label, true, wxGBPosition(r, 0));
208                         wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
209                         s->Add (_video_frame_rate, 1, wxEXPAND);
210                         s->Add (_set_video_frame_rate, 0, wxLEFT | wxRIGHT, 8);
211                         _grid->Add (s, wxGBPosition(r, 1), wxGBSpan(1, 2));
212                 }
213                 ++r;
214
215                 _grid->Add (_tip, wxGBPosition(r, 1), wxGBSpan(1, 2));
216         }
217
218         /* Completely speculative fix for #891 */
219         _grid->Layout ();
220 }
221
222 void
223 TimingPanel::update_full_length ()
224 {
225         set<DCPTime> check;
226         BOOST_FOREACH (shared_ptr<const Content> i, _parent->selected ()) {
227                 check.insert (i->full_length ());
228         }
229
230         if (check.size() == 1) {
231                 _full_length->set (_parent->selected().front()->full_length (), _parent->film()->video_frame_rate ());
232         } else {
233                 _full_length->clear ();
234         }
235 }
236
237 void
238 TimingPanel::update_play_length ()
239 {
240         set<DCPTime> check;
241         BOOST_FOREACH (shared_ptr<const Content> i, _parent->selected ()) {
242                 check.insert (i->length_after_trim ());
243         }
244
245         if (check.size() == 1) {
246                 _play_length->set (_parent->selected().front()->length_after_trim (), _parent->film()->video_frame_rate ());
247         } else {
248                 _play_length->clear ();
249         }
250 }
251
252 void
253 TimingPanel::film_content_changed (int property)
254 {
255         int const film_video_frame_rate = _parent->film()->video_frame_rate ();
256
257         /* Here we check to see if we have exactly one different value of various
258            properties, and fill the controls with that value if so.
259         */
260
261         if (property == ContentProperty::POSITION) {
262
263                 set<DCPTime> check;
264                 BOOST_FOREACH (shared_ptr<const Content> i, _parent->selected ()) {
265                         check.insert (i->position ());
266                 }
267
268                 if (check.size() == 1) {
269                         _position->set (_parent->selected().front()->position(), film_video_frame_rate);
270                 } else {
271                         _position->clear ();
272                 }
273
274         } else if (
275                 property == ContentProperty::LENGTH ||
276                 property == ContentProperty::VIDEO_FRAME_RATE ||
277                 property == VideoContentProperty::FRAME_TYPE
278                 ) {
279
280                 update_full_length ();
281
282         } else if (property == ContentProperty::TRIM_START) {
283
284                 set<ContentTime> check;
285                 BOOST_FOREACH (shared_ptr<const Content> i, _parent->selected ()) {
286                         check.insert (i->trim_start ());
287                 }
288
289                 if (check.size() == 1) {
290                         _trim_start->set (_parent->selected().front()->trim_start (), film_video_frame_rate);
291                 } else {
292                         _trim_start->clear ();
293                 }
294
295         } else if (property == ContentProperty::TRIM_END) {
296
297                 set<ContentTime> check;
298                 BOOST_FOREACH (shared_ptr<const Content> i, _parent->selected ()) {
299                         check.insert (i->trim_end ());
300                 }
301
302                 if (check.size() == 1) {
303                         _trim_end->set (_parent->selected().front()->trim_end (), film_video_frame_rate);
304                 } else {
305                         _trim_end->clear ();
306                 }
307         }
308
309         if (
310                 property == ContentProperty::LENGTH ||
311                 property == ContentProperty::TRIM_START ||
312                 property == ContentProperty::TRIM_END ||
313                 property == ContentProperty::VIDEO_FRAME_RATE ||
314                 property == VideoContentProperty::FRAME_TYPE
315                 ) {
316
317                 update_play_length ();
318         }
319
320         if (property == ContentProperty::VIDEO_FRAME_RATE) {
321                 set<double> check_vc;
322                 shared_ptr<const Content> content;
323                 int count_ac = 0;
324                 int count_sc = 0;
325                 BOOST_FOREACH (shared_ptr<const Content> i, _parent->selected ()) {
326                         if (i->video && i->video_frame_rate()) {
327                                 check_vc.insert (i->video_frame_rate().get());
328                                 content = i;
329                         }
330                         if (i->audio && i->video_frame_rate()) {
331                                 ++count_ac;
332                                 content = i;
333                         }
334                         if (!i->text.empty() && i->video_frame_rate()) {
335                                 ++count_sc;
336                                 content = i;
337                         }
338
339                 }
340
341                 bool const single_frame_image_content = content && dynamic_pointer_cast<const ImageContent> (content) && content->number_of_paths() == 1;
342
343                 if ((check_vc.size() == 1 || count_ac == 1 || count_sc == 1) && !single_frame_image_content) {
344                         checked_set (_video_frame_rate, locale_convert<string> (content->video_frame_rate().get(), 5));
345                         _video_frame_rate->Enable (true);
346                 } else {
347                         checked_set (_video_frame_rate, wxT (""));
348                         _video_frame_rate->Enable (false);
349                 }
350         }
351
352         bool have_still = false;
353         BOOST_FOREACH (shared_ptr<const Content> i, _parent->selected ()) {
354                 shared_ptr<const ImageContent> ic = dynamic_pointer_cast<const ImageContent> (i);
355                 if (ic && ic->still ()) {
356                         have_still = true;
357                 }
358         }
359
360         _full_length->set_editable (have_still);
361         _play_length->set_editable (!have_still);
362         _set_video_frame_rate->Enable (false);
363         setup_sensitivity ();
364 }
365
366 void
367 TimingPanel::position_changed ()
368 {
369         DCPTime const pos = _position->get (_parent->film()->video_frame_rate ());
370         BOOST_FOREACH (shared_ptr<Content> i, _parent->selected ()) {
371                 i->set_position (pos);
372         }
373 }
374
375 void
376 TimingPanel::full_length_changed ()
377 {
378         int const vfr = _parent->film()->video_frame_rate ();
379         Frame const len = _full_length->get (vfr).frames_round (vfr);
380         BOOST_FOREACH (shared_ptr<Content> i, _parent->selected ()) {
381                 shared_ptr<ImageContent> ic = dynamic_pointer_cast<ImageContent> (i);
382                 if (ic && ic->still ()) {
383                         ic->video->set_length (len);
384                 }
385         }
386 }
387
388 void
389 TimingPanel::trim_start_changed ()
390 {
391         DCPTime const ph = _viewer->position ();
392
393         _viewer->set_coalesce_player_changes (true);
394
395         shared_ptr<Content> ref;
396         optional<FrameRateChange> ref_frc;
397         optional<DCPTime> ref_ph;
398
399         ContentTime const trim = _trim_start->get (_parent->film()->video_frame_rate ());
400
401         BOOST_FOREACH (shared_ptr<Content> i, _parent->selected ()) {
402                 if (i->position() <= ph && ph < i->end()) {
403                         /* The playhead is in i.  Use it as a reference to work out
404                            where to put the playhead post-trim; we're trying to keep the playhead
405                            at the same frame of content that we're looking at pre-trim.
406                         */
407                         ref = i;
408                         ref_frc = _parent->film()->active_frame_rate_change (i->position ());
409                         ref_ph = ph - i->position() + DCPTime (i->trim_start(), ref_frc.get());
410                 }
411
412                 i->set_trim_start (trim);
413         }
414
415         if (ref) {
416                 _viewer->set_position (max (DCPTime(), ref_ph.get() + ref->position() - DCPTime (ref->trim_start(), ref_frc.get())));
417         }
418
419         _viewer->set_coalesce_player_changes (false);
420 }
421
422 void
423 TimingPanel::trim_end_changed ()
424 {
425         _viewer->set_coalesce_player_changes (true);
426
427         ContentTime const trim = _trim_end->get (_parent->film()->video_frame_rate ());
428         BOOST_FOREACH (shared_ptr<Content> i, _parent->selected ()) {
429                 i->set_trim_end (trim);
430         }
431
432         /* XXX: maybe playhead-off-the-end-of-the-film should be handled elsewhere */
433         if (_viewer->position() >= _parent->film()->length()) {
434                 _viewer->set_position (_parent->film()->length() - DCPTime::from_frames (1, _parent->film()->video_frame_rate()));
435         }
436
437         _viewer->set_coalesce_player_changes (true);
438 }
439
440 void
441 TimingPanel::play_length_changed ()
442 {
443         DCPTime const play_length = _play_length->get (_parent->film()->video_frame_rate());
444         BOOST_FOREACH (shared_ptr<Content> i, _parent->selected ()) {
445                 FrameRateChange const frc = _parent->film()->active_frame_rate_change (i->position ());
446                 i->set_trim_end (
447                         ContentTime (max(DCPTime(), i->full_length() - play_length), frc) - i->trim_start ()
448                         );
449         }
450 }
451
452 void
453 TimingPanel::video_frame_rate_changed ()
454 {
455         _set_video_frame_rate->Enable (true);
456 }
457
458 void
459 TimingPanel::set_video_frame_rate ()
460 {
461         optional<double> fr;
462         if (_video_frame_rate->GetValue() != wxT("")) {
463                 fr = locale_convert<double> (wx_to_std (_video_frame_rate->GetValue ()));
464         }
465         BOOST_FOREACH (shared_ptr<Content> i, _parent->selected ()) {
466                 if (fr) {
467                         i->set_video_frame_rate (*fr);
468                 } else {
469                         i->unset_video_frame_rate ();
470                 }
471         }
472
473         _set_video_frame_rate->Enable (false);
474 }
475
476 void
477 TimingPanel::content_selection_changed ()
478 {
479         setup_sensitivity ();
480
481         film_content_changed (ContentProperty::POSITION);
482         film_content_changed (ContentProperty::LENGTH);
483         film_content_changed (ContentProperty::TRIM_START);
484         film_content_changed (ContentProperty::TRIM_END);
485         film_content_changed (ContentProperty::VIDEO_FRAME_RATE);
486 }
487
488 void
489 TimingPanel::film_changed (Film::Property p)
490 {
491         if (p == Film::VIDEO_FRAME_RATE) {
492                 update_full_length ();
493                 update_play_length ();
494         }
495 }
496
497 void
498 TimingPanel::trim_start_to_playhead_clicked ()
499 {
500         shared_ptr<const Film> film = _parent->film ();
501         DCPTime const ph = _viewer->position().floor (film->video_frame_rate ());
502         optional<DCPTime> new_ph;
503
504         _viewer->set_coalesce_player_changes (true);
505
506         BOOST_FOREACH (shared_ptr<Content> i, _parent->selected ()) {
507                 if (i->position() < ph && ph < i->end ()) {
508                         FrameRateChange const frc = film->active_frame_rate_change (i->position ());
509                         i->set_trim_start (i->trim_start() + ContentTime (ph - i->position (), frc));
510                         new_ph = i->position ();
511                 }
512         }
513
514         if (new_ph) {
515                 _viewer->set_position (new_ph.get());
516         }
517
518         _viewer->set_coalesce_player_changes (false);
519 }
520
521 void
522 TimingPanel::trim_end_to_playhead_clicked ()
523 {
524         shared_ptr<const Film> film = _parent->film ();
525         DCPTime const ph = _viewer->position().floor (film->video_frame_rate ());
526         BOOST_FOREACH (shared_ptr<Content> i, _parent->selected ()) {
527                 if (i->position() < ph && ph < i->end ()) {
528                         FrameRateChange const frc = film->active_frame_rate_change (i->position ());
529                         i->set_trim_end (ContentTime (i->position() + i->full_length() - ph - DCPTime::from_frames (1, frc.dcp), frc) - i->trim_start());
530                 }
531         }
532 }
533
534 void
535 TimingPanel::setup_sensitivity ()
536 {
537         bool const e = !_parent->selected().empty ();
538
539         _position->Enable (e);
540         _move_to_start_of_reel->Enable (e);
541         _full_length->Enable (e);
542         _trim_start->Enable (e);
543         _trim_end->Enable (e);
544         _play_length->Enable (e);
545         _video_frame_rate->Enable (e);
546
547         DCPTime const ph = _viewer->position ();
548         bool any_over_ph = false;
549         BOOST_FOREACH (shared_ptr<const Content> i, _parent->selected ()) {
550                 if (i->position() <= ph && ph < i->end()) {
551                         any_over_ph = true;
552                 }
553         }
554
555         _trim_start_to_playhead->Enable (any_over_ph);
556         _trim_end_to_playhead->Enable (any_over_ph);
557 }
558
559 void
560 TimingPanel::move_to_start_of_reel_clicked ()
561 {
562         /* Find common position of all selected content, if it exists */
563
564         optional<DCPTime> position;
565         BOOST_FOREACH (shared_ptr<Content> i, _parent->selected ()) {
566                 if (!position) {
567                         position = i->position();
568                 } else {
569                         if (position.get() != i->position()) {
570                                 position.reset ();
571                                 break;
572                         }
573                 }
574         }
575
576         MoveToDialog* d = new MoveToDialog (this, position, _parent->film());
577
578         if (d->ShowModal() == wxID_OK) {
579                 BOOST_FOREACH (shared_ptr<Content> i, _parent->selected ()) {
580                         i->set_position (d->position ());
581                 }
582         }
583         d->Destroy ();
584 }