f91ebe670244d9de702534459cc59bb99cd2dd84
[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 #include <iostream>
29
30 using std::string;
31 using std::vector;
32 using std::pair;
33 using std::make_pair;
34 using std::map;
35 using std::make_shared;
36 using boost::bind;
37 using boost::optional;
38 using std::shared_ptr;
39 using std::function;
40 #if BOOST_VERSION >= 106100
41 using namespace boost::placeholders;
42 #endif
43
44 static
45 bool
46 do_nothing ()
47 {
48         return false;
49 }
50
51 Page::Page (wxSize panel_size, int border)
52         : _border (border)
53         , _panel (0)
54         , _panel_size (panel_size)
55         , _window_exists (false)
56 {
57         _config_connection = Config::instance()->Changed.connect (bind (&Page::config_changed_wrapper, this));
58 }
59
60
61 wxWindow*
62 Page::CreateWindow (wxWindow* parent)
63 {
64         return create_window (parent);
65 }
66
67
68 wxWindow*
69 Page::create_window (wxWindow* parent)
70 {
71         _panel = new wxPanel (parent, wxID_ANY, wxDefaultPosition, _panel_size);
72         auto s = new wxBoxSizer (wxVERTICAL);
73         _panel->SetSizer (s);
74
75         setup ();
76         _window_exists = true;
77         config_changed ();
78
79         _panel->Bind (wxEVT_DESTROY, bind (&Page::window_destroyed, this));
80
81         return _panel;
82 }
83
84 void
85 Page::config_changed_wrapper ()
86 {
87         if (_window_exists) {
88                 config_changed ();
89         }
90 }
91
92 void
93 Page::window_destroyed ()
94 {
95         _window_exists = false;
96 }
97
98
99 GeneralPage::GeneralPage (wxSize panel_size, int border)
100         : Page (panel_size, border)
101 {
102
103 }
104
105
106 wxString
107 GeneralPage::GetName () const
108 {
109         return _("General");
110 }
111
112
113 void
114 GeneralPage::add_language_controls (wxGridBagSizer* table, int& r)
115 {
116         _set_language = new CheckBox (_panel, _("Set language"));
117         table->Add (_set_language, wxGBPosition (r, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
118         _language = new wxChoice (_panel, wxID_ANY);
119         vector<pair<string, string>> languages;
120         languages.push_back (make_pair("Čeština", "cs_CZ"));
121         languages.push_back (make_pair("汉语/漢語", "zh_CN"));
122         languages.push_back (make_pair("Dansk", "da_DK"));
123         languages.push_back (make_pair("Deutsch", "de_DE"));
124         languages.push_back (make_pair("English", "en_GB"));
125         languages.push_back (make_pair("Español", "es_ES"));
126         languages.push_back (make_pair("Français", "fr_FR"));
127         languages.push_back (make_pair("Italiano", "it_IT"));
128         languages.push_back (make_pair("Nederlands", "nl_NL"));
129         languages.push_back (make_pair("Русский", "ru_RU"));
130         languages.push_back (make_pair("Polski", "pl_PL"));
131         languages.push_back (make_pair("Português europeu", "pt_PT"));
132         languages.push_back (make_pair("Português do Brasil", "pt_BR"));
133         languages.push_back (make_pair("Svenska", "sv_SE"));
134         languages.push_back (make_pair("Slovenský jazyk", "sk_SK"));
135         // languages.push_back (make_pair("Türkçe", "tr_TR"));
136         languages.push_back (make_pair("українська мова", "uk_UA"));
137         checked_set (_language, languages);
138         table->Add (_language, wxGBPosition (r, 1));
139         ++r;
140
141         auto restart = add_label_to_sizer (
142                 table, _panel, _("(restart DCP-o-matic to see language changes)"), false, wxGBPosition (r, 0), wxGBSpan (1, 2)
143                 );
144         wxFont font = restart->GetFont();
145         font.SetStyle (wxFONTSTYLE_ITALIC);
146         font.SetPointSize (font.GetPointSize() - 1);
147         restart->SetFont (font);
148         ++r;
149
150         _set_language->Bind (wxEVT_CHECKBOX, bind (&GeneralPage::set_language_changed, this));
151         _language->Bind     (wxEVT_CHOICE,   bind (&GeneralPage::language_changed,     this));
152 }
153
154 void
155 GeneralPage::add_update_controls (wxGridBagSizer* table, int& r)
156 {
157         _check_for_updates = new CheckBox (_panel, _("Check for updates on startup"));
158         table->Add (_check_for_updates, wxGBPosition (r, 0), wxGBSpan (1, 2));
159         ++r;
160
161         _check_for_test_updates = new CheckBox (_panel, _("Check for testing updates on startup"));
162         table->Add (_check_for_test_updates, wxGBPosition (r, 0), wxGBSpan (1, 2));
163         ++r;
164
165         _check_for_updates->Bind (wxEVT_CHECKBOX, bind (&GeneralPage::check_for_updates_changed, this));
166         _check_for_test_updates->Bind (wxEVT_CHECKBOX, bind (&GeneralPage::check_for_test_updates_changed, this));
167 }
168
169 void
170 GeneralPage::config_changed ()
171 {
172         auto config = Config::instance ();
173
174         checked_set (_set_language, static_cast<bool>(config->language()));
175
176         /* Backwards compatibility of config file */
177
178         map<string, string> compat_map;
179         compat_map["fr"] = "fr_FR";
180         compat_map["it"] = "it_IT";
181         compat_map["es"] = "es_ES";
182         compat_map["sv"] = "sv_SE";
183         compat_map["de"] = "de_DE";
184         compat_map["nl"] = "nl_NL";
185         compat_map["ru"] = "ru_RU";
186         compat_map["pl"] = "pl_PL";
187         compat_map["da"] = "da_DK";
188         compat_map["pt"] = "pt_PT";
189         compat_map["sk"] = "sk_SK";
190         compat_map["cs"] = "cs_CZ";
191         compat_map["uk"] = "uk_UA";
192
193         auto lang = config->language().get_value_or("en_GB");
194         if (compat_map.find(lang) != compat_map.end ()) {
195                 lang = compat_map[lang];
196         }
197
198         checked_set (_language, lang);
199
200         checked_set (_check_for_updates, config->check_for_updates ());
201         checked_set (_check_for_test_updates, config->check_for_test_updates ());
202
203         setup_sensitivity ();
204 }
205
206 void
207 GeneralPage::setup_sensitivity ()
208 {
209         _language->Enable (_set_language->GetValue ());
210         _check_for_test_updates->Enable (_check_for_updates->GetValue ());
211 }
212
213 void
214 GeneralPage::set_language_changed ()
215 {
216         setup_sensitivity ();
217         if (_set_language->GetValue ()) {
218                 language_changed ();
219         } else {
220                 Config::instance()->unset_language ();
221         }
222 }
223
224 void
225 GeneralPage::language_changed ()
226 {
227         int const sel = _language->GetSelection ();
228         if (sel != -1) {
229                 Config::instance()->set_language (string_client_data (_language->GetClientObject (sel)));
230         } else {
231                 Config::instance()->unset_language ();
232         }
233 }
234
235 void
236 GeneralPage::check_for_updates_changed ()
237 {
238         Config::instance()->set_check_for_updates (_check_for_updates->GetValue ());
239 }
240
241 void
242 GeneralPage::check_for_test_updates_changed ()
243 {
244         Config::instance()->set_check_for_test_updates (_check_for_test_updates->GetValue ());
245 }
246
247 CertificateChainEditor::CertificateChainEditor (
248         wxWindow* parent,
249         wxString title,
250         int border,
251         function<void (shared_ptr<dcp::CertificateChain>)> set,
252         function<shared_ptr<const dcp::CertificateChain> (void)> get,
253         function<bool (void)> nag_alter
254         )
255         : wxDialog (parent, wxID_ANY, title)
256         , _set (set)
257         , _get (get)
258         , _nag_alter (nag_alter)
259 {
260         _sizer = new wxBoxSizer (wxVERTICAL);
261
262         auto certificates_sizer = new wxBoxSizer (wxHORIZONTAL);
263         _sizer->Add (certificates_sizer, 0, wxALL, border);
264
265         _certificates = new wxListCtrl (this, wxID_ANY, wxDefaultPosition, wxSize (440, 150), wxLC_REPORT | wxLC_SINGLE_SEL);
266
267         {
268                 wxListItem ip;
269                 ip.SetId (0);
270                 ip.SetText (_("Type"));
271                 ip.SetWidth (100);
272                 _certificates->InsertColumn (0, ip);
273         }
274
275         {
276                 wxListItem ip;
277                 ip.SetId (1);
278                 ip.SetText (_("Thumbprint"));
279                 ip.SetWidth (340);
280
281                 wxFont font = ip.GetFont ();
282                 font.SetFamily (wxFONTFAMILY_TELETYPE);
283                 ip.SetFont (font);
284
285                 _certificates->InsertColumn (1, ip);
286         }
287
288         certificates_sizer->Add (_certificates, 1, wxEXPAND);
289
290         {
291                 auto s = new wxBoxSizer (wxVERTICAL);
292                 _add_certificate = new Button (this, _("Add..."));
293                 s->Add (_add_certificate, 1, wxTOP | wxBOTTOM | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
294                 _remove_certificate = new Button (this, _("Remove"));
295                 s->Add (_remove_certificate, 1, wxTOP | wxBOTTOM | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
296                 _export_certificate = new Button (this, _("Export certificate..."));
297                 s->Add (_export_certificate, 1, wxTOP | wxBOTTOM | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
298                 _export_chain = new Button (this, _("Export chain..."));
299                 s->Add (_export_chain, 1, wxTOP | wxBOTTOM | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
300                 certificates_sizer->Add (s, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP);
301         }
302
303         auto table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
304         _sizer->Add (table, 1, wxALL | wxEXPAND, border);
305         int r = 0;
306
307         add_label_to_sizer (table, this, _("Leaf private key"), true, wxGBPosition (r, 0));
308         _private_key = new StaticText (this, wxT(""));
309         wxFont font = _private_key->GetFont ();
310         font.SetFamily (wxFONTFAMILY_TELETYPE);
311         _private_key->SetFont (font);
312         table->Add (_private_key, wxGBPosition (r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
313         _import_private_key = new Button (this, _("Import..."));
314         table->Add (_import_private_key, wxGBPosition (r, 2));
315         _export_private_key = new Button (this, _("Export..."));
316         table->Add (_export_private_key, wxGBPosition (r, 3));
317         ++r;
318
319         _button_sizer = new wxBoxSizer (wxHORIZONTAL);
320         _remake_certificates = new Button (this, _("Re-make certificates and key..."));
321         _button_sizer->Add (_remake_certificates, 1, wxRIGHT, border);
322         table->Add (_button_sizer, wxGBPosition (r, 0), wxGBSpan (1, 4));
323         ++r;
324
325         _private_key_bad = new StaticText (this, _("Leaf private key does not match leaf certificate!"));
326         font = *wxSMALL_FONT;
327         font.SetWeight (wxFONTWEIGHT_BOLD);
328         _private_key_bad->SetFont (font);
329         table->Add (_private_key_bad, wxGBPosition (r, 0), wxGBSpan (1, 3));
330         ++r;
331
332         _add_certificate->Bind     (wxEVT_BUTTON,       bind (&CertificateChainEditor::add_certificate, this));
333         _remove_certificate->Bind  (wxEVT_BUTTON,       bind (&CertificateChainEditor::remove_certificate, this));
334         _export_certificate->Bind  (wxEVT_BUTTON,       bind (&CertificateChainEditor::export_certificate, this));
335         _certificates->Bind        (wxEVT_LIST_ITEM_SELECTED,   bind (&CertificateChainEditor::update_sensitivity, this));
336         _certificates->Bind        (wxEVT_LIST_ITEM_DESELECTED, bind (&CertificateChainEditor::update_sensitivity, this));
337         _remake_certificates->Bind (wxEVT_BUTTON,       bind (&CertificateChainEditor::remake_certificates, this));
338         _export_chain->Bind        (wxEVT_BUTTON,       bind (&CertificateChainEditor::export_chain, this));
339         _import_private_key->Bind  (wxEVT_BUTTON,       bind (&CertificateChainEditor::import_private_key, this));
340         _export_private_key->Bind  (wxEVT_BUTTON,       bind (&CertificateChainEditor::export_private_key, this));
341
342         auto buttons = CreateSeparatedButtonSizer (wxCLOSE);
343         if (buttons) {
344                 _sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
345         }
346
347         SetSizerAndFit (_sizer);
348
349         update_certificate_list ();
350         update_private_key ();
351         update_sensitivity ();
352 }
353
354 void
355 CertificateChainEditor::add_button (wxWindow* button)
356 {
357         _button_sizer->Add (button, 0, wxLEFT | wxRIGHT, DCPOMATIC_SIZER_X_GAP);
358         _sizer->Layout ();
359 }
360
361 void
362 CertificateChainEditor::add_certificate ()
363 {
364         auto d = new wxFileDialog (this, _("Select Certificate File"));
365
366         if (d->ShowModal() == wxID_OK) {
367                 try {
368                         dcp::Certificate c;
369                         string extra;
370                         try {
371                                 extra = c.read_string (dcp::file_to_string (wx_to_std (d->GetPath ())));
372                         } catch (boost::filesystem::filesystem_error& e) {
373                                 error_dialog (this, _("Could not import certificate (%s)"), d->GetPath());
374                                 d->Destroy ();
375                                 return;
376                         }
377
378                         if (!extra.empty ()) {
379                                 message_dialog (
380                                         this,
381                                         _("This file contains other certificates (or other data) after its first certificate. "
382                                           "Only the first certificate will be used.")
383                                         );
384                         }
385                         auto chain = make_shared<dcp::CertificateChain>(*_get().get());
386                         chain->add (c);
387                         if (!chain->chain_valid ()) {
388                                 error_dialog (
389                                         this,
390                                         _("Adding this certificate would make the chain inconsistent, so it will not be added. "
391                                           "Add certificates in order from root to intermediate to leaf.")
392                                         );
393                                 chain->remove (c);
394                         } else {
395                                 _set (chain);
396                                 update_certificate_list ();
397                         }
398                 } catch (dcp::MiscError& e) {
399                         error_dialog (this, _("Could not read certificate file."), std_to_wx(e.what()));
400                 }
401         }
402
403         d->Destroy ();
404
405         update_sensitivity ();
406 }
407
408 void
409 CertificateChainEditor::remove_certificate ()
410 {
411         if (_nag_alter()) {
412                 /* Cancel was clicked */
413                 return;
414         }
415
416         int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
417         if (i == -1) {
418                 return;
419         }
420
421         _certificates->DeleteItem (i);
422         auto chain = make_shared<dcp::CertificateChain>(*_get().get());
423         chain->remove (i);
424         _set (chain);
425
426         update_sensitivity ();
427         update_certificate_list ();
428 }
429
430 void
431 CertificateChainEditor::export_certificate ()
432 {
433         int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
434         if (i == -1) {
435                 return;
436         }
437
438         auto all = _get()->root_to_leaf();
439
440         wxString default_name;
441         if (i == 0) {
442                 default_name = "root.pem";
443         } else if (i == static_cast<int>(all.size() - 1)) {
444                 default_name = "leaf.pem";
445         } else {
446                 default_name = "intermediate.pem";
447         }
448
449         auto d = new wxFileDialog(
450                 this, _("Select Certificate File"), wxEmptyString, default_name, wxT ("PEM files (*.pem)|*.pem"),
451                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT
452                 );
453
454         auto j = all.begin ();
455         for (int k = 0; k < i; ++k) {
456                 ++j;
457         }
458
459         if (d->ShowModal () == wxID_OK) {
460                 boost::filesystem::path path (wx_to_std(d->GetPath()));
461                 if (path.extension() != ".pem") {
462                         path += ".pem";
463                 }
464                 auto f = fopen_boost (path, "w");
465                 if (!f) {
466                         throw OpenFileError (path, errno, OpenFileError::WRITE);
467                 }
468
469                 string const s = j->certificate (true);
470                 checked_fwrite (s.c_str(), s.length(), f, path);
471                 fclose (f);
472         }
473         d->Destroy ();
474 }
475
476 void
477 CertificateChainEditor::export_chain ()
478 {
479         auto d = new wxFileDialog (
480                 this, _("Select Chain File"), wxEmptyString, wxT("certificate_chain.pem"), wxT("PEM files (*.pem)|*.pem"),
481                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT
482                 );
483
484         if (d->ShowModal () == wxID_OK) {
485                 boost::filesystem::path path (wx_to_std(d->GetPath()));
486                 if (path.extension() != ".pem") {
487                         path += ".pem";
488                 }
489                 auto f = fopen_boost (path, "w");
490                 if (!f) {
491                         throw OpenFileError (path, errno, OpenFileError::WRITE);
492                 }
493
494                 auto const s = _get()->chain();
495                 checked_fwrite (s.c_str(), s.length(), f, path);
496                 fclose (f);
497         }
498
499         d->Destroy ();
500 }
501
502 void
503 CertificateChainEditor::update_certificate_list ()
504 {
505         _certificates->DeleteAllItems ();
506         size_t n = 0;
507         auto certs = _get()->root_to_leaf();
508         for (auto const& i: certs) {
509                 wxListItem item;
510                 item.SetId (n);
511                 _certificates->InsertItem (item);
512                 _certificates->SetItem (n, 1, std_to_wx (i.thumbprint ()));
513
514                 if (n == 0) {
515                         _certificates->SetItem (n, 0, _("Root"));
516                 } else if (n == (certs.size() - 1)) {
517                         _certificates->SetItem (n, 0, _("Leaf"));
518                 } else {
519                         _certificates->SetItem (n, 0, _("Intermediate"));
520                 }
521
522                 ++n;
523         }
524
525         static wxColour normal = _private_key_bad->GetForegroundColour ();
526
527         if (_get()->private_key_valid()) {
528                 _private_key_bad->Hide ();
529                 _private_key_bad->SetForegroundColour (normal);
530         } else {
531                 _private_key_bad->Show ();
532                 _private_key_bad->SetForegroundColour (wxColour (255, 0, 0));
533         }
534 }
535
536 void
537 CertificateChainEditor::remake_certificates ()
538 {
539         if (_nag_alter()) {
540                 /* Cancel was clicked */
541                 return;
542         }
543
544         auto d = new MakeChainDialog (this, _get());
545
546         if (d->ShowModal () == wxID_OK) {
547                 _set (d->get());
548                 update_certificate_list ();
549                 update_private_key ();
550         }
551
552         d->Destroy ();
553 }
554
555 void
556 CertificateChainEditor::update_sensitivity ()
557 {
558         /* We can only remove the leaf certificate */
559         _remove_certificate->Enable (_certificates->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED) == (_certificates->GetItemCount() - 1));
560         _export_certificate->Enable (_certificates->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED) != -1);
561 }
562
563 void
564 CertificateChainEditor::update_private_key ()
565 {
566         checked_set (_private_key, dcp::private_key_fingerprint (_get()->key().get()));
567         _sizer->Layout ();
568 }
569
570 void
571 CertificateChainEditor::import_private_key ()
572 {
573         auto d = new wxFileDialog (this, _("Select Key File"));
574
575         if (d->ShowModal() == wxID_OK) {
576                 try {
577                         boost::filesystem::path p (wx_to_std (d->GetPath ()));
578                         if (boost::filesystem::file_size (p) > 8192) {
579                                 error_dialog (
580                                         this,
581                                         wxString::Format (_("Could not read key file; file is too long (%s)"), std_to_wx (p.string ()))
582                                         );
583                                 return;
584                         }
585
586                         auto chain = make_shared<dcp::CertificateChain>(*_get().get());
587                         chain->set_key (dcp::file_to_string (p));
588                         _set (chain);
589                         update_private_key ();
590                 } catch (std::exception& e) {
591                         error_dialog (this, _("Could not read certificate file."), std_to_wx(e.what()));
592                 }
593         }
594
595         d->Destroy ();
596
597         update_sensitivity ();
598 }
599
600 void
601 CertificateChainEditor::export_private_key ()
602 {
603         auto key = _get()->key();
604         if (!key) {
605                 return;
606         }
607
608         auto d = new wxFileDialog (
609                 this, _("Select Key File"), wxEmptyString, wxT("private_key.pem"), wxT("PEM files (*.pem)|*.pem"),
610                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT
611                 );
612
613         if (d->ShowModal () == wxID_OK) {
614                 boost::filesystem::path path (wx_to_std(d->GetPath()));
615                 if (path.extension() != ".pem") {
616                         path += ".pem";
617                 }
618                 auto f = fopen_boost (path, "w");
619                 if (!f) {
620                         throw OpenFileError (path, errno, OpenFileError::WRITE);
621                 }
622
623                 auto const s = _get()->key().get ();
624                 checked_fwrite (s.c_str(), s.length(), f, path);
625                 fclose (f);
626         }
627         d->Destroy ();
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 = new 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                 boost::filesystem::path path (wx_to_std(d->GetPath()));
735                 auto f = fopen_boost (path, "w");
736                 if (!f) {
737                         throw OpenFileError (path, errno, OpenFileError::WRITE);
738                 }
739
740                 auto const chain = Config::instance()->decryption_chain()->chain();
741                 checked_fwrite (chain.c_str(), chain.length(), f, path);
742                 optional<string> const key = Config::instance()->decryption_chain()->key();
743                 DCPOMATIC_ASSERT (key);
744                 checked_fwrite (key->c_str(), key->length(), f, path);
745                 fclose (f);
746         }
747         d->Destroy ();
748
749 }
750
751 void
752 KeysPage::import_decryption_chain_and_key ()
753 {
754         if (NagDialog::maybe_nag (
755                     _panel,
756                     Config::NAG_IMPORT_DECRYPTION_CHAIN,
757                     _("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!"),
758                     true
759                     )) {
760                 return;
761         }
762
763         auto d = new wxFileDialog (
764                 _panel, _("Select File To Import"), wxEmptyString, wxEmptyString, wxT ("DOM files (*.dom)|*.dom")
765                 );
766
767         if (d->ShowModal () == wxID_OK) {
768                 auto new_chain = make_shared<dcp::CertificateChain>();
769
770                 FILE* f = fopen_boost (wx_to_std (d->GetPath ()), "r");
771                 if (!f) {
772                         throw OpenFileError (wx_to_std (d->GetPath ()), errno, OpenFileError::WRITE);
773                 }
774
775                 string current;
776                 while (!feof (f)) {
777                         char buffer[128];
778                         if (fgets (buffer, 128, f) == 0) {
779                                 break;
780                         }
781                         current += buffer;
782                         if (strncmp (buffer, "-----END CERTIFICATE-----", 25) == 0) {
783                                 new_chain->add (dcp::Certificate (current));
784                                 current = "";
785                         } else if (strncmp (buffer, "-----END RSA PRIVATE KEY-----", 29) == 0) {
786                                 new_chain->set_key (current);
787                                 current = "";
788                         }
789                 }
790                 fclose (f);
791
792                 if (new_chain->chain_valid() && new_chain->private_key_valid()) {
793                         Config::instance()->set_decryption_chain (new_chain);
794                 } else {
795                         error_dialog (_panel, _("Invalid DCP-o-matic export file"));
796                 }
797         }
798         d->Destroy ();
799 }
800
801 bool
802 KeysPage::nag_alter_decryption_chain ()
803 {
804         return NagDialog::maybe_nag (
805                 _panel,
806                 Config::NAG_ALTER_DECRYPTION_CHAIN,
807                 _("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!"),
808                 true
809                 );
810 }
811
812 void
813 KeysPage::export_decryption_certificate ()
814 {
815         auto config = Config::instance();
816         wxString default_name = "dcpomatic";
817         if (!config->dcp_creator().empty()) {
818                 default_name += "_" + std_to_wx(careful_string_filter(config->dcp_creator()));
819         }
820         if (!config->dcp_issuer().empty()) {
821                 default_name += "_" + std_to_wx(careful_string_filter(config->dcp_issuer()));
822         }
823         default_name += wxT("_kdm_decryption_cert.pem");
824
825         auto d = new wxFileDialog (
826                 _panel, _("Select Certificate File"), wxEmptyString, default_name, wxT("PEM files (*.pem)|*.pem"),
827                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT
828                 );
829
830         if (d->ShowModal () == wxID_OK) {
831                 boost::filesystem::path path (wx_to_std(d->GetPath()));
832                 if (path.extension() != ".pem") {
833                         path += ".pem";
834                 }
835                 auto f = fopen_boost (path, "w");
836                 if (!f) {
837                         throw OpenFileError (path, errno, OpenFileError::WRITE);
838                 }
839
840                 auto const s = Config::instance()->decryption_chain()->leaf().certificate (true);
841                 checked_fwrite (s.c_str(), s.length(), f, path);
842                 fclose (f);
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        (wxEVT_CHECKBOX, 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(bitmap_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 }