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