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