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