Supporters update.
[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 "lib/constants.h"
28 #include "lib/cross.h"
29 #include "lib/dcpomatic_log.h"
30 #include "lib/null_log.h"
31 #include <dcp/cpl.h>
32 #include <dcp/dcp.h>
33 #include <dcp/reel.h>
34 #include <dcp/reel_picture_asset.h>
35 #include <dcp/reel_sound_asset.h>
36 #include <dcp/reel_subtitle_asset.h>
37 #include <dcp/warnings.h>
38 LIBDCP_DISABLE_WARNINGS
39 #include <wx/cmdline.h>
40 #include <wx/notebook.h>
41 #include <wx/spinctrl.h>
42 #include <wx/splash.h>
43 #include <wx/stdpaths.h>
44 #include <wx/wx.h>
45 LIBDCP_ENABLE_WARNINGS
46 #ifdef __WXGTK__
47 #include <X11/Xlib.h>
48 #endif
49 #include <iostream>
50
51
52 using std::exception;
53 using std::make_shared;
54 using std::shared_ptr;
55 using std::vector;
56 using boost::optional;
57 #if BOOST_VERSION >= 106100
58 using namespace boost::placeholders;
59 #endif
60
61
62 enum {
63         ID_file_open = DCPOMATIC_MAIN_MENU,
64         ID_file_save,
65 };
66
67
68 class AssetPanel : public wxPanel
69 {
70 public:
71         AssetPanel(wxWindow* parent, shared_ptr<dcp::ReelAsset> asset)
72                 : wxPanel(parent, wxID_ANY)
73                 , _asset(asset)
74         {
75                 auto sizer = new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
76
77                 int r = 0;
78
79                 add_label_to_sizer(sizer, this, _("Annotation text"), true, wxGBPosition(r, 0));
80                 _annotation_text = new wxTextCtrl(this, wxID_ANY, std_to_wx(asset->annotation_text().get_value_or("")), wxDefaultPosition, wxSize(600, -1));
81                 sizer->Add(_annotation_text, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND);
82                 ++r;
83
84                 add_label_to_sizer(sizer, this, _("Entry point"), true, wxGBPosition(r, 0));
85                 _entry_point = new wxSpinCtrl(this, wxID_ANY);
86                 sizer->Add(_entry_point, wxGBPosition(r, 1), wxDefaultSpan);
87                 ++r;
88
89                 add_label_to_sizer(sizer, this, _("Duration"), true, wxGBPosition(r, 0));
90                 _duration = new wxSpinCtrl(this, wxID_ANY);
91                 sizer->Add(_duration, wxGBPosition(r, 1), wxDefaultSpan);
92                 ++r;
93
94                 add_label_to_sizer(sizer, this, _("Intrinsic duration"), true, wxGBPosition(r, 0));
95                 auto intrinsic_duration = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_READONLY);
96                 sizer->Add(intrinsic_duration, wxGBPosition(r, 1), wxDefaultSpan);
97                 ++r;
98
99                 auto space = new wxBoxSizer(wxVERTICAL);
100                 space->Add(sizer, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
101                 SetSizerAndFit(space);
102
103                 _entry_point->SetRange(0, 259200);
104                 _entry_point->SetValue(asset->entry_point().get_value_or(0));
105
106                 _duration->SetRange(0, 259200);
107                 _duration->SetValue(asset->duration().get_value_or(0));
108
109                 intrinsic_duration->SetValue(wxString::Format("%ld", asset->intrinsic_duration()));
110
111                 _annotation_text->Bind(wxEVT_TEXT, boost::bind(&AssetPanel::annotation_text_changed, this));
112                 _entry_point->Bind(wxEVT_SPINCTRL, boost::bind(&AssetPanel::entry_point_changed, this));
113                 _duration->Bind(wxEVT_SPINCTRL, boost::bind(&AssetPanel::duration_changed, this));
114         }
115
116 private:
117         void annotation_text_changed()
118         {
119                 _asset->set_annotation_text(wx_to_std(_annotation_text->GetValue()));
120         }
121
122         void entry_point_changed()
123         {
124                 _asset->set_entry_point(_entry_point->GetValue());
125                 auto const fixed_duration = std::min(_asset->intrinsic_duration() - _asset->entry_point().get_value_or(0LL), _asset->duration().get_value_or(_asset->intrinsic_duration()));
126                 _duration->SetValue(fixed_duration);
127                 _asset->set_duration(fixed_duration);
128         }
129
130         void duration_changed()
131         {
132                 _asset->set_duration(_duration->GetValue());
133                 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));
134                 _entry_point->SetValue(fixed_entry_point);
135                 _asset->set_entry_point(fixed_entry_point);
136         }
137
138         wxTextCtrl* _annotation_text = nullptr;
139         wxSpinCtrl* _entry_point = nullptr;
140         wxSpinCtrl* _duration = nullptr;
141         shared_ptr<dcp::ReelAsset> _asset;
142 };
143
144
145 class ReelEditor : public wxDialog
146 {
147 public:
148         ReelEditor(wxWindow* parent)
149                 : wxDialog(parent, wxID_ANY, _("Edit reel"))
150         {
151                 _sizer = new wxBoxSizer(wxVERTICAL);
152                 _notebook = new wxNotebook(this, wxID_ANY);
153                 _sizer->Add(_notebook, wxEXPAND | wxALL, 1, DCPOMATIC_DIALOG_BORDER);
154                 SetSizerAndFit(_sizer);
155         }
156
157         optional<shared_ptr<dcp::Reel>> get() {
158                 return _reel;
159         }
160
161         void set(shared_ptr<dcp::Reel> reel)
162         {
163                 _reel = reel;
164
165                 _notebook->DeleteAllPages();
166                 if (_reel->main_picture()) {
167                         _notebook->AddPage(new AssetPanel(_notebook, _reel->main_picture()), _("Picture"));
168                 }
169                 if (_reel->main_sound()) {
170                         _notebook->AddPage(new AssetPanel(_notebook, _reel->main_sound()), _("Sound"));
171                 }
172                 if (_reel->main_subtitle()) {
173                         _notebook->AddPage(new AssetPanel(_notebook, _reel->main_subtitle()), _("Subtitle"));
174                 }
175
176                 _sizer->Layout();
177                 _sizer->SetSizeHints(this);
178         }
179
180 private:
181         wxNotebook* _notebook = nullptr;
182         wxSizer* _sizer = nullptr;
183         shared_ptr<dcp::Reel> _reel;
184 };
185
186
187 class CPLPanel : public wxPanel
188 {
189 public:
190         CPLPanel(wxWindow* parent, shared_ptr<dcp::CPL> cpl)
191                 : wxPanel(parent, wxID_ANY)
192                 , _cpl(cpl)
193         {
194                 auto sizer = new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
195
196                 int r = 0;
197
198                 add_label_to_sizer(sizer, this, _("Annotation text"), true, wxGBPosition(r, 0));
199                 _annotation_text = new wxTextCtrl(this, wxID_ANY, std_to_wx(cpl->annotation_text().get_value_or("")), wxDefaultPosition, wxSize(600, -1));
200                 sizer->Add(_annotation_text, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND);
201                 ++r;
202
203                 add_label_to_sizer(sizer, this, _("Issuer"), true, wxGBPosition(r, 0));
204                 _issuer = new wxTextCtrl(this, wxID_ANY, std_to_wx(cpl->issuer()), wxDefaultPosition, wxSize(600, -1));
205                 sizer->Add(_issuer, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND);
206                 ++r;
207
208                 add_label_to_sizer(sizer, this, _("Creator"), true, wxGBPosition(r, 0));
209                 _creator = new wxTextCtrl(this, wxID_ANY, std_to_wx(cpl->creator()), wxDefaultPosition, wxSize(600, -1));
210                 sizer->Add(_creator, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND);
211                 ++r;
212
213                 add_label_to_sizer(sizer, this, _("Content title text"), true, wxGBPosition(r, 0));
214                 _content_title_text = new wxTextCtrl(this, wxID_ANY, std_to_wx(cpl->content_title_text()), wxDefaultPosition, wxSize(600, -1));
215                 sizer->Add(_content_title_text, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND);
216                 ++r;
217
218                 add_label_to_sizer(sizer, this, _("Reels"), true, wxGBPosition(r, 0));
219                 _reels = new EditableList<shared_ptr<dcp::Reel>, ReelEditor>(
220                         this,
221                         { EditableListColumn("Name", 600, true) },
222                         [this]() { return _cpl->reels(); },
223                         [this](vector<shared_ptr<dcp::Reel>> reels) {
224                                 _cpl->set(reels);
225                         },
226                         [](shared_ptr<dcp::Reel> reel, int) {
227                                 return reel->id();
228                         },
229                         EditableListTitle::INVISIBLE,
230                         EditableListButton::EDIT
231                 );
232                 sizer->Add(_reels, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND);
233
234                 auto space = new wxBoxSizer(wxVERTICAL);
235                 space->Add(sizer, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
236                 SetSizerAndFit(space);
237
238                 _annotation_text->Bind(wxEVT_TEXT, boost::bind(&CPLPanel::annotation_text_changed, this));
239                 _issuer->Bind(wxEVT_TEXT, boost::bind(&CPLPanel::issuer_changed, this));
240                 _creator->Bind(wxEVT_TEXT, boost::bind(&CPLPanel::creator_changed, this));
241                 _content_title_text->Bind(wxEVT_TEXT, boost::bind(&CPLPanel::content_title_text_changed, this));
242         }
243
244 private:
245         void annotation_text_changed()
246         {
247                 _cpl->set_annotation_text(wx_to_std(_annotation_text->GetValue()));
248         }
249
250         void issuer_changed()
251         {
252                 _cpl->set_issuer(wx_to_std(_issuer->GetValue()));
253         }
254
255         void creator_changed()
256         {
257                 _cpl->set_creator(wx_to_std(_creator->GetValue()));
258         }
259
260         void content_title_text_changed()
261         {
262                 _cpl->set_content_title_text(wx_to_std(_content_title_text->GetValue()));
263         }
264
265         std::shared_ptr<dcp::CPL> _cpl;
266         wxTextCtrl* _annotation_text = nullptr;
267         wxTextCtrl* _issuer = nullptr;
268         wxTextCtrl* _creator = nullptr;
269         wxTextCtrl* _content_title_text = nullptr;
270         EditableList<shared_ptr<dcp::Reel>, ReelEditor>* _reels;
271 };
272
273
274 class DummyPanel : public wxPanel
275 {
276 public:
277         DummyPanel(wxWindow* parent)
278                 : wxPanel(parent, wxID_ANY)
279         {
280                 auto sizer = new wxBoxSizer(wxVERTICAL);
281                 add_label_to_sizer(sizer, this, _("Open a DCP using File -> Open"), false);
282                 auto space = new wxBoxSizer(wxVERTICAL);
283                 space->Add(sizer, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
284                 SetSizerAndFit(space);
285         }
286 };
287
288
289 class DOMFrame : public wxFrame
290 {
291 public:
292         DOMFrame ()
293                 : wxFrame(nullptr, -1, _("DCP-o-matic Editor"))
294                 , _main_sizer(new wxBoxSizer(wxVERTICAL))
295         {
296                 dcpomatic_log = make_shared<NullLog>();
297
298 #if defined(DCPOMATIC_WINDOWS)
299                 maybe_open_console();
300                 std::cout << "DCP-o-matic Editor is starting." << "\n";
301 #endif
302
303                 auto bar = new wxMenuBar;
304                 setup_menu(bar);
305                 SetMenuBar(bar);
306
307 #ifdef DCPOMATIC_WINDOWS
308                 SetIcon(wxIcon(std_to_wx("id")));
309 #endif
310
311                 Bind(wxEVT_MENU, boost::bind(&DOMFrame::file_open, this), ID_file_open);
312                 Bind(wxEVT_MENU, boost::bind(&DOMFrame::file_save, this), ID_file_save);
313                 Bind(wxEVT_MENU, boost::bind(&DOMFrame::file_exit, this), wxID_EXIT);
314                 Bind(wxEVT_MENU, boost::bind(&DOMFrame::help_about, this), wxID_ABOUT);
315
316                 /* Use a panel as the only child of the Frame so that we avoid
317                    the dark-grey background on Windows.
318                 */
319                 _overall_panel = new wxPanel (this, wxID_ANY);
320
321                 auto sizer = new wxBoxSizer(wxVERTICAL);
322
323                 _notebook = new wxNotebook(_overall_panel, wxID_ANY);
324                 _notebook->AddPage(new DummyPanel(_notebook), _("CPL"));
325
326                 sizer->Add(_notebook, 1, wxEXPAND);
327                 _overall_panel->SetSizerAndFit(sizer);
328         }
329
330         void load_dcp (boost::filesystem::path path)
331         {
332                 try {
333                         _dcp = dcp::DCP(path);
334                         _dcp->read();
335                 } catch (std::runtime_error& e) {
336                         error_dialog(this, _("Could not load DCP"), std_to_wx(e.what()));
337                         return;
338                 }
339
340                 _notebook->DeleteAllPages();
341                 for (auto cpl: _dcp->cpls()) {
342                         _notebook->AddPage(new CPLPanel(_notebook, cpl), wx_to_std(cpl->annotation_text().get_value_or(cpl->id())));
343                 }
344         }
345
346 private:
347
348         void setup_menu (wxMenuBar* m)
349         {
350                 _file_menu = new wxMenu;
351                 _file_menu->Append (ID_file_open, _("&Open...\tCtrl-O"));
352                 _file_menu->AppendSeparator ();
353                 _file_menu->Append (ID_file_save, _("&Save\tCtrl-S"));
354                 _file_menu->AppendSeparator ();
355 #ifdef __WXOSX__
356                 _file_menu->Append (wxID_EXIT, _("&Exit"));
357 #else
358                 _file_menu->Append (wxID_EXIT, _("&Quit"));
359 #endif
360
361                 auto help = new wxMenu;
362 #ifdef __WXOSX__
363                 help->Append (wxID_ABOUT, _("About DCP-o-matic"));
364 #else
365                 help->Append (wxID_ABOUT, _("About"));
366 #endif
367
368                 m->Append (_file_menu, _("&File"));
369                 m->Append (help, _("&Help"));
370         }
371
372         void file_open ()
373         {
374                 auto d = wxStandardPaths::Get().GetDocumentsDir();
375                 wxDirDialog dialog(this, _("Select DCP to open"), d, wxDEFAULT_DIALOG_STYLE | wxDD_DIR_MUST_EXIST);
376
377                 int r;
378                 while (true) {
379                         r = dialog.ShowModal();
380                         if (r == wxID_OK && dialog.GetPath() == wxStandardPaths::Get().GetDocumentsDir()) {
381                                 error_dialog (this, _("You did not select a folder.  Make sure that you select a folder before clicking Open."));
382                         } else {
383                                 break;
384                         }
385                 }
386
387                 if (r == wxID_OK) {
388                         boost::filesystem::path const dcp(wx_to_std(dialog.GetPath()));
389                         load_dcp (dcp);
390                 }
391         }
392
393         void file_save ()
394         {
395                 _dcp->write_xml();
396         }
397
398         void file_exit ()
399         {
400                 Close ();
401         }
402
403         void help_about ()
404         {
405                 AboutDialog dialog(this);
406                 dialog.ShowModal();
407         }
408
409         wxPanel* _overall_panel = nullptr;
410         wxMenu* _file_menu = nullptr;
411         wxSizer* _main_sizer = nullptr;
412         wxNotebook* _notebook = nullptr;
413         optional<dcp::DCP> _dcp;
414 };
415
416
417 static const wxCmdLineEntryDesc command_line_description[] = {
418         { wxCMD_LINE_PARAM, 0, 0, "DCP to edit", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
419         { wxCMD_LINE_NONE, "", "", "", wxCmdLineParamType (0), 0 }
420 };
421
422
423 /** @class App
424  *  @brief The magic App class for wxWidgets.
425  */
426 class App : public wxApp
427 {
428 public:
429         App ()
430                 : wxApp ()
431         {
432 #ifdef DCPOMATIC_LINUX
433                 XInitThreads ();
434 #endif
435         }
436
437 private:
438
439         bool OnInit () override
440         {
441                 wxSplashScreen* splash;
442                 try {
443                         wxInitAllImageHandlers ();
444
445                         splash = maybe_show_splash ();
446
447                         SetAppName (_("DCP-o-matic Editor"));
448
449                         if (!wxApp::OnInit()) {
450                                 return false;
451                         }
452
453 #ifdef DCPOMATIC_LINUX
454                         unsetenv ("UBUNTU_MENUPROXY");
455 #endif
456
457 #ifdef DCPOMATIC_OSX
458                         make_foreground_application ();
459 #endif
460
461                         dcpomatic_setup_path_encoding ();
462
463                         /* Enable i18n; this will create a Config object
464                            to look for a force-configured language.  This Config
465                            object will be wrong, however, because dcpomatic_setup
466                            hasn't yet been called and there aren't any filters etc.
467                            set up yet.
468                         */
469                         dcpomatic_setup_i18n ();
470
471                         /* Set things up, including filters etc.
472                            which will now be internationalised correctly.
473                         */
474                         dcpomatic_setup ();
475
476                         signal_manager = new wxSignalManager (this);
477
478                         _frame = new DOMFrame ();
479                         SetTopWindow (_frame);
480                         _frame->Maximize ();
481                         if (splash) {
482                                 splash->Destroy ();
483                                 splash = nullptr;
484                         }
485                         _frame->Show ();
486
487                         if (_dcp_to_load) {
488                                 _frame->load_dcp(*_dcp_to_load);
489                         }
490
491                         Bind (wxEVT_IDLE, boost::bind (&App::idle, this));
492                 }
493                 catch (exception& e)
494                 {
495                         if (splash) {
496                                 splash->Destroy ();
497                         }
498                         error_dialog (0, _("DCP-o-matic Editor could not start."), std_to_wx(e.what()));
499                 }
500
501                 return true;
502         }
503
504         void OnInitCmdLine (wxCmdLineParser& parser) override
505         {
506                 parser.SetDesc (command_line_description);
507                 parser.SetSwitchChars (wxT ("-"));
508         }
509
510         bool OnCmdLineParsed (wxCmdLineParser& parser) override
511         {
512                 if (parser.GetParamCount() > 0) {
513                         _dcp_to_load = wx_to_std(parser.GetParam(0));
514                 }
515
516                 return true;
517         }
518
519         void report_exception ()
520         {
521                 try {
522                         throw;
523                 } catch (FileError& e) {
524                         error_dialog (
525                                 0,
526                                 wxString::Format (
527                                         _("An exception occurred: %s (%s)\n\n") + REPORT_PROBLEM,
528                                         std_to_wx (e.what()),
529                                         std_to_wx (e.file().string().c_str ())
530                                         )
531                                 );
532                 } catch (exception& e) {
533                         error_dialog (
534                                 0,
535                                 wxString::Format (
536                                         _("An exception occurred: %s.\n\n") + REPORT_PROBLEM,
537                                         std_to_wx (e.what ())
538                                         )
539                                 );
540                 } catch (...) {
541                         error_dialog (0, _("An unknown exception occurred.") + "  " + REPORT_PROBLEM);
542                 }
543         }
544
545         /* An unhandled exception has occurred inside the main event loop */
546         bool OnExceptionInMainLoop () override
547         {
548                 report_exception ();
549                 /* This will terminate the program */
550                 return false;
551         }
552
553         void OnUnhandledException () override
554         {
555                 report_exception ();
556         }
557
558         void idle ()
559         {
560                 signal_manager->ui_idle ();
561         }
562
563         DOMFrame* _frame = nullptr;
564         optional<boost::filesystem::path> _dcp_to_load;
565 };
566
567
568 IMPLEMENT_APP (App)