Use more ScopeGuards.
[dcpomatic.git] / src / wx / config_dialog.cc
1 /*
2     Copyright (C) 2012-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 "audio_mapping_view.h"
23 #include "check_box.h"
24 #include "config_dialog.h"
25 #include "dcpomatic_button.h"
26 #include "nag_dialog.h"
27 #include "static_text.h"
28 #include "lib/constants.h"
29 #include <dcp/file.h>
30 #include <dcp/raw_convert.h>
31
32
33 using std::function;
34 using std::make_pair;
35 using std::make_shared;
36 using std::map;
37 using std::pair;
38 using std::shared_ptr;
39 using std::string;
40 using std::vector;
41 using boost::bind;
42 using boost::optional;
43 #if BOOST_VERSION >= 106100
44 using namespace boost::placeholders;
45 #endif
46
47
48 static
49 bool
50 do_nothing ()
51 {
52         return false;
53 }
54
55 Page::Page (wxSize panel_size, int border)
56         : _border (border)
57         , _panel (0)
58         , _panel_size (panel_size)
59         , _window_exists (false)
60 {
61         _config_connection = Config::instance()->Changed.connect (bind (&Page::config_changed_wrapper, this));
62 }
63
64
65 wxWindow*
66 Page::CreateWindow (wxWindow* parent)
67 {
68         return create_window (parent);
69 }
70
71
72 wxWindow*
73 Page::create_window (wxWindow* parent)
74 {
75         _panel = new wxPanel (parent, wxID_ANY, wxDefaultPosition, _panel_size);
76         auto s = new wxBoxSizer (wxVERTICAL);
77         _panel->SetSizer (s);
78
79         setup ();
80         _window_exists = true;
81         config_changed ();
82
83         _panel->Bind (wxEVT_DESTROY, bind (&Page::window_destroyed, this));
84
85         return _panel;
86 }
87
88 void
89 Page::config_changed_wrapper ()
90 {
91         if (_window_exists) {
92                 config_changed ();
93         }
94 }
95
96 void
97 Page::window_destroyed ()
98 {
99         _window_exists = false;
100 }
101
102
103 GeneralPage::GeneralPage (wxSize panel_size, int border)
104         : Page (panel_size, border)
105 {
106
107 }
108
109
110 wxString
111 GeneralPage::GetName () const
112 {
113         return _("General");
114 }
115
116
117 void
118 GeneralPage::add_language_controls (wxGridBagSizer* table, int& r)
119 {
120         _set_language = new CheckBox (_panel, _("Set language"));
121         table->Add (_set_language, wxGBPosition (r, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
122         _language = new wxChoice (_panel, wxID_ANY);
123         vector<pair<string, string>> languages;
124         languages.push_back (make_pair("Čeština", "cs_CZ"));
125         languages.push_back (make_pair("汉语/漢語", "zh_CN"));
126         languages.push_back (make_pair("Dansk", "da_DK"));
127         languages.push_back (make_pair("Deutsch", "de_DE"));
128         languages.push_back (make_pair("English", "en_GB"));
129         languages.push_back (make_pair("Español", "es_ES"));
130         languages.push_back (make_pair("Français", "fr_FR"));
131         languages.push_back (make_pair("Italiano", "it_IT"));
132         languages.push_back (make_pair("Nederlands", "nl_NL"));
133         languages.push_back (make_pair("Русский", "ru_RU"));
134         languages.push_back (make_pair("Polski", "pl_PL"));
135         languages.push_back (make_pair("Português europeu", "pt_PT"));
136         languages.push_back (make_pair("Português do Brasil", "pt_BR"));
137         languages.push_back (make_pair("Svenska", "sv_SE"));
138         languages.push_back (make_pair("Slovenščina", "sl_SI"));
139         languages.push_back (make_pair("Slovenský jazyk", "sk_SK"));
140         // languages.push_back (make_pair("Türkçe", "tr_TR"));
141         languages.push_back (make_pair("українська мова", "uk_UA"));
142         languages.push_back (make_pair("Magyar nyelv", "hu_HU"));
143         checked_set (_language, languages);
144         table->Add (_language, wxGBPosition (r, 1));
145         ++r;
146
147         auto restart = add_label_to_sizer (
148                 table, _panel, _("(restart DCP-o-matic to see language changes)"), false, wxGBPosition (r, 0), wxGBSpan (1, 2)
149                 );
150         wxFont font = restart->GetFont();
151         font.SetStyle (wxFONTSTYLE_ITALIC);
152         font.SetPointSize (font.GetPointSize() - 1);
153         restart->SetFont (font);
154         ++r;
155
156         _set_language->bind(&GeneralPage::set_language_changed, this);
157         _language->Bind     (wxEVT_CHOICE,   bind (&GeneralPage::language_changed,     this));
158 }
159
160 void
161 GeneralPage::add_update_controls (wxGridBagSizer* table, int& r)
162 {
163         _check_for_updates = new CheckBox (_panel, _("Check for updates on startup"));
164         table->Add (_check_for_updates, wxGBPosition (r, 0), wxGBSpan (1, 2));
165         ++r;
166
167         _check_for_test_updates = new CheckBox (_panel, _("Check for testing updates on startup"));
168         table->Add (_check_for_test_updates, wxGBPosition (r, 0), wxGBSpan (1, 2));
169         ++r;
170
171         _check_for_updates->bind(&GeneralPage::check_for_updates_changed, this);
172         _check_for_test_updates->bind(&GeneralPage::check_for_test_updates_changed, this);
173 }
174
175 void
176 GeneralPage::config_changed ()
177 {
178         auto config = Config::instance ();
179
180         checked_set (_set_language, static_cast<bool>(config->language()));
181
182         /* Backwards compatibility of config file */
183
184         map<string, string> compat_map;
185         compat_map["fr"] = "fr_FR";
186         compat_map["it"] = "it_IT";
187         compat_map["es"] = "es_ES";
188         compat_map["sv"] = "sv_SE";
189         compat_map["de"] = "de_DE";
190         compat_map["nl"] = "nl_NL";
191         compat_map["ru"] = "ru_RU";
192         compat_map["pl"] = "pl_PL";
193         compat_map["da"] = "da_DK";
194         compat_map["pt"] = "pt_PT";
195         compat_map["sk"] = "sk_SK";
196         compat_map["cs"] = "cs_CZ";
197         compat_map["uk"] = "uk_UA";
198
199         auto lang = config->language().get_value_or("en_GB");
200         if (compat_map.find(lang) != compat_map.end ()) {
201                 lang = compat_map[lang];
202         }
203
204         checked_set (_language, lang);
205
206         checked_set (_check_for_updates, config->check_for_updates ());
207         checked_set (_check_for_test_updates, config->check_for_test_updates ());
208
209         setup_sensitivity ();
210 }
211
212 void
213 GeneralPage::setup_sensitivity ()
214 {
215         _language->Enable (_set_language->GetValue ());
216         _check_for_test_updates->Enable (_check_for_updates->GetValue ());
217 }
218
219 void
220 GeneralPage::set_language_changed ()
221 {
222         setup_sensitivity ();
223         if (_set_language->GetValue ()) {
224                 language_changed ();
225         } else {
226                 Config::instance()->unset_language ();
227         }
228 }
229
230 void
231 GeneralPage::language_changed ()
232 {
233         int const sel = _language->GetSelection ();
234         if (sel != -1) {
235                 Config::instance()->set_language (string_client_data (_language->GetClientObject (sel)));
236         } else {
237                 Config::instance()->unset_language ();
238         }
239 }
240
241 void
242 GeneralPage::check_for_updates_changed ()
243 {
244         Config::instance()->set_check_for_updates (_check_for_updates->GetValue ());
245 }
246
247 void
248 GeneralPage::check_for_test_updates_changed ()
249 {
250         Config::instance()->set_check_for_test_updates (_check_for_test_updates->GetValue ());
251 }
252
253 CertificateChainEditor::CertificateChainEditor (
254         wxWindow* parent,
255         wxString title,
256         int border,
257         function<void (shared_ptr<dcp::CertificateChain>)> set,
258         function<shared_ptr<const dcp::CertificateChain> (void)> get,
259         function<bool (void)> nag_alter
260         )
261         : wxDialog (parent, wxID_ANY, title)
262         , _set (set)
263         , _get (get)
264         , _nag_alter (nag_alter)
265 {
266         _sizer = new wxBoxSizer (wxVERTICAL);
267
268         auto certificates_sizer = new wxBoxSizer (wxHORIZONTAL);
269         _sizer->Add (certificates_sizer, 0, wxALL, border);
270
271         _certificates = new wxListCtrl (this, wxID_ANY, wxDefaultPosition, wxSize (440, 150), wxLC_REPORT | wxLC_SINGLE_SEL);
272
273         {
274                 wxListItem ip;
275                 ip.SetId (0);
276                 ip.SetText (_("Type"));
277                 ip.SetWidth (100);
278                 _certificates->InsertColumn (0, ip);
279         }
280
281         {
282                 wxListItem ip;
283                 ip.SetId (1);
284                 ip.SetText (_("Thumbprint"));
285                 ip.SetWidth (340);
286
287                 wxFont font = ip.GetFont ();
288                 font.SetFamily (wxFONTFAMILY_TELETYPE);
289                 ip.SetFont (font);
290
291                 _certificates->InsertColumn (1, ip);
292         }
293
294         certificates_sizer->Add (_certificates, 1, wxEXPAND);
295
296         {
297                 auto s = new wxBoxSizer (wxVERTICAL);
298                 _add_certificate = new Button (this, _("Add..."));
299                 s->Add (_add_certificate, 1, wxTOP | wxBOTTOM | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
300                 _remove_certificate = new Button (this, _("Remove"));
301                 s->Add (_remove_certificate, 1, wxTOP | wxBOTTOM | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
302                 _export_certificate = new Button (this, _("Export certificate..."));
303                 s->Add (_export_certificate, 1, wxTOP | wxBOTTOM | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
304                 _export_chain = new Button (this, _("Export chain..."));
305                 s->Add (_export_chain, 1, wxTOP | wxBOTTOM | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
306                 certificates_sizer->Add (s, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP);
307         }
308
309         auto table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
310         _sizer->Add (table, 1, wxALL | wxEXPAND, border);
311         int r = 0;
312
313         add_label_to_sizer (table, this, _("Leaf private key"), true, wxGBPosition (r, 0));
314         _private_key = new StaticText (this, wxT(""));
315         wxFont font = _private_key->GetFont ();
316         font.SetFamily (wxFONTFAMILY_TELETYPE);
317         _private_key->SetFont (font);
318         table->Add (_private_key, wxGBPosition (r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
319         _import_private_key = new Button (this, _("Import..."));
320         table->Add (_import_private_key, wxGBPosition (r, 2));
321         _export_private_key = new Button (this, _("Export..."));
322         table->Add (_export_private_key, wxGBPosition (r, 3));
323         ++r;
324
325         _button_sizer = new wxBoxSizer (wxHORIZONTAL);
326         _remake_certificates = new Button (this, _("Re-make certificates and key..."));
327         _button_sizer->Add (_remake_certificates, 1, wxRIGHT, border);
328         table->Add (_button_sizer, wxGBPosition (r, 0), wxGBSpan (1, 4));
329         ++r;
330
331         _private_key_bad = new StaticText (this, _("Leaf private key does not match leaf certificate!"));
332         font = *wxSMALL_FONT;
333         font.SetWeight (wxFONTWEIGHT_BOLD);
334         _private_key_bad->SetFont (font);
335         table->Add (_private_key_bad, wxGBPosition (r, 0), wxGBSpan (1, 3));
336         ++r;
337
338         _add_certificate->Bind     (wxEVT_BUTTON,       bind (&CertificateChainEditor::add_certificate, this));
339         _remove_certificate->Bind  (wxEVT_BUTTON,       bind (&CertificateChainEditor::remove_certificate, this));
340         _export_certificate->Bind  (wxEVT_BUTTON,       bind (&CertificateChainEditor::export_certificate, this));
341         _certificates->Bind        (wxEVT_LIST_ITEM_SELECTED,   bind (&CertificateChainEditor::update_sensitivity, this));
342         _certificates->Bind        (wxEVT_LIST_ITEM_DESELECTED, bind (&CertificateChainEditor::update_sensitivity, this));
343         _remake_certificates->Bind (wxEVT_BUTTON,       bind (&CertificateChainEditor::remake_certificates, this));
344         _export_chain->Bind        (wxEVT_BUTTON,       bind (&CertificateChainEditor::export_chain, this));
345         _import_private_key->Bind  (wxEVT_BUTTON,       bind (&CertificateChainEditor::import_private_key, this));
346         _export_private_key->Bind  (wxEVT_BUTTON,       bind (&CertificateChainEditor::export_private_key, this));
347
348         auto buttons = CreateSeparatedButtonSizer (wxCLOSE);
349         if (buttons) {
350                 _sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
351         }
352
353         SetSizerAndFit (_sizer);
354
355         update_certificate_list ();
356         update_private_key ();
357         update_sensitivity ();
358 }
359
360 void
361 CertificateChainEditor::add_button (wxWindow* button)
362 {
363         _button_sizer->Add (button, 0, wxLEFT | wxRIGHT, DCPOMATIC_SIZER_X_GAP);
364         _sizer->Layout ();
365 }
366
367 void
368 CertificateChainEditor::add_certificate ()
369 {
370         auto d = new wxFileDialog (this, _("Select Certificate File"));
371         ScopeGuard sg = [d]() { d->Destroy(); };
372
373         if (d->ShowModal() == wxID_OK) {
374                 try {
375                         dcp::Certificate c;
376                         string extra;
377                         try {
378                                 extra = c.read_string (dcp::file_to_string (wx_to_std (d->GetPath ())));
379                         } catch (boost::filesystem::filesystem_error& e) {
380                                 error_dialog (this, _("Could not import certificate (%s)"), d->GetPath());
381                                 return;
382                         }
383
384                         if (!extra.empty ()) {
385                                 message_dialog (
386                                         this,
387                                         _("This file contains other certificates (or other data) after its first certificate. "
388                                           "Only the first certificate will be used.")
389                                         );
390                         }
391                         auto chain = make_shared<dcp::CertificateChain>(*_get().get());
392                         chain->add (c);
393                         if (!chain->chain_valid ()) {
394                                 error_dialog (
395                                         this,
396                                         _("Adding this certificate would make the chain inconsistent, so it will not be added. "
397                                           "Add certificates in order from root to intermediate to leaf.")
398                                         );
399                                 chain->remove (c);
400                         } else {
401                                 _set (chain);
402                                 update_certificate_list ();
403                         }
404                 } catch (dcp::MiscError& e) {
405                         error_dialog (this, _("Could not read certificate file."), std_to_wx(e.what()));
406                 }
407         }
408
409         update_sensitivity ();
410 }
411
412 void
413 CertificateChainEditor::remove_certificate ()
414 {
415         if (_nag_alter()) {
416                 /* Cancel was clicked */
417                 return;
418         }
419
420         int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
421         if (i == -1) {
422                 return;
423         }
424
425         _certificates->DeleteItem (i);
426         auto chain = make_shared<dcp::CertificateChain>(*_get().get());
427         chain->remove (i);
428         _set (chain);
429
430         update_sensitivity ();
431         update_certificate_list ();
432 }
433
434 void
435 CertificateChainEditor::export_certificate ()
436 {
437         int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
438         if (i == -1) {
439                 return;
440         }
441
442         auto all = _get()->root_to_leaf();
443
444         wxString default_name;
445         if (i == 0) {
446                 default_name = "root.pem";
447         } else if (i == static_cast<int>(all.size() - 1)) {
448                 default_name = "leaf.pem";
449         } else {
450                 default_name = "intermediate.pem";
451         }
452
453         auto d = new wxFileDialog(
454                 this, _("Select Certificate File"), wxEmptyString, default_name, wxT ("PEM files (*.pem)|*.pem"),
455                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT
456                 );
457         ScopeGuard sg = [d]() { d->Destroy(); };
458
459         auto j = all.begin ();
460         for (int k = 0; k < i; ++k) {
461                 ++j;
462         }
463
464         if (d->ShowModal() != wxID_OK) {
465                 return;
466         }
467
468         boost::filesystem::path path(wx_to_std(d->GetPath()));
469         if (path.extension() != ".pem") {
470                 path += ".pem";
471         }
472         dcp::File f(path, "w");
473         if (!f) {
474                 throw OpenFileError(path, errno, OpenFileError::WRITE);
475         }
476
477         string const s = j->certificate(true);
478         f.checked_write(s.c_str(), s.length());
479 }
480
481 void
482 CertificateChainEditor::export_chain ()
483 {
484         auto d = new wxFileDialog (
485                 this, _("Select Chain File"), wxEmptyString, wxT("certificate_chain.pem"), wxT("PEM files (*.pem)|*.pem"),
486                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT
487                 );
488         ScopeGuard sg = [d]() { d->Destroy(); };
489
490         if (d->ShowModal() != wxID_OK) {
491                 return;
492         }
493
494         boost::filesystem::path path(wx_to_std(d->GetPath()));
495         if (path.extension() != ".pem") {
496                 path += ".pem";
497         }
498         dcp::File f(path, "w");
499         if (!f) {
500                 throw OpenFileError(path, errno, OpenFileError::WRITE);
501         }
502
503         auto const s = _get()->chain();
504         f.checked_write(s.c_str(), s.length());
505 }
506
507 void
508 CertificateChainEditor::update_certificate_list ()
509 {
510         _certificates->DeleteAllItems ();
511         size_t n = 0;
512         auto certs = _get()->root_to_leaf();
513         for (auto const& i: certs) {
514                 wxListItem item;
515                 item.SetId (n);
516                 _certificates->InsertItem (item);
517                 _certificates->SetItem (n, 1, std_to_wx (i.thumbprint ()));
518
519                 if (n == 0) {
520                         _certificates->SetItem (n, 0, _("Root"));
521                 } else if (n == (certs.size() - 1)) {
522                         _certificates->SetItem (n, 0, _("Leaf"));
523                 } else {
524                         _certificates->SetItem (n, 0, _("Intermediate"));
525                 }
526
527                 ++n;
528         }
529
530         static wxColour normal = _private_key_bad->GetForegroundColour ();
531
532         if (_get()->private_key_valid()) {
533                 _private_key_bad->Hide ();
534                 _private_key_bad->SetForegroundColour (normal);
535         } else {
536                 _private_key_bad->Show ();
537                 _private_key_bad->SetForegroundColour (wxColour (255, 0, 0));
538         }
539 }
540
541 void
542 CertificateChainEditor::remake_certificates ()
543 {
544         if (_nag_alter()) {
545                 /* Cancel was clicked */
546                 return;
547         }
548
549         auto d = new MakeChainDialog (this, _get());
550         ScopeGuard sg = [d]() { d->Destroy(); };
551
552         if (d->ShowModal () == wxID_OK) {
553                 _set (d->get());
554                 update_certificate_list ();
555                 update_private_key ();
556         }
557 }
558
559 void
560 CertificateChainEditor::update_sensitivity ()
561 {
562         /* We can only remove the leaf certificate */
563         _remove_certificate->Enable (_certificates->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED) == (_certificates->GetItemCount() - 1));
564         _export_certificate->Enable (_certificates->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED) != -1);
565 }
566
567 void
568 CertificateChainEditor::update_private_key ()
569 {
570         checked_set (_private_key, dcp::private_key_fingerprint (_get()->key().get()));
571         _sizer->Layout ();
572 }
573
574 void
575 CertificateChainEditor::import_private_key ()
576 {
577         auto d = new wxFileDialog (this, _("Select Key File"));
578         ScopeGuard sg = [d]() { d->Destroy(); };
579
580         if (d->ShowModal() == wxID_OK) {
581                 try {
582                         boost::filesystem::path p (wx_to_std (d->GetPath ()));
583                         if (boost::filesystem::file_size (p) > 8192) {
584                                 error_dialog (
585                                         this,
586                                         wxString::Format (_("Could not read key file; file is too long (%s)"), std_to_wx (p.string ()))
587                                         );
588                                 return;
589                         }
590
591                         auto chain = make_shared<dcp::CertificateChain>(*_get().get());
592                         chain->set_key (dcp::file_to_string (p));
593                         _set (chain);
594                         update_private_key ();
595                 } catch (std::exception& e) {
596                         error_dialog (this, _("Could not read certificate file."), std_to_wx(e.what()));
597                 }
598         }
599
600         update_sensitivity ();
601 }
602
603 void
604 CertificateChainEditor::export_private_key ()
605 {
606         auto key = _get()->key();
607         if (!key) {
608                 return;
609         }
610
611         auto d = new wxFileDialog (
612                 this, _("Select Key File"), wxEmptyString, wxT("private_key.pem"), wxT("PEM files (*.pem)|*.pem"),
613                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT
614                 );
615         ScopeGuard sg = [d]() { d->Destroy(); };
616
617         if (d->ShowModal () == wxID_OK) {
618                 boost::filesystem::path path (wx_to_std(d->GetPath()));
619                 if (path.extension() != ".pem") {
620                         path += ".pem";
621                 }
622                 dcp::File f(path, "w");
623                 if (!f) {
624                         throw OpenFileError (path, errno, OpenFileError::WRITE);
625                 }
626
627                 auto const s = _get()->key().get ();
628                 f.checked_write(s.c_str(), s.length());
629         }
630 }
631
632 wxString
633 KeysPage::GetName () const
634 {
635         return _("Keys");
636 }
637
638 void
639 KeysPage::setup ()
640 {
641         wxFont subheading_font (*wxNORMAL_FONT);
642         subheading_font.SetWeight (wxFONTWEIGHT_BOLD);
643
644         auto sizer = _panel->GetSizer();
645
646         {
647                 auto m = new StaticText (_panel, _("Decrypting KDMs"));
648                 m->SetFont (subheading_font);
649                 sizer->Add (m, 0, wxALL | wxEXPAND, _border);
650         }
651
652         auto kdm_buttons = new wxBoxSizer (wxVERTICAL);
653
654         auto export_decryption_certificate = new Button (_panel, _("Export KDM decryption leaf certificate..."));
655         kdm_buttons->Add (export_decryption_certificate, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
656         auto export_settings = new Button (_panel, _("Export all KDM decryption settings..."));
657         kdm_buttons->Add (export_settings, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
658         auto import_settings = new Button (_panel, _("Import all KDM decryption settings..."));
659         kdm_buttons->Add (import_settings, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
660         auto decryption_advanced = new Button (_panel, _("Advanced..."));
661         kdm_buttons->Add (decryption_advanced, 0);
662
663         sizer->Add (kdm_buttons, 0, wxLEFT, _border);
664
665         export_decryption_certificate->Bind (wxEVT_BUTTON, bind (&KeysPage::export_decryption_certificate, this));
666         export_settings->Bind (wxEVT_BUTTON, bind (&KeysPage::export_decryption_chain_and_key, this));
667         import_settings->Bind (wxEVT_BUTTON, bind (&KeysPage::import_decryption_chain_and_key, this));
668         decryption_advanced->Bind (wxEVT_BUTTON, bind (&KeysPage::decryption_advanced, this));
669
670         {
671                 auto m = new StaticText (_panel, _("Signing DCPs and KDMs"));
672                 m->SetFont (subheading_font);
673                 sizer->Add (m, 0, wxALL | wxEXPAND, _border);
674         }
675
676         auto signing_buttons = new wxBoxSizer (wxVERTICAL);
677
678         auto signing_advanced = new Button (_panel, _("Advanced..."));
679         signing_buttons->Add (signing_advanced, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
680         auto remake_signing = new Button (_panel, _("Re-make certificates and key..."));
681         signing_buttons->Add (remake_signing, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
682
683         sizer->Add (signing_buttons, 0, wxLEFT, _border);
684
685         signing_advanced->Bind (wxEVT_BUTTON, bind (&KeysPage::signing_advanced, this));
686         remake_signing->Bind (wxEVT_BUTTON, bind(&KeysPage::remake_signing, this));
687 }
688
689
690 void
691 KeysPage::remake_signing ()
692 {
693         auto d = new MakeChainDialog (_panel, Config::instance()->signer_chain());
694
695         if (d->ShowModal () == wxID_OK) {
696                 Config::instance()->set_signer_chain(d->get());
697         }
698 }
699
700
701 void
702 KeysPage::decryption_advanced ()
703 {
704         auto c = new CertificateChainEditor (
705                 _panel, _("Decrypting KDMs"), _border,
706                 bind(&Config::set_decryption_chain, Config::instance(), _1),
707                 bind(&Config::decryption_chain, Config::instance()),
708                 bind(&KeysPage::nag_alter_decryption_chain, this)
709                 );
710
711         c->ShowModal();
712 }
713
714 void
715 KeysPage::signing_advanced ()
716 {
717         auto c = new CertificateChainEditor (
718                 _panel, _("Signing DCPs and KDMs"), _border,
719                 bind(&Config::set_signer_chain, Config::instance(), _1),
720                 bind(&Config::signer_chain, Config::instance()),
721                 bind(&do_nothing)
722                 );
723
724         c->ShowModal();
725 }
726
727 void
728 KeysPage::export_decryption_chain_and_key ()
729 {
730         auto d = new wxFileDialog (
731                 _panel, _("Select Export File"), wxEmptyString, wxEmptyString, wxT ("DOM files (*.dom)|*.dom"),
732                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT
733                 );
734         ScopeGuard sg = [d]() { d->Destroy(); };
735
736         if (d->ShowModal() != wxID_OK) {
737                 return;
738         }
739
740         boost::filesystem::path path(wx_to_std(d->GetPath()));
741         dcp::File f(path, "w");
742         if (!f) {
743                 throw OpenFileError(path, errno, OpenFileError::WRITE);
744         }
745
746         auto const chain = Config::instance()->decryption_chain()->chain();
747         f.checked_write(chain.c_str(), chain.length());
748         auto const key = Config::instance()->decryption_chain()->key();
749         DCPOMATIC_ASSERT(key);
750         f.checked_write(key->c_str(), key->length());
751 }
752
753 void
754 KeysPage::import_decryption_chain_and_key ()
755 {
756         if (NagDialog::maybe_nag (
757                     _panel,
758                     Config::NAG_IMPORT_DECRYPTION_CHAIN,
759                     _("If you continue with this operation you will no longer be able to use any DKDMs that you have created with the current certificates and key.  Also, any KDMs that have been sent to you for those certificates will become useless.  Proceed with caution!"),
760                     true
761                     )) {
762                 return;
763         }
764
765         auto d = new wxFileDialog (
766                 _panel, _("Select File To Import"), wxEmptyString, wxEmptyString, wxT ("DOM files (*.dom)|*.dom")
767                 );
768         ScopeGuard sg = [d]() { d->Destroy(); };
769
770         if (d->ShowModal() != wxID_OK) {
771                 return;
772         }
773
774         auto new_chain = make_shared<dcp::CertificateChain>();
775
776         dcp::File f(wx_to_std(d->GetPath()), "r");
777         if (!f) {
778                 throw OpenFileError(f.path(), errno, OpenFileError::WRITE);
779         }
780
781         string current;
782         while (!f.eof()) {
783                 char buffer[128];
784                 if (f.gets(buffer, 128) == 0) {
785                         break;
786                 }
787                 current += buffer;
788                 if (strncmp (buffer, "-----END CERTIFICATE-----", 25) == 0) {
789                         new_chain->add(dcp::Certificate(current));
790                         current = "";
791                 } else if (strncmp (buffer, "-----END RSA PRIVATE KEY-----", 29) == 0) {
792                         new_chain->set_key(current);
793                         current = "";
794                 }
795         }
796
797         if (new_chain->chain_valid() && new_chain->private_key_valid()) {
798                 Config::instance()->set_decryption_chain(new_chain);
799         } else {
800                 error_dialog(_panel, _("Invalid DCP-o-matic export file"));
801         }
802 }
803
804 bool
805 KeysPage::nag_alter_decryption_chain ()
806 {
807         return NagDialog::maybe_nag (
808                 _panel,
809                 Config::NAG_ALTER_DECRYPTION_CHAIN,
810                 _("If you continue with this operation you will no longer be able to use any DKDMs that you have created.  Also, any KDMs that have been sent to you will become useless.  Proceed with caution!"),
811                 true
812                 );
813 }
814
815 void
816 KeysPage::export_decryption_certificate ()
817 {
818         auto config = Config::instance();
819         wxString default_name = "dcpomatic";
820         if (!config->dcp_creator().empty()) {
821                 default_name += "_" + std_to_wx(careful_string_filter(config->dcp_creator()));
822         }
823         if (!config->dcp_issuer().empty()) {
824                 default_name += "_" + std_to_wx(careful_string_filter(config->dcp_issuer()));
825         }
826         default_name += wxT("_kdm_decryption_cert.pem");
827
828         auto d = new wxFileDialog (
829                 _panel, _("Select Certificate File"), wxEmptyString, default_name, wxT("PEM files (*.pem)|*.pem"),
830                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT
831                 );
832         ScopeGuard sg = [d]() { d->Destroy(); };
833
834         if (d->ShowModal() != wxID_OK) {
835                 return;
836         }
837
838         boost::filesystem::path path(wx_to_std(d->GetPath()));
839         if (path.extension() != ".pem") {
840                 path += ".pem";
841         }
842         dcp::File f(path, "w");
843         if (!f) {
844                 throw OpenFileError(path, errno, OpenFileError::WRITE);
845         }
846
847         auto const s = Config::instance()->decryption_chain()->leaf().certificate (true);
848         f.checked_write(s.c_str(), s.length());
849 }
850
851 wxString
852 SoundPage::GetName () const
853 {
854         return _("Sound");
855 }
856
857 void
858 SoundPage::setup ()
859 {
860         auto table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
861         _panel->GetSizer()->Add (table, 1, wxALL | wxEXPAND, _border);
862
863         int r = 0;
864
865         _sound = new CheckBox (_panel, _("Play sound via"));
866         table->Add (_sound, wxGBPosition (r, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
867         wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
868         _sound_output = new wxChoice (_panel, wxID_ANY);
869         s->Add (_sound_output, 0);
870         _sound_output_details = new wxStaticText (_panel, wxID_ANY, wxT(""));
871         s->Add (_sound_output_details, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, DCPOMATIC_SIZER_X_GAP);
872         table->Add (s, wxGBPosition(r, 1));
873         ++r;
874
875         add_label_to_sizer (table, _panel, _("Mapping"), true, wxGBPosition(r, 0));
876         _map = new AudioMappingView (_panel, _("DCP"), _("DCP"), _("Output"), _("output"));
877         _map->SetSize (-1, 400);
878         table->Add (_map, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND);
879         ++r;
880
881         _reset_to_default = new Button (_panel, _("Reset to default"));
882         table->Add (_reset_to_default, wxGBPosition(r, 1));
883         ++r;
884
885         wxFont font = _sound_output_details->GetFont();
886         font.SetStyle (wxFONTSTYLE_ITALIC);
887         font.SetPointSize (font.GetPointSize() - 1);
888         _sound_output_details->SetFont (font);
889
890         RtAudio audio (DCPOMATIC_RTAUDIO_API);
891         for (unsigned int i = 0; i < audio.getDeviceCount(); ++i) {
892                 try {
893                         auto dev = audio.getDeviceInfo (i);
894                         if (dev.probed && dev.outputChannels > 0) {
895                                 _sound_output->Append (std_to_wx (dev.name));
896                         }
897                 } catch (RtAudioError&) {
898                         /* Something went wrong so let's just ignore that device */
899                 }
900         }
901
902         _sound->bind(&SoundPage::sound_changed, this);
903         _sound_output->Bind (wxEVT_CHOICE,   bind(&SoundPage::sound_output_changed, this));
904         _map->Changed.connect (bind(&SoundPage::map_changed, this, _1));
905         _reset_to_default->Bind (wxEVT_BUTTON,   bind(&SoundPage::reset_to_default, this));
906 }
907
908 void
909 SoundPage::reset_to_default ()
910 {
911         Config::instance()->set_audio_mapping_to_default ();
912 }
913
914 void
915 SoundPage::map_changed (AudioMapping m)
916 {
917         Config::instance()->set_audio_mapping (m);
918 }
919
920 void
921 SoundPage::sound_changed ()
922 {
923         Config::instance()->set_sound (_sound->GetValue ());
924 }
925
926 void
927 SoundPage::sound_output_changed ()
928 {
929         RtAudio audio (DCPOMATIC_RTAUDIO_API);
930         auto const so = get_sound_output();
931         string default_device;
932         try {
933                 default_device = audio.getDeviceInfo(audio.getDefaultOutputDevice()).name;
934         } catch (RtAudioError&) {
935                 /* Never mind */
936         }
937         if (!so || *so == default_device) {
938                 Config::instance()->unset_sound_output ();
939         } else {
940                 Config::instance()->set_sound_output (*so);
941         }
942 }
943
944 void
945 SoundPage::config_changed ()
946 {
947         auto config = Config::instance ();
948
949         checked_set (_sound, config->sound ());
950
951         auto const current_so = get_sound_output ();
952         optional<string> configured_so;
953
954         if (config->sound_output()) {
955                 configured_so = config->sound_output().get();
956         } else {
957                 /* No configured output means we should use the default */
958                 RtAudio audio (DCPOMATIC_RTAUDIO_API);
959                 try {
960                         configured_so = audio.getDeviceInfo(audio.getDefaultOutputDevice()).name;
961                 } catch (RtAudioError&) {
962                         /* Probably no audio devices at all */
963                 }
964         }
965
966         if (configured_so && current_so != configured_so) {
967                 /* Update _sound_output with the configured value */
968                 unsigned int i = 0;
969                 while (i < _sound_output->GetCount()) {
970                         if (_sound_output->GetString(i) == std_to_wx(*configured_so)) {
971                                 _sound_output->SetSelection (i);
972                                 break;
973                         }
974                         ++i;
975                 }
976         }
977
978         RtAudio audio (DCPOMATIC_RTAUDIO_API);
979
980         map<int, wxString> apis;
981         apis[RtAudio::MACOSX_CORE]    = _("CoreAudio");
982         apis[RtAudio::WINDOWS_ASIO]   = _("ASIO");
983         apis[RtAudio::WINDOWS_DS]     = _("Direct Sound");
984         apis[RtAudio::WINDOWS_WASAPI] = _("WASAPI");
985         apis[RtAudio::UNIX_JACK]      = _("JACK");
986         apis[RtAudio::LINUX_ALSA]     = _("ALSA");
987         apis[RtAudio::LINUX_PULSE]    = _("PulseAudio");
988         apis[RtAudio::LINUX_OSS]      = _("OSS");
989         apis[RtAudio::RTAUDIO_DUMMY]  = _("Dummy");
990
991         int channels = 0;
992         if (configured_so) {
993                 for (unsigned int i = 0; i < audio.getDeviceCount(); ++i) {
994                         try {
995                                 auto info = audio.getDeviceInfo(i);
996                                 if (info.name == *configured_so && info.outputChannels > 0) {
997                                         channels = info.outputChannels;
998                                 }
999                         } catch (RtAudioError&) {
1000                                 /* Never mind */
1001                         }
1002                 }
1003         }
1004
1005         _sound_output_details->SetLabel (
1006                 wxString::Format(_("%d channels on %s"), channels, apis[audio.getCurrentApi()])
1007                 );
1008
1009         _map->set (Config::instance()->audio_mapping(channels));
1010
1011         vector<NamedChannel> input;
1012         for (int i = 0; i < MAX_DCP_AUDIO_CHANNELS; ++i) {
1013                 input.push_back (NamedChannel(short_audio_channel_name(i), i));
1014         }
1015         _map->set_input_channels (input);
1016
1017         vector<NamedChannel> output;
1018         for (int i = 0; i < channels; ++i) {
1019                 output.push_back (NamedChannel(dcp::raw_convert<string>(i), i));
1020         }
1021         _map->set_output_channels (output);
1022
1023         setup_sensitivity ();
1024 }
1025
1026 void
1027 SoundPage::setup_sensitivity ()
1028 {
1029         _sound_output->Enable (_sound->GetValue());
1030 }
1031
1032 /** @return Currently-selected preview sound output in the dialogue */
1033 optional<string>
1034 SoundPage::get_sound_output ()
1035 {
1036         int const sel = _sound_output->GetSelection ();
1037         if (sel == wxNOT_FOUND) {
1038                 return optional<string> ();
1039         }
1040
1041         return wx_to_std (_sound_output->GetString (sel));
1042 }
1043
1044
1045 LocationsPage::LocationsPage (wxSize panel_size, int border)
1046         : Page (panel_size, border)
1047 {
1048
1049 }
1050
1051 wxString
1052 LocationsPage::GetName () const
1053 {
1054         return _("Locations");
1055 }
1056
1057 #ifdef DCPOMATIC_OSX
1058 wxBitmap
1059 LocationsPage::GetLargeIcon () const
1060 {
1061         return wxBitmap(icon_path("locations"), wxBITMAP_TYPE_PNG);
1062 }
1063 #endif
1064
1065 void
1066 LocationsPage::setup ()
1067 {
1068         int r = 0;
1069
1070         auto table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
1071         _panel->GetSizer()->Add (table, 1, wxALL | wxEXPAND, _border);
1072
1073         add_label_to_sizer (table, _panel, _("Content directory"), true, wxGBPosition (r, 0));
1074         _content_directory = new wxDirPickerCtrl (_panel, wxID_ANY, wxEmptyString, wxDirSelectorPromptStr, wxDefaultPosition, wxSize (300, -1));
1075         table->Add (_content_directory, wxGBPosition (r, 1));
1076         ++r;
1077
1078         add_label_to_sizer (table, _panel, _("Playlist directory"), true, wxGBPosition (r, 0));
1079         _playlist_directory = new wxDirPickerCtrl (_panel, wxID_ANY, wxEmptyString, wxDirSelectorPromptStr, wxDefaultPosition, wxSize (300, -1));
1080         table->Add (_playlist_directory, wxGBPosition (r, 1));
1081         ++r;
1082
1083         add_label_to_sizer (table, _panel, _("KDM directory"), true, wxGBPosition (r, 0));
1084         _kdm_directory = new wxDirPickerCtrl (_panel, wxID_ANY, wxEmptyString, wxDirSelectorPromptStr, wxDefaultPosition, wxSize (300, -1));
1085         table->Add (_kdm_directory, wxGBPosition (r, 1));
1086         ++r;
1087
1088         _content_directory->Bind (wxEVT_DIRPICKER_CHANGED, bind(&LocationsPage::content_directory_changed, this));
1089         _playlist_directory->Bind (wxEVT_DIRPICKER_CHANGED, bind(&LocationsPage::playlist_directory_changed, this));
1090         _kdm_directory->Bind (wxEVT_DIRPICKER_CHANGED, bind(&LocationsPage::kdm_directory_changed, this));
1091 }
1092
1093 void
1094 LocationsPage::config_changed ()
1095 {
1096         auto config = Config::instance ();
1097
1098         if (config->player_content_directory()) {
1099                 checked_set (_content_directory, *config->player_content_directory());
1100         }
1101         if (config->player_playlist_directory()) {
1102                 checked_set (_playlist_directory, *config->player_playlist_directory());
1103         }
1104         if (config->player_kdm_directory()) {
1105                 checked_set (_kdm_directory, *config->player_kdm_directory());
1106         }
1107 }
1108
1109 void
1110 LocationsPage::content_directory_changed ()
1111 {
1112         Config::instance()->set_player_content_directory(wx_to_std(_content_directory->GetPath()));
1113 }
1114
1115 void
1116 LocationsPage::playlist_directory_changed ()
1117 {
1118         Config::instance()->set_player_playlist_directory(wx_to_std(_playlist_directory->GetPath()));
1119 }
1120
1121 void
1122 LocationsPage::kdm_directory_changed ()
1123 {
1124         Config::instance()->set_player_kdm_directory(wx_to_std(_kdm_directory->GetPath()));
1125 }