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