2 Copyright (C) 2022 Carl Hetherington <cth@carlh.net>
4 This file is part of DCP-o-matic.
6 DCP-o-matic is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
11 DCP-o-matic is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
22 #include "wx/about_dialog.h"
23 #include "wx/editable_list.h"
25 #include "wx/wx_signal_manager.h"
26 #include "wx/wx_util.h"
27 #include "wx/wx_variant.h"
28 #include "lib/constants.h"
29 #include "lib/cross.h"
30 #include "lib/dcpomatic_log.h"
31 #include "lib/null_log.h"
33 #include "lib/variant.h"
37 #include <dcp/reel_picture_asset.h>
38 #include <dcp/reel_sound_asset.h>
39 #include <dcp/reel_subtitle_asset.h>
40 #include <dcp/warnings.h>
41 LIBDCP_DISABLE_WARNINGS
42 #include <wx/cmdline.h>
43 #include <wx/notebook.h>
44 #include <wx/spinctrl.h>
45 #include <wx/splash.h>
46 #include <wx/stdpaths.h>
48 LIBDCP_ENABLE_WARNINGS
56 using std::make_shared;
57 using std::shared_ptr;
59 using boost::optional;
60 #if BOOST_VERSION >= 106100
61 using namespace boost::placeholders;
66 ID_file_open = DCPOMATIC_MAIN_MENU,
71 class AssetPanel : public wxPanel
74 AssetPanel(wxWindow* parent, shared_ptr<dcp::ReelAsset> asset)
75 : wxPanel(parent, wxID_ANY)
78 auto sizer = new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
82 add_label_to_sizer(sizer, this, _("Annotation text"), true, wxGBPosition(r, 0));
83 _annotation_text = new wxTextCtrl(this, wxID_ANY, std_to_wx(asset->annotation_text().get_value_or("")), wxDefaultPosition, wxSize(600, -1));
84 sizer->Add(_annotation_text, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND);
87 add_label_to_sizer(sizer, this, _("Entry point"), true, wxGBPosition(r, 0));
88 _entry_point = new wxSpinCtrl(this, wxID_ANY);
89 sizer->Add(_entry_point, wxGBPosition(r, 1), wxDefaultSpan);
92 add_label_to_sizer(sizer, this, _("Duration"), true, wxGBPosition(r, 0));
93 _duration = new wxSpinCtrl(this, wxID_ANY);
94 sizer->Add(_duration, wxGBPosition(r, 1), wxDefaultSpan);
97 add_label_to_sizer(sizer, this, _("Intrinsic duration"), true, wxGBPosition(r, 0));
98 auto intrinsic_duration = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_READONLY);
99 sizer->Add(intrinsic_duration, wxGBPosition(r, 1), wxDefaultSpan);
102 auto space = new wxBoxSizer(wxVERTICAL);
103 space->Add(sizer, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
104 SetSizerAndFit(space);
106 _entry_point->SetRange(0, 259200);
107 _entry_point->SetValue(asset->entry_point().get_value_or(0));
109 _duration->SetRange(0, 259200);
110 _duration->SetValue(asset->duration().get_value_or(0));
112 intrinsic_duration->SetValue(wxString::Format("%ld", asset->intrinsic_duration()));
114 _annotation_text->Bind(wxEVT_TEXT, boost::bind(&AssetPanel::annotation_text_changed, this));
115 _entry_point->Bind(wxEVT_SPINCTRL, boost::bind(&AssetPanel::entry_point_changed, this));
116 _duration->Bind(wxEVT_SPINCTRL, boost::bind(&AssetPanel::duration_changed, this));
120 void annotation_text_changed()
122 _asset->set_annotation_text(wx_to_std(_annotation_text->GetValue()));
125 void entry_point_changed()
127 _asset->set_entry_point(_entry_point->GetValue());
128 auto const fixed_duration = std::min(_asset->intrinsic_duration() - _asset->entry_point().get_value_or(0LL), _asset->duration().get_value_or(_asset->intrinsic_duration()));
129 _duration->SetValue(fixed_duration);
130 _asset->set_duration(fixed_duration);
133 void duration_changed()
135 _asset->set_duration(_duration->GetValue());
136 auto const fixed_entry_point = std::min(_asset->intrinsic_duration() - _asset->duration().get_value_or(_asset->intrinsic_duration()), _asset->entry_point().get_value_or(0LL));
137 _entry_point->SetValue(fixed_entry_point);
138 _asset->set_entry_point(fixed_entry_point);
141 wxTextCtrl* _annotation_text = nullptr;
142 wxSpinCtrl* _entry_point = nullptr;
143 wxSpinCtrl* _duration = nullptr;
144 shared_ptr<dcp::ReelAsset> _asset;
148 class ReelEditor : public wxDialog
151 ReelEditor(wxWindow* parent)
152 : wxDialog(parent, wxID_ANY, _("Edit reel"))
154 _sizer = new wxBoxSizer(wxVERTICAL);
155 _notebook = new wxNotebook(this, wxID_ANY);
156 _sizer->Add(_notebook, wxEXPAND | wxALL, 1, DCPOMATIC_DIALOG_BORDER);
157 SetSizerAndFit(_sizer);
160 optional<shared_ptr<dcp::Reel>> get() {
164 void set(shared_ptr<dcp::Reel> reel)
168 _notebook->DeleteAllPages();
169 if (_reel->main_picture()) {
170 _notebook->AddPage(new AssetPanel(_notebook, _reel->main_picture()), _("Picture"));
172 if (_reel->main_sound()) {
173 _notebook->AddPage(new AssetPanel(_notebook, _reel->main_sound()), _("Sound"));
175 if (_reel->main_subtitle()) {
176 _notebook->AddPage(new AssetPanel(_notebook, _reel->main_subtitle()), _("Subtitle"));
180 _sizer->SetSizeHints(this);
184 wxNotebook* _notebook = nullptr;
185 wxSizer* _sizer = nullptr;
186 shared_ptr<dcp::Reel> _reel;
190 class CPLPanel : public wxPanel
193 CPLPanel(wxWindow* parent, shared_ptr<dcp::CPL> cpl)
194 : wxPanel(parent, wxID_ANY)
197 auto sizer = new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
201 add_label_to_sizer(sizer, this, _("Annotation text"), true, wxGBPosition(r, 0));
202 _annotation_text = new wxTextCtrl(this, wxID_ANY, std_to_wx(cpl->annotation_text().get_value_or("")), wxDefaultPosition, wxSize(600, -1));
203 sizer->Add(_annotation_text, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND);
206 add_label_to_sizer(sizer, this, _("Issuer"), true, wxGBPosition(r, 0));
207 _issuer = new wxTextCtrl(this, wxID_ANY, std_to_wx(cpl->issuer()), wxDefaultPosition, wxSize(600, -1));
208 sizer->Add(_issuer, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND);
211 add_label_to_sizer(sizer, this, _("Creator"), true, wxGBPosition(r, 0));
212 _creator = new wxTextCtrl(this, wxID_ANY, std_to_wx(cpl->creator()), wxDefaultPosition, wxSize(600, -1));
213 sizer->Add(_creator, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND);
216 add_label_to_sizer(sizer, this, _("Content title text"), true, wxGBPosition(r, 0));
217 _content_title_text = new wxTextCtrl(this, wxID_ANY, std_to_wx(cpl->content_title_text()), wxDefaultPosition, wxSize(600, -1));
218 sizer->Add(_content_title_text, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND);
221 add_label_to_sizer(sizer, this, _("Reels"), true, wxGBPosition(r, 0));
222 _reels = new EditableList<shared_ptr<dcp::Reel>, ReelEditor>(
224 { EditableListColumn("Name", 600, true) },
225 [this]() { return _cpl->reels(); },
226 [this](vector<shared_ptr<dcp::Reel>> reels) {
229 [](shared_ptr<dcp::Reel> reel, int) {
232 EditableListTitle::INVISIBLE,
233 EditableListButton::EDIT
235 sizer->Add(_reels, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND);
237 auto space = new wxBoxSizer(wxVERTICAL);
238 space->Add(sizer, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
239 SetSizerAndFit(space);
241 _annotation_text->Bind(wxEVT_TEXT, boost::bind(&CPLPanel::annotation_text_changed, this));
242 _issuer->Bind(wxEVT_TEXT, boost::bind(&CPLPanel::issuer_changed, this));
243 _creator->Bind(wxEVT_TEXT, boost::bind(&CPLPanel::creator_changed, this));
244 _content_title_text->Bind(wxEVT_TEXT, boost::bind(&CPLPanel::content_title_text_changed, this));
248 void annotation_text_changed()
250 _cpl->set_annotation_text(wx_to_std(_annotation_text->GetValue()));
253 void issuer_changed()
255 _cpl->set_issuer(wx_to_std(_issuer->GetValue()));
258 void creator_changed()
260 _cpl->set_creator(wx_to_std(_creator->GetValue()));
263 void content_title_text_changed()
265 _cpl->set_content_title_text(wx_to_std(_content_title_text->GetValue()));
268 std::shared_ptr<dcp::CPL> _cpl;
269 wxTextCtrl* _annotation_text = nullptr;
270 wxTextCtrl* _issuer = nullptr;
271 wxTextCtrl* _creator = nullptr;
272 wxTextCtrl* _content_title_text = nullptr;
273 EditableList<shared_ptr<dcp::Reel>, ReelEditor>* _reels;
277 class DummyPanel : public wxPanel
280 DummyPanel(wxWindow* parent)
281 : wxPanel(parent, wxID_ANY)
283 auto sizer = new wxBoxSizer(wxVERTICAL);
284 add_label_to_sizer(sizer, this, _("Open a DCP using File -> Open"), false);
285 auto space = new wxBoxSizer(wxVERTICAL);
286 space->Add(sizer, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
287 SetSizerAndFit(space);
292 class DOMFrame : public wxFrame
296 : wxFrame(nullptr, -1, variant::wx::dcpomatic_editor())
297 , _main_sizer(new wxBoxSizer(wxVERTICAL))
299 dcpomatic_log = make_shared<NullLog>();
301 #if defined(DCPOMATIC_WINDOWS)
302 maybe_open_console();
303 std::cout << variant::dcpomatic_editor() << " is starting." << "\n";
306 auto bar = new wxMenuBar;
310 #ifdef DCPOMATIC_WINDOWS
311 SetIcon(wxIcon(std_to_wx("id")));
314 Bind(wxEVT_MENU, boost::bind(&DOMFrame::file_open, this), ID_file_open);
315 Bind(wxEVT_MENU, boost::bind(&DOMFrame::file_save, this), ID_file_save);
316 Bind(wxEVT_MENU, boost::bind(&DOMFrame::file_exit, this), wxID_EXIT);
317 Bind(wxEVT_MENU, boost::bind(&DOMFrame::help_about, this), wxID_ABOUT);
319 /* Use a panel as the only child of the Frame so that we avoid
320 the dark-grey background on Windows.
322 _overall_panel = new wxPanel (this, wxID_ANY);
324 auto sizer = new wxBoxSizer(wxVERTICAL);
326 _notebook = new wxNotebook(_overall_panel, wxID_ANY);
327 _notebook->AddPage(new DummyPanel(_notebook), _("CPL"));
329 sizer->Add(_notebook, 1, wxEXPAND);
330 _overall_panel->SetSizerAndFit(sizer);
333 void load_dcp (boost::filesystem::path path)
336 _dcp = dcp::DCP(path);
338 } catch (std::runtime_error& e) {
339 error_dialog(this, _("Could not load DCP"), std_to_wx(e.what()));
343 _notebook->DeleteAllPages();
344 for (auto cpl: _dcp->cpls()) {
345 _notebook->AddPage(new CPLPanel(_notebook, cpl), wx_to_std(cpl->annotation_text().get_value_or(cpl->id())));
351 void setup_menu (wxMenuBar* m)
353 _file_menu = new wxMenu;
354 _file_menu->Append (ID_file_open, _("&Open...\tCtrl-O"));
355 _file_menu->AppendSeparator ();
356 _file_menu->Append (ID_file_save, _("&Save\tCtrl-S"));
357 _file_menu->AppendSeparator ();
359 _file_menu->Append (wxID_EXIT, _("&Exit"));
361 _file_menu->Append (wxID_EXIT, _("&Quit"));
364 auto help = new wxMenu;
366 help->Append(wxID_ABOUT, variant::wx::insert_dcpomatic_editor(_("About %s")));
368 help->Append (wxID_ABOUT, _("About"));
371 m->Append (_file_menu, _("&File"));
372 m->Append (help, _("&Help"));
377 auto d = wxStandardPaths::Get().GetDocumentsDir();
378 wxDirDialog dialog(this, _("Select DCP to open"), d, wxDEFAULT_DIALOG_STYLE | wxDD_DIR_MUST_EXIST);
382 r = dialog.ShowModal();
383 if (r == wxID_OK && dialog.GetPath() == wxStandardPaths::Get().GetDocumentsDir()) {
384 error_dialog (this, _("You did not select a folder. Make sure that you select a folder before clicking Open."));
391 boost::filesystem::path const dcp(wx_to_std(dialog.GetPath()));
408 AboutDialog dialog(this);
412 wxPanel* _overall_panel = nullptr;
413 wxMenu* _file_menu = nullptr;
414 wxSizer* _main_sizer = nullptr;
415 wxNotebook* _notebook = nullptr;
416 optional<dcp::DCP> _dcp;
420 static const wxCmdLineEntryDesc command_line_description[] = {
421 { wxCMD_LINE_PARAM, 0, 0, "DCP to edit", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
422 { wxCMD_LINE_NONE, "", "", "", wxCmdLineParamType (0), 0 }
427 * @brief The magic App class for wxWidgets.
429 class App : public wxApp
435 #ifdef DCPOMATIC_LINUX
442 bool OnInit () override
444 wxSplashScreen* splash;
446 wxInitAllImageHandlers ();
448 splash = maybe_show_splash ();
450 SetAppName(variant::wx::dcpomatic_editor());
452 if (!wxApp::OnInit()) {
456 #ifdef DCPOMATIC_LINUX
457 unsetenv ("UBUNTU_MENUPROXY");
461 make_foreground_application ();
464 dcpomatic_setup_path_encoding ();
466 /* Enable i18n; this will create a Config object
467 to look for a force-configured language. This Config
468 object will be wrong, however, because dcpomatic_setup
469 hasn't yet been called and there aren't any filters etc.
472 dcpomatic_setup_i18n ();
474 /* Set things up, including filters etc.
475 which will now be internationalised correctly.
479 signal_manager = new wxSignalManager (this);
481 _frame = new DOMFrame ();
482 SetTopWindow (_frame);
491 _frame->load_dcp(*_dcp_to_load);
494 Bind (wxEVT_IDLE, boost::bind (&App::idle, this));
501 error_dialog(nullptr, variant::wx::insert_dcpomatic_editor(_("%s could not start.")), std_to_wx(e.what()));
507 void OnInitCmdLine (wxCmdLineParser& parser) override
509 parser.SetDesc (command_line_description);
510 parser.SetSwitchChars (wxT ("-"));
513 bool OnCmdLineParsed (wxCmdLineParser& parser) override
515 if (parser.GetParamCount() > 0) {
516 _dcp_to_load = wx_to_std(parser.GetParam(0));
522 void report_exception ()
526 } catch (FileError& e) {
530 _("An exception occurred: %s (%s)\n\n") + REPORT_PROBLEM,
531 std_to_wx (e.what()),
532 std_to_wx (e.file().string().c_str ())
535 } catch (exception& e) {
539 _("An exception occurred: %s.\n\n") + REPORT_PROBLEM,
540 std_to_wx (e.what ())
544 error_dialog (0, _("An unknown exception occurred.") + " " + REPORT_PROBLEM);
548 /* An unhandled exception has occurred inside the main event loop */
549 bool OnExceptionInMainLoop () override
552 /* This will terminate the program */
556 void OnUnhandledException () override
563 signal_manager->ui_idle ();
566 DOMFrame* _frame = nullptr;
567 optional<boost::filesystem::path> _dcp_to_load;