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