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