Extract indices array and make some offset constants.
[dcpomatic.git] / src / wx / config_dialog.cc
index a0df1a6406f8a7ac6a9f3f3726fe5b253f8ba8f0..1bd5036e71c6aef2fd30a55af10713645928f75c 100644 (file)
 /*
-    Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2021 Carl Hetherington <cth@carlh.net>
 
-    This program is free software; you can redistribute it and/or modify
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
     the Free Software Foundation; either version 2 of the License, or
     (at your option) any later version.
 
-    This program is distributed in the hope that it will be useful,
+    DCP-o-matic is distributed in the hope that it will be useful,
     but WITHOUT ANY WARRANTY; without even the implied warranty of
     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     GNU General Public License for more details.
 
     You should have received a copy of the GNU General Public License
-    along with this program; if not, write to the Free Software
-    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
 
 */
 
-/** @file src/config_dialog.cc
- *  @brief A dialogue to edit DCP-o-matic configuration.
- */
-
-#include <iostream>
-#include <boost/lexical_cast.hpp>
-#include <boost/filesystem.hpp>
-#include <wx/stdpaths.h>
-#include <wx/preferences.h>
-#include <wx/filepicker.h>
-#include <wx/spinctrl.h>
-#include <libdcp/colour_matrix.h>
-#include "lib/config.h"
-#include "lib/ratio.h"
-#include "lib/scaler.h"
-#include "lib/filter.h"
-#include "lib/dcp_content_type.h"
-#include "lib/colour_conversion.h"
 #include "config_dialog.h"
-#include "wx_util.h"
-#include "editable_list.h"
-#include "filter_dialog.h"
-#include "dir_picker_ctrl.h"
-#include "isdcf_metadata_dialog.h"
-#include "preset_colour_conversion_dialog.h"
-#include "server_dialog.h"
+#include "static_text.h"
+#include "check_box.h"
+#include "nag_dialog.h"
+#include "dcpomatic_button.h"
+#include "audio_mapping_view.h"
+#include <dcp/raw_convert.h>
+#include <iostream>
 
-using std::vector;
 using std::string;
-using std::list;
-using std::cout;
+using std::vector;
+using std::pair;
+using std::make_pair;
+using std::map;
+using std::make_shared;
 using boost::bind;
-using boost::shared_ptr;
-using boost::lexical_cast;
+using boost::optional;
+using std::shared_ptr;
+using std::function;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+
+static
+bool
+do_nothing ()
+{
+       return false;
+}
 
-class Page
+Page::Page (wxSize panel_size, int border)
+       : _border (border)
+       , _panel (0)
+       , _panel_size (panel_size)
+       , _window_exists (false)
 {
-public:
-       Page (wxSize panel_size, int border)
-               : _panel_size (panel_size)
-               , _border (border)
-       {}
+       _config_connection = Config::instance()->Changed.connect (bind (&Page::config_changed_wrapper, this));
+}
 
-protected:
-       wxSize _panel_size;
-       int _border;
-};
 
-class GeneralPage : public wxStockPreferencesPage, public Page
+wxWindow*
+Page::CreateWindow (wxWindow* parent)
 {
-public:
-       GeneralPage (wxSize panel_size, int border)
-               : wxStockPreferencesPage (Kind_General)
-               , Page (panel_size, border)
-       {}
+       return create_window (parent);
+}
 
-       wxWindow* CreateWindow (wxWindow* parent)
-       {
-               wxPanel* panel = new wxPanel (parent);
-               wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
-               panel->SetSizer (s);
-
-               wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
-               table->AddGrowableCol (1, 1);
-               s->Add (table, 1, wxALL | wxEXPAND, _border);
-               
-               _set_language = new wxCheckBox (panel, wxID_ANY, _("Set language"));
-               table->Add (_set_language, 1);
-               _language = new wxChoice (panel, wxID_ANY);
-               _language->Append (wxT ("Deutsch"));
-               _language->Append (wxT ("English"));
-               _language->Append (wxT ("Español"));
-               _language->Append (wxT ("Français"));
-               _language->Append (wxT ("Italiano"));
-               _language->Append (wxT ("Nederlands"));
-               _language->Append (wxT ("Svenska"));
-               table->Add (_language);
-               
-               wxStaticText* restart = add_label_to_sizer (table, panel, _("(restart DCP-o-matic to see language changes)"), false);
-               wxFont font = restart->GetFont();
-               font.SetStyle (wxFONTSTYLE_ITALIC);
-               font.SetPointSize (font.GetPointSize() - 1);
-               restart->SetFont (font);
-               table->AddSpacer (0);
-               
-               add_label_to_sizer (table, panel, _("Threads to use for encoding on this host"), true);
-               _num_local_encoding_threads = new wxSpinCtrl (panel);
-               table->Add (_num_local_encoding_threads, 1);
-
-               
-               _check_for_updates = new wxCheckBox (panel, wxID_ANY, _("Check for updates on startup"));
-               table->Add (_check_for_updates, 1, wxEXPAND | wxALL);
-               table->AddSpacer (0);
-               
-               _check_for_test_updates = new wxCheckBox (panel, wxID_ANY, _("Check for testing updates as well as stable ones"));
-               table->Add (_check_for_test_updates, 1, wxEXPAND | wxALL);
-               table->AddSpacer (0);
-               
-               Config* config = Config::instance ();
-               
-               _set_language->SetValue (config->language ());
-               
-               if (config->language().get_value_or ("") == "fr") {
-                       _language->SetSelection (3);
-               } else if (config->language().get_value_or ("") == "it") {
-                       _language->SetSelection (4);
-               } else if (config->language().get_value_or ("") == "es") {
-                       _language->SetSelection (2);
-               } else if (config->language().get_value_or ("") == "sv") {
-                       _language->SetSelection (6);
-               } else if (config->language().get_value_or ("") == "de") {
-                       _language->SetSelection (0);
-               } else if (config->language().get_value_or ("") == "nl") {
-                       _language->SetSelection (5);
-               } else {
-                       _language->SetSelection (1);
-               }
-               
-               setup_language_sensitivity ();
-               
-               _set_language->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&GeneralPage::set_language_changed, this));
-               _language->Bind     (wxEVT_COMMAND_CHOICE_SELECTED,  boost::bind (&GeneralPage::language_changed,     this));
-               
-               _num_local_encoding_threads->SetRange (1, 128);
-               _num_local_encoding_threads->SetValue (config->num_local_encoding_threads ());
-               _num_local_encoding_threads->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&GeneralPage::num_local_encoding_threads_changed, this));
-
-               _check_for_updates->SetValue (config->check_for_updates ());
-               _check_for_updates->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&GeneralPage::check_for_updates_changed, this));
-               _check_for_test_updates->SetValue (config->check_for_test_updates ());
-               _check_for_test_updates->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&GeneralPage::check_for_test_updates_changed, this));
-               
-               return panel;
-       }
-
-private:       
-       void setup_language_sensitivity ()
-       {
-               _language->Enable (_set_language->GetValue ());
+
+wxWindow*
+Page::create_window (wxWindow* parent)
+{
+       _panel = new wxPanel (parent, wxID_ANY, wxDefaultPosition, _panel_size);
+       auto s = new wxBoxSizer (wxVERTICAL);
+       _panel->SetSizer (s);
+
+       setup ();
+       _window_exists = true;
+       config_changed ();
+
+       _panel->Bind (wxEVT_DESTROY, bind (&Page::window_destroyed, this));
+
+       return _panel;
+}
+
+void
+Page::config_changed_wrapper ()
+{
+       if (_window_exists) {
+               config_changed ();
        }
+}
 
-       void set_language_changed ()
-       {
-               setup_language_sensitivity ();
-               if (_set_language->GetValue ()) {
-                       language_changed ();
-               } else {
-                       Config::instance()->unset_language ();
-               }
+void
+Page::window_destroyed ()
+{
+       _window_exists = false;
+}
+
+
+GeneralPage::GeneralPage (wxSize panel_size, int border)
+       : Page (panel_size, border)
+{
+
+}
+
+
+wxString
+GeneralPage::GetName () const
+{
+       return _("General");
+}
+
+
+void
+GeneralPage::add_language_controls (wxGridBagSizer* table, int& r)
+{
+       _set_language = new CheckBox (_panel, _("Set language"));
+       table->Add (_set_language, wxGBPosition (r, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
+       _language = new wxChoice (_panel, wxID_ANY);
+       vector<pair<string, string>> languages;
+       languages.push_back (make_pair("Čeština", "cs_CZ"));
+       languages.push_back (make_pair("汉语/漢語", "zh_CN"));
+       languages.push_back (make_pair("Dansk", "da_DK"));
+       languages.push_back (make_pair("Deutsch", "de_DE"));
+       languages.push_back (make_pair("English", "en_GB"));
+       languages.push_back (make_pair("Español", "es_ES"));
+       languages.push_back (make_pair("Français", "fr_FR"));
+       languages.push_back (make_pair("Italiano", "it_IT"));
+       languages.push_back (make_pair("Nederlands", "nl_NL"));
+       languages.push_back (make_pair("Русский", "ru_RU"));
+       languages.push_back (make_pair("Polski", "pl_PL"));
+       languages.push_back (make_pair("Português europeu", "pt_PT"));
+       languages.push_back (make_pair("Português do Brasil", "pt_BR"));
+       languages.push_back (make_pair("Svenska", "sv_SE"));
+       languages.push_back (make_pair("Slovenský jazyk", "sk_SK"));
+       languages.push_back (make_pair("Türkçe", "tr_TR"));
+       languages.push_back (make_pair("українська мова", "uk_UA"));
+       checked_set (_language, languages);
+       table->Add (_language, wxGBPosition (r, 1));
+       ++r;
+
+       auto restart = add_label_to_sizer (
+               table, _panel, _("(restart DCP-o-matic to see language changes)"), false, wxGBPosition (r, 0), wxGBSpan (1, 2)
+               );
+       wxFont font = restart->GetFont();
+       font.SetStyle (wxFONTSTYLE_ITALIC);
+       font.SetPointSize (font.GetPointSize() - 1);
+       restart->SetFont (font);
+       ++r;
+
+       _set_language->Bind (wxEVT_CHECKBOX, bind (&GeneralPage::set_language_changed, this));
+       _language->Bind     (wxEVT_CHOICE,   bind (&GeneralPage::language_changed,     this));
+}
+
+void
+GeneralPage::add_update_controls (wxGridBagSizer* table, int& r)
+{
+       _check_for_updates = new CheckBox (_panel, _("Check for updates on startup"));
+       table->Add (_check_for_updates, wxGBPosition (r, 0), wxGBSpan (1, 2));
+       ++r;
+
+       _check_for_test_updates = new CheckBox (_panel, _("Check for testing updates on startup"));
+       table->Add (_check_for_test_updates, wxGBPosition (r, 0), wxGBSpan (1, 2));
+       ++r;
+
+       _check_for_updates->Bind (wxEVT_CHECKBOX, bind (&GeneralPage::check_for_updates_changed, this));
+       _check_for_test_updates->Bind (wxEVT_CHECKBOX, bind (&GeneralPage::check_for_test_updates_changed, this));
+}
+
+void
+GeneralPage::config_changed ()
+{
+       auto config = Config::instance ();
+
+       checked_set (_set_language, static_cast<bool>(config->language()));
+
+       /* Backwards compatibility of config file */
+
+       map<string, string> compat_map;
+       compat_map["fr"] = "fr_FR";
+       compat_map["it"] = "it_IT";
+       compat_map["es"] = "es_ES";
+       compat_map["sv"] = "sv_SE";
+       compat_map["de"] = "de_DE";
+       compat_map["nl"] = "nl_NL";
+       compat_map["ru"] = "ru_RU";
+       compat_map["pl"] = "pl_PL";
+       compat_map["da"] = "da_DK";
+       compat_map["pt"] = "pt_PT";
+       compat_map["sk"] = "sk_SK";
+       compat_map["cs"] = "cs_CZ";
+       compat_map["uk"] = "uk_UA";
+
+       auto lang = config->language().get_value_or("en_GB");
+       if (compat_map.find(lang) != compat_map.end ()) {
+               lang = compat_map[lang];
        }
 
-       void language_changed ()
-       {
-               switch (_language->GetSelection ()) {
-               case 0:
-                       Config::instance()->set_language ("de");
-                       break;
-               case 1:
-                       Config::instance()->set_language ("en");
-                       break;
-               case 2:
-                       Config::instance()->set_language ("es");
-                       break;
-               case 3:
-                       Config::instance()->set_language ("fr");
-                       break;
-               case 4:
-                       Config::instance()->set_language ("it");
-                       break;
-               case 5:
-                       Config::instance()->set_language ("nl");
-                       break;
-               case 6:
-                       Config::instance()->set_language ("sv");
-                       break;
-               }
+       checked_set (_language, lang);
+
+       checked_set (_check_for_updates, config->check_for_updates ());
+       checked_set (_check_for_test_updates, config->check_for_test_updates ());
+
+       setup_sensitivity ();
+}
+
+void
+GeneralPage::setup_sensitivity ()
+{
+       _language->Enable (_set_language->GetValue ());
+       _check_for_test_updates->Enable (_check_for_updates->GetValue ());
+}
+
+void
+GeneralPage::set_language_changed ()
+{
+       setup_sensitivity ();
+       if (_set_language->GetValue ()) {
+               language_changed ();
+       } else {
+               Config::instance()->unset_language ();
        }
-       
-       void check_for_updates_changed ()
-       {
-               Config::instance()->set_check_for_updates (_check_for_updates->GetValue ());
+}
+
+void
+GeneralPage::language_changed ()
+{
+       int const sel = _language->GetSelection ();
+       if (sel != -1) {
+               Config::instance()->set_language (string_client_data (_language->GetClientObject (sel)));
+       } else {
+               Config::instance()->unset_language ();
        }
-       
-       void check_for_test_updates_changed ()
+}
+
+void
+GeneralPage::check_for_updates_changed ()
+{
+       Config::instance()->set_check_for_updates (_check_for_updates->GetValue ());
+}
+
+void
+GeneralPage::check_for_test_updates_changed ()
+{
+       Config::instance()->set_check_for_test_updates (_check_for_test_updates->GetValue ());
+}
+
+CertificateChainEditor::CertificateChainEditor (
+       wxWindow* parent,
+       wxString title,
+       int border,
+       function<void (shared_ptr<dcp::CertificateChain>)> set,
+       function<shared_ptr<const dcp::CertificateChain> (void)> get,
+       function<bool (void)> nag_alter
+       )
+       : wxDialog (parent, wxID_ANY, title)
+       , _set (set)
+       , _get (get)
+       , _nag_alter (nag_alter)
+{
+       _sizer = new wxBoxSizer (wxVERTICAL);
+
+       auto certificates_sizer = new wxBoxSizer (wxHORIZONTAL);
+       _sizer->Add (certificates_sizer, 0, wxALL, border);
+
+       _certificates = new wxListCtrl (this, wxID_ANY, wxDefaultPosition, wxSize (440, 150), wxLC_REPORT | wxLC_SINGLE_SEL);
+
        {
-               Config::instance()->set_check_for_test_updates (_check_for_test_updates->GetValue ());
+               wxListItem ip;
+               ip.SetId (0);
+               ip.SetText (_("Type"));
+               ip.SetWidth (100);
+               _certificates->InsertColumn (0, ip);
        }
 
-       void num_local_encoding_threads_changed ()
        {
-               Config::instance()->set_num_local_encoding_threads (_num_local_encoding_threads->GetValue ());
+               wxListItem ip;
+               ip.SetId (1);
+               ip.SetText (_("Thumbprint"));
+               ip.SetWidth (340);
+
+               wxFont font = ip.GetFont ();
+               font.SetFamily (wxFONTFAMILY_TELETYPE);
+               ip.SetFont (font);
+
+               _certificates->InsertColumn (1, ip);
        }
 
-       wxCheckBox* _set_language;
-       wxChoice* _language;
-       wxSpinCtrl* _num_local_encoding_threads;
-       wxCheckBox* _check_for_updates;
-       wxCheckBox* _check_for_test_updates;
-};
+       certificates_sizer->Add (_certificates, 1, wxEXPAND);
 
-class DefaultsPage : public wxPreferencesPage, public Page
-{
-public:
-       DefaultsPage (wxSize panel_size, int border)
-               : Page (panel_size, border)
-       {}
-       
-       wxString GetName () const
        {
-               return _("Defaults");
+               auto s = new wxBoxSizer (wxVERTICAL);
+               _add_certificate = new Button (this, _("Add..."));
+               s->Add (_add_certificate, 1, wxTOP | wxBOTTOM | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
+               _remove_certificate = new Button (this, _("Remove"));
+               s->Add (_remove_certificate, 1, wxTOP | wxBOTTOM | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
+               _export_certificate = new Button (this, _("Export certificate..."));
+               s->Add (_export_certificate, 1, wxTOP | wxBOTTOM | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
+               _export_chain = new Button (this, _("Export chain..."));
+               s->Add (_export_chain, 1, wxTOP | wxBOTTOM | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
+               certificates_sizer->Add (s, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP);
        }
 
-#ifdef DCPOMATIC_OSX   
-       wxBitmap GetLargeIcon () const
-       {
-               return wxBitmap ("defaults", wxBITMAP_TYPE_PNG_RESOURCE);
+       auto table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
+       _sizer->Add (table, 1, wxALL | wxEXPAND, border);
+       int r = 0;
+
+       add_label_to_sizer (table, this, _("Leaf private key"), true, wxGBPosition (r, 0));
+       _private_key = new StaticText (this, wxT(""));
+       wxFont font = _private_key->GetFont ();
+       font.SetFamily (wxFONTFAMILY_TELETYPE);
+       _private_key->SetFont (font);
+       table->Add (_private_key, wxGBPosition (r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
+       _import_private_key = new Button (this, _("Import..."));
+       table->Add (_import_private_key, wxGBPosition (r, 2));
+       _export_private_key = new Button (this, _("Export..."));
+       table->Add (_export_private_key, wxGBPosition (r, 3));
+       ++r;
+
+       _button_sizer = new wxBoxSizer (wxHORIZONTAL);
+       _remake_certificates = new Button (this, _("Re-make certificates and key..."));
+       _button_sizer->Add (_remake_certificates, 1, wxRIGHT, border);
+       table->Add (_button_sizer, wxGBPosition (r, 0), wxGBSpan (1, 4));
+       ++r;
+
+       _private_key_bad = new StaticText (this, _("Leaf private key does not match leaf certificate!"));
+       font = *wxSMALL_FONT;
+       font.SetWeight (wxFONTWEIGHT_BOLD);
+       _private_key_bad->SetFont (font);
+       table->Add (_private_key_bad, wxGBPosition (r, 0), wxGBSpan (1, 3));
+       ++r;
+
+       _add_certificate->Bind     (wxEVT_BUTTON,       bind (&CertificateChainEditor::add_certificate, this));
+       _remove_certificate->Bind  (wxEVT_BUTTON,       bind (&CertificateChainEditor::remove_certificate, this));
+       _export_certificate->Bind  (wxEVT_BUTTON,       bind (&CertificateChainEditor::export_certificate, this));
+       _certificates->Bind        (wxEVT_LIST_ITEM_SELECTED,   bind (&CertificateChainEditor::update_sensitivity, this));
+       _certificates->Bind        (wxEVT_LIST_ITEM_DESELECTED, bind (&CertificateChainEditor::update_sensitivity, this));
+       _remake_certificates->Bind (wxEVT_BUTTON,       bind (&CertificateChainEditor::remake_certificates, this));
+       _export_chain->Bind        (wxEVT_BUTTON,       bind (&CertificateChainEditor::export_chain, this));
+       _import_private_key->Bind  (wxEVT_BUTTON,       bind (&CertificateChainEditor::import_private_key, this));
+       _export_private_key->Bind  (wxEVT_BUTTON,       bind (&CertificateChainEditor::export_private_key, this));
+
+       auto buttons = CreateSeparatedButtonSizer (wxCLOSE);
+       if (buttons) {
+               _sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
        }
-#endif 
 
-       wxWindow* CreateWindow (wxWindow* parent)
-       {
-               wxPanel* panel = new wxPanel (parent);
-               wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
-               panel->SetSizer (s);
-
-               wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
-               table->AddGrowableCol (1, 1);
-               s->Add (table, 1, wxALL | wxEXPAND, _border);
-               
-               {
-                       add_label_to_sizer (table, panel, _("Default duration of still images"), true);
-                       wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-                       _still_length = new wxSpinCtrl (panel);
-                       s->Add (_still_length);
-                       add_label_to_sizer (s, panel, _("s"), false);
-                       table->Add (s, 1);
-               }
-               
-               add_label_to_sizer (table, panel, _("Default directory for new films"), true);
-#ifdef DCPOMATIC_USE_OWN_DIR_PICKER
-               _directory = new DirPickerCtrl (panel);
-#else  
-               _directory = new wxDirPickerCtrl (panel, wxDD_DIR_MUST_EXIST);
-#endif
-               table->Add (_directory, 1, wxEXPAND);
-               
-               add_label_to_sizer (table, panel, _("Default ISDCF name details"), true);
-               _isdcf_metadata_button = new wxButton (panel, wxID_ANY, _("Edit..."));
-               table->Add (_isdcf_metadata_button);
-
-               add_label_to_sizer (table, panel, _("Default scale to"), true);
-               _scale = new wxChoice (panel, wxID_ANY);
-               table->Add (_scale);
-               
-               add_label_to_sizer (table, panel, _("Default container"), true);
-               _container = new wxChoice (panel, wxID_ANY);
-               table->Add (_container);
-               
-               add_label_to_sizer (table, panel, _("Default content type"), true);
-               _dcp_content_type = new wxChoice (panel, wxID_ANY);
-               table->Add (_dcp_content_type);
-               
-               {
-                       add_label_to_sizer (table, panel, _("Default JPEG2000 bandwidth"), true);
-                       wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-                       _j2k_bandwidth = new wxSpinCtrl (panel);
-                       s->Add (_j2k_bandwidth);
-                       add_label_to_sizer (s, panel, _("Mbit/s"), false);
-                       table->Add (s, 1);
-               }
-               
-               {
-                       add_label_to_sizer (table, panel, _("Default audio delay"), true);
-                       wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-                       _audio_delay = new wxSpinCtrl (panel);
-                       s->Add (_audio_delay);
-                       add_label_to_sizer (s, panel, _("ms"), false);
-                       table->Add (s, 1);
-               }
+       SetSizerAndFit (_sizer);
+
+       update_certificate_list ();
+       update_private_key ();
+       update_sensitivity ();
+}
+
+void
+CertificateChainEditor::add_button (wxWindow* button)
+{
+       _button_sizer->Add (button, 0, wxLEFT | wxRIGHT, DCPOMATIC_SIZER_X_GAP);
+       _sizer->Layout ();
+}
 
-               add_label_to_sizer (table, panel, _("Default issuer"), true);
-               _issuer = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_issuer, 1, wxEXPAND);
-
-               add_label_to_sizer (table, panel, _("Default creator"), true);
-               _creator = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_creator, 1, wxEXPAND);
-               
-               Config* config = Config::instance ();
-               
-               _still_length->SetRange (1, 3600);
-               _still_length->SetValue (config->default_still_length ());
-               _still_length->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&DefaultsPage::still_length_changed, this));
-               
-               _directory->SetPath (std_to_wx (config->default_directory_or (wx_to_std (wxStandardPaths::Get().GetDocumentsDir())).string ()));
-               _directory->Bind (wxEVT_COMMAND_DIRPICKER_CHANGED, boost::bind (&DefaultsPage::directory_changed, this));
-               
-               _isdcf_metadata_button->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&DefaultsPage::edit_isdcf_metadata_clicked, this, parent));
-               
-               vector<Ratio const *> ratio = Ratio::all ();
-               int n = 0;
-               for (vector<Ratio const *>::iterator i = ratio.begin(); i != ratio.end(); ++i) {
-                       _scale->Append (std_to_wx ((*i)->nickname ()));
-                       if (*i == config->default_scale ()) {
-                               _scale->SetSelection (n);
+void
+CertificateChainEditor::add_certificate ()
+{
+       auto d = new wxFileDialog (this, _("Select Certificate File"));
+
+       if (d->ShowModal() == wxID_OK) {
+               try {
+                       dcp::Certificate c;
+                       string extra;
+                       try {
+                               extra = c.read_string (dcp::file_to_string (wx_to_std (d->GetPath ())));
+                       } catch (boost::filesystem::filesystem_error& e) {
+                               error_dialog (this, _("Could not import certificate (%s)"), d->GetPath());
+                               d->Destroy ();
+                               return;
                        }
-                       _container->Append (std_to_wx ((*i)->nickname ()));
-                       if (*i == config->default_container ()) {
-                               _container->SetSelection (n);
+
+                       if (!extra.empty ()) {
+                               message_dialog (
+                                       this,
+                                       _("This file contains other certificates (or other data) after its first certificate. "
+                                         "Only the first certificate will be used.")
+                                       );
                        }
-                       ++n;
-               }
-               
-               _scale->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&DefaultsPage::scale_changed, this));
-               _container->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&DefaultsPage::container_changed, this));
-               
-               vector<DCPContentType const *> const ct = DCPContentType::all ();
-               n = 0;
-               for (vector<DCPContentType const *>::const_iterator i = ct.begin(); i != ct.end(); ++i) {
-                       _dcp_content_type->Append (std_to_wx ((*i)->pretty_name ()));
-                       if (*i == config->default_dcp_content_type ()) {
-                               _dcp_content_type->SetSelection (n);
+                       auto chain = make_shared<dcp::CertificateChain>(*_get().get());
+                       chain->add (c);
+                       if (!chain->chain_valid ()) {
+                               error_dialog (
+                                       this,
+                                       _("Adding this certificate would make the chain inconsistent, so it will not be added. "
+                                         "Add certificates in order from root to intermediate to leaf.")
+                                       );
+                               chain->remove (c);
+                       } else {
+                               _set (chain);
+                               update_certificate_list ();
                        }
-                       ++n;
+               } catch (dcp::MiscError& e) {
+                       error_dialog (this, _("Could not read certificate file."), std_to_wx(e.what()));
                }
-               
-               _dcp_content_type->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&DefaultsPage::dcp_content_type_changed, this));
-               
-               _j2k_bandwidth->SetRange (50, 250);
-               _j2k_bandwidth->SetValue (config->default_j2k_bandwidth() / 1000000);
-               _j2k_bandwidth->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&DefaultsPage::j2k_bandwidth_changed, this));
-               
-               _audio_delay->SetRange (-1000, 1000);
-               _audio_delay->SetValue (config->default_audio_delay ());
-               _audio_delay->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&DefaultsPage::audio_delay_changed, this));
-
-               _issuer->SetValue (std_to_wx (config->dcp_metadata().issuer));
-               _issuer->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&DefaultsPage::issuer_changed, this));
-               _creator->SetValue (std_to_wx (config->dcp_metadata().creator));
-               _creator->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&DefaultsPage::creator_changed, this));
-
-               return panel;
-       }
-
-private:
-       void j2k_bandwidth_changed ()
-       {
-               Config::instance()->set_default_j2k_bandwidth (_j2k_bandwidth->GetValue() * 1000000);
-       }
-       
-       void audio_delay_changed ()
-       {
-               Config::instance()->set_default_audio_delay (_audio_delay->GetValue());
        }
 
-       void directory_changed ()
-       {
-               Config::instance()->set_default_directory (wx_to_std (_directory->GetPath ()));
-       }
+       d->Destroy ();
 
-       void edit_isdcf_metadata_clicked (wxWindow* parent)
-       {
-               ISDCFMetadataDialog* d = new ISDCFMetadataDialog (parent, Config::instance()->default_isdcf_metadata ());
-               d->ShowModal ();
-               Config::instance()->set_default_isdcf_metadata (d->isdcf_metadata ());
-               d->Destroy ();
-       }
+       update_sensitivity ();
+}
 
-       void still_length_changed ()
-       {
-               Config::instance()->set_default_still_length (_still_length->GetValue ());
+void
+CertificateChainEditor::remove_certificate ()
+{
+       if (_nag_alter()) {
+               /* Cancel was clicked */
+               return;
        }
 
-       void scale_changed ()
-       {
-               vector<Ratio const *> ratio = Ratio::all ();
-               Config::instance()->set_default_scale (ratio[_scale->GetSelection()]);
+       int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
+       if (i == -1) {
+               return;
        }
-       
-       void container_changed ()
-       {
-               vector<Ratio const *> ratio = Ratio::all ();
-               Config::instance()->set_default_container (ratio[_container->GetSelection()]);
+
+       _certificates->DeleteItem (i);
+       auto chain = make_shared<dcp::CertificateChain>(*_get().get());
+       chain->remove (i);
+       _set (chain);
+
+       update_sensitivity ();
+       update_certificate_list ();
+}
+
+void
+CertificateChainEditor::export_certificate ()
+{
+       int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
+       if (i == -1) {
+               return;
        }
-       
-       void dcp_content_type_changed ()
-       {
-               vector<DCPContentType const *> ct = DCPContentType::all ();
-               Config::instance()->set_default_dcp_content_type (ct[_dcp_content_type->GetSelection()]);
+
+       auto all = _get()->root_to_leaf();
+
+       wxString default_name;
+       if (i == 0) {
+               default_name = "root.pem";
+       } else if (i == static_cast<int>(all.size() - 1)) {
+               default_name = "leaf.pem";
+       } else {
+               default_name = "intermediate.pem";
        }
 
-       void issuer_changed ()
-       {
-               libdcp::XMLMetadata m = Config::instance()->dcp_metadata ();
-               m.issuer = wx_to_std (_issuer->GetValue ());
-               Config::instance()->set_dcp_metadata (m);
+       auto d = new wxFileDialog(
+               this, _("Select Certificate File"), wxEmptyString, default_name, wxT ("PEM files (*.pem)|*.pem"),
+               wxFD_SAVE | wxFD_OVERWRITE_PROMPT
+               );
+
+       auto j = all.begin ();
+       for (int k = 0; k < i; ++k) {
+               ++j;
        }
-       
-       void creator_changed ()
-       {
-               libdcp::XMLMetadata m = Config::instance()->dcp_metadata ();
-               m.creator = wx_to_std (_creator->GetValue ());
-               Config::instance()->set_dcp_metadata (m);
-       }
-       
-       wxSpinCtrl* _j2k_bandwidth;
-       wxSpinCtrl* _audio_delay;
-       wxButton* _isdcf_metadata_button;
-       wxSpinCtrl* _still_length;
-#ifdef DCPOMATIC_USE_OWN_DIR_PICKER
-       DirPickerCtrl* _directory;
-#else
-       wxDirPickerCtrl* _directory;
-#endif
-       wxChoice* _scale;
-       wxChoice* _container;
-       wxChoice* _dcp_content_type;
-       wxTextCtrl* _issuer;
-       wxTextCtrl* _creator;
-};
-
-class EncodingServersPage : public wxPreferencesPage, public Page
-{
-public:
-       EncodingServersPage (wxSize panel_size, int border)
-               : Page (panel_size, border)
-       {}
-       
-       wxString GetName () const
-       {
-               return _("Servers");
+
+       if (d->ShowModal () == wxID_OK) {
+               boost::filesystem::path path (wx_to_std(d->GetPath()));
+               if (path.extension() != ".pem") {
+                       path += ".pem";
+               }
+               auto f = fopen_boost (path, "w");
+               if (!f) {
+                       throw OpenFileError (path, errno, OpenFileError::WRITE);
+               }
+
+               string const s = j->certificate (true);
+               checked_fwrite (s.c_str(), s.length(), f, path);
+               fclose (f);
        }
+       d->Destroy ();
+}
 
-#ifdef DCPOMATIC_OSX   
-       wxBitmap GetLargeIcon () const
-       {
-               return wxBitmap ("servers", wxBITMAP_TYPE_PNG_RESOURCE);
+void
+CertificateChainEditor::export_chain ()
+{
+       auto d = new wxFileDialog (
+               this, _("Select Chain File"), wxEmptyString, wxT("certificate_chain.pem"), wxT("PEM files (*.pem)|*.pem"),
+               wxFD_SAVE | wxFD_OVERWRITE_PROMPT
+               );
+
+       if (d->ShowModal () == wxID_OK) {
+               boost::filesystem::path path (wx_to_std(d->GetPath()));
+               if (path.extension() != ".pem") {
+                       path += ".pem";
+               }
+               auto f = fopen_boost (path, "w");
+               if (!f) {
+                       throw OpenFileError (path, errno, OpenFileError::WRITE);
+               }
+
+               auto const s = _get()->chain();
+               checked_fwrite (s.c_str(), s.length(), f, path);
+               fclose (f);
        }
-#endif 
 
-       wxWindow* CreateWindow (wxWindow* parent)
-       {
-               wxPanel* panel = new wxPanel (parent, wxID_ANY, wxDefaultPosition, _panel_size);
-               wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
-               panel->SetSizer (s);
-               
-               _use_any_servers = new wxCheckBox (panel, wxID_ANY, _("Use all servers"));
-               s->Add (_use_any_servers, 0, wxALL, _border);
-               
-               vector<string> columns;
-               columns.push_back (wx_to_std (_("IP address / host name")));
-               _servers_list = new EditableList<string, ServerDialog> (
-                       panel,
-                       columns,
-                       boost::bind (&Config::servers, Config::instance()),
-                       boost::bind (&Config::set_servers, Config::instance(), _1),
-                       boost::bind (&EncodingServersPage::server_column, this, _1)
-                       );
-               
-               s->Add (_servers_list, 1, wxEXPAND | wxALL, _border);
-               
-               _use_any_servers->SetValue (Config::instance()->use_any_servers ());
-               _use_any_servers->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&EncodingServersPage::use_any_servers_changed, this));
+       d->Destroy ();
+}
+
+void
+CertificateChainEditor::update_certificate_list ()
+{
+       _certificates->DeleteAllItems ();
+       size_t n = 0;
+       auto certs = _get()->root_to_leaf();
+       for (auto const& i: certs) {
+               wxListItem item;
+               item.SetId (n);
+               _certificates->InsertItem (item);
+               _certificates->SetItem (n, 1, std_to_wx (i.thumbprint ()));
+
+               if (n == 0) {
+                       _certificates->SetItem (n, 0, _("Root"));
+               } else if (n == (certs.size() - 1)) {
+                       _certificates->SetItem (n, 0, _("Leaf"));
+               } else {
+                       _certificates->SetItem (n, 0, _("Intermediate"));
+               }
 
-               return panel;
+               ++n;
        }
 
-private:       
+       static wxColour normal = _private_key_bad->GetForegroundColour ();
 
-       void use_any_servers_changed ()
-       {
-               Config::instance()->set_use_any_servers (_use_any_servers->GetValue ());
+       if (_get()->private_key_valid()) {
+               _private_key_bad->Hide ();
+               _private_key_bad->SetForegroundColour (normal);
+       } else {
+               _private_key_bad->Show ();
+               _private_key_bad->SetForegroundColour (wxColour (255, 0, 0));
        }
+}
 
-       string server_column (string s)
-       {
-               return s;
+void
+CertificateChainEditor::remake_certificates ()
+{
+       auto chain = _get();
+
+       string subject_organization_name;
+       string subject_organizational_unit_name;
+       string root_common_name;
+       string intermediate_common_name;
+       string leaf_common_name;
+
+       auto all = chain->root_to_leaf ();
+
+       if (all.size() >= 1) {
+               /* Have a root */
+               subject_organization_name = chain->root().subject_organization_name ();
+               subject_organizational_unit_name = chain->root().subject_organizational_unit_name ();
+               root_common_name = chain->root().subject_common_name ();
        }
 
-       wxCheckBox* _use_any_servers;
-       EditableList<string, ServerDialog>* _servers_list;
-};
+       if (all.size() >= 2) {
+               /* Have a leaf */
+               leaf_common_name = chain->leaf().subject_common_name ();
+       }
 
-class ColourConversionsPage : public wxPreferencesPage, public Page
-{
-public:
-       ColourConversionsPage (wxSize panel_size, int border)
-               : Page (panel_size, border)
-       {}
-       
-       wxString GetName () const
-       {
-               return _("Colour Conversions");
+       if (all.size() >= 3) {
+               /* Have an intermediate */
+               dcp::CertificateChain::List::iterator i = all.begin ();
+               ++i;
+               intermediate_common_name = i->subject_common_name ();
        }
 
-#ifdef DCPOMATIC_OSX   
-       wxBitmap GetLargeIcon () const
-       {
-               return wxBitmap ("colour_conversions", wxBITMAP_TYPE_PNG_RESOURCE);
+       if (_nag_alter()) {
+               /* Cancel was clicked */
+               return;
        }
-#endif 
-       wxWindow* CreateWindow (wxWindow* parent)
-       {
-               wxPanel* panel = new wxPanel (parent, wxID_ANY, wxDefaultPosition, _panel_size);
-               wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
-               panel->SetSizer (s);
-
-               vector<string> columns;
-               columns.push_back (wx_to_std (_("Name")));
-               wxPanel* list = new EditableList<PresetColourConversion, PresetColourConversionDialog> (
-                       panel,
-                       columns,
-                       boost::bind (&Config::colour_conversions, Config::instance()),
-                       boost::bind (&Config::set_colour_conversions, Config::instance(), _1),
-                       boost::bind (&ColourConversionsPage::colour_conversion_column, this, _1),
-                       300
+
+       auto d = new MakeChainDialog (
+               this,
+               subject_organization_name,
+               subject_organizational_unit_name,
+               root_common_name,
+               intermediate_common_name,
+               leaf_common_name
+               );
+
+       if (d->ShowModal () == wxID_OK) {
+               _set (
+                       make_shared<dcp::CertificateChain> (
+                               openssl_path (),
+                               d->organisation (),
+                               d->organisational_unit (),
+                               d->root_common_name (),
+                               d->intermediate_common_name (),
+                               d->leaf_common_name ()
+                               )
                        );
 
-               s->Add (list, 1, wxEXPAND | wxALL, _border);
-               return panel;
+               update_certificate_list ();
+               update_private_key ();
        }
 
-private:
-       string colour_conversion_column (PresetColourConversion c)
-       {
-               return c.name;
-       }
-};
+       d->Destroy ();
+}
 
-class TMSPage : public wxPreferencesPage, public Page
+void
+CertificateChainEditor::update_sensitivity ()
 {
-public:
-       TMSPage (wxSize panel_size, int border)
-               : Page (panel_size, border)
-       {}
+       /* We can only remove the leaf certificate */
+       _remove_certificate->Enable (_certificates->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED) == (_certificates->GetItemCount() - 1));
+       _export_certificate->Enable (_certificates->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED) != -1);
+}
 
-       wxString GetName () const
-       {
-               return _("TMS");
-       }
+void
+CertificateChainEditor::update_private_key ()
+{
+       checked_set (_private_key, dcp::private_key_fingerprint (_get()->key().get()));
+       _sizer->Layout ();
+}
 
-#ifdef DCPOMATIC_OSX   
-       wxBitmap GetLargeIcon () const
-       {
-               return wxBitmap ("tms", wxBITMAP_TYPE_PNG_RESOURCE);
+void
+CertificateChainEditor::import_private_key ()
+{
+       auto d = new wxFileDialog (this, _("Select Key File"));
+
+       if (d->ShowModal() == wxID_OK) {
+               try {
+                       boost::filesystem::path p (wx_to_std (d->GetPath ()));
+                       if (boost::filesystem::file_size (p) > 8192) {
+                               error_dialog (
+                                       this,
+                                       wxString::Format (_("Could not read key file; file is too long (%s)"), std_to_wx (p.string ()))
+                                       );
+                               return;
+                       }
+
+                       auto chain = make_shared<dcp::CertificateChain>(*_get().get());
+                       chain->set_key (dcp::file_to_string (p));
+                       _set (chain);
+                       update_private_key ();
+               } catch (dcp::MiscError& e) {
+                       error_dialog (this, _("Could not read certificate file."), std_to_wx(e.what()));
+               }
        }
-#endif 
 
-       wxWindow* CreateWindow (wxWindow* parent)
-       {
-               wxPanel* panel = new wxPanel (parent, wxID_ANY, wxDefaultPosition, _panel_size);
-               wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
-               panel->SetSizer (s);
-
-               wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
-               table->AddGrowableCol (1, 1);
-               s->Add (table, 1, wxALL | wxEXPAND, _border);
-               
-               add_label_to_sizer (table, panel, _("IP address"), true);
-               _tms_ip = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_tms_ip, 1, wxEXPAND);
-               
-               add_label_to_sizer (table, panel, _("Target path"), true);
-               _tms_path = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_tms_path, 1, wxEXPAND);
-               
-               add_label_to_sizer (table, panel, _("User name"), true);
-               _tms_user = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_tms_user, 1, wxEXPAND);
-               
-               add_label_to_sizer (table, panel, _("Password"), true);
-               _tms_password = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_tms_password, 1, wxEXPAND);
-               
-               Config* config = Config::instance ();
-               
-               _tms_ip->SetValue (std_to_wx (config->tms_ip ()));
-               _tms_ip->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&TMSPage::tms_ip_changed, this));
-               _tms_path->SetValue (std_to_wx (config->tms_path ()));
-               _tms_path->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&TMSPage::tms_path_changed, this));
-               _tms_user->SetValue (std_to_wx (config->tms_user ()));
-               _tms_user->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&TMSPage::tms_user_changed, this));
-               _tms_password->SetValue (std_to_wx (config->tms_password ()));
-               _tms_password->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&TMSPage::tms_password_changed, this));
-
-               return panel;
-       }
-
-private:
-       void tms_ip_changed ()
-       {
-               Config::instance()->set_tms_ip (wx_to_std (_tms_ip->GetValue ()));
+       d->Destroy ();
+
+       update_sensitivity ();
+}
+
+void
+CertificateChainEditor::export_private_key ()
+{
+       auto key = _get()->key();
+       if (!key) {
+               return;
        }
-       
-       void tms_path_changed ()
-       {
-               Config::instance()->set_tms_path (wx_to_std (_tms_path->GetValue ()));
+
+       auto d = new wxFileDialog (
+               this, _("Select Key File"), wxEmptyString, wxT("private_key.pem"), wxT("PEM files (*.pem)|*.pem"),
+               wxFD_SAVE | wxFD_OVERWRITE_PROMPT
+               );
+
+       if (d->ShowModal () == wxID_OK) {
+               boost::filesystem::path path (wx_to_std(d->GetPath()));
+               if (path.extension() != ".pem") {
+                       path += ".pem";
+               }
+               auto f = fopen_boost (path, "w");
+               if (!f) {
+                       throw OpenFileError (path, errno, OpenFileError::WRITE);
+               }
+
+               auto const s = _get()->key().get ();
+               checked_fwrite (s.c_str(), s.length(), f, path);
+               fclose (f);
        }
-       
-       void tms_user_changed ()
+       d->Destroy ();
+}
+
+wxString
+KeysPage::GetName () const
+{
+       return _("Keys");
+}
+
+void
+KeysPage::setup ()
+{
+       wxFont subheading_font (*wxNORMAL_FONT);
+       subheading_font.SetWeight (wxFONTWEIGHT_BOLD);
+
+       auto sizer = _panel->GetSizer();
+
        {
-               Config::instance()->set_tms_user (wx_to_std (_tms_user->GetValue ()));
+               auto m = new StaticText (_panel, _("Decrypting KDMs"));
+               m->SetFont (subheading_font);
+               sizer->Add (m, 0, wxALL, _border);
        }
-       
-       void tms_password_changed ()
+
+       auto buttons = new wxBoxSizer (wxVERTICAL);
+
+       auto export_decryption_certificate = new Button (_panel, _("Export KDM decryption leaf certificate..."));
+       buttons->Add (export_decryption_certificate, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
+       auto export_settings = new Button (_panel, _("Export all KDM decryption settings..."));
+       buttons->Add (export_settings, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
+       auto import_settings = new Button (_panel, _("Import all KDM decryption settings..."));
+       buttons->Add (import_settings, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
+       auto decryption_advanced = new Button (_panel, _("Advanced..."));
+       buttons->Add (decryption_advanced, 0);
+
+       sizer->Add (buttons, 0, wxLEFT, _border);
+
+       export_decryption_certificate->Bind (wxEVT_BUTTON, bind (&KeysPage::export_decryption_certificate, this));
+       export_settings->Bind (wxEVT_BUTTON, bind (&KeysPage::export_decryption_chain_and_key, this));
+       import_settings->Bind (wxEVT_BUTTON, bind (&KeysPage::import_decryption_chain_and_key, this));
+       decryption_advanced->Bind (wxEVT_BUTTON, bind (&KeysPage::decryption_advanced, this));
+
        {
-               Config::instance()->set_tms_password (wx_to_std (_tms_password->GetValue ()));
+               auto m = new StaticText (_panel, _("Signing DCPs and KDMs"));
+               m->SetFont (subheading_font);
+               sizer->Add (m, 0, wxALL, _border);
        }
 
-       wxTextCtrl* _tms_ip;
-       wxTextCtrl* _tms_path;
-       wxTextCtrl* _tms_user;
-       wxTextCtrl* _tms_password;
-};
+       auto signing_advanced = new Button (_panel, _("Advanced..."));
+       sizer->Add (signing_advanced, 0, wxLEFT | wxBOTTOM, _border);
+       signing_advanced->Bind (wxEVT_BUTTON, bind (&KeysPage::signing_advanced, this));
+}
 
-class KDMEmailPage : public wxPreferencesPage, public Page
+void
+KeysPage::decryption_advanced ()
 {
-public:
+       auto c = new CertificateChainEditor (
+               _panel, _("Decrypting KDMs"), _border,
+               bind(&Config::set_decryption_chain, Config::instance(), _1),
+               bind(&Config::decryption_chain, Config::instance()),
+               bind(&KeysPage::nag_alter_decryption_chain, this)
+               );
+
+       c->ShowModal();
+}
 
-       KDMEmailPage (wxSize panel_size, int border)
-               : Page (panel_size, border)
-       {}
-       
-       wxString GetName () const
-       {
-               return _("KDM Email");
+void
+KeysPage::signing_advanced ()
+{
+       auto c = new CertificateChainEditor (
+               _panel, _("Signing DCPs and KDMs"), _border,
+               bind(&Config::set_signer_chain, Config::instance(), _1),
+               bind(&Config::signer_chain, Config::instance()),
+               bind(&do_nothing)
+               );
+
+       c->ShowModal();
+}
+
+void
+KeysPage::export_decryption_chain_and_key ()
+{
+       auto d = new wxFileDialog (
+               _panel, _("Select Export File"), wxEmptyString, wxEmptyString, wxT ("DOM files (*.dom)|*.dom"),
+               wxFD_SAVE | wxFD_OVERWRITE_PROMPT
+               );
+
+       if (d->ShowModal () == wxID_OK) {
+               boost::filesystem::path path (wx_to_std(d->GetPath()));
+               auto f = fopen_boost (path, "w");
+               if (!f) {
+                       throw OpenFileError (path, errno, OpenFileError::WRITE);
+               }
+
+               auto const chain = Config::instance()->decryption_chain()->chain();
+               checked_fwrite (chain.c_str(), chain.length(), f, path);
+               optional<string> const key = Config::instance()->decryption_chain()->key();
+               DCPOMATIC_ASSERT (key);
+               checked_fwrite (key->c_str(), key->length(), f, path);
+               fclose (f);
        }
+       d->Destroy ();
 
-#ifdef DCPOMATIC_OSX   
-       wxBitmap GetLargeIcon () const
-       {
-               return wxBitmap ("kdm_email", wxBITMAP_TYPE_PNG_RESOURCE);
+}
+
+void
+KeysPage::import_decryption_chain_and_key ()
+{
+       if (NagDialog::maybe_nag (
+                   _panel,
+                   Config::NAG_IMPORT_DECRYPTION_CHAIN,
+                   _("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!"),
+                   true
+                   )) {
+               return;
        }
-#endif 
 
-       wxWindow* CreateWindow (wxWindow* parent)
-       {
-               /* We have to force both width and height of this one */
-#ifdef DCPOMATIC_OSX
-               wxPanel* panel = new wxPanel (parent, wxID_ANY, wxDefaultPosition, wxSize (480, 128));
-#else          
-               wxPanel* panel = new wxPanel (parent);
-#endif         
-               wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
-               panel->SetSizer (s);
-
-               wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
-               table->AddGrowableCol (1, 1);
-               s->Add (table, 1, wxEXPAND | wxALL, _border);
-
-               add_label_to_sizer (table, panel, _("Outgoing mail server"), true);
-               _mail_server = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_mail_server, 1, wxEXPAND | wxALL);
-               
-               add_label_to_sizer (table, panel, _("Mail user name"), true);
-               _mail_user = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_mail_user, 1, wxEXPAND | wxALL);
-               
-               add_label_to_sizer (table, panel, _("Mail password"), true);
-               _mail_password = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_mail_password, 1, wxEXPAND | wxALL);
-               
-               wxStaticText* plain = add_label_to_sizer (table, panel, _("(password will be stored on disk in plaintext)"), false);
-               wxFont font = plain->GetFont();
-               font.SetStyle (wxFONTSTYLE_ITALIC);
-               font.SetPointSize (font.GetPointSize() - 1);
-               plain->SetFont (font);
-               table->AddSpacer (0);
-
-               add_label_to_sizer (table, panel, _("Subject"), true);
-               _kdm_subject = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_kdm_subject, 1, wxEXPAND | wxALL);
-               
-               add_label_to_sizer (table, panel, _("From address"), true);
-               _kdm_from = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_kdm_from, 1, wxEXPAND | wxALL);
-
-               add_label_to_sizer (table, panel, _("CC address"), true);
-               _kdm_cc = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_kdm_cc, 1, wxEXPAND | wxALL);
-
-               add_label_to_sizer (table, panel, _("BCC address"), true);
-               _kdm_bcc = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_kdm_bcc, 1, wxEXPAND | wxALL);
-               
-               _kdm_email = new wxTextCtrl (panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize (480, 128), wxTE_MULTILINE);
-               s->Add (_kdm_email, 1, wxEXPAND | wxALL, _border);
-
-               _reset_kdm_email = new wxButton (panel, wxID_ANY, _("Reset to default text"));
-               s->Add (_reset_kdm_email, 0, wxEXPAND | wxALL, _border);
-
-               Config* config = Config::instance ();
-               _mail_server->SetValue (std_to_wx (config->mail_server ()));
-               _mail_server->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&KDMEmailPage::mail_server_changed, this));
-               _mail_user->SetValue (std_to_wx (config->mail_user ()));
-               _mail_user->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&KDMEmailPage::mail_user_changed, this));
-               _mail_password->SetValue (std_to_wx (config->mail_password ()));
-               _mail_password->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&KDMEmailPage::mail_password_changed, this));
-               _kdm_subject->SetValue (std_to_wx (config->kdm_subject ()));
-               _kdm_subject->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&KDMEmailPage::kdm_subject_changed, this));
-               _kdm_from->SetValue (std_to_wx (config->kdm_from ()));
-               _kdm_from->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&KDMEmailPage::kdm_from_changed, this));
-               _kdm_cc->SetValue (std_to_wx (config->kdm_cc ()));
-               _kdm_cc->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&KDMEmailPage::kdm_cc_changed, this));
-               _kdm_bcc->SetValue (std_to_wx (config->kdm_bcc ()));
-               _kdm_bcc->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&KDMEmailPage::kdm_bcc_changed, this));
-               _kdm_email->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&KDMEmailPage::kdm_email_changed, this));
-               _kdm_email->SetValue (std_to_wx (Config::instance()->kdm_email ()));
-               _reset_kdm_email->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KDMEmailPage::reset_kdm_email, this));
-
-               return panel;
-       }
-
-private:
-       void mail_server_changed ()
-       {
-               Config::instance()->set_mail_server (wx_to_std (_mail_server->GetValue ()));
+       auto d = new wxFileDialog (
+               _panel, _("Select File To Import"), wxEmptyString, wxEmptyString, wxT ("DOM files (*.dom)|*.dom")
+               );
+
+       if (d->ShowModal () == wxID_OK) {
+               auto new_chain = make_shared<dcp::CertificateChain>();
+
+               FILE* f = fopen_boost (wx_to_std (d->GetPath ()), "r");
+               if (!f) {
+                       throw OpenFileError (wx_to_std (d->GetPath ()), errno, OpenFileError::WRITE);
+               }
+
+               string current;
+               while (!feof (f)) {
+                       char buffer[128];
+                       if (fgets (buffer, 128, f) == 0) {
+                               break;
+                       }
+                       current += buffer;
+                       if (strncmp (buffer, "-----END CERTIFICATE-----", 25) == 0) {
+                               new_chain->add (dcp::Certificate (current));
+                               current = "";
+                       } else if (strncmp (buffer, "-----END RSA PRIVATE KEY-----", 29) == 0) {
+                               new_chain->set_key (current);
+                               current = "";
+                       }
+               }
+               fclose (f);
+
+               if (new_chain->chain_valid() && new_chain->private_key_valid()) {
+                       Config::instance()->set_decryption_chain (new_chain);
+               } else {
+                       error_dialog (_panel, _("Invalid DCP-o-matic export file"));
+               }
        }
-       
-       void mail_user_changed ()
-       {
-               Config::instance()->set_mail_user (wx_to_std (_mail_user->GetValue ()));
+       d->Destroy ();
+}
+
+bool
+KeysPage::nag_alter_decryption_chain ()
+{
+       return NagDialog::maybe_nag (
+               _panel,
+               Config::NAG_ALTER_DECRYPTION_CHAIN,
+               _("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!"),
+               true
+               );
+}
+
+void
+KeysPage::export_decryption_certificate ()
+{
+       auto config = Config::instance();
+       wxString default_name = "dcpomatic";
+       if (!config->dcp_creator().empty()) {
+               default_name += "_" + std_to_wx(careful_string_filter(config->dcp_creator()));
        }
-       
-       void mail_password_changed ()
-       {
-               Config::instance()->set_mail_password (wx_to_std (_mail_password->GetValue ()));
+       if (!config->dcp_issuer().empty()) {
+               default_name += "_" + std_to_wx(careful_string_filter(config->dcp_issuer()));
        }
+       default_name += wxT("_kdm_decryption_cert.pem");
 
-       void kdm_subject_changed ()
-       {
-               Config::instance()->set_kdm_subject (wx_to_std (_kdm_subject->GetValue ()));
+       auto d = new wxFileDialog (
+               _panel, _("Select Certificate File"), wxEmptyString, default_name, wxT("PEM files (*.pem)|*.pem"),
+               wxFD_SAVE | wxFD_OVERWRITE_PROMPT
+               );
+
+       if (d->ShowModal () == wxID_OK) {
+               boost::filesystem::path path (wx_to_std(d->GetPath()));
+               if (path.extension() != ".pem") {
+                       path += ".pem";
+               }
+               auto f = fopen_boost (path, "w");
+               if (!f) {
+                       throw OpenFileError (path, errno, OpenFileError::WRITE);
+               }
+
+               auto const s = Config::instance()->decryption_chain()->leaf().certificate (true);
+               checked_fwrite (s.c_str(), s.length(), f, path);
+               fclose (f);
        }
-       
-       void kdm_from_changed ()
-       {
-               Config::instance()->set_kdm_from (wx_to_std (_kdm_from->GetValue ()));
+
+       d->Destroy ();
+}
+
+wxString
+SoundPage::GetName () const
+{
+       return _("Sound");
+}
+
+void
+SoundPage::setup ()
+{
+       auto table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
+       _panel->GetSizer()->Add (table, 1, wxALL | wxEXPAND, _border);
+
+       int r = 0;
+
+       _sound = new CheckBox (_panel, _("Play sound via"));
+       table->Add (_sound, wxGBPosition (r, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
+       wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
+       _sound_output = new wxChoice (_panel, wxID_ANY);
+       s->Add (_sound_output, 0);
+       _sound_output_details = new wxStaticText (_panel, wxID_ANY, wxT(""));
+       s->Add (_sound_output_details, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, DCPOMATIC_SIZER_X_GAP);
+       table->Add (s, wxGBPosition(r, 1));
+       ++r;
+
+       add_label_to_sizer (table, _panel, _("Mapping"), true, wxGBPosition(r, 0));
+       _map = new AudioMappingView (_panel, _("DCP"), _("DCP"), _("Output"), _("output"));
+       _map->SetSize (-1, 600);
+       table->Add (_map, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND);
+       ++r;
+
+       _reset_to_default = new Button (_panel, _("Reset to default"));
+       table->Add (_reset_to_default, wxGBPosition(r, 1));
+       ++r;
+
+       wxFont font = _sound_output_details->GetFont();
+       font.SetStyle (wxFONTSTYLE_ITALIC);
+       font.SetPointSize (font.GetPointSize() - 1);
+       _sound_output_details->SetFont (font);
+
+       RtAudio audio (DCPOMATIC_RTAUDIO_API);
+       for (unsigned int i = 0; i < audio.getDeviceCount(); ++i) {
+               try {
+                       auto dev = audio.getDeviceInfo (i);
+                       if (dev.probed && dev.outputChannels > 0) {
+                               _sound_output->Append (std_to_wx (dev.name));
+                       }
+               } catch (RtAudioError&) {
+                       /* Something went wrong so let's just ignore that device */
+               }
        }
 
-       void kdm_cc_changed ()
-       {
-               Config::instance()->set_kdm_cc (wx_to_std (_kdm_cc->GetValue ()));
+       _sound->Bind        (wxEVT_CHECKBOX, bind(&SoundPage::sound_changed, this));
+       _sound_output->Bind (wxEVT_CHOICE,   bind(&SoundPage::sound_output_changed, this));
+       _map->Changed.connect (bind(&SoundPage::map_changed, this, _1));
+       _reset_to_default->Bind (wxEVT_BUTTON,   bind(&SoundPage::reset_to_default, this));
+}
+
+void
+SoundPage::reset_to_default ()
+{
+       Config::instance()->set_audio_mapping_to_default ();
+}
+
+void
+SoundPage::map_changed (AudioMapping m)
+{
+       Config::instance()->set_audio_mapping (m);
+}
+
+void
+SoundPage::sound_changed ()
+{
+       Config::instance()->set_sound (_sound->GetValue ());
+}
+
+void
+SoundPage::sound_output_changed ()
+{
+       RtAudio audio (DCPOMATIC_RTAUDIO_API);
+       auto const so = get_sound_output();
+       string default_device;
+       try {
+               default_device = audio.getDeviceInfo(audio.getDefaultOutputDevice()).name;
+       } catch (RtAudioError&) {
+               /* Never mind */
+       }
+       if (!so || *so == default_device) {
+               Config::instance()->unset_sound_output ();
+       } else {
+               Config::instance()->set_sound_output (*so);
        }
+}
 
-       void kdm_bcc_changed ()
-       {
-               Config::instance()->set_kdm_bcc (wx_to_std (_kdm_bcc->GetValue ()));
+void
+SoundPage::config_changed ()
+{
+       auto config = Config::instance ();
+
+       checked_set (_sound, config->sound ());
+
+       auto const current_so = get_sound_output ();
+       optional<string> configured_so;
+
+       if (config->sound_output()) {
+               configured_so = config->sound_output().get();
+       } else {
+               /* No configured output means we should use the default */
+               RtAudio audio (DCPOMATIC_RTAUDIO_API);
+               try {
+                       configured_so = audio.getDeviceInfo(audio.getDefaultOutputDevice()).name;
+               } catch (RtAudioError&) {
+                       /* Probably no audio devices at all */
+               }
        }
-       
-       void kdm_email_changed ()
-       {
-               Config::instance()->set_kdm_email (wx_to_std (_kdm_email->GetValue ()));
+
+       if (configured_so && current_so != configured_so) {
+               /* Update _sound_output with the configured value */
+               unsigned int i = 0;
+               while (i < _sound_output->GetCount()) {
+                       if (_sound_output->GetString(i) == std_to_wx(*configured_so)) {
+                               _sound_output->SetSelection (i);
+                               break;
+                       }
+                       ++i;
+               }
        }
 
-       void reset_kdm_email ()
-       {
-               Config::instance()->reset_kdm_email ();
-               _kdm_email->SetValue (wx_to_std (Config::instance()->kdm_email ()));
-       }
-
-       wxTextCtrl* _mail_server;
-       wxTextCtrl* _mail_user;
-       wxTextCtrl* _mail_password;
-       wxTextCtrl* _kdm_subject;
-       wxTextCtrl* _kdm_from;
-       wxTextCtrl* _kdm_cc;
-       wxTextCtrl* _kdm_bcc;
-       wxTextCtrl* _kdm_email;
-       wxButton* _reset_kdm_email;
-};
-
-class AdvancedPage : public wxStockPreferencesPage, public Page
-{
-public:
-
-       AdvancedPage (wxSize panel_size, int border)
-               : wxStockPreferencesPage (Kind_Advanced)
-               , Page (panel_size, border)
-       {}
-       
-       wxWindow* CreateWindow (wxWindow* parent)
-       {
-               wxPanel* panel = new wxPanel (parent);
-
-               wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
-               panel->SetSizer (s);
-
-               wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
-               table->AddGrowableCol (1, 1);
-               s->Add (table, 1, wxALL | wxEXPAND, _border);
-
-               {
-                       add_label_to_sizer (table, panel, _("Maximum JPEG2000 bandwidth"), true);
-                       wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-                       _maximum_j2k_bandwidth = new wxSpinCtrl (panel);
-                       s->Add (_maximum_j2k_bandwidth, 1);
-                       add_label_to_sizer (s, panel, _("Mbit/s"), false);
-                       table->Add (s, 1);
+       RtAudio audio (DCPOMATIC_RTAUDIO_API);
+
+       map<int, wxString> apis;
+       apis[RtAudio::MACOSX_CORE]    = _("CoreAudio");
+       apis[RtAudio::WINDOWS_ASIO]   = _("ASIO");
+       apis[RtAudio::WINDOWS_DS]     = _("Direct Sound");
+       apis[RtAudio::WINDOWS_WASAPI] = _("WASAPI");
+       apis[RtAudio::UNIX_JACK]      = _("JACK");
+       apis[RtAudio::LINUX_ALSA]     = _("ALSA");
+       apis[RtAudio::LINUX_PULSE]    = _("PulseAudio");
+       apis[RtAudio::LINUX_OSS]      = _("OSS");
+       apis[RtAudio::RTAUDIO_DUMMY]  = _("Dummy");
+
+       int channels = 0;
+       if (configured_so) {
+               for (unsigned int i = 0; i < audio.getDeviceCount(); ++i) {
+                       try {
+                               auto info = audio.getDeviceInfo(i);
+                               if (info.name == *configured_so && info.outputChannels > 0) {
+                                       channels = info.outputChannels;
+                               }
+                       } catch (RtAudioError&) {
+                               /* Never mind */
+                       }
                }
+       }
 
-               _allow_any_dcp_frame_rate = new wxCheckBox (panel, wxID_ANY, _("Allow any DCP frame rate"));
-               table->Add (_allow_any_dcp_frame_rate, 1, wxEXPAND | wxALL);
-               table->AddSpacer (0);
-
-               add_label_to_sizer (table, panel, _("Log"), true);
-               _log_general = new wxCheckBox (panel, wxID_ANY, _("General"));
-               table->Add (_log_general, 1, wxEXPAND | wxALL);
-               _log_warning = new wxCheckBox (panel, wxID_ANY, _("Warnings"));
-               table->AddSpacer (0);
-               table->Add (_log_warning, 1, wxEXPAND | wxALL);
-               _log_error = new wxCheckBox (panel, wxID_ANY, _("Errors"));
-               table->AddSpacer (0);
-               table->Add (_log_error, 1, wxEXPAND | wxALL);
-               _log_timing = new wxCheckBox (panel, wxID_ANY, S_("Config|Timing"));
-               table->AddSpacer (0);
-               table->Add (_log_timing, 1, wxEXPAND | wxALL);
-
-               Config* config = Config::instance ();
-               
-               _maximum_j2k_bandwidth->SetRange (1, 500);
-               _maximum_j2k_bandwidth->SetValue (config->maximum_j2k_bandwidth() / 1000000);
-               _maximum_j2k_bandwidth->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&AdvancedPage::maximum_j2k_bandwidth_changed, this));
-               _allow_any_dcp_frame_rate->SetValue (config->allow_any_dcp_frame_rate ());
-               _allow_any_dcp_frame_rate->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::allow_any_dcp_frame_rate_changed, this));
-               _log_general->SetValue (config->log_types() & Log::TYPE_GENERAL);
-               _log_general->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::log_changed, this));
-               _log_warning->SetValue (config->log_types() & Log::TYPE_WARNING);
-               _log_warning->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::log_changed, this));
-               _log_error->SetValue (config->log_types() & Log::TYPE_ERROR);
-               _log_error->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::log_changed, this));
-               _log_timing->SetValue (config->log_types() & Log::TYPE_TIMING);
-               _log_timing->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::log_changed, this));
-               
-               return panel;
-       }
-
-private:
-
-       void maximum_j2k_bandwidth_changed ()
-       {
-               Config::instance()->set_maximum_j2k_bandwidth (_maximum_j2k_bandwidth->GetValue() * 1000000);
+       _sound_output_details->SetLabel (
+               wxString::Format(_("%d channels on %s"), channels, apis[audio.getCurrentApi()])
+               );
+
+       _map->set (Config::instance()->audio_mapping(channels));
+
+       vector<NamedChannel> input;
+       for (int i = 0; i < MAX_DCP_AUDIO_CHANNELS; ++i) {
+               input.push_back (NamedChannel(short_audio_channel_name(i), i));
        }
+       _map->set_input_channels (input);
 
-       void allow_any_dcp_frame_rate_changed ()
-       {
-               Config::instance()->set_allow_any_dcp_frame_rate (_allow_any_dcp_frame_rate->GetValue ());
+       vector<NamedChannel> output;
+       for (int i = 0; i < channels; ++i) {
+               output.push_back (NamedChannel(dcp::raw_convert<string>(i), i));
        }
+       _map->set_output_channels (output);
 
-       void log_changed ()
-       {
-               int types = 0;
-               if (_log_general->GetValue ()) {
-                       types |= Log::TYPE_GENERAL;
-               }
-               if (_log_warning->GetValue ()) {
-                       types |= Log::TYPE_WARNING;
-               }
-               if (_log_error->GetValue ())  {
-                       types |= Log::TYPE_ERROR;
-               }
-               if (_log_timing->GetValue ()) {
-                       types |= Log::TYPE_TIMING;
-               }
-               Config::instance()->set_log_types (types);
-       }
-       
-       wxSpinCtrl* _maximum_j2k_bandwidth;
-       wxCheckBox* _allow_any_dcp_frame_rate;
-       wxCheckBox* _log_general;
-       wxCheckBox* _log_warning;
-       wxCheckBox* _log_error;
-       wxCheckBox* _log_timing;
-};
-       
-wxPreferencesEditor*
-create_config_dialog ()
-{
-       wxPreferencesEditor* e = new wxPreferencesEditor ();
+       setup_sensitivity ();
+}
+
+void
+SoundPage::setup_sensitivity ()
+{
+       _sound_output->Enable (_sound->GetValue());
+}
+
+/** @return Currently-selected preview sound output in the dialogue */
+optional<string>
+SoundPage::get_sound_output ()
+{
+       int const sel = _sound_output->GetSelection ();
+       if (sel == wxNOT_FOUND) {
+               return optional<string> ();
+       }
+
+       return wx_to_std (_sound_output->GetString (sel));
+}
+
+
+LocationsPage::LocationsPage (wxSize panel_size, int border)
+       : Page (panel_size, border)
+{
+
+}
+
+wxString
+LocationsPage::GetName () const
+{
+       return _("Locations");
+}
 
 #ifdef DCPOMATIC_OSX
-       /* Width that we force some of the config panels to be on OSX so that
-          the containing window doesn't shrink too much when we select those panels.
-          This is obviously an unpleasant hack.
-       */
-       wxSize ps = wxSize (480, -1);
-       int const border = 16;
-#else
-       wxSize ps = wxSize (-1, -1);
-       int const border = 8;
+wxBitmap
+LocationsPage::GetLargeIcon () const
+{
+       return wxBitmap(bitmap_path("locations"), wxBITMAP_TYPE_PNG);
+}
 #endif
-       
-       e->AddPage (new GeneralPage (ps, border));
-       e->AddPage (new DefaultsPage (ps, border));
-       e->AddPage (new EncodingServersPage (ps, border));
-       e->AddPage (new ColourConversionsPage (ps, border));
-       e->AddPage (new TMSPage (ps, border));
-       e->AddPage (new KDMEmailPage (ps, border));
-       e->AddPage (new AdvancedPage (ps, border));
-       return e;
+
+void
+LocationsPage::setup ()
+{
+       int r = 0;
+
+       auto table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
+       _panel->GetSizer()->Add (table, 1, wxALL | wxEXPAND, _border);
+
+       add_label_to_sizer (table, _panel, _("Content directory"), true, wxGBPosition (r, 0));
+       _content_directory = new wxDirPickerCtrl (_panel, wxID_ANY, wxEmptyString, wxDirSelectorPromptStr, wxDefaultPosition, wxSize (300, -1));
+       table->Add (_content_directory, wxGBPosition (r, 1));
+       ++r;
+
+       add_label_to_sizer (table, _panel, _("Playlist directory"), true, wxGBPosition (r, 0));
+       _playlist_directory = new wxDirPickerCtrl (_panel, wxID_ANY, wxEmptyString, wxDirSelectorPromptStr, wxDefaultPosition, wxSize (300, -1));
+       table->Add (_playlist_directory, wxGBPosition (r, 1));
+       ++r;
+
+       add_label_to_sizer (table, _panel, _("KDM directory"), true, wxGBPosition (r, 0));
+       _kdm_directory = new wxDirPickerCtrl (_panel, wxID_ANY, wxEmptyString, wxDirSelectorPromptStr, wxDefaultPosition, wxSize (300, -1));
+       table->Add (_kdm_directory, wxGBPosition (r, 1));
+       ++r;
+
+       _content_directory->Bind (wxEVT_DIRPICKER_CHANGED, bind(&LocationsPage::content_directory_changed, this));
+       _playlist_directory->Bind (wxEVT_DIRPICKER_CHANGED, bind(&LocationsPage::playlist_directory_changed, this));
+       _kdm_directory->Bind (wxEVT_DIRPICKER_CHANGED, bind(&LocationsPage::kdm_directory_changed, this));
+}
+
+void
+LocationsPage::config_changed ()
+{
+       auto config = Config::instance ();
+
+       if (config->player_content_directory()) {
+               checked_set (_content_directory, *config->player_content_directory());
+       }
+       if (config->player_playlist_directory()) {
+               checked_set (_playlist_directory, *config->player_playlist_directory());
+       }
+       if (config->player_kdm_directory()) {
+               checked_set (_kdm_directory, *config->player_kdm_directory());
+       }
+}
+
+void
+LocationsPage::content_directory_changed ()
+{
+       Config::instance()->set_player_content_directory(wx_to_std(_content_directory->GetPath()));
+}
+
+void
+LocationsPage::playlist_directory_changed ()
+{
+       Config::instance()->set_player_playlist_directory(wx_to_std(_playlist_directory->GetPath()));
+}
+
+void
+LocationsPage::kdm_directory_changed ()
+{
+       Config::instance()->set_player_kdm_directory(wx_to_std(_kdm_directory->GetPath()));
 }