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