Merge 1.0
[dcpomatic.git] / src / tools / dcpomatic.cc
1 /*
2     Copyright (C) 2012-2013 Carl Hetherington <cth@carlh.net>
3
4     This program is free software; you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation; either version 2 of the License, or
7     (at your option) any later version.
8
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU General Public License
15     along with this program; if not, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
18 */
19
20 #include <iostream>
21 #include <fstream>
22 #include <boost/filesystem.hpp>
23 #ifdef __WXMSW__
24 #include <shellapi.h>
25 #endif
26 #ifdef __WXOSX__
27 #include <ApplicationServices/ApplicationServices.h>
28 #endif
29 #include <wx/generic/aboutdlgg.h>
30 #include <wx/stdpaths.h>
31 #include <wx/cmdline.h>
32 #include "wx/film_viewer.h"
33 #include "wx/film_editor.h"
34 #include "wx/job_manager_view.h"
35 #include "wx/config_dialog.h"
36 #include "wx/job_wrapper.h"
37 #include "wx/wx_util.h"
38 #include "wx/new_film_dialog.h"
39 #include "wx/properties_dialog.h"
40 #include "wx/wx_ui_signaller.h"
41 #include "wx/about_dialog.h"
42 #include "wx/kdm_dialog.h"
43 #include "wx/servers_list_dialog.h"
44 #include "wx/hints_dialog.h"
45 #include "lib/film.h"
46 #include "lib/config.h"
47 #include "lib/util.h"
48 #include "lib/version.h"
49 #include "lib/ui_signaller.h"
50 #include "lib/log.h"
51 #include "lib/job_manager.h"
52 #include "lib/transcode_job.h"
53 #include "lib/exceptions.h"
54 #include "lib/cinema.h"
55 #include "lib/kdm.h"
56 #include "lib/send_kdm_email_job.h"
57 #include "lib/server_finder.h"
58
59 using std::cout;
60 using std::string;
61 using std::wstring;
62 using std::stringstream;
63 using std::map;
64 using std::make_pair;
65 using std::list;
66 using std::exception;
67 using boost::shared_ptr;
68 using boost::dynamic_pointer_cast;
69
70 static FilmEditor* film_editor = 0;
71 static FilmViewer* film_viewer = 0;
72 static shared_ptr<Film> film;
73 static std::string log_level;
74 static std::string film_to_load;
75 static std::string film_to_create;
76 static wxMenu* jobs_menu = 0;
77
78 static void set_menu_sensitivity ();
79
80 class FilmChangedDialog
81 {
82 public:
83         FilmChangedDialog ()
84         {
85                 _dialog = new wxMessageDialog (
86                         0,
87                         wxString::Format (_("Save changes to film \"%s\" before closing?"), std_to_wx (film->name ()).data()),
88                         _("Film changed"),
89                         wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION
90                         );
91         }
92
93         ~FilmChangedDialog ()
94         {
95                 _dialog->Destroy ();
96         }
97
98         int run ()
99         {
100                 return _dialog->ShowModal ();
101         }
102
103 private:
104         /* Not defined */
105         FilmChangedDialog (FilmChangedDialog const &);
106         
107         wxMessageDialog* _dialog;
108 };
109
110
111 void
112 maybe_save_then_delete_film ()
113 {
114         if (!film) {
115                 return;
116         }
117                         
118         if (film->dirty ()) {
119                 FilmChangedDialog d;
120                 switch (d.run ()) {
121                 case wxID_NO:
122                         break;
123                 case wxID_YES:
124                         film->write_metadata ();
125                         break;
126                 }
127         }
128         
129         film.reset ();
130 }
131
132 #define ALWAYS                  0x0
133 #define NEEDS_FILM              0x1
134 #define NOT_DURING_DCP_CREATION 0x2
135 #define NEEDS_DCP               0x4
136
137 map<wxMenuItem*, int> menu_items;
138         
139 void
140 add_item (wxMenu* menu, wxString text, int id, int sens)
141 {
142         wxMenuItem* item = menu->Append (id, text);
143         menu_items.insert (make_pair (item, sens));
144 }
145
146 void
147 set_menu_sensitivity ()
148 {
149         list<shared_ptr<Job> > jobs = JobManager::instance()->get ();
150         list<shared_ptr<Job> >::iterator i = jobs.begin();
151         while (i != jobs.end() && dynamic_pointer_cast<TranscodeJob> (*i) == 0) {
152                 ++i;
153         }
154         bool const dcp_creation = (i != jobs.end ()) && !(*i)->finished ();
155         bool const have_dcp = film && !film->dcps().empty ();
156
157         for (map<wxMenuItem*, int>::iterator j = menu_items.begin(); j != menu_items.end(); ++j) {
158
159                 bool enabled = true;
160
161                 if ((j->second & NEEDS_FILM) && film == 0) {
162                         enabled = false;
163                 }
164
165                 if ((j->second & NOT_DURING_DCP_CREATION) && dcp_creation) {
166                         enabled = false;
167                 }
168
169                 if ((j->second & NEEDS_DCP) && !have_dcp) {
170                         enabled = false;
171                 }
172                 
173                 j->first->Enable (enabled);
174         }
175 }
176
177 enum {
178         ID_file_new = 1,
179         ID_file_open,
180         ID_file_save,
181         ID_file_properties,
182         ID_jobs_make_dcp,
183         ID_jobs_make_kdms,
184         ID_jobs_send_dcp_to_tms,
185         ID_jobs_show_dcp,
186         ID_tools_hints,
187         ID_tools_encoding_servers,
188 };
189
190 void
191 setup_menu (wxMenuBar* m)
192 {
193         wxMenu* file = new wxMenu;
194         add_item (file, _("New..."), ID_file_new, ALWAYS);
195         add_item (file, _("&Open..."), ID_file_open, ALWAYS);
196         file->AppendSeparator ();
197         add_item (file, _("&Save"), ID_file_save, NEEDS_FILM);
198         file->AppendSeparator ();
199         add_item (file, _("&Properties..."), ID_file_properties, NEEDS_FILM);
200 #ifndef __WXOSX__       
201         file->AppendSeparator ();
202 #endif
203
204 #ifdef __WXOSX__        
205         add_item (file, _("&Exit"), wxID_EXIT, ALWAYS);
206 #else
207         add_item (file, _("&Quit"), wxID_EXIT, ALWAYS);
208 #endif  
209         
210
211 #ifdef __WXOSX__        
212         add_item (file, _("&Preferences..."), wxID_PREFERENCES, ALWAYS);
213 #else
214         wxMenu* edit = new wxMenu;
215         add_item (edit, _("&Preferences..."), wxID_PREFERENCES, ALWAYS);
216 #endif  
217
218         jobs_menu = new wxMenu;
219         add_item (jobs_menu, _("&Make DCP"), ID_jobs_make_dcp, NEEDS_FILM | NOT_DURING_DCP_CREATION);
220         add_item (jobs_menu, _("Make &KDMs..."), ID_jobs_make_kdms, NEEDS_FILM | NEEDS_DCP);
221         add_item (jobs_menu, _("&Send DCP to TMS"), ID_jobs_send_dcp_to_tms, NEEDS_FILM | NOT_DURING_DCP_CREATION | NEEDS_DCP);
222         add_item (jobs_menu, _("S&how DCP"), ID_jobs_show_dcp, NEEDS_FILM | NOT_DURING_DCP_CREATION | NEEDS_DCP);
223
224         wxMenu* tools = new wxMenu;
225         add_item (tools, _("Hints..."), ID_tools_hints, 0);
226         add_item (tools, _("Encoding Servers..."), ID_tools_encoding_servers, 0);
227
228         wxMenu* help = new wxMenu;
229 #ifdef __WXOSX__        
230         add_item (help, _("About DCP-o-matic"), wxID_ABOUT, ALWAYS);
231 #else   
232         add_item (help, _("About"), wxID_ABOUT, ALWAYS);
233 #endif  
234
235         m->Append (file, _("&File"));
236 #ifndef __WXOSX__       
237         m->Append (edit, _("&Edit"));
238 #endif  
239         m->Append (jobs_menu, _("&Jobs"));
240         m->Append (tools, _("&Tools"));
241         m->Append (help, _("&Help"));
242 }
243
244 class Frame : public wxFrame
245 {
246 public:
247         Frame (wxString const & title)
248                 : wxFrame (NULL, -1, title)
249                 , _hints_dialog (0)
250                 , _servers_list_dialog (0)
251         {
252 #ifdef DCPOMATIC_WINDOWS_CONSOLE                
253                 AllocConsole();
254                 
255                 HANDLE handle_out = GetStdHandle(STD_OUTPUT_HANDLE);
256                 int hCrt = _open_osfhandle((intptr_t) handle_out, _O_TEXT);
257                 FILE* hf_out = _fdopen(hCrt, "w");
258                 setvbuf(hf_out, NULL, _IONBF, 1);
259                 *stdout = *hf_out;
260                 
261                 HANDLE handle_in = GetStdHandle(STD_INPUT_HANDLE);
262                 hCrt = _open_osfhandle((intptr_t) handle_in, _O_TEXT);
263                 FILE* hf_in = _fdopen(hCrt, "r");
264                 setvbuf(hf_in, NULL, _IONBF, 128);
265                 *stdin = *hf_in;
266 #endif
267
268                 wxMenuBar* bar = new wxMenuBar;
269                 setup_menu (bar);
270                 SetMenuBar (bar);
271
272                 Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::file_new, this),               ID_file_new);
273                 Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::file_open, this),              ID_file_open);
274                 Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::file_save, this),              ID_file_save);
275                 Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::file_properties, this),        ID_file_properties);
276                 Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::file_exit, this),              wxID_EXIT);
277                 Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::edit_preferences, this),       wxID_PREFERENCES);
278                 Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::jobs_make_dcp, this),          ID_jobs_make_dcp);
279                 Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::jobs_make_kdms, this),         ID_jobs_make_kdms);
280                 Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::jobs_send_dcp_to_tms, this),   ID_jobs_send_dcp_to_tms);
281                 Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::jobs_show_dcp, this),          ID_jobs_show_dcp);
282                 Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::tools_hints, this),            ID_tools_hints);
283                 Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::tools_encoding_servers, this), ID_tools_encoding_servers);
284                 Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::help_about, this),             wxID_ABOUT);
285
286                 Bind (wxEVT_CLOSE_WINDOW, boost::bind (&Frame::close, this, _1));
287
288                 /* Use a panel as the only child of the Frame so that we avoid
289                    the dark-grey background on Windows.
290                 */
291                 wxPanel* overall_panel = new wxPanel (this, wxID_ANY);
292
293                 film_editor = new FilmEditor (film, overall_panel);
294                 film_viewer = new FilmViewer (film, overall_panel);
295                 JobManagerView* job_manager_view = new JobManagerView (overall_panel, static_cast<JobManagerView::Buttons> (0));
296
297                 wxBoxSizer* right_sizer = new wxBoxSizer (wxVERTICAL);
298                 right_sizer->Add (film_viewer, 2, wxEXPAND | wxALL, 6);
299                 right_sizer->Add (job_manager_view, 1, wxEXPAND | wxALL, 6);
300
301                 wxBoxSizer* main_sizer = new wxBoxSizer (wxHORIZONTAL);
302                 main_sizer->Add (film_editor, 1, wxEXPAND | wxALL, 6);
303                 main_sizer->Add (right_sizer, 2, wxEXPAND | wxALL, 6);
304
305                 set_menu_sensitivity ();
306
307                 film_editor->FileChanged.connect (bind (&Frame::file_changed, this, _1));
308                 if (film) {
309                         file_changed (film->directory ());
310                 } else {
311                         file_changed ("");
312                 }
313
314                 JobManager::instance()->ActiveJobsChanged.connect (boost::bind (set_menu_sensitivity));
315
316                 set_film ();
317                 overall_panel->SetSizer (main_sizer);
318         }
319
320 private:
321
322         void set_film ()
323         {
324                 film_viewer->set_film (film);
325                 film_editor->set_film (film);
326                 set_menu_sensitivity ();
327         }
328
329         void file_changed (boost::filesystem::path f)
330         {
331                 stringstream s;
332                 s << wx_to_std (_("DCP-o-matic"));
333                 if (!f.empty ()) {
334                         s << " - " << f.string ();
335                 }
336                 
337                 SetTitle (std_to_wx (s.str()));
338         }
339         
340         void file_new ()
341         {
342                 NewFilmDialog* d = new NewFilmDialog (this);
343                 int const r = d->ShowModal ();
344                 
345                 if (r == wxID_OK) {
346
347                         if (boost::filesystem::is_directory (d->get_path()) && !boost::filesystem::is_empty(d->get_path())) {
348                                 if (!confirm_dialog (
349                                             this,
350                                             std_to_wx (
351                                                     String::compose (wx_to_std (_("The directory %1 already exists and is not empty.  "
352                                                                                   "Are you sure you want to use it?")),
353                                                                      d->get_path().string().c_str())
354                                                     )
355                                             )) {
356                                         return;
357                                 }
358                         } else if (boost::filesystem::is_regular_file (d->get_path())) {
359                                 error_dialog (
360                                         this,
361                                         String::compose (wx_to_std (_("%1 already exists as a file, so you cannot use it for a new film.")), d->get_path().c_str())
362                                         );
363                                 return;
364                         }
365                         
366                         maybe_save_then_delete_film ();
367                         film.reset (new Film (d->get_path ()));
368                         film->write_metadata ();
369                         film->log()->set_level (log_level);
370                         film->set_name (boost::filesystem::path (d->get_path()).filename().generic_string());
371                         set_film ();
372                 }
373                 
374                 d->Destroy ();
375         }
376
377         void file_open ()
378         {
379                 wxDirDialog* c = new wxDirDialog (
380                         this,
381                         _("Select film to open"),
382                         std_to_wx (Config::instance()->default_directory_or (wx_to_std (wxStandardPaths::Get().GetDocumentsDir())).string ()),
383                         wxDEFAULT_DIALOG_STYLE | wxDD_DIR_MUST_EXIST
384                         );
385                 
386                 int r;
387                 while (1) {
388                         r = c->ShowModal ();
389                         if (r == wxID_OK && c->GetPath() == wxStandardPaths::Get().GetDocumentsDir()) {
390                                 error_dialog (this, _("You did not select a folder.  Make sure that you select a folder before clicking Open."));
391                         } else {
392                                 break;
393                         }
394                 }
395                         
396                 if (r == wxID_OK) {
397                         maybe_save_then_delete_film ();
398                         try {
399                                 film.reset (new Film (wx_to_std (c->GetPath ())));
400                                 film->read_metadata ();
401                                 film->log()->set_level (log_level);
402                                 set_film ();
403                         } catch (std::exception& e) {
404                                 wxString p = c->GetPath ();
405                                 wxCharBuffer b = p.ToUTF8 ();
406                                 error_dialog (this, wxString::Format (_("Could not open film at %s (%s)"), p.data(), std_to_wx (e.what()).data()));
407                         }
408                 }
409
410                 c->Destroy ();
411         }
412
413         void file_save ()
414         {
415                 film->write_metadata ();
416         }
417
418         void file_properties ()
419         {
420                 PropertiesDialog* d = new PropertiesDialog (this, film);
421                 d->ShowModal ();
422                 d->Destroy ();
423         }
424         
425         void file_exit ()
426         {
427                 if (!should_close ()) {
428                         return;
429                 }
430                 
431                 maybe_save_then_delete_film ();
432                 Close (true);
433         }
434
435         void edit_preferences ()
436         {
437                 ConfigDialog* d = new ConfigDialog (this);
438                 d->ShowModal ();
439                 d->Destroy ();
440                 Config::instance()->write ();
441         }
442
443         void jobs_make_dcp ()
444         {
445                 JobWrapper::make_dcp (this, film);
446         }
447
448         void jobs_make_kdms ()
449         {
450                 if (!film) {
451                         return;
452                 }
453                 
454                 KDMDialog* d = new KDMDialog (this, film);
455                 if (d->ShowModal () != wxID_OK) {
456                         d->Destroy ();
457                         return;
458                 }
459
460                 try {
461                         if (d->write_to ()) {
462                                 write_kdm_files (film, d->screens (), d->dcp (), d->from (), d->until (), d->directory ());
463                         } else {
464                                 JobManager::instance()->add (
465                                         shared_ptr<Job> (new SendKDMEmailJob (film, d->screens (), d->dcp (), d->from (), d->until ()))
466                                         );
467                         }
468                 } catch (KDMError& e) {
469                         error_dialog (this, e.what ());
470                 }
471         
472                 d->Destroy ();
473         }
474         
475         void jobs_send_dcp_to_tms ()
476         {
477                 film->send_dcp_to_tms ();
478         }
479
480         void jobs_show_dcp ()
481         {
482 #ifdef __WXMSW__
483                 string d = film->directory().string ();
484                 wstring w;
485                 w.assign (d.begin(), d.end());
486                 ShellExecute (0, L"open", w.c_str(), 0, 0, SW_SHOWDEFAULT);
487 #else
488                 int r = system ("which nautilus");
489                 if (WEXITSTATUS (r) == 0) {
490                         r = system (string ("nautilus " + film->directory().string()).c_str ());
491                         if (WEXITSTATUS (r)) {
492                                 error_dialog (this, _("Could not show DCP (could not run nautilus)"));
493                         }
494                 } else {
495                         int r = system ("which konqueror");
496                         if (WEXITSTATUS (r) == 0) {
497                                 r = system (string ("konqueror " + film->directory().string()).c_str ());
498                                 if (WEXITSTATUS (r)) {
499                                         error_dialog (this, _("Could not show DCP (could not run konqueror)"));
500                                 }
501                         }
502                 }
503 #endif          
504         }
505
506         void tools_hints ()
507         {
508                 if (!_hints_dialog) {
509                         _hints_dialog = new HintsDialog (this, film);
510                 }
511
512                 _hints_dialog->Show ();
513         }
514
515         void tools_encoding_servers ()
516         {
517                 if (!_servers_list_dialog) {
518                         _servers_list_dialog = new ServersListDialog (this);
519                 }
520
521                 _servers_list_dialog->Show ();
522         }
523
524         void help_about ()
525         {
526                 AboutDialog* d = new AboutDialog (this);
527                 d->ShowModal ();
528                 d->Destroy ();
529         }
530
531         bool should_close ()
532         {
533                 if (!JobManager::instance()->work_to_do ()) {
534                         return true;
535                 }
536
537                 wxMessageDialog* d = new wxMessageDialog (
538                         0,
539                         _("There are unfinished jobs; are you sure you want to quit?"),
540                         _("Unfinished jobs"),
541                         wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION
542                         );
543
544                 bool const r = d->ShowModal() == wxID_YES;
545                 d->Destroy ();
546                 return r;
547         }
548                 
549         void close (wxCloseEvent& ev)
550         {
551                 if (!should_close ()) {
552                         ev.Veto ();
553                         return;
554                 }
555
556                 maybe_save_then_delete_film ();
557
558                 ev.Skip ();
559         }
560
561         HintsDialog* _hints_dialog;
562         ServersListDialog* _servers_list_dialog;
563 };
564
565 static const wxCmdLineEntryDesc command_line_description[] = {
566         { wxCMD_LINE_OPTION, "l", "log", "set log level (silent, verbose or timing)", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
567         { wxCMD_LINE_SWITCH, "n", "new", "create new film", wxCMD_LINE_VAL_NONE, wxCMD_LINE_PARAM_OPTIONAL },
568         { wxCMD_LINE_PARAM, 0, 0, "film to load or create", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_MULTIPLE | wxCMD_LINE_PARAM_OPTIONAL },
569         { wxCMD_LINE_NONE, "", "", "", wxCmdLineParamType (0), 0 }
570 };
571
572 class App : public wxApp
573 {
574         bool OnInit ()
575         try
576         {
577                 SetAppName (_("DCP-o-matic"));
578                 
579                 if (!wxApp::OnInit()) {
580                         return false;
581                 }
582                 
583 #ifdef DCPOMATIC_LINUX  
584                 unsetenv ("UBUNTU_MENUPROXY");
585 #endif
586
587 #ifdef __WXOSX__                
588                 ProcessSerialNumber serial;
589                 GetCurrentProcess (&serial);
590                 TransformProcessType (&serial, kProcessTransformToForegroundApplication);
591 #endif          
592
593                 wxInitAllImageHandlers ();
594
595                 /* Enable i18n; this will create a Config object
596                    to look for a force-configured language.  This Config
597                    object will be wrong, however, because dcpomatic_setup
598                    hasn't yet been called and there aren't any scalers, filters etc.
599                    set up yet.
600                 */
601                 dcpomatic_setup_i18n ();
602
603                 /* Set things up, including scalers / filters etc.
604                    which will now be internationalised correctly.
605                 */
606                 dcpomatic_setup ();
607
608                 /* Force the configuration to be re-loaded correctly next
609                    time it is needed.
610                 */
611                 Config::drop ();
612
613                 if (!film_to_load.empty() && boost::filesystem::is_directory (film_to_load)) {
614                         try {
615                                 film.reset (new Film (film_to_load));
616                                 film->read_metadata ();
617                                 film->log()->set_level (log_level);
618                         } catch (exception& e) {
619                                 error_dialog (0, std_to_wx (String::compose (wx_to_std (_("Could not load film %1 (%2)")), film_to_load, e.what())));
620                         }
621                 }
622
623                 if (!film_to_create.empty ()) {
624                         film.reset (new Film (film_to_create));
625                         film->write_metadata ();
626                         film->log()->set_level (log_level);
627                         film->set_name (boost::filesystem::path (film_to_create).filename().generic_string ());
628                 }
629
630                 Frame* f = new Frame (_("DCP-o-matic"));
631                 SetTopWindow (f);
632                 f->Maximize ();
633                 f->Show ();
634
635                 ui_signaller = new wxUISignaller (this);
636                 Bind (wxEVT_IDLE, boost::bind (&App::idle, this));
637
638                 Bind (wxEVT_TIMER, boost::bind (&App::check, this));
639                 _timer.reset (new wxTimer (this));
640                 _timer->Start (1000);
641                 
642                 return true;
643         }
644         catch (exception& e)
645         {
646                 error_dialog (0, wxString::Format ("DCP-o-matic could not start: %s", e.what ()));
647                 return true;
648         }
649
650         void OnInitCmdLine (wxCmdLineParser& parser)
651         {
652                 parser.SetDesc (command_line_description);
653                 parser.SetSwitchChars (wxT ("-"));
654         }
655
656         bool OnCmdLineParsed (wxCmdLineParser& parser)
657         {
658                 if (parser.GetParamCount() > 0) {
659                         if (parser.Found (wxT ("new"))) {
660                                 film_to_create = wx_to_std (parser.GetParam (0));
661                         } else {
662                                 film_to_load = wx_to_std (parser.GetParam(0));
663                         }
664                 }
665
666                 wxString log;
667                 if (parser.Found (wxT ("log"), &log)) {
668                         log_level = wx_to_std (log);
669                 }
670
671                 return true;
672         }
673
674         void idle ()
675         {
676                 ui_signaller->ui_idle ();
677         }
678
679         void check ()
680         {
681                 try {
682                         ServerFinder::instance()->rethrow ();
683                 } catch (exception& e) {
684                         error_dialog (0, std_to_wx (e.what ()));
685                 }
686         }
687
688         shared_ptr<wxTimer> _timer;
689 };
690
691 IMPLEMENT_APP (App)