Save playlist when content is added, moved or removed.
[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
328         wxSizer* sizer ()
329         {
330                 return _sizer;
331         }
332
333         void set (shared_ptr<SignalSPL> playlist)
334         {
335                 _playlist = playlist;
336                 _list->DeleteAllItems ();
337                 if (_playlist) {
338                         for (auto i: _playlist->get()) {
339                                 add (i);
340                         }
341                         _name->SetValue (std_to_wx(_playlist->name()));
342                 } else {
343                         _name->SetValue (wxT(""));
344                 }
345                 setup_sensitivity ();
346         }
347
348         shared_ptr<SignalSPL> playlist () const
349         {
350                 return _playlist;
351         }
352
353
354 private:
355         void name_changed ()
356         {
357                 if (_playlist) {
358                         _playlist->set_name (wx_to_std(_name->GetValue()));
359                 }
360         }
361
362         void add (SPLEntry e)
363         {
364                 wxListItem item;
365                 item.SetId (_list->GetItemCount());
366                 long const N = _list->InsertItem (item);
367                 set_item (N, e);
368         }
369
370         void set_item (long N, SPLEntry e)
371         {
372                 _list->SetItem (N, 0, std_to_wx(e.name));
373                 _list->SetItem (N, 1, std_to_wx(e.id));
374                 _list->SetItem (N, 2, std_to_wx(e.kind->name()));
375                 _list->SetItem (N, 3, e.encrypted ? S_("Question|Y") : S_("Question|N"));
376         }
377
378         void setup_sensitivity ()
379         {
380                 bool const have_list = static_cast<bool>(_playlist);
381                 int const num_selected = _list->GetSelectedItemCount ();
382                 long int selected = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
383                 _name->Enable (have_list);
384                 _list->Enable (have_list);
385                 _up->Enable (have_list && selected > 0);
386                 _down->Enable (have_list && selected != -1 && selected < (_list->GetItemCount() - 1));
387                 _add->Enable (have_list);
388                 _remove->Enable (have_list && num_selected > 0);
389         }
390
391         void add_clicked ()
392         {
393                 int const r = _content_dialog->ShowModal ();
394                 if (r == wxID_OK) {
395                         auto content = _content_dialog->selected ();
396                         if (content) {
397                                 SPLEntry e (content);
398                                 add (e);
399                                 DCPOMATIC_ASSERT (_playlist);
400                                 _playlist->add (e);
401                         }
402                 }
403         }
404
405         void up_clicked ()
406         {
407                 long int s = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
408                 if (s < 1) {
409                         return;
410                 }
411
412                 DCPOMATIC_ASSERT (_playlist);
413
414                 _playlist->swap(s, s - 1);
415
416                 set_item (s - 1, (*_playlist)[s-1]);
417                 set_item (s, (*_playlist)[s]);
418         }
419
420         void down_clicked ()
421         {
422                 long int s = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
423                 if (s > (_list->GetItemCount() - 1)) {
424                         return;
425                 }
426
427                 DCPOMATIC_ASSERT (_playlist);
428
429                 _playlist->swap(s, s + 1);
430
431                 set_item (s + 1, (*_playlist)[s+1]);
432                 set_item (s, (*_playlist)[s]);
433         }
434
435         void remove_clicked ()
436         {
437                 long int s = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
438                 if (s == -1) {
439                         return;
440                 }
441
442                 DCPOMATIC_ASSERT (_playlist);
443                 _playlist->remove (s);
444                 _list->DeleteItem (s);
445         }
446
447         ContentDialog* _content_dialog;
448         wxBoxSizer* _sizer;
449         wxTextCtrl* _name;
450         wxListCtrl* _list;
451         wxButton* _up;
452         wxButton* _down;
453         wxButton* _add;
454         wxButton* _remove;
455         shared_ptr<SignalSPL> _playlist;
456 };
457
458
459 class DOMFrame : public wxFrame
460 {
461 public:
462         explicit DOMFrame (wxString const & title)
463                 : wxFrame (nullptr, wxID_ANY, title)
464                 , _content_dialog (new ContentDialog(this))
465                 , _config_dialog (nullptr)
466         {
467                 auto bar = new wxMenuBar;
468                 setup_menu (bar);
469                 SetMenuBar (bar);
470
471                 /* Use a panel as the only child of the Frame so that we avoid
472                    the dark-grey background on Windows.
473                 */
474                 auto overall_panel = new wxPanel (this, wxID_ANY);
475                 auto sizer = new wxBoxSizer (wxVERTICAL);
476
477                 _playlist_list = new PlaylistList (overall_panel, _content_dialog);
478                 _playlist_content = new PlaylistContent (overall_panel, _content_dialog);
479
480                 sizer->Add (_playlist_list->sizer());
481                 sizer->Add (_playlist_content->sizer());
482
483                 overall_panel->SetSizer (sizer);
484
485                 _playlist_list->Edit.connect (bind(&DOMFrame::change_playlist, this, _1));
486
487                 _playlist_content->set (_playlist_list->first_playlist());
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)