6403d7e12a25b551cc92220515883e18873859f1
[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
372         if (d->ShowModal() == wxID_OK) {
373                 try {
374                         dcp::Certificate c;
375                         string extra;
376                         try {
377                                 extra = c.read_string (dcp::file_to_string (wx_to_std (d->GetPath ())));
378                         } catch (boost::filesystem::filesystem_error& e) {
379                                 error_dialog (this, _("Could not import certificate (%s)"), d->GetPath());
380                                 d->Destroy ();
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         d->Destroy ();
410
411         update_sensitivity ();
412 }
413
414 void
415 CertificateChainEditor::remove_certificate ()
416 {
417         if (_nag_alter()) {
418                 /* Cancel was clicked */
419                 return;
420         }
421
422         int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
423         if (i == -1) {
424                 return;
425         }
426
427         _certificates->DeleteItem (i);
428         auto chain = make_shared<dcp::CertificateChain>(*_get().get());
429         chain->remove (i);
430         _set (chain);
431
432         update_sensitivity ();
433         update_certificate_list ();
434 }
435
436 void
437 CertificateChainEditor::export_certificate ()
438 {
439         int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
440         if (i == -1) {
441                 return;
442         }
443
444         auto all = _get()->root_to_leaf();
445
446         wxString default_name;
447         if (i == 0) {
448                 default_name = "root.pem";
449         } else if (i == static_cast<int>(all.size() - 1)) {
450                 default_name = "leaf.pem";
451         } else {
452                 default_name = "intermediate.pem";
453         }
454
455         auto d = new wxFileDialog(
456                 this, _("Select Certificate File"), wxEmptyString, default_name, wxT ("PEM files (*.pem)|*.pem"),
457                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT
458                 );
459
460         auto j = all.begin ();
461         for (int k = 0; k < i; ++k) {
462                 ++j;
463         }
464
465         if (d->ShowModal () == wxID_OK) {
466                 boost::filesystem::path path (wx_to_std(d->GetPath()));
467                 if (path.extension() != ".pem") {
468                         path += ".pem";
469                 }
470                 dcp::File f(path, "w");
471                 if (!f) {
472                         throw OpenFileError (path, errno, OpenFileError::WRITE);
473                 }
474
475                 string const s = j->certificate (true);
476                 f.checked_write(s.c_str(), s.length());
477         }
478         d->Destroy ();
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
489         if (d->ShowModal () == wxID_OK) {
490                 boost::filesystem::path path (wx_to_std(d->GetPath()));
491                 if (path.extension() != ".pem") {
492                         path += ".pem";
493                 }
494                 dcp::File f(path, "w");
495                 if (!f) {
496                         throw OpenFileError (path, errno, OpenFileError::WRITE);
497                 }
498
499                 auto const s = _get()->chain();
500                 f.checked_write (s.c_str(), s.length());
501         }
502
503         d->Destroy ();
504 }
505
506 void
507 CertificateChainEditor::update_certificate_list ()
508 {
509         _certificates->DeleteAllItems ();
510         size_t n = 0;
511         auto certs = _get()->root_to_leaf();
512         for (auto const& i: certs) {
513                 wxListItem item;
514                 item.SetId (n);
515                 _certificates->InsertItem (item);
516                 _certificates->SetItem (n, 1, std_to_wx (i.thumbprint ()));
517
518                 if (n == 0) {
519                         _certificates->SetItem (n, 0, _("Root"));
520                 } else if (n == (certs.size() - 1)) {
521                         _certificates->SetItem (n, 0, _("Leaf"));
522                 } else {
523                         _certificates->SetItem (n, 0, _("Intermediate"));
524                 }
525
526                 ++n;
527         }
528
529         static wxColour normal = _private_key_bad->GetForegroundColour ();
530
531         if (_get()->private_key_valid()) {
532                 _private_key_bad->Hide ();
533                 _private_key_bad->SetForegroundColour (normal);
534         } else {
535                 _private_key_bad->Show ();
536                 _private_key_bad->SetForegroundColour (wxColour (255, 0, 0));
537         }
538 }
539
540 void
541 CertificateChainEditor::remake_certificates ()
542 {
543         if (_nag_alter()) {
544                 /* Cancel was clicked */
545                 return;
546         }
547
548         auto d = new MakeChainDialog (this, _get());
549
550         if (d->ShowModal () == wxID_OK) {
551                 _set (d->get());
552                 update_certificate_list ();
553                 update_private_key ();
554         }
555
556         d->Destroy ();
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
579         if (d->ShowModal() == wxID_OK) {
580                 try {
581                         boost::filesystem::path p (wx_to_std (d->GetPath ()));
582                         if (boost::filesystem::file_size (p) > 8192) {
583                                 error_dialog (
584                                         this,
585                                         wxString::Format (_("Could not read key file; file is too long (%s)"), std_to_wx (p.string ()))
586                                         );
587                                 return;
588                         }
589
590                         auto chain = make_shared<dcp::CertificateChain>(*_get().get());
591                         chain->set_key (dcp::file_to_string (p));
592                         _set (chain);
593                         update_private_key ();
594                 } catch (std::exception& e) {
595                         error_dialog (this, _("Could not read certificate file."), std_to_wx(e.what()));
596                 }
597         }
598
599         d->Destroy ();
600
601         update_sensitivity ();
602 }
603
604 void
605 CertificateChainEditor::export_private_key ()
606 {
607         auto key = _get()->key();
608         if (!key) {
609                 return;
610         }
611
612         auto d = new wxFileDialog (
613                 this, _("Select Key File"), wxEmptyString, wxT("private_key.pem"), wxT("PEM files (*.pem)|*.pem"),
614                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT
615                 );
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         d->Destroy ();
631 }
632
633 wxString
634 KeysPage::GetName () const
635 {
636         return _("Keys");
637 }
638
639 void
640 KeysPage::setup ()
641 {
642         wxFont subheading_font (*wxNORMAL_FONT);
643         subheading_font.SetWeight (wxFONTWEIGHT_BOLD);
644
645         auto sizer = _panel->GetSizer();
646
647         {
648                 auto m = new StaticText (_panel, _("Decrypting KDMs"));
649                 m->SetFont (subheading_font);
650                 sizer->Add (m, 0, wxALL | wxEXPAND, _border);
651         }
652
653         auto kdm_buttons = new wxBoxSizer (wxVERTICAL);
654
655         auto export_decryption_certificate = new Button (_panel, _("Export KDM decryption leaf certificate..."));
656         kdm_buttons->Add (export_decryption_certificate, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
657         auto export_settings = new Button (_panel, _("Export all KDM decryption settings..."));
658         kdm_buttons->Add (export_settings, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
659         auto import_settings = new Button (_panel, _("Import all KDM decryption settings..."));
660         kdm_buttons->Add (import_settings, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
661         auto decryption_advanced = new Button (_panel, _("Advanced..."));
662         kdm_buttons->Add (decryption_advanced, 0);
663
664         sizer->Add (kdm_buttons, 0, wxLEFT, _border);
665
666         export_decryption_certificate->Bind (wxEVT_BUTTON, bind (&KeysPage::export_decryption_certificate, this));
667         export_settings->Bind (wxEVT_BUTTON, bind (&KeysPage::export_decryption_chain_and_key, this));
668         import_settings->Bind (wxEVT_BUTTON, bind (&KeysPage::import_decryption_chain_and_key, this));
669         decryption_advanced->Bind (wxEVT_BUTTON, bind (&KeysPage::decryption_advanced, this));
670
671         {
672                 auto m = new StaticText (_panel, _("Signing DCPs and KDMs"));
673                 m->SetFont (subheading_font);
674                 sizer->Add (m, 0, wxALL | wxEXPAND, _border);
675         }
676
677         auto signing_buttons = new wxBoxSizer (wxVERTICAL);
678
679         auto signing_advanced = new Button (_panel, _("Advanced..."));
680         signing_buttons->Add (signing_advanced, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
681         auto remake_signing = new Button (_panel, _("Re-make certificates and key..."));
682         signing_buttons->Add (remake_signing, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
683
684         sizer->Add (signing_buttons, 0, wxLEFT, _border);
685
686         signing_advanced->Bind (wxEVT_BUTTON, bind (&KeysPage::signing_advanced, this));
687         remake_signing->Bind (wxEVT_BUTTON, bind(&KeysPage::remake_signing, this));
688 }
689
690
691 void
692 KeysPage::remake_signing ()
693 {
694         auto d = new MakeChainDialog (_panel, Config::instance()->signer_chain());
695
696         if (d->ShowModal () == wxID_OK) {
697                 Config::instance()->set_signer_chain(d->get());
698         }
699 }
700
701
702 void
703 KeysPage::decryption_advanced ()
704 {
705         auto c = new CertificateChainEditor (
706                 _panel, _("Decrypting KDMs"), _border,
707                 bind(&Config::set_decryption_chain, Config::instance(), _1),
708                 bind(&Config::decryption_chain, Config::instance()),
709                 bind(&KeysPage::nag_alter_decryption_chain, this)
710                 );
711
712         c->ShowModal();
713 }
714
715 void
716 KeysPage::signing_advanced ()
717 {
718         auto c = new CertificateChainEditor (
719                 _panel, _("Signing DCPs and KDMs"), _border,
720                 bind(&Config::set_signer_chain, Config::instance(), _1),
721                 bind(&Config::signer_chain, Config::instance()),
722                 bind(&do_nothing)
723                 );
724
725         c->ShowModal();
726 }
727
728 void
729 KeysPage::export_decryption_chain_and_key ()
730 {
731         auto d = new wxFileDialog (
732                 _panel, _("Select Export File"), wxEmptyString, wxEmptyString, wxT ("DOM files (*.dom)|*.dom"),
733                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT
734                 );
735
736         if (d->ShowModal () == wxID_OK) {
737                 boost::filesystem::path path (wx_to_std(d->GetPath()));
738                 dcp::File f(path, "w");
739                 if (!f) {
740                         throw OpenFileError (path, errno, OpenFileError::WRITE);
741                 }
742
743                 auto const chain = Config::instance()->decryption_chain()->chain();
744                 f.checked_write (chain.c_str(), chain.length());
745                 auto const key = Config::instance()->decryption_chain()->key();
746                 DCPOMATIC_ASSERT (key);
747                 f.checked_write(key->c_str(), key->length());
748         }
749         d->Destroy ();
750
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
769         if (d->ShowModal () == wxID_OK) {
770                 auto new_chain = make_shared<dcp::CertificateChain>();
771
772                 dcp::File f(wx_to_std(d->GetPath()), "r");
773                 if (!f) {
774                         throw OpenFileError (f.path(), errno, OpenFileError::WRITE);
775                 }
776
777                 string current;
778                 while (!f.eof()) {
779                         char buffer[128];
780                         if (f.gets(buffer, 128) == 0) {
781                                 break;
782                         }
783                         current += buffer;
784                         if (strncmp (buffer, "-----END CERTIFICATE-----", 25) == 0) {
785                                 new_chain->add (dcp::Certificate (current));
786                                 current = "";
787                         } else if (strncmp (buffer, "-----END RSA PRIVATE KEY-----", 29) == 0) {
788                                 new_chain->set_key (current);
789                                 current = "";
790                         }
791                 }
792
793                 if (new_chain->chain_valid() && new_chain->private_key_valid()) {
794                         Config::instance()->set_decryption_chain (new_chain);
795                 } else {
796                         error_dialog (_panel, _("Invalid DCP-o-matic export file"));
797                 }
798         }
799         d->Destroy ();
800 }
801
802 bool
803 KeysPage::nag_alter_decryption_chain ()
804 {
805         return NagDialog::maybe_nag (
806                 _panel,
807                 Config::NAG_ALTER_DECRYPTION_CHAIN,
808                 _("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!"),
809                 true
810                 );
811 }
812
813 void
814 KeysPage::export_decryption_certificate ()
815 {
816         auto config = Config::instance();
817         wxString default_name = "dcpomatic";
818         if (!config->dcp_creator().empty()) {
819                 default_name += "_" + std_to_wx(careful_string_filter(config->dcp_creator()));
820         }
821         if (!config->dcp_issuer().empty()) {
822                 default_name += "_" + std_to_wx(careful_string_filter(config->dcp_issuer()));
823         }
824         default_name += wxT("_kdm_decryption_cert.pem");
825
826         auto d = new wxFileDialog (
827                 _panel, _("Select Certificate File"), wxEmptyString, default_name, wxT("PEM files (*.pem)|*.pem"),
828                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT
829                 );
830
831         if (d->ShowModal () == wxID_OK) {
832                 boost::filesystem::path path (wx_to_std(d->GetPath()));
833                 if (path.extension() != ".pem") {
834                         path += ".pem";
835                 }
836                 dcp::File f(path, "w");
837                 if (!f) {
838                         throw OpenFileError (path, errno, OpenFileError::WRITE);
839                 }
840
841                 auto const s = Config::instance()->decryption_chain()->leaf().certificate (true);
842                 f.checked_write(s.c_str(), s.length());
843         }
844
845         d->Destroy ();
846 }
847
848 wxString
849 SoundPage::GetName () const
850 {
851         return _("Sound");
852 }
853
854 void
855 SoundPage::setup ()
856 {
857         auto table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
858         _panel->GetSizer()->Add (table, 1, wxALL | wxEXPAND, _border);
859
860         int r = 0;
861
862         _sound = new CheckBox (_panel, _("Play sound via"));
863         table->Add (_sound, wxGBPosition (r, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
864         wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
865         _sound_output = new wxChoice (_panel, wxID_ANY);
866         s->Add (_sound_output, 0);
867         _sound_output_details = new wxStaticText (_panel, wxID_ANY, wxT(""));
868         s->Add (_sound_output_details, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, DCPOMATIC_SIZER_X_GAP);
869         table->Add (s, wxGBPosition(r, 1));
870         ++r;
871
872         add_label_to_sizer (table, _panel, _("Mapping"), true, wxGBPosition(r, 0));
873         _map = new AudioMappingView (_panel, _("DCP"), _("DCP"), _("Output"), _("output"));
874         _map->SetSize (-1, 400);
875         table->Add (_map, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND);
876         ++r;
877
878         _reset_to_default = new Button (_panel, _("Reset to default"));
879         table->Add (_reset_to_default, wxGBPosition(r, 1));
880         ++r;
881
882         wxFont font = _sound_output_details->GetFont();
883         font.SetStyle (wxFONTSTYLE_ITALIC);
884         font.SetPointSize (font.GetPointSize() - 1);
885         _sound_output_details->SetFont (font);
886
887         RtAudio audio (DCPOMATIC_RTAUDIO_API);
888         for (unsigned int i = 0; i < audio.getDeviceCount(); ++i) {
889                 try {
890                         auto dev = audio.getDeviceInfo (i);
891                         if (dev.probed && dev.outputChannels > 0) {
892                                 _sound_output->Append (std_to_wx (dev.name));
893                         }
894                 } catch (RtAudioError&) {
895                         /* Something went wrong so let's just ignore that device */
896                 }
897         }
898
899         _sound->bind(&SoundPage::sound_changed, this);
900         _sound_output->Bind (wxEVT_CHOICE,   bind(&SoundPage::sound_output_changed, this));
901         _map->Changed.connect (bind(&SoundPage::map_changed, this, _1));
902         _reset_to_default->Bind (wxEVT_BUTTON,   bind(&SoundPage::reset_to_default, this));
903 }
904
905 void
906 SoundPage::reset_to_default ()
907 {
908         Config::instance()->set_audio_mapping_to_default ();
909 }
910
911 void
912 SoundPage::map_changed (AudioMapping m)
913 {
914         Config::instance()->set_audio_mapping (m);
915 }
916
917 void
918 SoundPage::sound_changed ()
919 {
920         Config::instance()->set_sound (_sound->GetValue ());
921 }
922
923 void
924 SoundPage::sound_output_changed ()
925 {
926         RtAudio audio (DCPOMATIC_RTAUDIO_API);
927         auto const so = get_sound_output();
928         string default_device;
929         try {
930                 default_device = audio.getDeviceInfo(audio.getDefaultOutputDevice()).name;
931         } catch (RtAudioError&) {
932                 /* Never mind */
933         }
934         if (!so || *so == default_device) {
935                 Config::instance()->unset_sound_output ();
936         } else {
937                 Config::instance()->set_sound_output (*so);
938         }
939 }
940
941 void
942 SoundPage::config_changed ()
943 {
944         auto config = Config::instance ();
945
946         checked_set (_sound, config->sound ());
947
948         auto const current_so = get_sound_output ();
949         optional<string> configured_so;
950
951         if (config->sound_output()) {
952                 configured_so = config->sound_output().get();
953         } else {
954                 /* No configured output means we should use the default */
955                 RtAudio audio (DCPOMATIC_RTAUDIO_API);
956                 try {
957                         configured_so = audio.getDeviceInfo(audio.getDefaultOutputDevice()).name;
958                 } catch (RtAudioError&) {
959                         /* Probably no audio devices at all */
960                 }
961         }
962
963         if (configured_so && current_so != configured_so) {
964                 /* Update _sound_output with the configured value */
965                 unsigned int i = 0;
966                 while (i < _sound_output->GetCount()) {
967                         if (_sound_output->GetString(i) == std_to_wx(*configured_so)) {
968                                 _sound_output->SetSelection (i);
969                                 break;
970                         }
971                         ++i;
972                 }
973         }
974
975         RtAudio audio (DCPOMATIC_RTAUDIO_API);
976
977         map<int, wxString> apis;
978         apis[RtAudio::MACOSX_CORE]    = _("CoreAudio");
979         apis[RtAudio::WINDOWS_ASIO]   = _("ASIO");
980         apis[RtAudio::WINDOWS_DS]     = _("Direct Sound");
981         apis[RtAudio::WINDOWS_WASAPI] = _("WASAPI");
982         apis[RtAudio::UNIX_JACK]      = _("JACK");
983         apis[RtAudio::LINUX_ALSA]     = _("ALSA");
984         apis[RtAudio::LINUX_PULSE]    = _("PulseAudio");
985         apis[RtAudio::LINUX_OSS]      = _("OSS");
986         apis[RtAudio::RTAUDIO_DUMMY]  = _("Dummy");
987
988         int channels = 0;
989         if (configured_so) {
990                 for (unsigned int i = 0; i < audio.getDeviceCount(); ++i) {
991                         try {
992                                 auto info = audio.getDeviceInfo(i);
993                                 if (info.name == *configured_so && info.outputChannels > 0) {
994                                         channels = info.outputChannels;
995                                 }
996                         } catch (RtAudioError&) {
997                                 /* Never mind */
998                         }
999                 }
1000         }
1001
1002         _sound_output_details->SetLabel (
1003                 wxString::Format(_("%d channels on %s"), channels, apis[audio.getCurrentApi()])
1004                 );
1005
1006         _map->set (Config::instance()->audio_mapping(channels));
1007
1008         vector<NamedChannel> input;
1009         for (int i = 0; i < MAX_DCP_AUDIO_CHANNELS; ++i) {
1010                 input.push_back (NamedChannel(short_audio_channel_name(i), i));
1011         }
1012         _map->set_input_channels (input);
1013
1014         vector<NamedChannel> output;
1015         for (int i = 0; i < channels; ++i) {
1016                 output.push_back (NamedChannel(dcp::raw_convert<string>(i), i));
1017         }
1018         _map->set_output_channels (output);
1019
1020         setup_sensitivity ();
1021 }
1022
1023 void
1024 SoundPage::setup_sensitivity ()
1025 {
1026         _sound_output->Enable (_sound->GetValue());
1027 }
1028
1029 /** @return Currently-selected preview sound output in the dialogue */
1030 optional<string>
1031 SoundPage::get_sound_output ()
1032 {
1033         int const sel = _sound_output->GetSelection ();
1034         if (sel == wxNOT_FOUND) {
1035                 return optional<string> ();
1036         }
1037
1038         return wx_to_std (_sound_output->GetString (sel));
1039 }
1040
1041
1042 LocationsPage::LocationsPage (wxSize panel_size, int border)
1043         : Page (panel_size, border)
1044 {
1045
1046 }
1047
1048 wxString
1049 LocationsPage::GetName () const
1050 {
1051         return _("Locations");
1052 }
1053
1054 #ifdef DCPOMATIC_OSX
1055 wxBitmap
1056 LocationsPage::GetLargeIcon () const
1057 {
1058         return wxBitmap(icon_path("locations"), wxBITMAP_TYPE_PNG);
1059 }
1060 #endif
1061
1062 void
1063 LocationsPage::setup ()
1064 {
1065         int r = 0;
1066
1067         auto table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
1068         _panel->GetSizer()->Add (table, 1, wxALL | wxEXPAND, _border);
1069
1070         add_label_to_sizer (table, _panel, _("Content directory"), true, wxGBPosition (r, 0));
1071         _content_directory = new wxDirPickerCtrl (_panel, wxID_ANY, wxEmptyString, wxDirSelectorPromptStr, wxDefaultPosition, wxSize (300, -1));
1072         table->Add (_content_directory, wxGBPosition (r, 1));
1073         ++r;
1074
1075         add_label_to_sizer (table, _panel, _("Playlist directory"), true, wxGBPosition (r, 0));
1076         _playlist_directory = new wxDirPickerCtrl (_panel, wxID_ANY, wxEmptyString, wxDirSelectorPromptStr, wxDefaultPosition, wxSize (300, -1));
1077         table->Add (_playlist_directory, wxGBPosition (r, 1));
1078         ++r;
1079
1080         add_label_to_sizer (table, _panel, _("KDM directory"), true, wxGBPosition (r, 0));
1081         _kdm_directory = new wxDirPickerCtrl (_panel, wxID_ANY, wxEmptyString, wxDirSelectorPromptStr, wxDefaultPosition, wxSize (300, -1));
1082         table->Add (_kdm_directory, wxGBPosition (r, 1));
1083         ++r;
1084
1085         _content_directory->Bind (wxEVT_DIRPICKER_CHANGED, bind(&LocationsPage::content_directory_changed, this));
1086         _playlist_directory->Bind (wxEVT_DIRPICKER_CHANGED, bind(&LocationsPage::playlist_directory_changed, this));
1087         _kdm_directory->Bind (wxEVT_DIRPICKER_CHANGED, bind(&LocationsPage::kdm_directory_changed, this));
1088 }
1089
1090 void
1091 LocationsPage::config_changed ()
1092 {
1093         auto config = Config::instance ();
1094
1095         if (config->player_content_directory()) {
1096                 checked_set (_content_directory, *config->player_content_directory());
1097         }
1098         if (config->player_playlist_directory()) {
1099                 checked_set (_playlist_directory, *config->player_playlist_directory());
1100         }
1101         if (config->player_kdm_directory()) {
1102                 checked_set (_kdm_directory, *config->player_kdm_directory());
1103         }
1104 }
1105
1106 void
1107 LocationsPage::content_directory_changed ()
1108 {
1109         Config::instance()->set_player_content_directory(wx_to_std(_content_directory->GetPath()));
1110 }
1111
1112 void
1113 LocationsPage::playlist_directory_changed ()
1114 {
1115         Config::instance()->set_player_playlist_directory(wx_to_std(_playlist_directory->GetPath()));
1116 }
1117
1118 void
1119 LocationsPage::kdm_directory_changed ()
1120 {
1121         Config::instance()->set_player_kdm_directory(wx_to_std(_kdm_directory->GetPath()));
1122 }