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