Change bitmap_path to take a full name with extension.
[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         checked_set (_language, languages);
142         table->Add (_language, wxGBPosition (r, 1));
143         ++r;
144
145         auto restart = add_label_to_sizer (
146                 table, _panel, _("(restart DCP-o-matic to see language changes)"), false, wxGBPosition (r, 0), wxGBSpan (1, 2)
147                 );
148         wxFont font = restart->GetFont();
149         font.SetStyle (wxFONTSTYLE_ITALIC);
150         font.SetPointSize (font.GetPointSize() - 1);
151         restart->SetFont (font);
152         ++r;
153
154         _set_language->Bind (wxEVT_CHECKBOX, bind (&GeneralPage::set_language_changed, this));
155         _language->Bind     (wxEVT_CHOICE,   bind (&GeneralPage::language_changed,     this));
156 }
157
158 void
159 GeneralPage::add_update_controls (wxGridBagSizer* table, int& r)
160 {
161         _check_for_updates = new CheckBox (_panel, _("Check for updates on startup"));
162         table->Add (_check_for_updates, wxGBPosition (r, 0), wxGBSpan (1, 2));
163         ++r;
164
165         _check_for_test_updates = new CheckBox (_panel, _("Check for testing updates on startup"));
166         table->Add (_check_for_test_updates, wxGBPosition (r, 0), wxGBSpan (1, 2));
167         ++r;
168
169         _check_for_updates->Bind (wxEVT_CHECKBOX, bind (&GeneralPage::check_for_updates_changed, this));
170         _check_for_test_updates->Bind (wxEVT_CHECKBOX, bind (&GeneralPage::check_for_test_updates_changed, this));
171 }
172
173 void
174 GeneralPage::config_changed ()
175 {
176         auto config = Config::instance ();
177
178         checked_set (_set_language, static_cast<bool>(config->language()));
179
180         /* Backwards compatibility of config file */
181
182         map<string, string> compat_map;
183         compat_map["fr"] = "fr_FR";
184         compat_map["it"] = "it_IT";
185         compat_map["es"] = "es_ES";
186         compat_map["sv"] = "sv_SE";
187         compat_map["de"] = "de_DE";
188         compat_map["nl"] = "nl_NL";
189         compat_map["ru"] = "ru_RU";
190         compat_map["pl"] = "pl_PL";
191         compat_map["da"] = "da_DK";
192         compat_map["pt"] = "pt_PT";
193         compat_map["sk"] = "sk_SK";
194         compat_map["cs"] = "cs_CZ";
195         compat_map["uk"] = "uk_UA";
196
197         auto lang = config->language().get_value_or("en_GB");
198         if (compat_map.find(lang) != compat_map.end ()) {
199                 lang = compat_map[lang];
200         }
201
202         checked_set (_language, lang);
203
204         checked_set (_check_for_updates, config->check_for_updates ());
205         checked_set (_check_for_test_updates, config->check_for_test_updates ());
206
207         setup_sensitivity ();
208 }
209
210 void
211 GeneralPage::setup_sensitivity ()
212 {
213         _language->Enable (_set_language->GetValue ());
214         _check_for_test_updates->Enable (_check_for_updates->GetValue ());
215 }
216
217 void
218 GeneralPage::set_language_changed ()
219 {
220         setup_sensitivity ();
221         if (_set_language->GetValue ()) {
222                 language_changed ();
223         } else {
224                 Config::instance()->unset_language ();
225         }
226 }
227
228 void
229 GeneralPage::language_changed ()
230 {
231         int const sel = _language->GetSelection ();
232         if (sel != -1) {
233                 Config::instance()->set_language (string_client_data (_language->GetClientObject (sel)));
234         } else {
235                 Config::instance()->unset_language ();
236         }
237 }
238
239 void
240 GeneralPage::check_for_updates_changed ()
241 {
242         Config::instance()->set_check_for_updates (_check_for_updates->GetValue ());
243 }
244
245 void
246 GeneralPage::check_for_test_updates_changed ()
247 {
248         Config::instance()->set_check_for_test_updates (_check_for_test_updates->GetValue ());
249 }
250
251 CertificateChainEditor::CertificateChainEditor (
252         wxWindow* parent,
253         wxString title,
254         int border,
255         function<void (shared_ptr<dcp::CertificateChain>)> set,
256         function<shared_ptr<const dcp::CertificateChain> (void)> get,
257         function<bool (void)> nag_alter
258         )
259         : wxDialog (parent, wxID_ANY, title)
260         , _set (set)
261         , _get (get)
262         , _nag_alter (nag_alter)
263 {
264         _sizer = new wxBoxSizer (wxVERTICAL);
265
266         auto certificates_sizer = new wxBoxSizer (wxHORIZONTAL);
267         _sizer->Add (certificates_sizer, 0, wxALL, border);
268
269         _certificates = new wxListCtrl (this, wxID_ANY, wxDefaultPosition, wxSize (440, 150), wxLC_REPORT | wxLC_SINGLE_SEL);
270
271         {
272                 wxListItem ip;
273                 ip.SetId (0);
274                 ip.SetText (_("Type"));
275                 ip.SetWidth (100);
276                 _certificates->InsertColumn (0, ip);
277         }
278
279         {
280                 wxListItem ip;
281                 ip.SetId (1);
282                 ip.SetText (_("Thumbprint"));
283                 ip.SetWidth (340);
284
285                 wxFont font = ip.GetFont ();
286                 font.SetFamily (wxFONTFAMILY_TELETYPE);
287                 ip.SetFont (font);
288
289                 _certificates->InsertColumn (1, ip);
290         }
291
292         certificates_sizer->Add (_certificates, 1, wxEXPAND);
293
294         {
295                 auto s = new wxBoxSizer (wxVERTICAL);
296                 _add_certificate = new Button (this, _("Add..."));
297                 s->Add (_add_certificate, 1, wxTOP | wxBOTTOM | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
298                 _remove_certificate = new Button (this, _("Remove"));
299                 s->Add (_remove_certificate, 1, wxTOP | wxBOTTOM | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
300                 _export_certificate = new Button (this, _("Export certificate..."));
301                 s->Add (_export_certificate, 1, wxTOP | wxBOTTOM | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
302                 _export_chain = new Button (this, _("Export chain..."));
303                 s->Add (_export_chain, 1, wxTOP | wxBOTTOM | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
304                 certificates_sizer->Add (s, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP);
305         }
306
307         auto table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
308         _sizer->Add (table, 1, wxALL | wxEXPAND, border);
309         int r = 0;
310
311         add_label_to_sizer (table, this, _("Leaf private key"), true, wxGBPosition (r, 0));
312         _private_key = new StaticText (this, wxT(""));
313         wxFont font = _private_key->GetFont ();
314         font.SetFamily (wxFONTFAMILY_TELETYPE);
315         _private_key->SetFont (font);
316         table->Add (_private_key, wxGBPosition (r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
317         _import_private_key = new Button (this, _("Import..."));
318         table->Add (_import_private_key, wxGBPosition (r, 2));
319         _export_private_key = new Button (this, _("Export..."));
320         table->Add (_export_private_key, wxGBPosition (r, 3));
321         ++r;
322
323         _button_sizer = new wxBoxSizer (wxHORIZONTAL);
324         _remake_certificates = new Button (this, _("Re-make certificates and key..."));
325         _button_sizer->Add (_remake_certificates, 1, wxRIGHT, border);
326         table->Add (_button_sizer, wxGBPosition (r, 0), wxGBSpan (1, 4));
327         ++r;
328
329         _private_key_bad = new StaticText (this, _("Leaf private key does not match leaf certificate!"));
330         font = *wxSMALL_FONT;
331         font.SetWeight (wxFONTWEIGHT_BOLD);
332         _private_key_bad->SetFont (font);
333         table->Add (_private_key_bad, wxGBPosition (r, 0), wxGBSpan (1, 3));
334         ++r;
335
336         _add_certificate->Bind     (wxEVT_BUTTON,       bind (&CertificateChainEditor::add_certificate, this));
337         _remove_certificate->Bind  (wxEVT_BUTTON,       bind (&CertificateChainEditor::remove_certificate, this));
338         _export_certificate->Bind  (wxEVT_BUTTON,       bind (&CertificateChainEditor::export_certificate, this));
339         _certificates->Bind        (wxEVT_LIST_ITEM_SELECTED,   bind (&CertificateChainEditor::update_sensitivity, this));
340         _certificates->Bind        (wxEVT_LIST_ITEM_DESELECTED, bind (&CertificateChainEditor::update_sensitivity, this));
341         _remake_certificates->Bind (wxEVT_BUTTON,       bind (&CertificateChainEditor::remake_certificates, this));
342         _export_chain->Bind        (wxEVT_BUTTON,       bind (&CertificateChainEditor::export_chain, this));
343         _import_private_key->Bind  (wxEVT_BUTTON,       bind (&CertificateChainEditor::import_private_key, this));
344         _export_private_key->Bind  (wxEVT_BUTTON,       bind (&CertificateChainEditor::export_private_key, this));
345
346         auto buttons = CreateSeparatedButtonSizer (wxCLOSE);
347         if (buttons) {
348                 _sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
349         }
350
351         SetSizerAndFit (_sizer);
352
353         update_certificate_list ();
354         update_private_key ();
355         update_sensitivity ();
356 }
357
358 void
359 CertificateChainEditor::add_button (wxWindow* button)
360 {
361         _button_sizer->Add (button, 0, wxLEFT | wxRIGHT, DCPOMATIC_SIZER_X_GAP);
362         _sizer->Layout ();
363 }
364
365 void
366 CertificateChainEditor::add_certificate ()
367 {
368         auto d = new wxFileDialog (this, _("Select Certificate File"));
369
370         if (d->ShowModal() == wxID_OK) {
371                 try {
372                         dcp::Certificate c;
373                         string extra;
374                         try {
375                                 extra = c.read_string (dcp::file_to_string (wx_to_std (d->GetPath ())));
376                         } catch (boost::filesystem::filesystem_error& e) {
377                                 error_dialog (this, _("Could not import certificate (%s)"), d->GetPath());
378                                 d->Destroy ();
379                                 return;
380                         }
381
382                         if (!extra.empty ()) {
383                                 message_dialog (
384                                         this,
385                                         _("This file contains other certificates (or other data) after its first certificate. "
386                                           "Only the first certificate will be used.")
387                                         );
388                         }
389                         auto chain = make_shared<dcp::CertificateChain>(*_get().get());
390                         chain->add (c);
391                         if (!chain->chain_valid ()) {
392                                 error_dialog (
393                                         this,
394                                         _("Adding this certificate would make the chain inconsistent, so it will not be added. "
395                                           "Add certificates in order from root to intermediate to leaf.")
396                                         );
397                                 chain->remove (c);
398                         } else {
399                                 _set (chain);
400                                 update_certificate_list ();
401                         }
402                 } catch (dcp::MiscError& e) {
403                         error_dialog (this, _("Could not read certificate file."), std_to_wx(e.what()));
404                 }
405         }
406
407         d->Destroy ();
408
409         update_sensitivity ();
410 }
411
412 void
413 CertificateChainEditor::remove_certificate ()
414 {
415         if (_nag_alter()) {
416                 /* Cancel was clicked */
417                 return;
418         }
419
420         int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
421         if (i == -1) {
422                 return;
423         }
424
425         _certificates->DeleteItem (i);
426         auto chain = make_shared<dcp::CertificateChain>(*_get().get());
427         chain->remove (i);
428         _set (chain);
429
430         update_sensitivity ();
431         update_certificate_list ();
432 }
433
434 void
435 CertificateChainEditor::export_certificate ()
436 {
437         int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
438         if (i == -1) {
439                 return;
440         }
441
442         auto all = _get()->root_to_leaf();
443
444         wxString default_name;
445         if (i == 0) {
446                 default_name = "root.pem";
447         } else if (i == static_cast<int>(all.size() - 1)) {
448                 default_name = "leaf.pem";
449         } else {
450                 default_name = "intermediate.pem";
451         }
452
453         auto d = new wxFileDialog(
454                 this, _("Select Certificate File"), wxEmptyString, default_name, wxT ("PEM files (*.pem)|*.pem"),
455                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT
456                 );
457
458         auto j = all.begin ();
459         for (int k = 0; k < i; ++k) {
460                 ++j;
461         }
462
463         if (d->ShowModal () == wxID_OK) {
464                 boost::filesystem::path path (wx_to_std(d->GetPath()));
465                 if (path.extension() != ".pem") {
466                         path += ".pem";
467                 }
468                 dcp::File f(path, "w");
469                 if (!f) {
470                         throw OpenFileError (path, errno, OpenFileError::WRITE);
471                 }
472
473                 string const s = j->certificate (true);
474                 f.checked_write(s.c_str(), s.length());
475         }
476         d->Destroy ();
477 }
478
479 void
480 CertificateChainEditor::export_chain ()
481 {
482         auto d = new wxFileDialog (
483                 this, _("Select Chain File"), wxEmptyString, wxT("certificate_chain.pem"), wxT("PEM files (*.pem)|*.pem"),
484                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT
485                 );
486
487         if (d->ShowModal () == wxID_OK) {
488                 boost::filesystem::path path (wx_to_std(d->GetPath()));
489                 if (path.extension() != ".pem") {
490                         path += ".pem";
491                 }
492                 dcp::File f(path, "w");
493                 if (!f) {
494                         throw OpenFileError (path, errno, OpenFileError::WRITE);
495                 }
496
497                 auto const s = _get()->chain();
498                 f.checked_write (s.c_str(), s.length());
499         }
500
501         d->Destroy ();
502 }
503
504 void
505 CertificateChainEditor::update_certificate_list ()
506 {
507         _certificates->DeleteAllItems ();
508         size_t n = 0;
509         auto certs = _get()->root_to_leaf();
510         for (auto const& i: certs) {
511                 wxListItem item;
512                 item.SetId (n);
513                 _certificates->InsertItem (item);
514                 _certificates->SetItem (n, 1, std_to_wx (i.thumbprint ()));
515
516                 if (n == 0) {
517                         _certificates->SetItem (n, 0, _("Root"));
518                 } else if (n == (certs.size() - 1)) {
519                         _certificates->SetItem (n, 0, _("Leaf"));
520                 } else {
521                         _certificates->SetItem (n, 0, _("Intermediate"));
522                 }
523
524                 ++n;
525         }
526
527         static wxColour normal = _private_key_bad->GetForegroundColour ();
528
529         if (_get()->private_key_valid()) {
530                 _private_key_bad->Hide ();
531                 _private_key_bad->SetForegroundColour (normal);
532         } else {
533                 _private_key_bad->Show ();
534                 _private_key_bad->SetForegroundColour (wxColour (255, 0, 0));
535         }
536 }
537
538 void
539 CertificateChainEditor::remake_certificates ()
540 {
541         if (_nag_alter()) {
542                 /* Cancel was clicked */
543                 return;
544         }
545
546         auto d = new MakeChainDialog (this, _get());
547
548         if (d->ShowModal () == wxID_OK) {
549                 _set (d->get());
550                 update_certificate_list ();
551                 update_private_key ();
552         }
553
554         d->Destroy ();
555 }
556
557 void
558 CertificateChainEditor::update_sensitivity ()
559 {
560         /* We can only remove the leaf certificate */
561         _remove_certificate->Enable (_certificates->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED) == (_certificates->GetItemCount() - 1));
562         _export_certificate->Enable (_certificates->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED) != -1);
563 }
564
565 void
566 CertificateChainEditor::update_private_key ()
567 {
568         checked_set (_private_key, dcp::private_key_fingerprint (_get()->key().get()));
569         _sizer->Layout ();
570 }
571
572 void
573 CertificateChainEditor::import_private_key ()
574 {
575         auto d = new wxFileDialog (this, _("Select Key File"));
576
577         if (d->ShowModal() == wxID_OK) {
578                 try {
579                         boost::filesystem::path p (wx_to_std (d->GetPath ()));
580                         if (boost::filesystem::file_size (p) > 8192) {
581                                 error_dialog (
582                                         this,
583                                         wxString::Format (_("Could not read key file; file is too long (%s)"), std_to_wx (p.string ()))
584                                         );
585                                 return;
586                         }
587
588                         auto chain = make_shared<dcp::CertificateChain>(*_get().get());
589                         chain->set_key (dcp::file_to_string (p));
590                         _set (chain);
591                         update_private_key ();
592                 } catch (std::exception& e) {
593                         error_dialog (this, _("Could not read certificate file."), std_to_wx(e.what()));
594                 }
595         }
596
597         d->Destroy ();
598
599         update_sensitivity ();
600 }
601
602 void
603 CertificateChainEditor::export_private_key ()
604 {
605         auto key = _get()->key();
606         if (!key) {
607                 return;
608         }
609
610         auto d = new wxFileDialog (
611                 this, _("Select Key File"), wxEmptyString, wxT("private_key.pem"), wxT("PEM files (*.pem)|*.pem"),
612                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT
613                 );
614
615         if (d->ShowModal () == wxID_OK) {
616                 boost::filesystem::path path (wx_to_std(d->GetPath()));
617                 if (path.extension() != ".pem") {
618                         path += ".pem";
619                 }
620                 dcp::File f(path, "w");
621                 if (!f) {
622                         throw OpenFileError (path, errno, OpenFileError::WRITE);
623                 }
624
625                 auto const s = _get()->key().get ();
626                 f.checked_write(s.c_str(), s.length());
627         }
628         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                 dcp::File f(path, "w");
737                 if (!f) {
738                         throw OpenFileError (path, errno, OpenFileError::WRITE);
739                 }
740
741                 auto const chain = Config::instance()->decryption_chain()->chain();
742                 f.checked_write (chain.c_str(), chain.length());
743                 auto const key = Config::instance()->decryption_chain()->key();
744                 DCPOMATIC_ASSERT (key);
745                 f.checked_write(key->c_str(), key->length());
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                 dcp::File f(wx_to_std(d->GetPath()), "r");
771                 if (!f) {
772                         throw OpenFileError (f.path(), errno, OpenFileError::WRITE);
773                 }
774
775                 string current;
776                 while (!f.eof()) {
777                         char buffer[128];
778                         if (f.gets(buffer, 128) == 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
791                 if (new_chain->chain_valid() && new_chain->private_key_valid()) {
792                         Config::instance()->set_decryption_chain (new_chain);
793                 } else {
794                         error_dialog (_panel, _("Invalid DCP-o-matic export file"));
795                 }
796         }
797         d->Destroy ();
798 }
799
800 bool
801 KeysPage::nag_alter_decryption_chain ()
802 {
803         return NagDialog::maybe_nag (
804                 _panel,
805                 Config::NAG_ALTER_DECRYPTION_CHAIN,
806                 _("If you continue with this operation you will no longer be able to use any DKDMs that you have created.  Also, any KDMs that have been sent to you will become useless.  Proceed with caution!"),
807                 true
808                 );
809 }
810
811 void
812 KeysPage::export_decryption_certificate ()
813 {
814         auto config = Config::instance();
815         wxString default_name = "dcpomatic";
816         if (!config->dcp_creator().empty()) {
817                 default_name += "_" + std_to_wx(careful_string_filter(config->dcp_creator()));
818         }
819         if (!config->dcp_issuer().empty()) {
820                 default_name += "_" + std_to_wx(careful_string_filter(config->dcp_issuer()));
821         }
822         default_name += wxT("_kdm_decryption_cert.pem");
823
824         auto d = new wxFileDialog (
825                 _panel, _("Select Certificate File"), wxEmptyString, default_name, wxT("PEM files (*.pem)|*.pem"),
826                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT
827                 );
828
829         if (d->ShowModal () == wxID_OK) {
830                 boost::filesystem::path path (wx_to_std(d->GetPath()));
831                 if (path.extension() != ".pem") {
832                         path += ".pem";
833                 }
834                 dcp::File f(path, "w");
835                 if (!f) {
836                         throw OpenFileError (path, errno, OpenFileError::WRITE);
837                 }
838
839                 auto const s = Config::instance()->decryption_chain()->leaf().certificate (true);
840                 f.checked_write(s.c_str(), s.length());
841         }
842
843         d->Destroy ();
844 }
845
846 wxString
847 SoundPage::GetName () const
848 {
849         return _("Sound");
850 }
851
852 void
853 SoundPage::setup ()
854 {
855         auto table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
856         _panel->GetSizer()->Add (table, 1, wxALL | wxEXPAND, _border);
857
858         int r = 0;
859
860         _sound = new CheckBox (_panel, _("Play sound via"));
861         table->Add (_sound, wxGBPosition (r, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
862         wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
863         _sound_output = new wxChoice (_panel, wxID_ANY);
864         s->Add (_sound_output, 0);
865         _sound_output_details = new wxStaticText (_panel, wxID_ANY, wxT(""));
866         s->Add (_sound_output_details, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, DCPOMATIC_SIZER_X_GAP);
867         table->Add (s, wxGBPosition(r, 1));
868         ++r;
869
870         add_label_to_sizer (table, _panel, _("Mapping"), true, wxGBPosition(r, 0));
871         _map = new AudioMappingView (_panel, _("DCP"), _("DCP"), _("Output"), _("output"));
872         _map->SetSize (-1, 400);
873         table->Add (_map, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND);
874         ++r;
875
876         _reset_to_default = new Button (_panel, _("Reset to default"));
877         table->Add (_reset_to_default, wxGBPosition(r, 1));
878         ++r;
879
880         wxFont font = _sound_output_details->GetFont();
881         font.SetStyle (wxFONTSTYLE_ITALIC);
882         font.SetPointSize (font.GetPointSize() - 1);
883         _sound_output_details->SetFont (font);
884
885         RtAudio audio (DCPOMATIC_RTAUDIO_API);
886         for (unsigned int i = 0; i < audio.getDeviceCount(); ++i) {
887                 try {
888                         auto dev = audio.getDeviceInfo (i);
889                         if (dev.probed && dev.outputChannels > 0) {
890                                 _sound_output->Append (std_to_wx (dev.name));
891                         }
892                 } catch (RtAudioError&) {
893                         /* Something went wrong so let's just ignore that device */
894                 }
895         }
896
897         _sound->Bind        (wxEVT_CHECKBOX, bind(&SoundPage::sound_changed, this));
898         _sound_output->Bind (wxEVT_CHOICE,   bind(&SoundPage::sound_output_changed, this));
899         _map->Changed.connect (bind(&SoundPage::map_changed, this, _1));
900         _reset_to_default->Bind (wxEVT_BUTTON,   bind(&SoundPage::reset_to_default, this));
901 }
902
903 void
904 SoundPage::reset_to_default ()
905 {
906         Config::instance()->set_audio_mapping_to_default ();
907 }
908
909 void
910 SoundPage::map_changed (AudioMapping m)
911 {
912         Config::instance()->set_audio_mapping (m);
913 }
914
915 void
916 SoundPage::sound_changed ()
917 {
918         Config::instance()->set_sound (_sound->GetValue ());
919 }
920
921 void
922 SoundPage::sound_output_changed ()
923 {
924         RtAudio audio (DCPOMATIC_RTAUDIO_API);
925         auto const so = get_sound_output();
926         string default_device;
927         try {
928                 default_device = audio.getDeviceInfo(audio.getDefaultOutputDevice()).name;
929         } catch (RtAudioError&) {
930                 /* Never mind */
931         }
932         if (!so || *so == default_device) {
933                 Config::instance()->unset_sound_output ();
934         } else {
935                 Config::instance()->set_sound_output (*so);
936         }
937 }
938
939 void
940 SoundPage::config_changed ()
941 {
942         auto config = Config::instance ();
943
944         checked_set (_sound, config->sound ());
945
946         auto const current_so = get_sound_output ();
947         optional<string> configured_so;
948
949         if (config->sound_output()) {
950                 configured_so = config->sound_output().get();
951         } else {
952                 /* No configured output means we should use the default */
953                 RtAudio audio (DCPOMATIC_RTAUDIO_API);
954                 try {
955                         configured_so = audio.getDeviceInfo(audio.getDefaultOutputDevice()).name;
956                 } catch (RtAudioError&) {
957                         /* Probably no audio devices at all */
958                 }
959         }
960
961         if (configured_so && current_so != configured_so) {
962                 /* Update _sound_output with the configured value */
963                 unsigned int i = 0;
964                 while (i < _sound_output->GetCount()) {
965                         if (_sound_output->GetString(i) == std_to_wx(*configured_so)) {
966                                 _sound_output->SetSelection (i);
967                                 break;
968                         }
969                         ++i;
970                 }
971         }
972
973         RtAudio audio (DCPOMATIC_RTAUDIO_API);
974
975         map<int, wxString> apis;
976         apis[RtAudio::MACOSX_CORE]    = _("CoreAudio");
977         apis[RtAudio::WINDOWS_ASIO]   = _("ASIO");
978         apis[RtAudio::WINDOWS_DS]     = _("Direct Sound");
979         apis[RtAudio::WINDOWS_WASAPI] = _("WASAPI");
980         apis[RtAudio::UNIX_JACK]      = _("JACK");
981         apis[RtAudio::LINUX_ALSA]     = _("ALSA");
982         apis[RtAudio::LINUX_PULSE]    = _("PulseAudio");
983         apis[RtAudio::LINUX_OSS]      = _("OSS");
984         apis[RtAudio::RTAUDIO_DUMMY]  = _("Dummy");
985
986         int channels = 0;
987         if (configured_so) {
988                 for (unsigned int i = 0; i < audio.getDeviceCount(); ++i) {
989                         try {
990                                 auto info = audio.getDeviceInfo(i);
991                                 if (info.name == *configured_so && info.outputChannels > 0) {
992                                         channels = info.outputChannels;
993                                 }
994                         } catch (RtAudioError&) {
995                                 /* Never mind */
996                         }
997                 }
998         }
999
1000         _sound_output_details->SetLabel (
1001                 wxString::Format(_("%d channels on %s"), channels, apis[audio.getCurrentApi()])
1002                 );
1003
1004         _map->set (Config::instance()->audio_mapping(channels));
1005
1006         vector<NamedChannel> input;
1007         for (int i = 0; i < MAX_DCP_AUDIO_CHANNELS; ++i) {
1008                 input.push_back (NamedChannel(short_audio_channel_name(i), i));
1009         }
1010         _map->set_input_channels (input);
1011
1012         vector<NamedChannel> output;
1013         for (int i = 0; i < channels; ++i) {
1014                 output.push_back (NamedChannel(dcp::raw_convert<string>(i), i));
1015         }
1016         _map->set_output_channels (output);
1017
1018         setup_sensitivity ();
1019 }
1020
1021 void
1022 SoundPage::setup_sensitivity ()
1023 {
1024         _sound_output->Enable (_sound->GetValue());
1025 }
1026
1027 /** @return Currently-selected preview sound output in the dialogue */
1028 optional<string>
1029 SoundPage::get_sound_output ()
1030 {
1031         int const sel = _sound_output->GetSelection ();
1032         if (sel == wxNOT_FOUND) {
1033                 return optional<string> ();
1034         }
1035
1036         return wx_to_std (_sound_output->GetString (sel));
1037 }
1038
1039
1040 LocationsPage::LocationsPage (wxSize panel_size, int border)
1041         : Page (panel_size, border)
1042 {
1043
1044 }
1045
1046 wxString
1047 LocationsPage::GetName () const
1048 {
1049         return _("Locations");
1050 }
1051
1052 #ifdef DCPOMATIC_OSX
1053 wxBitmap
1054 LocationsPage::GetLargeIcon () const
1055 {
1056         return wxBitmap(bitmap_path("locations.png"), wxBITMAP_TYPE_PNG);
1057 }
1058 #endif
1059
1060 void
1061 LocationsPage::setup ()
1062 {
1063         int r = 0;
1064
1065         auto table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
1066         _panel->GetSizer()->Add (table, 1, wxALL | wxEXPAND, _border);
1067
1068         add_label_to_sizer (table, _panel, _("Content directory"), true, wxGBPosition (r, 0));
1069         _content_directory = new wxDirPickerCtrl (_panel, wxID_ANY, wxEmptyString, wxDirSelectorPromptStr, wxDefaultPosition, wxSize (300, -1));
1070         table->Add (_content_directory, wxGBPosition (r, 1));
1071         ++r;
1072
1073         add_label_to_sizer (table, _panel, _("Playlist directory"), true, wxGBPosition (r, 0));
1074         _playlist_directory = new wxDirPickerCtrl (_panel, wxID_ANY, wxEmptyString, wxDirSelectorPromptStr, wxDefaultPosition, wxSize (300, -1));
1075         table->Add (_playlist_directory, wxGBPosition (r, 1));
1076         ++r;
1077
1078         add_label_to_sizer (table, _panel, _("KDM directory"), true, wxGBPosition (r, 0));
1079         _kdm_directory = new wxDirPickerCtrl (_panel, wxID_ANY, wxEmptyString, wxDirSelectorPromptStr, wxDefaultPosition, wxSize (300, -1));
1080         table->Add (_kdm_directory, wxGBPosition (r, 1));
1081         ++r;
1082
1083         _content_directory->Bind (wxEVT_DIRPICKER_CHANGED, bind(&LocationsPage::content_directory_changed, this));
1084         _playlist_directory->Bind (wxEVT_DIRPICKER_CHANGED, bind(&LocationsPage::playlist_directory_changed, this));
1085         _kdm_directory->Bind (wxEVT_DIRPICKER_CHANGED, bind(&LocationsPage::kdm_directory_changed, this));
1086 }
1087
1088 void
1089 LocationsPage::config_changed ()
1090 {
1091         auto config = Config::instance ();
1092
1093         if (config->player_content_directory()) {
1094                 checked_set (_content_directory, *config->player_content_directory());
1095         }
1096         if (config->player_playlist_directory()) {
1097                 checked_set (_playlist_directory, *config->player_playlist_directory());
1098         }
1099         if (config->player_kdm_directory()) {
1100                 checked_set (_kdm_directory, *config->player_kdm_directory());
1101         }
1102 }
1103
1104 void
1105 LocationsPage::content_directory_changed ()
1106 {
1107         Config::instance()->set_player_content_directory(wx_to_std(_content_directory->GetPath()));
1108 }
1109
1110 void
1111 LocationsPage::playlist_directory_changed ()
1112 {
1113         Config::instance()->set_player_playlist_directory(wx_to_std(_playlist_directory->GetPath()));
1114 }
1115
1116 void
1117 LocationsPage::kdm_directory_changed ()
1118 {
1119         Config::instance()->set_player_kdm_directory(wx_to_std(_kdm_directory->GetPath()));
1120 }