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