Missed update to private test repo version.
[dcpomatic.git] / src / wx / text_panel.cc
1 /*
2     Copyright (C) 2012-2021 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
22 #include "check_box.h"
23 #include "content_panel.h"
24 #include "dcp_text_track_dialog.h"
25 #include "dcpomatic_button.h"
26 #include "dcpomatic_spin_ctrl.h"
27 #include "film_editor.h"
28 #include "film_viewer.h"
29 #include "fonts_dialog.h"
30 #include "language_tag_widget.h"
31 #include "static_text.h"
32 #include "subtitle_appearance_dialog.h"
33 #include "text_panel.h"
34 #include "text_view.h"
35 #include "wx_util.h"
36 #include "lib/analyse_subtitles_job.h"
37 #include "lib/dcp_content.h"
38 #include "lib/dcp_subtitle_content.h"
39 #include "lib/dcp_subtitle_decoder.h"
40 #include "lib/decoder_factory.h"
41 #include "lib/ffmpeg_content.h"
42 #include "lib/ffmpeg_subtitle_stream.h"
43 #include "lib/film.h"
44 #include "lib/job_manager.h"
45 #include "lib/string_text_file_content.h"
46 #include "lib/string_text_file_decoder.h"
47 #include "lib/subtitle_analysis.h"
48 #include "lib/text_content.h"
49 #include <dcp/filesystem.h>
50 #include <dcp/scope_guard.h>
51 #include <dcp/warnings.h>
52 LIBDCP_DISABLE_WARNINGS
53 #include <wx/spinctrl.h>
54 LIBDCP_ENABLE_WARNINGS
55
56
57 using std::cout;
58 using std::dynamic_pointer_cast;
59 using std::list;
60 using std::shared_ptr;
61 using std::string;
62 using std::vector;
63 using boost::bind;
64 using boost::optional;
65 #if BOOST_VERSION >= 106100
66 using namespace boost::placeholders;
67 #endif
68
69
70 /** @param t Original text type of the content, if known */
71 TextPanel::TextPanel (ContentPanel* p, TextType t)
72         : ContentSubPanel (p, std_to_wx(text_type_to_name(t)))
73         , _original_type (t)
74 {
75
76 }
77
78
79 void
80 TextPanel::create ()
81 {
82         wxString refer = _("Use this DCP's subtitle as OV and make VF");
83         if (_original_type == TextType::CLOSED_CAPTION) {
84                 refer = _("Use this DCP's closed caption as OV and make VF");
85         }
86
87         _reference = new CheckBox (this, refer);
88         _reference_note = new StaticText (this, wxT(""));
89         _reference_note->Wrap (200);
90         auto font = _reference_note->GetFont();
91         font.SetStyle(wxFONTSTYLE_ITALIC);
92         font.SetPointSize(font.GetPointSize() - 1);
93         _reference_note->SetFont(font);
94
95         _use = new CheckBox (this, _("Use as"));
96         _type = new wxChoice (this, wxID_ANY);
97         _type->Append (_("open subtitles"));
98         _type->Append (_("closed captions"));
99
100         _burn = new CheckBox (this, _("Burn subtitles into image"));
101
102         _offset_label = create_label (this, _("Offset"), true);
103         _x_offset_label = create_label (this, _("X"), true);
104         _x_offset = new SpinCtrl (this, DCPOMATIC_SPIN_CTRL_WIDTH);
105         _x_offset_pc_label = new StaticText (this, _("%"));
106         _y_offset_label = create_label (this, _("Y"), true);
107         _y_offset = new SpinCtrl (this, DCPOMATIC_SPIN_CTRL_WIDTH);
108         _y_offset_pc_label = new StaticText (this, _("%"));
109
110         _scale_label = create_label (this, _("Scale"), true);
111         _x_scale_label = create_label (this, _("X"), true);
112         _x_scale = new SpinCtrl (this, DCPOMATIC_SPIN_CTRL_WIDTH);
113         _x_scale_pc_label = new StaticText (this, _("%"));
114         _y_scale_label = create_label (this, S_("Coord|Y"), true);
115         _y_scale = new SpinCtrl (this, DCPOMATIC_SPIN_CTRL_WIDTH);
116         _y_scale_pc_label = new StaticText (this, _("%"));
117
118         _line_spacing_label = create_label (this, _("Line spacing"), true);
119         _line_spacing = new SpinCtrl (this, DCPOMATIC_SPIN_CTRL_WIDTH);
120         _line_spacing_pc_label = new StaticText (this, _("%"));
121
122         _stream_label = create_label (this, _("Stream"), true);
123         _stream = new wxChoice (this, wxID_ANY);
124
125         _text_view_button = new Button (this, _("View..."));
126         _fonts_dialog_button = new Button (this, _("Fonts..."));
127         _appearance_dialog_button = new Button (this, _("Appearance..."));
128
129         _x_offset->SetRange (-100, 100);
130         _y_offset->SetRange (-100, 100);
131         _x_scale->SetRange (0, 1000);
132         _y_scale->SetRange (0, 1000);
133         _line_spacing->SetRange (0, 1000);
134
135         _reference->bind(&TextPanel::reference_clicked, this);
136         _use->bind(&TextPanel::use_toggled, this);
137         _type->Bind                     (wxEVT_CHOICE,   boost::bind (&TextPanel::type_changed, this));
138         _burn->bind(&TextPanel::burn_toggled, this);
139         _x_offset->Bind                 (wxEVT_SPINCTRL, boost::bind (&TextPanel::x_offset_changed, this));
140         _y_offset->Bind                 (wxEVT_SPINCTRL, boost::bind (&TextPanel::y_offset_changed, this));
141         _x_scale->Bind                  (wxEVT_SPINCTRL, boost::bind (&TextPanel::x_scale_changed, this));
142         _y_scale->Bind                  (wxEVT_SPINCTRL, boost::bind (&TextPanel::y_scale_changed, this));
143         _line_spacing->Bind             (wxEVT_SPINCTRL, boost::bind (&TextPanel::line_spacing_changed, this));
144         _stream->Bind                   (wxEVT_CHOICE,   boost::bind (&TextPanel::stream_changed, this));
145         _text_view_button->Bind         (wxEVT_BUTTON,   boost::bind (&TextPanel::text_view_clicked, this));
146         _fonts_dialog_button->Bind      (wxEVT_BUTTON,   boost::bind (&TextPanel::fonts_dialog_clicked, this));
147         _appearance_dialog_button->Bind (wxEVT_BUTTON,   boost::bind (&TextPanel::appearance_dialog_clicked, this));
148
149         add_to_grid();
150         content_selection_changed ();
151
152         _sizer->Layout ();
153 }
154
155
156 void
157 TextPanel::setup_visibility ()
158 {
159         switch (current_type()) {
160         case TextType::OPEN_SUBTITLE:
161                 if (_dcp_track_label) {
162                         _dcp_track_label->Destroy ();
163                         _dcp_track_label = nullptr;
164                 }
165                 if (_dcp_track) {
166                         _dcp_track->Destroy ();
167                         _dcp_track = nullptr;
168                 }
169                 if (!_outline_subtitles) {
170                         _outline_subtitles = new CheckBox (this, _("Show subtitle area"));
171                         _outline_subtitles->bind(&TextPanel::outline_subtitles_changed, this);
172                         _grid->Add (_outline_subtitles, wxGBPosition(_outline_subtitles_row, 0), wxGBSpan(1, 2));
173                 }
174                 if (!_language) {
175                         _language_label = create_label (this, _("Language"), true);
176                         add_label_to_sizer (_grid, _language_label, true, wxGBPosition(_ccap_track_or_language_row, 0));
177                         _language_sizer = new wxBoxSizer (wxHORIZONTAL);
178                         _language = new LanguageTagWidget (this, _("Language of these subtitles"), boost::none, wxString("en-US-"));
179                         _language->Changed.connect (boost::bind(&TextPanel::language_changed, this));
180                         _language_sizer->Add (_language->sizer(), 1, wxRIGHT, DCPOMATIC_SIZER_GAP);
181                         _language_type = new wxChoice (this, wxID_ANY);
182                         /// TRANSLATORS: Main and Additional here are a choice for whether a set of subtitles is in the "main" language of the
183                         /// film or an "additional" language.
184                         _language_type->Append (_("Main"));
185                         _language_type->Append (_("Additional"));
186                         _language_type->Bind (wxEVT_CHOICE, boost::bind(&TextPanel::language_is_additional_changed, this));
187                         _language_sizer->Add (_language_type, 0, wxALIGN_CENTER_VERTICAL | wxTOP, DCPOMATIC_CHOICE_TOP_PAD);
188                         _grid->Add (_language_sizer, wxGBPosition(_ccap_track_or_language_row, 1), wxGBSpan(1, 2));
189                         film_content_changed (TextContentProperty::LANGUAGE);
190                         film_content_changed (TextContentProperty::LANGUAGE_IS_ADDITIONAL);
191                 }
192                 break;
193         case TextType::CLOSED_CAPTION:
194                 if (_language_label) {
195                         _language_label->Destroy ();
196                         _language_label = nullptr;
197                         _grid->Remove (_language->sizer());
198                         delete _language;
199                         _grid->Remove (_language_sizer);
200                         _language_sizer = nullptr;
201                         _language = nullptr;
202                         _language_type->Destroy ();
203                         _language_type = nullptr;
204                 }
205                 if (!_dcp_track_label) {
206                         _dcp_track_label = create_label (this, _("CCAP track"), true);
207                         add_label_to_sizer (_grid, _dcp_track_label, true, wxGBPosition(_ccap_track_or_language_row, 0));
208                 }
209                 if (!_dcp_track) {
210                         _dcp_track = new wxChoice (this, wxID_ANY);
211                         _dcp_track->Bind (wxEVT_CHOICE, boost::bind(&TextPanel::dcp_track_changed, this));
212                         _grid->Add (_dcp_track, wxGBPosition(_ccap_track_or_language_row, 1), wxDefaultSpan, wxEXPAND);
213                         update_dcp_tracks ();
214                         film_content_changed (TextContentProperty::DCP_TRACK);
215                 }
216                 if (_outline_subtitles) {
217                         _outline_subtitles->Destroy ();
218                         _outline_subtitles = nullptr;
219                         clear_outline_subtitles ();
220                 }
221                 break;
222         default:
223                 break;
224         }
225
226         _grid->Layout ();
227 }
228
229
230 void
231 TextPanel::add_to_grid ()
232 {
233         int r = 0;
234
235         auto reference_sizer = new wxBoxSizer (wxVERTICAL);
236         reference_sizer->Add (_reference, 0);
237         reference_sizer->Add (_reference_note, 0);
238         _grid->Add (reference_sizer, wxGBPosition(r, 0), wxGBSpan(1, 4));
239         ++r;
240
241         auto use = new wxBoxSizer (wxHORIZONTAL);
242         use->Add (_use, 0, wxEXPAND | wxRIGHT, DCPOMATIC_SIZER_GAP);
243         use->Add (_type, 1, wxEXPAND, 0);
244         _grid->Add (use, wxGBPosition (r, 0), wxGBSpan (1, 2));
245         ++r;
246
247         _grid->Add (_burn, wxGBPosition (r, 0), wxGBSpan (1, 2));
248         ++r;
249
250         _outline_subtitles_row = r;
251         ++r;
252
253         add_label_to_sizer (_grid, _offset_label, true, wxGBPosition (r, 0));
254         auto offset = new wxBoxSizer (wxHORIZONTAL);
255         add_label_to_sizer (offset, _x_offset_label, true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTER_VERTICAL);
256         offset->Add (_x_offset, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL, DCPOMATIC_SIZER_X_GAP);
257         offset->Add (_x_offset_pc_label, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL, DCPOMATIC_SIZER_X_GAP * 2);
258 #ifdef __WXGTK3__
259         _grid->Add (offset, wxGBPosition(r, 1));
260         ++r;
261         offset = new wxBoxSizer (wxHORIZONTAL);
262 #endif
263         add_label_to_sizer (offset, _y_offset_label, true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTER_VERTICAL);
264         offset->Add (_y_offset, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL, DCPOMATIC_SIZER_X_GAP);
265         add_label_to_sizer (offset, _y_offset_pc_label, false, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL);
266         _grid->Add (offset, wxGBPosition (r, 1));
267         ++r;
268
269         add_label_to_sizer (_grid, _scale_label, true, wxGBPosition (r, 0));
270         auto scale = new wxBoxSizer (wxHORIZONTAL);
271         add_label_to_sizer (scale, _x_scale_label, true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTER_VERTICAL);
272         scale->Add (_x_scale, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL, DCPOMATIC_SIZER_X_GAP);
273         scale->Add (_x_scale_pc_label, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL, DCPOMATIC_SIZER_X_GAP * 2);
274 #ifdef __WXGTK3__
275         _grid->Add (scale, wxGBPosition(r, 1));
276         ++r;
277         scale = new wxBoxSizer (wxHORIZONTAL);
278 #endif
279         add_label_to_sizer (scale, _y_scale_label, true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTER_VERTICAL);
280         scale->Add (_y_scale, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL, DCPOMATIC_SIZER_X_GAP);
281         add_label_to_sizer (scale, _y_scale_pc_label, false, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL);
282         _grid->Add (scale, wxGBPosition (r, 1));
283         ++r;
284
285         {
286                 add_label_to_sizer (_grid, _line_spacing_label, true, wxGBPosition (r, 0));
287                 auto s = new wxBoxSizer (wxHORIZONTAL);
288                 s->Add (_line_spacing);
289                 add_label_to_sizer (s, _line_spacing_pc_label, false, 0, wxLEFT | wxRIGHT | wxALIGN_CENTER_VERTICAL);
290                 _grid->Add (s, wxGBPosition (r, 1));
291                 ++r;
292         }
293
294         _ccap_track_or_language_row = r;
295         ++r;
296
297         add_label_to_sizer (_grid, _stream_label, true, wxGBPosition (r, 0));
298         _grid->Add (_stream, wxGBPosition (r, 1));
299         ++r;
300
301         {
302                 auto s = new wxBoxSizer (wxHORIZONTAL);
303
304                 s->Add (_text_view_button, 0, wxALL, DCPOMATIC_SIZER_GAP);
305                 s->Add (_fonts_dialog_button, 0, wxALL, DCPOMATIC_SIZER_GAP);
306                 s->Add (_appearance_dialog_button, 0, wxALL, DCPOMATIC_SIZER_GAP);
307
308                 _grid->Add (s, wxGBPosition(r, 0), wxGBSpan(1, 2));
309                 ++r;
310         }
311
312         setup_visibility ();
313 }
314
315
316 void
317 TextPanel::update_dcp_track_selection ()
318 {
319         DCPOMATIC_ASSERT (_dcp_track);
320
321         optional<DCPTextTrack> selected;
322         bool many = false;
323         for (auto i: _parent->selected_text()) {
324                 auto t = i->text_of_original_type(_original_type);
325                 if (t) {
326                         auto dt = t->dcp_track();
327                         if (dt && selected && *dt != *selected) {
328                                 many = true;
329                         } else if (!selected) {
330                                 selected = dt;
331                         }
332                 }
333         }
334
335         int n = 0;
336         for (auto i: _parent->film()->closed_caption_tracks()) {
337                 if (!many && selected && *selected == i) {
338                         _dcp_track->SetSelection (n);
339                 }
340                 ++n;
341         }
342
343         if (!selected || many) {
344                 _dcp_track->SetSelection (wxNOT_FOUND);
345         }
346 }
347
348
349 void
350 TextPanel::update_dcp_tracks ()
351 {
352         DCPOMATIC_ASSERT (_dcp_track);
353
354         _dcp_track->Clear ();
355         for (auto i: _parent->film()->closed_caption_tracks()) {
356                 /* XXX: don't display the "magic" track which has empty name and language;
357                    this is a nasty hack (see also Film::closed_caption_tracks)
358                 */
359                 if (!i.name.empty() || i.language) {
360                         _dcp_track->Append (std_to_wx(i.summary()));
361                 }
362         }
363
364         if (_parent->film()->closed_caption_tracks().size() < 6) {
365                 _dcp_track->Append (_("Add new..."));
366         }
367
368         update_dcp_track_selection ();
369 }
370
371
372 void
373 TextPanel::dcp_track_changed ()
374 {
375         optional<DCPTextTrack> track;
376
377         if (_dcp_track->GetSelection() == int(_dcp_track->GetCount()) - 1) {
378                 auto d = make_wx<DCPTextTrackDialog>(this);
379                 if (d->ShowModal() == wxID_OK) {
380                         track = d->get();
381                 }
382         } else {
383                 /* Find the DCPTextTrack that was selected */
384                 for (auto i: _parent->film()->closed_caption_tracks()) {
385                         if (i.summary() == wx_to_std(_dcp_track->GetStringSelection())) {
386                                 track = i;
387                         }
388                 }
389         }
390
391         if (track) {
392                 for (auto i: _parent->selected_text()) {
393                         auto t = i->text_of_original_type(_original_type);
394                         if (t && t->type() == TextType::CLOSED_CAPTION) {
395                                 t->set_dcp_track(*track);
396                         }
397                 }
398         }
399
400         update_dcp_tracks ();
401 }
402
403
404 void
405 TextPanel::film_changed(FilmProperty property)
406 {
407         if (property == FilmProperty::CONTENT || property == FilmProperty::REEL_TYPE || property == FilmProperty::INTEROP) {
408                 setup_sensitivity ();
409         }
410 }
411
412
413 void
414 TextPanel::film_content_changed (int property)
415 {
416         auto fc = _parent->selected_ffmpeg ();
417         auto sc = _parent->selected_text ();
418
419         shared_ptr<FFmpegContent> fcs;
420         if (fc.size() == 1) {
421                 fcs = fc.front ();
422         }
423
424         shared_ptr<Content> scs;
425         if (sc.size() == 1) {
426                 scs = sc.front ();
427         }
428
429         shared_ptr<TextContent> text;
430         if (scs) {
431                 text = scs->text_of_original_type(_original_type);
432         }
433
434         if (property == FFmpegContentProperty::SUBTITLE_STREAMS) {
435                 _stream->Clear ();
436                 if (fcs) {
437                         for (auto i: fcs->subtitle_streams()) {
438                                 _stream->Append (std_to_wx(i->name), new wxStringClientData(std_to_wx(i->identifier())));
439                         }
440
441                         if (fcs->subtitle_stream()) {
442                                 checked_set (_stream, fcs->subtitle_stream()->identifier ());
443                         } else {
444                                 _stream->SetSelection (wxNOT_FOUND);
445                         }
446                 }
447                 setup_sensitivity ();
448                 clear_outline_subtitles ();
449         } else if (property == TextContentProperty::USE) {
450                 checked_set (_use, text ? text->use() : false);
451                 setup_sensitivity ();
452                 clear_outline_subtitles ();
453         } else if (property == TextContentProperty::TYPE) {
454                 if (text) {
455                         switch (text->type()) {
456                         case TextType::OPEN_SUBTITLE:
457                                 _type->SetSelection (0);
458                                 break;
459                         case TextType::CLOSED_CAPTION:
460                                 _type->SetSelection (1);
461                                 break;
462                         default:
463                                 DCPOMATIC_ASSERT (false);
464                         }
465                 } else {
466                         _type->SetSelection (0);
467                 }
468                 setup_sensitivity ();
469                 setup_visibility ();
470         } else if (property == TextContentProperty::BURN) {
471                 checked_set (_burn, text ? text->burn() : false);
472         } else if (property == TextContentProperty::X_OFFSET) {
473                 checked_set (_x_offset, text ? lrint (text->x_offset() * 100) : 0);
474                 update_outline_subtitles_in_viewer ();
475         } else if (property == TextContentProperty::Y_OFFSET) {
476                 checked_set (_y_offset, text ? lrint (text->y_offset() * 100) : 0);
477                 update_outline_subtitles_in_viewer ();
478         } else if (property == TextContentProperty::X_SCALE) {
479                 checked_set (_x_scale, text ? lrint (text->x_scale() * 100) : 100);
480                 clear_outline_subtitles ();
481         } else if (property == TextContentProperty::Y_SCALE) {
482                 checked_set (_y_scale, text ? lrint (text->y_scale() * 100) : 100);
483                 clear_outline_subtitles ();
484         } else if (property == TextContentProperty::LINE_SPACING) {
485                 checked_set (_line_spacing, text ? lrint (text->line_spacing() * 100) : 100);
486                 clear_outline_subtitles ();
487         } else if (property == TextContentProperty::DCP_TRACK) {
488                 if (_dcp_track) {
489                         update_dcp_track_selection ();
490                 }
491         } else if (property == TextContentProperty::LANGUAGE) {
492                 if (_language) {
493                         _language->set (text ? text->language() : boost::none);
494                 }
495         } else if (property == TextContentProperty::LANGUAGE_IS_ADDITIONAL) {
496                 if (_language_type) {
497                         _language_type->SetSelection (text ? (text->language_is_additional() ? 1 : 0) : 0);
498                 }
499         } else if (property == DCPContentProperty::REFERENCE_TEXT) {
500                 if (scs) {
501                         auto dcp = dynamic_pointer_cast<DCPContent> (scs);
502                         checked_set (_reference, dcp ? dcp->reference_text(_original_type) : false);
503                 } else {
504                         checked_set (_reference, false);
505                 }
506
507                 setup_sensitivity ();
508         } else if (property == DCPContentProperty::TEXTS) {
509                 setup_sensitivity ();
510         } else if (property == ContentProperty::TRIM_START) {
511                 setup_sensitivity ();
512         }
513 }
514
515
516 void
517 TextPanel::use_toggled ()
518 {
519         for (auto i: _parent->selected_text()) {
520                 i->text_of_original_type(_original_type)->set_use (_use->GetValue());
521         }
522 }
523
524
525 /** @return the text type that is currently selected in the drop-down */
526 TextType
527 TextPanel::current_type () const
528 {
529         switch (_type->GetSelection()) {
530         case 0:
531                 return TextType::OPEN_SUBTITLE;
532         case 1:
533                 return TextType::CLOSED_CAPTION;
534         }
535
536         return TextType::UNKNOWN;
537 }
538
539
540 void
541 TextPanel::type_changed ()
542 {
543         for (auto i: _parent->selected_text()) {
544                 i->text_of_original_type(_original_type)->set_type (current_type ());
545         }
546
547         setup_visibility ();
548 }
549
550
551 void
552 TextPanel::burn_toggled ()
553 {
554         for (auto i: _parent->selected_text ()) {
555                 i->text_of_original_type(_original_type)->set_burn (_burn->GetValue());
556         }
557 }
558
559
560 void
561 TextPanel::setup_sensitivity ()
562 {
563         int any_subs = 0;
564         /* We currently assume that FFmpeg subtitles are bitmapped */
565         int ffmpeg_subs = 0;
566         /* DCP subs can't have their line spacing changed */
567         int dcp_subs = 0;
568         auto sel = _parent->selected_text ();
569         for (auto i: sel) {
570                 /* These are the content types that could include subtitles */
571                 auto fc = std::dynamic_pointer_cast<const FFmpegContent>(i);
572                 auto sc = std::dynamic_pointer_cast<const StringTextFileContent>(i);
573                 auto dc = std::dynamic_pointer_cast<const DCPContent>(i);
574                 auto dsc = std::dynamic_pointer_cast<const DCPSubtitleContent>(i);
575                 if (fc) {
576                         if (!fc->text.empty()) {
577                                 ++ffmpeg_subs;
578                                 ++any_subs;
579                         }
580                 } else if (dc || dsc) {
581                         ++dcp_subs;
582                         ++any_subs;
583                 } else if (sc) {
584                         /* XXX: in the future there could be bitmap subs from DCPs */
585                         ++any_subs;
586                 }
587         }
588
589         /* Decide whether we can reference these subs */
590
591         shared_ptr<DCPContent> dcp;
592         if (sel.size() == 1) {
593                 dcp = dynamic_pointer_cast<DCPContent>(sel.front());
594         }
595
596         string why_not;
597         bool const can_reference = dcp && dcp->can_reference_text (_parent->film(), _original_type, why_not);
598         wxString cannot;
599         if (why_not.empty()) {
600                 cannot = _("Cannot reference this DCP's subtitles or captions.");
601         } else {
602                 cannot = _("Cannot reference this DCP's subtitles or captions: ") + std_to_wx(why_not);
603         }
604         setup_refer_button (_reference, _reference_note, dcp, can_reference, cannot);
605
606         bool const reference = _reference->GetValue ();
607
608         auto const type = current_type ();
609
610         /* Set up _type */
611         _type->Clear ();
612         _type->Append (_("open subtitles"));
613         if (ffmpeg_subs == 0) {
614                 _type->Append (_("closed captions"));
615         }
616
617         switch (type) {
618         case TextType::OPEN_SUBTITLE:
619                 _type->SetSelection (0);
620                 break;
621         case TextType::CLOSED_CAPTION:
622                 if (_type->GetCount() > 1) {
623                         _type->SetSelection (1);
624                 }
625                 break;
626         default:
627                 break;
628         }
629
630         /* Set up sensitivity */
631         _use->Enable (!reference && any_subs > 0);
632         bool const use = _use->GetValue ();
633         if (_outline_subtitles) {
634                 _outline_subtitles->Enable (!_loading_analysis && any_subs && use && type == TextType::OPEN_SUBTITLE);
635         }
636         _type->Enable (!reference && any_subs > 0 && use);
637         _burn->Enable (!reference && any_subs > 0 && use && type == TextType::OPEN_SUBTITLE);
638         _x_offset->Enable (!reference && any_subs > 0 && use && type == TextType::OPEN_SUBTITLE);
639         _y_offset->Enable (!reference && any_subs > 0 && use && type == TextType::OPEN_SUBTITLE);
640         _x_scale->Enable (!reference && any_subs > 0 && use && type == TextType::OPEN_SUBTITLE);
641         _y_scale->Enable (!reference && any_subs > 0 && use && type == TextType::OPEN_SUBTITLE);
642         _line_spacing->Enable (!reference && use && type == TextType::OPEN_SUBTITLE && dcp_subs < any_subs);
643         _stream->Enable (!reference && ffmpeg_subs == 1);
644         /* Ideally we would check here to see if the FFmpeg content has "string" subs (i.e. not bitmaps) */
645         _text_view_button->Enable (!reference && any_subs > 0 && ffmpeg_subs == 0);
646         _fonts_dialog_button->Enable (!reference && any_subs > 0 && ffmpeg_subs == 0 && type == TextType::OPEN_SUBTITLE);
647         _appearance_dialog_button->Enable (!reference && any_subs > 0 && use && type == TextType::OPEN_SUBTITLE);
648 }
649
650
651 void
652 TextPanel::stream_changed ()
653 {
654         auto fc = _parent->selected_ffmpeg ();
655         if (fc.size() != 1) {
656                 return;
657         }
658
659         auto fcs = fc.front ();
660
661         auto a = fcs->subtitle_streams ();
662         auto i = a.begin ();
663         auto const s = string_client_data (_stream->GetClientObject(_stream->GetSelection()));
664         while (i != a.end() && (*i)->identifier () != s) {
665                 ++i;
666         }
667
668         if (i != a.end ()) {
669                 fcs->set_subtitle_stream (*i);
670         }
671 }
672
673
674 void
675 TextPanel::x_offset_changed ()
676 {
677         for (auto i: _parent->selected_text ()) {
678                 i->text_of_original_type(_original_type)->set_x_offset (_x_offset->GetValue() / 100.0);
679         }
680 }
681
682
683 void
684 TextPanel::y_offset_changed ()
685 {
686         for (auto i: _parent->selected_text ()) {
687                 i->text_of_original_type(_original_type)->set_y_offset (_y_offset->GetValue() / 100.0);
688         }
689 }
690
691
692 void
693 TextPanel::x_scale_changed ()
694 {
695         for (auto i: _parent->selected_text ()) {
696                 i->text_of_original_type(_original_type)->set_x_scale (_x_scale->GetValue() / 100.0);
697         }
698 }
699
700
701 void
702 TextPanel::y_scale_changed ()
703 {
704         for (auto i: _parent->selected_text ()) {
705                 i->text_of_original_type(_original_type)->set_y_scale (_y_scale->GetValue() / 100.0);
706         }
707 }
708
709
710 void
711 TextPanel::line_spacing_changed ()
712 {
713         for (auto i: _parent->selected_text ()) {
714                 i->text_of_original_type(_original_type)->set_line_spacing (_line_spacing->GetValue() / 100.0);
715         }
716 }
717
718
719 void
720 TextPanel::content_selection_changed ()
721 {
722         film_content_changed (FFmpegContentProperty::SUBTITLE_STREAMS);
723         film_content_changed (TextContentProperty::USE);
724         film_content_changed (TextContentProperty::BURN);
725         film_content_changed (TextContentProperty::X_OFFSET);
726         film_content_changed (TextContentProperty::Y_OFFSET);
727         film_content_changed (TextContentProperty::X_SCALE);
728         film_content_changed (TextContentProperty::Y_SCALE);
729         film_content_changed (TextContentProperty::LINE_SPACING);
730         film_content_changed (TextContentProperty::FONTS);
731         film_content_changed (TextContentProperty::TYPE);
732         film_content_changed (TextContentProperty::DCP_TRACK);
733         film_content_changed (TextContentProperty::LANGUAGE);
734         film_content_changed (TextContentProperty::LANGUAGE_IS_ADDITIONAL);
735         film_content_changed (DCPContentProperty::REFERENCE_TEXT);
736 }
737
738
739 void
740 TextPanel::text_view_clicked ()
741 {
742         auto c = _parent->selected_text ();
743         DCPOMATIC_ASSERT (c.size() == 1);
744
745         auto decoder = decoder_factory (_parent->film(), c.front(), false, false, shared_ptr<Decoder>());
746
747         if (decoder) {
748                 _text_view.reset(this, _parent->film(), c.front(), c.front()->text_of_original_type(_original_type), decoder, _parent->film_viewer());
749                 _text_view->Show ();
750         }
751 }
752
753
754 void
755 TextPanel::fonts_dialog_clicked ()
756 {
757         auto c = _parent->selected_text ();
758         DCPOMATIC_ASSERT (c.size() == 1);
759
760         _fonts_dialog.reset(this, c.front(), c.front()->text_of_original_type(_original_type));
761         _fonts_dialog->Show ();
762 }
763
764
765 void
766 TextPanel::reference_clicked ()
767 {
768         auto c = _parent->selected ();
769         if (c.size() != 1) {
770                 return;
771         }
772
773         auto d = dynamic_pointer_cast<DCPContent> (c.front ());
774         if (!d) {
775                 return;
776         }
777
778         d->set_reference_text (_original_type, _reference->GetValue ());
779 }
780
781
782 void
783 TextPanel::appearance_dialog_clicked ()
784 {
785         auto c = _parent->selected_text ();
786         DCPOMATIC_ASSERT (c.size() == 1);
787
788         SubtitleAppearanceDialog dialog(this, _parent->film(), c.front(), c.front()->text_of_original_type(_original_type));
789         if (dialog.ShowModal() == wxID_OK) {
790                 dialog.apply();
791         }
792 }
793
794
795 /** The user has clicked on the outline subtitles check box */
796 void
797 TextPanel::outline_subtitles_changed ()
798 {
799         if (_outline_subtitles->GetValue()) {
800                 _analysis_content = _parent->selected_text().front();
801                 try_to_load_analysis ();
802         } else {
803                 clear_outline_subtitles ();
804         }
805 }
806
807
808 void
809 TextPanel::try_to_load_analysis ()
810 {
811         if (_loading_analysis) {
812                 return;
813         }
814
815         _loading_analysis = true;
816         dcp::ScopeGuard sg = [this]() {
817                 _loading_analysis = false;
818                 setup_sensitivity();
819         };
820
821         setup_sensitivity ();
822         _analysis.reset ();
823
824         auto content = _analysis_content.lock ();
825         if (!content) {
826                 return;
827         }
828
829         auto const path = _parent->film()->subtitle_analysis_path(content);
830
831         if (!dcp::filesystem::exists(path)) {
832                 for (auto i: JobManager::instance()->get()) {
833                         if (dynamic_pointer_cast<AnalyseSubtitlesJob>(i) && !i->finished()) {
834                                 i->cancel ();
835                         }
836                 }
837
838                 JobManager::instance()->analyse_subtitles (
839                         _parent->film(), content, _analysis_finished_connection, bind(&TextPanel::analysis_finished, this, _1)
840                         );
841                 return;
842         }
843
844         try {
845                 _analysis.reset (new SubtitleAnalysis(path));
846         } catch (OldFormatError& e) {
847                 /* An old analysis file: recreate it */
848                 JobManager::instance()->analyse_subtitles (
849                         _parent->film(), content, _analysis_finished_connection, bind(&TextPanel::analysis_finished, this, _1)
850                         );
851                 return;
852         }
853
854         update_outline_subtitles_in_viewer ();
855 }
856
857
858 void
859 TextPanel::update_outline_subtitles_in_viewer ()
860 {
861         auto& fv = _parent->film_viewer();
862
863         if (_analysis) {
864                 auto rect = _analysis->bounding_box ();
865                 if (rect) {
866                         auto content = _analysis_content.lock ();
867                         DCPOMATIC_ASSERT (content);
868                         rect->x += content->text.front()->x_offset() - _analysis->analysis_x_offset();
869                         rect->y += content->text.front()->y_offset() - _analysis->analysis_y_offset();
870                 }
871                 fv.set_outline_subtitles(rect);
872         } else {
873                 fv.set_outline_subtitles({});
874         }
875 }
876
877
878 /** Remove any current subtitle outline display */
879 void
880 TextPanel::clear_outline_subtitles ()
881 {
882         _analysis.reset ();
883         update_outline_subtitles_in_viewer ();
884         if (_outline_subtitles) {
885                 _outline_subtitles->SetValue (false);
886         }
887 }
888
889
890 void
891 TextPanel::analysis_finished(Job::Result result)
892 {
893         _loading_analysis = false;
894
895         auto content = _analysis_content.lock ();
896         if (!content || result == Job::Result::RESULT_CANCELLED) {
897                 clear_outline_subtitles();
898                 setup_sensitivity();
899                 return;
900         }
901
902         if (!dcp::filesystem::exists(_parent->film()->subtitle_analysis_path(content))) {
903                 /* We analysed and still nothing showed up, so maybe it failed.  Give up. */
904                 error_dialog (_parent->window(), _("Could not analyse subtitles."));
905                 clear_outline_subtitles ();
906                 setup_sensitivity ();
907                 return;
908         }
909
910         try_to_load_analysis ();
911 }
912
913
914 void
915 TextPanel::language_changed ()
916 {
917         for (auto i: _parent->selected_text()) {
918                 auto t = i->text_of_original_type(_original_type);
919                 if (t) {
920                         t->set_language (_language->get());
921                 }
922         }
923 }
924
925
926 void
927 TextPanel::language_is_additional_changed ()
928 {
929         for (auto i: _parent->selected_text()) {
930                 auto t = i->text_of_original_type(_original_type);
931                 if (t) {
932                         t->set_language_is_additional (_language_type->GetSelection() == 1);
933                 }
934         }
935 }
936