OS X build fix.
[dcpomatic.git] / src / tools / dcpomatic_player.cc
1 /*
2     Copyright (C) 2017 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21 #include "lib/cross.h"
22 #include "lib/config.h"
23 #include "lib/util.h"
24 #include "lib/update_checker.h"
25 #include "lib/compose.hpp"
26 #include "lib/encode_server_finder.h"
27 #include "lib/dcp_content.h"
28 #include "lib/job_manager.h"
29 #include "lib/job.h"
30 #include "lib/video_content.h"
31 #include "wx/wx_signal_manager.h"
32 #include "wx/wx_util.h"
33 #include "wx/about_dialog.h"
34 #include "wx/report_problem_dialog.h"
35 #include "wx/film_viewer.h"
36 #include "wx/player_information.h"
37 #include "wx/update_dialog.h"
38 #include "wx/player_config_dialog.h"
39 #include <wx/wx.h>
40 #include <wx/stdpaths.h>
41 #include <wx/splash.h>
42 #include <wx/cmdline.h>
43 #include <wx/preferences.h>
44 #ifdef __WXOSX__
45 #include <ApplicationServices/ApplicationServices.h>
46 #endif
47 #include <boost/bind.hpp>
48 #include <iostream>
49
50 #ifdef check
51 #undef check
52 #endif
53
54 using std::string;
55 using std::cout;
56 using std::exception;
57 using boost::shared_ptr;
58 using boost::optional;
59
60 enum {
61         ID_file_open = 1,
62         ID_view_scale_appropriate,
63         ID_view_scale_full,
64         ID_view_scale_half,
65         ID_view_scale_quarter,
66         ID_help_report_a_problem,
67         ID_tools_check_for_updates,
68 };
69
70 class DOMFrame : public wxFrame
71 {
72 public:
73         DOMFrame ()
74                 : wxFrame (0, -1, _("DCP-o-matic Player"))
75                 , _update_news_requested (false)
76                 , _info (0)
77                 , _config_dialog (0)
78                 , _viewer (0)
79         {
80
81 #if defined(DCPOMATIC_WINDOWS)
82                 maybe_open_console ();
83                 cout << "DCP-o-matic Player is starting." << "\n";
84 #endif
85
86                 wxMenuBar* bar = new wxMenuBar;
87                 setup_menu (bar);
88                 SetMenuBar (bar);
89
90 #ifdef DCPOMATIC_WINDOWS
91                 SetIcon (wxIcon (std_to_wx ("id")));
92 #endif
93
94                 _config_changed_connection = Config::instance()->Changed.connect (boost::bind (&DOMFrame::config_changed, this));
95
96                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_open, this), ID_file_open);
97                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_exit, this), wxID_EXIT);
98                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::edit_preferences, this), wxID_PREFERENCES);
99                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::set_decode_reduction, this, optional<int>()), ID_view_scale_appropriate);
100                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::set_decode_reduction, this, optional<int>(0)), ID_view_scale_full);
101                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::set_decode_reduction, this, optional<int>(1)), ID_view_scale_half);
102                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::set_decode_reduction, this, optional<int>(2)), ID_view_scale_quarter);
103                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::help_about, this), wxID_ABOUT);
104                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::help_report_a_problem, this), ID_help_report_a_problem);
105                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_check_for_updates, this), ID_tools_check_for_updates);
106
107                 /* Use a panel as the only child of the Frame so that we avoid
108                    the dark-grey background on Windows.
109                 */
110                 wxPanel* overall_panel = new wxPanel (this, wxID_ANY);
111
112                 _viewer = new FilmViewer (overall_panel, false, false);
113                 _info = new PlayerInformation (overall_panel, _viewer);
114                 wxSizer* main_sizer = new wxBoxSizer (wxVERTICAL);
115                 main_sizer->Add (_viewer, 1, wxEXPAND | wxALL, 6);
116                 main_sizer->Add (_info, 0, wxEXPAND | wxALL, 6);
117                 overall_panel->SetSizer (main_sizer);
118
119                 UpdateChecker::instance()->StateChanged.connect (boost::bind (&DOMFrame::update_checker_state_changed, this));
120         }
121
122         void set_decode_reduction (optional<int> reduction)
123         {
124                 _viewer->set_dcp_decode_reduction (reduction);
125         }
126
127         void load_dcp (boost::filesystem::path dir)
128         {
129                 _film.reset (new Film (optional<boost::filesystem::path>()));
130                 shared_ptr<DCPContent> dcp (new DCPContent (_film, dir));
131                 _film->examine_and_add_content (dcp);
132
133                 JobManager* jm = JobManager::instance ();
134                 while (jm->work_to_do ()) {
135                         /* XXX: progress dialog */
136                         while (signal_manager->ui_idle ()) {}
137                         dcpomatic_sleep (1);
138                 }
139
140                 while (signal_manager->ui_idle ()) {}
141
142                 if (jm->errors ()) {
143                         wxString errors;
144                         BOOST_FOREACH (shared_ptr<Job> i, jm->get()) {
145                                 if (i->finished_in_error()) {
146                                         errors += std_to_wx (i->error_summary()) + "\n";
147                                 }
148                         }
149                         error_dialog (this, errors);
150                         return;
151                 }
152
153                 _viewer->set_film (_film);
154                 _info->triggered_update ();
155         }
156
157 private:
158
159         void setup_menu (wxMenuBar* m)
160         {
161                 wxMenu* file = new wxMenu;
162                 file->Append (ID_file_open, _("&Open...\tCtrl-O"));
163
164 #ifdef __WXOSX__
165                 file->Append (wxID_EXIT, _("&Exit"));
166 #else
167                 file->Append (wxID_EXIT, _("&Quit"));
168 #endif
169
170 #ifdef __WXOSX__
171                 file->Append (wxID_PREFERENCES, _("&Preferences...\tCtrl-P"));
172 #else
173                 wxMenu* edit = new wxMenu;
174                 edit->Append (wxID_PREFERENCES, _("&Preferences...\tCtrl-P"));
175 #endif
176
177                 wxMenu* view = new wxMenu;
178                 view->AppendRadioItem (ID_view_scale_appropriate, _("Set decode resolution to match display"));
179                 view->AppendRadioItem (ID_view_scale_full, _("Decode at full resolution"));
180                 view->AppendRadioItem (ID_view_scale_half, _("Decode at half resolution"));
181                 view->AppendRadioItem (ID_view_scale_quarter, _("Decode at quarter resolution"));
182
183                 wxMenu* tools = new wxMenu;
184                 tools->Append (ID_tools_check_for_updates, _("Check for updates"));
185
186                 wxMenu* help = new wxMenu;
187 #ifdef __WXOSX__
188                 help->Append (wxID_ABOUT, _("About DCP-o-matic"));
189 #else
190                 help->Append (wxID_ABOUT, _("About"));
191 #endif
192                 help->Append (ID_help_report_a_problem, _("Report a problem..."));
193
194                 m->Append (file, _("&File"));
195 #ifndef __WXOSX__
196                 m->Append (edit, _("&Edit"));
197 #endif
198                 m->Append (view, _("&View"));
199                 m->Append (tools, _("&Tools"));
200                 m->Append (help, _("&Help"));
201         }
202
203         void file_open ()
204         {
205                 wxDirDialog* c = new wxDirDialog (
206                         this,
207                         _("Select DCP to open"),
208                         wxStandardPaths::Get().GetDocumentsDir(),
209                         wxDEFAULT_DIALOG_STYLE | wxDD_DIR_MUST_EXIST
210                         );
211
212                 int r;
213                 while (true) {
214                         r = c->ShowModal ();
215                         if (r == wxID_OK && c->GetPath() == wxStandardPaths::Get().GetDocumentsDir()) {
216                                 error_dialog (this, _("You did not select a folder.  Make sure that you select a folder before clicking Open."));
217                         } else {
218                                 break;
219                         }
220                 }
221
222                 if (r == wxID_OK) {
223                         load_dcp (wx_to_std (c->GetPath ()));
224                 }
225
226                 c->Destroy ();
227         }
228
229         void file_exit ()
230         {
231                 Close ();
232         }
233
234         void edit_preferences ()
235         {
236                 if (!_config_dialog) {
237                         _config_dialog = create_player_config_dialog ();
238                 }
239                 _config_dialog->Show (this);
240         }
241
242         void tools_check_for_updates ()
243         {
244                 UpdateChecker::instance()->run ();
245                 _update_news_requested = true;
246         }
247
248         void help_about ()
249         {
250                 AboutDialog* d = new AboutDialog (this);
251                 d->ShowModal ();
252                 d->Destroy ();
253         }
254
255         void help_report_a_problem ()
256         {
257                 ReportProblemDialog* d = new ReportProblemDialog (this);
258                 if (d->ShowModal () == wxID_OK) {
259                         d->report ();
260                 }
261                 d->Destroy ();
262         }
263
264         void update_checker_state_changed ()
265         {
266                 UpdateChecker* uc = UpdateChecker::instance ();
267
268                 bool const announce =
269                         _update_news_requested ||
270                         (uc->stable() && Config::instance()->check_for_updates()) ||
271                         (uc->test() && Config::instance()->check_for_updates() && Config::instance()->check_for_test_updates());
272
273                 _update_news_requested = false;
274
275                 if (!announce) {
276                         return;
277                 }
278
279                 if (uc->state() == UpdateChecker::YES) {
280                         UpdateDialog* dialog = new UpdateDialog (this, uc->stable (), uc->test ());
281                         dialog->ShowModal ();
282                         dialog->Destroy ();
283                 } else if (uc->state() == UpdateChecker::FAILED) {
284                         error_dialog (this, _("The DCP-o-matic download server could not be contacted."));
285                 } else {
286                         error_dialog (this, _("There are no new versions of DCP-o-matic available."));
287                 }
288
289                 _update_news_requested = false;
290         }
291
292         void config_changed ()
293         {
294                 /* Instantly save any config changes when using the player GUI */
295                 try {
296                         Config::instance()->write_config();
297                 } catch (exception& e) {
298                         error_dialog (
299                                 this,
300                                 wxString::Format (
301                                         _("Could not write to config file at %s.  Your changes have not been saved."),
302                                         std_to_wx (Config::instance()->cinemas_file().string()).data()
303                                         )
304                                 );
305                 }
306         }
307
308         bool _update_news_requested;
309         PlayerInformation* _info;
310         wxPreferencesEditor* _config_dialog;
311         FilmViewer* _viewer;
312         boost::shared_ptr<Film> _film;
313         boost::signals2::scoped_connection _config_changed_connection;
314 };
315
316 static const wxCmdLineEntryDesc command_line_description[] = {
317         { wxCMD_LINE_PARAM, 0, 0, "DCP to load or create", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
318         { wxCMD_LINE_NONE, "", "", "", wxCmdLineParamType (0), 0 }
319 };
320
321 /** @class App
322  *  @brief The magic App class for wxWidgets.
323  */
324 class App : public wxApp
325 {
326 public:
327         App ()
328                 : wxApp ()
329                 , _frame (0)
330         {}
331
332 private:
333
334         bool OnInit ()
335         try
336         {
337                 wxInitAllImageHandlers ();
338
339                 Config::FailedToLoad.connect (boost::bind (&App::config_failed_to_load, this));
340
341                 wxSplashScreen* splash = 0;
342                 try {
343                         if (!Config::have_existing ("config.xml")) {
344                                 wxBitmap bitmap;
345                                 boost::filesystem::path p = shared_path () / "splash.png";
346                                 if (bitmap.LoadFile (std_to_wx (p.string ()), wxBITMAP_TYPE_PNG)) {
347                                         splash = new wxSplashScreen (bitmap, wxSPLASH_CENTRE_ON_SCREEN | wxSPLASH_NO_TIMEOUT, 0, 0, -1);
348                                         wxYield ();
349                                 }
350                         }
351                 } catch (boost::filesystem::filesystem_error& e) {
352                         /* Maybe we couldn't find the splash image; never mind */
353                 }
354
355                 SetAppName (_("DCP-o-matic Player"));
356
357                 if (!wxApp::OnInit()) {
358                         return false;
359                 }
360
361 #ifdef DCPOMATIC_LINUX
362                 unsetenv ("UBUNTU_MENUPROXY");
363 #endif
364
365 #ifdef __WXOSX__
366                 ProcessSerialNumber serial;
367                 GetCurrentProcess (&serial);
368                 TransformProcessType (&serial, kProcessTransformToForegroundApplication);
369 #endif
370
371                 dcpomatic_setup_path_encoding ();
372
373                 /* Enable i18n; this will create a Config object
374                    to look for a force-configured language.  This Config
375                    object will be wrong, however, because dcpomatic_setup
376                    hasn't yet been called and there aren't any filters etc.
377                    set up yet.
378                 */
379                 dcpomatic_setup_i18n ();
380
381                 /* Set things up, including filters etc.
382                    which will now be internationalised correctly.
383                 */
384                 dcpomatic_setup ();
385
386                 /* Force the configuration to be re-loaded correctly next
387                    time it is needed.
388                 */
389                 Config::drop ();
390
391                 _frame = new DOMFrame ();
392                 SetTopWindow (_frame);
393                 _frame->Maximize ();
394                 if (splash) {
395                         splash->Destroy ();
396                 }
397                 _frame->Show ();
398
399                 signal_manager = new wxSignalManager (this);
400
401                 if (!_dcp_to_load.empty() && boost::filesystem::is_directory (_dcp_to_load)) {
402                         try {
403                                 _frame->load_dcp (_dcp_to_load);
404                         } catch (exception& e) {
405                                 error_dialog (0, std_to_wx (String::compose (wx_to_std (_("Could not load DCP %1 (%2)")), _dcp_to_load, e.what())));
406                         }
407                 }
408
409                 Bind (wxEVT_IDLE, boost::bind (&App::idle, this));
410
411                 Bind (wxEVT_TIMER, boost::bind (&App::check, this));
412                 _timer.reset (new wxTimer (this));
413                 _timer->Start (1000);
414
415                 if (Config::instance()->check_for_updates ()) {
416                         UpdateChecker::instance()->run ();
417                 }
418
419                 return true;
420         }
421         catch (exception& e)
422         {
423                 error_dialog (0, wxString::Format ("DCP-o-matic Player could not start: %s", e.what ()));
424                 return true;
425         }
426
427         void OnInitCmdLine (wxCmdLineParser& parser)
428         {
429                 parser.SetDesc (command_line_description);
430                 parser.SetSwitchChars (wxT ("-"));
431         }
432
433         bool OnCmdLineParsed (wxCmdLineParser& parser)
434         {
435                 if (parser.GetParamCount() > 0) {
436                         _dcp_to_load = wx_to_std (parser.GetParam (0));
437                 }
438
439                 return true;
440         }
441
442         void report_exception ()
443         {
444                 try {
445                         throw;
446                 } catch (FileError& e) {
447                         error_dialog (
448                                 0,
449                                 wxString::Format (
450                                         _("An exception occurred: %s (%s)\n\n") + REPORT_PROBLEM,
451                                         std_to_wx (e.what()),
452                                         std_to_wx (e.file().string().c_str ())
453                                         )
454                                 );
455                 } catch (exception& e) {
456                         error_dialog (
457                                 0,
458                                 wxString::Format (
459                                         _("An exception occurred: %s.\n\n") + REPORT_PROBLEM,
460                                         std_to_wx (e.what ())
461                                         )
462                                 );
463                 } catch (...) {
464                         error_dialog (0, _("An unknown exception occurred.") + "  " + REPORT_PROBLEM);
465                 }
466         }
467
468         /* An unhandled exception has occurred inside the main event loop */
469         bool OnExceptionInMainLoop ()
470         {
471                 report_exception ();
472                 /* This will terminate the program */
473                 return false;
474         }
475
476         void OnUnhandledException ()
477         {
478                 report_exception ();
479         }
480
481         void idle ()
482         {
483                 signal_manager->ui_idle ();
484         }
485
486         void check ()
487         {
488                 try {
489                         EncodeServerFinder::instance()->rethrow ();
490                 } catch (exception& e) {
491                         error_dialog (0, std_to_wx (e.what ()));
492                 }
493         }
494
495         void config_failed_to_load ()
496         {
497                 message_dialog (_frame, _("The existing configuration failed to load.  Default values will be used instead.  These may take a short time to create."));
498         }
499
500         DOMFrame* _frame;
501         shared_ptr<wxTimer> _timer;
502         string _dcp_to_load;
503 };
504
505 IMPLEMENT_APP (App)