Add option to load prefs from ZIP (#2771).
[dcpomatic.git] / src / tools / dcpomatic.cc
index 82da556980979bcede966283370f35a900af68f1..459ce341c3d026254522d36afa3b47437dd36b17 100644 (file)
@@ -36,7 +36,9 @@
 #include "wx/full_config_dialog.h"
 #include "wx/hints_dialog.h"
 #include "wx/html_dialog.h"
+#include "wx/file_dialog.h"
 #include "wx/i18n_hook.h"
+#include "wx/id.h"
 #include "wx/job_manager_view.h"
 #include "wx/kdm_dialog.h"
 #include "wx/nag_dialog.h"
@@ -74,6 +76,7 @@
 #include "lib/exceptions.h"
 #include "lib/ffmpeg_encoder.h"
 #include "lib/film.h"
+#include "lib/font_config.h"
 #include "lib/hints.h"
 #include "lib/job_manager.h"
 #include "lib/kdm_with_metadata.h"
@@ -90,6 +93,7 @@
 #include "lib/version.h"
 #include "lib/video_content.h"
 #include <dcp/exceptions.h>
+#include <dcp/filesystem.h>
 #include <dcp/raw_convert.h>
 #include <dcp/warnings.h>
 LIBDCP_DISABLE_WARNINGS
@@ -209,7 +213,7 @@ private:
 map<wxMenuItem*, int> menu_items;
 
 enum {
-       ID_file_new = 1,
+       ID_file_new = DCPOMATIC_MAIN_MENU,
        ID_file_open,
        ID_file_save,
        ID_file_save_as_template,
@@ -217,7 +221,7 @@ enum {
        ID_file_duplicate_and_open,
        ID_file_history,
        /* Allow spare IDs after _history for the recent files list */
-       ID_file_close = 100,
+       ID_file_close = DCPOMATIC_MAIN_MENU + 100,
        ID_edit_copy,
        ID_edit_paste,
        ID_edit_select_all,
@@ -241,6 +245,7 @@ enum {
        ID_tools_system_information,
        ID_tools_restore_default_preferences,
        ID_tools_export_preferences,
+       ID_tools_import_preferences,
        ID_help_report_a_problem,
        /* IDs for shortcuts (with no associated menu item) */
        ID_add_file,
@@ -305,25 +310,6 @@ public:
                , _right_panel(new wxPanel(_splitter, wxID_ANY))
                , _film_viewer(_right_panel)
        {
-#if defined(DCPOMATIC_WINDOWS)
-               if (Config::instance()->win32_console()) {
-                       AllocConsole();
-
-                       HANDLE handle_out = GetStdHandle(STD_OUTPUT_HANDLE);
-                       int hCrt = _open_osfhandle((intptr_t) handle_out, _O_TEXT);
-                       FILE* hf_out = _fdopen(hCrt, "w");
-                       setvbuf(hf_out, NULL, _IONBF, 1);
-                       *stdout = *hf_out;
-
-                       HANDLE handle_in = GetStdHandle(STD_INPUT_HANDLE);
-                       hCrt = _open_osfhandle((intptr_t) handle_in, _O_TEXT);
-                       FILE* hf_in = _fdopen(hCrt, "r");
-                       setvbuf(hf_in, NULL, _IONBF, 128);
-                       *stdin = *hf_in;
-
-                       cout << "DCP-o-matic is starting." << "\n";
-               }
-#endif
 
                auto bar = new wxMenuBar;
                setup_menu (bar);
@@ -371,6 +357,7 @@ public:
                Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_system_information, this),ID_tools_system_information);
                Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_restore_default_preferences, this), ID_tools_restore_default_preferences);
                Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_export_preferences, this), ID_tools_export_preferences);
+               Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_import_preferences, this), ID_tools_import_preferences);
                Bind (wxEVT_MENU, boost::bind (&DOMFrame::help_about, this),              wxID_ABOUT);
                Bind (wxEVT_MENU, boost::bind (&DOMFrame::help_report_a_problem, this),   ID_help_report_a_problem);
 
@@ -387,6 +374,7 @@ public:
                left_panel->SetSizerAndFit(left_sizer);
 
                _controls = new StandardControls(_right_panel, _film_viewer, true);
+               _controls->set_film(_film_viewer.film());
                auto job_manager_view = new JobManagerView(_right_panel, false);
 
                auto right_sizer = new wxBoxSizer (wxVERTICAL);
@@ -492,7 +480,7 @@ public:
        }
        catch (FileNotFoundError& e) {
                auto const dir = e.file().parent_path();
-               if (boost::filesystem::exists(dir / "ASSETMAP") || boost::filesystem::exists(dir / "ASSETMAP.xml")) {
+               if (dcp::filesystem::exists(dir / "ASSETMAP") || dcp::filesystem::exists(dir / "ASSETMAP.xml")) {
                        error_dialog (
                                this, _("Could not open this folder as a DCP-o-matic project."),
                                _("It looks like you are trying to open a DCP.  File -> Open is for loading DCP-o-matic projects, not DCPs.  To import a DCP, create a new project with File -> New and then click the \"Add DCP...\" button.")
@@ -567,7 +555,7 @@ private:
                } catch (boost::filesystem::filesystem_error& e) {
 #ifdef DCPOMATIC_WINDOWS
                        string bad_chars = "<>:\"/|?*";
-                       string const filename = d->path().filename().string();
+                       string const filename = dialog.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) {
@@ -614,14 +602,22 @@ private:
 
        void file_save ()
        {
-               _film->write_metadata ();
+               try {
+                       _film->write_metadata ();
+               } catch (exception& e) {
+                       error_dialog(this, _("Could not save project."), std_to_wx(e.what()));
+               }
        }
 
        void file_save_as_template ()
        {
                SaveTemplateDialog dialog(this);
                if (dialog.ShowModal() == wxID_OK) {
-                       Config::instance()->save_template(_film, dialog.name());
+                       try {
+                               Config::instance()->save_template(_film, dialog.name());
+                       } catch (exception& e) {
+                               error_dialog(this, _("Could not save template."), std_to_wx(e.what()));
+                       }
                }
        }
 
@@ -633,7 +629,11 @@ private:
                        auto film = make_shared<Film>(dialog.path());
                        film->copy_from (_film);
                        film->set_name(dialog.path().filename().generic_string());
-                       film->write_metadata ();
+                       try {
+                               film->write_metadata();
+                       } catch (exception& e) {
+                               error_dialog(this, _("Could not duplicate project."), std_to_wx(e.what()));
+                       }
                }
        }
 
@@ -645,8 +645,12 @@ private:
                        auto film = make_shared<Film>(dialog.path());
                        film->copy_from (_film);
                        film->set_name(dialog.path().filename().generic_string());
-                       film->write_metadata ();
-                       set_film (film);
+                       try {
+                               film->write_metadata ();
+                               set_film (film);
+                       } catch (exception& e) {
+                               error_dialog(this, _("Could not duplicate project."), std_to_wx(e.what()));
+                       }
                }
        }
 
@@ -689,13 +693,16 @@ private:
        void edit_copy ()
        {
                auto const sel = _film_editor->content_panel()->selected();
-               DCPOMATIC_ASSERT (sel.size() == 1);
-               _clipboard = sel.front()->clone();
+               if (sel.size() == 1) {
+                       _clipboard = sel.front()->clone();
+               }
        }
 
        void edit_paste ()
        {
-               DCPOMATIC_ASSERT (_clipboard);
+               if (!_clipboard) {
+                       return;
+               }
 
                PasteDialog dialog(this, static_cast<bool>(_clipboard->video), static_cast<bool>(_clipboard->audio), !_clipboard->text.empty());
                if (dialog.ShowModal() != wxID_OK) {
@@ -753,13 +760,31 @@ private:
 
        void tools_export_preferences ()
        {
-               wxFileDialog dialog(
-                       this, _("Specify ZIP file"), wxEmptyString, wxT("dcpomatic_config.zip"), wxT("ZIP files (*.zip)|*.zip"),
-                       wxFD_SAVE | wxFD_OVERWRITE_PROMPT
+               FileDialog dialog(
+                       this, _("Specify ZIP file"), wxT("ZIP files (*.zip)|*.zip"), wxFD_SAVE | wxFD_OVERWRITE_PROMPT, "Preferences", string("dcpomatic_config.zip")
                        );
 
                if (dialog.ShowModal() == wxID_OK) {
-                       save_all_config_as_zip(wx_to_std(dialog.GetPath()));
+                       auto const path = boost::filesystem::path(wx_to_std(dialog.GetPath()));
+                       if (boost::filesystem::exists(path)) {
+                               boost::system::error_code ec;
+                               boost::filesystem::remove(path, ec);
+                               if (ec) {
+                                       error_dialog(nullptr, _("Could not remove existing preferences file"), std_to_wx(path.string()));
+                                       return;
+                               }
+                       }
+
+                       save_all_config_as_zip(path);
+               }
+       }
+
+       void tools_import_preferences()
+       {
+               FileDialog dialog(this, _("Specify ZIP file"), wxT("ZIP files (*.zip)|*.zip"), wxFD_OPEN, "Preferences");
+
+               if (dialog.show()) {
+                       Config::instance()->load_from_zip(dialog.path());
                }
        }
 
@@ -801,11 +826,11 @@ private:
 
                /* Remove any existing DCP if the user agrees */
                auto const dcp_dir = _film->dir (_film->dcp_name(), false);
-               if (boost::filesystem::exists(dcp_dir)) {
+               if (dcp::filesystem::exists(dcp_dir)) {
                        if (!confirm_dialog (this, wxString::Format (_("Do you want to overwrite the existing DCP %s?"), std_to_wx(dcp_dir.string()).data()))) {
                                return;
                        }
-                       boost::filesystem::remove_all (dcp_dir);
+                       dcp::filesystem::remove_all(dcp_dir);
                }
 
                try {
@@ -944,34 +969,30 @@ private:
                dcp::LocalTime to (Config::instance()->signer_chain()->leaf().not_after());
                to.add_days (-1);
 
-               optional<dcp::EncryptedKDM> kdm;
-               try {
-                       kdm = _film->make_kdm (
-                               Config::instance()->decryption_chain()->leaf(),
-                               vector<string>(),
-                               dialog.cpl(),
-                               from, to,
-                               dcp::Formulation::MODIFIED_TRANSITIONAL_1,
-                               true,
-                               0
-                               );
-               } catch (dcp::NotEncryptedError& e) {
-                       error_dialog (this, _("CPL's content is not encrypted."));
-               } catch (exception& e) {
-                       error_dialog (this, e.what ());
-               } catch (...) {
-                       error_dialog (this, _("An unknown exception occurred."));
+               auto signer = Config::instance()->signer_chain();
+               if (!signer->valid()) {
+                       error_dialog(this, _("The certificate chain for signing is invalid"));
+                       return;
                }
 
-               if (kdm) {
+               optional<dcp::EncryptedKDM> kdm;
+               try {
+                       auto const decrypted_kdm = _film->make_kdm(dialog.cpl(), from, to);
+                       auto const kdm = decrypted_kdm.encrypt(signer, Config::instance()->decryption_chain()->leaf(), {}, dcp::Formulation::MODIFIED_TRANSITIONAL_1, true, 0);
                        if (dialog.internal()) {
                                auto dkdms = Config::instance()->dkdms();
-                               dkdms->add (make_shared<DKDM>(kdm.get()));
+                               dkdms->add(make_shared<DKDM>(kdm));
                                Config::instance()->changed ();
                        } else {
                                auto path = dialog.directory() / (_film->dcp_name(false) + "_DKDM.xml");
-                               kdm->as_xml (path);
+                               kdm.as_xml(path);
                        }
+               } catch (dcp::NotEncryptedError& e) {
+                       error_dialog (this, _("CPL's content is not encrypted."));
+               } catch (exception& e) {
+                       error_dialog (this, e.what ());
+               } catch (...) {
+                       error_dialog (this, _("An unknown exception occurred."));
                }
        }
 
@@ -983,7 +1004,7 @@ private:
                        return;
                }
 
-               if (boost::filesystem::exists(dialog.path())) {
+               if (dcp::filesystem::exists(dialog.path())) {
                        bool ok = confirm_dialog(
                                        this,
                                        wxString::Format(_("File %s already exists.  Do you want to overwrite it?"), std_to_wx(dialog.path().string()).data())
@@ -1102,12 +1123,11 @@ private:
                for (auto i: translations) {
                        body += i.first + "\n" + i.second + "\n\n";
                }
-               list<string> to = { "carl@dcpomatic.com" };
                if (dialog.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(dialog.email(), to, "DCP-o-matic translations", body);
+                       Emailer emailer(dialog.email(), { "carl@dcpomatic.com" }, "DCP-o-matic translations", body);
                        try {
                                emailer.send ("main.carlh.net", 2525, EmailProtocol::STARTTLS);
                        } catch (NetworkError& e) {
@@ -1179,6 +1199,8 @@ private:
                /* Also stop hearing about analytics-related stuff */
                _analytics_message_connection.disconnect ();
 
+               FontConfig::drop();
+
                ev.Skip ();
        }
 
@@ -1254,14 +1276,20 @@ private:
                        return true;
                }
 
-               if (_film->dirty ()) {
-                       T d (_film->name ());
-                       switch (d.run ()) {
+               while (_film->dirty()) {
+                       T dialog(_film->name());
+                       switch (dialog.run()) {
                        case wxID_NO:
                                return true;
                        case wxID_YES:
-                               _film->write_metadata ();
-                               return true;
+                               try {
+                                       _film->write_metadata();
+                                       return true;
+                               } catch (exception& e) {
+                                       error_dialog(this, _("Could not save project."), std_to_wx(e.what()));
+                                       /* Go round again for another try */
+                               }
+                               break;
                        case wxID_CANCEL:
                                return false;
                        }
@@ -1327,7 +1355,7 @@ private:
                add_item (edit, _("Select all\tShift-Ctrl-A"), ID_edit_select_all, NEEDS_FILM);
 
 #ifdef __WXOSX__
-               add_item (_file_menu, _("&Preferences...\tCtrl-P"), wxID_PREFERENCES, ALWAYS);
+               add_item(_file_menu, _("&Preferences...\tCtrl-,"), wxID_PREFERENCES, ALWAYS);
 #else
                edit->AppendSeparator ();
                /* [Shortcut] Ctrl+P:Open preferences window */
@@ -1377,6 +1405,7 @@ private:
                add_item (tools, _("Restore default preferences"), ID_tools_restore_default_preferences, ALWAYS);
                tools->AppendSeparator ();
                add_item (tools, _("Export preferences..."), ID_tools_export_preferences, ALWAYS);
+               add_item (tools, _("Import preferences..."), ID_tools_import_preferences, ALWAYS);
 
                wxMenu* help = new wxMenu;
 #ifdef __WXOSX__
@@ -1397,7 +1426,8 @@ private:
        void config_changed (Config::Property what)
        {
                /* Instantly save any config changes when using the DCP-o-matic GUI */
-               if (what == Config::CINEMAS) {
+               switch (what) {
+               case Config::CINEMAS:
                        try {
                                Config::instance()->write_cinemas();
                        } catch (exception& e) {
@@ -1409,7 +1439,21 @@ private:
                                                )
                                        );
                        }
-               } else {
+                       break;
+               case Config::DKDM_RECIPIENTS:
+                       try {
+                               Config::instance()->write_dkdm_recipients();
+                       } catch (exception& e) {
+                               error_dialog (
+                                       this,
+                                       wxString::Format (
+                                               _("Could not write to DKDM recipients file at %s.  Your changes have not been saved."),
+                                               std_to_wx(Config::instance()->dkdm_recipients_file().string()).data()
+                                               )
+                                       );
+                       }
+                       break;
+               default:
                        try {
                                Config::instance()->write_config();
                        } catch (exception& e) {
@@ -1576,6 +1620,7 @@ public:
        App ()
                : wxApp ()
        {
+               dcpomatic_setup_path_encoding ();
 #ifdef DCPOMATIC_LINUX
                XInitThreads ();
 #endif
@@ -1586,6 +1631,26 @@ private:
        bool OnInit () override
        {
                try {
+
+#if defined(DCPOMATIC_WINDOWS)
+               if (Config::instance()->win32_console()) {
+                       AllocConsole();
+
+                       HANDLE handle_out = GetStdHandle(STD_OUTPUT_HANDLE);
+                       int hCrt = _open_osfhandle((intptr_t) handle_out, _O_TEXT);
+                       FILE* hf_out = _fdopen(hCrt, "w");
+                       setvbuf(hf_out, NULL, _IONBF, 1);
+                       *stdout = *hf_out;
+
+                       HANDLE handle_in = GetStdHandle(STD_INPUT_HANDLE);
+                       hCrt = _open_osfhandle((intptr_t) handle_in, _O_TEXT);
+                       FILE* hf_in = _fdopen(hCrt, "r");
+                       setvbuf(hf_in, NULL, _IONBF, 128);
+                       *stdin = *hf_in;
+
+                       cout << "DCP-o-matic is starting." << "\n";
+               }
+#endif
                        wxInitAllImageHandlers ();
 
                        Config::FailedToLoad.connect(boost::bind(&App::config_failed_to_load, this, _1));
@@ -1608,8 +1673,6 @@ private:
                        make_foreground_application ();
 #endif
 
-                       dcpomatic_setup_path_encoding ();
-
                        /* Enable i18n; this will create a Config object
                           to look for a force-configured language.  This Config
                           object will be wrong, however, because dcpomatic_setup
@@ -1651,7 +1714,7 @@ private:
                        signal_manager = new wxSignalManager (this);
                        Bind (wxEVT_IDLE, boost::bind (&App::idle, this, _1));
 
-                       if (!_film_to_load.empty() && boost::filesystem::is_directory(_film_to_load)) {
+                       if (!_film_to_load.empty() && dcp::filesystem::is_directory(_film_to_load)) {
                                try {
                                        _frame->load_film (_film_to_load);
                                } catch (exception& e) {
@@ -1688,7 +1751,7 @@ private:
                }
                catch (exception& e)
                {
-                       _splash.reset();
+                       close_splash();
                        error_dialog (nullptr, wxString::Format ("DCP-o-matic could not start."), std_to_wx(e.what()));
                }
 
@@ -1800,7 +1863,10 @@ private:
 
        void close_splash ()
        {
-               _splash.reset();
+               if (_splash) {
+                       _splash->Destroy();
+                       _splash = nullptr;
+               }
        }
 
        void config_failed_to_load (Config::LoadFailure what)
@@ -1818,7 +1884,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.reset();
+               close_splash();
 
                auto config = Config::instance();
                switch (reason) {
@@ -1881,13 +1947,26 @@ private:
                        }
                        return true;
                }
+               case Config::BAD_SIGNER_DN_QUALIFIER:
+               {
+                       RecreateChainDialog dialog(
+                               _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.  This error was caused\n"
+                                 "by a bug in DCP-o-matic which has now been fixed. Do you want to re-create the certificate chain\n"
+                                 "for signing DCPs and KDMs?"),
+                               _("Do nothing"),
+                               Config::NAG_BAD_SIGNER_DN_QUALIFIER
+                               );
+                       return dialog.ShowModal() == wxID_OK;
+               }
                default:
                        DCPOMATIC_ASSERT (false);
                }
        }
 
        DOMFrame* _frame = nullptr;
-       wx_ptr<wxSplashScreen> _splash;
+       wxSplashScreen* _splash = nullptr;
        shared_ptr<wxTimer> _timer;
        string _film_to_load;
        string _film_to_create;