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