Merge branch 'main' into v2.17.x
[dcpomatic.git] / src / tools / dcpomatic.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 /** @file  src/tools/dcpomatic.cc
23  *  @brief The main DCP-o-matic GUI.
24  */
25
26
27 #include "wx/about_dialog.h"
28 #include "wx/content_panel.h"
29 #include "wx/dcp_referencing_dialog.h"
30 #include "wx/dkdm_dialog.h"
31 #include "wx/export_subtitles_dialog.h"
32 #include "wx/export_video_file_dialog.h"
33 #include "wx/film_editor.h"
34 #include "wx/film_name_location_dialog.h"
35 #include "wx/film_viewer.h"
36 #include "wx/focus_manager.h"
37 #include "wx/full_config_dialog.h"
38 #include "wx/hints_dialog.h"
39 #include "wx/html_dialog.h"
40 #include "wx/file_dialog.h"
41 #include "wx/i18n_hook.h"
42 #include "wx/id.h"
43 #include "wx/job_manager_view.h"
44 #include "wx/kdm_dialog.h"
45 #include "wx/nag_dialog.h"
46 #include "wx/paste_dialog.h"
47 #include "wx/recreate_chain_dialog.h"
48 #include "wx/report_problem_dialog.h"
49 #include "wx/save_template_dialog.h"
50 #include "wx/self_dkdm_dialog.h"
51 #include "wx/send_i18n_dialog.h"
52 #include "wx/servers_list_dialog.h"
53 #include "wx/standard_controls.h"
54 #include "wx/system_information_dialog.h"
55 #include "wx/templates_dialog.h"
56 #include "wx/update_dialog.h"
57 #include "wx/video_waveform_dialog.h"
58 #include "wx/wx_signal_manager.h"
59 #include "wx/wx_util.h"
60 #include "lib/analytics.h"
61 #include "lib/audio_content.h"
62 #include "lib/check_content_job.h"
63 #include "lib/cinema.h"
64 #include "lib/compose.hpp"
65 #include "lib/config.h"
66 #include "lib/constants.h"
67 #include "lib/content.h"
68 #include "lib/content_factory.h"
69 #include "lib/cross.h"
70 #include "lib/cross.h"
71 #include "lib/dcp_content.h"
72 #include "lib/dcpomatic_log.h"
73 #include "lib/dcpomatic_socket.h"
74 #include "lib/dkdm_wrapper.h"
75 #include "lib/email.h"
76 #include "lib/encode_server_finder.h"
77 #include "lib/exceptions.h"
78 #include "lib/ffmpeg_encoder.h"
79 #include "lib/film.h"
80 #include "lib/font_config.h"
81 #ifdef DCPOMATIC_GROK
82 #include "lib/grok/context.h"
83 #endif
84 #include "lib/hints.h"
85 #include "lib/job_manager.h"
86 #include "lib/kdm_with_metadata.h"
87 #include "lib/log.h"
88 #include "lib/make_dcp.h"
89 #include "lib/release_notes.h"
90 #include "lib/screen.h"
91 #include "lib/send_kdm_email_job.h"
92 #include "lib/signal_manager.h"
93 #include "lib/subtitle_encoder.h"
94 #include "lib/text_content.h"
95 #include "lib/transcode_job.h"
96 #include "lib/update_checker.h"
97 #include "lib/version.h"
98 #include "lib/video_content.h"
99 #include <dcp/exceptions.h>
100 #include <dcp/filesystem.h>
101 #include <dcp/raw_convert.h>
102 #include <dcp/warnings.h>
103 LIBDCP_DISABLE_WARNINGS
104 #include <wx/cmdline.h>
105 #include <wx/generic/aboutdlgg.h>
106 #include <wx/preferences.h>
107 #include <wx/splash.h>
108 #include <wx/stdpaths.h>
109 #include <wx/wxhtml.h>
110 LIBDCP_ENABLE_WARNINGS
111 #ifdef __WXGTK__
112 #include <X11/Xlib.h>
113 #endif
114 #ifdef __WXMSW__
115 #include <shellapi.h>
116 #endif
117 #include <boost/algorithm/string.hpp>
118 #include <boost/filesystem.hpp>
119 #include <iostream>
120 #include <fstream>
121 /* This is OK as it's only used with DCPOMATIC_WINDOWS */
122 #include <sstream>
123
124 #ifdef check
125 #undef check
126 #endif
127
128
129 using std::cout;
130 using std::dynamic_pointer_cast;
131 using std::exception;
132 using std::function;
133 using std::list;
134 using std::make_pair;
135 using std::make_shared;
136 using std::map;
137 using std::shared_ptr;
138 using std::string;
139 using std::vector;
140 using std::wcout;
141 using boost::optional;
142 using boost::is_any_of;
143 using boost::algorithm::find;
144 #if BOOST_VERSION >= 106100
145 using namespace boost::placeholders;
146 #endif
147 using dcp::raw_convert;
148
149
150 class FilmChangedClosingDialog
151 {
152 public:
153         explicit FilmChangedClosingDialog (string name)
154                 : _dialog(
155                         nullptr,
156                         wxString::Format(_("Save changes to film \"%s\" before closing?"), std_to_wx (name).data()),
157                         /// TRANSLATORS: this is the heading for a dialog box, which tells the user that the current
158                         /// project (Film) has been changed since it was last saved.
159                         _("Film changed"),
160                         wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxICON_QUESTION
161                         )
162         {
163                 _dialog.SetYesNoCancelLabels(
164                         _("Save film and close"), _("Close without saving film"), _("Don't close")
165                         );
166         }
167
168         int run ()
169         {
170                 return _dialog.ShowModal();
171         }
172
173 private:
174         wxMessageDialog _dialog;
175 };
176
177
178 class FilmChangedDuplicatingDialog
179 {
180 public:
181         explicit FilmChangedDuplicatingDialog (string name)
182                 : _dialog(
183                         nullptr,
184                         wxString::Format(_("Save changes to film \"%s\" before duplicating?"), std_to_wx (name).data()),
185                         /// TRANSLATORS: this is the heading for a dialog box, which tells the user that the current
186                         /// project (Film) has been changed since it was last saved.
187                         _("Film changed"),
188                         wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxICON_QUESTION
189                         )
190         {
191                 _dialog.SetYesNoCancelLabels(
192                         _("Save film and duplicate"), _("Duplicate without saving film"), _("Don't duplicate")
193                         );
194         }
195
196         int run ()
197         {
198                 return _dialog.ShowModal();
199         }
200
201 private:
202         wxMessageDialog _dialog;
203 };
204
205
206 #define ALWAYS                        0x0
207 #define NEEDS_FILM                    0x1
208 #define NOT_DURING_DCP_CREATION       0x2
209 #define NEEDS_CPL                     0x4
210 #define NEEDS_SINGLE_SELECTED_CONTENT 0x8
211 #define NEEDS_SELECTED_CONTENT        0x10
212 #define NEEDS_SELECTED_VIDEO_CONTENT  0x20
213 #define NEEDS_CLIPBOARD               0x40
214 #define NEEDS_ENCRYPTION              0x80
215 #define NEEDS_DCP_CONTENT             0x100
216
217
218 map<wxMenuItem*, int> menu_items;
219
220 enum {
221         ID_file_new = DCPOMATIC_MAIN_MENU,
222         ID_file_open,
223         ID_file_save,
224         ID_file_save_as_template,
225         ID_file_duplicate,
226         ID_file_duplicate_and_open,
227         ID_file_history,
228         /* Allow spare IDs after _history for the recent files list */
229         ID_file_close = DCPOMATIC_MAIN_MENU + 100,
230         ID_edit_copy,
231         ID_edit_paste,
232         ID_edit_select_all,
233         ID_jobs_make_dcp,
234         ID_jobs_make_dcp_batch,
235         ID_jobs_make_kdms,
236         ID_jobs_make_dkdms,
237         ID_jobs_make_self_dkdm,
238         ID_jobs_export_video_file,
239         ID_jobs_export_subtitles,
240         ID_jobs_send_dcp_to_tms,
241         ID_jobs_show_dcp,
242         ID_jobs_open_dcp_in_player,
243         ID_view_closed_captions,
244         ID_view_video_waveform,
245         ID_tools_version_file,
246         ID_tools_hints,
247         ID_tools_encoding_servers,
248         ID_tools_manage_templates,
249         ID_tools_check_for_updates,
250         ID_tools_send_translations,
251         ID_tools_system_information,
252         ID_tools_restore_default_preferences,
253         ID_tools_export_preferences,
254         ID_tools_import_preferences,
255         ID_help_report_a_problem,
256         /* IDs for shortcuts (with no associated menu item) */
257         ID_add_file,
258         ID_remove,
259         ID_start_stop,
260         ID_timeline,
261         ID_back_frame,
262         ID_forward_frame
263 };
264
265
266 class LimitedFrameSplitter : public wxSplitterWindow
267 {
268 public:
269         LimitedFrameSplitter(wxWindow* parent)
270                 : wxSplitterWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxSP_NOBORDER | wxSP_3DSASH | wxSP_LIVE_UPDATE)
271         {
272                 /* This value doesn't really mean much but we just want to stop double-click on the
273                    divider from shrinking the left panel.
274                    */
275                 SetMinimumPaneSize(64);
276
277                 Bind(wxEVT_SIZE, boost::bind(&LimitedFrameSplitter::sized, this, _1));
278         }
279
280         bool OnSashPositionChange(int new_position) override
281         {
282                 /* Try to stop the left bit of the splitter getting too small */
283                 auto const ok = new_position > _left_panel_minimum_size;
284                 if (ok) {
285                         Config::instance()->set_main_divider_sash_position(new_position);
286                 }
287                 return ok;
288         }
289
290 private:
291         void sized(wxSizeEvent& ev)
292         {
293                 if (GetSize().GetWidth() > _left_panel_minimum_size && GetSashPosition() < _left_panel_minimum_size) {
294                         /* The window is now fairly big but the left panel is small; this happens when the DCP-o-matic window
295                          * is shrunk and then made larger again.  Try to set a sensible left panel size in this case.
296                          */
297                         SetSashPosition(Config::instance()->main_divider_sash_position().get_value_or(_left_panel_minimum_size));
298                 }
299
300                 ev.Skip();
301         }
302
303         int const _left_panel_minimum_size = 200;
304 };
305
306
307 class DOMFrame : public wxFrame
308 {
309 public:
310         explicit DOMFrame (wxString const& title)
311                 : wxFrame (nullptr, -1, title)
312                 /* Use a panel as the only child of the Frame so that we avoid
313                    the dark-grey background on Windows.
314                 */
315                 , _splitter(new LimitedFrameSplitter(this))
316                 , _right_panel(new wxPanel(_splitter, wxID_ANY))
317                 , _film_viewer(_right_panel)
318         {
319
320                 auto bar = new wxMenuBar;
321                 setup_menu (bar);
322                 SetMenuBar (bar);
323
324 #ifdef DCPOMATIC_WINDOWS
325                 SetIcon (wxIcon (std_to_wx ("id")));
326 #endif
327
328                 _config_changed_connection = Config::instance()->Changed.connect(boost::bind(&DOMFrame::config_changed, this, _1));
329                 config_changed (Config::OTHER);
330
331                 _analytics_message_connection = Analytics::instance()->Message.connect(boost::bind(&DOMFrame::analytics_message, this, _1, _2));
332
333                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_new, this),                ID_file_new);
334                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_open, this),               ID_file_open);
335                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_save, this),               ID_file_save);
336                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_save_as_template, this),   ID_file_save_as_template);
337                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_duplicate, this),          ID_file_duplicate);
338                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_duplicate_and_open, this), ID_file_duplicate_and_open);
339                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_close, this),              ID_file_close);
340                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_history, this, _1),        ID_file_history, ID_file_history + HISTORY_SIZE);
341                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_exit, this),               wxID_EXIT);
342                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::edit_copy, this),               ID_edit_copy);
343                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::edit_paste, this),              ID_edit_paste);
344                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::edit_select_all, this),         ID_edit_select_all);
345                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::edit_preferences, this),        wxID_PREFERENCES);
346                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_make_dcp, this),           ID_jobs_make_dcp);
347                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_make_kdms, this),          ID_jobs_make_kdms);
348                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_make_dkdms, this),         ID_jobs_make_dkdms);
349                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_make_dcp_batch, this),     ID_jobs_make_dcp_batch);
350                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_make_self_dkdm, this),     ID_jobs_make_self_dkdm);
351                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_export_video_file, this),  ID_jobs_export_video_file);
352                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_export_subtitles, this),   ID_jobs_export_subtitles);
353                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_send_dcp_to_tms, this),    ID_jobs_send_dcp_to_tms);
354                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_show_dcp, this),           ID_jobs_show_dcp);
355                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_open_dcp_in_player, this), ID_jobs_open_dcp_in_player);
356                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::view_closed_captions, this),    ID_view_closed_captions);
357                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::view_video_waveform, this),     ID_view_video_waveform);
358                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_version_file, this),      ID_tools_version_file);
359                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_hints, this),             ID_tools_hints);
360                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_encoding_servers, this),  ID_tools_encoding_servers);
361                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_manage_templates, this),  ID_tools_manage_templates);
362                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_check_for_updates, this), ID_tools_check_for_updates);
363                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_send_translations, this), ID_tools_send_translations);
364                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_system_information, this),ID_tools_system_information);
365                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_restore_default_preferences, this), ID_tools_restore_default_preferences);
366                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_export_preferences, this), ID_tools_export_preferences);
367                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_import_preferences, this), ID_tools_import_preferences);
368                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::help_about, this),              wxID_ABOUT);
369                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::help_report_a_problem, this),   ID_help_report_a_problem);
370
371                 Bind (wxEVT_CLOSE_WINDOW, boost::bind (&DOMFrame::close, this, _1));
372                 Bind (wxEVT_SHOW, boost::bind (&DOMFrame::show, this, _1));
373
374                 auto left_panel = new wxPanel(_splitter, wxID_ANY);
375
376                 _film_editor = new FilmEditor(left_panel, _film_viewer);
377
378                 auto left_sizer = new wxBoxSizer(wxHORIZONTAL);
379                 left_sizer->Add(_film_editor, 1, wxEXPAND);
380
381                 left_panel->SetSizerAndFit(left_sizer);
382
383                 _controls = new StandardControls(_right_panel, _film_viewer, true);
384                 _controls->set_film(_film_viewer.film());
385                 auto job_manager_view = new JobManagerView(_right_panel, false);
386
387                 auto right_sizer = new wxBoxSizer (wxVERTICAL);
388                 right_sizer->Add(_film_viewer.panel(), 2, wxEXPAND | wxALL, 6);
389                 right_sizer->Add (_controls, 0, wxEXPAND | wxALL, 6);
390                 right_sizer->Add (job_manager_view, 1, wxEXPAND | wxALL, 6);
391
392                 _right_panel->SetSizer(right_sizer);
393
394                 _splitter->SplitVertically(left_panel, _right_panel, Config::instance()->main_divider_sash_position().get_value_or(left_panel->GetSize().GetWidth() + 8));
395
396                 set_menu_sensitivity ();
397
398                 _film_editor->content_panel()->SelectionChanged.connect (boost::bind (&DOMFrame::set_menu_sensitivity, this));
399                 set_title ();
400
401                 JobManager::instance()->ActiveJobsChanged.connect(boost::bind(&DOMFrame::active_jobs_changed, this));
402
403                 UpdateChecker::instance()->StateChanged.connect(boost::bind(&DOMFrame::update_checker_state_changed, this));
404
405                 FocusManager::instance()->SetFocus.connect (boost::bind (&DOMFrame::remove_accelerators, this));
406                 FocusManager::instance()->KillFocus.connect (boost::bind (&DOMFrame::add_accelerators, this));
407                 add_accelerators ();
408         }
409
410         void add_accelerators ()
411         {
412 #ifdef __WXOSX__
413                 int accelerators = 7;
414 #else
415                 int accelerators = 6;
416 #endif
417                 std::vector<wxAcceleratorEntry> accel(accelerators);
418                 /* [Shortcut] Ctrl+A:Add file(s) to the film */
419                 accel[0].Set (wxACCEL_CTRL, static_cast<int>('A'), ID_add_file);
420                 /* [Shortcut] Delete:Remove selected content from film */
421                 accel[1].Set (wxACCEL_NORMAL, WXK_DELETE, ID_remove);
422                 /* [Shortcut] Space:Start/stop playback */
423                 accel[2].Set (wxACCEL_NORMAL, WXK_SPACE, ID_start_stop);
424                 /* [Shortcut] Ctrl+T:Open timeline window */
425                 accel[3].Set (wxACCEL_CTRL, static_cast<int>('T'), ID_timeline);
426                 /* [Shortcut] Left arrow:Move back one frame */
427                 accel[4].Set (wxACCEL_NORMAL, WXK_LEFT, ID_back_frame);
428                 /* [Shortcut] Right arrow:Move forward one frame */
429                 accel[5].Set (wxACCEL_NORMAL, WXK_RIGHT, ID_forward_frame);
430 #ifdef __WXOSX__
431                 accel[6].Set (wxACCEL_CTRL, static_cast<int>('W'), ID_file_close);
432 #endif
433                 Bind (wxEVT_MENU, boost::bind (&ContentPanel::add_file_clicked, _film_editor->content_panel()), ID_add_file);
434                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::remove_clicked, this, _1), ID_remove);
435                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::start_stop_pressed, this), ID_start_stop);
436                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::timeline_pressed, this), ID_timeline);
437                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::back_frame, this), ID_back_frame);
438                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::forward_frame, this), ID_forward_frame);
439                 wxAcceleratorTable accel_table (accelerators, accel.data());
440                 SetAcceleratorTable (accel_table);
441         }
442
443         void remove_accelerators ()
444         {
445                 SetAcceleratorTable (wxAcceleratorTable ());
446         }
447
448         void remove_clicked (wxCommandEvent& ev)
449         {
450                 if (_film_editor->content_panel()->remove_clicked (true)) {
451                         ev.Skip ();
452                 }
453         }
454
455         void new_film (boost::filesystem::path path, optional<string> template_name)
456         {
457                 auto film = make_shared<Film>(path);
458                 if (template_name) {
459                         film->use_template (template_name.get());
460                 }
461                 film->set_name (path.filename().generic_string());
462                 film->write_metadata ();
463                 set_film (film);
464         }
465
466         void load_film (boost::filesystem::path file)
467         try
468         {
469                 auto film = make_shared<Film>(file);
470                 auto const notes = film->read_metadata ();
471                 film->read_ui_state();
472
473                 if (film->state_version() == 4) {
474                         error_dialog (
475                                 0,
476                                 _("This film was created with an old version of DVD-o-matic and may not load correctly "
477                                   "in this version.  Please check the film's settings carefully.")
478                                 );
479                 }
480
481                 for (auto i: notes) {
482                         error_dialog (0, std_to_wx(i));
483                 }
484
485                 set_film (film);
486
487                 JobManager::instance()->add(make_shared<CheckContentJob>(film));
488         }
489         catch (FileNotFoundError& e) {
490                 auto const dir = e.file().parent_path();
491                 if (dcp::filesystem::exists(dir / "ASSETMAP") || dcp::filesystem::exists(dir / "ASSETMAP.xml")) {
492                         error_dialog (
493                                 this, _("Could not open this folder as a DCP-o-matic project."),
494                                 _("It looks like you are trying to open a DCP.  File -> Open is for loading DCP-o-matic projects, not DCPs.  To import a DCP, create a new project with File -> New and then click the \"Add DCP...\" button.")
495                                 );
496                 } else {
497                         auto const p = std_to_wx(file.string ());
498                         error_dialog (this, wxString::Format(_("Could not open film at %s"), p.data()), std_to_wx(e.what()));
499                 }
500
501         } catch (std::exception& e) {
502                 auto const p = std_to_wx (file.string());
503                 error_dialog (this, wxString::Format(_("Could not open film at %s"), p.data()), std_to_wx(e.what()));
504         }
505
506         void set_film (shared_ptr<Film> film)
507         {
508                 _film = film;
509                 _film_viewer.set_film(_film);
510                 _film_editor->set_film(_film);
511                 _controls->set_film (_film);
512                 _video_waveform_dialog.reset();
513                 set_menu_sensitivity ();
514                 if (_film && _film->directory()) {
515                         Config::instance()->add_to_history (_film->directory().get());
516                 }
517                 if (_film) {
518                         _film->Change.connect (boost::bind (&DOMFrame::film_change, this, _1));
519                         _film->Message.connect (boost::bind(&DOMFrame::film_message, this, _1));
520                         _film->DirtyChange.connect (boost::bind(&DOMFrame::set_title, this));
521                         dcpomatic_log = _film->log ();
522                 }
523                 set_title ();
524         }
525
526         shared_ptr<Film> film () const {
527                 return _film;
528         }
529
530 private:
531
532         void show (wxShowEvent& ev)
533         {
534                 if (ev.IsShown() && !_first_shown_called) {
535                         _film_editor->first_shown ();
536                         _first_shown_called = true;
537                 }
538         }
539
540         void film_message (string m)
541         {
542                 message_dialog (this, std_to_wx(m));
543         }
544
545         void film_change (ChangeType type)
546         {
547                 if (type == ChangeType::DONE) {
548                         set_menu_sensitivity ();
549                 }
550         }
551
552         void file_new ()
553         {
554                 FilmNameLocationDialog dialog(this, _("New Film"), true);
555                 int const r = dialog.ShowModal();
556
557                 if (r != wxID_OK || !dialog.check_path() || !maybe_save_then_delete_film<FilmChangedClosingDialog>()) {
558                         return;
559                 }
560
561                 try {
562                         new_film(dialog.path(), dialog.template_name());
563                 } catch (boost::filesystem::filesystem_error& e) {
564 #ifdef DCPOMATIC_WINDOWS
565                         string bad_chars = "<>:\"/|?*";
566                         string const filename = dialog.path().filename().string();
567                         string found_bad_chars;
568                         for (size_t i = 0; i < bad_chars.length(); ++i) {
569                                 if (filename.find(bad_chars[i]) != string::npos && found_bad_chars.find(bad_chars[i]) == string::npos) {
570                                         found_bad_chars += bad_chars[i];
571                                 }
572                         }
573                         wxString message = _("Could not create folder to store film.");
574                         message += "  ";
575                         if (!found_bad_chars.empty()) {
576                                 message += wxString::Format (_("Try removing the %s characters from your folder name."), std_to_wx(found_bad_chars).data());
577                         } else {
578                                 message += _("Please check that you do not have Windows controlled folder access enabled for DCP-o-matic.");
579                         }
580                         error_dialog (this, message, std_to_wx(e.what()));
581 #else
582                         error_dialog (this, _("Could not create folder to store film."), std_to_wx(e.what()));
583 #endif
584                 }
585         }
586
587         void file_open ()
588         {
589                 wxDirDialog dialog(
590                         this,
591                         _("Select film to open"),
592                         std_to_wx (Config::instance()->default_directory_or (wx_to_std (wxStandardPaths::Get().GetDocumentsDir())).string ()),
593                         wxDEFAULT_DIALOG_STYLE | wxDD_DIR_MUST_EXIST
594                         );
595
596                 int r;
597                 while (true) {
598                         r = dialog.ShowModal();
599                         if (r == wxID_OK && dialog.GetPath() == wxStandardPaths::Get().GetDocumentsDir()) {
600                                 error_dialog (this, _("You did not select a folder.  Make sure that you select a folder before clicking Open."));
601                         } else {
602                                 break;
603                         }
604                 }
605
606                 if (r == wxID_OK && maybe_save_then_delete_film<FilmChangedClosingDialog>()) {
607                         load_film(wx_to_std(dialog.GetPath()));
608                 }
609         }
610
611         void file_save ()
612         {
613                 try {
614                         _film->write_metadata ();
615                 } catch (exception& e) {
616                         error_dialog(this, _("Could not save project."), std_to_wx(e.what()));
617                 }
618         }
619
620         void file_save_as_template ()
621         {
622                 SaveTemplateDialog dialog(this);
623                 if (dialog.ShowModal() == wxID_OK) {
624                         try {
625                                 Config::instance()->save_template(_film, dialog.name());
626                         } catch (exception& e) {
627                                 error_dialog(this, _("Could not save template."), std_to_wx(e.what()));
628                         }
629                 }
630         }
631
632         void file_duplicate ()
633         {
634                 FilmNameLocationDialog dialog(this, _("Duplicate Film"), false);
635
636                 if (dialog.ShowModal() == wxID_OK && dialog.check_path() && maybe_save_film<FilmChangedDuplicatingDialog>()) {
637                         auto film = make_shared<Film>(dialog.path());
638                         film->copy_from (_film);
639                         film->set_name(dialog.path().filename().generic_string());
640                         try {
641                                 film->write_metadata();
642                         } catch (exception& e) {
643                                 error_dialog(this, _("Could not duplicate project."), std_to_wx(e.what()));
644                         }
645                 }
646         }
647
648         void file_duplicate_and_open ()
649         {
650                 FilmNameLocationDialog dialog(this, _("Duplicate Film"), false);
651
652                 if (dialog.ShowModal() == wxID_OK && dialog.check_path() && maybe_save_film<FilmChangedDuplicatingDialog>()) {
653                         auto film = make_shared<Film>(dialog.path());
654                         film->copy_from (_film);
655                         film->set_name(dialog.path().filename().generic_string());
656                         try {
657                                 film->write_metadata ();
658                                 set_film (film);
659                         } catch (exception& e) {
660                                 error_dialog(this, _("Could not duplicate project."), std_to_wx(e.what()));
661                         }
662                 }
663         }
664
665         void file_close ()
666         {
667                 if (_film && _film->dirty ()) {
668                         FilmChangedClosingDialog dialog(_film->name());
669                         switch (dialog.run()) {
670                         case wxID_NO:
671                                 /* Don't save and carry on to close */
672                                 break;
673                         case wxID_YES:
674                                 /* Save and carry on to close */
675                                 _film->write_metadata ();
676                                 break;
677                         case wxID_CANCEL:
678                                 /* Stop */
679                                 return;
680                         }
681                 }
682
683                 set_film (shared_ptr<Film>());
684         }
685
686         void file_history (wxCommandEvent& event)
687         {
688                 auto history = Config::instance()->history ();
689                 int n = event.GetId() - ID_file_history;
690                 if (n >= 0 && n < static_cast<int> (history.size ()) && maybe_save_then_delete_film<FilmChangedClosingDialog>()) {
691                         load_film (history[n]);
692                 }
693         }
694
695         void file_exit ()
696         {
697                 /* false here allows the close handler to veto the close request */
698                 Close (false);
699         }
700
701         void edit_copy ()
702         {
703                 auto const sel = _film_editor->content_panel()->selected();
704                 if (sel.size() == 1) {
705                         _clipboard = sel.front()->clone();
706                 }
707         }
708
709         void edit_paste ()
710         {
711                 if (!_clipboard) {
712                         return;
713                 }
714
715                 PasteDialog dialog(this, static_cast<bool>(_clipboard->video), static_cast<bool>(_clipboard->audio), !_clipboard->text.empty());
716                 if (dialog.ShowModal() != wxID_OK) {
717                         return;
718                 }
719
720                 for (auto i: _film_editor->content_panel()->selected()) {
721                         if (dialog.video() && i->video) {
722                                 DCPOMATIC_ASSERT (_clipboard->video);
723                                 i->video->take_settings_from (_clipboard->video);
724                         }
725                         if (dialog.audio() && i->audio) {
726                                 DCPOMATIC_ASSERT (_clipboard->audio);
727                                 i->audio->take_settings_from (_clipboard->audio);
728                         }
729
730                         if (dialog.text()) {
731                                 auto j = i->text.begin ();
732                                 auto k = _clipboard->text.begin ();
733                                 while (j != i->text.end() && k != _clipboard->text.end()) {
734                                         (*j)->take_settings_from (*k);
735                                         ++j;
736                                         ++k;
737                                 }
738                         }
739                 }
740         }
741
742         void edit_select_all ()
743         {
744                 _film_editor->content_panel()->select_all();
745         }
746
747         void edit_preferences ()
748         {
749                 if (!_config_dialog) {
750                         _config_dialog = create_full_config_dialog ();
751                 }
752                 _config_dialog->Show (this);
753         }
754
755         void tools_restore_default_preferences ()
756         {
757                 wxMessageDialog dialog(
758                         nullptr,
759                         _("Are you sure you want to restore preferences to their defaults?  This cannot be undone."),
760                         _("Restore default preferences"),
761                         wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION
762                         );
763
764                 if (dialog.ShowModal() == wxID_YES) {
765                         Config::restore_defaults ();
766                 }
767         }
768
769         void tools_export_preferences ()
770         {
771                 FileDialog dialog(
772                         this, _("Specify ZIP file"), wxT("ZIP files (*.zip)|*.zip"), wxFD_SAVE | wxFD_OVERWRITE_PROMPT, "Preferences", string("dcpomatic_config.zip")
773                         );
774
775                 if (dialog.ShowModal() == wxID_OK) {
776                         auto const path = boost::filesystem::path(wx_to_std(dialog.GetPath()));
777                         if (boost::filesystem::exists(path)) {
778                                 boost::system::error_code ec;
779                                 boost::filesystem::remove(path, ec);
780                                 if (ec) {
781                                         error_dialog(nullptr, _("Could not remove existing preferences file"), std_to_wx(path.string()));
782                                         return;
783                                 }
784                         }
785
786                         save_all_config_as_zip(path);
787                 }
788         }
789
790         void tools_import_preferences()
791         {
792                 FileDialog dialog(this, _("Specify ZIP file"), wxT("ZIP files (*.zip)|*.zip"), wxFD_OPEN, "Preferences");
793
794                 if (dialog.show()) {
795                         Config::instance()->load_from_zip(dialog.path());
796                 }
797         }
798
799         void jobs_make_dcp ()
800         {
801                 double required;
802                 double available;
803                 bool can_hard_link;
804
805                 if (!_film->should_be_enough_disk_space (required, available, can_hard_link)) {
806                         wxString message;
807                         if (can_hard_link) {
808                                 message = wxString::Format (_("The DCP for this film will take up about %.1f GB, and the disk that you are using only has %.1f GB available.  Do you want to continue anyway?"), required, available);
809                         } else {
810                                 message = wxString::Format (_("The DCP and intermediate files for this film will take up about %.1f GB, and the disk that you are using only has %.1f GB available.  You would need half as much space if the filesystem supported hard links, but it does not.  Do you want to continue anyway?"), required, available);
811                         }
812                         if (!confirm_dialog (this, message)) {
813                                 return;
814                         }
815                 }
816
817                 if (Config::instance()->show_hints_before_make_dcp()) {
818                         HintsDialog hints(this, _film, false);
819                         if (hints.ShowModal() == wxID_CANCEL) {
820                                 return;
821                         }
822                 }
823
824                 if (_film->encrypted ()) {
825                         NagDialog::maybe_nag (
826                                 this,
827                                 Config::NAG_ENCRYPTED_METADATA,
828                                 _("You are making an encrypted DCP.  It will not be possible to make KDMs for this DCP unless you have copies of "
829                                   "the <tt>metadata.xml</tt> file within the film and the metadata files within the DCP.\n\n"
830                                   "You should ensure that these files are <span weight=\"bold\" size=\"larger\">BACKED UP</span> "
831                                   "if you want to make KDMs for this film.")
832                                 );
833                 }
834
835                 /* Remove any existing DCP if the user agrees */
836                 auto const dcp_dir = _film->dir (_film->dcp_name(), false);
837                 if (dcp::filesystem::exists(dcp_dir)) {
838                         if (!confirm_dialog (this, wxString::Format (_("Do you want to overwrite the existing DCP %s?"), std_to_wx(dcp_dir.string()).data()))) {
839                                 return;
840                         }
841                         dcp::filesystem::remove_all(dcp_dir);
842                 }
843
844                 try {
845                         /* It seems to make sense to auto-save metadata here, since the make DCP may last
846                            a long time, and crashes/power failures are moderately likely.
847                         */
848                         _film->write_metadata ();
849                         make_dcp (_film, TranscodeJob::ChangedBehaviour::EXAMINE_THEN_STOP);
850                 } catch (BadSettingError& e) {
851                         error_dialog (this, wxString::Format (_("Bad setting for %s."), std_to_wx(e.setting()).data()), std_to_wx(e.what()));
852                 } catch (std::exception& e) {
853                         error_dialog (this, wxString::Format (_("Could not make DCP.")), std_to_wx(e.what()));
854                 }
855         }
856
857         void jobs_make_kdms ()
858         {
859                 if (!_film) {
860                         return;
861                 }
862
863                 _kdm_dialog.reset(this, _film);
864                 _kdm_dialog->Show ();
865         }
866
867         void jobs_make_dkdms ()
868         {
869                 if (!_film) {
870                         return;
871                 }
872
873                 _dkdm_dialog.reset(this, _film);
874                 _dkdm_dialog->Show ();
875         }
876
877         /** @return false if we succeeded, true if not */
878         bool send_to_other_tool (int port, function<void()> start, string message)
879         {
880                 /* i = 0; try to connect via socket
881                    i = 1; try again, and then try to start the tool
882                    i = 2 onwards; try again.
883                 */
884                 for (int i = 0; i < 8; ++i) {
885                         try {
886                                 boost::asio::io_service io_service;
887                                 boost::asio::ip::tcp::resolver resolver (io_service);
888                                 boost::asio::ip::tcp::resolver::query query ("127.0.0.1", raw_convert<string> (port));
889                                 boost::asio::ip::tcp::resolver::iterator endpoint_iterator = resolver.resolve (query);
890                                 Socket socket (5);
891                                 socket.connect (*endpoint_iterator);
892                                 DCPOMATIC_ASSERT (_film->directory ());
893                                 socket.write (message.length() + 1);
894                                 socket.write ((uint8_t *) message.c_str(), message.length() + 1);
895                                 /* OK\0 */
896                                 uint8_t ok[3];
897                                 socket.read (ok, 3);
898                                 return false;
899                         } catch (exception& e) {
900
901                         }
902
903                         if (i == 1) {
904                                 start ();
905                         }
906
907                         dcpomatic_sleep_seconds (1);
908                 }
909
910                 return true;
911         }
912
913         void jobs_make_dcp_batch ()
914         {
915                 if (!_film) {
916                         return;
917                 }
918
919                 if (Config::instance()->show_hints_before_make_dcp()) {
920                         HintsDialog hints(this, _film, false);
921                         if (hints.ShowModal() == wxID_CANCEL) {
922                                 return;
923                         }
924                 }
925
926                 _film->write_metadata ();
927
928                 if (send_to_other_tool (BATCH_JOB_PORT, &start_batch_converter, _film->directory()->string())) {
929 #ifdef DCPOMATIC_OSX
930                         error_dialog (this, _("Could not start the batch converter.  You may need to download it from dcpomatic.com."));
931 #else
932                         error_dialog (this, _("Could not find batch converter."));
933 #endif
934                 }
935         }
936
937         void jobs_open_dcp_in_player ()
938         {
939                 if (!_film) {
940                         return;
941                 }
942
943                 if (send_to_other_tool (PLAYER_PLAY_PORT, &start_player, _film->dir(_film->dcp_name(false)).string())) {
944 #ifdef DCPOMATIC_OSX
945                         error_dialog (this, _("Could not start the player.  You may need to download it from dcpomatic.com."));
946 #else
947                         error_dialog (this, _("Could not find player."));
948 #endif
949                 }
950         }
951
952         void jobs_make_self_dkdm ()
953         {
954                 if (!_film) {
955                         return;
956                 }
957
958                 SelfDKDMDialog dialog(this, _film);
959                 if (dialog.ShowModal() != wxID_OK) {
960                         return;
961                 }
962
963                 NagDialog::maybe_nag (
964                         this,
965                         Config::NAG_DKDM_CONFIG,
966                         wxString::Format (
967                                 _("You are making a DKDM which is encrypted by a private key held in"
968                                   "\n\n<tt>%s</tt>\n\nIt is <span weight=\"bold\" size=\"larger\">VITALLY IMPORTANT</span> "
969                                   "that you <span weight=\"bold\" size=\"larger\">BACK UP THIS FILE</span> since if it is lost "
970                                   "your DKDMs (and the DCPs they protect) will become useless."), std_to_wx(Config::config_read_file().string()).data()
971                                 )
972                         );
973
974
975                 dcp::LocalTime from (Config::instance()->signer_chain()->leaf().not_before());
976                 from.add_days (1);
977                 dcp::LocalTime to (Config::instance()->signer_chain()->leaf().not_after());
978                 to.add_days (-1);
979
980                 auto signer = Config::instance()->signer_chain();
981                 if (!signer->valid()) {
982                         error_dialog(this, _("The certificate chain for signing is invalid"));
983                         return;
984                 }
985
986                 optional<dcp::EncryptedKDM> kdm;
987                 try {
988                         auto const decrypted_kdm = _film->make_kdm(dialog.cpl(), from, to);
989                         auto const kdm = decrypted_kdm.encrypt(signer, Config::instance()->decryption_chain()->leaf(), {}, dcp::Formulation::MODIFIED_TRANSITIONAL_1, true, 0);
990                         if (dialog.internal()) {
991                                 auto dkdms = Config::instance()->dkdms();
992                                 dkdms->add(make_shared<DKDM>(kdm));
993                                 Config::instance()->changed ();
994                         } else {
995                                 auto path = dialog.directory() / (_film->dcp_name(false) + "_DKDM.xml");
996                                 kdm.as_xml(path);
997                         }
998                 } catch (dcp::NotEncryptedError& e) {
999                         error_dialog (this, _("CPL's content is not encrypted."));
1000                 } catch (exception& e) {
1001                         error_dialog (this, e.what ());
1002                 } catch (...) {
1003                         error_dialog (this, _("An unknown exception occurred."));
1004                 }
1005         }
1006
1007
1008         void jobs_export_video_file ()
1009         {
1010                 ExportVideoFileDialog dialog(this, _film->isdcf_name(true));
1011                 if (dialog.ShowModal() != wxID_OK) {
1012                         return;
1013                 }
1014
1015                 if (dcp::filesystem::exists(dialog.path())) {
1016                         bool ok = confirm_dialog(
1017                                         this,
1018                                         wxString::Format(_("File %s already exists.  Do you want to overwrite it?"), std_to_wx(dialog.path().string()).data())
1019                                         );
1020
1021                         if (!ok) {
1022                                 return;
1023                         }
1024                 }
1025
1026                 auto job = make_shared<TranscodeJob>(_film, TranscodeJob::ChangedBehaviour::EXAMINE_THEN_STOP);
1027                 job->set_encoder (
1028                         make_shared<FFmpegEncoder> (
1029                                 _film, job, dialog.path(), dialog.format(), dialog.mixdown_to_stereo(), dialog.split_reels(), dialog.split_streams(), dialog.x264_crf())
1030                         );
1031                 JobManager::instance()->add (job);
1032         }
1033
1034
1035         void jobs_export_subtitles ()
1036         {
1037                 ExportSubtitlesDialog dialog(this, _film->reels().size(), _film->interop());
1038                 if (dialog.ShowModal() != wxID_OK) {
1039                         return;
1040                 }
1041                 auto job = make_shared<TranscodeJob>(_film, TranscodeJob::ChangedBehaviour::EXAMINE_THEN_STOP);
1042                 job->set_encoder(
1043                         make_shared<SubtitleEncoder>(_film, job, dialog.path(), _film->isdcf_name(true), dialog.split_reels(), dialog.include_font())
1044                         );
1045                 JobManager::instance()->add(job);
1046         }
1047
1048
1049         void jobs_send_dcp_to_tms ()
1050         {
1051                 _film->send_dcp_to_tms ();
1052         }
1053
1054         void jobs_show_dcp ()
1055         {
1056                 DCPOMATIC_ASSERT (_film->directory ());
1057                 if (show_in_file_manager(_film->directory().get(), _film->dir(_film->dcp_name(false)))) {
1058                         error_dialog (this, _("Could not show DCP."));
1059                 }
1060         }
1061
1062         void view_closed_captions ()
1063         {
1064                 _film_viewer.show_closed_captions ();
1065         }
1066
1067         void view_video_waveform ()
1068         {
1069                 if (!_video_waveform_dialog) {
1070                         _video_waveform_dialog.reset(this, _film, _film_viewer);
1071                 }
1072
1073                 _video_waveform_dialog->Show ();
1074         }
1075
1076         void tools_system_information ()
1077         {
1078                 if (!_system_information_dialog) {
1079                         _system_information_dialog = new SystemInformationDialog (this, _film_viewer);
1080                 }
1081
1082                 _system_information_dialog->Show ();
1083         }
1084
1085         void tools_version_file()
1086         {
1087                 if (_dcp_referencing_dialog) {
1088                         _dcp_referencing_dialog->Destroy();
1089                         _dcp_referencing_dialog = nullptr;
1090                 }
1091
1092                 _dcp_referencing_dialog = new DCPReferencingDialog(this, _film);
1093                 _dcp_referencing_dialog->Show();
1094         }
1095
1096         void tools_hints ()
1097         {
1098                 if (!_hints_dialog) {
1099                         _hints_dialog = new HintsDialog (this, _film, true);
1100                 }
1101
1102                 _hints_dialog->Show ();
1103         }
1104
1105         void tools_encoding_servers ()
1106         {
1107                 if (!_servers_list_dialog) {
1108                         _servers_list_dialog = new ServersListDialog (this);
1109                 }
1110
1111                 _servers_list_dialog->Show ();
1112         }
1113
1114         void tools_manage_templates ()
1115         {
1116                 if (!_templates_dialog) {
1117                         _templates_dialog.reset(this);
1118                 }
1119
1120                 _templates_dialog->Show ();
1121         }
1122
1123         void tools_check_for_updates ()
1124         {
1125                 _update_news_requested = true;
1126                 UpdateChecker::instance()->run();
1127         }
1128
1129         void tools_send_translations ()
1130         {
1131                 SendI18NDialog dialog(this);
1132                 if (dialog.ShowModal() != wxID_OK) {
1133                         return;
1134                 }
1135
1136                 string body;
1137                 body += dialog.name() + "\n";
1138                 body += dialog.language() + "\n";
1139                 body += string(dcpomatic_version) + " " + string(dcpomatic_git_commit) + "\n";
1140                 body += "--\n";
1141                 auto translations = I18NHook::translations ();
1142                 for (auto i: translations) {
1143                         body += i.first + "\n" + i.second + "\n\n";
1144                 }
1145                 if (dialog.email().find("@") == string::npos) {
1146                         error_dialog (this, _("You must enter a valid email address when sending translations, "
1147                                               "otherwise the DCP-o-matic maintainers cannot credit you or contact you with questions."));
1148                 } else {
1149                         Email email(dialog.email(), { "carl@dcpomatic.com" }, "DCP-o-matic translations", body);
1150                         try {
1151                                 email.send("main.carlh.net", 2525, EmailProtocol::STARTTLS);
1152                         } catch (NetworkError& e) {
1153                                 error_dialog (this, _("Could not send translations"), std_to_wx(e.what()));
1154                         }
1155                 }
1156         }
1157
1158         void help_about ()
1159         {
1160                 AboutDialog dialog(this);
1161                 dialog.ShowModal();
1162         }
1163
1164         void help_report_a_problem ()
1165         {
1166                 ReportProblemDialog dialog(this, _film);
1167                 if (dialog.ShowModal() == wxID_OK) {
1168                         dialog.report();
1169                 }
1170         }
1171
1172         bool should_close ()
1173         {
1174                 if (!JobManager::instance()->work_to_do ()) {
1175                         return true;
1176                 }
1177
1178                 wxMessageDialog dialog(
1179                         nullptr,
1180                         _("There are unfinished jobs; are you sure you want to quit?"),
1181                         _("Unfinished jobs"),
1182                         wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION
1183                         );
1184
1185                 return dialog.ShowModal() == wxID_YES;
1186         }
1187
1188         void close (wxCloseEvent& ev)
1189         {
1190                 if (!should_close ()) {
1191                         ev.Veto ();
1192                         return;
1193                 }
1194
1195                 if (_film && _film->dirty ()) {
1196                         FilmChangedClosingDialog dialog(_film->name());
1197                         switch (dialog.run()) {
1198                         case wxID_NO:
1199                                 /* Don't save and carry on to close */
1200                                 break;
1201                         case wxID_YES:
1202                                 /* Save and carry on to close */
1203                                 _film->write_metadata ();
1204                                 break;
1205                         case wxID_CANCEL:
1206                                 /* Veto the event and stop */
1207                                 ev.Veto ();
1208                                 return;
1209                         }
1210                 }
1211
1212                 /* We don't want to hear about any more configuration changes, since they
1213                    cause the File menu to be altered, which itself will be deleted around
1214                    now (without, as far as I can see, any way for us to find out).
1215                 */
1216                 _config_changed_connection.disconnect ();
1217
1218                 /* Also stop hearing about analytics-related stuff */
1219                 _analytics_message_connection.disconnect ();
1220
1221                 FontConfig::drop();
1222
1223                 ev.Skip ();
1224                 JobManager::drop ();
1225         }
1226
1227         void active_jobs_changed()
1228         {
1229                 /* ActiveJobsChanged can be called while JobManager holds a lock on its mutex, making
1230                  * the call to JobManager::get() in set_menu_sensitivity() deadlock unless we work around
1231                  * it by using an idle callback.  This feels quite unpleasant.
1232                  */
1233                 signal_manager->when_idle(boost::bind(&DOMFrame::set_menu_sensitivity, this));
1234         }
1235
1236         void set_menu_sensitivity ()
1237         {
1238                 auto jobs = JobManager::instance()->get ();
1239                 auto i = jobs.begin();
1240                 while (i != jobs.end() && (*i)->json_name() != "transcode") {
1241                         ++i;
1242                 }
1243                 bool const dcp_creation = (i != jobs.end ()) && !(*i)->finished ();
1244                 bool const have_cpl = _film && !_film->cpls().empty ();
1245                 bool const have_single_selected_content = _film_editor->content_panel()->selected().size() == 1;
1246                 bool const have_selected_content = !_film_editor->content_panel()->selected().empty();
1247                 bool const have_selected_video_content = !_film_editor->content_panel()->selected_video().empty();
1248                 vector<shared_ptr<Content>> content;
1249                 if (_film) {
1250                         content = _film->content();
1251                 }
1252                 bool const have_dcp_content = std::find_if(content.begin(), content.end(), [](shared_ptr<const Content> content) {
1253                         return static_cast<bool>(dynamic_pointer_cast<const DCPContent>(content));
1254                 }) != content.end();
1255
1256                 for (auto j: menu_items) {
1257
1258                         bool enabled = true;
1259
1260                         if ((j.second & NEEDS_FILM) && !_film) {
1261                                 enabled = false;
1262                         }
1263
1264                         if ((j.second & NOT_DURING_DCP_CREATION) && dcp_creation) {
1265                                 enabled = false;
1266                         }
1267
1268                         if ((j.second & NEEDS_CPL) && !have_cpl) {
1269                                 enabled = false;
1270                         }
1271
1272                         if ((j.second & NEEDS_SELECTED_CONTENT) && !have_selected_content) {
1273                                 enabled = false;
1274                         }
1275
1276                         if ((j.second & NEEDS_SINGLE_SELECTED_CONTENT) && !have_single_selected_content) {
1277                                 enabled = false;
1278                         }
1279
1280                         if ((j.second & NEEDS_SELECTED_VIDEO_CONTENT) && !have_selected_video_content) {
1281                                 enabled = false;
1282                         }
1283
1284                         if ((j.second & NEEDS_CLIPBOARD) && !_clipboard) {
1285                                 enabled = false;
1286                         }
1287
1288                         if ((j.second & NEEDS_ENCRYPTION) && (!_film || !_film->encrypted())) {
1289                                 enabled = false;
1290                         }
1291
1292                         if ((j.second & NEEDS_DCP_CONTENT) && !have_dcp_content) {
1293                                 enabled = false;
1294                         }
1295
1296                         j.first->Enable (enabled);
1297                 }
1298         }
1299
1300         /** @return true if the operation that called this method
1301          *  should continue, false to abort it.
1302          */
1303         template <class T>
1304         bool maybe_save_film ()
1305         {
1306                 if (!_film) {
1307                         return true;
1308                 }
1309
1310                 while (_film->dirty()) {
1311                         T dialog(_film->name());
1312                         switch (dialog.run()) {
1313                         case wxID_NO:
1314                                 return true;
1315                         case wxID_YES:
1316                                 try {
1317                                         _film->write_metadata();
1318                                         return true;
1319                                 } catch (exception& e) {
1320                                         error_dialog(this, _("Could not save project."), std_to_wx(e.what()));
1321                                         /* Go round again for another try */
1322                                 }
1323                                 break;
1324                         case wxID_CANCEL:
1325                                 return false;
1326                         }
1327                 }
1328
1329                 return true;
1330         }
1331
1332         template <class T>
1333         bool maybe_save_then_delete_film ()
1334         {
1335                 bool const r = maybe_save_film<T> ();
1336                 if (r) {
1337                         _film.reset ();
1338                 }
1339                 return r;
1340         }
1341
1342         void add_item (wxMenu* menu, wxString text, int id, int sens)
1343         {
1344                 auto item = menu->Append (id, text);
1345                 menu_items.insert (make_pair (item, sens));
1346         }
1347
1348         void setup_menu (wxMenuBar* m)
1349         {
1350                 _file_menu = new wxMenu;
1351                 /* [Shortcut] Ctrl+N:New film */
1352                 add_item (_file_menu, _("New...\tCtrl-N"), ID_file_new, ALWAYS);
1353                 /* [Shortcut] Ctrl+O:Open existing film */
1354                 add_item (_file_menu, _("&Open...\tCtrl-O"), ID_file_open, ALWAYS);
1355                 _file_menu->AppendSeparator ();
1356                 /* [Shortcut] Ctrl+S:Save current film */
1357                 add_item (_file_menu, _("&Save\tCtrl-S"), ID_file_save, NEEDS_FILM);
1358                 _file_menu->AppendSeparator ();
1359                 add_item (_file_menu, _("Save as &template..."), ID_file_save_as_template, NEEDS_FILM);
1360                 add_item (_file_menu, _("Duplicate..."), ID_file_duplicate, NEEDS_FILM);
1361                 add_item (_file_menu, _("Duplicate and open..."), ID_file_duplicate_and_open, NEEDS_FILM);
1362
1363                 _history_position = _file_menu->GetMenuItems().GetCount();
1364
1365                 _file_menu->AppendSeparator ();
1366                 /* [Shortcut] Ctrl+W:Close current film */
1367                 add_item (_file_menu, _("&Close\tCtrl-W"), ID_file_close, NEEDS_FILM);
1368
1369 #ifndef __WXOSX__
1370                 _file_menu->AppendSeparator ();
1371 #endif
1372
1373 #ifdef __WXOSX__
1374                 add_item (_file_menu, _("&Exit"), wxID_EXIT, ALWAYS);
1375 #else
1376                 add_item (_file_menu, _("&Quit"), wxID_EXIT, ALWAYS);
1377 #endif
1378
1379                 auto edit = new wxMenu;
1380                 /* [Shortcut] Ctrl+C:Copy settings from currently selected content */
1381                 add_item (edit, _("Copy settings\tCtrl-C"), ID_edit_copy, NEEDS_FILM | NOT_DURING_DCP_CREATION | NEEDS_SINGLE_SELECTED_CONTENT);
1382                 /* [Shortcut] Ctrl+V:Paste settings into currently selected content */
1383                 add_item (edit, _("Paste settings...\tCtrl-V"), ID_edit_paste, NEEDS_FILM | NOT_DURING_DCP_CREATION | NEEDS_SELECTED_CONTENT | NEEDS_CLIPBOARD);
1384                 edit->AppendSeparator ();
1385                 /* [Shortcut] Shift+Ctrl+A:Select all content */
1386                 add_item (edit, _("Select all\tShift-Ctrl-A"), ID_edit_select_all, NEEDS_FILM);
1387
1388 #ifdef __WXOSX__
1389                 add_item(_file_menu, _("&Preferences...\tCtrl-,"), wxID_PREFERENCES, ALWAYS);
1390 #else
1391                 edit->AppendSeparator ();
1392                 /* [Shortcut] Ctrl+P:Open preferences window */
1393                 add_item (edit, _("&Preferences...\tCtrl-P"), wxID_PREFERENCES, ALWAYS);
1394 #endif
1395
1396                 auto jobs_menu = new wxMenu;
1397                 /* [Shortcut] Ctrl+M:Make DCP */
1398                 add_item (jobs_menu, _("&Make DCP\tCtrl-M"), ID_jobs_make_dcp, NEEDS_FILM | NOT_DURING_DCP_CREATION);
1399                 /* [Shortcut] Ctrl+B:Make DCP in the batch converter*/
1400                 add_item (jobs_menu, _("Make DCP in &batch converter\tCtrl-B"), ID_jobs_make_dcp_batch, NEEDS_FILM | NOT_DURING_DCP_CREATION);
1401                 jobs_menu->AppendSeparator ();
1402                 /* [Shortcut] Ctrl+K:Make KDMs */
1403                 add_item (jobs_menu, _("Make &KDMs...\tCtrl-K"), ID_jobs_make_kdms, NEEDS_FILM);
1404                 /* [Shortcut] Ctrl+D:Make DKDMs */
1405                 add_item (jobs_menu, _("Make &DKDMs...\tCtrl-D"), ID_jobs_make_dkdms, NEEDS_FILM);
1406                 add_item (jobs_menu, _("Make DKDM for DCP-o-matic..."), ID_jobs_make_self_dkdm, NEEDS_FILM | NEEDS_ENCRYPTION);
1407                 jobs_menu->AppendSeparator ();
1408                 /* [Shortcut] Ctrl+E:Export video file */
1409                 add_item (jobs_menu, _("Export video file...\tCtrl-E"), ID_jobs_export_video_file, NEEDS_FILM);
1410                 add_item (jobs_menu, _("Export subtitles..."), ID_jobs_export_subtitles, NEEDS_FILM);
1411                 jobs_menu->AppendSeparator ();
1412                 add_item (jobs_menu, _("&Send DCP to TMS"), ID_jobs_send_dcp_to_tms, NEEDS_FILM | NOT_DURING_DCP_CREATION | NEEDS_CPL);
1413
1414 #if defined(DCPOMATIC_OSX)
1415                 add_item (jobs_menu, _("S&how DCP in Finder"), ID_jobs_show_dcp, NEEDS_FILM | NOT_DURING_DCP_CREATION | NEEDS_CPL);
1416 #elif defined(DCPOMATIC_WINDOWS)
1417                 add_item (jobs_menu, _("S&how DCP in Explorer"), ID_jobs_show_dcp, NEEDS_FILM | NOT_DURING_DCP_CREATION | NEEDS_CPL);
1418 #else
1419                 add_item (jobs_menu, _("S&how DCP in Files"), ID_jobs_show_dcp, NEEDS_FILM | NOT_DURING_DCP_CREATION | NEEDS_CPL);
1420 #endif
1421
1422                 add_item (jobs_menu, _("Open DCP in &player"), ID_jobs_open_dcp_in_player, NEEDS_FILM | NOT_DURING_DCP_CREATION | NEEDS_CPL);
1423
1424                 auto view = new wxMenu;
1425                 add_item (view, _("Closed captions..."), ID_view_closed_captions, NEEDS_FILM);
1426                 add_item (view, _("Video waveform..."), ID_view_video_waveform, NEEDS_FILM);
1427
1428                 auto tools = new wxMenu;
1429                 add_item (tools, _("Version File (VF)..."), ID_tools_version_file, NEEDS_FILM | NEEDS_DCP_CONTENT);
1430                 add_item (tools, _("Hints..."), ID_tools_hints, NEEDS_FILM);
1431                 add_item (tools, _("Encoding servers..."), ID_tools_encoding_servers, 0);
1432                 add_item (tools, _("Manage templates..."), ID_tools_manage_templates, 0);
1433                 add_item (tools, _("Check for updates"), ID_tools_check_for_updates, 0);
1434                 add_item (tools, _("Send translations..."), ID_tools_send_translations, 0);
1435                 add_item (tools, _("System information..."), ID_tools_system_information, 0);
1436                 tools->AppendSeparator ();
1437                 add_item (tools, _("Restore default preferences"), ID_tools_restore_default_preferences, ALWAYS);
1438                 tools->AppendSeparator ();
1439                 add_item (tools, _("Export preferences..."), ID_tools_export_preferences, ALWAYS);
1440                 add_item (tools, _("Import preferences..."), ID_tools_import_preferences, ALWAYS);
1441
1442                 wxMenu* help = new wxMenu;
1443 #ifdef __WXOSX__
1444                 add_item (help, _("About DCP-o-matic"), wxID_ABOUT, ALWAYS);
1445 #else
1446                 add_item (help, _("About"), wxID_ABOUT, ALWAYS);
1447 #endif
1448                 add_item (help, _("Report a problem..."), ID_help_report_a_problem, NEEDS_FILM);
1449
1450                 m->Append (_file_menu, _("&File"));
1451                 m->Append (edit, _("&Edit"));
1452                 m->Append (jobs_menu, _("&Jobs"));
1453                 m->Append (view, _("&View"));
1454                 m->Append (tools, _("&Tools"));
1455                 m->Append (help, _("&Help"));
1456         }
1457
1458         void config_changed (Config::Property what)
1459         {
1460                 /* Instantly save any config changes when using the DCP-o-matic GUI */
1461                 switch (what) {
1462                 case Config::CINEMAS:
1463                         try {
1464                                 Config::instance()->write_cinemas();
1465                         } catch (exception& e) {
1466                                 error_dialog (
1467                                         this,
1468                                         wxString::Format (
1469                                                 _("Could not write to cinemas file at %s.  Your changes have not been saved."),
1470                                                 std_to_wx (Config::instance()->cinemas_file().string()).data()
1471                                                 )
1472                                         );
1473                         }
1474                         break;
1475                 case Config::DKDM_RECIPIENTS:
1476                         try {
1477                                 Config::instance()->write_dkdm_recipients();
1478                         } catch (exception& e) {
1479                                 error_dialog (
1480                                         this,
1481                                         wxString::Format (
1482                                                 _("Could not write to DKDM recipients file at %s.  Your changes have not been saved."),
1483                                                 std_to_wx(Config::instance()->dkdm_recipients_file().string()).data()
1484                                                 )
1485                                         );
1486                         }
1487                         break;
1488                 default:
1489                         try {
1490                                 Config::instance()->write_config();
1491                         } catch (exception& e) {
1492                                 error_dialog (
1493                                         this,
1494                                         wxString::Format (
1495                                                 _("Could not write to config file at %s.  Your changes have not been saved."),
1496                                                 std_to_wx (Config::instance()->cinemas_file().string()).data()
1497                                                 )
1498                                         );
1499                         }
1500                 }
1501
1502                 for (int i = 0; i < _history_items; ++i) {
1503                         delete _file_menu->Remove (ID_file_history + i);
1504                 }
1505
1506                 if (_history_separator) {
1507                         _file_menu->Remove (_history_separator);
1508                 }
1509                 delete _history_separator;
1510                 _history_separator = 0;
1511
1512                 int pos = _history_position;
1513
1514                 /* Clear out non-existent history items before we re-build the menu */
1515                 Config::instance()->clean_history ();
1516                 auto history = Config::instance()->history();
1517
1518                 if (!history.empty ()) {
1519                         _history_separator = _file_menu->InsertSeparator (pos++);
1520                 }
1521
1522                 for (size_t i = 0; i < history.size(); ++i) {
1523                         string s;
1524                         if (i < 9) {
1525                                 s = String::compose ("&%1 %2", i + 1, history[i].string());
1526                         } else {
1527                                 s = history[i].string();
1528                         }
1529                         _file_menu->Insert (pos++, ID_file_history + i, std_to_wx (s));
1530                 }
1531
1532                 _history_items = history.size ();
1533
1534                 dcpomatic_log->set_types (Config::instance()->log_types());
1535
1536 #ifdef DCPOMATIC_GROK
1537                 if (what == Config::GROK) {
1538                         setup_grok_library_path();
1539                 }
1540 #endif
1541         }
1542
1543         void update_checker_state_changed ()
1544         {
1545                 auto uc = UpdateChecker::instance ();
1546
1547                 bool const announce =
1548                         _update_news_requested ||
1549                         (uc->stable() && Config::instance()->check_for_updates()) ||
1550                         (uc->test() && Config::instance()->check_for_updates() && Config::instance()->check_for_test_updates());
1551
1552                 _update_news_requested = false;
1553
1554                 if (!announce) {
1555                         return;
1556                 }
1557
1558                 if (uc->state() == UpdateChecker::State::YES) {
1559                         UpdateDialog dialog(this, uc->stable(), uc->test());
1560                         dialog.ShowModal();
1561                 } else if (uc->state() == UpdateChecker::State::FAILED) {
1562                         error_dialog (this, _("The DCP-o-matic download server could not be contacted."));
1563                 } else {
1564                         error_dialog (this, _("There are no new versions of DCP-o-matic available."));
1565                 }
1566
1567                 _update_news_requested = false;
1568         }
1569
1570         void start_stop_pressed ()
1571         {
1572                 if (_film_viewer.playing()) {
1573                         _film_viewer.stop();
1574                 } else {
1575                         _film_viewer.start();
1576                 }
1577         }
1578
1579         void timeline_pressed ()
1580         {
1581                 _film_editor->content_panel()->timeline_clicked ();
1582         }
1583
1584         void back_frame ()
1585         {
1586                 _film_viewer.seek_by(-_film_viewer.one_video_frame(), true);
1587         }
1588
1589         void forward_frame ()
1590         {
1591                 _film_viewer.seek_by(_film_viewer.one_video_frame(), true);
1592         }
1593
1594         void analytics_message (string title, string html)
1595         {
1596                 HTMLDialog dialog(this, std_to_wx(title), std_to_wx(html));
1597                 dialog.ShowModal();
1598         }
1599
1600         void set_title ()
1601         {
1602                 auto s = wx_to_std(_("DCP-o-matic"));
1603                 if (_film) {
1604                         if (_film->directory()) {
1605                                 s += " - " + _film->directory()->string();
1606                         }
1607                         if (_film->dirty()) {
1608                                 s += " *";
1609                         }
1610                 }
1611
1612                 SetTitle (std_to_wx(s));
1613         }
1614
1615         FilmEditor* _film_editor;
1616         LimitedFrameSplitter* _splitter;
1617         wxPanel* _right_panel;
1618         FilmViewer _film_viewer;
1619         StandardControls* _controls;
1620         wx_ptr<VideoWaveformDialog> _video_waveform_dialog;
1621         SystemInformationDialog* _system_information_dialog = nullptr;
1622         DCPReferencingDialog* _dcp_referencing_dialog = nullptr;
1623         HintsDialog* _hints_dialog = nullptr;
1624         ServersListDialog* _servers_list_dialog = nullptr;
1625         wxPreferencesEditor* _config_dialog = nullptr;
1626         wx_ptr<KDMDialog> _kdm_dialog;
1627         wx_ptr<DKDMDialog> _dkdm_dialog;
1628         wx_ptr<TemplatesDialog> _templates_dialog;
1629         wxMenu* _file_menu = nullptr;
1630         shared_ptr<Film> _film;
1631         int _history_items = 0;
1632         int _history_position = 0;
1633         wxMenuItem* _history_separator = nullptr;
1634         boost::signals2::scoped_connection _config_changed_connection;
1635         boost::signals2::scoped_connection _analytics_message_connection;
1636         bool _update_news_requested = false;
1637         shared_ptr<Content> _clipboard;
1638         bool _first_shown_called = false;
1639 };
1640
1641
1642 static const wxCmdLineEntryDesc command_line_description[] = {
1643         { wxCMD_LINE_SWITCH, "n", "new", "create new film", wxCMD_LINE_VAL_NONE, wxCMD_LINE_PARAM_OPTIONAL },
1644         { wxCMD_LINE_OPTION, "c", "content", "add content file / directory", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
1645         { wxCMD_LINE_OPTION, "d", "dcp", "add content DCP", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
1646         { wxCMD_LINE_SWITCH, "v", "version", "show DCP-o-matic version", wxCMD_LINE_VAL_NONE, wxCMD_LINE_PARAM_OPTIONAL },
1647         { wxCMD_LINE_OPTION, "", "config", "directory containing config.xml and cinemas.xml", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
1648         { wxCMD_LINE_PARAM, 0, 0, "film to load or create", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
1649         { wxCMD_LINE_NONE, "", "", "", wxCmdLineParamType (0), 0 }
1650 };
1651
1652
1653 /** @class App
1654  *  @brief The magic App class for wxWidgets.
1655  */
1656 class App : public wxApp
1657 {
1658 public:
1659         App ()
1660                 : wxApp ()
1661         {
1662                 dcpomatic_setup_path_encoding ();
1663 #ifdef DCPOMATIC_LINUX
1664                 XInitThreads ();
1665 #endif
1666         }
1667
1668 private:
1669
1670         bool OnInit () override
1671         {
1672                 try {
1673
1674 #if defined(DCPOMATIC_WINDOWS)
1675                 if (Config::instance()->win32_console()) {
1676                         AllocConsole();
1677
1678                         HANDLE handle_out = GetStdHandle(STD_OUTPUT_HANDLE);
1679                         int hCrt = _open_osfhandle((intptr_t) handle_out, _O_TEXT);
1680                         FILE* hf_out = _fdopen(hCrt, "w");
1681                         setvbuf(hf_out, NULL, _IONBF, 1);
1682                         *stdout = *hf_out;
1683
1684                         HANDLE handle_in = GetStdHandle(STD_INPUT_HANDLE);
1685                         hCrt = _open_osfhandle((intptr_t) handle_in, _O_TEXT);
1686                         FILE* hf_in = _fdopen(hCrt, "r");
1687                         setvbuf(hf_in, NULL, _IONBF, 128);
1688                         *stdin = *hf_in;
1689
1690                         cout << "DCP-o-matic is starting." << "\n";
1691                 }
1692 #endif
1693                         wxInitAllImageHandlers ();
1694
1695                         Config::FailedToLoad.connect(boost::bind(&App::config_failed_to_load, this, _1));
1696                         Config::Warning.connect (boost::bind (&App::config_warning, this, _1));
1697
1698                         _splash = maybe_show_splash ();
1699
1700                         SetAppName (_("DCP-o-matic"));
1701
1702                         if (!wxApp::OnInit()) {
1703                                 return false;
1704                         }
1705
1706 #ifdef DCPOMATIC_LINUX
1707                         unsetenv ("UBUNTU_MENUPROXY");
1708 #endif
1709
1710 #ifdef DCPOMATIC_OSX
1711                         dcpomatic_sleep_seconds (1);
1712                         make_foreground_application ();
1713 #endif
1714
1715                         /* Enable i18n; this will create a Config object
1716                            to look for a force-configured language.  This Config
1717                            object will be wrong, however, because dcpomatic_setup
1718                            hasn't yet been called and there aren't any filters etc.
1719                            set up yet.
1720                         */
1721                         dcpomatic_setup_i18n ();
1722
1723                         /* Set things up, including filters etc.
1724                            which will now be internationalised correctly.
1725                         */
1726                         dcpomatic_setup ();
1727
1728                         /* Force the configuration to be re-loaded correctly next
1729                            time it is needed.
1730                         */
1731                         Config::drop ();
1732
1733                         /* We only look out for bad configuration from here on, as before
1734                            dcpomatic_setup() we haven't got OpenSSL ready so there will be
1735                            incorrect certificate chain validity errors.
1736                         */
1737                         Config::Bad.connect (boost::bind(&App::config_bad, this, _1));
1738
1739                         _frame = new DOMFrame (_("DCP-o-matic"));
1740                         SetTopWindow (_frame);
1741                         _frame->Maximize ();
1742                         close_splash ();
1743
1744                         if (running_32_on_64 ()) {
1745                                 NagDialog::maybe_nag (
1746                                         _frame, Config::NAG_32_ON_64,
1747                                         _("You are running the 32-bit version of DCP-o-matic on a 64-bit version of Windows.  This will limit the memory available to DCP-o-matic and may cause errors.  You are strongly advised to install the 64-bit version of DCP-o-matic."),
1748                                         false);
1749                         }
1750
1751                         _frame->Show ();
1752
1753                         signal_manager = new wxSignalManager (this);
1754                         Bind (wxEVT_IDLE, boost::bind (&App::idle, this, _1));
1755
1756                         if (!_film_to_load.empty() && dcp::filesystem::is_directory(_film_to_load)) {
1757                                 try {
1758                                         _frame->load_film (_film_to_load);
1759                                 } catch (exception& e) {
1760                                         error_dialog (nullptr, std_to_wx(String::compose(wx_to_std(_("Could not load film %1 (%2)")), _film_to_load)), std_to_wx(e.what()));
1761                                 }
1762                         }
1763
1764                         if (!_film_to_create.empty ()) {
1765                                 _frame->new_film (_film_to_create, optional<string>());
1766                                 if (!_content_to_add.empty()) {
1767                                         for (auto i: content_factory(_content_to_add)) {
1768                                                 _frame->film()->examine_and_add_content(i);
1769                                         }
1770                                 }
1771                                 if (!_dcp_to_add.empty ()) {
1772                                         _frame->film()->examine_and_add_content(make_shared<DCPContent>(_dcp_to_add));
1773                                 }
1774                         }
1775
1776                         Bind (wxEVT_TIMER, boost::bind (&App::check, this));
1777                         _timer.reset (new wxTimer (this));
1778                         _timer->Start (1000);
1779
1780                         if (Config::instance()->check_for_updates ()) {
1781                                 UpdateChecker::instance()->run ();
1782                         }
1783
1784                         auto release_notes = find_release_notes(gui_is_dark());
1785                         if (release_notes) {
1786                                 HTMLDialog notes(nullptr, _("Release notes"), std_to_wx(*release_notes), true);
1787                                 notes.Centre();
1788                                 notes.ShowModal();
1789                         }
1790
1791 #ifdef DCPOMATIC_GROK
1792                         grk_plugin::setMessengerLogger(new grk_plugin::GrokLogger("[GROK] "));
1793                         setup_grok_library_path();
1794 #endif
1795                 }
1796                 catch (exception& e)
1797                 {
1798                         close_splash();
1799                         error_dialog (nullptr, wxString::Format ("DCP-o-matic could not start."), std_to_wx(e.what()));
1800                 }
1801
1802                 return true;
1803         }
1804
1805         void OnInitCmdLine (wxCmdLineParser& parser) override
1806         {
1807                 parser.SetDesc (command_line_description);
1808                 parser.SetSwitchChars (wxT ("-"));
1809         }
1810
1811         bool OnCmdLineParsed (wxCmdLineParser& parser) override
1812         {
1813                 if (parser.Found (wxT("version"))) {
1814                         cout << "dcpomatic version " << dcpomatic_version << " " << dcpomatic_git_commit << "\n";
1815                         exit (EXIT_SUCCESS);
1816                 }
1817
1818                 if (parser.GetParamCount() > 0) {
1819                         if (parser.Found (wxT ("new"))) {
1820                                 _film_to_create = wx_to_std (parser.GetParam (0));
1821                         } else {
1822                                 _film_to_load = wx_to_std (parser.GetParam (0));
1823                         }
1824                 }
1825
1826                 wxString content;
1827                 if (parser.Found (wxT ("content"), &content)) {
1828                         _content_to_add = wx_to_std (content);
1829                 }
1830
1831                 wxString dcp;
1832                 if (parser.Found (wxT ("dcp"), &dcp)) {
1833                         _dcp_to_add = wx_to_std (dcp);
1834                 }
1835
1836                 wxString config;
1837                 if (parser.Found (wxT("config"), &config)) {
1838                         State::override_path = wx_to_std (config);
1839                 }
1840
1841                 return true;
1842         }
1843
1844         void report_exception ()
1845         {
1846                 try {
1847                         throw;
1848                 } catch (FileError& e) {
1849                         error_dialog (
1850                                 nullptr,
1851                                 wxString::Format(
1852                                         _("An exception occurred: %s (%s)\n\n") + REPORT_PROBLEM,
1853                                         std_to_wx (e.what()),
1854                                         std_to_wx (e.file().string().c_str())
1855                                         )
1856                                 );
1857                 } catch (boost::filesystem::filesystem_error& e) {
1858                         error_dialog (
1859                                 nullptr,
1860                                 wxString::Format(
1861                                         _("An exception occurred: %s (%s) (%s)\n\n") + REPORT_PROBLEM,
1862                                         std_to_wx (e.what()),
1863                                         std_to_wx (e.path1().string()),
1864                                         std_to_wx (e.path2().string())
1865                                         )
1866                                 );
1867                 } catch (exception& e) {
1868                         error_dialog (
1869                                 nullptr,
1870                                 wxString::Format(
1871                                         _("An exception occurred: %s.\n\n") + REPORT_PROBLEM,
1872                                         std_to_wx (e.what ())
1873                                         )
1874                                 );
1875                 } catch (...) {
1876                         error_dialog (0, _("An unknown exception occurred.") + "  " + REPORT_PROBLEM);
1877                 }
1878         }
1879
1880         /* An unhandled exception has occurred inside the main event loop */
1881         bool OnExceptionInMainLoop () override
1882         {
1883                 report_exception ();
1884                 /* This will terminate the program */
1885                 return false;
1886         }
1887
1888         void OnUnhandledException () override
1889         {
1890                 report_exception ();
1891         }
1892
1893         void idle (wxIdleEvent& ev)
1894         {
1895                 signal_manager->ui_idle ();
1896                 ev.Skip ();
1897         }
1898
1899         void check ()
1900         {
1901                 try {
1902                         EncodeServerFinder::instance()->rethrow ();
1903                 } catch (exception& e) {
1904                         error_dialog (0, std_to_wx (e.what ()));
1905                 }
1906         }
1907
1908         void close_splash ()
1909         {
1910                 if (_splash) {
1911                         _splash->Destroy();
1912                         _splash = nullptr;
1913                 }
1914         }
1915
1916         void config_failed_to_load (Config::LoadFailure what)
1917         {
1918                 report_config_load_failure(_frame, what);
1919         }
1920
1921         void config_warning (string m)
1922         {
1923                 message_dialog (_frame, std_to_wx(m));
1924         }
1925
1926         bool config_bad (Config::BadReason reason)
1927         {
1928                 /* Destroy the splash screen here, as otherwise bad things seem to happen (for reasons unknown)
1929                    when we open our recreate dialog, close it, *then* try to Destroy the splash (the Destroy fails).
1930                 */
1931                 close_splash();
1932
1933                 auto config = Config::instance();
1934                 switch (reason) {
1935                 case Config::BAD_SIGNER_UTF8_STRINGS:
1936                 {
1937                         if (config->nagged(Config::NAG_BAD_SIGNER_CHAIN_UTF8)) {
1938                                 return false;
1939                         }
1940                         RecreateChainDialog dialog(
1941                                 _frame, _("Recreate signing certificates"),
1942                                 _("The certificate chain that DCP-o-matic uses for signing DCPs and KDMs contains a small error\n"
1943                                   "which will prevent DCPs from being validated correctly on some systems.  Do you want to re-create\n"
1944                                   "the certificate chain for signing DCPs and KDMs?"),
1945                                 _("Do nothing"),
1946                                 Config::NAG_BAD_SIGNER_CHAIN_UTF8
1947                                 );
1948                         return dialog.ShowModal() == wxID_OK;
1949                 }
1950                 case Config::BAD_SIGNER_VALIDITY_TOO_LONG:
1951                 {
1952                         if (config->nagged(Config::NAG_BAD_SIGNER_CHAIN_VALIDITY)) {
1953                                 return false;
1954                         }
1955                         RecreateChainDialog dialog(
1956                                 _frame, _("Recreate signing certificates"),
1957                                 _("The certificate chain that DCP-o-matic uses for signing DCPs and KDMs has a validity period\n"
1958                                   "that is too long.  This will cause problems playing back DCPs on some systems.\n"
1959                                   "Do you want to re-create the certificate chain for signing DCPs and KDMs?"),
1960                                 _("Do nothing"),
1961                                 Config::NAG_BAD_SIGNER_CHAIN_VALIDITY
1962                                 );
1963                         return dialog.ShowModal() == wxID_OK;
1964                 }
1965                 case Config::BAD_SIGNER_INCONSISTENT:
1966                 {
1967                         RecreateChainDialog dialog(
1968                                 _frame, _("Recreate signing certificates"),
1969                                 _("The certificate chain that DCP-o-matic uses for signing DCPs and KDMs is inconsistent and\n"
1970                                   "cannot be used.  DCP-o-matic cannot start unless you re-create it.  Do you want to re-create\n"
1971                                   "the certificate chain for signing DCPs and KDMs?"),
1972                                 _("Close DCP-o-matic")
1973                                 );
1974                         if (dialog.ShowModal() != wxID_OK) {
1975                                 exit (EXIT_FAILURE);
1976                         }
1977                         return true;
1978                 }
1979                 case Config::BAD_DECRYPTION_INCONSISTENT:
1980                 {
1981                         RecreateChainDialog dialog(
1982                                 _frame, _("Recreate KDM decryption chain"),
1983                                 _("The certificate chain that DCP-o-matic uses for decrypting KDMs is inconsistent and\n"
1984                                   "cannot be used.  DCP-o-matic cannot start unless you re-create it.  Do you want to re-create\n"
1985                                   "the certificate chain for decrypting KDMs?  You may want to say \"No\" here and back up your\n"
1986                                   "configuration before continuing."),
1987                                 _("Close DCP-o-matic")
1988                                 );
1989                         if (dialog.ShowModal() != wxID_OK) {
1990                                 exit (EXIT_FAILURE);
1991                         }
1992                         return true;
1993                 }
1994                 case Config::BAD_SIGNER_DN_QUALIFIER:
1995                 {
1996                         RecreateChainDialog dialog(
1997                                 _frame, _("Recreate signing certificates"),
1998                                 _("The certificate chain that DCP-o-matic uses for signing DCPs and KDMs contains a small error\n"
1999                                   "which will prevent DCPs from being validated correctly on some systems.  This error was caused\n"
2000                                   "by a bug in DCP-o-matic which has now been fixed. Do you want to re-create the certificate chain\n"
2001                                   "for signing DCPs and KDMs?"),
2002                                 _("Do nothing"),
2003                                 Config::NAG_BAD_SIGNER_DN_QUALIFIER
2004                                 );
2005                         return dialog.ShowModal() == wxID_OK;
2006                 }
2007                 default:
2008                         DCPOMATIC_ASSERT (false);
2009                 }
2010         }
2011
2012         DOMFrame* _frame = nullptr;
2013         wxSplashScreen* _splash = nullptr;
2014         shared_ptr<wxTimer> _timer;
2015         string _film_to_load;
2016         string _film_to_create;
2017         string _content_to_add;
2018         string _dcp_to_add;
2019 };
2020
2021
2022 IMPLEMENT_APP (App)