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