Use libdcp's warnings.h
[dcpomatic.git] / src / wx / config_dialog.cc
index 92a700d4d7532235112da53d7426468ac408d3f8..08e2721da35e49f448b3d2ccabd9de93dbea9f22 100644 (file)
 /*
-    Copyright (C) 2012-2015 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 "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 "server_dialog.h"
-#include "make_signer_chain_dialog.h"
-#include "lib/config.h"
-#include "lib/ratio.h"
-#include "lib/filter.h"
-#include "lib/dcp_content_type.h"
-#include "lib/log.h"
-#include "lib/util.h"
-#include "lib/raw_convert.h"
-#include "lib/cross.h"
-#include "lib/exceptions.h"
-#include <dcp/exceptions.h>
-#include <dcp/signer.h>
-#include <wx/stdpaths.h>
-#include <wx/preferences.h>
-#include <wx/filepicker.h>
-#include <wx/spinctrl.h>
-#include <boost/lexical_cast.hpp>
-#include <boost/filesystem.hpp>
-#include <iostream>
-
-using std::vector;
+#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>
+
+
+using std::function;
+using std::make_pair;
+using std::make_shared;
+using std::map;
+using std::pair;
+using std::shared_ptr;
 using std::string;
-using std::list;
-using std::cout;
+using std::vector;
 using boost::bind;
-using boost::shared_ptr;
-using boost::lexical_cast;
+using boost::optional;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
 
-class Page
+
+static
+bool
+do_nothing ()
 {
-public:
-       Page (wxSize panel_size, int border)
-               : _border (border)
-               , _panel (0)
-               , _panel_size (panel_size)
-               , _window_exists (false)
-       {
-               _config_connection = Config::instance()->Changed.connect (boost::bind (&Page::config_changed_wrapper, this));
-       }
+       return false;
+}
 
-       virtual ~Page () {}
+Page::Page (wxSize panel_size, int border)
+       : _border (border)
+       , _panel (0)
+       , _panel_size (panel_size)
+       , _window_exists (false)
+{
+       _config_connection = Config::instance()->Changed.connect (bind (&Page::config_changed_wrapper, this));
+}
 
-protected:
-       wxWindow* create_window (wxWindow* parent)
-       {
-               _panel = new wxPanel (parent, wxID_ANY, wxDefaultPosition, _panel_size);
-               wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
-               _panel->SetSizer (s);
 
-               setup ();
-               _window_exists = true;
-               config_changed ();
+wxWindow*
+Page::CreateWindow (wxWindow* parent)
+{
+       return create_window (parent);
+}
 
-               _panel->Bind (wxEVT_DESTROY, boost::bind (&Page::window_destroyed, this));
 
-               return _panel;
-       }
+wxWindow*
+Page::create_window (wxWindow* parent)
+{
+       _panel = new wxPanel (parent, wxID_ANY, wxDefaultPosition, _panel_size);
+       auto s = new wxBoxSizer (wxVERTICAL);
+       _panel->SetSizer (s);
 
-       int _border;
-       wxPanel* _panel;
+       setup ();
+       _window_exists = true;
+       config_changed ();
 
-private:
-       virtual void config_changed () = 0;
-       virtual void setup () = 0;
+       _panel->Bind (wxEVT_DESTROY, bind (&Page::window_destroyed, this));
 
-       void config_changed_wrapper ()
-       {
-               if (_window_exists) {
-                       config_changed ();
-               }
-       }
+       return _panel;
+}
 
-       void window_destroyed ()
-       {
-               _window_exists = false;
+void
+Page::config_changed_wrapper ()
+{
+       if (_window_exists) {
+               config_changed ();
        }
+}
+
+void
+Page::window_destroyed ()
+{
+       _window_exists = false;
+}
 
-       wxSize _panel_size;
-       boost::signals2::scoped_connection _config_connection;
-       bool _window_exists;
-};
 
-class StockPage : public wxStockPreferencesPage, public Page
+GeneralPage::GeneralPage (wxSize panel_size, int border)
+       : Page (panel_size, border)
 {
-public:
-       StockPage (Kind kind, wxSize panel_size, int border)
-               : wxStockPreferencesPage (kind)
-               , Page (panel_size, border)
-       {}
 
-       wxWindow* CreateWindow (wxWindow* parent)
-       {
-               return create_window (parent);
-       }
-};
+}
 
-class StandardPage : public wxPreferencesPage, public Page
+
+wxString
+GeneralPage::GetName () const
 {
-public:
-       StandardPage (wxSize panel_size, int border)
-               : Page (panel_size, border)
-       {}
+       return _("General");
+}
 
-       wxWindow* CreateWindow (wxWindow* parent)
-       {
-               return create_window (parent);
-       }
-};
 
-class GeneralPage : public StockPage
+void
+GeneralPage::add_language_controls (wxGridBagSizer* table, int& r)
 {
-public:
-       GeneralPage (wxSize panel_size, int border)
-               : StockPage (Kind_General, panel_size, border)
-       {}
+       _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));
+}
 
-private:
-       void setup ()
-       {
-               wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
-               table->AddGrowableCol (1, 1);
-               _panel->GetSizer()->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"));
-               _language->Append (wxT ("Русский"));
-               _language->Append (wxT ("Polski"));
-               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);
-
-               _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->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&GeneralPage::num_local_encoding_threads_changed, this));
-
-               _check_for_updates->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&GeneralPage::check_for_updates_changed, this));
-               _check_for_test_updates->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&GeneralPage::check_for_test_updates_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;
 
-       void config_changed ()
-       {
-               Config* config = Config::instance ();
-
-               checked_set (_set_language, 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 if (config->language().get_value_or ("") == "ru") {
-                       _language->SetSelection (7);
-               } else if (config->language().get_value_or ("") == "pl") {
-                       checked_set (_language, 8);
-               } else {
-                       _language->SetSelection (1);
-               }
+       _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;
 
-               setup_language_sensitivity ();
+       _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));
+}
 
-               checked_set (_num_local_encoding_threads, config->num_local_encoding_threads ());
-               checked_set (_check_for_updates, config->check_for_updates ());
-               checked_set (_check_for_test_updates, config->check_for_test_updates ());
-       }
+void
+GeneralPage::config_changed ()
+{
+       auto config = Config::instance ();
 
-       void setup_language_sensitivity ()
-       {
-               _language->Enable (_set_language->GetValue ());
-       }
+       checked_set (_set_language, static_cast<bool>(config->language()));
 
-       void set_language_changed ()
-       {
-               setup_language_sensitivity ();
-               if (_set_language->GetValue ()) {
-                       language_changed ();
-               } else {
-                       Config::instance()->unset_language ();
-               }
-       }
+       /* Backwards compatibility of config file */
 
-       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;
-               case 7:
-                       Config::instance()->set_language ("ru");
-                       break;
-               case 8:
-                       Config::instance()->set_language ("pl");
-                       break;
-               }
-       }
+       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";
 
-       void check_for_updates_changed ()
-       {
-               Config::instance()->set_check_for_updates (_check_for_updates->GetValue ());
+       auto lang = config->language().get_value_or("en_GB");
+       if (compat_map.find(lang) != compat_map.end ()) {
+               lang = compat_map[lang];
        }
 
-       void check_for_test_updates_changed ()
-       {
-               Config::instance()->set_check_for_test_updates (_check_for_test_updates->GetValue ());
-       }
+       checked_set (_language, lang);
 
-       void num_local_encoding_threads_changed ()
-       {
-               Config::instance()->set_num_local_encoding_threads (_num_local_encoding_threads->GetValue ());
-       }
+       checked_set (_check_for_updates, config->check_for_updates ());
+       checked_set (_check_for_test_updates, config->check_for_test_updates ());
 
-       wxCheckBox* _set_language;
-       wxChoice* _language;
-       wxSpinCtrl* _num_local_encoding_threads;
-       wxCheckBox* _check_for_updates;
-       wxCheckBox* _check_for_test_updates;
-};
+       setup_sensitivity ();
+}
 
-class DefaultsPage : public StandardPage
+void
+GeneralPage::setup_sensitivity ()
 {
-public:
-       DefaultsPage (wxSize panel_size, int border)
-               : StandardPage (panel_size, border)
-       {}
+       _language->Enable (_set_language->GetValue ());
+       _check_for_test_updates->Enable (_check_for_updates->GetValue ());
+}
 
-       wxString GetName () const
-       {
-               return _("Defaults");
+void
+GeneralPage::set_language_changed ()
+{
+       setup_sensitivity ();
+       if (_set_language->GetValue ()) {
+               language_changed ();
+       } else {
+               Config::instance()->unset_language ();
        }
+}
 
-#ifdef DCPOMATIC_OSX
-       wxBitmap GetLargeIcon () const
-       {
-               return wxBitmap ("defaults", wxBITMAP_TYPE_PNG_RESOURCE);
+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 ();
        }
-#endif
+}
 
-private:
-       void setup ()
-       {
-               wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
-               table->AddGrowableCol (1, 1);
-               _panel->GetSizer()->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);
-               }
+void
+GeneralPage::check_for_updates_changed ()
+{
+       Config::instance()->set_check_for_updates (_check_for_updates->GetValue ());
+}
 
-               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 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);
-               }
+void
+GeneralPage::check_for_test_updates_changed ()
+{
+       Config::instance()->set_check_for_test_updates (_check_for_test_updates->GetValue ());
+}
 
-               {
-                       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);
-               }
+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);
 
-               add_label_to_sizer (table, _panel, _("Default issuer"), true);
-               _issuer = new wxTextCtrl (_panel, wxID_ANY);
-               table->Add (_issuer, 1, wxEXPAND);
+       auto certificates_sizer = new wxBoxSizer (wxHORIZONTAL);
+       _sizer->Add (certificates_sizer, 0, wxALL, border);
 
-               _still_length->SetRange (1, 3600);
-               _still_length->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&DefaultsPage::still_length_changed, this));
+       _certificates = new wxListCtrl (this, wxID_ANY, wxDefaultPosition, wxSize (440, 150), wxLC_REPORT | wxLC_SINGLE_SEL);
 
-               _directory->Bind (wxEVT_COMMAND_DIRPICKER_CHANGED, boost::bind (&DefaultsPage::directory_changed, this));
+       {
+               wxListItem ip;
+               ip.SetId (0);
+               ip.SetText (_("Type"));
+               ip.SetWidth (100);
+               _certificates->InsertColumn (0, ip);
+       }
 
-               _isdcf_metadata_button->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&DefaultsPage::edit_isdcf_metadata_clicked, this));
+       {
+               wxListItem ip;
+               ip.SetId (1);
+               ip.SetText (_("Thumbprint"));
+               ip.SetWidth (340);
 
-               vector<Ratio const *> ratios = Ratio::all ();
-               for (size_t i = 0; i < ratios.size(); ++i) {
-                       _container->Append (std_to_wx (ratios[i]->nickname ()));
-               }
+               wxFont font = ip.GetFont ();
+               font.SetFamily (wxFONTFAMILY_TELETYPE);
+               ip.SetFont (font);
+
+               _certificates->InsertColumn (1, ip);
+       }
+
+       certificates_sizer->Add (_certificates, 1, wxEXPAND);
+
+       {
+               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);
+       }
+
+       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());
+       }
+
+       SetSizerAndFit (_sizer);
+
+       update_certificate_list ();
+       update_private_key ();
+       update_sensitivity ();
+}
 
-               _container->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&DefaultsPage::container_changed, this));
+void
+CertificateChainEditor::add_button (wxWindow* button)
+{
+       _button_sizer->Add (button, 0, wxLEFT | wxRIGHT, DCPOMATIC_SIZER_X_GAP);
+       _sizer->Layout ();
+}
 
-               vector<DCPContentType const *> const ct = DCPContentType::all ();
-               for (size_t i = 0; i < ct.size(); ++i) {
-                       _dcp_content_type->Append (std_to_wx (ct[i]->pretty_name ()));
-               }
+void
+CertificateChainEditor::add_certificate ()
+{
+       auto d = new wxFileDialog (this, _("Select Certificate File"));
 
-               _dcp_content_type->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&DefaultsPage::dcp_content_type_changed, this));
+       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;
+                       }
 
-               _j2k_bandwidth->SetRange (50, 250);
-               _j2k_bandwidth->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&DefaultsPage::j2k_bandwidth_changed, this));
+                       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.")
+                                       );
+                       }
+                       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 ();
+                       }
+               } catch (dcp::MiscError& e) {
+                       error_dialog (this, _("Could not read certificate file."), std_to_wx(e.what()));
+               }
+       }
 
-               _audio_delay->SetRange (-1000, 1000);
-               _audio_delay->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&DefaultsPage::audio_delay_changed, this));
+       d->Destroy ();
+
+       update_sensitivity ();
+}
 
-               _issuer->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&DefaultsPage::issuer_changed, this));
+void
+CertificateChainEditor::remove_certificate ()
+{
+       if (_nag_alter()) {
+               /* Cancel was clicked */
+               return;
        }
 
-       void config_changed ()
-       {
-               Config* config = Config::instance ();
+       int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
+       if (i == -1) {
+               return;
+       }
 
-               vector<Ratio const *> ratios = Ratio::all ();
-               for (size_t i = 0; i < ratios.size(); ++i) {
-                       if (ratios[i] == config->default_container ()) {
-                               _container->SetSelection (i);
-                       }
-               }
+       _certificates->DeleteItem (i);
+       auto chain = make_shared<dcp::CertificateChain>(*_get().get());
+       chain->remove (i);
+       _set (chain);
 
-               vector<DCPContentType const *> const ct = DCPContentType::all ();
-               for (size_t i = 0; i < ct.size(); ++i) {
-                       if (ct[i] == config->default_dcp_content_type ()) {
-                               _dcp_content_type->SetSelection (i);
-                       }
-               }
+       update_sensitivity ();
+       update_certificate_list ();
+}
 
-               checked_set (_still_length, config->default_still_length ());
-               _directory->SetPath (std_to_wx (config->default_directory_or (wx_to_std (wxStandardPaths::Get().GetDocumentsDir())).string ()));
-               checked_set (_j2k_bandwidth, config->default_j2k_bandwidth() / 1000000);
-               _j2k_bandwidth->SetRange (50, config->maximum_j2k_bandwidth() / 1000000);
-               checked_set (_audio_delay, config->default_audio_delay ());
-               checked_set (_issuer, config->dcp_issuer ());
+void
+CertificateChainEditor::export_certificate ()
+{
+       int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
+       if (i == -1) {
+               return;
        }
 
-       void j2k_bandwidth_changed ()
-       {
-               Config::instance()->set_default_j2k_bandwidth (_j2k_bandwidth->GetValue() * 1000000);
-       }
+       auto all = _get()->root_to_leaf();
 
-       void audio_delay_changed ()
-       {
-               Config::instance()->set_default_audio_delay (_audio_delay->GetValue());
+       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 directory_changed ()
-       {
-               Config::instance()->set_default_directory (wx_to_std (_directory->GetPath ()));
-       }
+       auto d = new wxFileDialog(
+               this, _("Select Certificate File"), wxEmptyString, default_name, wxT ("PEM files (*.pem)|*.pem"),
+               wxFD_SAVE | wxFD_OVERWRITE_PROMPT
+               );
 
-       void edit_isdcf_metadata_clicked ()
-       {
-               ISDCFMetadataDialog* d = new ISDCFMetadataDialog (_panel, Config::instance()->default_isdcf_metadata ());
-               d->ShowModal ();
-               Config::instance()->set_default_isdcf_metadata (d->isdcf_metadata ());
-               d->Destroy ();
+       auto j = all.begin ();
+       for (int k = 0; k < i; ++k) {
+               ++j;
        }
 
-       void still_length_changed ()
-       {
-               Config::instance()->set_default_still_length (_still_length->GetValue ());
-       }
+       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);
+               }
 
-       void container_changed ()
-       {
-               vector<Ratio const *> ratio = Ratio::all ();
-               Config::instance()->set_default_container (ratio[_container->GetSelection()]);
+               string const s = j->certificate (true);
+               checked_fwrite (s.c_str(), s.length(), f, path);
+               fclose (f);
        }
+       d->Destroy ();
+}
 
-       void dcp_content_type_changed ()
-       {
-               vector<DCPContentType const *> ct = DCPContentType::all ();
-               Config::instance()->set_default_dcp_content_type (ct[_dcp_content_type->GetSelection()]);
-       }
+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);
+               }
 
-       void issuer_changed ()
-       {
-               Config::instance()->set_dcp_issuer (wx_to_std (_issuer->GetValue ()));
+               auto const s = _get()->chain();
+               checked_fwrite (s.c_str(), s.length(), f, path);
+               fclose (f);
        }
 
-       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* _container;
-       wxChoice* _dcp_content_type;
-       wxTextCtrl* _issuer;
-};
+       d->Destroy ();
+}
 
-class EncodingServersPage : public StandardPage
+void
+CertificateChainEditor::update_certificate_list ()
 {
-public:
-       EncodingServersPage (wxSize panel_size, int border)
-               : StandardPage (panel_size, border)
-       {}
+       _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"));
+               }
 
-       wxString GetName () const
-       {
-               return _("Servers");
+               ++n;
        }
 
-#ifdef DCPOMATIC_OSX
-       wxBitmap GetLargeIcon () const
-       {
-               return wxBitmap ("servers", wxBITMAP_TYPE_PNG_RESOURCE);
-       }
-#endif
+       static wxColour normal = _private_key_bad->GetForegroundColour ();
 
-private:
-       void setup ()
-       {
-               _use_any_servers = new wxCheckBox (_panel, wxID_ANY, _("Use all servers"));
-               _panel->GetSizer()->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)
-                       );
-
-               _panel->GetSizer()->Add (_servers_list, 1, wxEXPAND | wxALL, _border);
-
-               _use_any_servers->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&EncodingServersPage::use_any_servers_changed, this));
+       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));
        }
+}
 
-       void config_changed ()
-       {
-               checked_set (_use_any_servers, Config::instance()->use_any_servers ());
-               _servers_list->refresh ();
+void
+CertificateChainEditor::remake_certificates ()
+{
+       if (_nag_alter()) {
+               /* Cancel was clicked */
+               return;
        }
 
-       void use_any_servers_changed ()
-       {
-               Config::instance()->set_use_any_servers (_use_any_servers->GetValue ());
-       }
+       auto d = new MakeChainDialog (this, _get());
 
-       string server_column (string s)
-       {
-               return s;
+       if (d->ShowModal () == wxID_OK) {
+               _set (d->get());
+               update_certificate_list ();
+               update_private_key ();
        }
 
-       wxCheckBox* _use_any_servers;
-       EditableList<string, ServerDialog>* _servers_list;
-};
+       d->Destroy ();
+}
 
-class KeysPage : public StandardPage
+void
+CertificateChainEditor::update_sensitivity ()
 {
-public:
-       KeysPage (wxSize panel_size, int border)
-               : StandardPage (panel_size, border)
-       {}
-
-       wxString GetName () const
-       {
-               return _("Keys");
-       }
-
-#ifdef DCPOMATIC_OSX
-       wxBitmap GetLargeIcon () const
-       {
-               return wxBitmap ("keys", wxBITMAP_TYPE_PNG_RESOURCE);
-       }
-#endif
-
-private:
-       void setup ()
-       {
-               wxStaticText* m = new wxStaticText (_panel, wxID_ANY, _("Certificate chain for signing DCPs and KDMs:"));
-               _panel->GetSizer()->Add (m, 0, wxALL, _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);
+}
 
-               wxBoxSizer* certificates_sizer = new wxBoxSizer (wxHORIZONTAL);
-               _panel->GetSizer()->Add (certificates_sizer, 0, wxLEFT | wxRIGHT, _border);
+void
+CertificateChainEditor::update_private_key ()
+{
+       checked_set (_private_key, dcp::private_key_fingerprint (_get()->key().get()));
+       _sizer->Layout ();
+}
 
-               _certificates = new wxListCtrl (_panel, wxID_ANY, wxDefaultPosition, wxSize (400, 200), wxLC_REPORT | wxLC_SINGLE_SEL);
+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;
+                       }
 
-               {
-                       wxListItem ip;
-                       ip.SetId (0);
-                       ip.SetText (_("Type"));
-                       ip.SetWidth (100);
-                       _certificates->InsertColumn (0, ip);
+                       auto chain = make_shared<dcp::CertificateChain>(*_get().get());
+                       chain->set_key (dcp::file_to_string (p));
+                       _set (chain);
+                       update_private_key ();
+               } catch (std::exception& e) {
+                       error_dialog (this, _("Could not read certificate file."), std_to_wx(e.what()));
                }
+       }
 
-               {
-                       wxListItem ip;
-                       ip.SetId (1);
-                       ip.SetText (_("Thumbprint"));
-                       ip.SetWidth (300);
+       d->Destroy ();
 
-                       wxFont font = ip.GetFont ();
-                       font.SetFamily (wxFONTFAMILY_TELETYPE);
-                       ip.SetFont (font);
+       update_sensitivity ();
+}
 
-                       _certificates->InsertColumn (1, ip);
-               }
+void
+CertificateChainEditor::export_private_key ()
+{
+       auto key = _get()->key();
+       if (!key) {
+               return;
+       }
 
-               certificates_sizer->Add (_certificates, 1, wxEXPAND);
+       auto d = new wxFileDialog (
+               this, _("Select Key File"), wxEmptyString, wxT("private_key.pem"), wxT("PEM files (*.pem)|*.pem"),
+               wxFD_SAVE | wxFD_OVERWRITE_PROMPT
+               );
 
-               {
-                       wxSizer* s = new wxBoxSizer (wxVERTICAL);
-                       _add_certificate = new wxButton (_panel, wxID_ANY, _("Add..."));
-                       s->Add (_add_certificate, 0, wxTOP | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
-                       _remove_certificate = new wxButton (_panel, wxID_ANY, _("Remove"));
-                       s->Add (_remove_certificate, 0, wxTOP | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
-                       certificates_sizer->Add (s, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP);
+       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);
                }
 
-               wxGridBagSizer* table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
-               _panel->GetSizer()->Add (table, 1, wxALL | wxEXPAND, _border);
+               auto const s = _get()->key().get ();
+               checked_fwrite (s.c_str(), s.length(), f, path);
+               fclose (f);
+       }
+       d->Destroy ();
+}
 
-               int r = 0;
+wxString
+KeysPage::GetName () const
+{
+       return _("Keys");
+}
 
-               _remake_certificates = new wxButton (_panel, wxID_ANY, _("Re-make certificates..."));
-               table->Add (_remake_certificates, wxGBPosition (r, 0), wxGBSpan (1, 3));
-               ++r;
+void
+KeysPage::setup ()
+{
+       wxFont subheading_font (*wxNORMAL_FONT);
+       subheading_font.SetWeight (wxFONTWEIGHT_BOLD);
 
-               add_label_to_grid_bag_sizer (table, _panel, _("Private key for leaf certificate"), true, wxGBPosition (r, 0));
-               _signer_private_key = new wxStaticText (_panel, wxID_ANY, wxT (""));
-               wxFont font = _signer_private_key->GetFont ();
-               font.SetFamily (wxFONTFAMILY_TELETYPE);
-               _signer_private_key->SetFont (font);
-               table->Add (_signer_private_key, wxGBPosition (r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
-               _load_signer_private_key = new wxButton (_panel, wxID_ANY, _("Load..."));
-               table->Add (_load_signer_private_key, wxGBPosition (r, 2));
-               ++r;
-
-               add_label_to_grid_bag_sizer (table, _panel, _("Certificate for decrypting DCPs"), true, wxGBPosition (r, 0));
-               _decryption_certificate = new wxStaticText (_panel, wxID_ANY, wxT (""));
-               _decryption_certificate->SetFont (font);
-               table->Add (_decryption_certificate, wxGBPosition (r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
-               _load_decryption_certificate = new wxButton (_panel, wxID_ANY, _("Load..."));
-               table->Add (_load_decryption_certificate, wxGBPosition (r, 2));
-               ++r;
-
-               add_label_to_grid_bag_sizer (table, _panel, _("Private key for decrypting DCPs"), true, wxGBPosition (r, 0));
-               _decryption_private_key = new wxStaticText (_panel, wxID_ANY, wxT (""));
-               _decryption_private_key->SetFont (font);
-               table->Add (_decryption_private_key, wxGBPosition (r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
-               _load_decryption_private_key = new wxButton (_panel, wxID_ANY, _("Load..."));
-               table->Add (_load_decryption_private_key, wxGBPosition (r, 2));
-               ++r;
-
-               _export_decryption_certificate = new wxButton (_panel, wxID_ANY, _("Export DCP decryption certificate..."));
-               table->Add (_export_decryption_certificate, wxGBPosition (r, 0), wxGBSpan (1, 3));
-               ++r;
-
-               _add_certificate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::add_certificate, this));
-               _remove_certificate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::remove_certificate, this));
-               _certificates->Bind (wxEVT_COMMAND_LIST_ITEM_SELECTED, boost::bind (&KeysPage::update_sensitivity, this));
-               _certificates->Bind (wxEVT_COMMAND_LIST_ITEM_DESELECTED, boost::bind (&KeysPage::update_sensitivity, this));
-               _remake_certificates->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::remake_certificates, this));
-               _load_signer_private_key->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::load_signer_private_key, this));
-               _load_decryption_certificate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::load_decryption_certificate, this));
-               _load_decryption_private_key->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::load_decryption_private_key, this));
-               _export_decryption_certificate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::export_decryption_certificate, this));
-       }
+       auto sizer = _panel->GetSizer();
 
-       void config_changed ()
        {
-               _signer.reset (new dcp::Signer (*Config::instance()->signer().get ()));
-
-               update_certificate_list ();
-               update_signer_private_key ();
-               update_decryption_certificate ();
-               update_decryption_private_key ();
-               update_sensitivity ();
+               auto m = new StaticText (_panel, _("Decrypting KDMs"));
+               m->SetFont (subheading_font);
+               sizer->Add (m, 0, wxALL | wxEXPAND, _border);
        }
 
-       void add_certificate ()
-       {
-               wxFileDialog* d = new wxFileDialog (_panel, _("Select Certificate File"));
+       auto kdm_buttons = new wxBoxSizer (wxVERTICAL);
 
-               if (d->ShowModal() == wxID_OK) {
-                       try {
-                               dcp::Certificate c (dcp::file_to_string (wx_to_std (d->GetPath ())));
-                               _signer->certificates().add (c);
-                               Config::instance()->set_signer (_signer);
-                               update_certificate_list ();
-                       } catch (dcp::MiscError& e) {
-                               error_dialog (_panel, wxString::Format (_("Could not read certificate file (%s)"), e.what ()));
-                       }
-               }
+       auto export_decryption_certificate = new Button (_panel, _("Export KDM decryption leaf certificate..."));
+       kdm_buttons->Add (export_decryption_certificate, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
+       auto export_settings = new Button (_panel, _("Export all KDM decryption settings..."));
+       kdm_buttons->Add (export_settings, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
+       auto import_settings = new Button (_panel, _("Import all KDM decryption settings..."));
+       kdm_buttons->Add (import_settings, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
+       auto decryption_advanced = new Button (_panel, _("Advanced..."));
+       kdm_buttons->Add (decryption_advanced, 0);
 
-               d->Destroy ();
+       sizer->Add (kdm_buttons, 0, wxLEFT, _border);
 
-               update_sensitivity ();
-       }
+       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));
 
-       void remove_certificate ()
        {
-               int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
-               if (i == -1) {
-                       return;
-               }
-
-               _certificates->DeleteItem (i);
-               _signer->certificates().remove (i);
-               Config::instance()->set_signer (_signer);
-
-               update_sensitivity ();
+               auto m = new StaticText (_panel, _("Signing DCPs and KDMs"));
+               m->SetFont (subheading_font);
+               sizer->Add (m, 0, wxALL | wxEXPAND, _border);
        }
 
-       void update_certificate_list ()
-       {
-               _certificates->DeleteAllItems ();
-               dcp::CertificateChain::List certs = _signer->certificates().root_to_leaf ();
-               size_t n = 0;
-               for (dcp::CertificateChain::List::const_iterator i = certs.begin(); i != certs.end(); ++i) {
-                       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"));
-                       }
+       auto signing_buttons = new wxBoxSizer (wxVERTICAL);
 
-                       ++n;
-               }
-       }
+       auto signing_advanced = new Button (_panel, _("Advanced..."));
+       signing_buttons->Add (signing_advanced, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
+       auto remake_signing = new Button (_panel, _("Re-make certificates and key..."));
+       signing_buttons->Add (remake_signing, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
 
-       void remake_certificates ()
-       {
-               dcp::CertificateChain chain = Config::instance()->signer()->certificates ();
+       sizer->Add (signing_buttons, 0, wxLEFT, _border);
 
-               string intermediate_common_name;
-               if (chain.root_to_leaf().size() >= 3) {
-                       dcp::CertificateChain::List all = chain.root_to_leaf ();
-                       dcp::CertificateChain::List::iterator i = all.begin ();
-                       ++i;
-                       intermediate_common_name = i->subject_common_name ();
-               }
+       signing_advanced->Bind (wxEVT_BUTTON, bind (&KeysPage::signing_advanced, this));
+       remake_signing->Bind (wxEVT_BUTTON, bind(&KeysPage::remake_signing, this));
+}
 
-               MakeSignerChainDialog* d = new MakeSignerChainDialog (
-                       _panel,
-                       chain.root().subject_organization_name (),
-                       chain.root().subject_organizational_unit_name (),
-                       chain.root().subject_common_name (),
-                       intermediate_common_name,
-                       chain.leaf().subject_common_name ()
-                       );
-
-               if (d->ShowModal () == wxID_OK) {
-                       _signer.reset (
-                               new dcp::Signer (
-                                       openssl_path (),
-                                       d->organisation (),
-                                       d->organisational_unit (),
-                                       d->root_common_name (),
-                                       d->intermediate_common_name (),
-                                       d->leaf_common_name ()
-                                       )
-                               );
-
-                       Config::instance()->set_signer (_signer);
-                       update_certificate_list ();
-                       update_signer_private_key ();
-               }
 
-               d->Destroy ();
-       }
+void
+KeysPage::remake_signing ()
+{
+       auto d = new MakeChainDialog (_panel, Config::instance()->signer_chain());
 
-       void update_sensitivity ()
-       {
-               _remove_certificate->Enable (_certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED) != -1);
+       if (d->ShowModal () == wxID_OK) {
+               Config::instance()->set_signer_chain(d->get());
        }
+}
 
-       void update_signer_private_key ()
-       {
-               checked_set (_signer_private_key, dcp::private_key_fingerprint (_signer->key ()));
-       }
 
-       void load_signer_private_key ()
-       {
-               wxFileDialog* d = new wxFileDialog (_panel, _("Select Key File"));
+void
+KeysPage::decryption_advanced ()
+{
+       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();
+}
 
-               if (d->ShowModal() == wxID_OK) {
-                       try {
-                               boost::filesystem::path p (wx_to_std (d->GetPath ()));
-                               if (boost::filesystem::file_size (p) > 1024) {
-                                       error_dialog (_panel, wxString::Format (_("Could not read key file (%s)"), std_to_wx (p.string ())));
-                                       return;
-                               }
+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();
+}
 
-                               _signer->set_key (dcp::file_to_string (p));
-                               Config::instance()->set_signer (_signer);
-                               update_signer_private_key ();
-                       } catch (dcp::MiscError& e) {
-                               error_dialog (_panel, wxString::Format (_("Could not read certificate file (%s)"), e.what ()));
-                       }
+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);
                }
 
-               d->Destroy ();
-
-               update_sensitivity ();
-
+               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 ();
 
-       void load_decryption_certificate ()
-       {
-               wxFileDialog* d = new wxFileDialog (_panel, _("Select Certificate File"));
-
-               if (d->ShowModal() == wxID_OK) {
-                       try {
-                               dcp::Certificate c (dcp::file_to_string (wx_to_std (d->GetPath ())));
-                               Config::instance()->set_decryption_certificate (c);
-                               update_decryption_certificate ();
-                       } catch (dcp::MiscError& e) {
-                               error_dialog (_panel, wxString::Format (_("Could not read certificate file (%s)"), e.what ()));
-                       }
-               }
+}
 
-               d->Destroy ();
+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;
        }
 
-       void update_decryption_certificate ()
-       {
-               checked_set (_decryption_certificate, Config::instance()->decryption_certificate().thumbprint ());
-       }
+       auto d = new wxFileDialog (
+               _panel, _("Select File To Import"), wxEmptyString, wxEmptyString, wxT ("DOM files (*.dom)|*.dom")
+               );
 
-       void load_decryption_private_key ()
-       {
-               wxFileDialog* d = new wxFileDialog (_panel, _("Select Key File"));
+       if (d->ShowModal () == wxID_OK) {
+               auto new_chain = make_shared<dcp::CertificateChain>();
 
-               if (d->ShowModal() == wxID_OK) {
-                       try {
-                               boost::filesystem::path p (wx_to_std (d->GetPath ()));
-                               Config::instance()->set_decryption_private_key (dcp::file_to_string (p));
-                               update_decryption_private_key ();
-                       } catch (dcp::MiscError& e) {
-                               error_dialog (_panel, wxString::Format (_("Could not read key file (%s)"), e.what ()));
-                       }
+               FILE* f = fopen_boost (wx_to_std (d->GetPath ()), "r");
+               if (!f) {
+                       throw OpenFileError (wx_to_std (d->GetPath ()), errno, OpenFileError::WRITE);
                }
 
-               d->Destroy ();
-       }
-
-       void update_decryption_private_key ()
-       {
-               checked_set (_decryption_private_key, dcp::private_key_fingerprint (Config::instance()->decryption_private_key()));
-       }
-
-       void export_decryption_certificate ()
-       {
-               wxFileDialog* d = new wxFileDialog (
-                       _panel, _("Select Certificate File"), wxEmptyString, wxEmptyString, wxT ("PEM files (*.pem)|*.pem"),
-                       wxFD_SAVE | wxFD_OVERWRITE_PROMPT
-                       );
-
-               if (d->ShowModal () == wxID_OK) {
-                       FILE* f = fopen_boost (wx_to_std (d->GetPath ()), "w");
-                       if (!f) {
-                               throw OpenFileError (wx_to_std (d->GetPath ()));
+               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);
 
-                       string const s = Config::instance()->decryption_certificate().certificate (true);
-                       fwrite (s.c_str(), 1, s.length(), f);
-                       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"));
                }
-               d->Destroy ();
        }
+       d->Destroy ();
+}
 
-       wxListCtrl* _certificates;
-       wxButton* _add_certificate;
-       wxButton* _remove_certificate;
-       wxButton* _remake_certificates;
-       wxStaticText* _signer_private_key;
-       wxButton* _load_signer_private_key;
-       wxStaticText* _decryption_certificate;
-       wxButton* _load_decryption_certificate;
-       wxStaticText* _decryption_private_key;
-       wxButton* _load_decryption_private_key;
-       wxButton* _export_decryption_certificate;
-       shared_ptr<dcp::Signer> _signer;
-};
-
-class TMSPage : public StandardPage
+bool
+KeysPage::nag_alter_decryption_chain ()
 {
-public:
-       TMSPage (wxSize panel_size, int border)
-               : StandardPage (panel_size, border)
-       {}
-
-       wxString GetName () const
-       {
-               return _("TMS");
-       }
+       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
+               );
+}
 
-#ifdef DCPOMATIC_OSX
-       wxBitmap GetLargeIcon () const
-       {
-               return wxBitmap ("tms", wxBITMAP_TYPE_PNG_RESOURCE);
+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()));
        }
-#endif
-
-private:
-       void setup ()
-       {
-               wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
-               table->AddGrowableCol (1, 1);
-               _panel->GetSizer()->Add (table, 1, wxALL | wxEXPAND, _border);
-
-               add_label_to_sizer (table, _panel, _("Protocol"), true);
-               _tms_protocol = new wxChoice (_panel, wxID_ANY);
-               table->Add (_tms_protocol, 1, wxEXPAND);
-
-               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);
-
-               _tms_protocol->Append (_("SCP (for AAM)"));
-               _tms_protocol->Append (_("FTP (for Dolby)"));
-
-               _tms_protocol->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&TMSPage::tms_protocol_changed, this));
-               _tms_ip->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&TMSPage::tms_ip_changed, this));
-               _tms_path->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&TMSPage::tms_path_changed, this));
-               _tms_user->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&TMSPage::tms_user_changed, this));
-               _tms_password->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&TMSPage::tms_password_changed, this));
+       if (!config->dcp_issuer().empty()) {
+               default_name += "_" + std_to_wx(careful_string_filter(config->dcp_issuer()));
        }
+       default_name += wxT("_kdm_decryption_cert.pem");
 
-       void config_changed ()
-       {
-               Config* config = Config::instance ();
+       auto d = new wxFileDialog (
+               _panel, _("Select Certificate File"), wxEmptyString, default_name, wxT("PEM files (*.pem)|*.pem"),
+               wxFD_SAVE | wxFD_OVERWRITE_PROMPT
+               );
 
-               checked_set (_tms_protocol, config->tms_protocol ());
-               checked_set (_tms_ip, config->tms_ip ());
-               checked_set (_tms_path, config->tms_path ());
-               checked_set (_tms_user, config->tms_user ());
-               checked_set (_tms_password, config->tms_password ());
-       }
+       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);
+               }
 
-       void tms_protocol_changed ()
-       {
-               Config::instance()->set_tms_protocol (static_cast<Protocol> (_tms_protocol->GetSelection ()));
+               auto const s = Config::instance()->decryption_chain()->leaf().certificate (true);
+               checked_fwrite (s.c_str(), s.length(), f, path);
+               fclose (f);
        }
 
-       void tms_ip_changed ()
-       {
-               Config::instance()->set_tms_ip (wx_to_std (_tms_ip->GetValue ()));
-       }
+       d->Destroy ();
+}
 
-       void tms_path_changed ()
-       {
-               Config::instance()->set_tms_path (wx_to_std (_tms_path->GetValue ()));
-       }
+wxString
+SoundPage::GetName () const
+{
+       return _("Sound");
+}
 
-       void tms_user_changed ()
-       {
-               Config::instance()->set_tms_user (wx_to_std (_tms_user->GetValue ()));
+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, 400);
+       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 tms_password_changed ()
-       {
-               Config::instance()->set_tms_password (wx_to_std (_tms_password->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));
+}
 
-       wxChoice* _tms_protocol;
-       wxTextCtrl* _tms_ip;
-       wxTextCtrl* _tms_path;
-       wxTextCtrl* _tms_user;
-       wxTextCtrl* _tms_password;
-};
+void
+SoundPage::reset_to_default ()
+{
+       Config::instance()->set_audio_mapping_to_default ();
+}
 
-class KDMEmailPage : public StandardPage
+void
+SoundPage::map_changed (AudioMapping m)
 {
-public:
+       Config::instance()->set_audio_mapping (m);
+}
 
-       KDMEmailPage (wxSize panel_size, int border)
-#ifdef DCPOMATIC_OSX
-               /* We have to force both width and height of this one */
-               : StandardPage (wxSize (480, 128), border)
-#else
-                : StandardPage (panel_size, border)
-#endif
-       {}
+void
+SoundPage::sound_changed ()
+{
+       Config::instance()->set_sound (_sound->GetValue ());
+}
 
-       wxString GetName () const
-       {
-               return _("KDM Email");
+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);
        }
+}
 
-#ifdef DCPOMATIC_OSX
-       wxBitmap GetLargeIcon () const
-       {
-               return wxBitmap ("kdm_email", wxBITMAP_TYPE_PNG_RESOURCE);
+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 */
+               }
        }
-#endif
 
-private:
-       void setup ()
-       {
-               wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
-               table->AddGrowableCol (1, 1);
-               _panel->GetSizer()->Add (table, 1, wxEXPAND | wxALL, _border);
-
-               add_label_to_sizer (table, _panel, _("Outgoing mail server"), true);
-               {
-                       wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-                       _mail_server = new wxTextCtrl (_panel, wxID_ANY);
-                       s->Add (_mail_server, 1, wxEXPAND | wxALL);
-                       add_label_to_sizer (s, _panel, _("port"), false);
-                       _mail_port = new wxSpinCtrl (_panel, wxID_ANY);
-                       _mail_port->SetRange (0, 65535);
-                       s->Add (_mail_port);
-                       table->Add (s, 1, wxEXPAND | wxALL);
+       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;
                }
-
-               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);
-
-               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);
-               _panel->GetSizer()->Add (_kdm_email, 1, wxEXPAND | wxALL, _border);
-
-               _reset_kdm_email = new wxButton (_panel, wxID_ANY, _("Reset to default text"));
-               _panel->GetSizer()->Add (_reset_kdm_email, 0, wxEXPAND | wxALL, _border);
-
-               _mail_server->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&KDMEmailPage::mail_server_changed, this));
-               _mail_port->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&KDMEmailPage::mail_port_changed, this));
-               _mail_user->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&KDMEmailPage::mail_user_changed, this));
-               _mail_password->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&KDMEmailPage::mail_password_changed, this));
-               _kdm_subject->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&KDMEmailPage::kdm_subject_changed, this));
-               _kdm_from->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&KDMEmailPage::kdm_from_changed, this));
-               _kdm_cc->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&KDMEmailPage::kdm_cc_changed, this));
-               _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));
-               _reset_kdm_email->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KDMEmailPage::reset_kdm_email, this));
        }
 
-       void config_changed ()
-       {
-               Config* config = Config::instance ();
-
-               checked_set (_mail_server, config->mail_server ());
-               checked_set (_mail_port, config->mail_port ());
-               checked_set (_mail_user, config->mail_user ());
-               checked_set (_mail_password, config->mail_password ());
-               checked_set (_kdm_subject, config->kdm_subject ());
-               checked_set (_kdm_from, config->kdm_from ());
-               checked_set (_kdm_cc, config->kdm_cc ());
-               checked_set (_kdm_bcc, config->kdm_bcc ());
-               checked_set (_kdm_email, Config::instance()->kdm_email ());
-       }
+       RtAudio audio (DCPOMATIC_RTAUDIO_API);
 
-       void mail_server_changed ()
-       {
-               Config::instance()->set_mail_server (wx_to_std (_mail_server->GetValue ()));
-       }
+       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");
 
-       void mail_port_changed ()
-       {
-               Config::instance()->set_mail_port (_mail_port->GetValue ());
+       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 */
+                       }
+               }
        }
 
-       void mail_user_changed ()
-       {
-               Config::instance()->set_mail_user (wx_to_std (_mail_user->GetValue ()));
-       }
+       _sound_output_details->SetLabel (
+               wxString::Format(_("%d channels on %s"), channels, apis[audio.getCurrentApi()])
+               );
 
-       void mail_password_changed ()
-       {
-               Config::instance()->set_mail_password (wx_to_std (_mail_password->GetValue ()));
-       }
+       _map->set (Config::instance()->audio_mapping(channels));
 
-       void kdm_subject_changed ()
-       {
-               Config::instance()->set_kdm_subject (wx_to_std (_kdm_subject->GetValue ()));
+       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 kdm_from_changed ()
-       {
-               Config::instance()->set_kdm_from (wx_to_std (_kdm_from->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 kdm_cc_changed ()
-       {
-               Config::instance()->set_kdm_cc (wx_to_std (_kdm_cc->GetValue ()));
-       }
+       setup_sensitivity ();
+}
 
-       void kdm_bcc_changed ()
-       {
-               Config::instance()->set_kdm_bcc (wx_to_std (_kdm_bcc->GetValue ()));
-       }
+void
+SoundPage::setup_sensitivity ()
+{
+       _sound_output->Enable (_sound->GetValue());
+}
 
-       void kdm_email_changed ()
-       {
-               if (_kdm_email->GetValue().IsEmpty ()) {
-                       /* Sometimes we get sent an erroneous notification that the email
-                          is empty; I don't know why.
-                       */
-                       return;
-               }
-               Config::instance()->set_kdm_email (wx_to_std (_kdm_email->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> ();
        }
 
-       void reset_kdm_email ()
-       {
-               Config::instance()->reset_kdm_email ();
-               checked_set (_kdm_email, Config::instance()->kdm_email ());
-       }
+       return wx_to_std (_sound_output->GetString (sel));
+}
+
 
-       wxTextCtrl* _mail_server;
-       wxSpinCtrl* _mail_port;
-       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
- *  @brief Advanced page of the preferences dialog.
- */
-class AdvancedPage : public StockPage
+LocationsPage::LocationsPage (wxSize panel_size, int border)
+       : Page (panel_size, border)
 {
-public:
-       AdvancedPage (wxSize panel_size, int border)
-               : StockPage (Kind_Advanced, panel_size, border)
-               , _maximum_j2k_bandwidth (0)
-               , _allow_any_dcp_frame_rate (0)
-               , _log_general (0)
-               , _log_warning (0)
-               , _log_error (0)
-               , _log_timing (0)
-               , _log_debug_decode (0)
-               , _log_debug_encode (0)
-       {}
-
-private:
-       void setup ()
-       {
-               wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
-               table->AddGrowableCol (1, 1);
-               _panel->GetSizer()->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);
-               }
 
-               _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);
+}
+
+wxString
+LocationsPage::GetName () const
+{
+       return _("Locations");
+}
 
-#ifdef __WXOSX__
-               wxStaticText* m = new wxStaticText (_panel, wxID_ANY, _("Log:"));
-               table->Add (m, 0, wxALIGN_TOP | wxLEFT | wxRIGHT | wxEXPAND | wxALL | wxALIGN_RIGHT, 6);
-#else
-               wxStaticText* m = new wxStaticText (_panel, wxID_ANY, _("Log"));
-               table->Add (m, 0, wxALIGN_TOP | wxLEFT | wxRIGHT | wxEXPAND | wxALL, 6);
+#ifdef DCPOMATIC_OSX
+wxBitmap
+LocationsPage::GetLargeIcon () const
+{
+       return wxBitmap(bitmap_path("locations"), wxBITMAP_TYPE_PNG);
+}
 #endif
 
-               {
-                       wxBoxSizer* t = new wxBoxSizer (wxVERTICAL);
-                       _log_general = new wxCheckBox (_panel, wxID_ANY, _("General"));
-                       t->Add (_log_general, 1, wxEXPAND | wxALL);
-                       _log_warning = new wxCheckBox (_panel, wxID_ANY, _("Warnings"));
-                       t->Add (_log_warning, 1, wxEXPAND | wxALL);
-                       _log_error = new wxCheckBox (_panel, wxID_ANY, _("Errors"));
-                       t->Add (_log_error, 1, wxEXPAND | wxALL);
-                       _log_timing = new wxCheckBox (_panel, wxID_ANY, S_("Config|Timing"));
-                       t->Add (_log_timing, 1, wxEXPAND | wxALL);
-                       _log_debug_decode = new wxCheckBox (_panel, wxID_ANY, _("Debug: decode"));
-                       t->Add (_log_debug_decode, 1, wxEXPAND | wxALL);
-                       _log_debug_encode = new wxCheckBox (_panel, wxID_ANY, _("Debug: encode"));
-                       t->Add (_log_debug_encode, 1, wxEXPAND | wxALL);
-                       table->Add (t, 0, wxALL, 6);
-               }
+void
+LocationsPage::setup ()
+{
+       int r = 0;
 
-#ifdef DCPOMATIC_WINDOWS
-               _win32_console = new wxCheckBox (_panel, wxID_ANY, _("Open console window"));
-               table->Add (_win32_console, 1, wxEXPAND | wxALL);
-               table->AddSpacer (0);
-#endif
+       auto table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
+       _panel->GetSizer()->Add (table, 1, wxALL | wxEXPAND, _border);
 
-               _maximum_j2k_bandwidth->SetRange (1, 1000);
-               _maximum_j2k_bandwidth->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&AdvancedPage::maximum_j2k_bandwidth_changed, this));
-               _allow_any_dcp_frame_rate->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::allow_any_dcp_frame_rate_changed, this));
-               _log_general->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::log_changed, this));
-               _log_warning->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::log_changed, this));
-               _log_error->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::log_changed, this));
-               _log_timing->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::log_changed, this));
-               _log_debug_decode->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::log_changed, this));
-               _log_debug_encode->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::log_changed, this));
-#ifdef DCPOMATIC_WINDOWS
-               _win32_console->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::win32_console_changed, this));
-#endif
-       }
+       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;
 
-       void config_changed ()
-       {
-               Config* config = Config::instance ();
-
-               checked_set (_maximum_j2k_bandwidth, config->maximum_j2k_bandwidth() / 1000000);
-               checked_set (_allow_any_dcp_frame_rate, config->allow_any_dcp_frame_rate ());
-               checked_set (_log_general, config->log_types() & Log::TYPE_GENERAL);
-               checked_set (_log_warning, config->log_types() & Log::TYPE_WARNING);
-               checked_set (_log_error, config->log_types() & Log::TYPE_ERROR);
-               checked_set (_log_timing, config->log_types() & Log::TYPE_TIMING);
-               checked_set (_log_debug_decode, config->log_types() & Log::TYPE_DEBUG_DECODE);
-               checked_set (_log_debug_encode, config->log_types() & Log::TYPE_DEBUG_ENCODE);
-#ifdef DCPOMATIC_WINDOWS
-               checked_set (_win32_console, config->win32_console());
-#endif
-       }
+       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;
 
-       void maximum_j2k_bandwidth_changed ()
-       {
-               Config::instance()->set_maximum_j2k_bandwidth (_maximum_j2k_bandwidth->GetValue() * 1000000);
-       }
+       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;
 
-       void allow_any_dcp_frame_rate_changed ()
-       {
-               Config::instance()->set_allow_any_dcp_frame_rate (_allow_any_dcp_frame_rate->GetValue ());
-       }
+       _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 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;
-               }
-               if (_log_debug_decode->GetValue ()) {
-                       types |= Log::TYPE_DEBUG_DECODE;
-               }
-               if (_log_debug_encode->GetValue ()) {
-                       types |= Log::TYPE_DEBUG_ENCODE;
-               }
-               Config::instance()->set_log_types (types);
-       }
+void
+LocationsPage::config_changed ()
+{
+       auto config = Config::instance ();
 
-#ifdef DCPOMATIC_WINDOWS
-       void win32_console_changed ()
-       {
-               Config::instance()->set_win32_console (_win32_console->GetValue ());
+       if (config->player_content_directory()) {
+               checked_set (_content_directory, *config->player_content_directory());
        }
-#endif
-
-       wxSpinCtrl* _maximum_j2k_bandwidth;
-       wxCheckBox* _allow_any_dcp_frame_rate;
-       wxCheckBox* _log_general;
-       wxCheckBox* _log_warning;
-       wxCheckBox* _log_error;
-       wxCheckBox* _log_timing;
-       wxCheckBox* _log_debug_decode;
-       wxCheckBox* _log_debug_encode;
-#ifdef DCPOMATIC_WINDOWS
-       wxCheckBox* _win32_console;
-#endif
-};
+       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());
+       }
+}
 
-wxPreferencesEditor*
-create_config_dialog ()
+void
+LocationsPage::content_directory_changed ()
 {
-       wxPreferencesEditor* e = new wxPreferencesEditor ();
+       Config::instance()->set_player_content_directory(wx_to_std(_content_directory->GetPath()));
+}
 
-#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 (520, -1);
-       int const border = 16;
-#else
-       wxSize ps = wxSize (-1, -1);
-       int const border = 8;
-#endif
+void
+LocationsPage::playlist_directory_changed ()
+{
+       Config::instance()->set_player_playlist_directory(wx_to_std(_playlist_directory->GetPath()));
+}
 
-       e->AddPage (new GeneralPage (ps, border));
-       e->AddPage (new DefaultsPage (ps, border));
-       e->AddPage (new EncodingServersPage (ps, border));
-       e->AddPage (new KeysPage (ps, border));
-       e->AddPage (new TMSPage (ps, border));
-       e->AddPage (new KDMEmailPage (ps, border));
-       e->AddPage (new AdvancedPage (ps, border));
-       return e;
+void
+LocationsPage::kdm_directory_changed ()
+{
+       Config::instance()->set_player_kdm_directory(wx_to_std(_kdm_directory->GetPath()));
 }