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