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