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