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