Use wx_ptr more.
[dcpomatic.git] / src / tools / dcpomatic.cc
index 57fad5a0a49ac031ca42365feebe956fdad23493..4ac804cc9f3dbddfef6474f1491fbdfcc1d7e1d6 100644 (file)
@@ -53,6 +53,7 @@
 #include "wx/update_dialog.h"
 #include "wx/video_waveform_dialog.h"
 #include "wx/wx_signal_manager.h"
+#include "wx/wx_ptr.h"
 #include "wx/wx_util.h"
 #include "lib/analytics.h"
 #include "lib/audio_content.h"
@@ -60,6 +61,7 @@
 #include "lib/cinema.h"
 #include "lib/compose.hpp"
 #include "lib/config.h"
+#include "lib/constants.h"
 #include "lib/content.h"
 #include "lib/content_factory.h"
 #include "lib/cross.h"
@@ -86,7 +88,6 @@
 #include "lib/text_content.h"
 #include "lib/transcode_job.h"
 #include "lib/update_checker.h"
-#include "lib/util.h"
 #include "lib/version.h"
 #include "lib/video_content.h"
 #include <dcp/exceptions.h>
@@ -144,7 +145,7 @@ class FilmChangedClosingDialog
 public:
        explicit FilmChangedClosingDialog (string name)
        {
-               _dialog = new wxMessageDialog (
+               _dialog.reset(
                        nullptr,
                        wxString::Format(_("Save changes to film \"%s\" before closing?"), std_to_wx (name).data()),
                        /// TRANSLATORS: this is the heading for a dialog box, which tells the user that the current
@@ -158,21 +159,13 @@ public:
                        );
        }
 
-       ~FilmChangedClosingDialog ()
-       {
-               _dialog->Destroy ();
-       }
-
-       FilmChangedClosingDialog (FilmChangedClosingDialog const&) = delete;
-       FilmChangedClosingDialog& operator= (FilmChangedClosingDialog const&) = delete;
-
        int run ()
        {
                return _dialog->ShowModal ();
        }
 
 private:
-       wxMessageDialog* _dialog;
+       wx_ptr<wxMessageDialog> _dialog;
 };
 
 
@@ -181,7 +174,7 @@ class FilmChangedDuplicatingDialog
 public:
        explicit FilmChangedDuplicatingDialog (string name)
        {
-               _dialog = new wxMessageDialog (
+               _dialog.reset(
                        nullptr,
                        wxString::Format(_("Save changes to film \"%s\" before duplicating?"), std_to_wx (name).data()),
                        /// TRANSLATORS: this is the heading for a dialog box, which tells the user that the current
@@ -195,21 +188,13 @@ public:
                        );
        }
 
-       ~FilmChangedDuplicatingDialog ()
-       {
-               _dialog->Destroy ();
-       }
-
-       FilmChangedDuplicatingDialog (FilmChangedDuplicatingDialog const&) = delete;
-       FilmChangedDuplicatingDialog& operator= (FilmChangedDuplicatingDialog const&) = delete;
-
        int run ()
        {
                return _dialog->ShowModal ();
        }
 
 private:
-       wxMessageDialog* _dialog;
+       wx_ptr<wxMessageDialog> _dialog;
 };
 
 
@@ -270,11 +255,58 @@ enum {
 };
 
 
+class LimitedFrameSplitter : public wxSplitterWindow
+{
+public:
+       LimitedFrameSplitter(wxWindow* parent)
+               : wxSplitterWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxSP_NOBORDER | wxSP_3DSASH | wxSP_LIVE_UPDATE)
+       {
+               /* This value doesn't really mean much but we just want to stop double-click on the
+                  divider from shrinking the left panel.
+                  */
+               SetMinimumPaneSize(64);
+
+               Bind(wxEVT_SIZE, boost::bind(&LimitedFrameSplitter::sized, this, _1));
+       }
+
+       bool OnSashPositionChange(int new_position) override
+       {
+               /* Try to stop the left bit of the splitter getting too small */
+               auto const ok = new_position > _left_panel_minimum_size;
+               if (ok) {
+                       Config::instance()->set_main_divider_sash_position(new_position);
+               }
+               return ok;
+       }
+
+private:
+       void sized(wxSizeEvent& ev)
+       {
+               if (GetSize().GetWidth() > _left_panel_minimum_size && GetSashPosition() < _left_panel_minimum_size) {
+                       /* The window is now fairly big but the left panel is small; this happens when the DCP-o-matic window
+                        * is shrunk and then made larger again.  Try to set a sensible left panel size in this case.
+                        */
+                       SetSashPosition(Config::instance()->main_divider_sash_position().get_value_or(_left_panel_minimum_size));
+               }
+
+               ev.Skip();
+       }
+
+       int const _left_panel_minimum_size = 200;
+};
+
+
 class DOMFrame : public wxFrame
 {
 public:
        explicit DOMFrame (wxString const& title)
                : wxFrame (nullptr, -1, title)
+               /* Use a panel as the only child of the Frame so that we avoid
+                  the dark-grey background on Windows.
+               */
+               , _splitter(new LimitedFrameSplitter(this))
+               , _right_panel(new wxPanel(_splitter, wxID_ANY))
+               , _film_viewer(_right_panel)
        {
 #if defined(DCPOMATIC_WINDOWS)
                if (Config::instance()->win32_console()) {
@@ -348,24 +380,26 @@ public:
                Bind (wxEVT_CLOSE_WINDOW, boost::bind (&DOMFrame::close, this, _1));
                Bind (wxEVT_SHOW, boost::bind (&DOMFrame::show, this, _1));
 
-               /* Use a panel as the only child of the Frame so that we avoid
-                  the dark-grey background on Windows.
-               */
-               auto overall_panel = new wxPanel (this, wxID_ANY);
+               auto left_panel = new wxPanel(_splitter, wxID_ANY);
+
+               _film_editor = new FilmEditor(left_panel, _film_viewer);
+
+               auto left_sizer = new wxBoxSizer(wxHORIZONTAL);
+               left_sizer->Add(_film_editor, 1, wxEXPAND);
 
-               _film_viewer.reset (new FilmViewer (overall_panel));
-               _controls = new StandardControls (overall_panel, _film_viewer, true);
-               _film_editor = new FilmEditor (overall_panel, _film_viewer);
-               auto job_manager_view = new JobManagerView (overall_panel, false);
+               left_panel->SetSizerAndFit(left_sizer);
+
+               _controls = new StandardControls(_right_panel, _film_viewer, true);
+               auto job_manager_view = new JobManagerView(_right_panel, false);
 
                auto right_sizer = new wxBoxSizer (wxVERTICAL);
-               right_sizer->Add (_film_viewer->panel(), 2, wxEXPAND | wxALL, 6);
+               right_sizer->Add(_film_viewer.panel(), 2, wxEXPAND | wxALL, 6);
                right_sizer->Add (_controls, 0, wxEXPAND | wxALL, 6);
                right_sizer->Add (job_manager_view, 1, wxEXPAND | wxALL, 6);
 
-               wxBoxSizer* main_sizer = new wxBoxSizer (wxHORIZONTAL);
-               main_sizer->Add (_film_editor, 0, wxEXPAND | wxALL, 6);
-               main_sizer->Add (right_sizer, 1, wxEXPAND | wxALL, 6);
+               _right_panel->SetSizer(right_sizer);
+
+               _splitter->SplitVertically(left_panel, _right_panel, Config::instance()->main_divider_sash_position().get_value_or(left_panel->GetSize().GetWidth() + 8));
 
                set_menu_sensitivity ();
 
@@ -374,8 +408,6 @@ public:
 
                JobManager::instance()->ActiveJobsChanged.connect(boost::bind(&DOMFrame::active_jobs_changed, this));
 
-               overall_panel->SetSizer (main_sizer);
-
                UpdateChecker::instance()->StateChanged.connect(boost::bind(&DOMFrame::update_checker_state_changed, this));
 
                FocusManager::instance()->SetFocus.connect (boost::bind (&DOMFrame::remove_accelerators, this));
@@ -481,13 +513,10 @@ public:
        void set_film (shared_ptr<Film> film)
        {
                _film = film;
-               _film_viewer->set_film (_film);
-               _film_editor->set_film (_film);
+               _film_viewer.set_film(_film);
+               _film_editor->set_film(_film);
                _controls->set_film (_film);
-               if (_video_waveform_dialog) {
-                       _video_waveform_dialog->Destroy ();
-                       _video_waveform_dialog = nullptr;
-               }
+               _video_waveform_dialog.reset();
                set_menu_sensitivity ();
                if (_film && _film->directory()) {
                        Config::instance()->add_to_history (_film->directory().get());
@@ -529,42 +558,42 @@ private:
 
        void file_new ()
        {
-               auto d = new FilmNameLocationDialog (this, _("New Film"), true);
+               auto d = make_wx<FilmNameLocationDialog>(this, _("New Film"), true);
                int const r = d->ShowModal ();
 
-               if (r == wxID_OK && d->check_path() && maybe_save_then_delete_film<FilmChangedClosingDialog>()) {
-                       try {
-                               new_film (d->path(), d->template_name());
-                       } catch (boost::filesystem::filesystem_error& e) {
+               if (r != wxID_OK || !d->check_path() || !maybe_save_then_delete_film<FilmChangedClosingDialog>()) {
+                       return;
+               }
+
+               try {
+                       new_film (d->path(), d->template_name());
+               } catch (boost::filesystem::filesystem_error& e) {
 #ifdef DCPOMATIC_WINDOWS
-                               string bad_chars = "<>:\"/|?*";
-                               string const filename = d->path().filename().string();
-                               string found_bad_chars;
-                               for (size_t i = 0; i < bad_chars.length(); ++i) {
-                                       if (filename.find(bad_chars[i]) != string::npos && found_bad_chars.find(bad_chars[i]) == string::npos) {
-                                               found_bad_chars += bad_chars[i];
-                                       }
+                       string bad_chars = "<>:\"/|?*";
+                       string const filename = d->path().filename().string();
+                       string found_bad_chars;
+                       for (size_t i = 0; i < bad_chars.length(); ++i) {
+                               if (filename.find(bad_chars[i]) != string::npos && found_bad_chars.find(bad_chars[i]) == string::npos) {
+                                       found_bad_chars += bad_chars[i];
                                }
-                               wxString message = _("Could not create folder to store film.");
-                               message += "  ";
-                               if (!found_bad_chars.empty()) {
-                                       message += wxString::Format (_("Try removing the %s characters from your folder name."), std_to_wx(found_bad_chars).data());
-                               } else {
-                                       message += _("Please check that you do not have Windows controlled folder access enabled for DCP-o-matic.");
-                               }
-                               error_dialog (this, message, std_to_wx(e.what()));
+                       }
+                       wxString message = _("Could not create folder to store film.");
+                       message += "  ";
+                       if (!found_bad_chars.empty()) {
+                               message += wxString::Format (_("Try removing the %s characters from your folder name."), std_to_wx(found_bad_chars).data());
+                       } else {
+                               message += _("Please check that you do not have Windows controlled folder access enabled for DCP-o-matic.");
+                       }
+                       error_dialog (this, message, std_to_wx(e.what()));
 #else
-                               error_dialog (this, _("Could not create folder to store film."), std_to_wx(e.what()));
+                       error_dialog (this, _("Could not create folder to store film."), std_to_wx(e.what()));
 #endif
-                       }
                }
-
-               d->Destroy ();
        }
 
        void file_open ()
        {
-               auto c = new wxDirDialog (
+               auto c = make_wx<wxDirDialog>(
                        this,
                        _("Select film to open"),
                        std_to_wx (Config::instance()->default_directory_or (wx_to_std (wxStandardPaths::Get().GetDocumentsDir())).string ()),
@@ -584,8 +613,6 @@ private:
                if (r == wxID_OK && maybe_save_then_delete_film<FilmChangedClosingDialog>()) {
                        load_film (wx_to_std (c->GetPath ()));
                }
-
-               c->Destroy ();
        }
 
        void file_save ()
@@ -595,43 +622,35 @@ private:
 
        void file_save_as_template ()
        {
-               auto d = new SaveTemplateDialog (this);
-               int const r = d->ShowModal ();
-               if (r == wxID_OK) {
+               auto d = make_wx<SaveTemplateDialog>(this);
+               if (d->ShowModal() == wxID_OK) {
                        Config::instance()->save_template (_film, d->name ());
                }
-               d->Destroy ();
        }
 
        void file_duplicate ()
        {
-               auto d = new FilmNameLocationDialog (this, _("Duplicate Film"), false);
-               int const r = d->ShowModal ();
+               auto d = make_wx<FilmNameLocationDialog>(this, _("Duplicate Film"), false);
 
-               if (r == wxID_OK && d->check_path() && maybe_save_film<FilmChangedDuplicatingDialog>()) {
+               if (d->ShowModal() == wxID_OK && d->check_path() && maybe_save_film<FilmChangedDuplicatingDialog>()) {
                        shared_ptr<Film> film (new Film (d->path()));
                        film->copy_from (_film);
                        film->set_name (d->path().filename().generic_string());
                        film->write_metadata ();
                }
-
-               d->Destroy ();
        }
 
        void file_duplicate_and_open ()
        {
-               auto d = new FilmNameLocationDialog (this, _("Duplicate Film"), false);
-               int const r = d->ShowModal ();
+               auto d = make_wx<FilmNameLocationDialog>(this, _("Duplicate Film"), false);
 
-               if (r == wxID_OK && d->check_path() && maybe_save_film<FilmChangedDuplicatingDialog>()) {
-                       shared_ptr<Film> film (new Film (d->path()));
+               if (d->ShowModal() == wxID_OK && d->check_path() && maybe_save_film<FilmChangedDuplicatingDialog>()) {
+                       auto film = make_shared<Film>(d->path());
                        film->copy_from (_film);
                        film->set_name (d->path().filename().generic_string());
                        film->write_metadata ();
                        set_film (film);
                }
-
-               d->Destroy ();
        }
 
        void file_close ()
@@ -685,30 +704,31 @@ private:
        {
                DCPOMATIC_ASSERT (_clipboard);
 
-               auto d = new PasteDialog (this, static_cast<bool>(_clipboard->video), static_cast<bool>(_clipboard->audio), !_clipboard->text.empty());
-               if (d->ShowModal() == wxID_OK) {
-                       for (auto i: _film_editor->content_panel()->selected()) {
-                               if (d->video() && i->video) {
-                                       DCPOMATIC_ASSERT (_clipboard->video);
-                                       i->video->take_settings_from (_clipboard->video);
-                               }
-                               if (d->audio() && i->audio) {
-                                       DCPOMATIC_ASSERT (_clipboard->audio);
-                                       i->audio->take_settings_from (_clipboard->audio);
-                               }
+               auto d = make_wx<PasteDialog>(this, static_cast<bool>(_clipboard->video), static_cast<bool>(_clipboard->audio), !_clipboard->text.empty());
+               if (d->ShowModal() != wxID_OK) {
+                       return;
+               }
 
-                               if (d->text()) {
-                                       auto j = i->text.begin ();
-                                       auto k = _clipboard->text.begin ();
-                                       while (j != i->text.end() && k != _clipboard->text.end()) {
-                                               (*j)->take_settings_from (*k);
-                                               ++j;
-                                               ++k;
-                                       }
+               for (auto i: _film_editor->content_panel()->selected()) {
+                       if (d->video() && i->video) {
+                               DCPOMATIC_ASSERT (_clipboard->video);
+                               i->video->take_settings_from (_clipboard->video);
+                       }
+                       if (d->audio() && i->audio) {
+                               DCPOMATIC_ASSERT (_clipboard->audio);
+                               i->audio->take_settings_from (_clipboard->audio);
+                       }
+
+                       if (d->text()) {
+                               auto j = i->text.begin ();
+                               auto k = _clipboard->text.begin ();
+                               while (j != i->text.end() && k != _clipboard->text.end()) {
+                                       (*j)->take_settings_from (*k);
+                                       ++j;
+                                       ++k;
                                }
                        }
                }
-               d->Destroy ();
        }
 
        void edit_select_all ()
@@ -726,32 +746,26 @@ private:
 
        void tools_restore_default_preferences ()
        {
-               auto d = new wxMessageDialog (
-                       0,
+               auto d = make_wx<wxMessageDialog>(
+                       nullptr,
                        _("Are you sure you want to restore preferences to their defaults?  This cannot be undone."),
                        _("Restore default preferences"),
                        wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION
                        );
 
-               int const r = d->ShowModal ();
-               d->Destroy ();
-
-               if (r == wxID_YES) {
+               if (d->ShowModal() == wxID_YES) {
                        Config::restore_defaults ();
                }
        }
 
        void tools_export_preferences ()
        {
-               auto dialog = new wxFileDialog (
+               auto dialog = make_wx<wxFileDialog>(
                        this, _("Specify ZIP file"), wxEmptyString, wxT("dcpomatic_config.zip"), wxT("ZIP files (*.zip)|*.zip"),
                        wxFD_SAVE | wxFD_OVERWRITE_PROMPT
                        );
 
-               int const r = dialog->ShowModal ();
-               dialog->Destroy ();
-
-               if (r == wxID_OK) {
+               if (dialog->ShowModal() == wxID_OK) {
                        save_all_config_as_zip (wx_to_std(dialog->GetPath()));
                }
        }
@@ -775,10 +789,8 @@ private:
                }
 
                if (Config::instance()->show_hints_before_make_dcp()) {
-                       auto hints = new HintsDialog (this, _film, false);
-                       int const r = hints->ShowModal();
-                       hints->Destroy ();
-                       if (r == wxID_CANCEL) {
+                       auto hints = make_wx<HintsDialog>(this, _film, false);
+                       if (hints->ShowModal() == wxID_CANCEL) {
                                return;
                        }
                }
@@ -822,12 +834,7 @@ private:
                        return;
                }
 
-               if (_kdm_dialog) {
-                       _kdm_dialog->Destroy ();
-                       _kdm_dialog = 0;
-               }
-
-               _kdm_dialog = new KDMDialog (this, _film);
+               _kdm_dialog.reset(this, _film);
                _kdm_dialog->Show ();
        }
 
@@ -837,12 +844,7 @@ private:
                        return;
                }
 
-               if (_dkdm_dialog) {
-                       _dkdm_dialog->Destroy ();
-                       _dkdm_dialog = nullptr;
-               }
-
-               _dkdm_dialog = new DKDMDialog (this, _film);
+               _dkdm_dialog.reset(this, _film);
                _dkdm_dialog->Show ();
        }
 
@@ -889,10 +891,8 @@ private:
                }
 
                if (Config::instance()->show_hints_before_make_dcp()) {
-                       auto hints = new HintsDialog (this, _film, false);
-                       int const r = hints->ShowModal();
-                       hints->Destroy ();
-                       if (r == wxID_CANCEL) {
+                       auto hints = make_wx<HintsDialog>(this, _film, false);
+                       if (hints->ShowModal() == wxID_CANCEL) {
                                return;
                        }
                }
@@ -929,9 +929,8 @@ private:
                        return;
                }
 
-               auto d = new SelfDKDMDialog (this, _film);
+               auto d = make_wx<SelfDKDMDialog>(this, _film);
                if (d->ShowModal () != wxID_OK) {
-                       d->Destroy ();
                        return;
                }
 
@@ -981,49 +980,47 @@ private:
                                kdm->as_xml (path);
                        }
                }
-
-               d->Destroy ();
        }
 
 
        void jobs_export_video_file ()
        {
-               auto d = new ExportVideoFileDialog (this, _film->isdcf_name(true));
-               if (d->ShowModal() == wxID_OK) {
-                       if (boost::filesystem::exists(d->path())) {
-                               bool ok = confirm_dialog(
-                                               this,
-                                               wxString::Format (_("File %s already exists.  Do you want to overwrite it?"), std_to_wx(d->path().string()).data())
-                                               );
-
-                               if (!ok) {
-                                       d->Destroy ();
-                                       return;
-                               }
-                       }
+               auto d = make_wx<ExportVideoFileDialog>(this, _film->isdcf_name(true));
+               if (d->ShowModal() != wxID_OK) {
+                       return;
+               }
 
-                       auto job = make_shared<TranscodeJob>(_film, TranscodeJob::ChangedBehaviour::EXAMINE_THEN_STOP);
-                       job->set_encoder (
-                               make_shared<FFmpegEncoder> (
-                                       _film, job, d->path(), d->format(), d->mixdown_to_stereo(), d->split_reels(), d->split_streams(), d->x264_crf())
-                               );
-                       JobManager::instance()->add (job);
+               if (boost::filesystem::exists(d->path())) {
+                       bool ok = confirm_dialog(
+                                       this,
+                                       wxString::Format (_("File %s already exists.  Do you want to overwrite it?"), std_to_wx(d->path().string()).data())
+                                       );
+
+                       if (!ok) {
+                               return;
+                       }
                }
-               d->Destroy ();
+
+               auto job = make_shared<TranscodeJob>(_film, TranscodeJob::ChangedBehaviour::EXAMINE_THEN_STOP);
+               job->set_encoder (
+                       make_shared<FFmpegEncoder> (
+                               _film, job, d->path(), d->format(), d->mixdown_to_stereo(), d->split_reels(), d->split_streams(), d->x264_crf())
+                       );
+               JobManager::instance()->add (job);
        }
 
 
        void jobs_export_subtitles ()
        {
-               auto d = new ExportSubtitlesDialog (this, _film->reels().size(), _film->interop());
-               if (d->ShowModal() == wxID_OK) {
-                       auto job = make_shared<TranscodeJob>(_film, TranscodeJob::ChangedBehaviour::EXAMINE_THEN_STOP);
-                       job->set_encoder (
-                               make_shared<SubtitleEncoder>(_film, job, d->path(), _film->isdcf_name(true), d->split_reels(), d->include_font())
-                               );
-                       JobManager::instance()->add (job);
+               auto d = make_wx<ExportSubtitlesDialog>(this, _film->reels().size(), _film->interop());
+               if (d->ShowModal() != wxID_OK) {
+                       return;
                }
-               d->Destroy ();
+               auto job = make_shared<TranscodeJob>(_film, TranscodeJob::ChangedBehaviour::EXAMINE_THEN_STOP);
+               job->set_encoder(
+                       make_shared<SubtitleEncoder>(_film, job, d->path(), _film->isdcf_name(true), d->split_reels(), d->include_font())
+                       );
+               JobManager::instance()->add(job);
        }
 
 
@@ -1042,13 +1039,13 @@ private:
 
        void view_closed_captions ()
        {
-               _film_viewer->show_closed_captions ();
+               _film_viewer.show_closed_captions ();
        }
 
        void view_video_waveform ()
        {
                if (!_video_waveform_dialog) {
-                       _video_waveform_dialog = new VideoWaveformDialog (this, _film, _film_viewer);
+                       _video_waveform_dialog.reset(this, _film, _film_viewer);
                }
 
                _video_waveform_dialog->Show ();
@@ -1084,7 +1081,7 @@ private:
        void tools_manage_templates ()
        {
                if (!_templates_dialog) {
-                       _templates_dialog = new TemplatesDialog (this);
+                       _templates_dialog.reset(this);
                }
 
                _templates_dialog->Show ();
@@ -1098,48 +1095,46 @@ private:
 
        void tools_send_translations ()
        {
-               auto d = new SendI18NDialog (this);
-               if (d->ShowModal() == wxID_OK) {
-                       string body;
-                       body += d->name() + "\n";
-                       body += d->language() + "\n";
-                       body += string(dcpomatic_version) + " " + string(dcpomatic_git_commit) + "\n";
-                       body += "--\n";
-                       auto translations = I18NHook::translations ();
-                       for (auto i: translations) {
-                               body += i.first + "\n" + i.second + "\n\n";
-                       }
-                       list<string> to = { "carl@dcpomatic.com" };
-                       if (d->email().find("@") == string::npos) {
-                               error_dialog (this, _("You must enter a valid email address when sending translations, "
-                                                     "otherwise the DCP-o-matic maintainers cannot credit you or contact you with questions."));
-                       } else {
-                               Emailer emailer (d->email(), to, "DCP-o-matic translations", body);
-                               try {
-                                       emailer.send ("main.carlh.net", 2525, EmailProtocol::STARTTLS);
-                               } catch (NetworkError& e) {
-                                       error_dialog (this, _("Could not send translations"), std_to_wx(e.what()));
-                               }
-                       }
+               auto d = make_wx<SendI18NDialog>(this);
+               if (d->ShowModal() != wxID_OK) {
+                       return;
                }
 
-               d->Destroy ();
+               string body;
+               body += d->name() + "\n";
+               body += d->language() + "\n";
+               body += string(dcpomatic_version) + " " + string(dcpomatic_git_commit) + "\n";
+               body += "--\n";
+               auto translations = I18NHook::translations ();
+               for (auto i: translations) {
+                       body += i.first + "\n" + i.second + "\n\n";
+               }
+               list<string> to = { "carl@dcpomatic.com" };
+               if (d->email().find("@") == string::npos) {
+                       error_dialog (this, _("You must enter a valid email address when sending translations, "
+                                             "otherwise the DCP-o-matic maintainers cannot credit you or contact you with questions."));
+               } else {
+                       Emailer emailer (d->email(), to, "DCP-o-matic translations", body);
+                       try {
+                               emailer.send ("main.carlh.net", 2525, EmailProtocol::STARTTLS);
+                       } catch (NetworkError& e) {
+                               error_dialog (this, _("Could not send translations"), std_to_wx(e.what()));
+                       }
+               }
        }
 
        void help_about ()
        {
-               auto d = new AboutDialog (this);
+               auto d = make_wx<AboutDialog>(this);
                d->ShowModal ();
-               d->Destroy ();
        }
 
        void help_report_a_problem ()
        {
-               auto d = new ReportProblemDialog (this, _film);
+               auto d = make_wx<ReportProblemDialog>(this, _film);
                if (d->ShowModal () == wxID_OK) {
                        d->report ();
                }
-               d->Destroy ();
        }
 
        bool should_close ()
@@ -1148,16 +1143,14 @@ private:
                        return true;
                }
 
-               auto d = new wxMessageDialog (
-                       0,
+               auto d = make_wx<wxMessageDialog>(
+                       nullptr,
                        _("There are unfinished jobs; are you sure you want to quit?"),
                        _("Unfinished jobs"),
                        wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION
                        );
 
-               bool const r = d->ShowModal() == wxID_YES;
-               d->Destroy ();
-               return r;
+               return d->ShowModal() == wxID_YES;
        }
 
        void close (wxCloseEvent& ev)
@@ -1492,9 +1485,8 @@ private:
                }
 
                if (uc->state() == UpdateChecker::State::YES) {
-                       auto dialog = new UpdateDialog (this, uc->stable(), uc->test());
+                       auto dialog = make_wx<UpdateDialog>(this, uc->stable(), uc->test());
                        dialog->ShowModal ();
-                       dialog->Destroy ();
                } else if (uc->state() == UpdateChecker::State::FAILED) {
                        error_dialog (this, _("The DCP-o-matic download server could not be contacted."));
                } else {
@@ -1506,10 +1498,10 @@ private:
 
        void start_stop_pressed ()
        {
-               if (_film_viewer->playing()) {
-                       _film_viewer->stop();
+               if (_film_viewer.playing()) {
+                       _film_viewer.stop();
                } else {
-                       _film_viewer->start();
+                       _film_viewer.start();
                }
        }
 
@@ -1520,19 +1512,18 @@ private:
 
        void back_frame ()
        {
-               _film_viewer->seek_by (-_film_viewer->one_video_frame(), true);
+               _film_viewer.seek_by(-_film_viewer.one_video_frame(), true);
        }
 
        void forward_frame ()
        {
-               _film_viewer->seek_by (_film_viewer->one_video_frame(), true);
+               _film_viewer.seek_by(_film_viewer.one_video_frame(), true);
        }
 
        void analytics_message (string title, string html)
        {
-               auto d = new HTMLDialog(this, std_to_wx(title), std_to_wx(html));
+               auto d = make_wx<HTMLDialog>(this, std_to_wx(title), std_to_wx(html));
                d->ShowModal();
-               d->Destroy();
        }
 
        void set_title ()
@@ -1551,16 +1542,18 @@ private:
        }
 
        FilmEditor* _film_editor;
-       std::shared_ptr<FilmViewer> _film_viewer;
+       LimitedFrameSplitter* _splitter;
+       wxPanel* _right_panel;
+       FilmViewer _film_viewer;
        StandardControls* _controls;
-       VideoWaveformDialog* _video_waveform_dialog = nullptr;
+       wx_ptr<VideoWaveformDialog> _video_waveform_dialog;
        SystemInformationDialog* _system_information_dialog = nullptr;
        HintsDialog* _hints_dialog = nullptr;
        ServersListDialog* _servers_list_dialog = nullptr;
        wxPreferencesEditor* _config_dialog = nullptr;
-       KDMDialog* _kdm_dialog = nullptr;
-       DKDMDialog* _dkdm_dialog = nullptr;
-       TemplatesDialog* _templates_dialog = nullptr;
+       wx_ptr<KDMDialog> _kdm_dialog;
+       wx_ptr<DKDMDialog> _dkdm_dialog;
+       wx_ptr<TemplatesDialog> _templates_dialog;
        wxMenu* _file_menu = nullptr;
        shared_ptr<Film> _film;
        int _history_items = 0;
@@ -1606,7 +1599,7 @@ private:
                try {
                        wxInitAllImageHandlers ();
 
-                       Config::FailedToLoad.connect (boost::bind (&App::config_failed_to_load, this));
+                       Config::FailedToLoad.connect(boost::bind(&App::config_failed_to_load, this, _1));
                        Config::Warning.connect (boost::bind (&App::config_warning, this, _1));
 
                        _splash = maybe_show_splash ();
@@ -1697,20 +1690,16 @@ private:
                                UpdateChecker::instance()->run ();
                        }
 
-                       auto release_notes = find_release_notes();
+                       auto release_notes = find_release_notes(gui_is_dark());
                        if (release_notes) {
-                               auto notes = new HTMLDialog(nullptr, _("Release notes"), std_to_wx(*release_notes), true);
+                               auto notes = make_wx<HTMLDialog>(nullptr, _("Release notes"), std_to_wx(*release_notes), true);
                                notes->Centre();
                                notes->ShowModal();
-                               notes->Destroy();
                        }
                }
                catch (exception& e)
                {
-                       if (_splash) {
-                               _splash->Destroy ();
-                               _splash = nullptr;
-                       }
+                       _splash.reset();
                        error_dialog (nullptr, wxString::Format ("DCP-o-matic could not start."), std_to_wx(e.what()));
                }
 
@@ -1822,15 +1811,12 @@ private:
 
        void close_splash ()
        {
-               if (_splash) {
-                       _splash->Destroy ();
-                       _splash = 0;
-               }
+               _splash.reset();
        }
 
-       void config_failed_to_load ()
+       void config_failed_to_load (Config::LoadFailure what)
        {
-               message_dialog (_frame, _("The existing configuration failed to load.  Default values will be used instead.  These may take a short time to create."));
+               report_config_load_failure(_frame, what);
        }
 
        void config_warning (string m)
@@ -1843,8 +1829,7 @@ private:
                /* Destroy the splash screen here, as otherwise bad things seem to happen (for reasons unknown)
                   when we open our recreate dialog, close it, *then* try to Destroy the splash (the Destroy fails).
                */
-               _splash->Destroy ();
-               _splash = nullptr;
+               _splash.reset();
 
                auto config = Config::instance();
                switch (reason) {
@@ -1853,7 +1838,7 @@ private:
                        if (config->nagged(Config::NAG_BAD_SIGNER_CHAIN_UTF8)) {
                                return false;
                        }
-                       auto d = new RecreateChainDialog (
+                       auto d = make_wx<RecreateChainDialog>(
                                _frame, _("Recreate signing certificates"),
                                _("The certificate chain that DCP-o-matic uses for signing DCPs and KDMs contains a small error\n"
                                  "which will prevent DCPs from being validated correctly on some systems.  Do you want to re-create\n"
@@ -1861,16 +1846,14 @@ private:
                                _("Do nothing"),
                                Config::NAG_BAD_SIGNER_CHAIN_UTF8
                                );
-                       int const r = d->ShowModal ();
-                       d->Destroy ();
-                       return r == wxID_OK;
+                       return d->ShowModal() == wxID_OK;
                }
                case Config::BAD_SIGNER_VALIDITY_TOO_LONG:
                {
                        if (config->nagged(Config::NAG_BAD_SIGNER_CHAIN_VALIDITY)) {
                                return false;
                        }
-                       auto d = new RecreateChainDialog (
+                       auto d = make_wx<RecreateChainDialog>(
                                _frame, _("Recreate signing certificates"),
                                _("The certificate chain that DCP-o-matic uses for signing DCPs and KDMs has a validity period\n"
                                  "that is too long.  This will cause problems playing back DCPs on some systems.\n"
@@ -1878,29 +1861,25 @@ private:
                                _("Do nothing"),
                                Config::NAG_BAD_SIGNER_CHAIN_VALIDITY
                                );
-                       int const r = d->ShowModal ();
-                       d->Destroy ();
-                       return r == wxID_OK;
+                       return d->ShowModal() == wxID_OK;
                }
                case Config::BAD_SIGNER_INCONSISTENT:
                {
-                       auto d = new RecreateChainDialog (
+                       auto d = make_wx<RecreateChainDialog>(
                                _frame, _("Recreate signing certificates"),
                                _("The certificate chain that DCP-o-matic uses for signing DCPs and KDMs is inconsistent and\n"
                                  "cannot be used.  DCP-o-matic cannot start unless you re-create it.  Do you want to re-create\n"
                                  "the certificate chain for signing DCPs and KDMs?"),
                                _("Close DCP-o-matic")
                                );
-                       int const r = d->ShowModal ();
-                       d->Destroy ();
-                       if (r != wxID_OK) {
+                       if (d->ShowModal() != wxID_OK) {
                                exit (EXIT_FAILURE);
                        }
                        return true;
                }
                case Config::BAD_DECRYPTION_INCONSISTENT:
                {
-                       auto d = new RecreateChainDialog (
+                       auto d = make_wx<RecreateChainDialog>(
                                _frame, _("Recreate KDM decryption chain"),
                                _("The certificate chain that DCP-o-matic uses for decrypting KDMs is inconsistent and\n"
                                  "cannot be used.  DCP-o-matic cannot start unless you re-create it.  Do you want to re-create\n"
@@ -1908,9 +1887,7 @@ private:
                                  "configuration before continuing."),
                                _("Close DCP-o-matic")
                                );
-                       int const r = d->ShowModal ();
-                       d->Destroy ();
-                       if (r != wxID_OK) {
+                       if (d->ShowModal() != wxID_OK) {
                                exit (EXIT_FAILURE);
                        }
                        return true;
@@ -1921,7 +1898,7 @@ private:
        }
 
        DOMFrame* _frame = nullptr;
-       wxSplashScreen* _splash = nullptr;
+       wx_ptr<wxSplashScreen> _splash;
        shared_ptr<wxTimer> _timer;
        string _film_to_load;
        string _film_to_create;