Fix crash with a non-existent playlist directory.
[dcpomatic.git] / src / tools / dcpomatic_playlist.cc
1 /*
2     Copyright (C) 2018-2021 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21
22 #include "wx/about_dialog.h"
23 #include "wx/content_view.h"
24 #include "wx/dcpomatic_button.h"
25 #include "wx/playlist_editor_config_dialog.h"
26 #include "wx/wx_signal_manager.h"
27 #include "wx/wx_util.h"
28 #include "lib/config.h"
29 #include "lib/constants.h"
30 #include "lib/cross.h"
31 #include "lib/dcp_content.h"
32 #include "lib/film.h"
33 #include "lib/spl.h"
34 #include "lib/spl_entry.h"
35 #include <dcp/filesystem.h>
36 #include <dcp/warnings.h>
37 LIBDCP_DISABLE_WARNINGS
38 #include <wx/imaglist.h>
39 #include <wx/listctrl.h>
40 #include <wx/preferences.h>
41 #include <wx/spinctrl.h>
42 #include <wx/wx.h>
43 LIBDCP_ENABLE_WARNINGS
44
45
46 using std::cout;
47 using std::exception;
48 using std::make_pair;
49 using std::make_shared;
50 using std::map;
51 using std::shared_ptr;
52 using std::string;
53 using std::vector;
54 using std::weak_ptr;
55 using boost::bind;
56 using boost::optional;
57 using std::dynamic_pointer_cast;
58 #if BOOST_VERSION >= 106100
59 using namespace boost::placeholders;
60 #endif
61
62
63 static
64 void
65 save_playlist(shared_ptr<const SPL> playlist)
66 {
67         if (auto dir = Config::instance()->player_playlist_directory()) {
68                 playlist->write(*dir / (playlist->id() + ".xml"));
69         }
70 }
71
72
73 class ContentDialog : public wxDialog, public ContentStore
74 {
75 public:
76         ContentDialog (wxWindow* parent)
77                 : wxDialog (parent, wxID_ANY, _("Add content"), wxDefaultPosition, wxSize(800, 640))
78                 , _content_view (new ContentView(this))
79         {
80                 _content_view->update ();
81
82                 auto overall_sizer = new wxBoxSizer (wxVERTICAL);
83                 SetSizer (overall_sizer);
84
85                 overall_sizer->Add (_content_view, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
86
87                 auto buttons = CreateSeparatedButtonSizer (wxOK | wxCANCEL);
88                 if (buttons) {
89                         overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
90                 }
91
92                 overall_sizer->Layout ();
93
94                 _content_view->Bind(wxEVT_LIST_ITEM_ACTIVATED, boost::bind(&ContentDialog::EndModal, this, wxID_OK));
95                 _config_changed_connection = Config::instance()->Changed.connect(boost::bind(&ContentView::update, _content_view));
96         }
97
98         shared_ptr<Content> selected () const
99         {
100                 return _content_view->selected ();
101         }
102
103         shared_ptr<Content> get (string digest) const override
104         {
105                 return _content_view->get (digest);
106         }
107
108 private:
109         ContentView* _content_view;
110         boost::signals2::scoped_connection _config_changed_connection;
111 };
112
113
114
115 class PlaylistList
116 {
117 public:
118         PlaylistList (wxPanel* parent, ContentStore* content_store)
119                 : _sizer (new wxBoxSizer(wxVERTICAL))
120                 , _content_store (content_store)
121                 , _parent(parent)
122         {
123                 auto label = new wxStaticText (parent, wxID_ANY, wxEmptyString);
124                 label->SetLabelMarkup (_("<b>Playlists</b>"));
125                 _sizer->Add (label, 0, wxTOP | wxLEFT, DCPOMATIC_SIZER_GAP * 2);
126
127                 _list = new wxListCtrl (
128                         parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL
129                         );
130
131                 _list->AppendColumn (_("Name"), wxLIST_FORMAT_LEFT, 840);
132                 _list->AppendColumn (_("Length"), wxLIST_FORMAT_LEFT, 100);
133
134                 auto button_sizer = new wxBoxSizer (wxVERTICAL);
135                 _new = new Button (parent, _("New"));
136                 button_sizer->Add (_new, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
137                 _delete = new Button (parent, _("Delete"));
138                 button_sizer->Add (_delete, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
139
140                 auto list = new wxBoxSizer (wxHORIZONTAL);
141                 list->Add (_list, 1, wxEXPAND | wxALL, DCPOMATIC_SIZER_GAP);
142                 list->Add (button_sizer, 0, wxALL, DCPOMATIC_SIZER_GAP);
143
144                 _sizer->Add (list);
145
146                 load_playlists ();
147
148                 _list->Bind (wxEVT_COMMAND_LIST_ITEM_SELECTED, bind(&PlaylistList::selection_changed, this));
149                 _list->Bind (wxEVT_COMMAND_LIST_ITEM_DESELECTED, bind(&PlaylistList::selection_changed, this));
150                 _new->Bind (wxEVT_BUTTON, bind(&PlaylistList::new_playlist, this));
151                 _delete->Bind (wxEVT_BUTTON, bind(&PlaylistList::delete_playlist, this));
152
153                 setup_sensitivity();
154         }
155
156         wxSizer* sizer ()
157         {
158                 return _sizer;
159         }
160
161         shared_ptr<SignalSPL> first_playlist () const
162         {
163                 if (_playlists.empty()) {
164                         return {};
165                 }
166
167                 return _playlists.front ();
168         }
169
170         boost::signals2::signal<void (shared_ptr<SignalSPL>)> Edit;
171
172 private:
173         void setup_sensitivity()
174         {
175                 _delete->Enable(static_cast<bool>(selected()));
176         }
177
178         void add_playlist_to_view (shared_ptr<const SignalSPL> playlist)
179         {
180                 wxListItem item;
181                 item.SetId (_list->GetItemCount());
182                 long const N = _list->InsertItem (item);
183                 _list->SetItem (N, 0, std_to_wx(playlist->name()));
184         }
185
186         void add_playlist_to_model (shared_ptr<SignalSPL> playlist)
187         {
188                 _playlists.push_back (playlist);
189                 playlist->Changed.connect(bind(&PlaylistList::changed, this, weak_ptr<SignalSPL>(playlist), _1));
190         }
191
192         void changed(weak_ptr<SignalSPL> wp, SignalSPL::Change change)
193         {
194                 auto playlist = wp.lock ();
195                 if (!playlist) {
196                         return;
197                 }
198
199                 switch (change) {
200                 case SignalSPL::Change::NAME:
201                 {
202                         int N = 0;
203                         for (auto i: _playlists) {
204                                 if (i == playlist) {
205                                         _list->SetItem (N, 0, std_to_wx(i->name()));
206                                 }
207                                 ++N;
208                         }
209                         break;
210                 }
211                 case SignalSPL::Change::CONTENT:
212                         save_playlist(playlist);
213                         break;
214                 }
215         }
216
217         void load_playlists ()
218         {
219                 auto path = Config::instance()->player_playlist_directory();
220                 if (!path) {
221                         return;
222                 }
223
224                 _list->DeleteAllItems ();
225                 _playlists.clear ();
226                 try {
227                         for (auto i: dcp::filesystem::directory_iterator(*path)) {
228                                 auto spl = make_shared<SignalSPL>();
229                                 try {
230                                         spl->read (i, _content_store);
231                                         add_playlist_to_model (spl);
232                                 } catch (...) {}
233                         }
234                 } catch (...) {}
235
236                 for (auto i: _playlists) {
237                         add_playlist_to_view (i);
238                 }
239         }
240
241         void new_playlist ()
242         {
243                 auto dir = Config::instance()->player_playlist_directory();
244                 if (!dir) {
245                         error_dialog(_parent, _("No playlist folder is specified in preferences.  Please set one and then try again."));
246                         return;
247                 }
248
249                 shared_ptr<SignalSPL> spl (new SignalSPL(wx_to_std(_("New Playlist"))));
250                 add_playlist_to_model (spl);
251                 add_playlist_to_view (spl);
252                 _list->SetItemState (_list->GetItemCount() - 1, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
253         }
254
255         boost::optional<int> selected() const
256         {
257                 long int selected = _list->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
258                 if (selected < 0 || selected >= int(_playlists.size())) {
259                         return {};
260                 }
261
262                 return selected;
263         }
264
265         void delete_playlist ()
266         {
267                 auto index = selected();
268                 if (!index) {
269                         return;
270                 }
271
272                 auto dir = Config::instance()->player_playlist_directory();
273                 if (!dir) {
274                         return;
275                 }
276
277                 dcp::filesystem::remove(*dir / (_playlists[*index]->id() + ".xml"));
278                 _list->DeleteItem(*index);
279                 _playlists.erase(_playlists.begin() + *index);
280
281                 Edit(shared_ptr<SignalSPL>());
282         }
283
284         void selection_changed ()
285         {
286                 long int selected = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
287                 if (selected < 0 || selected >= int(_playlists.size())) {
288                         Edit (shared_ptr<SignalSPL>());
289                 } else {
290                         Edit (_playlists[selected]);
291                 }
292
293                 setup_sensitivity();
294         }
295
296         wxBoxSizer* _sizer;
297         wxListCtrl* _list;
298         wxButton* _new;
299         wxButton* _delete;
300         vector<shared_ptr<SignalSPL>> _playlists;
301         ContentStore* _content_store;
302         wxWindow* _parent;
303 };
304
305
306 class PlaylistContent
307 {
308 public:
309         PlaylistContent (wxPanel* parent, ContentDialog* content_dialog)
310                 : _content_dialog (content_dialog)
311                 , _sizer (new wxBoxSizer(wxVERTICAL))
312         {
313                 auto title = new wxBoxSizer (wxHORIZONTAL);
314                 auto label = new wxStaticText (parent, wxID_ANY, wxEmptyString);
315                 label->SetLabelMarkup (_("<b>Playlist:</b>"));
316                 title->Add (label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, DCPOMATIC_SIZER_GAP);
317                 _name = new wxTextCtrl (parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(400, -1));
318                 title->Add (_name, 0, wxRIGHT, DCPOMATIC_SIZER_GAP);
319                 _save_name = new Button(parent, _("Save"));
320                 title->Add(_save_name);
321                 _sizer->Add (title, 0, wxTOP | wxLEFT, DCPOMATIC_SIZER_GAP * 2);
322
323                 auto list = new wxBoxSizer (wxHORIZONTAL);
324
325                 _list = new wxListCtrl (
326                         parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL
327                         );
328
329                 _list->AppendColumn (_("Name"), wxLIST_FORMAT_LEFT, 400);
330                 _list->AppendColumn (_("CPL"), wxLIST_FORMAT_LEFT, 350);
331                 _list->AppendColumn (_("Type"), wxLIST_FORMAT_LEFT, 100);
332                 _list->AppendColumn (_("Encrypted"), wxLIST_FORMAT_CENTRE, 90);
333
334                 auto images = new wxImageList (16, 16);
335                 wxIcon tick_icon;
336                 wxIcon no_tick_icon;
337                 tick_icon.LoadFile (bitmap_path("tick.png"), wxBITMAP_TYPE_PNG);
338                 no_tick_icon.LoadFile (bitmap_path("no_tick.png"), wxBITMAP_TYPE_PNG);
339                 images->Add (tick_icon);
340                 images->Add (no_tick_icon);
341
342                 _list->SetImageList (images, wxIMAGE_LIST_SMALL);
343
344                 list->Add (_list, 1, wxEXPAND | wxALL, DCPOMATIC_SIZER_GAP);
345
346                 auto button_sizer = new wxBoxSizer (wxVERTICAL);
347                 _up = new Button (parent, _("Up"));
348                 _down = new Button (parent, _("Down"));
349                 _add = new Button (parent, _("Add"));
350                 _remove = new Button (parent, _("Remove"));
351                 button_sizer->Add (_up, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
352                 button_sizer->Add (_down, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
353                 button_sizer->Add (_add, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
354                 button_sizer->Add (_remove, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
355
356                 list->Add (button_sizer, 0, wxALL, DCPOMATIC_SIZER_GAP);
357
358                 _sizer->Add (list);
359
360                 _list->Bind (wxEVT_COMMAND_LIST_ITEM_SELECTED, bind(&PlaylistContent::setup_sensitivity, this));
361                 _list->Bind (wxEVT_COMMAND_LIST_ITEM_DESELECTED, bind(&PlaylistContent::setup_sensitivity, this));
362                 _name->Bind (wxEVT_TEXT, bind(&PlaylistContent::name_changed, this));
363                 _save_name->bind(&PlaylistContent::save_name_clicked, this);
364                 _up->Bind (wxEVT_BUTTON, bind(&PlaylistContent::up_clicked, this));
365                 _down->Bind (wxEVT_BUTTON, bind(&PlaylistContent::down_clicked, this));
366                 _add->Bind (wxEVT_BUTTON, bind(&PlaylistContent::add_clicked, this));
367                 _remove->Bind (wxEVT_BUTTON, bind(&PlaylistContent::remove_clicked, this));
368
369                 setup_sensitivity();
370         }
371
372         wxSizer* sizer ()
373         {
374                 return _sizer;
375         }
376
377         void set (shared_ptr<SignalSPL> playlist)
378         {
379                 _playlist = playlist;
380                 _list->DeleteAllItems ();
381                 if (_playlist) {
382                         for (auto i: _playlist->get()) {
383                                 add (i);
384                         }
385                         _name->SetValue (std_to_wx(_playlist->name()));
386                 } else {
387                         _name->SetValue (wxT(""));
388                 }
389                 setup_sensitivity ();
390         }
391
392         shared_ptr<SignalSPL> playlist () const
393         {
394                 return _playlist;
395         }
396
397
398 private:
399         void save_name_clicked()
400         {
401                 if (_playlist) {
402                         _playlist->set_name(wx_to_std(_name->GetValue()));
403                         save_playlist(_playlist);
404                 }
405                 setup_sensitivity();
406         }
407
408         void name_changed ()
409         {
410                 setup_sensitivity();
411         }
412
413         void add (SPLEntry e)
414         {
415                 wxListItem item;
416                 item.SetId (_list->GetItemCount());
417                 long const N = _list->InsertItem (item);
418                 set_item (N, e);
419         }
420
421         void set_item (long N, SPLEntry e)
422         {
423                 _list->SetItem (N, 0, std_to_wx(e.name));
424                 _list->SetItem (N, 1, std_to_wx(e.id));
425                 _list->SetItem (N, 2, std_to_wx(e.kind->name()));
426                 _list->SetItem (N, 3, e.encrypted ? S_("Question|Y") : S_("Question|N"));
427         }
428
429         void setup_sensitivity ()
430         {
431                 bool const have_list = static_cast<bool>(_playlist);
432                 int const num_selected = _list->GetSelectedItemCount ();
433                 long int selected = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
434                 _name->Enable (have_list);
435                 _save_name->Enable(_playlist && _playlist->name() != wx_to_std(_name->GetValue()));
436                 _list->Enable (have_list);
437                 _up->Enable (have_list && selected > 0);
438                 _down->Enable (have_list && selected != -1 && selected < (_list->GetItemCount() - 1));
439                 _add->Enable (have_list);
440                 _remove->Enable (have_list && num_selected > 0);
441         }
442
443         void add_clicked ()
444         {
445                 int const r = _content_dialog->ShowModal ();
446                 if (r == wxID_OK) {
447                         auto content = _content_dialog->selected ();
448                         if (content) {
449                                 SPLEntry e (content);
450                                 add (e);
451                                 DCPOMATIC_ASSERT (_playlist);
452                                 _playlist->add (e);
453                         }
454                 }
455         }
456
457         void up_clicked ()
458         {
459                 long int s = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
460                 if (s < 1) {
461                         return;
462                 }
463
464                 DCPOMATIC_ASSERT (_playlist);
465
466                 _playlist->swap(s, s - 1);
467
468                 set_item (s - 1, (*_playlist)[s-1]);
469                 set_item (s, (*_playlist)[s]);
470         }
471
472         void down_clicked ()
473         {
474                 long int s = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
475                 if (s > (_list->GetItemCount() - 1)) {
476                         return;
477                 }
478
479                 DCPOMATIC_ASSERT (_playlist);
480
481                 _playlist->swap(s, s + 1);
482
483                 set_item (s + 1, (*_playlist)[s+1]);
484                 set_item (s, (*_playlist)[s]);
485         }
486
487         void remove_clicked ()
488         {
489                 long int s = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
490                 if (s == -1) {
491                         return;
492                 }
493
494                 DCPOMATIC_ASSERT (_playlist);
495                 _playlist->remove (s);
496                 _list->DeleteItem (s);
497         }
498
499         ContentDialog* _content_dialog;
500         wxBoxSizer* _sizer;
501         wxTextCtrl* _name;
502         Button* _save_name;
503         wxListCtrl* _list;
504         wxButton* _up;
505         wxButton* _down;
506         wxButton* _add;
507         wxButton* _remove;
508         shared_ptr<SignalSPL> _playlist;
509 };
510
511
512 class DOMFrame : public wxFrame
513 {
514 public:
515         explicit DOMFrame (wxString const & title)
516                 : wxFrame (nullptr, wxID_ANY, title)
517                 , _content_dialog (new ContentDialog(this))
518                 , _config_dialog (nullptr)
519         {
520                 auto bar = new wxMenuBar;
521                 setup_menu (bar);
522                 SetMenuBar (bar);
523
524                 /* Use a panel as the only child of the Frame so that we avoid
525                    the dark-grey background on Windows.
526                 */
527                 auto overall_panel = new wxPanel (this, wxID_ANY);
528                 auto sizer = new wxBoxSizer (wxVERTICAL);
529
530                 _playlist_list = new PlaylistList (overall_panel, _content_dialog);
531                 _playlist_content = new PlaylistContent (overall_panel, _content_dialog);
532
533                 sizer->Add (_playlist_list->sizer());
534                 sizer->Add (_playlist_content->sizer());
535
536                 overall_panel->SetSizer (sizer);
537
538                 _playlist_list->Edit.connect (bind(&DOMFrame::change_playlist, this, _1));
539
540                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_exit, this), wxID_EXIT);
541                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::help_about, this), wxID_ABOUT);
542                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::edit_preferences, this), wxID_PREFERENCES);
543
544                 _config_changed_connection = Config::instance()->Changed.connect(boost::bind(&DOMFrame::config_changed, this));
545         }
546
547 private:
548
549         void file_exit ()
550         {
551                 /* false here allows the close handler to veto the close request */
552                 Close (false);
553         }
554
555         void help_about ()
556         {
557                 auto d = make_wx<AboutDialog>(this);
558                 d->ShowModal ();
559         }
560
561         void edit_preferences ()
562         {
563                 if (!_config_dialog) {
564                         _config_dialog = create_playlist_editor_config_dialog ();
565                 }
566                 _config_dialog->Show (this);
567         }
568
569         void change_playlist (shared_ptr<SignalSPL> playlist)
570         {
571                 auto old = _playlist_content->playlist ();
572                 if (old) {
573                         save_playlist (old);
574                 }
575                 _playlist_content->set (playlist);
576         }
577
578         void setup_menu (wxMenuBar* m)
579         {
580                 auto file = new wxMenu;
581 #ifdef __WXOSX__
582                 file->Append (wxID_PREFERENCES, _("&Preferences...\tCtrl-P"));
583                 file->Append (wxID_EXIT, _("&Exit"));
584 #else
585                 file->Append (wxID_EXIT, _("&Quit"));
586 #endif
587
588 #ifndef __WXOSX__
589                 auto edit = new wxMenu;
590                 edit->Append (wxID_PREFERENCES, _("&Preferences...\tCtrl-P"));
591 #endif
592
593                 auto help = new wxMenu;
594 #ifdef __WXOSX__
595                 help->Append (wxID_ABOUT, _("About DCP-o-matic"));
596 #else
597                 help->Append (wxID_ABOUT, _("About"));
598 #endif
599
600                 m->Append (file, _("&File"));
601 #ifndef __WXOSX__
602                 m->Append (edit, _("&Edit"));
603 #endif
604                 m->Append (help, _("&Help"));
605         }
606
607
608         void config_changed ()
609         {
610                 try {
611                         Config::instance()->write_config();
612                 } catch (exception& e) {
613                         error_dialog (
614                                 this,
615                                 wxString::Format (
616                                         _("Could not write to config file at %s.  Your changes have not been saved."),
617                                         std_to_wx (Config::instance()->cinemas_file().string()).data()
618                                         )
619                                 );
620                 }
621         }
622
623         ContentDialog* _content_dialog;
624         PlaylistList* _playlist_list;
625         PlaylistContent* _playlist_content;
626         wxPreferencesEditor* _config_dialog;
627         boost::signals2::scoped_connection _config_changed_connection;
628 };
629
630
631 /** @class App
632  *  @brief The magic App class for wxWidgets.
633  */
634 class App : public wxApp
635 {
636 public:
637         App ()
638                 : wxApp ()
639                 , _frame (nullptr)
640         {}
641
642 private:
643
644         bool OnInit () override
645         try
646         {
647                 wxInitAllImageHandlers ();
648                 SetAppName (_("DCP-o-matic Playlist Editor"));
649
650                 if (!wxApp::OnInit()) {
651                         return false;
652                 }
653
654 #ifdef DCPOMATIC_LINUX
655                 unsetenv ("UBUNTU_MENUPROXY");
656 #endif
657
658 #ifdef DCPOMATIC_OSX
659                 make_foreground_application ();
660 #endif
661
662                 dcpomatic_setup_path_encoding ();
663
664                 /* Enable i18n; this will create a Config object
665                    to look for a force-configured language.  This Config
666                    object will be wrong, however, because dcpomatic_setup
667                    hasn't yet been called and there aren't any filters etc.
668                    set up yet.
669                 */
670                 dcpomatic_setup_i18n ();
671
672                 /* Set things up, including filters etc.
673                    which will now be internationalised correctly.
674                 */
675                 dcpomatic_setup ();
676
677                 /* Force the configuration to be re-loaded correctly next
678                    time it is needed.
679                 */
680                 Config::drop ();
681
682                 _frame = new DOMFrame (_("DCP-o-matic Playlist Editor"));
683                 SetTopWindow (_frame);
684                 _frame->Maximize ();
685                 _frame->Show ();
686
687                 signal_manager = new wxSignalManager (this);
688                 Bind (wxEVT_IDLE, boost::bind (&App::idle, this));
689
690                 return true;
691         }
692         catch (exception& e)
693         {
694                 error_dialog (0, _("DCP-o-matic could not start"), std_to_wx(e.what()));
695                 return true;
696         }
697
698         /* An unhandled exception has occurred inside the main event loop */
699         bool OnExceptionInMainLoop () override
700         {
701                 try {
702                         throw;
703                 } catch (FileError& e) {
704                         error_dialog (
705                                 0,
706                                 wxString::Format (
707                                         _("An exception occurred: %s (%s)\n\n") + REPORT_PROBLEM,
708                                         std_to_wx (e.what()),
709                                         std_to_wx (e.file().string().c_str ())
710                                         )
711                                 );
712                 } catch (exception& e) {
713                         error_dialog (
714                                 0,
715                                 wxString::Format (
716                                         _("An exception occurred: %s.\n\n") + " " + REPORT_PROBLEM,
717                                         std_to_wx (e.what ())
718                                         )
719                                 );
720                 } catch (...) {
721                         error_dialog (0, _("An unknown exception occurred.") + "  " + REPORT_PROBLEM);
722                 }
723
724                 /* This will terminate the program */
725                 return false;
726         }
727
728         void OnUnhandledException () override
729         {
730                 error_dialog (0, _("An unknown exception occurred.") + "  " + REPORT_PROBLEM);
731         }
732
733         void idle ()
734         {
735                 signal_manager->ui_idle ();
736         }
737
738         DOMFrame* _frame;
739 };
740
741 IMPLEMENT_APP (App)