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