Supporters update.
[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                 list->Add (_list, 1, wxEXPAND | wxALL, DCPOMATIC_SIZER_GAP);
335
336                 auto button_sizer = new wxBoxSizer (wxVERTICAL);
337                 _up = new Button (parent, _("Up"));
338                 _down = new Button (parent, _("Down"));
339                 _add = new Button (parent, _("Add"));
340                 _remove = new Button (parent, _("Remove"));
341                 button_sizer->Add (_up, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
342                 button_sizer->Add (_down, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
343                 button_sizer->Add (_add, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
344                 button_sizer->Add (_remove, 0, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
345
346                 list->Add (button_sizer, 0, wxALL, DCPOMATIC_SIZER_GAP);
347
348                 _sizer->Add (list);
349
350                 _list->Bind (wxEVT_COMMAND_LIST_ITEM_SELECTED, bind(&PlaylistContent::setup_sensitivity, this));
351                 _list->Bind (wxEVT_COMMAND_LIST_ITEM_DESELECTED, bind(&PlaylistContent::setup_sensitivity, this));
352                 _name->Bind (wxEVT_TEXT, bind(&PlaylistContent::name_changed, this));
353                 _save_name->bind(&PlaylistContent::save_name_clicked, this);
354                 _up->Bind (wxEVT_BUTTON, bind(&PlaylistContent::up_clicked, this));
355                 _down->Bind (wxEVT_BUTTON, bind(&PlaylistContent::down_clicked, this));
356                 _add->Bind (wxEVT_BUTTON, bind(&PlaylistContent::add_clicked, this));
357                 _remove->Bind (wxEVT_BUTTON, bind(&PlaylistContent::remove_clicked, this));
358
359                 setup_sensitivity();
360         }
361
362         wxSizer* sizer ()
363         {
364                 return _sizer;
365         }
366
367         void set (shared_ptr<SignalSPL> playlist)
368         {
369                 _playlist = playlist;
370                 _list->DeleteAllItems ();
371                 if (_playlist) {
372                         for (auto i: _playlist->get()) {
373                                 add (i);
374                         }
375                         _name->SetValue (std_to_wx(_playlist->name()));
376                 } else {
377                         _name->SetValue (wxT(""));
378                 }
379                 setup_sensitivity ();
380         }
381
382         shared_ptr<SignalSPL> playlist () const
383         {
384                 return _playlist;
385         }
386
387
388 private:
389         void save_name_clicked()
390         {
391                 if (_playlist) {
392                         _playlist->set_name(wx_to_std(_name->GetValue()));
393                         save_playlist(_playlist);
394                 }
395                 setup_sensitivity();
396         }
397
398         void name_changed ()
399         {
400                 setup_sensitivity();
401         }
402
403         void add (SPLEntry e)
404         {
405                 wxListItem item;
406                 item.SetId (_list->GetItemCount());
407                 long const N = _list->InsertItem (item);
408                 set_item (N, e);
409         }
410
411         void set_item (long N, SPLEntry e)
412         {
413                 _list->SetItem (N, 0, std_to_wx(e.name));
414                 _list->SetItem (N, 1, std_to_wx(e.id));
415                 _list->SetItem (N, 2, std_to_wx(e.kind->name()));
416                 _list->SetItem (N, 3, e.encrypted ? S_("Question|Y") : S_("Question|N"));
417         }
418
419         void setup_sensitivity ()
420         {
421                 bool const have_list = static_cast<bool>(_playlist);
422                 int const num_selected = _list->GetSelectedItemCount ();
423                 long int selected = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
424                 _name->Enable (have_list);
425                 _save_name->Enable(_playlist && _playlist->name() != wx_to_std(_name->GetValue()));
426                 _list->Enable (have_list);
427                 _up->Enable (have_list && selected > 0);
428                 _down->Enable (have_list && selected != -1 && selected < (_list->GetItemCount() - 1));
429                 _add->Enable (have_list);
430                 _remove->Enable (have_list && num_selected > 0);
431         }
432
433         void add_clicked ()
434         {
435                 int const r = _content_dialog->ShowModal ();
436                 if (r == wxID_OK) {
437                         auto content = _content_dialog->selected ();
438                         if (content) {
439                                 SPLEntry e (content);
440                                 add (e);
441                                 DCPOMATIC_ASSERT (_playlist);
442                                 _playlist->add (e);
443                         }
444                 }
445         }
446
447         void up_clicked ()
448         {
449                 long int s = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
450                 if (s < 1) {
451                         return;
452                 }
453
454                 DCPOMATIC_ASSERT (_playlist);
455
456                 _playlist->swap(s, s - 1);
457
458                 set_item (s - 1, (*_playlist)[s-1]);
459                 set_item (s, (*_playlist)[s]);
460         }
461
462         void down_clicked ()
463         {
464                 long int s = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
465                 if (s > (_list->GetItemCount() - 1)) {
466                         return;
467                 }
468
469                 DCPOMATIC_ASSERT (_playlist);
470
471                 _playlist->swap(s, s + 1);
472
473                 set_item (s + 1, (*_playlist)[s+1]);
474                 set_item (s, (*_playlist)[s]);
475         }
476
477         void remove_clicked ()
478         {
479                 long int s = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
480                 if (s == -1) {
481                         return;
482                 }
483
484                 DCPOMATIC_ASSERT (_playlist);
485                 _playlist->remove (s);
486                 _list->DeleteItem (s);
487         }
488
489         ContentDialog* _content_dialog;
490         wxBoxSizer* _sizer;
491         wxTextCtrl* _name;
492         Button* _save_name;
493         wxListCtrl* _list;
494         wxButton* _up;
495         wxButton* _down;
496         wxButton* _add;
497         wxButton* _remove;
498         shared_ptr<SignalSPL> _playlist;
499 };
500
501
502 class DOMFrame : public wxFrame
503 {
504 public:
505         explicit DOMFrame (wxString const & title)
506                 : wxFrame (nullptr, wxID_ANY, title)
507                 , _content_dialog (new ContentDialog(this))
508                 , _config_dialog (nullptr)
509         {
510                 auto bar = new wxMenuBar;
511                 setup_menu (bar);
512                 SetMenuBar (bar);
513
514                 /* Use a panel as the only child of the Frame so that we avoid
515                    the dark-grey background on Windows.
516                 */
517                 auto overall_panel = new wxPanel (this, wxID_ANY);
518                 auto sizer = new wxBoxSizer (wxVERTICAL);
519
520                 _playlist_list = new PlaylistList (overall_panel, _content_dialog);
521                 _playlist_content = new PlaylistContent (overall_panel, _content_dialog);
522
523                 sizer->Add (_playlist_list->sizer());
524                 sizer->Add (_playlist_content->sizer());
525
526                 overall_panel->SetSizer (sizer);
527
528                 _playlist_list->Edit.connect (bind(&DOMFrame::change_playlist, this, _1));
529
530                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_exit, this), wxID_EXIT);
531                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::help_about, this), wxID_ABOUT);
532                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::edit_preferences, this), wxID_PREFERENCES);
533
534                 _config_changed_connection = Config::instance()->Changed.connect(boost::bind(&DOMFrame::config_changed, this));
535         }
536
537 private:
538
539         void file_exit ()
540         {
541                 /* false here allows the close handler to veto the close request */
542                 Close (false);
543         }
544
545         void help_about ()
546         {
547                 auto d = make_wx<AboutDialog>(this);
548                 d->ShowModal ();
549         }
550
551         void edit_preferences ()
552         {
553                 if (!_config_dialog) {
554                         _config_dialog = create_playlist_editor_config_dialog ();
555                 }
556                 _config_dialog->Show (this);
557         }
558
559         void change_playlist (shared_ptr<SignalSPL> playlist)
560         {
561                 auto old = _playlist_content->playlist ();
562                 if (old) {
563                         save_playlist (old);
564                 }
565                 _playlist_content->set (playlist);
566         }
567
568         void setup_menu (wxMenuBar* m)
569         {
570                 auto file = new wxMenu;
571 #ifdef __WXOSX__
572                 file->Append(wxID_PREFERENCES, _("&Preferences...\tCtrl-,"));
573                 file->Append (wxID_EXIT, _("&Exit"));
574 #else
575                 file->Append (wxID_EXIT, _("&Quit"));
576 #endif
577
578 #ifndef __WXOSX__
579                 auto edit = new wxMenu;
580                 edit->Append (wxID_PREFERENCES, _("&Preferences...\tCtrl-P"));
581 #endif
582
583                 auto help = new wxMenu;
584 #ifdef __WXOSX__
585                 help->Append (wxID_ABOUT, _("About DCP-o-matic"));
586 #else
587                 help->Append (wxID_ABOUT, _("About"));
588 #endif
589
590                 m->Append (file, _("&File"));
591 #ifndef __WXOSX__
592                 m->Append (edit, _("&Edit"));
593 #endif
594                 m->Append (help, _("&Help"));
595         }
596
597
598         void config_changed ()
599         {
600                 try {
601                         Config::instance()->write_config();
602                 } catch (exception& e) {
603                         error_dialog (
604                                 this,
605                                 wxString::Format (
606                                         _("Could not write to config file at %s.  Your changes have not been saved."),
607                                         std_to_wx (Config::instance()->cinemas_file().string()).data()
608                                         )
609                                 );
610                 }
611         }
612
613         ContentDialog* _content_dialog;
614         PlaylistList* _playlist_list;
615         PlaylistContent* _playlist_content;
616         wxPreferencesEditor* _config_dialog;
617         boost::signals2::scoped_connection _config_changed_connection;
618 };
619
620
621 /** @class App
622  *  @brief The magic App class for wxWidgets.
623  */
624 class App : public wxApp
625 {
626 public:
627         App ()
628                 : wxApp ()
629                 , _frame (nullptr)
630         {}
631
632 private:
633
634         bool OnInit () override
635         try
636         {
637                 wxInitAllImageHandlers ();
638                 SetAppName (_("DCP-o-matic Playlist Editor"));
639
640                 if (!wxApp::OnInit()) {
641                         return false;
642                 }
643
644 #ifdef DCPOMATIC_LINUX
645                 unsetenv ("UBUNTU_MENUPROXY");
646 #endif
647
648 #ifdef DCPOMATIC_OSX
649                 make_foreground_application ();
650 #endif
651
652                 dcpomatic_setup_path_encoding ();
653
654                 /* Enable i18n; this will create a Config object
655                    to look for a force-configured language.  This Config
656                    object will be wrong, however, because dcpomatic_setup
657                    hasn't yet been called and there aren't any filters etc.
658                    set up yet.
659                 */
660                 dcpomatic_setup_i18n ();
661
662                 /* Set things up, including filters etc.
663                    which will now be internationalised correctly.
664                 */
665                 dcpomatic_setup ();
666
667                 /* Force the configuration to be re-loaded correctly next
668                    time it is needed.
669                 */
670                 Config::drop ();
671
672                 _frame = new DOMFrame (_("DCP-o-matic Playlist Editor"));
673                 SetTopWindow (_frame);
674                 _frame->Maximize ();
675                 _frame->Show ();
676
677                 signal_manager = new wxSignalManager (this);
678                 Bind (wxEVT_IDLE, boost::bind (&App::idle, this));
679
680                 return true;
681         }
682         catch (exception& e)
683         {
684                 error_dialog (0, _("DCP-o-matic could not start"), std_to_wx(e.what()));
685                 return true;
686         }
687
688         /* An unhandled exception has occurred inside the main event loop */
689         bool OnExceptionInMainLoop () override
690         {
691                 try {
692                         throw;
693                 } catch (FileError& e) {
694                         error_dialog (
695                                 0,
696                                 wxString::Format (
697                                         _("An exception occurred: %s (%s)\n\n") + REPORT_PROBLEM,
698                                         std_to_wx (e.what()),
699                                         std_to_wx (e.file().string().c_str ())
700                                         )
701                                 );
702                 } catch (exception& e) {
703                         error_dialog (
704                                 0,
705                                 wxString::Format (
706                                         _("An exception occurred: %s.\n\n") + " " + REPORT_PROBLEM,
707                                         std_to_wx (e.what ())
708                                         )
709                                 );
710                 } catch (...) {
711                         error_dialog (0, _("An unknown exception occurred.") + "  " + REPORT_PROBLEM);
712                 }
713
714                 /* This will terminate the program */
715                 return false;
716         }
717
718         void OnUnhandledException () override
719         {
720                 error_dialog (0, _("An unknown exception occurred.") + "  " + REPORT_PROBLEM);
721         }
722
723         void idle ()
724         {
725                 signal_manager->ui_idle ();
726         }
727
728         DOMFrame* _frame;
729 };
730
731 IMPLEMENT_APP (App)