Add some TRANSLATORS comments.
[dcpomatic.git] / src / wx / timing_panel.cc
1 /*
2     Copyright (C) 2012-2016 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 "timing_panel.h"
21 #include "wx_util.h"
22 #include "film_viewer.h"
23 #include "timecode.h"
24 #include "content_panel.h"
25 #include "lib/content.h"
26 #include "lib/image_content.h"
27 #include "lib/raw_convert.h"
28 #include "lib/subtitle_content.h"
29 #include "lib/dcp_subtitle_content.h"
30 #include "lib/audio_content.h"
31 #include "lib/text_subtitle_content.h"
32 #include <boost/foreach.hpp>
33 #include <set>
34 #include <iostream>
35
36 using std::cout;
37 using std::string;
38 using std::set;
39 using boost::shared_ptr;
40 using boost::dynamic_pointer_cast;
41 using boost::optional;
42
43 TimingPanel::TimingPanel (ContentPanel* p, FilmViewer* viewer)
44         /* horrid hack for apparent lack of context support with wxWidgets i18n code */
45         /// TRANSLATORS: translate the word "Timing" here; do not include the "Timing|" prefix
46         : ContentSubPanel (p, S_("Timing|Timing"))
47         , _viewer (viewer)
48 {
49         wxFlexGridSizer* grid = new wxFlexGridSizer (2, 4, 4);
50         _sizer->Add (grid, 0, wxALL, 8);
51
52         wxSize size = TimecodeBase::size (this);
53
54         wxSizer* labels = new wxBoxSizer (wxHORIZONTAL);
55         //// TRANSLATORS: this is an abbreviation for "hours"
56         wxStaticText* t = new wxStaticText (this, wxID_ANY, _("h"), wxDefaultPosition, size, wxALIGN_CENTRE_HORIZONTAL);
57 #ifdef DCPOMATIC_LINUX
58         /* Hack to work around failure to centre text on GTK */
59         gtk_label_set_line_wrap (GTK_LABEL (t->GetHandle()), FALSE);
60 #endif
61         labels->Add (t, 1, wxEXPAND);
62         add_label_to_sizer (labels, this, wxT (":"), false);
63         //// TRANSLATORS: this is an abbreviation for "minutes"
64         t = new wxStaticText (this, wxID_ANY, _("m"), wxDefaultPosition, size, wxALIGN_CENTRE_HORIZONTAL);
65 #ifdef DCPOMATIC_LINUX
66         gtk_label_set_line_wrap (GTK_LABEL (t->GetHandle()), FALSE);
67 #endif
68         labels->Add (t, 1, wxEXPAND);
69         add_label_to_sizer (labels, this, wxT (":"), false);
70         //// TRANSLATORS: this is an abbreviation for "seconds"
71         t = new wxStaticText (this, wxID_ANY, _("s"), wxDefaultPosition, size, wxALIGN_CENTRE_HORIZONTAL);
72 #ifdef DCPOMATIC_LINUX
73         gtk_label_set_line_wrap (GTK_LABEL (t->GetHandle()), FALSE);
74 #endif
75         labels->Add (t, 1, wxEXPAND);
76         add_label_to_sizer (labels, this, wxT (":"), false);
77         //// TRANSLATORS: this is an abbreviation for "frames"
78         t = new wxStaticText (this, wxID_ANY, _("f"), wxDefaultPosition, size, wxALIGN_CENTRE_HORIZONTAL);
79 #ifdef DCPOMATIC_LINUX
80         gtk_label_set_line_wrap (GTK_LABEL (t->GetHandle()), FALSE);
81 #endif
82         labels->Add (t, 1, wxEXPAND);
83         grid->Add (new wxStaticText (this, wxID_ANY, wxT ("")));
84         grid->Add (labels);
85
86         add_label_to_sizer (grid, this, _("Position"), true);
87         _position = new Timecode<DCPTime> (this);
88         grid->Add (_position);
89         add_label_to_sizer (grid, this, _("Full length"), true);
90         _full_length = new Timecode<DCPTime> (this);
91         grid->Add (_full_length);
92         add_label_to_sizer (grid, this, _("Trim from start"), true);
93         _trim_start = new Timecode<ContentTime> (this);
94         grid->Add (_trim_start);
95         _trim_start_to_playhead = new wxButton (this, wxID_ANY, _("Trim up to current position"));
96         grid->AddSpacer (0);
97         grid->Add (_trim_start_to_playhead);
98         add_label_to_sizer (grid, this, _("Trim from end"), true);
99         _trim_end = new Timecode<ContentTime> (this);
100         grid->Add (_trim_end);
101         _trim_end_to_playhead = new wxButton (this, wxID_ANY, _("Trim after current position"));
102         grid->AddSpacer (0);
103         grid->Add (_trim_end_to_playhead);
104         add_label_to_sizer (grid, this, _("Play length"), true);
105         _play_length = new Timecode<DCPTime> (this);
106         grid->Add (_play_length);
107
108         {
109                 add_label_to_sizer (grid, this, _("Video frame rate"), true);
110                 wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
111                 _video_frame_rate = new wxTextCtrl (this, wxID_ANY);
112                 s->Add (_video_frame_rate, 1, wxEXPAND);
113                 _set_video_frame_rate = new wxButton (this, wxID_ANY, _("Set"));
114                 _set_video_frame_rate->Enable (false);
115                 s->Add (_set_video_frame_rate, 0, wxLEFT | wxRIGHT, 8);
116                 grid->Add (s, 1, wxEXPAND);
117         }
118
119         grid->AddSpacer (0);
120
121         /* We can't use Wrap() here as it doesn't work with markup:
122          * http://trac.wxwidgets.org/ticket/13389
123          */
124
125         wxString in = _("<i>Only change this if it the content's frame rate has been read incorrectly.</i>");
126         wxString out;
127         int const width = 20;
128         int current = 0;
129         for (size_t i = 0; i < in.Length(); ++i) {
130                 if (in[i] == ' ' && current >= width) {
131                         out += '\n';
132                         current = 0;
133                 } else {
134                         out += in[i];
135                         ++current;
136                 }
137         }
138
139         t = new wxStaticText (this, wxID_ANY, wxT (""));
140         t->SetLabelMarkup (out);
141         grid->Add (t, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, 6);
142
143         _position->Changed.connect    (boost::bind (&TimingPanel::position_changed, this));
144         _full_length->Changed.connect (boost::bind (&TimingPanel::full_length_changed, this));
145         _trim_start->Changed.connect  (boost::bind (&TimingPanel::trim_start_changed, this));
146         _trim_start_to_playhead->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&TimingPanel::trim_start_to_playhead_clicked, this));
147         _trim_end->Changed.connect    (boost::bind (&TimingPanel::trim_end_changed, this));
148         _trim_end_to_playhead->Bind   (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&TimingPanel::trim_end_to_playhead_clicked, this));
149         _play_length->Changed.connect (boost::bind (&TimingPanel::play_length_changed, this));
150         _video_frame_rate->Bind       (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&TimingPanel::video_frame_rate_changed, this));
151         _set_video_frame_rate->Bind   (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&TimingPanel::set_video_frame_rate, this));
152
153         _viewer->ImageChanged.connect (boost::bind (&TimingPanel::setup_sensitivity, this));
154
155         setup_sensitivity ();
156 }
157
158 void
159 TimingPanel::update_full_length ()
160 {
161         set<DCPTime> check;
162         BOOST_FOREACH (shared_ptr<const Content> i, _parent->selected ()) {
163                 check.insert (i->full_length ());
164         }
165
166         if (check.size() == 1) {
167                 _full_length->set (_parent->selected().front()->full_length (), _parent->film()->video_frame_rate ());
168         } else {
169                 _full_length->clear ();
170         }
171 }
172
173 void
174 TimingPanel::update_play_length ()
175 {
176         set<DCPTime> check;
177         BOOST_FOREACH (shared_ptr<const Content> i, _parent->selected ()) {
178                 check.insert (i->length_after_trim ());
179         }
180
181         if (check.size() == 1) {
182                 _play_length->set (_parent->selected().front()->length_after_trim (), _parent->film()->video_frame_rate ());
183         } else {
184                 _play_length->clear ();
185         }
186 }
187
188 void
189 TimingPanel::film_content_changed (int property)
190 {
191         int const film_video_frame_rate = _parent->film()->video_frame_rate ();
192
193         /* Here we check to see if we have exactly one different value of various
194            properties, and fill the controls with that value if so.
195         */
196
197         if (property == ContentProperty::POSITION) {
198
199                 set<DCPTime> check;
200                 BOOST_FOREACH (shared_ptr<const Content> i, _parent->selected ()) {
201                         check.insert (i->position ());
202                 }
203
204                 if (check.size() == 1) {
205                         _position->set (_parent->selected().front()->position(), film_video_frame_rate);
206                 } else {
207                         _position->clear ();
208                 }
209
210         } else if (
211                 property == ContentProperty::LENGTH ||
212                 property == VideoContentProperty::VIDEO_FRAME_RATE ||
213                 property == VideoContentProperty::VIDEO_FRAME_TYPE ||
214                 property == AudioContentProperty::AUDIO_VIDEO_FRAME_RATE ||
215                 property == SubtitleContentProperty::SUBTITLE_VIDEO_FRAME_RATE
216                 ) {
217
218                 update_full_length ();
219
220         } else if (property == ContentProperty::TRIM_START) {
221
222                 set<ContentTime> check;
223                 BOOST_FOREACH (shared_ptr<const Content> i, _parent->selected ()) {
224                         check.insert (i->trim_start ());
225                 }
226
227                 if (check.size() == 1) {
228                         _trim_start->set (_parent->selected().front()->trim_start (), film_video_frame_rate);
229                 } else {
230                         _trim_start->clear ();
231                 }
232
233         } else if (property == ContentProperty::TRIM_END) {
234
235                 set<ContentTime> check;
236                 BOOST_FOREACH (shared_ptr<const Content> i, _parent->selected ()) {
237                         check.insert (i->trim_end ());
238                 }
239
240                 if (check.size() == 1) {
241                         _trim_end->set (_parent->selected().front()->trim_end (), film_video_frame_rate);
242                 } else {
243                         _trim_end->clear ();
244                 }
245         }
246
247         if (
248                 property == ContentProperty::LENGTH ||
249                 property == ContentProperty::TRIM_START ||
250                 property == ContentProperty::TRIM_END ||
251                 property == VideoContentProperty::VIDEO_FRAME_RATE ||
252                 property == VideoContentProperty::VIDEO_FRAME_TYPE ||
253                 property == AudioContentProperty::AUDIO_VIDEO_FRAME_RATE ||
254                 property == SubtitleContentProperty::SUBTITLE_VIDEO_FRAME_RATE
255                 ) {
256
257                 update_play_length ();
258         }
259
260         if (property == VideoContentProperty::VIDEO_FRAME_RATE || property == SubtitleContentProperty::SUBTITLE_VIDEO_FRAME_RATE) {
261                 set<double> check_vc;
262                 shared_ptr<const VideoContent> vc;
263                 int count_ac = 0;
264                 shared_ptr<const AudioContent> ac;
265                 int count_sc = 0;
266                 shared_ptr<const SubtitleContent> sc;
267                 BOOST_FOREACH (shared_ptr<const Content> i, _parent->selected ()) {
268                         shared_ptr<const VideoContent> vt = dynamic_pointer_cast<const VideoContent> (i);
269                         if (vt) {
270                                 check_vc.insert (vt->video_frame_rate ());
271                                 vc = vt;
272                         }
273                         shared_ptr<const AudioContent> at = dynamic_pointer_cast<const AudioContent> (i);
274                         if (at) {
275                                 ++count_ac;
276                                 ac = at;
277                         }
278                         shared_ptr<const SubtitleContent> st = dynamic_pointer_cast<const SubtitleContent> (i);
279                         if (st) {
280                                 ++count_sc;
281                                 sc = st;
282                         }
283
284                 }
285
286                 bool const single_frame_image_content = vc && dynamic_pointer_cast<const ImageContent> (vc) && vc->number_of_paths() == 1;
287
288                 if ((check_vc.size() == 1 || count_ac == 1 || count_sc == 1) && !single_frame_image_content) {
289                         if (vc) {
290                                 checked_set (_video_frame_rate, raw_convert<string> (vc->video_frame_rate (), 5));
291                         } else if (ac) {
292                                 checked_set (_video_frame_rate, raw_convert<string> (ac->audio_video_frame_rate (), 5));
293                         } else if (sc) {
294                                 checked_set (_video_frame_rate, raw_convert<string> (sc->subtitle_video_frame_rate (), 5));
295                         }
296                         _video_frame_rate->Enable (true);
297                 } else {
298                         checked_set (_video_frame_rate, wxT (""));
299                         _video_frame_rate->Enable (false);
300                 }
301         }
302
303         bool have_still = false;
304         BOOST_FOREACH (shared_ptr<const Content> i, _parent->selected ()) {
305                 shared_ptr<const ImageContent> ic = dynamic_pointer_cast<const ImageContent> (i);
306                 if (ic && ic->still ()) {
307                         have_still = true;
308                 }
309         }
310
311         _full_length->set_editable (have_still);
312         _play_length->set_editable (!have_still);
313         _set_video_frame_rate->Enable (false);
314         setup_sensitivity ();
315 }
316
317 void
318 TimingPanel::position_changed ()
319 {
320         BOOST_FOREACH (shared_ptr<Content> i, _parent->selected ()) {
321                 i->set_position (_position->get (_parent->film()->video_frame_rate ()));
322         }
323 }
324
325 void
326 TimingPanel::full_length_changed ()
327 {
328         BOOST_FOREACH (shared_ptr<Content> i, _parent->selected ()) {
329                 shared_ptr<ImageContent> ic = dynamic_pointer_cast<ImageContent> (i);
330                 if (ic && ic->still ()) {
331                         int const vfr = _parent->film()->video_frame_rate ();
332                         ic->set_video_length (_full_length->get (vfr).frames_round (vfr));
333                 }
334         }
335 }
336
337 void
338 TimingPanel::trim_start_changed ()
339 {
340         DCPTime const ph = _viewer->position ();
341
342         _viewer->set_coalesce_player_changes (true);
343
344         shared_ptr<Content> ref;
345         optional<FrameRateChange> ref_frc;
346         optional<DCPTime> ref_ph;
347         BOOST_FOREACH (shared_ptr<Content> i, _parent->selected ()) {
348                 if (i->position() <= ph && ph < i->end()) {
349                         /* The playhead is in i.  Use it as a reference to work out
350                            where to put the playhead post-trim; we're trying to keep the playhead
351                            at the same frame of content that we're looking at pre-trim.
352                         */
353                         ref = i;
354                         ref_frc = _parent->film()->active_frame_rate_change (i->position ());
355                         ref_ph = ph - i->position() + DCPTime (i->trim_start(), ref_frc.get());
356                 }
357
358                 i->set_trim_start (_trim_start->get (_parent->film()->video_frame_rate ()));
359         }
360
361         if (ref) {
362                 _viewer->set_position (max (DCPTime(), ref_ph.get() + ref->position() - DCPTime (ref->trim_start(), ref_frc.get())));
363         }
364
365         _viewer->set_coalesce_player_changes (false);
366 }
367
368 void
369 TimingPanel::trim_end_changed ()
370 {
371         _viewer->set_coalesce_player_changes (true);
372
373         BOOST_FOREACH (shared_ptr<Content> i, _parent->selected ()) {
374                 i->set_trim_end (_trim_end->get (_parent->film()->video_frame_rate ()));
375         }
376
377         /* XXX: maybe playhead-off-the-end-of-the-film should be handled elsewhere */
378         if (_viewer->position() >= _parent->film()->length()) {
379                 _viewer->set_position (_parent->film()->length() - DCPTime::from_frames (1, _parent->film()->video_frame_rate()));
380         }
381
382         _viewer->set_coalesce_player_changes (true);
383 }
384
385 void
386 TimingPanel::play_length_changed ()
387 {
388         BOOST_FOREACH (shared_ptr<Content> i, _parent->selected ()) {
389                 FrameRateChange const frc = _parent->film()->active_frame_rate_change (i->position ());
390                 i->set_trim_end (
391                         ContentTime (i->full_length() - _play_length->get (_parent->film()->video_frame_rate()), frc)
392                         - i->trim_start ()
393                         );
394         }
395 }
396
397 void
398 TimingPanel::video_frame_rate_changed ()
399 {
400         _set_video_frame_rate->Enable (true);
401 }
402
403 void
404 TimingPanel::set_video_frame_rate ()
405 {
406         BOOST_FOREACH (shared_ptr<Content> i, _parent->selected ()) {
407                 shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (i);
408                 shared_ptr<AudioContent> ac = dynamic_pointer_cast<AudioContent> (i);
409                 shared_ptr<DCPSubtitleContent> dsc = dynamic_pointer_cast<DCPSubtitleContent> (i);
410                 shared_ptr<TextSubtitleContent> tsc = dynamic_pointer_cast<TextSubtitleContent> (i);
411                 double const fr = raw_convert<double> (wx_to_std (_video_frame_rate->GetValue ()));
412                 if (vc) {
413                         vc->set_video_frame_rate (fr);
414                 } else if (ac) {
415                         /* Audio but not video, i.e. SndfileContent */
416                         ac->set_audio_video_frame_rate (fr);
417                 } else if (dsc) {
418                         dsc->set_subtitle_video_frame_rate (fr);
419                 } else if (tsc) {
420                         tsc->set_subtitle_video_frame_rate (fr);
421                 }
422                 _set_video_frame_rate->Enable (false);
423         }
424 }
425
426 void
427 TimingPanel::content_selection_changed ()
428 {
429         setup_sensitivity ();
430
431         film_content_changed (ContentProperty::POSITION);
432         film_content_changed (ContentProperty::LENGTH);
433         film_content_changed (ContentProperty::TRIM_START);
434         film_content_changed (ContentProperty::TRIM_END);
435         film_content_changed (VideoContentProperty::VIDEO_FRAME_RATE);
436         film_content_changed (SubtitleContentProperty::SUBTITLE_VIDEO_FRAME_RATE);
437         film_content_changed (AudioContentProperty::AUDIO_VIDEO_FRAME_RATE);
438 }
439
440 void
441 TimingPanel::film_changed (Film::Property p)
442 {
443         if (p == Film::VIDEO_FRAME_RATE) {
444                 update_full_length ();
445                 update_play_length ();
446         }
447 }
448
449 void
450 TimingPanel::trim_start_to_playhead_clicked ()
451 {
452         DCPTime const ph = _viewer->position ();
453         optional<DCPTime> new_ph;
454
455         _viewer->set_coalesce_player_changes (true);
456
457         BOOST_FOREACH (shared_ptr<Content> i, _parent->selected ()) {
458                 if (i->position() < ph && ph < i->end ()) {
459                         FrameRateChange const frc = _parent->film()->active_frame_rate_change (i->position ());
460                         i->set_trim_start (i->trim_start() + ContentTime (ph - i->position (), frc));
461                         new_ph = i->position ();
462                 }
463         }
464
465         if (new_ph) {
466                 _viewer->set_position (new_ph.get());
467         }
468
469         _viewer->set_coalesce_player_changes (false);
470 }
471
472 void
473 TimingPanel::trim_end_to_playhead_clicked ()
474 {
475         DCPTime const ph = _viewer->position ();
476         BOOST_FOREACH (shared_ptr<Content> i, _parent->selected ()) {
477                 if (i->position() < ph && ph < i->end ()) {
478                         FrameRateChange const frc = _parent->film()->active_frame_rate_change (i->position ());
479                         i->set_trim_end (ContentTime (i->position() + i->full_length() - ph - DCPTime::from_frames (1, frc.dcp), frc) - i->trim_start());
480                 }
481         }
482 }
483
484 void
485 TimingPanel::setup_sensitivity ()
486 {
487         bool const e = !_parent->selected().empty ();
488
489         _position->Enable (e);
490         _full_length->Enable (e);
491         _trim_start->Enable (e);
492         _trim_end->Enable (e);
493         _play_length->Enable (e);
494         _video_frame_rate->Enable (e);
495
496         DCPTime const ph = _viewer->position ();
497         bool any_over_ph = false;
498         BOOST_FOREACH (shared_ptr<const Content> i, _parent->selected ()) {
499                 if (i->position() <= ph && ph < i->end()) {
500                         any_over_ph = true;
501                 }
502         }
503
504         _trim_start_to_playhead->Enable (any_over_ph);
505         _trim_end_to_playhead->Enable (any_over_ph);
506 }