Be a little more careful to handle exceptions from boost::filesystem::file_size
[dcpomatic.git] / src / wx / config_dialog.cc
index c18cf3c92eae46e788f573182d3772bbb4d42578..e0effec53eafb90a25d2ff2e966f9d92f90406d7 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2012-2016 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2021 Carl Hetherington <cth@carlh.net>
 
     This file is part of DCP-o-matic.
 
 
 */
 
-/** @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 "file_picker_ctrl.h"
-#include "isdcf_metadata_dialog.h"
-#include "server_dialog.h"
-#include "make_chain_dialog.h"
-#include "email_dialog.h"
-#include "name_format_editor.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/cross.h"
-#include "lib/exceptions.h"
-#include <dcp/locale_convert.h>
-#include <dcp/exceptions.h>
-#include <dcp/certificate_chain.h>
-#include <wx/stdpaths.h>
-#include <wx/preferences.h>
-#include <wx/spinctrl.h>
-#include <wx/filepicker.h>
-#include <boost/filesystem.hpp>
-#include <boost/foreach.hpp>
+#include "static_text.h"
+#include "check_box.h"
+#include "nag_dialog.h"
+#include "dcpomatic_button.h"
+#include "audio_mapping_view.h"
+#include <dcp/raw_convert.h>
 #include <iostream>
 
-using std::vector;
 using std::string;
-using std::list;
-using std::cout;
+using std::vector;
 using std::pair;
 using std::make_pair;
 using std::map;
+using std::make_shared;
 using boost::bind;
-using boost::shared_ptr;
-using boost::function;
 using boost::optional;
-using dcp::locale_convert;
+using std::shared_ptr;
+using std::function;
+#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));
-       }
-
-       virtual ~Page () {}
-
-protected:
-       wxWindow* create_window (wxWindow* parent)
-       {
-               _panel = new wxPanel (parent, wxID_ANY, wxDefaultPosition, _panel_size);
-               wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
-               _panel->SetSizer (s);
+       return false;
+}
 
-               setup ();
-               _window_exists = true;
-               config_changed ();
+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));
+}
 
-               _panel->Bind (wxEVT_DESTROY, boost::bind (&Page::window_destroyed, this));
 
-               return _panel;
-       }
+wxWindow*
+Page::CreateWindow (wxWindow* parent)
+{
+       return create_window (parent);
+}
 
-       int _border;
-       wxPanel* _panel;
 
-private:
-       virtual void config_changed () = 0;
-       virtual void setup () = 0;
+wxWindow*
+Page::create_window (wxWindow* parent)
+{
+       _panel = new wxPanel (parent, wxID_ANY, wxDefaultPosition, _panel_size);
+       auto s = new wxBoxSizer (wxVERTICAL);
+       _panel->SetSizer (s);
 
-       void config_changed_wrapper ()
-       {
-               if (_window_exists) {
-                       config_changed ();
-               }
-       }
+       setup ();
+       _window_exists = true;
+       config_changed ();
 
-       void window_destroyed ()
-       {
-               _window_exists = false;
-       }
+       _panel->Bind (wxEVT_DESTROY, bind (&Page::window_destroyed, this));
 
-       wxSize _panel_size;
-       boost::signals2::scoped_connection _config_connection;
-       bool _window_exists;
-};
+       return _panel;
+}
 
-class StockPage : public wxStockPreferencesPage, public Page
+void
+Page::config_changed_wrapper ()
 {
-public:
-       StockPage (Kind kind, wxSize panel_size, int border)
-               : wxStockPreferencesPage (kind)
-               , Page (panel_size, border)
-       {}
-
-       wxWindow* CreateWindow (wxWindow* parent)
-       {
-               return create_window (parent);
+       if (_window_exists) {
+               config_changed ();
        }
-};
+}
 
-class StandardPage : public wxPreferencesPage, public Page
+void
+Page::window_destroyed ()
 {
-public:
-       StandardPage (wxSize panel_size, int border)
-               : Page (panel_size, border)
-       {}
+       _window_exists = false;
+}
 
-       wxWindow* CreateWindow (wxWindow* parent)
-       {
-               return create_window (parent);
-       }
-};
 
-class GeneralPage : public StockPage
+GeneralPage::GeneralPage (wxSize panel_size, int border)
+       : Page (panel_size, border)
 {
-public:
-       GeneralPage (wxSize panel_size, int border)
-               : StockPage (Kind_General, panel_size, border)
-       {}
-
-private:
-       void setup ()
-       {
-               wxGridBagSizer* table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
-               _panel->GetSizer()->Add (table, 1, wxALL | wxEXPAND, _border);
-
-               int r = 0;
-               _set_language = new wxCheckBox (_panel, wxID_ANY, _("Set language"));
-               table->Add (_set_language, wxGBPosition (r, 0));
-               _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 ("українська мова", "uk_UA"));
-               checked_set (_language, languages);
-               table->Add (_language, wxGBPosition (r, 1));
-               ++r;
-
-               wxStaticText* 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;
-
-               add_label_to_sizer (table, _panel, _("Threads to use for encoding on this host"), true, wxGBPosition (r, 0));
-               _num_local_encoding_threads = new wxSpinCtrl (_panel);
-               table->Add (_num_local_encoding_threads, wxGBPosition (r, 1));
-               ++r;
-
-               add_label_to_sizer (table, _panel, _("Cinema and screen database file"), true, wxGBPosition (r, 0));
-               _cinemas_file = new FilePickerCtrl (_panel, _("Select cinema and screen database file"), "*.xml");
-               table->Add (_cinemas_file, wxGBPosition (r, 1));
-               ++r;
-
-#ifdef DCPOMATIC_HAVE_EBUR128_PATCHED_FFMPEG
-               _analyse_ebur128 = new wxCheckBox (_panel, wxID_ANY, _("Find integrated loudness, true peak and loudness range when analysing audio"));
-               table->Add (_analyse_ebur128, wxGBPosition (r, 0), wxGBSpan (1, 2));
-               ++r;
-#endif
 
-               _automatic_audio_analysis = new wxCheckBox (_panel, wxID_ANY, _("Automatically analyse content audio"));
-               table->Add (_automatic_audio_analysis, wxGBPosition (r, 0), wxGBSpan (1, 2));
-               ++r;
-
-               _check_for_updates = new wxCheckBox (_panel, wxID_ANY, _("Check for updates on startup"));
-               table->Add (_check_for_updates, wxGBPosition (r, 0), wxGBSpan (1, 2));
-               ++r;
+}
 
-               _check_for_test_updates = new wxCheckBox (_panel, wxID_ANY, _("Check for testing updates on startup"));
-               table->Add (_check_for_test_updates, wxGBPosition (r, 0), wxGBSpan (1, 2));
-               ++r;
 
-               wxFlexGridSizer* bottom_table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
-               bottom_table->AddGrowableCol (1, 1);
+wxString
+GeneralPage::GetName () const
+{
+       return _("General");
+}
 
-               add_label_to_sizer (bottom_table, _panel, _("Issuer"), true);
-               _issuer = new wxTextCtrl (_panel, wxID_ANY);
-               bottom_table->Add (_issuer, 1, wxALL | wxEXPAND);
 
-               add_label_to_sizer (bottom_table, _panel, _("Creator"), true);
-               _creator = new wxTextCtrl (_panel, wxID_ANY);
-               bottom_table->Add (_creator, 1, wxALL | wxEXPAND);
+void
+GeneralPage::add_language_controls (wxGridBagSizer* table, int& r)
+{
+       _set_language = new CheckBox (_panel, _("Set language"));
+       table->Add (_set_language, wxGBPosition (r, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
+       _language = new wxChoice (_panel, wxID_ANY);
+       vector<pair<string, string>> languages;
+       languages.push_back (make_pair("Čeština", "cs_CZ"));
+       languages.push_back (make_pair("汉语/漢語", "zh_CN"));
+       languages.push_back (make_pair("Dansk", "da_DK"));
+       languages.push_back (make_pair("Deutsch", "de_DE"));
+       languages.push_back (make_pair("English", "en_GB"));
+       languages.push_back (make_pair("Español", "es_ES"));
+       languages.push_back (make_pair("Français", "fr_FR"));
+       languages.push_back (make_pair("Italiano", "it_IT"));
+       languages.push_back (make_pair("Nederlands", "nl_NL"));
+       languages.push_back (make_pair("Русский", "ru_RU"));
+       languages.push_back (make_pair("Polski", "pl_PL"));
+       languages.push_back (make_pair("Português europeu", "pt_PT"));
+       languages.push_back (make_pair("Português do Brasil", "pt_BR"));
+       languages.push_back (make_pair("Svenska", "sv_SE"));
+       languages.push_back (make_pair("Slovenský jazyk", "sk_SK"));
+       languages.push_back (make_pair("Türkçe", "tr_TR"));
+       languages.push_back (make_pair("українська мова", "uk_UA"));
+       checked_set (_language, languages);
+       table->Add (_language, wxGBPosition (r, 1));
+       ++r;
+
+       auto restart = add_label_to_sizer (
+               table, _panel, _("(restart DCP-o-matic to see language changes)"), false, wxGBPosition (r, 0), wxGBSpan (1, 2)
+               );
+       wxFont font = restart->GetFont();
+       font.SetStyle (wxFONTSTYLE_ITALIC);
+       font.SetPointSize (font.GetPointSize() - 1);
+       restart->SetFont (font);
+       ++r;
+
+       _set_language->Bind (wxEVT_CHECKBOX, bind (&GeneralPage::set_language_changed, this));
+       _language->Bind     (wxEVT_CHOICE,   bind (&GeneralPage::language_changed,     this));
+}
 
-               table->Add (bottom_table, wxGBPosition (r, 0), wxGBSpan (2, 2), wxEXPAND);
-               ++r;
+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;
 
-               _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));
-               _cinemas_file->Bind (wxEVT_COMMAND_FILEPICKER_CHANGED, boost::bind (&GeneralPage::cinemas_file_changed, this));
+       _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;
 
-               _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_CHECKBOX, bind (&GeneralPage::check_for_updates_changed, this));
+       _check_for_test_updates->Bind (wxEVT_CHECKBOX, bind (&GeneralPage::check_for_test_updates_changed, this));
+}
 
-#ifdef DCPOMATIC_HAVE_EBUR128_PATCHED_FFMPEG
-               _analyse_ebur128->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&GeneralPage::analyse_ebur128_changed, this));
-#endif
-               _automatic_audio_analysis->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&GeneralPage::automatic_audio_analysis_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::config_changed ()
+{
+       auto config = Config::instance ();
 
-               _issuer->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&GeneralPage::issuer_changed, this));
-               _creator->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&GeneralPage::creator_changed, this));
-       }
+       checked_set (_set_language, static_cast<bool>(config->language()));
 
-       void config_changed ()
-       {
-               Config* config = Config::instance ();
-
-               checked_set (_set_language, static_cast<bool>(config->language()));
-
-               /* Backwards compatibility of config file */
-
-               map<string, string> compat_map;
-               compat_map["fr"] = "fr_FR";
-               compat_map["it"] = "it_IT";
-               compat_map["es"] = "es_ES";
-               compat_map["sv"] = "sv_SE";
-               compat_map["de"] = "de_DE";
-               compat_map["nl"] = "nl_NL";
-               compat_map["ru"] = "ru_RU";
-               compat_map["pl"] = "pl_PL";
-               compat_map["da"] = "da_DK";
-               compat_map["pt"] = "pt_PT";
-               compat_map["sk"] = "sk_SK";
-               compat_map["cs"] = "cs_CZ";
-               compat_map["uk"] = "uk_UA";
-
-               string lang = config->language().get_value_or ("en_GB");
-               if (compat_map.find (lang) != compat_map.end ()) {
-                       lang = compat_map[lang];
-               }
+       /* Backwards compatibility of config file */
 
-               checked_set (_language, lang);
+       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";
 
-               checked_set (_num_local_encoding_threads, config->num_local_encoding_threads ());
-#ifdef DCPOMATIC_HAVE_EBUR128_PATCHED_FFMPEG
-               checked_set (_analyse_ebur128, config->analyse_ebur128 ());
-#endif
-               checked_set (_automatic_audio_analysis, config->automatic_audio_analysis ());
-               checked_set (_check_for_updates, config->check_for_updates ());
-               checked_set (_check_for_test_updates, config->check_for_test_updates ());
-               checked_set (_issuer, config->dcp_issuer ());
-               checked_set (_creator, config->dcp_creator ());
-               checked_set (_cinemas_file, config->cinemas_file());
-
-               setup_sensitivity ();
+       auto lang = config->language().get_value_or("en_GB");
+       if (compat_map.find(lang) != compat_map.end ()) {
+               lang = compat_map[lang];
        }
 
-       void setup_sensitivity ()
-       {
-               _language->Enable (_set_language->GetValue ());
-               _check_for_test_updates->Enable (_check_for_updates->GetValue ());
-       }
+       checked_set (_language, lang);
 
-       void set_language_changed ()
-       {
-               setup_sensitivity ();
-               if (_set_language->GetValue ()) {
-                       language_changed ();
-               } else {
-                       Config::instance()->unset_language ();
-               }
-       }
+       checked_set (_check_for_updates, config->check_for_updates ());
+       checked_set (_check_for_test_updates, config->check_for_test_updates ());
 
-       void 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 ();
-               }
-       }
-
-#ifdef DCPOMATIC_HAVE_EBUR128_PATCHED_FFMPEG
-       void analyse_ebur128_changed ()
-       {
-               Config::instance()->set_analyse_ebur128 (_analyse_ebur128->GetValue ());
-       }
-#endif
-
-       void automatic_audio_analysis_changed ()
-       {
-               Config::instance()->set_automatic_audio_analysis (_automatic_audio_analysis->GetValue ());
-       }
-
-       void check_for_updates_changed ()
-       {
-               Config::instance()->set_check_for_updates (_check_for_updates->GetValue ());
-       }
+       setup_sensitivity ();
+}
 
-       void check_for_test_updates_changed ()
-       {
-               Config::instance()->set_check_for_test_updates (_check_for_test_updates->GetValue ());
-       }
+void
+GeneralPage::setup_sensitivity ()
+{
+       _language->Enable (_set_language->GetValue ());
+       _check_for_test_updates->Enable (_check_for_updates->GetValue ());
+}
 
-       void num_local_encoding_threads_changed ()
-       {
-               Config::instance()->set_num_local_encoding_threads (_num_local_encoding_threads->GetValue ());
+void
+GeneralPage::set_language_changed ()
+{
+       setup_sensitivity ();
+       if (_set_language->GetValue ()) {
+               language_changed ();
+       } else {
+               Config::instance()->unset_language ();
        }
+}
 
-       void issuer_changed ()
-       {
-               Config::instance()->set_dcp_issuer (wx_to_std (_issuer->GetValue ()));
+void
+GeneralPage::language_changed ()
+{
+       int const sel = _language->GetSelection ();
+       if (sel != -1) {
+               Config::instance()->set_language (string_client_data (_language->GetClientObject (sel)));
+       } else {
+               Config::instance()->unset_language ();
        }
+}
 
-       void creator_changed ()
-       {
-               Config::instance()->set_dcp_creator (wx_to_std (_creator->GetValue ()));
-       }
+void
+GeneralPage::check_for_updates_changed ()
+{
+       Config::instance()->set_check_for_updates (_check_for_updates->GetValue ());
+}
 
-       void cinemas_file_changed ()
-       {
-               Config::instance()->set_cinemas_file (wx_to_std (_cinemas_file->GetPath ()));
-       }
+void
+GeneralPage::check_for_test_updates_changed ()
+{
+       Config::instance()->set_check_for_test_updates (_check_for_test_updates->GetValue ());
+}
 
-       wxCheckBox* _set_language;
-       wxChoice* _language;
-       wxSpinCtrl* _num_local_encoding_threads;
-       FilePickerCtrl* _cinemas_file;
-#ifdef DCPOMATIC_HAVE_EBUR128_PATCHED_FFMPEG
-       wxCheckBox* _analyse_ebur128;
-#endif
-       wxCheckBox* _automatic_audio_analysis;
-       wxCheckBox* _check_for_updates;
-       wxCheckBox* _check_for_test_updates;
-       wxTextCtrl* _issuer;
-       wxTextCtrl* _creator;
-};
-
-class DefaultsPage : public StandardPage
+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)
 {
-public:
-       DefaultsPage (wxSize panel_size, int border)
-               : StandardPage (panel_size, border)
-       {}
+       _sizer = new wxBoxSizer (wxVERTICAL);
 
-       wxString GetName () const
-       {
-               return _("Defaults");
-       }
+       auto certificates_sizer = new wxBoxSizer (wxHORIZONTAL);
+       _sizer->Add (certificates_sizer, 0, wxALL, border);
+
+       _certificates = new wxListCtrl (this, wxID_ANY, wxDefaultPosition, wxSize (440, 150), wxLC_REPORT | wxLC_SINGLE_SEL);
 
-#ifdef DCPOMATIC_OSX
-       wxBitmap GetLargeIcon () const
        {
-               return wxBitmap ("defaults", wxBITMAP_TYPE_PNG_RESOURCE);
+               wxListItem ip;
+               ip.SetId (0);
+               ip.SetText (_("Type"));
+               ip.SetWidth (100);
+               _certificates->InsertColumn (0, ip);
        }
-#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);
-               }
+               wxListItem ip;
+               ip.SetId (1);
+               ip.SetText (_("Thumbprint"));
+               ip.SetWidth (340);
 
-               add_label_to_sizer (table, _panel, _("Default directory for new films"), true);
-#ifdef DCPOMATIC_USE_OWN_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 DCP audio channels"), true);
-               _dcp_audio_channels = new wxChoice (_panel, wxID_ANY);
-               table->Add (_dcp_audio_channels);
-
-               {
-                       add_label_to_sizer (table, _panel, _("Default JPEG2000 bandwidth"), true);
-                       wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-                       _j2k_bandwidth = new wxSpinCtrl (_panel);
-                       s->Add (_j2k_bandwidth);
-                       add_label_to_sizer (s, _panel, _("Mbit/s"), false);
-                       table->Add (s, 1);
-               }
-
-               {
-                       add_label_to_sizer (table, _panel, _("Default audio delay"), true);
-                       wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-                       _audio_delay = new wxSpinCtrl (_panel);
-                       s->Add (_audio_delay);
-                       add_label_to_sizer (s, _panel, _("ms"), false);
-                       table->Add (s, 1);
-               }
-
-               add_label_to_sizer (table, _panel, _("Default standard"), true);
-               _standard = new wxChoice (_panel, wxID_ANY);
-               table->Add (_standard);
-
-               _still_length->SetRange (1, 3600);
-               _still_length->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&DefaultsPage::still_length_changed, this));
-
-               _directory->Bind (wxEVT_COMMAND_DIRPICKER_CHANGED, boost::bind (&DefaultsPage::directory_changed, this));
-
-               _isdcf_metadata_button->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&DefaultsPage::edit_isdcf_metadata_clicked, this));
-
-               vector<Ratio const *> ratios = Ratio::all ();
-               for (size_t i = 0; i < ratios.size(); ++i) {
-                       _container->Append (std_to_wx (ratios[i]->nickname ()));
-               }
-
-               _container->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&DefaultsPage::container_changed, this));
-
-               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 ()));
-               }
-
-               setup_audio_channels_choice (_dcp_audio_channels, 2);
-
-               _dcp_content_type->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&DefaultsPage::dcp_content_type_changed, this));
-               _dcp_audio_channels->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&DefaultsPage::dcp_audio_channels_changed, this));
-
-               _j2k_bandwidth->SetRange (50, 250);
-               _j2k_bandwidth->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&DefaultsPage::j2k_bandwidth_changed, this));
-
-               _audio_delay->SetRange (-1000, 1000);
-               _audio_delay->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&DefaultsPage::audio_delay_changed, this));
+               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 ();
+}
 
-               _standard->Append (_("SMPTE"));
-               _standard->Append (_("Interop"));
-               _standard->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&DefaultsPage::standard_changed, this));
-       }
+void
+CertificateChainEditor::add_button (wxWindow* button)
+{
+       _button_sizer->Add (button, 0, wxLEFT | wxRIGHT, DCPOMATIC_SIZER_X_GAP);
+       _sizer->Layout ();
+}
 
-       void config_changed ()
-       {
-               Config* config = Config::instance ();
+void
+CertificateChainEditor::add_certificate ()
+{
+       auto d = new wxFileDialog (this, _("Select Certificate File"));
 
-               vector<Ratio const *> ratios = Ratio::all ();
-               for (size_t i = 0; i < ratios.size(); ++i) {
-                       if (ratios[i] == config->default_container ()) {
-                               _container->SetSelection (i);
+       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;
                        }
-               }
 
-               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);
+                       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()));
                }
-
-               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 (_dcp_audio_channels, locale_convert<string> (config->default_dcp_audio_channels()));
-               checked_set (_audio_delay, config->default_audio_delay ());
-               checked_set (_standard, config->default_interop() ? 1 : 0);
        }
 
-       void j2k_bandwidth_changed ()
-       {
-               Config::instance()->set_default_j2k_bandwidth (_j2k_bandwidth->GetValue() * 1000000);
-       }
+       d->Destroy ();
 
-       void audio_delay_changed ()
-       {
-               Config::instance()->set_default_audio_delay (_audio_delay->GetValue());
-       }
-
-       void dcp_audio_channels_changed ()
-       {
-               int const s = _dcp_audio_channels->GetSelection ();
-               if (s != wxNOT_FOUND) {
-                       Config::instance()->set_default_dcp_audio_channels (
-                               locale_convert<int> (string_client_data (_dcp_audio_channels->GetClientObject (s)))
-                               );
-               }
-       }
-
-       void directory_changed ()
-       {
-               Config::instance()->set_default_directory (wx_to_std (_directory->GetPath ()));
-       }
-
-       void edit_isdcf_metadata_clicked ()
-       {
-               ISDCFMetadataDialog* d = new ISDCFMetadataDialog (_panel, Config::instance()->default_isdcf_metadata (), false);
-               d->ShowModal ();
-               Config::instance()->set_default_isdcf_metadata (d->isdcf_metadata ());
-               d->Destroy ();
-       }
-
-       void still_length_changed ()
-       {
-               Config::instance()->set_default_still_length (_still_length->GetValue ());
-       }
+       update_sensitivity ();
+}
 
-       void container_changed ()
-       {
-               vector<Ratio const *> ratio = Ratio::all ();
-               Config::instance()->set_default_container (ratio[_container->GetSelection()]);
+void
+CertificateChainEditor::remove_certificate ()
+{
+       if (_nag_alter()) {
+               /* Cancel was clicked */
+               return;
        }
 
-       void dcp_content_type_changed ()
-       {
-               vector<DCPContentType const *> ct = DCPContentType::all ();
-               Config::instance()->set_default_dcp_content_type (ct[_dcp_content_type->GetSelection()]);
+       int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
+       if (i == -1) {
+               return;
        }
 
-       void standard_changed ()
-       {
-               Config::instance()->set_default_interop (_standard->GetSelection() == 1);
-       }
+       _certificates->DeleteItem (i);
+       auto chain = make_shared<dcp::CertificateChain>(*_get().get());
+       chain->remove (i);
+       _set (chain);
 
-       wxSpinCtrl* _j2k_bandwidth;
-       wxSpinCtrl* _audio_delay;
-       wxButton* _isdcf_metadata_button;
-       wxSpinCtrl* _still_length;
-#ifdef DCPOMATIC_USE_OWN_PICKER
-       DirPickerCtrl* _directory;
-#else
-       wxDirPickerCtrl* _directory;
-#endif
-       wxChoice* _container;
-       wxChoice* _dcp_content_type;
-       wxChoice* _dcp_audio_channels;
-       wxChoice* _standard;
-};
+       update_sensitivity ();
+       update_certificate_list ();
+}
 
-class EncodingServersPage : public StandardPage
+void
+CertificateChainEditor::export_certificate ()
 {
-public:
-       EncodingServersPage (wxSize panel_size, int border)
-               : StandardPage (panel_size, border)
-       {}
-
-       wxString GetName () const
-       {
-               return _("Servers");
+       int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
+       if (i == -1) {
+               return;
        }
 
-#ifdef DCPOMATIC_OSX
-       wxBitmap GetLargeIcon () const
-       {
-               return wxBitmap ("servers", wxBITMAP_TYPE_PNG_RESOURCE);
-       }
-#endif
-
-private:
-       void setup ()
-       {
-               _use_any_servers = new wxCheckBox (_panel, wxID_ANY, _("Search network for 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 (&always_valid),
-                       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));
-       }
+       auto all = _get()->root_to_leaf();
 
-       void config_changed ()
-       {
-               checked_set (_use_any_servers, Config::instance()->use_any_servers ());
-               _servers_list->refresh ();
+       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 use_any_servers_changed ()
-       {
-               Config::instance()->set_use_any_servers (_use_any_servers->GetValue ());
-       }
+       auto d = new wxFileDialog(
+               this, _("Select Certificate File"), wxEmptyString, default_name, wxT ("PEM files (*.pem)|*.pem"),
+               wxFD_SAVE | wxFD_OVERWRITE_PROMPT
+               );
 
-       string server_column (string s)
-       {
-               return s;
+       auto j = all.begin ();
+       for (int k = 0; k < i; ++k) {
+               ++j;
        }
 
-       wxCheckBox* _use_any_servers;
-       EditableList<string, ServerDialog>* _servers_list;
-};
-
-class CertificateChainEditor : public wxPanel
-{
-public:
-       CertificateChainEditor (
-               wxWindow* parent,
-               wxString title,
-               int border,
-               function<void (shared_ptr<dcp::CertificateChain>)> set,
-               function<shared_ptr<const dcp::CertificateChain> (void)> get
-               )
-               : wxPanel (parent)
-               , _set (set)
-               , _get (get)
-       {
-               wxFont subheading_font (*wxNORMAL_FONT);
-               subheading_font.SetWeight (wxFONTWEIGHT_BOLD);
-
-               _sizer = new wxBoxSizer (wxVERTICAL);
-
-               {
-                       wxStaticText* m = new wxStaticText (this, wxID_ANY, title);
-                       m->SetFont (subheading_font);
-                       _sizer->Add (m, 0, wxALL, border);
+       if (d->ShowModal () == wxID_OK) {
+               boost::filesystem::path path (wx_to_std(d->GetPath()));
+               if (path.extension() != ".pem") {
+                       path += ".pem";
                }
-
-               wxBoxSizer* certificates_sizer = new wxBoxSizer (wxHORIZONTAL);
-               _sizer->Add (certificates_sizer, 0, wxLEFT | wxRIGHT, border);
-
-               _certificates = new wxListCtrl (this, wxID_ANY, wxDefaultPosition, wxSize (440, 150), wxLC_REPORT | wxLC_SINGLE_SEL);
-
-               {
-                       wxListItem ip;
-                       ip.SetId (0);
-                       ip.SetText (_("Type"));
-                       ip.SetWidth (100);
-                       _certificates->InsertColumn (0, ip);
+               auto f = fopen_boost (path, "w");
+               if (!f) {
+                       throw OpenFileError (path, errno, OpenFileError::WRITE);
                }
 
-               {
-                       wxListItem ip;
-                       ip.SetId (1);
-                       ip.SetText (_("Thumbprint"));
-                       ip.SetWidth (340);
-
-                       wxFont font = ip.GetFont ();
-                       font.SetFamily (wxFONTFAMILY_TELETYPE);
-                       ip.SetFont (font);
+               string const s = j->certificate (true);
+               checked_fwrite (s.c_str(), s.length(), f, path);
+               fclose (f);
+       }
+       d->Destroy ();
+}
 
-                       _certificates->InsertColumn (1, ip);
+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";
                }
-
-               certificates_sizer->Add (_certificates, 1, wxEXPAND);
-
-               {
-                       wxSizer* s = new wxBoxSizer (wxVERTICAL);
-                       _add_certificate = new wxButton (this, wxID_ANY, _("Add..."));
-                       s->Add (_add_certificate, 0, wxTOP | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
-                       _remove_certificate = new wxButton (this, wxID_ANY, _("Remove"));
-                       s->Add (_remove_certificate, 0, wxTOP | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
-                       _export_certificate = new wxButton (this, wxID_ANY, _("Export"));
-                       s->Add (_export_certificate, 0, wxTOP | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
-                       certificates_sizer->Add (s, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP);
+               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);
-               _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 wxStaticText (this, wxID_ANY, 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);
-               _load_private_key = new wxButton (this, wxID_ANY, _("Load..."));
-               table->Add (_load_private_key, wxGBPosition (r, 2));
-               _export_private_key = new wxButton (this, wxID_ANY, _("Export..."));
-               table->Add (_export_private_key, wxGBPosition (r, 3));
-               ++r;
-
-               _button_sizer = new wxBoxSizer (wxHORIZONTAL);
-               _remake_certificates = new wxButton (this, wxID_ANY, _("Re-make certificates\nand key..."));
-               _button_sizer->Add (_remake_certificates, 1, wxRIGHT, border);
-               table->Add (_button_sizer, wxGBPosition (r, 0), wxGBSpan (1, 4));
-               ++r;
-
-               _add_certificate->Bind     (wxEVT_COMMAND_BUTTON_CLICKED,       boost::bind (&CertificateChainEditor::add_certificate, this));
-               _remove_certificate->Bind  (wxEVT_COMMAND_BUTTON_CLICKED,       boost::bind (&CertificateChainEditor::remove_certificate, this));
-               _export_certificate->Bind  (wxEVT_COMMAND_BUTTON_CLICKED,       boost::bind (&CertificateChainEditor::export_certificate, this));
-               _certificates->Bind        (wxEVT_COMMAND_LIST_ITEM_SELECTED,   boost::bind (&CertificateChainEditor::update_sensitivity, this));
-               _certificates->Bind        (wxEVT_COMMAND_LIST_ITEM_DESELECTED, boost::bind (&CertificateChainEditor::update_sensitivity, this));
-               _remake_certificates->Bind (wxEVT_COMMAND_BUTTON_CLICKED,       boost::bind (&CertificateChainEditor::remake_certificates, this));
-               _load_private_key->Bind    (wxEVT_COMMAND_BUTTON_CLICKED,       boost::bind (&CertificateChainEditor::load_private_key, this));
-               _export_private_key->Bind  (wxEVT_COMMAND_BUTTON_CLICKED,       boost::bind (&CertificateChainEditor::export_private_key, this));
-
-               SetSizerAndFit (_sizer);
+               auto const s = _get()->chain();
+               checked_fwrite (s.c_str(), s.length(), f, path);
+               fclose (f);
        }
 
-       void config_changed ()
-       {
-               _chain.reset (new dcp::CertificateChain (*_get().get ()));
-
-               update_certificate_list ();
-               update_private_key ();
-               update_sensitivity ();
-       }
-
-       void add_button (wxWindow* button)
-       {
-               _button_sizer->Add (button, 0, wxLEFT | wxRIGHT, DCPOMATIC_SIZER_X_GAP);
-               _sizer->Layout ();
-       }
-
-private:
-       void add_certificate ()
-       {
-               wxFileDialog* d = new wxFileDialog (this, _("Select Certificate File"));
+       d->Destroy ();
+}
 
-               if (d->ShowModal() == wxID_OK) {
-                       try {
-                               dcp::Certificate c (dcp::file_to_string (wx_to_std (d->GetPath ())));
-                               if (c.extra_data ()) {
-                                       message_dialog (
-                                               this,
-                                               _("This file contains other certificates (or other data) after its first certificate. "
-                                                 "Only the first certificate will be used.")
-                                               );
-                               }
-                               _chain->add (c);
-                               _set (_chain);
-                               update_certificate_list ();
-                       } catch (dcp::MiscError& e) {
-                               error_dialog (this, wxString::Format (_("Could not read certificate file (%s)"), e.what ()));
-                       }
+void
+CertificateChainEditor::update_certificate_list ()
+{
+       _certificates->DeleteAllItems ();
+       size_t n = 0;
+       auto certs = _get()->root_to_leaf();
+       for (auto const& i: certs) {
+               wxListItem item;
+               item.SetId (n);
+               _certificates->InsertItem (item);
+               _certificates->SetItem (n, 1, std_to_wx (i.thumbprint ()));
+
+               if (n == 0) {
+                       _certificates->SetItem (n, 0, _("Root"));
+               } else if (n == (certs.size() - 1)) {
+                       _certificates->SetItem (n, 0, _("Leaf"));
+               } else {
+                       _certificates->SetItem (n, 0, _("Intermediate"));
                }
 
-               d->Destroy ();
-
-               update_sensitivity ();
+               ++n;
        }
 
-       void remove_certificate ()
-       {
-               int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
-               if (i == -1) {
-                       return;
-               }
-
-               _certificates->DeleteItem (i);
-               _chain->remove (i);
-               _set (_chain);
+       static wxColour normal = _private_key_bad->GetForegroundColour ();
 
-               update_sensitivity ();
+       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 export_certificate ()
-       {
-               int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
-               if (i == -1) {
-                       return;
-               }
-
-               wxFileDialog* d = new wxFileDialog (
-                       this, _("Select Certificate File"), wxEmptyString, wxEmptyString, wxT ("PEM files (*.pem)|*.pem"),
-                       wxFD_SAVE | wxFD_OVERWRITE_PROMPT
+void
+CertificateChainEditor::remake_certificates ()
+{
+       auto chain = _get();
+
+       string subject_organization_name;
+       string subject_organizational_unit_name;
+       string root_common_name;
+       string intermediate_common_name;
+       string leaf_common_name;
+
+       auto all = chain->root_to_leaf ();
+
+       if (all.size() >= 1) {
+               /* Have a root */
+               subject_organization_name = chain->root().subject_organization_name ();
+               subject_organizational_unit_name = chain->root().subject_organizational_unit_name ();
+               root_common_name = chain->root().subject_common_name ();
+       }
+
+       if (all.size() >= 2) {
+               /* Have a leaf */
+               leaf_common_name = chain->leaf().subject_common_name ();
+       }
+
+       if (all.size() >= 3) {
+               /* Have an intermediate */
+               dcp::CertificateChain::List::iterator i = all.begin ();
+               ++i;
+               intermediate_common_name = i->subject_common_name ();
+       }
+
+       if (_nag_alter()) {
+               /* Cancel was clicked */
+               return;
+       }
+
+       auto d = new MakeChainDialog (
+               this,
+               subject_organization_name,
+               subject_organizational_unit_name,
+               root_common_name,
+               intermediate_common_name,
+               leaf_common_name
+               );
+
+       if (d->ShowModal () == wxID_OK) {
+               _set (
+                       make_shared<dcp::CertificateChain> (
+                               openssl_path (),
+                               d->organisation (),
+                               d->organisational_unit (),
+                               d->root_common_name (),
+                               d->intermediate_common_name (),
+                               d->leaf_common_name ()
+                               )
                        );
 
-               dcp::CertificateChain::List all = _chain->root_to_leaf ();
-               dcp::CertificateChain::List::iterator j = all.begin ();
-               for (int k = 0; k < i; ++k) {
-                       ++j;
-               }
+               update_certificate_list ();
+               update_private_key ();
+       }
 
-               if (d->ShowModal () == wxID_OK) {
-                       FILE* f = fopen_boost (wx_to_std (d->GetPath ()), "w");
-                       if (!f) {
-                               throw OpenFileError (wx_to_std (d->GetPath ()));
-                       }
+       d->Destroy ();
+}
 
-                       string const s = j->certificate (true);
-                       fwrite (s.c_str(), 1, s.length(), f);
-                       fclose (f);
-               }
-               d->Destroy ();
-       }
+void
+CertificateChainEditor::update_sensitivity ()
+{
+       /* 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);
+}
 
-       void update_certificate_list ()
-       {
-               _certificates->DeleteAllItems ();
-               size_t n = 0;
-               dcp::CertificateChain::List certs = _chain->root_to_leaf ();
-               BOOST_FOREACH (dcp::Certificate 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"));
+void
+CertificateChainEditor::update_private_key ()
+{
+       checked_set (_private_key, dcp::private_key_fingerprint (_get()->key().get()));
+       _sizer->Layout ();
+}
+
+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;
                        }
 
-                       ++n;
+                       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()));
                }
        }
 
-       void remake_certificates ()
-       {
-               shared_ptr<const dcp::CertificateChain> chain = _get ();
+       d->Destroy ();
 
-               string subject_organization_name;
-               string subject_organizational_unit_name;
-               string root_common_name;
-               string intermediate_common_name;
-               string leaf_common_name;
+       update_sensitivity ();
+}
 
-               dcp::CertificateChain::List all = chain->root_to_leaf ();
+void
+CertificateChainEditor::export_private_key ()
+{
+       auto key = _get()->key();
+       if (!key) {
+               return;
+       }
 
-               if (all.size() >= 1) {
-                       /* Have a root */
-                       subject_organization_name = chain->root().subject_organization_name ();
-                       subject_organizational_unit_name = chain->root().subject_organizational_unit_name ();
-                       root_common_name = chain->root().subject_common_name ();
-               }
+       auto d = new wxFileDialog (
+               this, _("Select Key File"), wxEmptyString, wxT("private_key.pem"), wxT("PEM files (*.pem)|*.pem"),
+               wxFD_SAVE | wxFD_OVERWRITE_PROMPT
+               );
 
-               if (all.size() >= 2) {
-                       /* Have a leaf */
-                       leaf_common_name = chain->leaf().subject_common_name ();
+       if (d->ShowModal () == wxID_OK) {
+               boost::filesystem::path path (wx_to_std(d->GetPath()));
+               if (path.extension() != ".pem") {
+                       path += ".pem";
                }
-
-               if (all.size() >= 3) {
-                       /* Have an intermediate */
-                       dcp::CertificateChain::List::iterator i = all.begin ();
-                       ++i;
-                       intermediate_common_name = i->subject_common_name ();
+               auto f = fopen_boost (path, "w");
+               if (!f) {
+                       throw OpenFileError (path, errno, OpenFileError::WRITE);
                }
 
-               MakeChainDialog* d = new MakeChainDialog (
-                       this,
-                       subject_organization_name,
-                       subject_organizational_unit_name,
-                       root_common_name,
-                       intermediate_common_name,
-                       leaf_common_name
-                       );
+               auto const s = _get()->key().get ();
+               checked_fwrite (s.c_str(), s.length(), f, path);
+               fclose (f);
+       }
+       d->Destroy ();
+}
 
-               if (d->ShowModal () == wxID_OK) {
-                       _chain.reset (
-                               new dcp::CertificateChain (
-                                       openssl_path (),
-                                       d->organisation (),
-                                       d->organisational_unit (),
-                                       d->root_common_name (),
-                                       d->intermediate_common_name (),
-                                       d->leaf_common_name ()
-                                       )
-                               );
-
-                       _set (_chain);
-                       update_certificate_list ();
-                       update_private_key ();
-               }
+wxString
+KeysPage::GetName () const
+{
+       return _("Keys");
+}
 
-               d->Destroy ();
-       }
+void
+KeysPage::setup ()
+{
+       wxFont subheading_font (*wxNORMAL_FONT);
+       subheading_font.SetWeight (wxFONTWEIGHT_BOLD);
 
-       void update_sensitivity ()
-       {
-               _remove_certificate->Enable (_certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED) != -1);
-               _export_certificate->Enable (_certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED) != -1);
-       }
+       auto sizer = _panel->GetSizer();
 
-       void update_private_key ()
        {
-               checked_set (_private_key, dcp::private_key_fingerprint (_chain->key().get ()));
-               _sizer->Layout ();
+               auto m = new StaticText (_panel, _("Decrypting KDMs"));
+               m->SetFont (subheading_font);
+               sizer->Add (m, 0, wxALL | wxEXPAND, _border);
        }
 
-       void load_private_key ()
-       {
-               wxFileDialog* d = new wxFileDialog (this, _("Select Key File"));
+       auto buttons = new wxBoxSizer (wxVERTICAL);
 
-               if (d->ShowModal() == wxID_OK) {
-                       try {
-                               boost::filesystem::path p (wx_to_std (d->GetPath ()));
-                               if (boost::filesystem::file_size (p) > 8192) {
-                                       error_dialog (
-                                               this,
-                                               wxString::Format (_("Could not read key file; file is too long (%s)"), std_to_wx (p.string ()))
-                                               );
-                                       return;
-                               }
+       auto export_decryption_certificate = new Button (_panel, _("Export KDM decryption leaf certificate..."));
+       buttons->Add (export_decryption_certificate, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
+       auto export_settings = new Button (_panel, _("Export all KDM decryption settings..."));
+       buttons->Add (export_settings, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
+       auto import_settings = new Button (_panel, _("Import all KDM decryption settings..."));
+       buttons->Add (import_settings, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
+       auto decryption_advanced = new Button (_panel, _("Advanced..."));
+       buttons->Add (decryption_advanced, 0);
 
-                               _chain->set_key (dcp::file_to_string (p));
-                               _set (_chain);
-                               update_private_key ();
-                       } catch (dcp::MiscError& e) {
-                               error_dialog (this, wxString::Format (_("Could not read certificate file (%s)"), e.what ()));
-                       }
-               }
+       sizer->Add (buttons, 0, wxLEFT, _border);
 
-               d->Destroy ();
+       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));
 
-               update_sensitivity ();
-       }
-
-       void export_private_key ()
        {
-               optional<string> key = _chain->key ();
-               if (!key) {
-                       return;
-               }
-
-               wxFileDialog* d = new wxFileDialog (
-                       this, _("Select Key File"), wxEmptyString, wxEmptyString, wxT ("PEM files (*.pem)|*.pem"),
-                       wxFD_SAVE | wxFD_OVERWRITE_PROMPT
-                       );
+               auto m = new StaticText (_panel, _("Signing DCPs and KDMs"));
+               m->SetFont (subheading_font);
+               sizer->Add (m, 0, wxALL | wxEXPAND, _border);
+       }
 
-               if (d->ShowModal () == wxID_OK) {
-                       FILE* f = fopen_boost (wx_to_std (d->GetPath ()), "w");
-                       if (!f) {
-                               throw OpenFileError (wx_to_std (d->GetPath ()));
-                       }
+       auto signing_advanced = new Button (_panel, _("Advanced..."));
+       sizer->Add (signing_advanced, 0, wxLEFT | wxBOTTOM, _border);
+       signing_advanced->Bind (wxEVT_BUTTON, bind (&KeysPage::signing_advanced, this));
+}
 
-                       string const s = _chain->key().get ();
-                       fwrite (s.c_str(), 1, s.length(), f);
-                       fclose (f);
-               }
-               d->Destroy ();
-       }
+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();
+}
 
-       wxListCtrl* _certificates;
-       wxButton* _add_certificate;
-       wxButton* _export_certificate;
-       wxButton* _remove_certificate;
-       wxButton* _remake_certificates;
-       wxStaticText* _private_key;
-       wxButton* _load_private_key;
-       wxButton* _export_private_key;
-       wxSizer* _sizer;
-       wxBoxSizer* _button_sizer;
-       shared_ptr<dcp::CertificateChain> _chain;
-       boost::function<void (shared_ptr<dcp::CertificateChain>)> _set;
-       boost::function<shared_ptr<const dcp::CertificateChain> (void)> _get;
-};
-
-class KeysPage : public StandardPage
+void
+KeysPage::signing_advanced ()
 {
-public:
-       KeysPage (wxSize panel_size, int border)
-               : StandardPage (panel_size, border)
-       {}
+       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();
+}
 
-       wxString GetName () const
-       {
-               return _("Keys");
-       }
+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);
+               }
 
-#ifdef DCPOMATIC_OSX
-       wxBitmap GetLargeIcon () const
-       {
-               return wxBitmap ("keys", wxBITMAP_TYPE_PNG_RESOURCE);
+               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);
        }
-#endif
+       d->Destroy ();
 
-private:
-
-       void setup ()
-       {
-               _signer = new CertificateChainEditor (
-                       _panel, _("Signing DCPs and KDMs"), _border,
-                       boost::bind (&Config::set_signer_chain, Config::instance (), _1),
-                       boost::bind (&Config::signer_chain, Config::instance ())
-                       );
-
-               _panel->GetSizer()->Add (_signer);
-
-               _decryption = new CertificateChainEditor (
-                       _panel, _("Decrypting DCPs"), _border,
-                       boost::bind (&Config::set_decryption_chain, Config::instance (), _1),
-                       boost::bind (&Config::decryption_chain, Config::instance ())
-                       );
-
-               _panel->GetSizer()->Add (_decryption);
-
-               _export_decryption_certificate = new wxButton (_decryption, wxID_ANY, _("Export DCP decryption\ncertificate..."));
-               _decryption->add_button (_export_decryption_certificate);
-               _export_decryption_chain = new wxButton (_decryption, wxID_ANY, _("Export DCP decryption\nchain..."));
-               _decryption->add_button (_export_decryption_chain);
+}
 
-               _export_decryption_certificate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::export_decryption_certificate, this));
-               _export_decryption_chain->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::export_decryption_chain, this));
+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 export_decryption_certificate ()
-       {
-               wxFileDialog* d = new wxFileDialog (
-                       _panel, _("Select Certificate File"), wxEmptyString, wxEmptyString, wxT ("PEM files (*.pem)|*.pem"),
-                       wxFD_SAVE | wxFD_OVERWRITE_PROMPT
-                       );
+       auto d = new wxFileDialog (
+               _panel, _("Select File To Import"), wxEmptyString, wxEmptyString, wxT ("DOM files (*.dom)|*.dom")
+               );
 
-               if (d->ShowModal () == wxID_OK) {
-                       FILE* f = fopen_boost (wx_to_std (d->GetPath ()), "w");
-                       if (!f) {
-                               throw OpenFileError (wx_to_std (d->GetPath ()));
-                       }
+       if (d->ShowModal () == wxID_OK) {
+               auto new_chain = make_shared<dcp::CertificateChain>();
 
-                       string const s = Config::instance()->decryption_chain()->leaf().certificate (true);
-                       fwrite (s.c_str(), 1, s.length(), f);
-                       fclose (f);
+               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 export_decryption_chain ()
-       {
-               wxFileDialog* d = new wxFileDialog (
-                       _panel, _("Select Chain 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 = "";
                        }
-
-                       string const s = Config::instance()->decryption_chain()->chain();
-                       fwrite (s.c_str(), 1, s.length(), f);
-                       fclose (f);
                }
-               d->Destroy ();
-       }
+               fclose (f);
 
-       void config_changed ()
-       {
-               _signer->config_changed ();
-               _decryption->config_changed ();
+               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 ();
+}
 
-       CertificateChainEditor* _signer;
-       CertificateChainEditor* _decryption;
-       wxButton* _export_decryption_certificate;
-       wxButton* _export_decryption_chain;
-};
-
-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 and Doremi)"));
-               _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 ();
-
-               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 ());
-       }
+       auto d = new wxFileDialog (
+               _panel, _("Select Certificate File"), wxEmptyString, default_name, wxT("PEM files (*.pem)|*.pem"),
+               wxFD_SAVE | wxFD_OVERWRITE_PROMPT
+               );
 
-       void tms_protocol_changed ()
-       {
-               Config::instance()->set_tms_protocol (static_cast<Protocol> (_tms_protocol->GetSelection ()));
-       }
+       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_ip_changed ()
-       {
-               Config::instance()->set_tms_ip (wx_to_std (_tms_ip->GetValue ()));
+               auto const s = Config::instance()->decryption_chain()->leaf().certificate (true);
+               checked_fwrite (s.c_str(), s.length(), f, path);
+               fclose (f);
        }
 
-       void tms_path_changed ()
-       {
-               Config::instance()->set_tms_path (wx_to_std (_tms_path->GetValue ()));
-       }
+       d->Destroy ();
+}
 
-       void tms_user_changed ()
-       {
-               Config::instance()->set_tms_user (wx_to_std (_tms_user->GetValue ()));
-       }
+wxString
+SoundPage::GetName () const
+{
+       return _("Sound");
+}
 
-       void tms_password_changed ()
-       {
-               Config::instance()->set_tms_password (wx_to_std (_tms_password->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, 600);
+       table->Add (_map, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND);
+       ++r;
+
+       _reset_to_default = new Button (_panel, _("Reset to default"));
+       table->Add (_reset_to_default, wxGBPosition(r, 1));
+       ++r;
+
+       wxFont font = _sound_output_details->GetFont();
+       font.SetStyle (wxFONTSTYLE_ITALIC);
+       font.SetPointSize (font.GetPointSize() - 1);
+       _sound_output_details->SetFont (font);
+
+       RtAudio audio (DCPOMATIC_RTAUDIO_API);
+       for (unsigned int i = 0; i < audio.getDeviceCount(); ++i) {
+               try {
+                       auto dev = audio.getDeviceInfo (i);
+                       if (dev.probed && dev.outputChannels > 0) {
+                               _sound_output->Append (std_to_wx (dev.name));
+                       }
+               } catch (RtAudioError&) {
+                       /* Something went wrong so let's just ignore that device */
+               }
        }
 
-       wxChoice* _tms_protocol;
-       wxTextCtrl* _tms_ip;
-       wxTextCtrl* _tms_path;
-       wxTextCtrl* _tms_user;
-       wxTextCtrl* _tms_password;
-};
+       _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));
+}
 
-static string
-column (string s)
+void
+SoundPage::reset_to_default ()
 {
-       return s;
+       Config::instance()->set_audio_mapping_to_default ();
 }
 
-class KDMEmailPage : public StandardPage
+void
+SoundPage::map_changed (AudioMapping m)
 {
-public:
-
-       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
-       {}
+       Config::instance()->set_audio_mapping (m);
+}
 
-       wxString GetName () const
-       {
-               return _("KDM Email");
-       }
+void
+SoundPage::sound_changed ()
+{
+       Config::instance()->set_sound (_sound->GetValue ());
+}
 
-#ifdef DCPOMATIC_OSX
-       wxBitmap GetLargeIcon () const
-       {
-               return wxBitmap ("kdm_email", wxBITMAP_TYPE_PNG_RESOURCE);
+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);
        }
-#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);
+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 */
                }
-
-               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);
-
-               vector<string> columns;
-               columns.push_back (wx_to_std (_("Address")));
-               add_label_to_sizer (table, _panel, _("CC addresses"), true);
-               _kdm_cc = new EditableList<string, EmailDialog> (
-                       _panel,
-                       columns,
-                       bind (&Config::kdm_cc, Config::instance()),
-                       bind (&Config::set_kdm_cc, Config::instance(), _1),
-                       bind (&string_not_empty, _1),
-                       bind (&column, _1)
-                       );
-               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 (-1, 200), wxTE_MULTILINE);
-               _panel->GetSizer()->Add (_kdm_email, 0, wxEXPAND | wxALL, _border);
-
-               _reset_kdm_email = new wxButton (_panel, wxID_ANY, _("Reset to default subject and text"));
-               _panel->GetSizer()->Add (_reset_kdm_email, 0, wxEXPAND | wxALL, _border);
-
-               _kdm_cc->layout ();
-
-               _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_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_bcc, config->kdm_bcc ());
-               checked_set (_kdm_email, Config::instance()->kdm_email ());
+       if (configured_so && current_so != configured_so) {
+               /* Update _sound_output with the configured value */
+               unsigned int i = 0;
+               while (i < _sound_output->GetCount()) {
+                       if (_sound_output->GetString(i) == std_to_wx(*configured_so)) {
+                               _sound_output->SetSelection (i);
+                               break;
+                       }
+                       ++i;
+               }
        }
 
-       void mail_server_changed ()
-       {
-               Config::instance()->set_mail_server (wx_to_std (_mail_server->GetValue ()));
-       }
+       RtAudio audio (DCPOMATIC_RTAUDIO_API);
 
-       void mail_port_changed ()
-       {
-               Config::instance()->set_mail_port (_mail_port->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_user_changed ()
-       {
-               Config::instance()->set_mail_user (wx_to_std (_mail_user->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_password_changed ()
-       {
-               Config::instance()->set_mail_password (wx_to_std (_mail_password->GetValue ()));
-       }
+       _sound_output_details->SetLabel (
+               wxString::Format(_("%d channels on %s"), channels, apis[audio.getCurrentApi()])
+               );
 
-       void kdm_subject_changed ()
-       {
-               Config::instance()->set_kdm_subject (wx_to_std (_kdm_subject->GetValue ()));
-       }
+       _map->set (Config::instance()->audio_mapping(channels));
 
-       void kdm_from_changed ()
-       {
-               Config::instance()->set_kdm_from (wx_to_std (_kdm_from->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_bcc_changed ()
-       {
-               Config::instance()->set_kdm_bcc (wx_to_std (_kdm_bcc->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_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 ()));
-       }
+       setup_sensitivity ();
+}
 
-       void reset_kdm_email ()
-       {
-               Config::instance()->reset_kdm_email ();
-               checked_set (_kdm_email, Config::instance()->kdm_email ());
-       }
+void
+SoundPage::setup_sensitivity ()
+{
+       _sound_output->Enable (_sound->GetValue());
+}
 
-       wxTextCtrl* _mail_server;
-       wxSpinCtrl* _mail_port;
-       wxTextCtrl* _mail_user;
-       wxTextCtrl* _mail_password;
-       wxTextCtrl* _kdm_subject;
-       wxTextCtrl* _kdm_from;
-       EditableList<string, EmailDialog>* _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
+/** @return Currently-selected preview sound output in the dialogue */
+optional<string>
+SoundPage::get_sound_output ()
 {
-public:
-       AdvancedPage (wxSize panel_size, int border)
-               : StockPage (Kind_Advanced, panel_size, border)
-               , _maximum_j2k_bandwidth (0)
-               , _allow_any_dcp_frame_rate (0)
-               , _only_servers_encode (0)
-               , _log_general (0)
-               , _log_warning (0)
-               , _log_error (0)
-               , _log_timing (0)
-               , _log_debug_decode (0)
-               , _log_debug_encode (0)
-               , _log_debug_email (0)
-       {}
-
-private:
-       void add_top_aligned_label_to_sizer (wxSizer* table, wxWindow* parent, wxString text)
-       {
-               int flags = wxALIGN_TOP | wxTOP | wxLEFT;
-#ifdef __WXOSX__
-               flags |= wxALIGN_RIGHT;
-               text += wxT (":");
-#endif
-               wxStaticText* m = new wxStaticText (parent, wxID_ANY, text);
-               table->Add (m, 0, flags, DCPOMATIC_SIZER_Y_GAP);
+       int const sel = _sound_output->GetSelection ();
+       if (sel == wxNOT_FOUND) {
+               return optional<string> ();
        }
 
-       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);
-               }
+       return wx_to_std (_sound_output->GetString (sel));
+}
 
-               _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);
-
-               _only_servers_encode = new wxCheckBox (_panel, wxID_ANY, _("Only servers encode"));
-               table->Add (_only_servers_encode, 1, wxEXPAND | wxALL);
-               table->AddSpacer (0);
-
-               {
-                       add_top_aligned_label_to_sizer (table, _panel, _("DCP metadata filename format"));
-                       dcp::NameFormat::Map titles;
-                       titles['t'] = "type (cpl/pkl)";
-                       dcp::NameFormat::Map examples;
-                       examples['t'] = "cpl";
-                       _dcp_metadata_filename_format = new NameFormatEditor (
-                               _panel, Config::instance()->dcp_metadata_filename_format(), titles, examples, "_eb1c112c-ca3c-4ae6-9263-c6714ff05d64.xml"
-                               );
-                       table->Add (_dcp_metadata_filename_format->panel(), 1, wxEXPAND | wxALL);
-               }
 
-               {
-                       add_top_aligned_label_to_sizer (table, _panel, _("DCP asset filename format"));
-                       dcp::NameFormat::Map titles;
-                       titles['t'] = "type (j2c/pcm/sub)";
-                       titles['r'] = "reel number";
-                       titles['n'] = "number of reels";
-                       titles['c'] = "content filename";
-                       dcp::NameFormat::Map examples;
-                       examples['t'] = "j2c";
-                       examples['r'] = "1";
-                       examples['n'] = "4";
-                       examples['c'] = "myfile.mp4";
-                       _dcp_asset_filename_format = new NameFormatEditor (
-                               _panel, Config::instance()->dcp_asset_filename_format(), titles, examples, "_eb1c112c-ca3c-4ae6-9263-c6714ff05d64.mxf"
-                               );
-                       table->Add (_dcp_asset_filename_format->panel(), 1, wxEXPAND | wxALL);
-               }
+LocationsPage::LocationsPage (wxSize panel_size, int border)
+       : Page (panel_size, border)
+{
 
-               {
-                       add_top_aligned_label_to_sizer (table, _panel, _("Log"));
-                       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);
-                       /// TRANSLATORS: translate the word "Timing" here; do not include the "Config|" prefix
-                       _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);
-                       _log_debug_email = new wxCheckBox (_panel, wxID_ANY, _("Debug: email sending"));
-                       t->Add (_log_debug_email, 1, wxEXPAND | wxALL);
-                       table->Add (t, 0, wxALL, 6);
-               }
+}
 
-#ifdef DCPOMATIC_WINDOWS
-               _win32_console = new wxCheckBox (_panel, wxID_ANY, _("Open console window"));
-               table->Add (_win32_console, 1, wxEXPAND | wxALL);
-               table->AddSpacer (0);
-#endif
+wxString
+LocationsPage::GetName () const
+{
+       return _("Locations");
+}
 
-               _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));
-               _only_servers_encode->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::only_servers_encode_changed, this));
-               _dcp_metadata_filename_format->Changed.connect (boost::bind (&AdvancedPage::dcp_metadata_filename_format_changed, this));
-               _dcp_asset_filename_format->Changed.connect (boost::bind (&AdvancedPage::dcp_asset_filename_format_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));
-               _log_debug_email->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));
+#ifdef DCPOMATIC_OSX
+wxBitmap
+LocationsPage::GetLargeIcon () const
+{
+       return wxBitmap(bitmap_path("locations"), wxBITMAP_TYPE_PNG);
+}
 #endif
-       }
 
-       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 (_only_servers_encode, config->only_servers_encode ());
-               checked_set (_log_general, config->log_types() & LogEntry::TYPE_GENERAL);
-               checked_set (_log_warning, config->log_types() & LogEntry::TYPE_WARNING);
-               checked_set (_log_error, config->log_types() & LogEntry::TYPE_ERROR);
-               checked_set (_log_timing, config->log_types() & LogEntry::TYPE_TIMING);
-               checked_set (_log_debug_decode, config->log_types() & LogEntry::TYPE_DEBUG_DECODE);
-               checked_set (_log_debug_encode, config->log_types() & LogEntry::TYPE_DEBUG_ENCODE);
-               checked_set (_log_debug_email, config->log_types() & LogEntry::TYPE_DEBUG_EMAIL);
-#ifdef DCPOMATIC_WINDOWS
-               checked_set (_win32_console, config->win32_console());
-#endif
-       }
+void
+LocationsPage::setup ()
+{
+       int r = 0;
 
-       void maximum_j2k_bandwidth_changed ()
-       {
-               Config::instance()->set_maximum_j2k_bandwidth (_maximum_j2k_bandwidth->GetValue() * 1000000);
-       }
+       auto table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
+       _panel->GetSizer()->Add (table, 1, wxALL | wxEXPAND, _border);
 
-       void allow_any_dcp_frame_rate_changed ()
-       {
-               Config::instance()->set_allow_any_dcp_frame_rate (_allow_any_dcp_frame_rate->GetValue ());
-       }
+       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 only_servers_encode_changed ()
-       {
-               Config::instance()->set_only_servers_encode (_only_servers_encode->GetValue ());
-       }
+       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 dcp_metadata_filename_format_changed ()
-       {
-               Config::instance()->set_dcp_metadata_filename_format (_dcp_metadata_filename_format->get ());
-       }
+       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 dcp_asset_filename_format_changed ()
-       {
-               Config::instance()->set_dcp_asset_filename_format (_dcp_asset_filename_format->get ());
-       }
+       _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 |= LogEntry::TYPE_GENERAL;
-               }
-               if (_log_warning->GetValue ()) {
-                       types |= LogEntry::TYPE_WARNING;
-               }
-               if (_log_error->GetValue ())  {
-                       types |= LogEntry::TYPE_ERROR;
-               }
-               if (_log_timing->GetValue ()) {
-                       types |= LogEntry::TYPE_TIMING;
-               }
-               if (_log_debug_decode->GetValue ()) {
-                       types |= LogEntry::TYPE_DEBUG_DECODE;
-               }
-               if (_log_debug_encode->GetValue ()) {
-                       types |= LogEntry::TYPE_DEBUG_ENCODE;
-               }
-               if (_log_debug_email->GetValue ()) {
-                       types |= LogEntry::TYPE_DEBUG_EMAIL;
-               }
-               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* _only_servers_encode;
-       NameFormatEditor* _dcp_metadata_filename_format;
-       NameFormatEditor* _dcp_asset_filename_format;
-       wxCheckBox* _log_general;
-       wxCheckBox* _log_warning;
-       wxCheckBox* _log_error;
-       wxCheckBox* _log_timing;
-       wxCheckBox* _log_debug_decode;
-       wxCheckBox* _log_debug_encode;
-       wxCheckBox* _log_debug_email;
-#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()));
 }