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