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