Allow content factory to return multiple content.
[dcpomatic.git] / src / tools / dcpomatic.cc
1 /*
2     Copyright (C) 2012-2016 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21 /** @file  src/tools/dcpomatic.cc
22  *  @brief The main DCP-o-matic GUI.
23  */
24
25 #include "wx/film_viewer.h"
26 #include "wx/film_editor.h"
27 #include "wx/job_manager_view.h"
28 #include "wx/config_dialog.h"
29 #include "wx/wx_util.h"
30 #include "wx/new_film_dialog.h"
31 #include "wx/wx_signal_manager.h"
32 #include "wx/about_dialog.h"
33 #include "wx/kdm_dialog.h"
34 #include "wx/self_dkdm_dialog.h"
35 #include "wx/servers_list_dialog.h"
36 #include "wx/hints_dialog.h"
37 #include "wx/update_dialog.h"
38 #include "wx/content_panel.h"
39 #include "wx/report_problem_dialog.h"
40 #include "wx/video_waveform_dialog.h"
41 #include "wx/save_template_dialog.h"
42 #include "wx/templates_dialog.h"
43 #include "lib/film.h"
44 #include "lib/config.h"
45 #include "lib/util.h"
46 #include "lib/video_content.h"
47 #include "lib/content.h"
48 #include "lib/version.h"
49 #include "lib/signal_manager.h"
50 #include "lib/log.h"
51 #include "lib/job_manager.h"
52 #include "lib/exceptions.h"
53 #include "lib/cinema.h"
54 #include "lib/screen_kdm.h"
55 #include "lib/send_kdm_email_job.h"
56 #include "lib/encode_server_finder.h"
57 #include "lib/update_checker.h"
58 #include "lib/cross.h"
59 #include "lib/content_factory.h"
60 #include "lib/compose.hpp"
61 #include "lib/cinema_kdms.h"
62 #include "lib/dcpomatic_socket.h"
63 #include "lib/hints.h"
64 #include <dcp/exceptions.h>
65 #include <dcp/raw_convert.h>
66 #include <wx/generic/aboutdlgg.h>
67 #include <wx/stdpaths.h>
68 #include <wx/cmdline.h>
69 #include <wx/preferences.h>
70 #include <wx/splash.h>
71 #ifdef __WXMSW__
72 #include <shellapi.h>
73 #endif
74 #ifdef __WXOSX__
75 #include <ApplicationServices/ApplicationServices.h>
76 #endif
77 #include <boost/filesystem.hpp>
78 #include <boost/noncopyable.hpp>
79 #include <boost/foreach.hpp>
80 #include <iostream>
81 #include <fstream>
82 /* This is OK as it's only used with DCPOMATIC_WINDOWS */
83 #include <sstream>
84
85 #ifdef check
86 #undef check
87 #endif
88
89 using std::cout;
90 using std::wcout;
91 using std::string;
92 using std::vector;
93 using std::wstring;
94 using std::wstringstream;
95 using std::map;
96 using std::make_pair;
97 using std::list;
98 using std::exception;
99 using boost::shared_ptr;
100 using boost::dynamic_pointer_cast;
101 using boost::optional;
102 using dcp::raw_convert;
103
104 class FilmChangedDialog : public boost::noncopyable
105 {
106 public:
107         FilmChangedDialog (string name)
108         {
109                 _dialog = new wxMessageDialog (
110                         0,
111                         wxString::Format (_("Save changes to film \"%s\" before closing?"), std_to_wx (name).data()),
112                         /// TRANSLATORS: this is the heading for a dialog box, which tells the user that the current
113                         /// project (Film) has been changed since it was last saved.
114                         _("Film changed"),
115                         wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxICON_QUESTION
116                         );
117
118                 _dialog->SetYesNoCancelLabels (
119                         _("Save film and close"), _("Close without saving film"), _("Don't close")
120                         );
121         }
122
123         ~FilmChangedDialog ()
124         {
125                 _dialog->Destroy ();
126         }
127
128         int run ()
129         {
130                 return _dialog->ShowModal ();
131         }
132
133 private:
134         wxMessageDialog* _dialog;
135 };
136
137 #define ALWAYS                       0x0
138 #define NEEDS_FILM                   0x1
139 #define NOT_DURING_DCP_CREATION      0x2
140 #define NEEDS_CPL                    0x4
141 #define NEEDS_SELECTED_VIDEO_CONTENT 0x8
142
143 map<wxMenuItem*, int> menu_items;
144
145 enum {
146         ID_file_new = 1,
147         ID_file_open,
148         ID_file_save,
149         ID_file_save_as_template,
150         ID_file_history,
151         /* Allow spare IDs after _history for the recent files list */
152         ID_content_scale_to_fit_width = 100,
153         ID_content_scale_to_fit_height,
154         ID_jobs_make_dcp,
155         ID_jobs_make_dcp_batch,
156         ID_jobs_make_kdms,
157         ID_jobs_make_self_dkdm,
158         ID_jobs_send_dcp_to_tms,
159         ID_jobs_show_dcp,
160         ID_tools_video_waveform,
161         ID_tools_hints,
162         ID_tools_encoding_servers,
163         ID_tools_manage_templates,
164         ID_tools_check_for_updates,
165         ID_tools_restore_default_preferences,
166         ID_help_report_a_problem,
167         /* IDs for shortcuts (with no associated menu item) */
168         ID_add_file,
169         ID_remove
170 };
171
172 class DOMFrame : public wxFrame
173 {
174 public:
175         DOMFrame (wxString const & title)
176                 : wxFrame (NULL, -1, title)
177                 , _video_waveform_dialog (0)
178                 , _hints_dialog (0)
179                 , _servers_list_dialog (0)
180                 , _config_dialog (0)
181                 , _kdm_dialog (0)
182                 , _templates_dialog (0)
183                 , _file_menu (0)
184                 , _history_items (0)
185                 , _history_position (0)
186                 , _history_separator (0)
187                 , _update_news_requested (false)
188         {
189 #if defined(DCPOMATIC_WINDOWS)
190                 if (Config::instance()->win32_console ()) {
191                         AllocConsole();
192
193                         HANDLE handle_out = GetStdHandle(STD_OUTPUT_HANDLE);
194                         int hCrt = _open_osfhandle((intptr_t) handle_out, _O_TEXT);
195                         FILE* hf_out = _fdopen(hCrt, "w");
196                         setvbuf(hf_out, NULL, _IONBF, 1);
197                         *stdout = *hf_out;
198
199                         HANDLE handle_in = GetStdHandle(STD_INPUT_HANDLE);
200                         hCrt = _open_osfhandle((intptr_t) handle_in, _O_TEXT);
201                         FILE* hf_in = _fdopen(hCrt, "r");
202                         setvbuf(hf_in, NULL, _IONBF, 128);
203                         *stdin = *hf_in;
204
205                         cout << "DCP-o-matic is starting." << "\n";
206                 }
207 #endif
208
209                 wxMenuBar* bar = new wxMenuBar;
210                 setup_menu (bar);
211                 SetMenuBar (bar);
212
213                 _config_changed_connection = Config::instance()->Changed.connect (boost::bind (&DOMFrame::config_changed, this));
214                 config_changed ();
215
216                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_new, this),                ID_file_new);
217                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_open, this),               ID_file_open);
218                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_save, this),               ID_file_save);
219                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_save_as_template, this),   ID_file_save_as_template);
220                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_history, this, _1),        ID_file_history, ID_file_history + HISTORY_SIZE);
221                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_exit, this),               wxID_EXIT);
222                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::edit_preferences, this),        wxID_PREFERENCES);
223                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::content_scale_to_fit_width, this), ID_content_scale_to_fit_width);
224                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::content_scale_to_fit_height, this), ID_content_scale_to_fit_height);
225                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_make_dcp, this),           ID_jobs_make_dcp);
226                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_make_kdms, this),          ID_jobs_make_kdms);
227                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_make_dcp_batch, this),     ID_jobs_make_dcp_batch);
228                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_make_self_dkdm, this),     ID_jobs_make_self_dkdm);
229                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_send_dcp_to_tms, this),    ID_jobs_send_dcp_to_tms);
230                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::jobs_show_dcp, this),           ID_jobs_show_dcp);
231                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_video_waveform, this),    ID_tools_video_waveform);
232                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_hints, this),             ID_tools_hints);
233                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_encoding_servers, this),  ID_tools_encoding_servers);
234                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_manage_templates, this),  ID_tools_manage_templates);
235                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_check_for_updates, this), ID_tools_check_for_updates);
236                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_restore_default_preferences, this), ID_tools_restore_default_preferences);
237                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::help_about, this),              wxID_ABOUT);
238                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::help_report_a_problem, this),   ID_help_report_a_problem);
239
240                 Bind (wxEVT_CLOSE_WINDOW, boost::bind (&DOMFrame::close, this, _1));
241
242                 /* Use a panel as the only child of the Frame so that we avoid
243                    the dark-grey background on Windows.
244                 */
245                 wxPanel* overall_panel = new wxPanel (this, wxID_ANY);
246
247                 _film_viewer = new FilmViewer (overall_panel);
248                 _film_editor = new FilmEditor (overall_panel, _film_viewer);
249                 JobManagerView* job_manager_view = new JobManagerView (overall_panel, false);
250
251                 wxBoxSizer* right_sizer = new wxBoxSizer (wxVERTICAL);
252                 right_sizer->Add (_film_viewer, 2, wxEXPAND | wxALL, 6);
253                 right_sizer->Add (job_manager_view, 1, wxEXPAND | wxALL, 6);
254
255                 wxBoxSizer* main_sizer = new wxBoxSizer (wxHORIZONTAL);
256                 main_sizer->Add (_film_editor, 1, wxEXPAND | wxALL, 6);
257                 main_sizer->Add (right_sizer, 2, wxEXPAND | wxALL, 6);
258
259                 set_menu_sensitivity ();
260
261                 _film_editor->FileChanged.connect (bind (&DOMFrame::file_changed, this, _1));
262                 file_changed ("");
263
264                 JobManager::instance()->ActiveJobsChanged.connect (boost::bind (&DOMFrame::set_menu_sensitivity, this));
265
266                 overall_panel->SetSizer (main_sizer);
267
268                 wxAcceleratorEntry accel[2];
269                 accel[0].Set (wxACCEL_CTRL, static_cast<int>('A'), ID_add_file);
270                 accel[1].Set (wxACCEL_NORMAL, WXK_DELETE, ID_remove);
271                 Bind (wxEVT_MENU, boost::bind (&ContentPanel::add_file_clicked, _film_editor->content_panel()), ID_add_file);
272                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::remove_clicked, this, _1), ID_remove);
273                 wxAcceleratorTable accel_table (2, accel);
274                 SetAcceleratorTable (accel_table);
275
276                 /* Instantly save any config changes when using the DCP-o-matic GUI */
277                 Config::instance()->Changed.connect (boost::bind (&Config::write, Config::instance ()));
278
279                 UpdateChecker::instance()->StateChanged.connect (boost::bind (&DOMFrame::update_checker_state_changed, this));
280         }
281
282         void remove_clicked (wxCommandEvent& ev)
283         {
284                 if (_film_editor->content_panel()->remove_clicked (true)) {
285                         ev.Skip ();
286                 }
287         }
288
289         void new_film (boost::filesystem::path path, optional<string> template_name)
290         {
291                 shared_ptr<Film> film (new Film (path));
292                 if (template_name) {
293                         film->use_template (template_name.get());
294                 }
295                 film->write_metadata ();
296                 film->set_name (path.filename().generic_string());
297                 set_film (film);
298         }
299
300         void load_film (boost::filesystem::path file)
301         try
302         {
303                 shared_ptr<Film> film (new Film (file));
304                 list<string> const notes = film->read_metadata ();
305
306                 if (film->state_version() == 4) {
307                         error_dialog (
308                                 0,
309                                 _("This film was created with an old version of DVD-o-matic and may not load correctly "
310                                   "in this version.  Please check the film's settings carefully.")
311                                 );
312                 }
313
314                 for (list<string>::const_iterator i = notes.begin(); i != notes.end(); ++i) {
315                         error_dialog (0, std_to_wx (*i));
316                 }
317
318                 set_film (film);
319         }
320         catch (std::exception& e) {
321                 wxString p = std_to_wx (file.string ());
322                 wxCharBuffer b = p.ToUTF8 ();
323                 error_dialog (this, wxString::Format (_("Could not open film at %s (%s)"), p.data(), std_to_wx (e.what()).data()));
324         }
325
326         void set_film (shared_ptr<Film> film)
327         {
328                 _film = film;
329                 _film_viewer->set_film (_film);
330                 _film_editor->set_film (_film);
331                 delete _video_waveform_dialog;
332                 _video_waveform_dialog = 0;
333                 set_menu_sensitivity ();
334                 if (_film->directory()) {
335                         Config::instance()->add_to_history (_film->directory().get());
336                 }
337         }
338
339         shared_ptr<Film> film () const {
340                 return _film;
341         }
342
343 private:
344
345         void file_changed (boost::filesystem::path f)
346         {
347                 string s = wx_to_std (_("DCP-o-matic"));
348                 if (!f.empty ()) {
349                         s += " - " + f.string ();
350                 }
351
352                 SetTitle (std_to_wx (s));
353         }
354
355         void file_new ()
356         {
357                 NewFilmDialog* d = new NewFilmDialog (this);
358                 int const r = d->ShowModal ();
359
360                 if (r == wxID_OK) {
361
362                         if (boost::filesystem::is_directory (d->path()) && !boost::filesystem::is_empty(d->path())) {
363                                 if (!confirm_dialog (
364                                             this,
365                                             std_to_wx (
366                                                     String::compose (wx_to_std (_("The directory %1 already exists and is not empty.  "
367                                                                                   "Are you sure you want to use it?")),
368                                                                      d->path().string().c_str())
369                                                     )
370                                             )) {
371                                         return;
372                                 }
373                         } else if (boost::filesystem::is_regular_file (d->path())) {
374                                 error_dialog (
375                                         this,
376                                         String::compose (wx_to_std (_("%1 already exists as a file, so you cannot use it for a new film.")), d->path().c_str())
377                                         );
378                                 return;
379                         }
380
381                         if (maybe_save_then_delete_film ()) {
382                                 new_film (d->path(), d->template_name());
383                         }
384                 }
385
386                 d->Destroy ();
387         }
388
389         void file_open ()
390         {
391                 wxDirDialog* c = new wxDirDialog (
392                         this,
393                         _("Select film to open"),
394                         std_to_wx (Config::instance()->default_directory_or (wx_to_std (wxStandardPaths::Get().GetDocumentsDir())).string ()),
395                         wxDEFAULT_DIALOG_STYLE | wxDD_DIR_MUST_EXIST
396                         );
397
398                 int r;
399                 while (true) {
400                         r = c->ShowModal ();
401                         if (r == wxID_OK && c->GetPath() == wxStandardPaths::Get().GetDocumentsDir()) {
402                                 error_dialog (this, _("You did not select a folder.  Make sure that you select a folder before clicking Open."));
403                         } else {
404                                 break;
405                         }
406                 }
407
408                 if (r == wxID_OK && maybe_save_then_delete_film()) {
409                         load_film (wx_to_std (c->GetPath ()));
410                 }
411
412                 c->Destroy ();
413         }
414
415         void file_save ()
416         {
417                 _film->write_metadata ();
418         }
419
420         void file_save_as_template ()
421         {
422                 SaveTemplateDialog* d = new SaveTemplateDialog (this);
423                 int const r = d->ShowModal ();
424                 if (r == wxID_OK) {
425                         Config::instance()->save_template (_film, d->name ());
426                 }
427                 d->Destroy ();
428         }
429
430         void file_history (wxCommandEvent& event)
431         {
432                 vector<boost::filesystem::path> history = Config::instance()->history ();
433                 int n = event.GetId() - ID_file_history;
434                 if (n >= 0 && n < static_cast<int> (history.size ()) && maybe_save_then_delete_film()) {
435                         load_film (history[n]);
436                 }
437         }
438
439         void file_exit ()
440         {
441                 /* false here allows the close handler to veto the close request */
442                 Close (false);
443         }
444
445         void edit_preferences ()
446         {
447                 if (!_config_dialog) {
448                         _config_dialog = create_config_dialog ();
449                 }
450                 _config_dialog->Show (this);
451         }
452
453         void tools_restore_default_preferences ()
454         {
455                 wxMessageDialog* d = new wxMessageDialog (
456                         0,
457                         _("Are you sure you want to restore preferences to their defaults?  This cannot be undone."),
458                         _("Restore default preferences"),
459                         wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION
460                         );
461
462                 int const r = d->ShowModal ();
463                 d->Destroy ();
464
465                 if (r == wxID_YES) {
466                         Config::restore_defaults ();
467                 }
468         }
469
470         void jobs_make_dcp ()
471         {
472                 double required;
473                 double available;
474                 bool can_hard_link;
475
476                 if (!_film->should_be_enough_disk_space (required, available, can_hard_link)) {
477                         wxString message;
478                         if (can_hard_link) {
479                                 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);
480                         } else {
481                                 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);
482                         }
483                         if (!confirm_dialog (this, message)) {
484                                 return;
485                         }
486                 }
487
488                 if (!get_hints(_film).empty() && Config::instance()->show_hints_before_make_dcp()) {
489                         HintsDialog* hints = new HintsDialog (this, _film, false);
490                         int const r = hints->ShowModal();
491                         hints->Destroy ();
492                         if (r == wxID_CANCEL) {
493                                 return;
494                         }
495                 }
496
497                 try {
498                         /* It seems to make sense to auto-save metadata here, since the make DCP may last
499                            a long time, and crashes/power failures are moderately likely.
500                         */
501                         _film->write_metadata ();
502                         _film->make_dcp ();
503                 } catch (BadSettingError& e) {
504                         error_dialog (this, wxString::Format (_("Bad setting for %s (%s)"), std_to_wx(e.setting()).data(), std_to_wx(e.what()).data()));
505                 } catch (std::exception& e) {
506                         error_dialog (this, wxString::Format (_("Could not make DCP: %s"), std_to_wx(e.what()).data()));
507                 }
508         }
509
510         void jobs_make_kdms ()
511         {
512                 if (!_film) {
513                         return;
514                 }
515
516                 if (_kdm_dialog) {
517                         _kdm_dialog->Destroy ();
518                         _kdm_dialog = 0;
519                 }
520
521                 _kdm_dialog = new KDMDialog (this, _film);
522                 _kdm_dialog->Show ();
523         }
524
525         void jobs_make_dcp_batch ()
526         {
527                 if (!_film) {
528                         return;
529                 }
530
531                 if (!get_hints(_film).empty() && Config::instance()->show_hints_before_make_dcp()) {
532                         HintsDialog* hints = new HintsDialog (this, _film, false);
533                         int const r = hints->ShowModal();
534                         hints->Destroy ();
535                         if (r == wxID_CANCEL) {
536                                 return;
537                         }
538                 }
539
540                 _film->write_metadata ();
541
542                 /* i = 0; try to connect via socket
543                    i = 1; try again, and then try to start the batch converter
544                    i = 2 onwards; try again.
545                 */
546                 for (int i = 0; i < 8; ++i) {
547                         try {
548                                 boost::asio::io_service io_service;
549                                 boost::asio::ip::tcp::resolver resolver (io_service);
550                                 boost::asio::ip::tcp::resolver::query query ("127.0.0.1", raw_convert<string> (Config::instance()->server_port_base() + 2));
551                                 boost::asio::ip::tcp::resolver::iterator endpoint_iterator = resolver.resolve (query);
552                                 Socket socket (5);
553                                 socket.connect (*endpoint_iterator);
554                                 DCPOMATIC_ASSERT (_film->directory ());
555                                 string s = _film->directory()->string ();
556                                 socket.write (s.length() + 1);
557                                 socket.write ((uint8_t *) s.c_str(), s.length() + 1);
558                                 /* OK\0 */
559                                 uint8_t ok[3];
560                                 socket.read (ok, 3);
561                                 return;
562                         } catch (exception& e) {
563
564                         }
565
566                         if (i == 1) {
567                                 start_batch_converter (wx_to_std (wxStandardPaths::Get().GetExecutablePath()));
568                         }
569
570                         dcpomatic_sleep (1);
571                 }
572
573                 error_dialog (this, _("Could not find batch converter."));
574         }
575
576         void jobs_make_self_dkdm ()
577         {
578                 if (!_film) {
579                         return;
580                 }
581
582                 SelfDKDMDialog* d = new SelfDKDMDialog (this, _film);
583                 if (d->ShowModal () != wxID_OK) {
584                         d->Destroy ();
585                         return;
586                 }
587
588                 optional<dcp::EncryptedKDM> kdm;
589                 try {
590                         kdm = _film->make_kdm (
591                                 Config::instance()->decryption_chain()->leaf(),
592                                 vector<dcp::Certificate> (),
593                                 d->cpl (),
594                                 dcp::LocalTime ("2012-01-01T01:00:00+00:00"),
595                                 dcp::LocalTime ("2112-01-01T01:00:00+00:00"),
596                                 dcp::MODIFIED_TRANSITIONAL_1
597                                 );
598                 } catch (dcp::NotEncryptedError& e) {
599                         error_dialog (this, _("CPL's content is not encrypted."));
600                 } catch (exception& e) {
601                         error_dialog (this, e.what ());
602                 } catch (...) {
603                         error_dialog (this, _("An unknown exception occurred."));
604                 }
605
606                 if (kdm) {
607                         if (d->internal ()) {
608                                 vector<dcp::EncryptedKDM> dkdms = Config::instance()->dkdms ();
609                                 dkdms.push_back (kdm.get());
610                                 Config::instance()->set_dkdms (dkdms);
611                         } else {
612                                 boost::filesystem::path path = d->directory() / (_film->dcp_name(false) + "_DKDM.xml");
613                                 kdm->as_xml (path);
614                         }
615                 }
616
617                 d->Destroy ();
618         }
619
620         void content_scale_to_fit_width ()
621         {
622                 ContentList vc = _film_editor->content_panel()->selected_video ();
623                 for (ContentList::iterator i = vc.begin(); i != vc.end(); ++i) {
624                         (*i)->video->scale_and_crop_to_fit_width ();
625                 }
626         }
627
628         void content_scale_to_fit_height ()
629         {
630                 ContentList vc = _film_editor->content_panel()->selected_video ();
631                 for (ContentList::iterator i = vc.begin(); i != vc.end(); ++i) {
632                         (*i)->video->scale_and_crop_to_fit_height ();
633                 }
634         }
635
636         void jobs_send_dcp_to_tms ()
637         {
638                 _film->send_dcp_to_tms ();
639         }
640
641         void jobs_show_dcp ()
642         {
643                 DCPOMATIC_ASSERT (_film->directory ());
644 #ifdef DCPOMATIC_WINDOWS
645                 wstringstream args;
646                 args << "/select," << _film->dir (_film->dcp_name(false));
647                 ShellExecute (0, L"open", L"explorer.exe", args.str().c_str(), 0, SW_SHOWDEFAULT);
648 #endif
649
650 #ifdef DCPOMATIC_LINUX
651                 int r = system ("which nautilus");
652                 if (WEXITSTATUS (r) == 0) {
653                         r = system (string ("nautilus " + _film->directory()->string()).c_str ());
654                         if (WEXITSTATUS (r)) {
655                                 error_dialog (this, _("Could not show DCP (could not run nautilus)"));
656                         }
657                 } else {
658                         int r = system ("which konqueror");
659                         if (WEXITSTATUS (r) == 0) {
660                                 r = system (string ("konqueror " + _film->directory()->string()).c_str ());
661                                 if (WEXITSTATUS (r)) {
662                                         error_dialog (this, _("Could not show DCP (could not run konqueror)"));
663                                 }
664                         }
665                 }
666 #endif
667
668 #ifdef DCPOMATIC_OSX
669                 int r = system (string ("open -R " + _film->dir (_film->dcp_name (false)).string ()).c_str ());
670                 if (WEXITSTATUS (r)) {
671                         error_dialog (this, _("Could not show DCP"));
672                 }
673 #endif
674         }
675
676         void tools_video_waveform ()
677         {
678                 if (!_video_waveform_dialog) {
679                         _video_waveform_dialog = new VideoWaveformDialog (this, _film, _film_viewer);
680                 }
681
682                 _video_waveform_dialog->Show ();
683         }
684
685         void tools_hints ()
686         {
687                 if (!_hints_dialog) {
688                         _hints_dialog = new HintsDialog (this, _film, true);
689                 }
690
691                 _hints_dialog->Show ();
692         }
693
694         void tools_encoding_servers ()
695         {
696                 if (!_servers_list_dialog) {
697                         _servers_list_dialog = new ServersListDialog (this);
698                 }
699
700                 _servers_list_dialog->Show ();
701         }
702
703         void tools_manage_templates ()
704         {
705                 if (!_templates_dialog) {
706                         _templates_dialog = new TemplatesDialog (this);
707                 }
708
709                 _templates_dialog->Show ();
710         }
711
712         void tools_check_for_updates ()
713         {
714                 UpdateChecker::instance()->run ();
715                 _update_news_requested = true;
716         }
717
718         void help_about ()
719         {
720                 AboutDialog* d = new AboutDialog (this);
721                 d->ShowModal ();
722                 d->Destroy ();
723         }
724
725         void help_report_a_problem ()
726         {
727                 ReportProblemDialog* d = new ReportProblemDialog (this, _film);
728                 if (d->ShowModal () == wxID_OK) {
729                         d->report ();
730                 }
731                 d->Destroy ();
732         }
733
734         bool should_close ()
735         {
736                 if (!JobManager::instance()->work_to_do ()) {
737                         return true;
738                 }
739
740                 wxMessageDialog* d = new wxMessageDialog (
741                         0,
742                         _("There are unfinished jobs; are you sure you want to quit?"),
743                         _("Unfinished jobs"),
744                         wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION
745                         );
746
747                 bool const r = d->ShowModal() == wxID_YES;
748                 d->Destroy ();
749                 return r;
750         }
751
752         void close (wxCloseEvent& ev)
753         {
754                 if (!should_close ()) {
755                         ev.Veto ();
756                         return;
757                 }
758
759                 if (_film && _film->dirty ()) {
760
761                         FilmChangedDialog* dialog = new FilmChangedDialog (_film->name ());
762                         int const r = dialog->run ();
763                         delete dialog;
764
765                         switch (r) {
766                         case wxID_NO:
767                                 /* Don't save and carry on to close */
768                                 break;
769                         case wxID_YES:
770                                 /* Save and carry on to close */
771                                 _film->write_metadata ();
772                                 break;
773                         case wxID_CANCEL:
774                                 /* Veto the event and stop */
775                                 ev.Veto ();
776                                 return;
777                         }
778                 }
779
780                 /* We don't want to hear about any more configuration changes, since they
781                    cause the File menu to be altered, which itself will be deleted around
782                    now (without, as far as I can see, any way for us to find out).
783                 */
784                 _config_changed_connection.disconnect ();
785
786                 ev.Skip ();
787         }
788
789         void set_menu_sensitivity ()
790         {
791                 list<shared_ptr<Job> > jobs = JobManager::instance()->get ();
792                 list<shared_ptr<Job> >::iterator i = jobs.begin();
793                 while (i != jobs.end() && (*i)->json_name() != "transcode") {
794                         ++i;
795                 }
796                 bool const dcp_creation = (i != jobs.end ()) && !(*i)->finished ();
797                 bool const have_cpl = _film && !_film->cpls().empty ();
798                 bool const have_selected_video_content = !_film_editor->content_panel()->selected_video().empty();
799
800                 for (map<wxMenuItem*, int>::iterator j = menu_items.begin(); j != menu_items.end(); ++j) {
801
802                         bool enabled = true;
803
804                         if ((j->second & NEEDS_FILM) && !_film) {
805                                 enabled = false;
806                         }
807
808                         if ((j->second & NOT_DURING_DCP_CREATION) && dcp_creation) {
809                                 enabled = false;
810                         }
811
812                         if ((j->second & NEEDS_CPL) && !have_cpl) {
813                                 enabled = false;
814                         }
815
816                         if ((j->second & NEEDS_SELECTED_VIDEO_CONTENT) && !have_selected_video_content) {
817                                 enabled = false;
818                         }
819
820                         j->first->Enable (enabled);
821                 }
822         }
823
824         /** @return true if the operation that called this method
825          *  should continue, false to abort it.
826          */
827         bool maybe_save_then_delete_film ()
828         {
829                 if (!_film) {
830                         return true;
831                 }
832
833                 if (_film->dirty ()) {
834                         FilmChangedDialog d (_film->name ());
835                         switch (d.run ()) {
836                         case wxID_NO:
837                                 break;
838                         case wxID_YES:
839                                 _film->write_metadata ();
840                                 break;
841                         case wxID_CANCEL:
842                                 return false;
843                         }
844                 }
845
846                 _film.reset ();
847                 return true;
848         }
849
850         void add_item (wxMenu* menu, wxString text, int id, int sens)
851         {
852                 wxMenuItem* item = menu->Append (id, text);
853                 menu_items.insert (make_pair (item, sens));
854         }
855
856         void setup_menu (wxMenuBar* m)
857         {
858                 _file_menu = new wxMenu;
859                 add_item (_file_menu, _("New...\tCtrl-N"), ID_file_new, ALWAYS);
860                 add_item (_file_menu, _("&Open...\tCtrl-O"), ID_file_open, ALWAYS);
861                 _file_menu->AppendSeparator ();
862                 add_item (_file_menu, _("&Save\tCtrl-S"), ID_file_save, NEEDS_FILM);
863                 _file_menu->AppendSeparator ();
864                 add_item (_file_menu, _("Save as &template..."), ID_file_save_as_template, NEEDS_FILM);
865
866                 _history_position = _file_menu->GetMenuItems().GetCount();
867
868 #ifndef __WXOSX__
869                 _file_menu->AppendSeparator ();
870 #endif
871
872 #ifdef __WXOSX__
873                 add_item (_file_menu, _("&Exit"), wxID_EXIT, ALWAYS);
874 #else
875                 add_item (_file_menu, _("&Quit"), wxID_EXIT, ALWAYS);
876 #endif
877
878 #ifdef __WXOSX__
879                 add_item (_file_menu, _("&Preferences...\tCtrl-P"), wxID_PREFERENCES, ALWAYS);
880 #else
881                 wxMenu* edit = new wxMenu;
882                 add_item (edit, _("&Preferences...\tCtrl-P"), wxID_PREFERENCES, ALWAYS);
883 #endif
884
885                 wxMenu* content = new wxMenu;
886                 add_item (content, _("Scale to fit &width"), ID_content_scale_to_fit_width, NEEDS_FILM | NEEDS_SELECTED_VIDEO_CONTENT);
887                 add_item (content, _("Scale to fit &height"), ID_content_scale_to_fit_height, NEEDS_FILM | NEEDS_SELECTED_VIDEO_CONTENT);
888
889                 wxMenu* jobs_menu = new wxMenu;
890                 add_item (jobs_menu, _("&Make DCP\tCtrl-M"), ID_jobs_make_dcp, NEEDS_FILM | NOT_DURING_DCP_CREATION);
891                 add_item (jobs_menu, _("Make DCP in &batch converter\tCtrl-B"), ID_jobs_make_dcp_batch, NEEDS_FILM | NOT_DURING_DCP_CREATION);
892                 add_item (jobs_menu, _("Make &KDMs...\tCtrl-K"), ID_jobs_make_kdms, NEEDS_FILM);
893                 add_item (jobs_menu, _("Make DKDM for DCP-o-matic..."), ID_jobs_make_self_dkdm, NEEDS_FILM);
894                 add_item (jobs_menu, _("&Send DCP to TMS"), ID_jobs_send_dcp_to_tms, NEEDS_FILM | NOT_DURING_DCP_CREATION | NEEDS_CPL);
895                 add_item (jobs_menu, _("S&how DCP"), ID_jobs_show_dcp, NEEDS_FILM | NOT_DURING_DCP_CREATION | NEEDS_CPL);
896
897                 wxMenu* tools = new wxMenu;
898                 add_item (tools, _("Video waveform..."), ID_tools_video_waveform, NEEDS_FILM);
899                 add_item (tools, _("Hints..."), ID_tools_hints, 0);
900                 add_item (tools, _("Encoding servers..."), ID_tools_encoding_servers, 0);
901                 add_item (tools, _("Manage templates..."), ID_tools_manage_templates, 0);
902                 add_item (tools, _("Check for updates"), ID_tools_check_for_updates, 0);
903                 tools->AppendSeparator ();
904                 add_item (tools, _("Restore default preferences"), ID_tools_restore_default_preferences, ALWAYS);
905
906                 wxMenu* help = new wxMenu;
907 #ifdef __WXOSX__
908                 add_item (help, _("About DCP-o-matic"), wxID_ABOUT, ALWAYS);
909 #else
910                 add_item (help, _("About"), wxID_ABOUT, ALWAYS);
911 #endif
912                 add_item (help, _("Report a problem..."), ID_help_report_a_problem, NEEDS_FILM);
913
914                 m->Append (_file_menu, _("&File"));
915 #ifndef __WXOSX__
916                 m->Append (edit, _("&Edit"));
917 #endif
918                 m->Append (content, _("&Content"));
919                 m->Append (jobs_menu, _("&Jobs"));
920                 m->Append (tools, _("&Tools"));
921                 m->Append (help, _("&Help"));
922         }
923
924         void config_changed ()
925         {
926                 for (int i = 0; i < _history_items; ++i) {
927                         delete _file_menu->Remove (ID_file_history + i);
928                 }
929
930                 if (_history_separator) {
931                         _file_menu->Remove (_history_separator);
932                 }
933                 delete _history_separator;
934                 _history_separator = 0;
935
936                 int pos = _history_position;
937
938                 vector<boost::filesystem::path> history = Config::instance()->history ();
939
940                 if (!history.empty ()) {
941                         _history_separator = _file_menu->InsertSeparator (pos++);
942                 }
943
944                 for (size_t i = 0; i < history.size(); ++i) {
945                         string s;
946                         if (i < 9) {
947                                 s = String::compose ("&%1 %2", i + 1, history[i].string());
948                         } else {
949                                 s = history[i].string();
950                         }
951                         _file_menu->Insert (pos++, ID_file_history + i, std_to_wx (s));
952                 }
953
954                 _history_items = history.size ();
955         }
956
957         void update_checker_state_changed ()
958         {
959                 UpdateChecker* uc = UpdateChecker::instance ();
960
961                 bool const announce =
962                         _update_news_requested ||
963                         (uc->stable() && Config::instance()->check_for_updates()) ||
964                         (uc->test() && Config::instance()->check_for_updates() && Config::instance()->check_for_test_updates());
965
966                 _update_news_requested = false;
967
968                 if (!announce) {
969                         return;
970                 }
971
972                 if (uc->state() == UpdateChecker::YES) {
973                         UpdateDialog* dialog = new UpdateDialog (this, uc->stable (), uc->test ());
974                         dialog->ShowModal ();
975                         dialog->Destroy ();
976                 } else if (uc->state() == UpdateChecker::FAILED) {
977                         error_dialog (this, _("The DCP-o-matic download server could not be contacted."));
978                 } else {
979                         error_dialog (this, _("There are no new versions of DCP-o-matic available."));
980                 }
981
982                 _update_news_requested = false;
983         }
984
985         FilmEditor* _film_editor;
986         FilmViewer* _film_viewer;
987         VideoWaveformDialog* _video_waveform_dialog;
988         HintsDialog* _hints_dialog;
989         ServersListDialog* _servers_list_dialog;
990         wxPreferencesEditor* _config_dialog;
991         KDMDialog* _kdm_dialog;
992         TemplatesDialog* _templates_dialog;
993         wxMenu* _file_menu;
994         shared_ptr<Film> _film;
995         int _history_items;
996         int _history_position;
997         wxMenuItem* _history_separator;
998         boost::signals2::scoped_connection _config_changed_connection;
999         bool _update_news_requested;
1000 };
1001
1002 static const wxCmdLineEntryDesc command_line_description[] = {
1003         { wxCMD_LINE_SWITCH, "n", "new", "create new film", wxCMD_LINE_VAL_NONE, wxCMD_LINE_PARAM_OPTIONAL },
1004         { wxCMD_LINE_OPTION, "c", "content", "add content file", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
1005         { wxCMD_LINE_PARAM, 0, 0, "film to load or create", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
1006         { wxCMD_LINE_NONE, "", "", "", wxCmdLineParamType (0), 0 }
1007 };
1008
1009 /** @class App
1010  *  @brief The magic App class for wxWidgets.
1011  */
1012 class App : public wxApp
1013 {
1014 public:
1015         App ()
1016                 : wxApp ()
1017                 , _frame (0)
1018         {}
1019
1020 private:
1021
1022         bool OnInit ()
1023         try
1024         {
1025                 wxInitAllImageHandlers ();
1026
1027                 Config::FailedToLoad.connect (boost::bind (&App::config_failed_to_load, this));
1028
1029                 wxSplashScreen* splash = 0;
1030                 try {
1031                         if (!Config::have_existing ("config.xml")) {
1032                                 wxBitmap bitmap;
1033                                 boost::filesystem::path p = shared_path () / "splash.png";
1034                                 if (bitmap.LoadFile (std_to_wx (p.string ()), wxBITMAP_TYPE_PNG)) {
1035                                         splash = new wxSplashScreen (bitmap, wxSPLASH_CENTRE_ON_SCREEN | wxSPLASH_NO_TIMEOUT, 0, 0, -1);
1036                                         wxYield ();
1037                                 }
1038                         }
1039                 } catch (boost::filesystem::filesystem_error& e) {
1040                         /* Maybe we couldn't find the splash image; never mind */
1041                 }
1042
1043                 SetAppName (_("DCP-o-matic"));
1044
1045                 if (!wxApp::OnInit()) {
1046                         return false;
1047                 }
1048
1049 #ifdef DCPOMATIC_LINUX
1050                 unsetenv ("UBUNTU_MENUPROXY");
1051 #endif
1052
1053 #ifdef __WXOSX__
1054                 ProcessSerialNumber serial;
1055                 GetCurrentProcess (&serial);
1056                 TransformProcessType (&serial, kProcessTransformToForegroundApplication);
1057 #endif
1058
1059                 dcpomatic_setup_path_encoding ();
1060
1061                 /* Enable i18n; this will create a Config object
1062                    to look for a force-configured language.  This Config
1063                    object will be wrong, however, because dcpomatic_setup
1064                    hasn't yet been called and there aren't any filters etc.
1065                    set up yet.
1066                 */
1067                 dcpomatic_setup_i18n ();
1068
1069                 /* Set things up, including filters etc.
1070                    which will now be internationalised correctly.
1071                 */
1072                 dcpomatic_setup ();
1073
1074                 /* Force the configuration to be re-loaded correctly next
1075                    time it is needed.
1076                 */
1077                 Config::drop ();
1078
1079                 _frame = new DOMFrame (_("DCP-o-matic"));
1080                 SetTopWindow (_frame);
1081                 _frame->Maximize ();
1082                 if (splash) {
1083                         splash->Destroy ();
1084                 }
1085                 _frame->Show ();
1086
1087                 if (!_film_to_load.empty() && boost::filesystem::is_directory (_film_to_load)) {
1088                         try {
1089                                 _frame->load_film (_film_to_load);
1090                         } catch (exception& e) {
1091                                 error_dialog (0, std_to_wx (String::compose (wx_to_std (_("Could not load film %1 (%2)")), _film_to_load, e.what())));
1092                         }
1093                 }
1094
1095                 if (!_film_to_create.empty ()) {
1096                         _frame->new_film (_film_to_create, optional<string> ());
1097                         if (!_content_to_add.empty ()) {
1098                                 BOOST_FOREACH (shared_ptr<Content> i, content_factory (_frame->film(), _content_to_add)) {
1099                                         _frame->film()->examine_and_add_content (i);
1100                                 }
1101                         }
1102                 }
1103
1104                 signal_manager = new wxSignalManager (this);
1105                 Bind (wxEVT_IDLE, boost::bind (&App::idle, this));
1106
1107                 Bind (wxEVT_TIMER, boost::bind (&App::check, this));
1108                 _timer.reset (new wxTimer (this));
1109                 _timer->Start (1000);
1110
1111                 if (Config::instance()->check_for_updates ()) {
1112                         UpdateChecker::instance()->run ();
1113                 }
1114
1115                 return true;
1116         }
1117         catch (exception& e)
1118         {
1119                 error_dialog (0, wxString::Format ("DCP-o-matic could not start: %s", e.what ()));
1120                 return true;
1121         }
1122
1123         void OnInitCmdLine (wxCmdLineParser& parser)
1124         {
1125                 parser.SetDesc (command_line_description);
1126                 parser.SetSwitchChars (wxT ("-"));
1127         }
1128
1129         bool OnCmdLineParsed (wxCmdLineParser& parser)
1130         {
1131                 if (parser.GetParamCount() > 0) {
1132                         if (parser.Found (wxT ("new"))) {
1133                                 _film_to_create = wx_to_std (parser.GetParam (0));
1134                         } else {
1135                                 _film_to_load = wx_to_std (parser.GetParam (0));
1136                         }
1137                 }
1138
1139                 wxString content;
1140                 if (parser.Found (wxT ("content"), &content)) {
1141                         _content_to_add = wx_to_std (content);
1142                 }
1143
1144                 return true;
1145         }
1146
1147         void report_exception ()
1148         {
1149                 try {
1150                         throw;
1151                 } catch (FileError& e) {
1152                         error_dialog (
1153                                 0,
1154                                 wxString::Format (
1155                                         _("An exception occurred: %s (%s)\n\n") + REPORT_PROBLEM,
1156                                         std_to_wx (e.what()),
1157                                         std_to_wx (e.file().string().c_str ())
1158                                         )
1159                                 );
1160                 } catch (exception& e) {
1161                         error_dialog (
1162                                 0,
1163                                 wxString::Format (
1164                                         _("An exception occurred: %s.\n\n") + REPORT_PROBLEM,
1165                                         std_to_wx (e.what ())
1166                                         )
1167                                 );
1168                 } catch (...) {
1169                         error_dialog (0, _("An unknown exception occurred.") + "  " + REPORT_PROBLEM);
1170                 }
1171         }
1172
1173         /* An unhandled exception has occurred inside the main event loop */
1174         bool OnExceptionInMainLoop ()
1175         {
1176                 report_exception ();
1177                 /* This will terminate the program */
1178                 return false;
1179         }
1180
1181         void OnUnhandledException ()
1182         {
1183                 report_exception ();
1184         }
1185
1186         void idle ()
1187         {
1188                 signal_manager->ui_idle ();
1189         }
1190
1191         void check ()
1192         {
1193                 try {
1194                         EncodeServerFinder::instance()->rethrow ();
1195                 } catch (exception& e) {
1196                         error_dialog (0, std_to_wx (e.what ()));
1197                 }
1198         }
1199
1200         void config_failed_to_load ()
1201         {
1202                 message_dialog (_frame, _("The existing configuration failed to load.  Default values will be used instead.  These may take a short time to create."));
1203         }
1204
1205         DOMFrame* _frame;
1206         shared_ptr<wxTimer> _timer;
1207         string _film_to_load;
1208         string _film_to_create;
1209         string _content_to_add;
1210 };
1211
1212 IMPLEMENT_APP (App)