Remove pointless subheading in Keys->Advanced dialogues and improve spacing in the...
[dcpomatic.git] / src / wx / config_dialog.cc
index 368a94636a4515b0b4724abb7ae638ec6b2b7675..0ad7bfffaaa7555bc7d4d3f2294840ab0a8cf7fc 100644 (file)
 /*
-    Copyright (C) 2012-2015 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2019 Carl Hetherington <cth@carlh.net>
 
-    This program is free software; you can redistribute it and/or modify
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
     the Free Software Foundation; either version 2 of the License, or
     (at your option) any later version.
 
-    This program is distributed in the hope that it will be useful,
+    DCP-o-matic is distributed in the hope that it will be useful,
     but WITHOUT ANY WARRANTY; without even the implied warranty of
     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     GNU General Public License for more details.
 
     You should have received a copy of the GNU General Public License
-    along with this program; if not, write to the Free Software
-    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
 
 */
 
-/** @file src/config_dialog.cc
- *  @brief A dialogue to edit DCP-o-matic configuration.
- */
-
-#include <iostream>
-#include <boost/lexical_cast.hpp>
-#include <boost/filesystem.hpp>
-#include <wx/stdpaths.h>
-#include <wx/preferences.h>
-#include <wx/filepicker.h>
-#include <wx/spinctrl.h>
-#include <dcp/colour_matrix.h>
-#include <dcp/exceptions.h>
-#include <dcp/signer.h>
-#include "lib/config.h"
-#include "lib/ratio.h"
-#include "lib/scaler.h"
-#include "lib/filter.h"
-#include "lib/dcp_content_type.h"
-#include "lib/colour_conversion.h"
-#include "lib/log.h"
-#include "lib/util.h"
-#include "lib/cross.h"
-#include "lib/exceptions.h"
 #include "config_dialog.h"
-#include "wx_util.h"
-#include "editable_list.h"
-#include "filter_dialog.h"
-#include "dir_picker_ctrl.h"
-#include "isdcf_metadata_dialog.h"
-#include "preset_colour_conversion_dialog.h"
-#include "server_dialog.h"
-#include "make_signer_chain_dialog.h"
+#include "static_text.h"
+#include "check_box.h"
+#include "nag_dialog.h"
+#include "dcpomatic_button.h"
+#include "audio_mapping_view.h"
+#include <dcp/raw_convert.h>
+#include <iostream>
 
-using std::vector;
 using std::string;
-using std::list;
-using std::cout;
+using std::vector;
+using std::pair;
+using std::make_pair;
+using std::map;
 using boost::bind;
+using boost::optional;
 using boost::shared_ptr;
-using boost::lexical_cast;
+using boost::function;
 
-class Page
+static
+bool
+do_nothing ()
 {
-public:
-       Page (wxSize panel_size, int border)
-               : _panel_size (panel_size)
-               , _border (border)
-       {}
-
-protected:
-       wxPanel* make_panel (wxWindow* parent)
-       {
-               wxPanel* panel = new wxPanel (parent, wxID_ANY, wxDefaultPosition, _panel_size);
-               wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
-               panel->SetSizer (s);
-               return panel;
-       }
-       
-       wxSize _panel_size;
-       int _border;
-};
+       return false;
+}
 
-class GeneralPage : public wxStockPreferencesPage, public Page
+Page::Page (wxSize panel_size, int border)
+       : _border (border)
+       , _panel (0)
+       , _panel_size (panel_size)
+       , _window_exists (false)
 {
-public:
-       GeneralPage (wxSize panel_size, int border)
-               : wxStockPreferencesPage (Kind_General)
-               , Page (panel_size, border)
-               , _set_language (0)
-               , _language (0)
-               , _num_local_encoding_threads (0)
-               , _check_for_updates (0)
-               , _check_for_test_updates (0)
-       {}
-
-       wxWindow* CreateWindow (wxWindow* parent)
-       {
-               wxPanel* panel = make_panel (parent);
-
-               wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
-               table->AddGrowableCol (1, 1);
-               panel->GetSizer()->Add (table, 1, wxALL | wxEXPAND, _border);
-               
-               _set_language = new wxCheckBox (panel, wxID_ANY, _("Set language"));
-               table->Add (_set_language, 1);
-               _language = new wxChoice (panel, wxID_ANY);
-               _language->Append (wxT ("Deutsch"));
-               _language->Append (wxT ("English"));
-               _language->Append (wxT ("Español"));
-               _language->Append (wxT ("Français"));
-               _language->Append (wxT ("Italiano"));
-               _language->Append (wxT ("Nederlands"));
-               _language->Append (wxT ("Svenska"));
-               table->Add (_language);
-               
-               wxStaticText* restart = add_label_to_sizer (table, panel, _("(restart DCP-o-matic to see language changes)"), false);
-               wxFont font = restart->GetFont();
-               font.SetStyle (wxFONTSTYLE_ITALIC);
-               font.SetPointSize (font.GetPointSize() - 1);
-               restart->SetFont (font);
-               table->AddSpacer (0);
-               
-               add_label_to_sizer (table, panel, _("Threads to use for encoding on this host"), true);
-               _num_local_encoding_threads = new wxSpinCtrl (panel);
-               table->Add (_num_local_encoding_threads, 1);
-
-               _check_for_updates = new wxCheckBox (panel, wxID_ANY, _("Check for updates on startup"));
-               table->Add (_check_for_updates, 1, wxEXPAND | wxALL);
-               table->AddSpacer (0);
-               
-               _check_for_test_updates = new wxCheckBox (panel, wxID_ANY, _("Check for testing updates as well as stable ones"));
-               table->Add (_check_for_test_updates, 1, wxEXPAND | wxALL);
-               table->AddSpacer (0);
-               
-               Config* config = Config::instance ();
-               
-               _set_language->SetValue (config->language ());
-               
-               if (config->language().get_value_or ("") == "fr") {
-                       _language->SetSelection (3);
-               } else if (config->language().get_value_or ("") == "it") {
-                       _language->SetSelection (4);
-               } else if (config->language().get_value_or ("") == "es") {
-                       _language->SetSelection (2);
-               } else if (config->language().get_value_or ("") == "sv") {
-                       _language->SetSelection (6);
-               } else if (config->language().get_value_or ("") == "de") {
-                       _language->SetSelection (0);
-               } else if (config->language().get_value_or ("") == "nl") {
-                       _language->SetSelection (5);
-               } else {
-                       _language->SetSelection (1);
-               }
-               
-               setup_language_sensitivity ();
-               
-               _set_language->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&GeneralPage::set_language_changed, this));
-               _language->Bind     (wxEVT_COMMAND_CHOICE_SELECTED,  boost::bind (&GeneralPage::language_changed,     this));
-               
-               _num_local_encoding_threads->SetRange (1, 128);
-               _num_local_encoding_threads->SetValue (config->num_local_encoding_threads ());
-               _num_local_encoding_threads->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&GeneralPage::num_local_encoding_threads_changed, this));
-
-               _check_for_updates->SetValue (config->check_for_updates ());
-               _check_for_updates->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&GeneralPage::check_for_updates_changed, this));
-               _check_for_test_updates->SetValue (config->check_for_test_updates ());
-               _check_for_test_updates->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&GeneralPage::check_for_test_updates_changed, this));
-               
-               return panel;
-       }
+       _config_connection = Config::instance()->Changed.connect (bind (&Page::config_changed_wrapper, this));
+}
 
-private:       
-       void setup_language_sensitivity ()
-       {
-               _language->Enable (_set_language->GetValue ());
-       }
+wxWindow*
+Page::create_window (wxWindow* parent)
+{
+       _panel = new wxPanel (parent, wxID_ANY, wxDefaultPosition, _panel_size);
+       wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
+       _panel->SetSizer (s);
 
-       void set_language_changed ()
-       {
-               setup_language_sensitivity ();
-               if (_set_language->GetValue ()) {
-                       language_changed ();
-               } else {
-                       Config::instance()->unset_language ();
-               }
-       }
+       setup ();
+       _window_exists = true;
+       config_changed ();
 
-       void language_changed ()
-       {
-               switch (_language->GetSelection ()) {
-               case 0:
-                       Config::instance()->set_language ("de");
-                       break;
-               case 1:
-                       Config::instance()->set_language ("en");
-                       break;
-               case 2:
-                       Config::instance()->set_language ("es");
-                       break;
-               case 3:
-                       Config::instance()->set_language ("fr");
-                       break;
-               case 4:
-                       Config::instance()->set_language ("it");
-                       break;
-               case 5:
-                       Config::instance()->set_language ("nl");
-                       break;
-               case 6:
-                       Config::instance()->set_language ("sv");
-                       break;
-               }
-       }
-       
-       void check_for_updates_changed ()
-       {
-               Config::instance()->set_check_for_updates (_check_for_updates->GetValue ());
-       }
-       
-       void check_for_test_updates_changed ()
-       {
-               Config::instance()->set_check_for_test_updates (_check_for_test_updates->GetValue ());
-       }
+       _panel->Bind (wxEVT_DESTROY, bind (&Page::window_destroyed, this));
 
-       void num_local_encoding_threads_changed ()
-       {
-               Config::instance()->set_num_local_encoding_threads (_num_local_encoding_threads->GetValue ());
+       return _panel;
+}
+
+void
+Page::config_changed_wrapper ()
+{
+       if (_window_exists) {
+               config_changed ();
        }
+}
 
-       wxCheckBox* _set_language;
-       wxChoice* _language;
-       wxSpinCtrl* _num_local_encoding_threads;
-       wxCheckBox* _check_for_updates;
-       wxCheckBox* _check_for_test_updates;
-};
+void
+Page::window_destroyed ()
+{
+       _window_exists = false;
+}
 
-class DefaultsPage : public wxPreferencesPage, public Page
+
+StockPage::StockPage (Kind kind, wxSize panel_size, int border)
+       : wxStockPreferencesPage (kind)
+       , Page (panel_size, border)
 {
-public:
-       DefaultsPage (wxSize panel_size, int border)
-               : Page (panel_size, border)
-               , _j2k_bandwidth (0)
-               , _audio_delay (0)
-               , _isdcf_metadata_button (0)
-               , _still_length (0)
-               , _directory (0)
-               , _container (0)
-               , _dcp_content_type (0)
-               , _issuer (0)
-       {}
-       
-       wxString GetName () const
-       {
-               return _("Defaults");
-       }
 
-#ifdef DCPOMATIC_OSX   
-       wxBitmap GetLargeIcon () const
-       {
-               return wxBitmap ("defaults", wxBITMAP_TYPE_PNG_RESOURCE);
-       }
-#endif 
+}
 
-       wxWindow* CreateWindow (wxWindow* parent)
-       {
-               wxPanel* panel = make_panel (parent);
-
-               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);
-               }
-               
-               add_label_to_sizer (table, panel, _("Default directory for new films"), true);
-#ifdef DCPOMATIC_USE_OWN_DIR_PICKER
-               _directory = new DirPickerCtrl (panel);
-#else  
-               _directory = new wxDirPickerCtrl (panel, wxDD_DIR_MUST_EXIST);
-#endif
-               table->Add (_directory, 1, wxEXPAND);
-               
-               add_label_to_sizer (table, panel, _("Default ISDCF name details"), true);
-               _isdcf_metadata_button = new wxButton (panel, wxID_ANY, _("Edit..."));
-               table->Add (_isdcf_metadata_button);
-
-               add_label_to_sizer (table, panel, _("Default container"), true);
-               _container = new wxChoice (panel, wxID_ANY);
-               table->Add (_container);
-               
-               add_label_to_sizer (table, panel, _("Default content type"), true);
-               _dcp_content_type = new wxChoice (panel, wxID_ANY);
-               table->Add (_dcp_content_type);
-               
-               {
-                       add_label_to_sizer (table, panel, _("Default JPEG2000 bandwidth"), true);
-                       wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-                       _j2k_bandwidth = new wxSpinCtrl (panel);
-                       s->Add (_j2k_bandwidth);
-                       add_label_to_sizer (s, panel, _("Mbit/s"), false);
-                       table->Add (s, 1);
-               }
-               
-               {
-                       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);
-               }
+wxWindow*
+StockPage::CreateWindow (wxWindow* parent)
+{
+       return create_window (parent);
+}
 
-               add_label_to_sizer (table, panel, _("Default issuer"), true);
-               _issuer = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_issuer, 1, wxEXPAND);
-
-               Config* config = Config::instance ();
-               
-               _still_length->SetRange (1, 3600);
-               _still_length->SetValue (config->default_still_length ());
-               _still_length->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&DefaultsPage::still_length_changed, this));
-               
-               _directory->SetPath (std_to_wx (config->default_directory_or (wx_to_std (wxStandardPaths::Get().GetDocumentsDir())).string ()));
-               _directory->Bind (wxEVT_COMMAND_DIRPICKER_CHANGED, boost::bind (&DefaultsPage::directory_changed, this));
-               
-               _isdcf_metadata_button->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&DefaultsPage::edit_isdcf_metadata_clicked, this, parent));
-               
-               vector<Ratio const *> ratios = Ratio::all ();
-               for (size_t i = 0; i < ratios.size(); ++i) {
-                       _container->Append (std_to_wx (ratios[i]->nickname ()));
-                       if (ratios[i] == config->default_container ()) {
-                               _container->SetSelection (i);
-                       }
-               }
-               
-               _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 ()));
-                       if (ct[i] == config->default_dcp_content_type ()) {
-                               _dcp_content_type->SetSelection (i);
-                       }
-               }
-               
-               _dcp_content_type->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&DefaultsPage::dcp_content_type_changed, this));
-               
-               _j2k_bandwidth->SetRange (50, 250);
-               _j2k_bandwidth->SetValue (config->default_j2k_bandwidth() / 1000000);
-               _j2k_bandwidth->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&DefaultsPage::j2k_bandwidth_changed, this));
-               
-               _audio_delay->SetRange (-1000, 1000);
-               _audio_delay->SetValue (config->default_audio_delay ());
-               _audio_delay->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&DefaultsPage::audio_delay_changed, this));
-
-               _issuer->SetValue (std_to_wx (config->dcp_issuer ()));
-               _issuer->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&DefaultsPage::issuer_changed, this));
-
-               config->Changed.connect (boost::bind (&DefaultsPage::config_changed, this));
-
-               return panel;
-       }
+StandardPage::StandardPage (wxSize panel_size, int border)
+       : Page (panel_size, border)
+{
 
-private:
-       void config_changed ()
-       {
-               _j2k_bandwidth->SetRange (50, Config::instance()->maximum_j2k_bandwidth() / 1000000);
-       }
-               
-       void j2k_bandwidth_changed ()
-       {
-               Config::instance()->set_default_j2k_bandwidth (_j2k_bandwidth->GetValue() * 1000000);
-       }
-       
-       void audio_delay_changed ()
-       {
-               Config::instance()->set_default_audio_delay (_audio_delay->GetValue());
-       }
+}
 
-       void directory_changed ()
-       {
-               Config::instance()->set_default_directory (wx_to_std (_directory->GetPath ()));
-       }
+wxWindow*
+StandardPage::CreateWindow (wxWindow* parent)
+{
+       return create_window (parent);
+}
 
-       void edit_isdcf_metadata_clicked (wxWindow* parent)
-       {
-               ISDCFMetadataDialog* d = new ISDCFMetadataDialog (parent, Config::instance()->default_isdcf_metadata ());
-               d->ShowModal ();
-               Config::instance()->set_default_isdcf_metadata (d->isdcf_metadata ());
-               d->Destroy ();
-       }
+GeneralPage::GeneralPage (wxSize panel_size, int border)
+       : StockPage (Kind_General, panel_size, border)
+{
 
-       void still_length_changed ()
-       {
-               Config::instance()->set_default_still_length (_still_length->GetValue ());
-       }
+}
 
-       void container_changed ()
-       {
-               vector<Ratio const *> ratio = Ratio::all ();
-               Config::instance()->set_default_container (ratio[_container->GetSelection()]);
-       }
-       
-       void dcp_content_type_changed ()
-       {
-               vector<DCPContentType const *> ct = DCPContentType::all ();
-               Config::instance()->set_default_dcp_content_type (ct[_dcp_content_type->GetSelection()]);
+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;
+
+       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;
+
+       _set_language->Bind (wxEVT_CHECKBOX, bind (&GeneralPage::set_language_changed, this));
+       _language->Bind     (wxEVT_CHOICE,   bind (&GeneralPage::language_changed,     this));
+}
+
+void
+GeneralPage::add_update_controls (wxGridBagSizer* table, int& r)
+{
+       _check_for_updates = new CheckBox (_panel, _("Check for updates on startup"));
+       table->Add (_check_for_updates, wxGBPosition (r, 0), wxGBSpan (1, 2));
+       ++r;
+
+       _check_for_test_updates = new CheckBox (_panel, _("Check for testing updates on startup"));
+       table->Add (_check_for_test_updates, wxGBPosition (r, 0), wxGBSpan (1, 2));
+       ++r;
+
+       _check_for_updates->Bind (wxEVT_CHECKBOX, bind (&GeneralPage::check_for_updates_changed, this));
+       _check_for_test_updates->Bind (wxEVT_CHECKBOX, bind (&GeneralPage::check_for_test_updates_changed, this));
+}
+
+void
+GeneralPage::config_changed ()
+{
+       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];
        }
 
-       void issuer_changed ()
-       {
-               Config::instance()->set_dcp_issuer (wx_to_std (_issuer->GetValue ()));
+       checked_set (_language, lang);
+
+       checked_set (_check_for_updates, config->check_for_updates ());
+       checked_set (_check_for_test_updates, config->check_for_test_updates ());
+
+       setup_sensitivity ();
+}
+
+void
+GeneralPage::setup_sensitivity ()
+{
+       _language->Enable (_set_language->GetValue ());
+       _check_for_test_updates->Enable (_check_for_updates->GetValue ());
+}
+
+void
+GeneralPage::set_language_changed ()
+{
+       setup_sensitivity ();
+       if (_set_language->GetValue ()) {
+               language_changed ();
+       } else {
+               Config::instance()->unset_language ();
        }
-       
-       wxSpinCtrl* _j2k_bandwidth;
-       wxSpinCtrl* _audio_delay;
-       wxButton* _isdcf_metadata_button;
-       wxSpinCtrl* _still_length;
-#ifdef DCPOMATIC_USE_OWN_DIR_PICKER
-       DirPickerCtrl* _directory;
-#else
-       wxDirPickerCtrl* _directory;
-#endif
-       wxChoice* _container;
-       wxChoice* _dcp_content_type;
-       wxTextCtrl* _issuer;
-};
+}
 
-class EncodingServersPage : public wxPreferencesPage, public Page
+void
+GeneralPage::language_changed ()
 {
-public:
-       EncodingServersPage (wxSize panel_size, int border)
-               : Page (panel_size, border)
-               , _use_any_servers (0)
-               , _servers_list (0)
-       {}
-       
-       wxString GetName () const
-       {
-               return _("Servers");
+       int const sel = _language->GetSelection ();
+       if (sel != -1) {
+               Config::instance()->set_language (string_client_data (_language->GetClientObject (sel)));
+       } else {
+               Config::instance()->unset_language ();
        }
+}
+
+void
+GeneralPage::check_for_updates_changed ()
+{
+       Config::instance()->set_check_for_updates (_check_for_updates->GetValue ());
+}
+
+void
+GeneralPage::check_for_test_updates_changed ()
+{
+       Config::instance()->set_check_for_test_updates (_check_for_test_updates->GetValue ());
+}
+
+CertificateChainEditor::CertificateChainEditor (
+       wxWindow* parent,
+       wxString title,
+       int border,
+       function<void (shared_ptr<dcp::CertificateChain>)> set,
+       function<shared_ptr<const dcp::CertificateChain> (void)> get,
+       function<bool (void)> nag_alter
+       )
+       : wxDialog (parent, wxID_ANY, title)
+       , _set (set)
+       , _get (get)
+       , _nag_alter (nag_alter)
+{
+       _sizer = new wxBoxSizer (wxVERTICAL);
+
+       wxBoxSizer* 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 ("servers", wxBITMAP_TYPE_PNG_RESOURCE);
+               wxListItem ip;
+               ip.SetId (0);
+               ip.SetText (_("Type"));
+               ip.SetWidth (100);
+               _certificates->InsertColumn (0, ip);
        }
-#endif 
 
-       wxWindow* CreateWindow (wxWindow* parent)
        {
-               wxPanel* panel = make_panel (parent);
-               
-               _use_any_servers = new wxCheckBox (panel, wxID_ANY, _("Use all servers"));
-               panel->GetSizer()->Add (_use_any_servers, 0, wxALL, _border);
-               
-               vector<string> columns;
-               columns.push_back (wx_to_std (_("IP address / host name")));
-               _servers_list = new EditableList<string, ServerDialog> (
-                       panel,
-                       columns,
-                       boost::bind (&Config::servers, Config::instance()),
-                       boost::bind (&Config::set_servers, Config::instance(), _1),
-                       boost::bind (&EncodingServersPage::server_column, this, _1)
-                       );
-               
-               panel->GetSizer()->Add (_servers_list, 1, wxEXPAND | wxALL, _border);
-               
-               _use_any_servers->SetValue (Config::instance()->use_any_servers ());
-               _use_any_servers->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&EncodingServersPage::use_any_servers_changed, this));
+               wxListItem ip;
+               ip.SetId (1);
+               ip.SetText (_("Thumbprint"));
+               ip.SetWidth (340);
+
+               wxFont font = ip.GetFont ();
+               font.SetFamily (wxFONTFAMILY_TELETYPE);
+               ip.SetFont (font);
 
-               return panel;
+               _certificates->InsertColumn (1, ip);
        }
 
-private:       
+       certificates_sizer->Add (_certificates, 1, wxEXPAND);
 
-       void use_any_servers_changed ()
        {
-               Config::instance()->set_use_any_servers (_use_any_servers->GetValue ());
+               wxSizer* 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);
        }
 
-       string server_column (string s)
-       {
-               return s;
+       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 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));
+
+       wxSizer* buttons = CreateSeparatedButtonSizer (wxCLOSE);
+       if (buttons) {
+               _sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
        }
 
-       wxCheckBox* _use_any_servers;
-       EditableList<string, ServerDialog>* _servers_list;
-};
+       SetSizerAndFit (_sizer);
+
+       update_certificate_list ();
+       update_private_key ();
+       update_sensitivity ();
+}
+
+void
+CertificateChainEditor::add_button (wxWindow* button)
+{
+       _button_sizer->Add (button, 0, wxLEFT | wxRIGHT, DCPOMATIC_SIZER_X_GAP);
+       _sizer->Layout ();
+}
 
-class ColourConversionsPage : public wxPreferencesPage, public Page
+void
+CertificateChainEditor::add_certificate ()
 {
-public:
-       ColourConversionsPage (wxSize panel_size, int border)
-               : Page (panel_size, border)
-       {}
-       
-       wxString GetName () const
-       {
-               return _("Colour Conversions");
-       }
+       wxFileDialog* d = new wxFileDialog (this, _("Select Certificate File"));
 
-#ifdef DCPOMATIC_OSX   
-       wxBitmap GetLargeIcon () const
-       {
-               return wxBitmap ("colour_conversions", wxBITMAP_TYPE_PNG_RESOURCE);
+       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;
+                       }
+
+                       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.")
+                                       );
+                       }
+                       shared_ptr<dcp::CertificateChain> chain(new 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()));
+               }
        }
-#endif 
-       wxWindow* CreateWindow (wxWindow* parent)
-       {
-               wxPanel* panel = make_panel (parent);
-
-               vector<string> columns;
-               columns.push_back (wx_to_std (_("Name")));
-               wxPanel* list = new EditableList<PresetColourConversion, PresetColourConversionDialog> (
-                       panel,
-                       columns,
-                       boost::bind (&Config::colour_conversions, Config::instance()),
-                       boost::bind (&Config::set_colour_conversions, Config::instance(), _1),
-                       boost::bind (&ColourConversionsPage::colour_conversion_column, this, _1),
-                       300
-                       );
 
-               panel->GetSizer()->Add (list, 1, wxEXPAND | wxALL, _border);
-               return panel;
+       d->Destroy ();
+
+       update_sensitivity ();
+}
+
+void
+CertificateChainEditor::remove_certificate ()
+{
+       if (_nag_alter()) {
+               /* Cancel was clicked */
+               return;
        }
 
-private:
-       string colour_conversion_column (PresetColourConversion c)
-       {
-               return c.name;
+       int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
+       if (i == -1) {
+               return;
        }
-};
 
-class KeysPage : public wxPreferencesPage, public Page
+       _certificates->DeleteItem (i);
+       shared_ptr<dcp::CertificateChain> chain(new dcp::CertificateChain(*_get().get()));
+       chain->remove (i);
+       _set (chain);
+
+       update_sensitivity ();
+       update_certificate_list ();
+}
+
+void
+CertificateChainEditor::export_certificate ()
 {
-public:
-       KeysPage (wxSize panel_size, int border)
-               : Page (panel_size, border)
-               , _panel (0)
-               , _certificates (0)
-               , _add_certificate (0)
-               , _remove_certificate (0)
-               , _remake_certificates (0)
-               , _signer_private_key (0)
-               , _load_signer_private_key (0)
-               , _decryption_certificate (0)
-               , _load_decryption_certificate (0)
-               , _decryption_private_key (0)
-               , _load_decryption_private_key (0)
-               , _export_decryption_certificate (0)
-       {}
-
-       wxString GetName () const
-       {
-               return _("Keys");
+       int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
+       if (i == -1) {
+               return;
        }
 
-#ifdef DCPOMATIC_OSX
-       wxBitmap GetLargeIcon () const
-       {
-               return wxBitmap ("keys", wxBITMAP_TYPE_PNG_RESOURCE);
-       }
-#endif 
+       wxFileDialog* d = new wxFileDialog (
+               this, _("Select Certificate File"), wxEmptyString, wxEmptyString, wxT ("PEM files (*.pem)|*.pem"),
+               wxFD_SAVE | wxFD_OVERWRITE_PROMPT
+               );
 
-       wxWindow* CreateWindow (wxWindow* parent)
-       {
-               _panel = new wxPanel (parent, wxID_ANY, wxDefaultPosition, _panel_size);
-               wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL);
-               _panel->SetSizer (overall_sizer);
-
-               wxStaticText* m = new wxStaticText (_panel, wxID_ANY, _("Certificate chain for signing DCPs and KDMs:"));
-               overall_sizer->Add (m, 0, wxALL, _border);
-               
-               wxBoxSizer* certificates_sizer = new wxBoxSizer (wxHORIZONTAL);
-               overall_sizer->Add (certificates_sizer, 0, wxLEFT | wxRIGHT, _border);
-               
-               _certificates = new wxListCtrl (_panel, wxID_ANY, wxDefaultPosition, wxSize (400, 200), wxLC_REPORT | wxLC_SINGLE_SEL);
-
-               {
-                       wxListItem ip;
-                       ip.SetId (0);
-                       ip.SetText (_("Type"));
-                       ip.SetWidth (100);
-                       _certificates->InsertColumn (0, ip);
-               }
+       dcp::CertificateChain::List all = _get()->root_to_leaf ();
+       dcp::CertificateChain::List::iterator j = all.begin ();
+       for (int k = 0; k < i; ++k) {
+               ++j;
+       }
 
-               {
-                       wxListItem ip;
-                       ip.SetId (1);
-                       ip.SetText (_("Thumbprint"));
-                       ip.SetWidth (300);
-
-                       wxFont font = ip.GetFont ();
-                       font.SetFamily (wxFONTFAMILY_TELETYPE);
-                       ip.SetFont (font);
-                       
-                       _certificates->InsertColumn (1, ip);
+       if (d->ShowModal () == wxID_OK) {
+               boost::filesystem::path path (wx_to_std(d->GetPath()));
+               FILE* f = fopen_boost (path, "w");
+               if (!f) {
+                       throw OpenFileError (path, errno, OpenFileError::WRITE);
                }
 
-               certificates_sizer->Add (_certificates, 1, wxEXPAND);
+               string const s = j->certificate (true);
+               checked_fwrite (s.c_str(), s.length(), f, path);
+               fclose (f);
+       }
+       d->Destroy ();
+}
 
-               {
-                       wxSizer* s = new wxBoxSizer (wxVERTICAL);
-                       _add_certificate = new wxButton (_panel, wxID_ANY, _("Add..."));
-                       s->Add (_add_certificate, 0, wxTOP | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
-                       _remove_certificate = new wxButton (_panel, wxID_ANY, _("Remove"));
-                       s->Add (_remove_certificate, 0, wxTOP | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
-                       certificates_sizer->Add (s, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP);
+void
+CertificateChainEditor::export_chain ()
+{
+       wxFileDialog* d = new wxFileDialog (
+               this, _("Select Chain File"), wxEmptyString, wxEmptyString, wxT("PEM files (*.pem)|*.pem"),
+               wxFD_SAVE | wxFD_OVERWRITE_PROMPT
+               );
+
+       if (d->ShowModal () == wxID_OK) {
+               boost::filesystem::path path (wx_to_std(d->GetPath()));
+               FILE* f = fopen_boost (path, "w");
+               if (!f) {
+                       throw OpenFileError (path, errno, OpenFileError::WRITE);
                }
 
-               wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
-               table->AddGrowableCol (1, 1);
-               overall_sizer->Add (table, 1, wxALL | wxEXPAND, _border);
-
-               _remake_certificates = new wxButton (_panel, wxID_ANY, _("Re-make certificates..."));
-               table->Add (_remake_certificates, 0);
-               table->AddSpacer (0);
-
-               add_label_to_sizer (table, _panel, _("Private key for leaf certificate"), true);
-               {
-                       wxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-                       _signer_private_key = new wxStaticText (_panel, wxID_ANY, wxT (""));
-                       wxFont font = _signer_private_key->GetFont ();
-                       font.SetFamily (wxFONTFAMILY_TELETYPE);
-                       _signer_private_key->SetFont (font);
-                       s->Add (_signer_private_key, 1, wxLEFT | wxRIGHT | wxALIGN_CENTER_VERTICAL, DCPOMATIC_SIZER_X_GAP);
-                       _load_signer_private_key = new wxButton (_panel, wxID_ANY, _("Load..."));
-                       s->Add (_load_signer_private_key, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP);
-                       table->Add (s, 0);
-               }
+               string const s = _get()->chain();
+               checked_fwrite (s.c_str(), s.length(), f, path);
+               fclose (f);
+       }
 
-               add_label_to_sizer (table, _panel, _("Certificate for decrypting DCPs"), true);
-               {
-                       wxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-                       _decryption_certificate = new wxStaticText (_panel, wxID_ANY, wxT (""));
-                       wxFont font = _decryption_certificate->GetFont ();
-                       font.SetFamily (wxFONTFAMILY_TELETYPE);
-                       _decryption_certificate->SetFont (font);
-                       s->Add (_decryption_certificate, 1, wxLEFT | wxRIGHT | wxALIGN_CENTER_VERTICAL, DCPOMATIC_SIZER_X_GAP);
-                       _load_decryption_certificate = new wxButton (_panel, wxID_ANY, _("Load..."));
-                       s->Add (_load_decryption_certificate, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP);
-                       table->Add (s, 0);
-               }
+       d->Destroy ();
+}
 
-               add_label_to_sizer (table, _panel, _("Private key for decrypting DCPs"), true);
-               {
-                       wxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-                       _decryption_private_key = new wxStaticText (_panel, wxID_ANY, wxT (""));
-                       wxFont font = _decryption_private_key->GetFont ();
-                       font.SetFamily (wxFONTFAMILY_TELETYPE);
-                       _decryption_private_key->SetFont (font);
-                       s->Add (_decryption_private_key, 1, wxLEFT | wxRIGHT | wxALIGN_CENTER_VERTICAL, DCPOMATIC_SIZER_X_GAP);
-                       _load_decryption_private_key = new wxButton (_panel, wxID_ANY, _("Load..."));
-                       s->Add (_load_decryption_private_key, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP);
-                       table->Add (s, 0);
+void
+CertificateChainEditor::update_certificate_list ()
+{
+       _certificates->DeleteAllItems ();
+       size_t n = 0;
+       dcp::CertificateChain::List certs = _get()->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"));
                }
 
-               _export_decryption_certificate = new wxButton (_panel, wxID_ANY, _("Export DCP decryption certificate..."));
-               table->Add (_export_decryption_certificate);
-               table->AddSpacer (0);
-               
-               _add_certificate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::add_certificate, this));
-               _remove_certificate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::remove_certificate, this));
-               _certificates->Bind (wxEVT_COMMAND_LIST_ITEM_SELECTED, boost::bind (&KeysPage::update_sensitivity, this));
-               _certificates->Bind (wxEVT_COMMAND_LIST_ITEM_DESELECTED, boost::bind (&KeysPage::update_sensitivity, this));
-               _remake_certificates->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::remake_certificates, this));
-               _load_signer_private_key->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::load_signer_private_key, this));
-               _load_decryption_certificate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::load_decryption_certificate, this));
-               _load_decryption_private_key->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::load_decryption_private_key, this));
-               _export_decryption_certificate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::export_decryption_certificate, this));
-
-               _signer.reset (new dcp::Signer (*Config::instance()->signer().get ()));
+               ++n;
+       }
 
-               update_certificate_list ();
-               update_signer_private_key ();
-               update_decryption_certificate ();
-               update_decryption_private_key ();
-               update_sensitivity ();
+       static wxColour normal = _private_key_bad->GetForegroundColour ();
 
-               return _panel;
+       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));
        }
+}
 
-private:
-       void add_certificate ()
-       {
-               wxFileDialog* d = new wxFileDialog (_panel, _("Select Certificate File"));
-               
-               if (d->ShowModal() == wxID_OK) {
-                       try {
-                               dcp::Certificate c (dcp::file_to_string (wx_to_std (d->GetPath ())));
-                               _signer->certificates().add (c);
-                               Config::instance()->set_signer (_signer);
-                               update_certificate_list ();
-                       } catch (dcp::MiscError& e) {
-                               error_dialog (_panel, wxString::Format (_("Could not read certificate file (%s)"), e.what ()));
-                       }
-               }
-               
-               d->Destroy ();
+void
+CertificateChainEditor::remake_certificates ()
+{
+       shared_ptr<const dcp::CertificateChain> chain = _get();
 
-               update_sensitivity ();
-       }
+       string subject_organization_name;
+       string subject_organizational_unit_name;
+       string root_common_name;
+       string intermediate_common_name;
+       string leaf_common_name;
 
-       void remove_certificate ()
-       {
-               int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
-               if (i == -1) {
-                       return;
-               }
-               
-               _certificates->DeleteItem (i);
-               _signer->certificates().remove (i);
-               Config::instance()->set_signer (_signer);
+       dcp::CertificateChain::List all = chain->root_to_leaf ();
 
-               update_sensitivity ();
+       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 ();
        }
 
-       void update_certificate_list ()
-       {
-               _certificates->DeleteAllItems ();
-               dcp::CertificateChain::List certs = _signer->certificates().root_to_leaf ();
-               size_t n = 0;
-               for (dcp::CertificateChain::List::const_iterator i = certs.begin(); i != certs.end(); ++i) {
-                       wxListItem item;
-                       item.SetId (n);
-                       _certificates->InsertItem (item);
-                       _certificates->SetItem (n, 1, std_to_wx (i->thumbprint ()));
-
-                       if (n == 0) {
-                               _certificates->SetItem (n, 0, _("Root"));
-                       } else if (n == (certs.size() - 1)) {
-                               _certificates->SetItem (n, 0, _("Leaf"));
-                       } else {
-                               _certificates->SetItem (n, 0, _("Intermediate"));
-                       }
+       if (all.size() >= 2) {
+               /* Have a leaf */
+               leaf_common_name = chain->leaf().subject_common_name ();
+       }
 
-                       ++n;
-               }
+       if (all.size() >= 3) {
+               /* Have an intermediate */
+               dcp::CertificateChain::List::iterator i = all.begin ();
+               ++i;
+               intermediate_common_name = i->subject_common_name ();
        }
 
-       void remake_certificates ()
-       {
-               MakeSignerChainDialog* d = new MakeSignerChainDialog (_panel);
-               if (d->ShowModal () == wxID_OK) {
-                       _signer.reset (
-                               new dcp::Signer (
+       if (_nag_alter()) {
+               /* Cancel was clicked */
+               return;
+       }
+
+       MakeChainDialog* 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 (
+                       shared_ptr<dcp::CertificateChain> (
+                               new dcp::CertificateChain (
                                        openssl_path (),
                                        d->organisation (),
                                        d->organisational_unit (),
@@ -758,545 +579,564 @@ private:
                                        d->intermediate_common_name (),
                                        d->leaf_common_name ()
                                        )
-                               );
+                               )
+                       );
 
-                       Config::instance()->set_signer (_signer);
-                       update_certificate_list ();
-                       update_signer_private_key ();
-               }
-               
-               d->Destroy ();
+               update_certificate_list ();
+               update_private_key ();
        }
 
-       void update_sensitivity ()
-       {
-               _remove_certificate->Enable (_certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED) != -1);
-       }
+       d->Destroy ();
+}
 
-       void update_signer_private_key ()
-       {
-               _signer_private_key->SetLabel (std_to_wx (dcp::private_key_fingerprint (_signer->key ())));
-       }       
+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 load_signer_private_key ()
-       {
-               wxFileDialog* d = new wxFileDialog (_panel, _("Select Key File"));
+void
+CertificateChainEditor::update_private_key ()
+{
+       checked_set (_private_key, dcp::private_key_fingerprint (_get()->key().get()));
+       _sizer->Layout ();
+}
 
-               if (d->ShowModal() == wxID_OK) {
-                       try {
-                               boost::filesystem::path p (wx_to_std (d->GetPath ()));
-                               if (boost::filesystem::file_size (p) > 1024) {
-                                       error_dialog (_panel, wxString::Format (_("Could not read key file (%s)"), std_to_wx (p.string ())));
-                                       return;
-                               }
-                               
-                               _signer->set_key (dcp::file_to_string (p));
-                               Config::instance()->set_signer (_signer);
-                               update_signer_private_key ();
-                       } catch (dcp::MiscError& e) {
-                               error_dialog (_panel, wxString::Format (_("Could not read certificate file (%s)"), e.what ()));
+void
+CertificateChainEditor::import_private_key ()
+{
+       wxFileDialog* 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;
                        }
+
+                       shared_ptr<dcp::CertificateChain> chain(new dcp::CertificateChain(*_get().get()));
+                       chain->set_key (dcp::file_to_string (p));
+                       _set (chain);
+                       update_private_key ();
+               } catch (dcp::MiscError& e) {
+                       error_dialog (this, _("Could not read certificate file."), std_to_wx(e.what()));
                }
-               
-               d->Destroy ();
+       }
 
-               update_sensitivity ();
+       d->Destroy ();
 
+       update_sensitivity ();
+}
+
+void
+CertificateChainEditor::export_private_key ()
+{
+       optional<string> key = _get()->key();
+       if (!key) {
+               return;
        }
 
-       void load_decryption_certificate ()
-       {
-               wxFileDialog* d = new wxFileDialog (_panel, _("Select Certificate File"));
-               
-               if (d->ShowModal() == wxID_OK) {
-                       try {
-                               dcp::Certificate c (dcp::file_to_string (wx_to_std (d->GetPath ())));
-                               Config::instance()->set_decryption_certificate (c);
-                               update_decryption_certificate ();
-                       } catch (dcp::MiscError& e) {
-                               error_dialog (_panel, wxString::Format (_("Could not read certificate file (%s)"), e.what ()));
-                       }
+       wxFileDialog* d = new wxFileDialog (
+               this, _("Select Key File"), wxEmptyString, wxEmptyString, wxT ("PEM files (*.pem)|*.pem"),
+               wxFD_SAVE | wxFD_OVERWRITE_PROMPT
+               );
+
+       if (d->ShowModal () == wxID_OK) {
+               boost::filesystem::path path (wx_to_std(d->GetPath()));
+               FILE* f = fopen_boost (path, "w");
+               if (!f) {
+                       throw OpenFileError (path, errno, OpenFileError::WRITE);
                }
-               
-               d->Destroy ();
+
+               string const s = _get()->key().get ();
+               checked_fwrite (s.c_str(), s.length(), f, path);
+               fclose (f);
        }
+       d->Destroy ();
+}
+
+wxString
+KeysPage::GetName () const
+{
+       return _("Keys");
+}
+
+void
+KeysPage::setup ()
+{
+       wxFont subheading_font (*wxNORMAL_FONT);
+       subheading_font.SetWeight (wxFONTWEIGHT_BOLD);
+
+       wxSizer* sizer = _panel->GetSizer();
 
-       void update_decryption_certificate ()
        {
-               _decryption_certificate->SetLabel (std_to_wx (Config::instance()->decryption_certificate().thumbprint ()));
+               wxStaticText* m = new StaticText (_panel, _("Decrypting KDMs"));
+               m->SetFont (subheading_font);
+               sizer->Add (m, 0, wxALL, _border);
        }
 
-       void load_decryption_private_key ()
-       {
-               wxFileDialog* d = new wxFileDialog (_panel, _("Select Key File"));
+       wxSizer* buttons = new wxBoxSizer (wxVERTICAL);
 
-               if (d->ShowModal() == wxID_OK) {
-                       try {
-                               boost::filesystem::path p (wx_to_std (d->GetPath ()));
-                               Config::instance()->set_decryption_private_key (dcp::file_to_string (p));
-                               update_decryption_private_key ();
-                       } catch (dcp::MiscError& e) {
-                               error_dialog (_panel, wxString::Format (_("Could not read key file (%s)"), e.what ()));
-                       }
-               }
-               
-               d->Destroy ();
-       }
+       wxButton* export_decryption_certificate = new Button (_panel, _("Export KDM decryption certificate..."));
+       buttons->Add (export_decryption_certificate, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
+       wxButton* export_settings = new Button (_panel, _("Export all KDM decryption settings..."));
+       buttons->Add (export_settings, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
+       wxButton* import_settings = new Button (_panel, _("Import all KDM decryption settings..."));
+       buttons->Add (import_settings, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
+       wxButton* decryption_advanced = new Button (_panel, _("Advanced..."));
+       buttons->Add (decryption_advanced, 0);
+
+       sizer->Add (buttons, 0, wxLEFT, _border);
+
+       export_decryption_certificate->Bind (wxEVT_BUTTON, bind (&KeysPage::export_decryption_certificate, this));
+       export_settings->Bind (wxEVT_BUTTON, bind (&KeysPage::export_decryption_chain_and_key, this));
+       import_settings->Bind (wxEVT_BUTTON, bind (&KeysPage::import_decryption_chain_and_key, this));
+       decryption_advanced->Bind (wxEVT_BUTTON, bind (&KeysPage::decryption_advanced, this));
 
-       void update_decryption_private_key ()
        {
-               _decryption_private_key->SetLabel (std_to_wx (dcp::private_key_fingerprint (Config::instance()->decryption_private_key())));
+               wxStaticText* m = new StaticText (_panel, _("Signing DCPs and KDMs"));
+               m->SetFont (subheading_font);
+               sizer->Add (m, 0, wxALL, _border);
        }
 
-       void export_decryption_certificate ()
-       {
-               wxFileDialog* d = new wxFileDialog (
-                       _panel, _("Select Certificate File"), wxEmptyString, wxEmptyString, wxT ("PEM files (*.pem)|*.pem"),
-                       wxFD_SAVE | wxFD_OVERWRITE_PROMPT
-                       );
-               
-               if (d->ShowModal () == wxID_OK) {
-                       FILE* f = fopen_boost (wx_to_std (d->GetPath ()), "w");
-                       if (!f) {
-                               throw OpenFileError (wx_to_std (d->GetPath ()));
-                       }
+       wxButton* 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 = Config::instance()->decryption_certificate().certificate (true);
-                       fwrite (s.c_str(), 1, s.length(), f);
-                       fclose (f);
+void
+KeysPage::decryption_advanced ()
+{
+       CertificateChainEditor* 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();
+}
+
+void
+KeysPage::signing_advanced ()
+{
+       CertificateChainEditor* c = new CertificateChainEditor (
+               _panel, _("Signing DCPs and KDMs"), _border,
+               bind (&Config::set_signer_chain, Config::instance (), _1),
+               bind (&Config::signer_chain, Config::instance ()),
+               bind (&do_nothing)
+               );
+
+       c->ShowModal();
+}
+
+void
+KeysPage::export_decryption_chain_and_key ()
+{
+       wxFileDialog* 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()));
+               FILE* f = fopen_boost (path, "w");
+               if (!f) {
+                       throw OpenFileError (path, errno, OpenFileError::WRITE);
                }
-               d->Destroy ();
+
+               string const chain = Config::instance()->decryption_chain()->chain();
+               checked_fwrite (chain.c_str(), chain.length(), f, path);
+               optional<string> const key = Config::instance()->decryption_chain()->key();
+               DCPOMATIC_ASSERT (key);
+               checked_fwrite (key->c_str(), key->length(), f, path);
+               fclose (f);
        }
+       d->Destroy ();
 
-       wxPanel* _panel;
-       wxListCtrl* _certificates;
-       wxButton* _add_certificate;
-       wxButton* _remove_certificate;
-       wxButton* _remake_certificates;
-       wxStaticText* _signer_private_key;
-       wxButton* _load_signer_private_key;
-       wxStaticText* _decryption_certificate;
-       wxButton* _load_decryption_certificate;
-       wxStaticText* _decryption_private_key;
-       wxButton* _load_decryption_private_key;
-       wxButton* _export_decryption_certificate;
-       shared_ptr<dcp::Signer> _signer;
-};
-
-class TMSPage : public wxPreferencesPage, public Page
+}
+
+void
+KeysPage::import_decryption_chain_and_key ()
 {
-public:
-       TMSPage (wxSize panel_size, int border)
-               : Page (panel_size, border)
-               , _tms_ip (0)
-               , _tms_path (0)
-               , _tms_user (0)
-               , _tms_password (0)
-       {}
-
-       wxString GetName () const
-       {
-               return _("TMS");
+       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;
        }
 
-#ifdef DCPOMATIC_OSX   
-       wxBitmap GetLargeIcon () const
-       {
-               return wxBitmap ("tms", wxBITMAP_TYPE_PNG_RESOURCE);
-       }
-#endif 
+       wxFileDialog* d = new wxFileDialog (
+               _panel, _("Select File To Import"), wxEmptyString, wxEmptyString, wxT ("DOM files (*.dom)|*.dom")
+               );
 
-       wxWindow* CreateWindow (wxWindow* parent)
-       {
-               wxPanel* panel = make_panel (parent);
-
-               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, _("IP address"), true);
-               _tms_ip = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_tms_ip, 1, wxEXPAND);
-               
-               add_label_to_sizer (table, panel, _("Target path"), true);
-               _tms_path = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_tms_path, 1, wxEXPAND);
-               
-               add_label_to_sizer (table, panel, _("User name"), true);
-               _tms_user = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_tms_user, 1, wxEXPAND);
-               
-               add_label_to_sizer (table, panel, _("Password"), true);
-               _tms_password = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_tms_password, 1, wxEXPAND);
-               
-               Config* config = Config::instance ();
-               
-               _tms_ip->SetValue (std_to_wx (config->tms_ip ()));
-               _tms_ip->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&TMSPage::tms_ip_changed, this));
-               _tms_path->SetValue (std_to_wx (config->tms_path ()));
-               _tms_path->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&TMSPage::tms_path_changed, this));
-               _tms_user->SetValue (std_to_wx (config->tms_user ()));
-               _tms_user->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&TMSPage::tms_user_changed, this));
-               _tms_password->SetValue (std_to_wx (config->tms_password ()));
-               _tms_password->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&TMSPage::tms_password_changed, this));
-
-               return panel;
-       }
+       if (d->ShowModal () == wxID_OK) {
+               shared_ptr<dcp::CertificateChain> new_chain(new dcp::CertificateChain());
 
-private:
-       void tms_ip_changed ()
-       {
-               Config::instance()->set_tms_ip (wx_to_std (_tms_ip->GetValue ()));
-       }
-       
-       void tms_path_changed ()
-       {
-               Config::instance()->set_tms_path (wx_to_std (_tms_path->GetValue ()));
-       }
-       
-       void tms_user_changed ()
-       {
-               Config::instance()->set_tms_user (wx_to_std (_tms_user->GetValue ()));
-       }
-       
-       void tms_password_changed ()
-       {
-               Config::instance()->set_tms_password (wx_to_std (_tms_password->GetValue ()));
+               FILE* f = fopen_boost (wx_to_std (d->GetPath ()), "r");
+               if (!f) {
+                       throw OpenFileError (wx_to_std (d->GetPath ()), errno, OpenFileError::WRITE);
+               }
+
+               string current;
+               while (!feof (f)) {
+                       char buffer[128];
+                       if (fgets (buffer, 128, f) == 0) {
+                               break;
+                       }
+                       current += buffer;
+                       if (strncmp (buffer, "-----END CERTIFICATE-----", 25) == 0) {
+                               new_chain->add (dcp::Certificate (current));
+                               current = "";
+                       } else if (strncmp (buffer, "-----END RSA PRIVATE KEY-----", 29) == 0) {
+                               new_chain->set_key (current);
+                               current = "";
+                       }
+               }
+               fclose (f);
+
+               if (new_chain->chain_valid() && new_chain->private_key_valid()) {
+                       Config::instance()->set_decryption_chain (new_chain);
+               } else {
+                       error_dialog (_panel, _("Invalid DCP-o-matic export file"));
+               }
        }
+       d->Destroy ();
+}
 
-       wxTextCtrl* _tms_ip;
-       wxTextCtrl* _tms_path;
-       wxTextCtrl* _tms_user;
-       wxTextCtrl* _tms_password;
-};
+bool
+KeysPage::nag_alter_decryption_chain ()
+{
+       return NagDialog::maybe_nag (
+               _panel,
+               Config::NAG_ALTER_DECRYPTION_CHAIN,
+               _("If you continue with this operation you will no longer be able to use any DKDMs that you have created.  Also, any KDMs that have been sent to you will become useless.  Proceed with caution!"),
+               true
+               );
+}
 
-class KDMEmailPage : public wxPreferencesPage, public Page
+void
+KeysPage::export_decryption_certificate ()
 {
-public:
-
-       KDMEmailPage (wxSize panel_size, int border)
-               : Page (panel_size, border)
-               , _mail_server (0)
-               , _mail_user (0)
-               , _mail_password (0)
-               , _kdm_subject (0)
-               , _kdm_from (0)
-               , _kdm_cc (0)
-               , _kdm_bcc (0)
-               , _kdm_email (0)
-               , _reset_kdm_email (0)
-       {}
-       
-       wxString GetName () const
-       {
-               return _("KDM Email");
-       }
+       wxFileDialog* d = new wxFileDialog (
+               _panel, _("Select Certificate File"), wxEmptyString, _("dcpomatic_kdm_decryption_cert.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()));
+               FILE* f = fopen_boost (path, "w");
+               if (!f) {
+                       throw OpenFileError (path, errno, OpenFileError::WRITE);
+               }
 
-#ifdef DCPOMATIC_OSX   
-       wxBitmap GetLargeIcon () const
-       {
-               return wxBitmap ("kdm_email", wxBITMAP_TYPE_PNG_RESOURCE);
+               string const s = Config::instance()->decryption_chain()->leaf().certificate (true);
+               checked_fwrite (s.c_str(), s.length(), f, path);
+               fclose (f);
        }
-#endif 
 
-       wxWindow* CreateWindow (wxWindow* parent)
-       {
-#ifdef DCPOMATIC_OSX           
-               /* We have to force both width and height of this one */
-               wxPanel* panel = new wxPanel (parent, wxID_ANY, wxDefaultPosition, wxSize (480, 128));
-#else
-               wxPanel* panel = new wxPanel (parent);
-#endif
-               wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
-               panel->SetSizer (s);
-
-               wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
-               table->AddGrowableCol (1, 1);
-               panel->GetSizer()->Add (table, 1, wxEXPAND | wxALL, _border);
-
-               add_label_to_sizer (table, panel, _("Outgoing mail server"), true);
-               _mail_server = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_mail_server, 1, wxEXPAND | wxALL);
-               
-               add_label_to_sizer (table, panel, _("Mail user name"), true);
-               _mail_user = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_mail_user, 1, wxEXPAND | wxALL);
-               
-               add_label_to_sizer (table, panel, _("Mail password"), true);
-               _mail_password = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_mail_password, 1, wxEXPAND | wxALL);
-               
-               wxStaticText* plain = add_label_to_sizer (table, panel, _("(password will be stored on disk in plaintext)"), false);
-               wxFont font = plain->GetFont();
-               font.SetStyle (wxFONTSTYLE_ITALIC);
-               font.SetPointSize (font.GetPointSize() - 1);
-               plain->SetFont (font);
-               table->AddSpacer (0);
-
-               add_label_to_sizer (table, panel, _("Subject"), true);
-               _kdm_subject = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_kdm_subject, 1, wxEXPAND | wxALL);
-               
-               add_label_to_sizer (table, panel, _("From address"), true);
-               _kdm_from = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_kdm_from, 1, wxEXPAND | wxALL);
-
-               add_label_to_sizer (table, panel, _("CC address"), true);
-               _kdm_cc = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_kdm_cc, 1, wxEXPAND | wxALL);
-
-               add_label_to_sizer (table, panel, _("BCC address"), true);
-               _kdm_bcc = new wxTextCtrl (panel, wxID_ANY);
-               table->Add (_kdm_bcc, 1, wxEXPAND | wxALL);
-               
-               _kdm_email = new wxTextCtrl (panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize (480, 128), wxTE_MULTILINE);
-               panel->GetSizer()->Add (_kdm_email, 1, wxEXPAND | wxALL, _border);
-
-               _reset_kdm_email = new wxButton (panel, wxID_ANY, _("Reset to default text"));
-               panel->GetSizer()->Add (_reset_kdm_email, 0, wxEXPAND | wxALL, _border);
-
-               Config* config = Config::instance ();
-               _mail_server->SetValue (std_to_wx (config->mail_server ()));
-               _mail_server->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&KDMEmailPage::mail_server_changed, this));
-               _mail_user->SetValue (std_to_wx (config->mail_user ()));
-               _mail_user->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&KDMEmailPage::mail_user_changed, this));
-               _mail_password->SetValue (std_to_wx (config->mail_password ()));
-               _mail_password->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&KDMEmailPage::mail_password_changed, this));
-               _kdm_subject->SetValue (std_to_wx (config->kdm_subject ()));
-               _kdm_subject->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&KDMEmailPage::kdm_subject_changed, this));
-               _kdm_from->SetValue (std_to_wx (config->kdm_from ()));
-               _kdm_from->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&KDMEmailPage::kdm_from_changed, this));
-               _kdm_cc->SetValue (std_to_wx (config->kdm_cc ()));
-               _kdm_cc->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&KDMEmailPage::kdm_cc_changed, this));
-               _kdm_bcc->SetValue (std_to_wx (config->kdm_bcc ()));
-               _kdm_bcc->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&KDMEmailPage::kdm_bcc_changed, this));
-               _kdm_email->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&KDMEmailPage::kdm_email_changed, this));
-               _kdm_email->SetValue (std_to_wx (Config::instance()->kdm_email ()));
-               _reset_kdm_email->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KDMEmailPage::reset_kdm_email, this));
-
-               return panel;
-       }
+       d->Destroy ();
+}
 
-private:
-       void mail_server_changed ()
-       {
-               Config::instance()->set_mail_server (wx_to_std (_mail_server->GetValue ()));
-       }
-       
-       void mail_user_changed ()
-       {
-               Config::instance()->set_mail_user (wx_to_std (_mail_user->GetValue ()));
-       }
-       
-       void mail_password_changed ()
-       {
-               Config::instance()->set_mail_password (wx_to_std (_mail_password->GetValue ()));
-       }
+wxString
+SoundPage::GetName () const
+{
+       return _("Sound");
+}
 
-       void kdm_subject_changed ()
-       {
-               Config::instance()->set_kdm_subject (wx_to_std (_kdm_subject->GetValue ()));
-       }
-       
-       void kdm_from_changed ()
-       {
-               Config::instance()->set_kdm_from (wx_to_std (_kdm_from->GetValue ()));
+void
+SoundPage::setup ()
+{
+       wxGridBagSizer* 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) {
+               RtAudio::DeviceInfo dev = audio.getDeviceInfo (i);
+               if (dev.probed && dev.outputChannels > 0) {
+                       _sound_output->Append (std_to_wx (dev.name));
+               }
        }
 
-       void kdm_cc_changed ()
-       {
-               Config::instance()->set_kdm_cc (wx_to_std (_kdm_cc->GetValue ()));
-       }
+       _sound->Bind        (wxEVT_CHECKBOX, bind(&SoundPage::sound_changed, this));
+       _sound_output->Bind (wxEVT_CHOICE,   bind(&SoundPage::sound_output_changed, this));
+       _map->Changed.connect (bind(&SoundPage::map_changed, this, _1));
+       _reset_to_default->Bind (wxEVT_BUTTON,   bind(&SoundPage::reset_to_default, this));
+}
 
-       void kdm_bcc_changed ()
-       {
-               Config::instance()->set_kdm_bcc (wx_to_std (_kdm_bcc->GetValue ()));
-       }
-       
-       void kdm_email_changed ()
-       {
-               Config::instance()->set_kdm_email (wx_to_std (_kdm_email->GetValue ()));
-       }
+void
+SoundPage::reset_to_default ()
+{
+       Config::instance()->set_audio_mapping_to_default ();
+}
 
-       void reset_kdm_email ()
-       {
-               Config::instance()->reset_kdm_email ();
-               _kdm_email->SetValue (wx_to_std (Config::instance()->kdm_email ()));
+void
+SoundPage::map_changed (AudioMapping m)
+{
+       Config::instance()->set_audio_mapping (m);
+}
+
+void
+SoundPage::sound_changed ()
+{
+       Config::instance()->set_sound (_sound->GetValue ());
+}
+
+void
+SoundPage::sound_output_changed ()
+{
+       RtAudio audio (DCPOMATIC_RTAUDIO_API);
+       optional<string> const so = get_sound_output();
+       if (!so || *so == audio.getDeviceInfo(audio.getDefaultOutputDevice()).name) {
+               Config::instance()->unset_sound_output ();
+       } else {
+               Config::instance()->set_sound_output (*so);
        }
+}
 
-       wxTextCtrl* _mail_server;
-       wxTextCtrl* _mail_user;
-       wxTextCtrl* _mail_password;
-       wxTextCtrl* _kdm_subject;
-       wxTextCtrl* _kdm_from;
-       wxTextCtrl* _kdm_cc;
-       wxTextCtrl* _kdm_bcc;
-       wxTextCtrl* _kdm_email;
-       wxButton* _reset_kdm_email;
-};
-
-/** @class AdvancedPage
- *  @brief Advanced page of the preferences dialog.
- */
-class AdvancedPage : public wxStockPreferencesPage, public Page
+void
+SoundPage::config_changed ()
 {
-public:
-
-       AdvancedPage (wxSize panel_size, int border)
-               : wxStockPreferencesPage (Kind_Advanced)
-               , Page (panel_size, border)
-               , _maximum_j2k_bandwidth (0)
-               , _allow_any_dcp_frame_rate (0)
-               , _log_general (0)
-               , _log_warning (0)
-               , _log_error (0)
-               , _log_timing (0)
-       {}
-       
-       wxWindow* CreateWindow (wxWindow* parent)
-       {
-               wxPanel* panel = make_panel (parent);
-
-               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);
+       Config* config = Config::instance ();
+
+       checked_set (_sound, config->sound ());
+
+       optional<string> 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& e) {
+                       /* Probably no audio devices at all */
                }
+       }
 
-               _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);
-
-#ifdef __WXOSX__
-               wxStaticText* m = new wxStaticText (panel, wxID_ANY, _("Log:"));
-               table->Add (m, 0, wxALIGN_TOP | wxLEFT | wxRIGHT | wxEXPAND | wxALL | wxALIGN_RIGHT, 6);
-#else          
-               wxStaticText* m = new wxStaticText (panel, wxID_ANY, _("Log"));
-               table->Add (m, 0, wxALIGN_TOP | wxLEFT | wxRIGHT | wxEXPAND | wxALL, 6);
-#endif         
-               
-               {
-                       wxBoxSizer* t = new wxBoxSizer (wxVERTICAL);
-                       _log_general = new wxCheckBox (panel, wxID_ANY, _("General"));
-                       t->Add (_log_general, 1, wxEXPAND | wxALL);
-                       _log_warning = new wxCheckBox (panel, wxID_ANY, _("Warnings"));
-                       t->Add (_log_warning, 1, wxEXPAND | wxALL);
-                       _log_error = new wxCheckBox (panel, wxID_ANY, _("Errors"));
-                       t->Add (_log_error, 1, wxEXPAND | wxALL);
-                       _log_timing = new wxCheckBox (panel, wxID_ANY, S_("Config|Timing"));
-                       t->Add (_log_timing, 1, wxEXPAND | wxALL);
-                       table->Add (t, 0, wxALL, 6);
+       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;
                }
+       }
 
-#ifdef DCPOMATIC_WINDOWS               
-               _win32_console = new wxCheckBox (panel, wxID_ANY, _("Open console window"));
-               table->Add (_win32_console, 1, wxEXPAND | wxALL);
-               table->AddSpacer (0);
-#endif         
-               
-               Config* config = Config::instance ();
-               
-               _maximum_j2k_bandwidth->SetRange (1, 1000);
-               _maximum_j2k_bandwidth->SetValue (config->maximum_j2k_bandwidth() / 1000000);
-               _maximum_j2k_bandwidth->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&AdvancedPage::maximum_j2k_bandwidth_changed, this));
-               _allow_any_dcp_frame_rate->SetValue (config->allow_any_dcp_frame_rate ());
-               _allow_any_dcp_frame_rate->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::allow_any_dcp_frame_rate_changed, this));
-               _log_general->SetValue (config->log_types() & Log::TYPE_GENERAL);
-               _log_general->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::log_changed, this));
-               _log_warning->SetValue (config->log_types() & Log::TYPE_WARNING);
-               _log_warning->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::log_changed, this));
-               _log_error->SetValue (config->log_types() & Log::TYPE_ERROR);
-               _log_error->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::log_changed, this));
-               _log_timing->SetValue (config->log_types() & Log::TYPE_TIMING);
-               _log_timing->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::log_changed, this));
-#ifdef DCPOMATIC_WINDOWS
-               _win32_console->SetValue (config->win32_console());
-               _win32_console->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::win32_console_changed, this));
-#endif         
-               
-               return panel;
+       RtAudio audio (DCPOMATIC_RTAUDIO_API);
+
+       map<int, wxString> apis;
+       apis[RtAudio::MACOSX_CORE]    = _("CoreAudio");
+       apis[RtAudio::WINDOWS_ASIO]   = _("ASIO");
+       apis[RtAudio::WINDOWS_DS]     = _("Direct Sound");
+       apis[RtAudio::WINDOWS_WASAPI] = _("WASAPI");
+       apis[RtAudio::UNIX_JACK]      = _("JACK");
+       apis[RtAudio::LINUX_ALSA]     = _("ALSA");
+       apis[RtAudio::LINUX_PULSE]    = _("PulseAudio");
+       apis[RtAudio::LINUX_OSS]      = _("OSS");
+       apis[RtAudio::RTAUDIO_DUMMY]  = _("Dummy");
+
+       int channels = 0;
+       if (configured_so) {
+               for (unsigned int i = 0; i < audio.getDeviceCount(); ++i) {
+                       RtAudio::DeviceInfo info = audio.getDeviceInfo(i);
+                       if (info.name == *configured_so && info.outputChannels > 0) {
+                               channels = info.outputChannels;
+                       }
+               }
        }
 
-private:
+       _sound_output_details->SetLabel (
+               wxString::Format(_("%d channels on %s"), channels, apis[audio.getCurrentApi()])
+               );
 
-       void maximum_j2k_bandwidth_changed ()
-       {
-               Config::instance()->set_maximum_j2k_bandwidth (_maximum_j2k_bandwidth->GetValue() * 1000000);
-       }
+       _map->set (Config::instance()->audio_mapping(channels));
 
-       void allow_any_dcp_frame_rate_changed ()
-       {
-               Config::instance()->set_allow_any_dcp_frame_rate (_allow_any_dcp_frame_rate->GetValue ());
+       vector<string> input;
+       for (int i = 0; i < MAX_DCP_AUDIO_CHANNELS; ++i) {
+               input.push_back (short_audio_channel_name(i));
        }
+       _map->set_input_channels (input);
 
-       void log_changed ()
-       {
-               int types = 0;
-               if (_log_general->GetValue ()) {
-                       types |= Log::TYPE_GENERAL;
-               }
-               if (_log_warning->GetValue ()) {
-                       types |= Log::TYPE_WARNING;
-               }
-               if (_log_error->GetValue ())  {
-                       types |= Log::TYPE_ERROR;
-               }
-               if (_log_timing->GetValue ()) {
-                       types |= Log::TYPE_TIMING;
-               }
-               Config::instance()->set_log_types (types);
+       vector<string> output;
+       for (int i = 0; i < channels; ++i) {
+               output.push_back (dcp::raw_convert<string>(i));
        }
+       _map->set_output_channels (output);
 
-#ifdef DCPOMATIC_WINDOWS       
-       void win32_console_changed ()
-       {
-               Config::instance()->set_win32_console (_win32_console->GetValue ());
+       setup_sensitivity ();
+}
+
+void
+SoundPage::setup_sensitivity ()
+{
+       _sound_output->Enable (_sound->GetValue());
+}
+
+/** @return Currently-selected preview sound output in the dialogue */
+optional<string>
+SoundPage::get_sound_output ()
+{
+       int const sel = _sound_output->GetSelection ();
+       if (sel == wxNOT_FOUND) {
+               return optional<string> ();
        }
-#endif 
-       
-       wxSpinCtrl* _maximum_j2k_bandwidth;
-       wxCheckBox* _allow_any_dcp_frame_rate;
-       wxCheckBox* _log_general;
-       wxCheckBox* _log_warning;
-       wxCheckBox* _log_error;
-       wxCheckBox* _log_timing;
-#ifdef DCPOMATIC_WINDOWS       
-       wxCheckBox* _win32_console;
-#endif 
-};
-       
-wxPreferencesEditor*
-create_config_dialog ()
+
+       return wx_to_std (_sound_output->GetString (sel));
+}
+
+
+LocationsPage::LocationsPage (wxSize panel_size, int border)
+       : StandardPage (panel_size, border)
+{
+
+}
+
+wxString
+LocationsPage::GetName () const
 {
-       wxPreferencesEditor* e = new wxPreferencesEditor ();
+       return _("Locations");
+}
 
 #ifdef DCPOMATIC_OSX
-       /* Width that we force some of the config panels to be on OSX so that
-          the containing window doesn't shrink too much when we select those panels.
-          This is obviously an unpleasant hack.
-       */
-       wxSize ps = wxSize (520, -1);
-       int const border = 16;
-#else
-       wxSize ps = wxSize (-1, -1);
-       int const border = 8;
+wxBitmap
+LocationsPage::GetLargeIcon () const
+{
+       return wxBitmap ("locations", wxBITMAP_TYPE_PNG_RESOURCE);
+}
+#endif
+
+void
+LocationsPage::setup ()
+{
+
+       int r = 0;
+
+       wxGridBagSizer* table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
+       _panel->GetSizer()->Add (table, 1, wxALL | wxEXPAND, _border);
+
+       add_label_to_sizer (table, _panel, _("Content directory"), true, wxGBPosition (r, 0));
+       _content_directory = new wxDirPickerCtrl (_panel, wxID_ANY, wxEmptyString, wxDirSelectorPromptStr, wxDefaultPosition, wxSize (300, -1));
+       table->Add (_content_directory, wxGBPosition (r, 1));
+       ++r;
+
+       add_label_to_sizer (table, _panel, _("Playlist directory"), true, wxGBPosition (r, 0));
+       _playlist_directory = new wxDirPickerCtrl (_panel, wxID_ANY, wxEmptyString, wxDirSelectorPromptStr, wxDefaultPosition, wxSize (300, -1));
+       table->Add (_playlist_directory, wxGBPosition (r, 1));
+       ++r;
+
+       add_label_to_sizer (table, _panel, _("KDM directory"), true, wxGBPosition (r, 0));
+       _kdm_directory = new wxDirPickerCtrl (_panel, wxID_ANY, wxEmptyString, wxDirSelectorPromptStr, wxDefaultPosition, wxSize (300, -1));
+       table->Add (_kdm_directory, wxGBPosition (r, 1));
+       ++r;
+
+#ifdef DCPOMATIC_VARIANT_SWAROOP
+       add_label_to_sizer (table, _panel, _("Background image"), true, wxGBPosition (r, 0));
+       _background_image = new FilePickerCtrl (_panel, _("Select image file"), "*.png;*.jpg;*.jpeg;*.tif;*.tiff", true, false);
+       table->Add (_background_image, wxGBPosition (r, 1));
+       ++r;
+#endif
+
+       _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));
+#ifdef DCPOMATIC_VARIANT_SWAROOP
+       _background_image->Bind (wxEVT_FILEPICKER_CHANGED, bind(&LocationsPage::background_image_changed, this));
 #endif
-       
-       e->AddPage (new GeneralPage (ps, border));
-       e->AddPage (new DefaultsPage (ps, border));
-       e->AddPage (new EncodingServersPage (ps, border));
-       e->AddPage (new ColourConversionsPage (ps, border));
-       e->AddPage (new 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::config_changed ()
+{
+       Config* config = Config::instance ();
+
+       if (config->player_content_directory()) {
+               checked_set (_content_directory, *config->player_content_directory());
+       }
+       if (config->player_playlist_directory()) {
+               checked_set (_playlist_directory, *config->player_playlist_directory());
+       }
+       if (config->player_kdm_directory()) {
+               checked_set (_kdm_directory, *config->player_kdm_directory());
+       }
+#ifdef DCPOMATIC_VARIANT_SWAROOP
+       if (config->player_background_image()) {
+               checked_set (_background_image, *config->player_background_image());
+       }
+#endif
+}
+
+void
+LocationsPage::content_directory_changed ()
+{
+       Config::instance()->set_player_content_directory(wx_to_std(_content_directory->GetPath()));
+}
+
+void
+LocationsPage::playlist_directory_changed ()
+{
+       Config::instance()->set_player_playlist_directory(wx_to_std(_playlist_directory->GetPath()));
+}
+
+void
+LocationsPage::kdm_directory_changed ()
+{
+       Config::instance()->set_player_kdm_directory(wx_to_std(_kdm_directory->GetPath()));
+}
+
+#ifdef DCPOMATIC_VARIANT_SWAROOP
+void
+LocationsPage::background_image_changed ()
+{
+       boost::filesystem::path const f = wx_to_std(_background_image->GetPath());
+       if (!boost::filesystem::is_regular_file(f) || !wxImage::CanRead(std_to_wx(f.string()))) {
+               error_dialog (0, _("Could not load image file."));
+               if (Config::instance()->player_background_image()) {
+                       checked_set (_background_image, *Config::instance()->player_background_image());
+               }
+               return;
+       }
+
+       Config::instance()->set_player_background_image(f);
+}
+#endif