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