Work around deadlock when destroying J2KEncoder with a full writer queue (#2784).
[dcpomatic.git] / src / wx / config_dialog.cc
index a32b5d9680eaa327ea92d2243f0dad7089e9d60c..b4adc855e4af103398fd11a8c5a27cbc33c6a8c6 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2012-2018 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2021 Carl Hetherington <cth@carlh.net>
 
     This file is part of DCP-o-matic.
 
 
 */
 
-#include "config_dialog.h"
-#include "static_text.h"
+
+#include "audio_mapping_view.h"
 #include "check_box.h"
-#include "nag_dialog.h"
+#include "config_dialog.h"
 #include "dcpomatic_button.h"
+#include "nag_dialog.h"
+#include "static_text.h"
+#include "lib/constants.h"
+#include <dcp/file.h>
+#include <dcp/filesystem.h>
+#include <dcp/raw_convert.h>
 
-using std::string;
-using std::vector;
-using std::pair;
+
+using std::function;
 using std::make_pair;
+using std::make_shared;
 using std::map;
+using std::pair;
+using std::shared_ptr;
+using std::string;
+using std::vector;
 using boost::bind;
 using boost::optional;
-using boost::shared_ptr;
-using boost::function;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+
 
 static
 bool
@@ -50,11 +62,19 @@ Page::Page (wxSize panel_size, int border)
        _config_connection = Config::instance()->Changed.connect (bind (&Page::config_changed_wrapper, this));
 }
 
+
+wxWindow*
+Page::CreateWindow (wxWindow* parent)
+{
+       return create_window (parent);
+}
+
+
 wxWindow*
 Page::create_window (wxWindow* parent)
 {
        _panel = new wxPanel (parent, wxID_ANY, wxDefaultPosition, _panel_size);
-       wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
+       auto s = new wxBoxSizer (wxVERTICAL);
        _panel->SetSizer (s);
 
        setup ();
@@ -81,65 +101,53 @@ Page::window_destroyed ()
 }
 
 
-StockPage::StockPage (Kind kind, wxSize panel_size, int border)
-       : wxStockPreferencesPage (kind)
-       , Page (panel_size, border)
-{
-
-}
-
-wxWindow*
-StockPage::CreateWindow (wxWindow* parent)
-{
-       return create_window (parent);
-}
-
-StandardPage::StandardPage (wxSize panel_size, int border)
+GeneralPage::GeneralPage (wxSize panel_size, int border)
        : Page (panel_size, border)
 {
 
 }
 
-wxWindow*
-StandardPage::CreateWindow (wxWindow* parent)
-{
-       return create_window (parent);
-}
 
-GeneralPage::GeneralPage (wxSize panel_size, int border)
-       : StockPage (Kind_General, panel_size, border)
+wxString
+GeneralPage::GetName () const
 {
-
+       return _("General");
 }
 
+
 void
 GeneralPage::add_language_controls (wxGridBagSizer* table, int& r)
 {
        _set_language = new CheckBox (_panel, _("Set language"));
        table->Add (_set_language, wxGBPosition (r, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
        _language = new wxChoice (_panel, wxID_ANY);
-       vector<pair<string, string> > languages;
-       languages.push_back (make_pair ("Čeština", "cs_CZ"));
-       languages.push_back (make_pair ("汉语/漢語", "zh_CN"));
-       languages.push_back (make_pair ("Dansk", "da_DK"));
-       languages.push_back (make_pair ("Deutsch", "de_DE"));
-       languages.push_back (make_pair ("English", "en_GB"));
-       languages.push_back (make_pair ("Español", "es_ES"));
-       languages.push_back (make_pair ("Français", "fr_FR"));
-       languages.push_back (make_pair ("Italiano", "it_IT"));
-       languages.push_back (make_pair ("Nederlands", "nl_NL"));
-       languages.push_back (make_pair ("Русский", "ru_RU"));
-       languages.push_back (make_pair ("Polski", "pl_PL"));
-       languages.push_back (make_pair ("Português europeu", "pt_PT"));
-       languages.push_back (make_pair ("Português do Brasil", "pt_BR"));
-       languages.push_back (make_pair ("Svenska", "sv_SE"));
-       languages.push_back (make_pair ("Slovenský jazyk", "sk_SK"));
-       languages.push_back (make_pair ("українська мова", "uk_UA"));
+       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("فارسی", "fa_IR"));
+       languages.push_back (make_pair("Français", "fr_FR"));
+       languages.push_back (make_pair("Italiano", "it_IT"));
+       languages.push_back (make_pair("ქართული", "ka_KA"));
+       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("Slovenščina", "sl_SI"));
+       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"));
+       languages.push_back (make_pair("Magyar nyelv", "hu_HU"));
        checked_set (_language, languages);
        table->Add (_language, wxGBPosition (r, 1));
        ++r;
 
-       wxStaticText* restart = add_label_to_sizer (
+       auto restart = add_label_to_sizer (
                table, _panel, _("(restart DCP-o-matic to see language changes)"), false, wxGBPosition (r, 0), wxGBSpan (1, 2)
                );
        wxFont font = restart->GetFont();
@@ -148,31 +156,10 @@ GeneralPage::add_language_controls (wxGridBagSizer* table, int& r)
        restart->SetFont (font);
        ++r;
 
-       _set_language->Bind (wxEVT_CHECKBOX, bind (&GeneralPage::set_language_changed, this));
+       _set_language->bind(&GeneralPage::set_language_changed, this);
        _language->Bind     (wxEVT_CHOICE,   bind (&GeneralPage::language_changed,     this));
 }
 
-void
-GeneralPage::add_play_sound_controls (wxGridBagSizer* table, int& r)
-{
-       _sound = new CheckBox (_panel, _("Play sound via"));
-       table->Add (_sound, wxGBPosition (r, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
-       _sound_output = new wxChoice (_panel, wxID_ANY);
-       table->Add (_sound_output, wxGBPosition (r, 1));
-       ++r;
-
-       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));
-               }
-       }
-
-       _sound->Bind        (wxEVT_CHECKBOX, bind (&GeneralPage::sound_changed, this));
-       _sound_output->Bind (wxEVT_CHOICE,   bind (&GeneralPage::sound_output_changed, this));
-}
-
 void
 GeneralPage::add_update_controls (wxGridBagSizer* table, int& r)
 {
@@ -184,14 +171,14 @@ GeneralPage::add_update_controls (wxGridBagSizer* table, int& r)
        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));
+       _check_for_updates->bind(&GeneralPage::check_for_updates_changed, this);
+       _check_for_test_updates->bind(&GeneralPage::check_for_test_updates_changed, this);
 }
 
 void
 GeneralPage::config_changed ()
 {
-       Config* config = Config::instance ();
+       auto config = Config::instance ();
 
        checked_set (_set_language, static_cast<bool>(config->language()));
 
@@ -212,8 +199,8 @@ GeneralPage::config_changed ()
        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 ()) {
+       auto lang = config->language().get_value_or("en_GB");
+       if (compat_map.find(lang) != compat_map.end ()) {
                lang = compat_map[lang];
        }
 
@@ -222,35 +209,6 @@ GeneralPage::config_changed ()
        checked_set (_check_for_updates, config->check_for_updates ());
        checked_set (_check_for_test_updates, config->check_for_test_updates ());
 
-       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 */
-               }
-       }
-
-       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;
-               }
-       }
-
        setup_sensitivity ();
 }
 
@@ -259,19 +217,6 @@ GeneralPage::setup_sensitivity ()
 {
        _language->Enable (_set_language->GetValue ());
        _check_for_test_updates->Enable (_check_for_updates->GetValue ());
-       _sound_output->Enable (_sound->GetValue ());
-}
-
-/** @return Currently-selected preview sound output in the dialogue */
-optional<string>
-GeneralPage::get_sound_output ()
-{
-       int const sel = _sound_output->GetSelection ();
-       if (sel == wxNOT_FOUND) {
-               return optional<string> ();
-       }
-
-       return wx_to_std (_sound_output->GetString (sel));
 }
 
 void
@@ -308,50 +253,23 @@ GeneralPage::check_for_test_updates_changed ()
        Config::instance()->set_check_for_test_updates (_check_for_test_updates->GetValue ());
 }
 
-void
-GeneralPage::sound_changed ()
-{
-       Config::instance()->set_sound (_sound->GetValue ());
-}
-
-void
-GeneralPage::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);
-       }
-}
-
 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_remake
+       function<bool (void)> nag_alter
        )
        : wxDialog (parent, wxID_ANY, title)
        , _set (set)
        , _get (get)
-       , _nag_remake (nag_remake)
+       , _nag_alter (nag_alter)
 {
-       wxFont subheading_font (*wxNORMAL_FONT);
-       subheading_font.SetWeight (wxFONTWEIGHT_BOLD);
-
        _sizer = new wxBoxSizer (wxVERTICAL);
 
-       {
-               wxStaticText* m = new StaticText (this, title);
-               m->SetFont (subheading_font);
-               _sizer->Add (m, 0, wxALL, border);
-       }
-
-       wxBoxSizer* certificates_sizer = new wxBoxSizer (wxHORIZONTAL);
-       _sizer->Add (certificates_sizer, 0, wxLEFT | wxRIGHT, border);
+       auto certificates_sizer = new wxBoxSizer (wxHORIZONTAL);
+       _sizer->Add (certificates_sizer, 0, wxALL, border);
 
        _certificates = new wxListCtrl (this, wxID_ANY, wxDefaultPosition, wxSize (440, 150), wxLC_REPORT | wxLC_SINGLE_SEL);
 
@@ -379,17 +297,19 @@ CertificateChainEditor::CertificateChainEditor (
        certificates_sizer->Add (_certificates, 1, wxEXPAND);
 
        {
-               wxSizer* s = new wxBoxSizer (wxVERTICAL);
+               auto s = new wxBoxSizer (wxVERTICAL);
                _add_certificate = new Button (this, _("Add..."));
-               s->Add (_add_certificate, 0, wxTOP | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
+               s->Add (_add_certificate, 1, wxTOP | wxBOTTOM | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
                _remove_certificate = new Button (this, _("Remove"));
-               s->Add (_remove_certificate, 0, wxTOP | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
-               _export_certificate = new Button (this, _("Export"));
-               s->Add (_export_certificate, 0, wxTOP | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
+               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);
        }
 
-       wxGridBagSizer* table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
+       auto table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
        _sizer->Add (table, 1, wxALL | wxEXPAND, border);
        int r = 0;
 
@@ -408,8 +328,6 @@ CertificateChainEditor::CertificateChainEditor (
        _button_sizer = new wxBoxSizer (wxHORIZONTAL);
        _remake_certificates = new Button (this, _("Re-make certificates and key..."));
        _button_sizer->Add (_remake_certificates, 1, wxRIGHT, border);
-       _export_chain = new Button (this, _("Export chain..."));
-       _button_sizer->Add (_export_chain, 1, wxRIGHT, border);
        table->Add (_button_sizer, wxGBPosition (r, 0), wxGBSpan (1, 4));
        ++r;
 
@@ -430,7 +348,7 @@ CertificateChainEditor::CertificateChainEditor (
        _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);
+       auto buttons = CreateSeparatedButtonSizer (wxCLOSE);
        if (buttons) {
                _sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
        }
@@ -452,7 +370,7 @@ CertificateChainEditor::add_button (wxWindow* button)
 void
 CertificateChainEditor::add_certificate ()
 {
-       wxFileDialog* d = new wxFileDialog (this, _("Select Certificate File"));
+       auto d = make_wx<wxFileDialog>(this, _("Select Certificate File"));
 
        if (d->ShowModal() == wxID_OK) {
                try {
@@ -462,7 +380,6 @@ CertificateChainEditor::add_certificate ()
                                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;
                        }
 
@@ -473,7 +390,7 @@ CertificateChainEditor::add_certificate ()
                                          "Only the first certificate will be used.")
                                        );
                        }
-                       shared_ptr<dcp::CertificateChain> chain(new dcp::CertificateChain(*_get().get()));
+                       auto chain = make_shared<dcp::CertificateChain>(*_get().get());
                        chain->add (c);
                        if (!chain->chain_valid ()) {
                                error_dialog (
@@ -491,25 +408,29 @@ CertificateChainEditor::add_certificate ()
                }
        }
 
-       d->Destroy ();
-
        update_sensitivity ();
 }
 
 void
 CertificateChainEditor::remove_certificate ()
 {
+       if (_nag_alter()) {
+               /* Cancel was clicked */
+               return;
+       }
+
        int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
        if (i == -1) {
                return;
        }
 
        _certificates->DeleteItem (i);
-       shared_ptr<dcp::CertificateChain> chain(new dcp::CertificateChain(*_get().get()));
+       auto chain = make_shared<dcp::CertificateChain>(*_get().get());
        chain->remove (i);
        _set (chain);
 
        update_sensitivity ();
+       update_certificate_list ();
 }
 
 void
@@ -520,52 +441,67 @@ CertificateChainEditor::export_certificate ()
                return;
        }
 
-       wxFileDialog* d = new wxFileDialog (
-               this, _("Select Certificate File"), wxEmptyString, wxEmptyString, wxT ("PEM files (*.pem)|*.pem"),
+       auto all = _get()->root_to_leaf();
+
+       wxString default_name;
+       if (i == 0) {
+               default_name = "root.pem";
+       } else if (i == static_cast<int>(all.size() - 1)) {
+               default_name = "leaf.pem";
+       } else {
+               default_name = "intermediate.pem";
+       }
+
+       auto d = make_wx<wxFileDialog>(
+               this, _("Select Certificate File"), wxEmptyString, default_name, wxT ("PEM files (*.pem)|*.pem"),
                wxFD_SAVE | wxFD_OVERWRITE_PROMPT
                );
 
-       dcp::CertificateChain::List all = _get()->root_to_leaf ();
-       dcp::CertificateChain::List::iterator j = all.begin ();
+       auto j = all.begin ();
        for (int k = 0; k < i; ++k) {
                ++j;
        }
 
-       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, false);
-               }
+       if (d->ShowModal() != wxID_OK) {
+               return;
+       }
 
-               string const s = j->certificate (true);
-               checked_fwrite (s.c_str(), s.length(), f, path);
-               fclose (f);
+       boost::filesystem::path path(wx_to_std(d->GetPath()));
+       if (path.extension() != ".pem") {
+               path += ".pem";
+       }
+       dcp::File f(path, "w");
+       if (!f) {
+               throw OpenFileError(path, errno, OpenFileError::WRITE);
        }
-       d->Destroy ();
+
+       string const s = j->certificate(true);
+       f.checked_write(s.c_str(), s.length());
 }
 
 void
 CertificateChainEditor::export_chain ()
 {
-       wxFileDialog* d = new wxFileDialog (
-               this, _("Select Chain File"), wxEmptyString, wxEmptyString, wxT("PEM files (*.pem)|*.pem"),
+       auto d = make_wx<wxFileDialog>(
+               this, _("Select Chain File"), wxEmptyString, wxT("certificate_chain.pem"), wxT("PEM files (*.pem)|*.pem"),
                wxFD_SAVE | wxFD_OVERWRITE_PROMPT
                );
 
-       if (d->ShowModal () == wxID_OK) {
-               boost::filesystem::path path (wx_to_std(d->GetPath()));
-               FILE* f = fopen_boost (path, "w");
-               if (!f) {
-                       throw OpenFileError (path, errno, false);
-               }
+       if (d->ShowModal() != wxID_OK) {
+               return;
+       }
 
-               string const s = _get()->chain();
-               checked_fwrite (s.c_str(), s.length(), f, path);
-               fclose (f);
+       boost::filesystem::path path(wx_to_std(d->GetPath()));
+       if (path.extension() != ".pem") {
+               path += ".pem";
+       }
+       dcp::File f(path, "w");
+       if (!f) {
+               throw OpenFileError(path, errno, OpenFileError::WRITE);
        }
 
-       d->Destroy ();
+       auto const s = _get()->chain();
+       f.checked_write(s.c_str(), s.length());
 }
 
 void
@@ -573,8 +509,8 @@ 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) {
+       auto certs = _get()->root_to_leaf();
+       for (auto const& i: certs) {
                wxListItem item;
                item.SetId (n);
                _certificates->InsertItem (item);
@@ -605,76 +541,26 @@ CertificateChainEditor::update_certificate_list ()
 void
 CertificateChainEditor::remake_certificates ()
 {
-       shared_ptr<const dcp::CertificateChain> chain = _get();
-
-       string subject_organization_name;
-       string subject_organizational_unit_name;
-       string root_common_name;
-       string intermediate_common_name;
-       string leaf_common_name;
-
-       dcp::CertificateChain::List all = chain->root_to_leaf ();
-
-       if (all.size() >= 1) {
-               /* Have a root */
-               subject_organization_name = chain->root().subject_organization_name ();
-               subject_organizational_unit_name = chain->root().subject_organizational_unit_name ();
-               root_common_name = chain->root().subject_common_name ();
-       }
-
-       if (all.size() >= 2) {
-               /* Have a leaf */
-               leaf_common_name = chain->leaf().subject_common_name ();
-       }
-
-       if (all.size() >= 3) {
-               /* Have an intermediate */
-               dcp::CertificateChain::List::iterator i = all.begin ();
-               ++i;
-               intermediate_common_name = i->subject_common_name ();
-       }
-
-       if (_nag_remake()) {
+       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
-               );
+       auto d = make_wx<MakeChainDialog>(this, _get());
 
        if (d->ShowModal () == wxID_OK) {
-               _set (
-                       shared_ptr<dcp::CertificateChain> (
-                               new dcp::CertificateChain (
-                                       openssl_path (),
-                                       d->organisation (),
-                                       d->organisational_unit (),
-                                       d->root_common_name (),
-                                       d->intermediate_common_name (),
-                                       d->leaf_common_name ()
-                                       )
-                               )
-                       );
-
+               _set (d->get());
                update_certificate_list ();
                update_private_key ();
        }
-
-       d->Destroy ();
 }
 
 void
 CertificateChainEditor::update_sensitivity ()
 {
        /* We can only remove the leaf certificate */
-       _remove_certificate->Enable (_certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED) == (_certificates->GetItemCount() - 1));
-       _export_certificate->Enable (_certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED) != -1);
+       _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
@@ -687,12 +573,12 @@ CertificateChainEditor::update_private_key ()
 void
 CertificateChainEditor::import_private_key ()
 {
-       wxFileDialog* d = new wxFileDialog (this, _("Select Key File"));
+       auto d = make_wx<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) {
+                       if (dcp::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 ()))
@@ -700,45 +586,44 @@ CertificateChainEditor::import_private_key ()
                                return;
                        }
 
-                       shared_ptr<dcp::CertificateChain> chain(new dcp::CertificateChain(*_get().get()));
+                       auto chain = make_shared<dcp::CertificateChain>(*_get().get());
                        chain->set_key (dcp::file_to_string (p));
                        _set (chain);
                        update_private_key ();
-               } catch (dcp::MiscError& e) {
+               } catch (std::exception& e) {
                        error_dialog (this, _("Could not read certificate file."), std_to_wx(e.what()));
                }
        }
 
-       d->Destroy ();
-
        update_sensitivity ();
 }
 
 void
 CertificateChainEditor::export_private_key ()
 {
-       optional<string> key = _get()->key();
+       auto key = _get()->key();
        if (!key) {
                return;
        }
 
-       wxFileDialog* d = new wxFileDialog (
-               this, _("Select Key File"), wxEmptyString, wxEmptyString, wxT ("PEM files (*.pem)|*.pem"),
+       auto d = make_wx<wxFileDialog>(
+               this, _("Select Key File"), wxEmptyString, wxT("private_key.pem"), wxT("PEM files (*.pem)|*.pem"),
                wxFD_SAVE | wxFD_OVERWRITE_PROMPT
                );
 
        if (d->ShowModal () == wxID_OK) {
                boost::filesystem::path path (wx_to_std(d->GetPath()));
-               FILE* f = fopen_boost (path, "w");
+               if (path.extension() != ".pem") {
+                       path += ".pem";
+               }
+               dcp::File f(path, "w");
                if (!f) {
-                       throw OpenFileError (path, errno, false);
+                       throw OpenFileError (path, errno, OpenFileError::WRITE);
                }
 
-               string const s = _get()->key().get ();
-               checked_fwrite (s.c_str(), s.length(), f, path);
-               fclose (f);
+               auto const s = _get()->key().get ();
+               f.checked_write(s.c_str(), s.length());
        }
-       d->Destroy ();
 }
 
 wxString
@@ -753,22 +638,26 @@ KeysPage::setup ()
        wxFont subheading_font (*wxNORMAL_FONT);
        subheading_font.SetWeight (wxFONTWEIGHT_BOLD);
 
-       wxSizer* sizer = _panel->GetSizer();
+       auto sizer = _panel->GetSizer();
 
        {
-               wxStaticText* m = new StaticText (_panel, _("Decrypting KDMs"));
+               auto m = new StaticText (_panel, _("Decrypting KDMs"));
                m->SetFont (subheading_font);
-               sizer->Add (m, 0, wxALL, _border);
+               sizer->Add (m, 0, wxALL | wxEXPAND, _border);
        }
 
-       wxButton* export_decryption_certificate = new Button (_panel, _("Export KDM decryption certificate..."));
-       sizer->Add (export_decryption_certificate, 0, wxLEFT, _border);
-       wxButton* export_settings = new Button (_panel, _("Export all KDM decryption settings..."));
-       sizer->Add (export_settings, 0, wxLEFT, _border);
-       wxButton* import_settings = new Button (_panel, _("Import all KDM decryption settings..."));
-       sizer->Add (import_settings, 0, wxLEFT, _border);
-       wxButton* decryption_advanced = new Button (_panel, _("Advanced..."));
-       sizer->Add (decryption_advanced, 0, wxALL, _border);
+       auto kdm_buttons = new wxBoxSizer (wxVERTICAL);
+
+       auto export_decryption_certificate = new Button (_panel, _("Export KDM decryption leaf certificate..."));
+       kdm_buttons->Add (export_decryption_certificate, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
+       auto export_settings = new Button (_panel, _("Export all KDM decryption settings..."));
+       kdm_buttons->Add (export_settings, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
+       auto import_settings = new Button (_panel, _("Import all KDM decryption settings..."));
+       kdm_buttons->Add (import_settings, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
+       auto decryption_advanced = new Button (_panel, _("Advanced..."));
+       kdm_buttons->Add (decryption_advanced, 0);
+
+       sizer->Add (kdm_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));
@@ -776,24 +665,44 @@ KeysPage::setup ()
        decryption_advanced->Bind (wxEVT_BUTTON, bind (&KeysPage::decryption_advanced, this));
 
        {
-               wxStaticText* m = new StaticText (_panel, _("Signing DCPs and KDMs"));
+               auto m = new StaticText (_panel, _("Signing DCPs and KDMs"));
                m->SetFont (subheading_font);
-               sizer->Add (m, 0, wxALL, _border);
+               sizer->Add (m, 0, wxALL | wxEXPAND, _border);
        }
 
-       wxButton* signing_advanced = new Button (_panel, _("Advanced..."));
-       sizer->Add (signing_advanced, 0, wxLEFT, _border);
+       auto signing_buttons = new wxBoxSizer (wxVERTICAL);
+
+       auto signing_advanced = new Button (_panel, _("Advanced..."));
+       signing_buttons->Add (signing_advanced, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
+       auto remake_signing = new Button (_panel, _("Re-make certificates and key..."));
+       signing_buttons->Add (remake_signing, 0, wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
+
+       sizer->Add (signing_buttons, 0, wxLEFT, _border);
+
        signing_advanced->Bind (wxEVT_BUTTON, bind (&KeysPage::signing_advanced, this));
+       remake_signing->Bind (wxEVT_BUTTON, bind(&KeysPage::remake_signing, this));
 }
 
+
+void
+KeysPage::remake_signing ()
+{
+       auto d = new MakeChainDialog (_panel, Config::instance()->signer_chain());
+
+       if (d->ShowModal () == wxID_OK) {
+               Config::instance()->set_signer_chain(d->get());
+       }
+}
+
+
 void
 KeysPage::decryption_advanced ()
 {
-       CertificateChainEditor* c = new CertificateChainEditor (
+       auto c = new CertificateChainEditor (
                _panel, _("Decrypting KDMs"), _border,
-               bind (&Config::set_decryption_chain, Config::instance (), _1),
-               bind (&Config::decryption_chain, Config::instance ()),
-               bind (&KeysPage::nag_remake_decryption_chain, this)
+               bind(&Config::set_decryption_chain, Config::instance(), _1),
+               bind(&Config::decryption_chain, Config::instance()),
+               bind(&KeysPage::nag_alter_decryption_chain, this)
                );
 
        c->ShowModal();
@@ -802,11 +711,11 @@ KeysPage::decryption_advanced ()
 void
 KeysPage::signing_advanced ()
 {
-       CertificateChainEditor* c = new CertificateChainEditor (
+       auto c = new CertificateChainEditor (
                _panel, _("Signing DCPs and KDMs"), _border,
-               bind (&Config::set_signer_chain, Config::instance (), _1),
-               bind (&Config::signer_chain, Config::instance ()),
-               bind (&do_nothing)
+               bind(&Config::set_signer_chain, Config::instance(), _1),
+               bind(&Config::signer_chain, Config::instance()),
+               bind(&do_nothing)
                );
 
        c->ShowModal();
@@ -815,27 +724,26 @@ KeysPage::signing_advanced ()
 void
 KeysPage::export_decryption_chain_and_key ()
 {
-       wxFileDialog* d = new wxFileDialog (
+       auto d = make_wx<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, false);
-               }
+       if (d->ShowModal() != wxID_OK) {
+               return;
+       }
 
-               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);
+       boost::filesystem::path path(wx_to_std(d->GetPath()));
+       dcp::File f(path, "w");
+       if (!f) {
+               throw OpenFileError(path, errno, OpenFileError::WRITE);
        }
-       d->Destroy ();
 
+       auto const chain = Config::instance()->decryption_chain()->chain();
+       f.checked_write(chain.c_str(), chain.length());
+       auto const key = Config::instance()->decryption_chain()->key();
+       DCPOMATIC_ASSERT(key);
+       f.checked_write(key->c_str(), key->length());
 }
 
 void
@@ -844,57 +752,56 @@ KeysPage::import_decryption_chain_and_key ()
        if (NagDialog::maybe_nag (
                    _panel,
                    Config::NAG_IMPORT_DECRYPTION_CHAIN,
-                   /* XXX: this needs to be marked translatable */
-                   wxT("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!"),
+                   _("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;
        }
 
-       wxFileDialog* d = new wxFileDialog (
+       auto d = make_wx<wxFileDialog>(
                _panel, _("Select File To Import"), wxEmptyString, wxEmptyString, wxT ("DOM files (*.dom)|*.dom")
                );
 
-       if (d->ShowModal () == wxID_OK) {
-               shared_ptr<dcp::CertificateChain> new_chain(new dcp::CertificateChain());
+       if (d->ShowModal() != wxID_OK) {
+               return;
+       }
 
-               FILE* f = fopen_boost (wx_to_std (d->GetPath ()), "r");
-               if (!f) {
-                       throw OpenFileError (wx_to_std (d->GetPath ()), errno, false);
-               }
+       auto new_chain = make_shared<dcp::CertificateChain>();
 
-               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);
+       dcp::File f(wx_to_std(d->GetPath()), "r");
+       if (!f) {
+               throw OpenFileError(f.path(), errno, OpenFileError::WRITE);
+       }
 
-               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"));
+       string current;
+       while (!f.eof()) {
+               char buffer[128];
+               if (f.gets(buffer, 128) == 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 = "";
+               }
+       }
+
+       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 ();
 }
 
 bool
-KeysPage::nag_remake_decryption_chain ()
+KeysPage::nag_alter_decryption_chain ()
 {
        return NagDialog::maybe_nag (
                _panel,
-               Config::NAG_REMAKE_DECRYPTION_CHAIN,
+               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
                );
@@ -903,22 +810,335 @@ KeysPage::nag_remake_decryption_chain ()
 void
 KeysPage::export_decryption_certificate ()
 {
-       wxFileDialog* d = new wxFileDialog (
-               _panel, _("Select Certificate File"), wxEmptyString, _("dcpomatic_kdm_decryption_cert.pem"), wxT ("PEM files (*.pem)|*.pem"),
+       auto config = Config::instance();
+       wxString default_name = "dcpomatic";
+       if (!config->dcp_creator().empty()) {
+               default_name += "_" + std_to_wx(careful_string_filter(config->dcp_creator()));
+       }
+       if (!config->dcp_issuer().empty()) {
+               default_name += "_" + std_to_wx(careful_string_filter(config->dcp_issuer()));
+       }
+       default_name += wxT("_kdm_decryption_cert.pem");
+
+       auto d = make_wx<wxFileDialog>(
+               _panel, _("Select Certificate File"), wxEmptyString, default_name, wxT("PEM files (*.pem)|*.pem"),
                wxFD_SAVE | wxFD_OVERWRITE_PROMPT
                );
 
-       if (d->ShowModal () == wxID_OK) {
-               boost::filesystem::path path (wx_to_std(d->GetPath()));
-               FILE* f = fopen_boost (path, "w");
-               if (!f) {
-                       throw OpenFileError (path, errno, false);
+       if (d->ShowModal() != wxID_OK) {
+               return;
+       }
+
+       boost::filesystem::path path(wx_to_std(d->GetPath()));
+       if (path.extension() != ".pem") {
+               path += ".pem";
+       }
+       dcp::File f(path, "w");
+       if (!f) {
+               throw OpenFileError(path, errno, OpenFileError::WRITE);
+       }
+
+       auto const s = Config::instance()->decryption_chain()->leaf().certificate (true);
+       f.checked_write(s.c_str(), s.length());
+}
+
+wxString
+SoundPage::GetName () const
+{
+       return _("Sound");
+}
+
+void
+SoundPage::setup ()
+{
+       auto table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
+       _panel->GetSizer()->Add (table, 1, wxALL | wxEXPAND, _border);
+
+       int r = 0;
+
+       _sound = new CheckBox (_panel, _("Play sound via"));
+       table->Add (_sound, wxGBPosition (r, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
+       wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
+       _sound_output = new wxChoice (_panel, wxID_ANY);
+       s->Add (_sound_output, 0);
+       _sound_output_details = new wxStaticText (_panel, wxID_ANY, wxT(""));
+       s->Add (_sound_output_details, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, DCPOMATIC_SIZER_X_GAP);
+       table->Add (s, wxGBPosition(r, 1));
+       ++r;
+
+       add_label_to_sizer (table, _panel, _("Mapping"), true, wxGBPosition(r, 0));
+       _map = new AudioMappingView (_panel, _("DCP"), _("DCP"), _("Output"), _("output"));
+       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);
+#if (RTAUDIO_VERSION_MAJOR >= 6)
+       for (auto device_id: audio.getDeviceIds()) {
+               auto dev = audio.getDeviceInfo(device_id);
+               if (dev.outputChannels > 0) {
+                       _sound_output->Append(std_to_wx(dev.name));
+               }
+       }
+#else
+       for (unsigned int i = 0; i < audio.getDeviceCount(); ++i) {
+               try {
+                       auto dev = audio.getDeviceInfo (i);
+                       if (dev.probed && dev.outputChannels > 0) {
+                               _sound_output->Append (std_to_wx (dev.name));
+                       }
+               } catch (RtAudioError&) {
+                       /* Something went wrong so let's just ignore that device */
+               }
+       }
+#endif
+
+       _sound->bind(&SoundPage::sound_changed, this);
+       _sound_output->Bind (wxEVT_CHOICE,   bind(&SoundPage::sound_output_changed, this));
+       _map->Changed.connect (bind(&SoundPage::map_changed, this, _1));
+       _reset_to_default->Bind (wxEVT_BUTTON,   bind(&SoundPage::reset_to_default, this));
+}
+
+void
+SoundPage::reset_to_default ()
+{
+       Config::instance()->set_audio_mapping_to_default ();
+}
+
+void
+SoundPage::map_changed (AudioMapping m)
+{
+       Config::instance()->set_audio_mapping (m);
+}
+
+void
+SoundPage::sound_changed ()
+{
+       Config::instance()->set_sound (_sound->GetValue ());
+}
+
+void
+SoundPage::sound_output_changed ()
+{
+       RtAudio audio (DCPOMATIC_RTAUDIO_API);
+       auto const so = get_sound_output();
+       string default_device;
+#if (RTAUDIO_VERSION_MAJOR >= 6)
+       default_device = audio.getDeviceInfo(audio.getDefaultOutputDevice()).name;
+#else
+       try {
+               default_device = audio.getDeviceInfo(audio.getDefaultOutputDevice()).name;
+       } catch (RtAudioError&) {
+               /* Never mind */
+       }
+#endif
+       if (!so || *so == default_device) {
+               Config::instance()->unset_sound_output ();
+       } else {
+               Config::instance()->set_sound_output (*so);
+       }
+}
+
+void
+SoundPage::config_changed ()
+{
+       auto config = Config::instance ();
+
+       checked_set (_sound, config->sound ());
+
+       auto const current_so = get_sound_output ();
+       optional<string> configured_so;
+
+       if (config->sound_output()) {
+               configured_so = config->sound_output().get();
+       } else {
+               /* No configured output means we should use the default */
+               RtAudio audio (DCPOMATIC_RTAUDIO_API);
+#if (RTAUDIO_VERSION_MAJOR >= 6)
+               configured_so = audio.getDeviceInfo(audio.getDefaultOutputDevice()).name;
+#else
+               try {
+                       configured_so = audio.getDeviceInfo(audio.getDefaultOutputDevice()).name;
+               } catch (RtAudioError&) {
+                       /* Probably no audio devices at all */
                }
+#endif
+       }
+
+       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;
+               }
+       }
+
+       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) {
+#if (RTAUDIO_VERSION_MAJOR >= 6)
+               for (auto device_id: audio.getDeviceIds()) {
+                       auto info = audio.getDeviceInfo(device_id);
+                       if (info.name == *configured_so && info.outputChannels > 0) {
+                               channels = info.outputChannels;
+                       }
+               }
+#else
+               for (unsigned int i = 0; i < audio.getDeviceCount(); ++i) {
+                       try {
+                               auto info = audio.getDeviceInfo(i);
+                               if (info.name == *configured_so && info.outputChannels > 0) {
+                                       channels = info.outputChannels;
+                               }
+                       } catch (RtAudioError&) {
+                               /* Never mind */
+                       }
+               }
+#endif
+       }
+
+       _sound_output_details->SetLabel (
+               wxString::Format(_("%d channels on %s"), channels, apis[audio.getCurrentApi()])
+               );
+
+       _map->set (Config::instance()->audio_mapping(channels));
+
+       vector<NamedChannel> input;
+       for (int i = 0; i < MAX_DCP_AUDIO_CHANNELS; ++i) {
+               input.push_back (NamedChannel(short_audio_channel_name(i), i));
+       }
+       _map->set_input_channels (input);
+
+       vector<NamedChannel> output;
+       for (int i = 0; i < channels; ++i) {
+               output.push_back (NamedChannel(dcp::raw_convert<string>(i), i));
+       }
+       _map->set_output_channels (output);
+
+       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> ();
+       }
 
-               string const s = Config::instance()->decryption_chain()->leaf().certificate (true);
-               checked_fwrite (s.c_str(), s.length(), f, path);
-               fclose (f);
+       return wx_to_std (_sound_output->GetString (sel));
+}
+
+
+LocationsPage::LocationsPage (wxSize panel_size, int border)
+       : Page (panel_size, border)
+{
+
+}
+
+wxString
+LocationsPage::GetName () const
+{
+       return _("Locations");
+}
+
+#ifdef DCPOMATIC_OSX
+wxBitmap
+LocationsPage::GetLargeIcon () const
+{
+       return wxBitmap(icon_path("locations"), wxBITMAP_TYPE_PNG);
+}
+#endif
+
+void
+LocationsPage::setup ()
+{
+       int r = 0;
+
+       auto table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
+       _panel->GetSizer()->Add (table, 1, wxALL | wxEXPAND, _border);
+
+       add_label_to_sizer (table, _panel, _("Content directory"), true, wxGBPosition (r, 0));
+       _content_directory = new wxDirPickerCtrl (_panel, wxID_ANY, wxEmptyString, wxDirSelectorPromptStr, wxDefaultPosition, wxSize (300, -1));
+       table->Add (_content_directory, wxGBPosition (r, 1));
+       ++r;
+
+       add_label_to_sizer (table, _panel, _("Playlist directory"), true, wxGBPosition (r, 0));
+       _playlist_directory = new wxDirPickerCtrl (_panel, wxID_ANY, wxEmptyString, wxDirSelectorPromptStr, wxDefaultPosition, wxSize (300, -1));
+       table->Add (_playlist_directory, wxGBPosition (r, 1));
+       ++r;
+
+       add_label_to_sizer (table, _panel, _("KDM directory"), true, wxGBPosition (r, 0));
+       _kdm_directory = new wxDirPickerCtrl (_panel, wxID_ANY, wxEmptyString, wxDirSelectorPromptStr, wxDefaultPosition, wxSize (300, -1));
+       table->Add (_kdm_directory, wxGBPosition (r, 1));
+       ++r;
+
+       _content_directory->Bind (wxEVT_DIRPICKER_CHANGED, bind(&LocationsPage::content_directory_changed, this));
+       _playlist_directory->Bind (wxEVT_DIRPICKER_CHANGED, bind(&LocationsPage::playlist_directory_changed, this));
+       _kdm_directory->Bind (wxEVT_DIRPICKER_CHANGED, bind(&LocationsPage::kdm_directory_changed, this));
+}
+
+void
+LocationsPage::config_changed ()
+{
+       auto config = Config::instance ();
+
+       if (config->player_content_directory()) {
+               checked_set (_content_directory, *config->player_content_directory());
+       }
+       if (config->player_playlist_directory()) {
+               checked_set (_playlist_directory, *config->player_playlist_directory());
+       }
+       if (config->player_kdm_directory()) {
+               checked_set (_kdm_directory, *config->player_kdm_directory());
        }
+}
+
+void
+LocationsPage::content_directory_changed ()
+{
+       Config::instance()->set_player_content_directory(wx_to_std(_content_directory->GetPath()));
+}
 
-       d->Destroy ();
+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()));
 }