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