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