Rename Encoder -> FilmEncoder, and subclasses.
[dcpomatic.git] / src / tools / dcpomatic_editor.cc
1 /*
2     Copyright (C) 2022 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
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.
10
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.
15
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/>.
18
19 */
20
21
22 #include "wx/about_dialog.h"
23 #include "wx/editable_list.h"
24 #include "wx/id.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"
32 #include "lib/util.h"
33 #include "lib/variant.h"
34 #include <dcp/cpl.h>
35 #include <dcp/dcp.h>
36 #include <dcp/reel.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>
47 #include <wx/wx.h>
48 LIBDCP_ENABLE_WARNINGS
49 #ifdef __WXGTK__
50 #include <X11/Xlib.h>
51 #endif
52 #include <iostream>
53
54
55 using std::exception;
56 using std::make_shared;
57 using std::shared_ptr;
58 using std::vector;
59 using boost::optional;
60 #if BOOST_VERSION >= 106100
61 using namespace boost::placeholders;
62 #endif
63
64
65 enum {
66         ID_file_open = DCPOMATIC_MAIN_MENU,
67         ID_file_save,
68 };
69
70
71 class AssetPanel : public wxPanel
72 {
73 public:
74         AssetPanel(wxWindow* parent, shared_ptr<dcp::ReelAsset> asset)
75                 : wxPanel(parent, wxID_ANY)
76                 , _asset(asset)
77         {
78                 auto sizer = new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
79
80                 int r = 0;
81
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);
85                 ++r;
86
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);
90                 ++r;
91
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);
95                 ++r;
96
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);
100                 ++r;
101
102                 auto space = new wxBoxSizer(wxVERTICAL);
103                 space->Add(sizer, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
104                 SetSizerAndFit(space);
105
106                 _entry_point->SetRange(0, 259200);
107                 _entry_point->SetValue(asset->entry_point().get_value_or(0));
108
109                 _duration->SetRange(0, 259200);
110                 _duration->SetValue(asset->duration().get_value_or(0));
111
112                 intrinsic_duration->SetValue(wxString::Format("%ld", asset->intrinsic_duration()));
113
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));
117         }
118
119 private:
120         void annotation_text_changed()
121         {
122                 _asset->set_annotation_text(wx_to_std(_annotation_text->GetValue()));
123         }
124
125         void entry_point_changed()
126         {
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);
131         }
132
133         void duration_changed()
134         {
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);
139         }
140
141         wxTextCtrl* _annotation_text = nullptr;
142         wxSpinCtrl* _entry_point = nullptr;
143         wxSpinCtrl* _duration = nullptr;
144         shared_ptr<dcp::ReelAsset> _asset;
145 };
146
147
148 class ReelEditor : public wxDialog
149 {
150 public:
151         ReelEditor(wxWindow* parent)
152                 : wxDialog(parent, wxID_ANY, _("Edit reel"))
153         {
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);
158         }
159
160         optional<shared_ptr<dcp::Reel>> get() {
161                 return _reel;
162         }
163
164         void set(shared_ptr<dcp::Reel> reel)
165         {
166                 _reel = reel;
167
168                 _notebook->DeleteAllPages();
169                 if (_reel->main_picture()) {
170                         _notebook->AddPage(new AssetPanel(_notebook, _reel->main_picture()), _("Picture"));
171                 }
172                 if (_reel->main_sound()) {
173                         _notebook->AddPage(new AssetPanel(_notebook, _reel->main_sound()), _("Sound"));
174                 }
175                 if (_reel->main_subtitle()) {
176                         _notebook->AddPage(new AssetPanel(_notebook, _reel->main_subtitle()), _("Subtitle"));
177                 }
178
179                 _sizer->Layout();
180                 _sizer->SetSizeHints(this);
181         }
182
183 private:
184         wxNotebook* _notebook = nullptr;
185         wxSizer* _sizer = nullptr;
186         shared_ptr<dcp::Reel> _reel;
187 };
188
189
190 class CPLPanel : public wxPanel
191 {
192 public:
193         CPLPanel(wxWindow* parent, shared_ptr<dcp::CPL> cpl)
194                 : wxPanel(parent, wxID_ANY)
195                 , _cpl(cpl)
196         {
197                 auto sizer = new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
198
199                 int r = 0;
200
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);
204                 ++r;
205
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);
209                 ++r;
210
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);
214                 ++r;
215
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);
219                 ++r;
220
221                 add_label_to_sizer(sizer, this, _("Reels"), true, wxGBPosition(r, 0));
222                 _reels = new EditableList<shared_ptr<dcp::Reel>, ReelEditor>(
223                         this,
224                         { EditableListColumn("Name", 600, true) },
225                         [this]() { return _cpl->reels(); },
226                         [this](vector<shared_ptr<dcp::Reel>> reels) {
227                                 _cpl->set(reels);
228                         },
229                         [](shared_ptr<dcp::Reel> reel, int) {
230                                 return reel->id();
231                         },
232                         EditableListTitle::INVISIBLE,
233                         EditableListButton::EDIT
234                 );
235                 sizer->Add(_reels, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND);
236
237                 auto space = new wxBoxSizer(wxVERTICAL);
238                 space->Add(sizer, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
239                 SetSizerAndFit(space);
240
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));
245         }
246
247 private:
248         void annotation_text_changed()
249         {
250                 _cpl->set_annotation_text(wx_to_std(_annotation_text->GetValue()));
251         }
252
253         void issuer_changed()
254         {
255                 _cpl->set_issuer(wx_to_std(_issuer->GetValue()));
256         }
257
258         void creator_changed()
259         {
260                 _cpl->set_creator(wx_to_std(_creator->GetValue()));
261         }
262
263         void content_title_text_changed()
264         {
265                 _cpl->set_content_title_text(wx_to_std(_content_title_text->GetValue()));
266         }
267
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;
274 };
275
276
277 class DummyPanel : public wxPanel
278 {
279 public:
280         DummyPanel(wxWindow* parent)
281                 : wxPanel(parent, wxID_ANY)
282         {
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);
288         }
289 };
290
291
292 class DOMFrame : public wxFrame
293 {
294 public:
295         DOMFrame ()
296                 : wxFrame(nullptr, -1, variant::wx::dcpomatic_editor())
297                 , _main_sizer(new wxBoxSizer(wxVERTICAL))
298         {
299                 dcpomatic_log = make_shared<NullLog>();
300
301 #if defined(DCPOMATIC_WINDOWS)
302                 maybe_open_console();
303                 std::cout << variant::dcpomatic_editor() << " is starting." << "\n";
304 #endif
305
306                 auto bar = new wxMenuBar;
307                 setup_menu(bar);
308                 SetMenuBar(bar);
309
310 #ifdef DCPOMATIC_WINDOWS
311                 SetIcon(wxIcon(std_to_wx("id")));
312 #endif
313
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);
318
319                 /* Use a panel as the only child of the Frame so that we avoid
320                    the dark-grey background on Windows.
321                 */
322                 _overall_panel = new wxPanel (this, wxID_ANY);
323
324                 auto sizer = new wxBoxSizer(wxVERTICAL);
325
326                 _notebook = new wxNotebook(_overall_panel, wxID_ANY);
327                 _notebook->AddPage(new DummyPanel(_notebook), _("CPL"));
328
329                 sizer->Add(_notebook, 1, wxEXPAND);
330                 _overall_panel->SetSizerAndFit(sizer);
331         }
332
333         void load_dcp (boost::filesystem::path path)
334         {
335                 try {
336                         _dcp = dcp::DCP(path);
337                         _dcp->read();
338                 } catch (std::runtime_error& e) {
339                         error_dialog(this, _("Could not load DCP"), std_to_wx(e.what()));
340                         return;
341                 }
342
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())));
346                 }
347         }
348
349 private:
350
351         void setup_menu (wxMenuBar* m)
352         {
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 ();
358 #ifdef __WXOSX__
359                 _file_menu->Append (wxID_EXIT, _("&Exit"));
360 #else
361                 _file_menu->Append (wxID_EXIT, _("&Quit"));
362 #endif
363
364                 auto help = new wxMenu;
365 #ifdef __WXOSX__
366                 help->Append(wxID_ABOUT, variant::wx::insert_dcpomatic_editor(_("About %s")));
367 #else
368                 help->Append (wxID_ABOUT, _("About"));
369 #endif
370
371                 m->Append (_file_menu, _("&File"));
372                 m->Append (help, _("&Help"));
373         }
374
375         void file_open ()
376         {
377                 auto d = wxStandardPaths::Get().GetDocumentsDir();
378                 wxDirDialog dialog(this, _("Select DCP to open"), d, wxDEFAULT_DIALOG_STYLE | wxDD_DIR_MUST_EXIST);
379
380                 int r;
381                 while (true) {
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."));
385                         } else {
386                                 break;
387                         }
388                 }
389
390                 if (r == wxID_OK) {
391                         boost::filesystem::path const dcp(wx_to_std(dialog.GetPath()));
392                         load_dcp (dcp);
393                 }
394         }
395
396         void file_save ()
397         {
398                 _dcp->write_xml();
399         }
400
401         void file_exit ()
402         {
403                 Close ();
404         }
405
406         void help_about ()
407         {
408                 AboutDialog dialog(this);
409                 dialog.ShowModal();
410         }
411
412         wxPanel* _overall_panel = nullptr;
413         wxMenu* _file_menu = nullptr;
414         wxSizer* _main_sizer = nullptr;
415         wxNotebook* _notebook = nullptr;
416         optional<dcp::DCP> _dcp;
417 };
418
419
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 }
423 };
424
425
426 /** @class App
427  *  @brief The magic App class for wxWidgets.
428  */
429 class App : public wxApp
430 {
431 public:
432         App ()
433                 : wxApp ()
434         {
435 #ifdef DCPOMATIC_LINUX
436                 XInitThreads ();
437 #endif
438         }
439
440 private:
441
442         bool OnInit () override
443         {
444                 wxSplashScreen* splash;
445                 try {
446                         wxInitAllImageHandlers ();
447
448                         splash = maybe_show_splash ();
449
450                         SetAppName(variant::wx::dcpomatic_editor());
451
452                         if (!wxApp::OnInit()) {
453                                 return false;
454                         }
455
456 #ifdef DCPOMATIC_LINUX
457                         unsetenv ("UBUNTU_MENUPROXY");
458 #endif
459
460 #ifdef DCPOMATIC_OSX
461                         make_foreground_application ();
462 #endif
463
464                         dcpomatic_setup_path_encoding ();
465
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.
470                            set up yet.
471                         */
472                         dcpomatic_setup_i18n ();
473
474                         /* Set things up, including filters etc.
475                            which will now be internationalised correctly.
476                         */
477                         dcpomatic_setup ();
478
479                         signal_manager = new wxSignalManager (this);
480
481                         _frame = new DOMFrame ();
482                         SetTopWindow (_frame);
483                         _frame->Maximize ();
484                         if (splash) {
485                                 splash->Destroy ();
486                                 splash = nullptr;
487                         }
488                         _frame->Show ();
489
490                         if (_dcp_to_load) {
491                                 _frame->load_dcp(*_dcp_to_load);
492                         }
493
494                         Bind (wxEVT_IDLE, boost::bind (&App::idle, this));
495                 }
496                 catch (exception& e)
497                 {
498                         if (splash) {
499                                 splash->Destroy ();
500                         }
501                         error_dialog(nullptr, variant::wx::insert_dcpomatic_editor(_("%s could not start.")), std_to_wx(e.what()));
502                 }
503
504                 return true;
505         }
506
507         void OnInitCmdLine (wxCmdLineParser& parser) override
508         {
509                 parser.SetDesc (command_line_description);
510                 parser.SetSwitchChars (wxT ("-"));
511         }
512
513         bool OnCmdLineParsed (wxCmdLineParser& parser) override
514         {
515                 if (parser.GetParamCount() > 0) {
516                         _dcp_to_load = wx_to_std(parser.GetParam(0));
517                 }
518
519                 return true;
520         }
521
522         void report_exception ()
523         {
524                 try {
525                         throw;
526                 } catch (FileError& e) {
527                         error_dialog (
528                                 0,
529                                 wxString::Format (
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 ())
533                                         )
534                                 );
535                 } catch (exception& e) {
536                         error_dialog (
537                                 0,
538                                 wxString::Format (
539                                         _("An exception occurred: %s.\n\n") + REPORT_PROBLEM,
540                                         std_to_wx (e.what ())
541                                         )
542                                 );
543                 } catch (...) {
544                         error_dialog (0, _("An unknown exception occurred.") + "  " + REPORT_PROBLEM);
545                 }
546         }
547
548         /* An unhandled exception has occurred inside the main event loop */
549         bool OnExceptionInMainLoop () override
550         {
551                 report_exception ();
552                 /* This will terminate the program */
553                 return false;
554         }
555
556         void OnUnhandledException () override
557         {
558                 report_exception ();
559         }
560
561         void idle ()
562         {
563                 signal_manager->ui_idle ();
564         }
565
566         DOMFrame* _frame = nullptr;
567         optional<boost::filesystem::path> _dcp_to_load;
568 };
569
570
571 IMPLEMENT_APP (App)