Allow specification of video bit rate separately for J2K and MPEG2.
[dcpomatic.git] / src / tools / dcpomatic_combiner.cc
1 /*
2     Copyright (C) 2020-2021 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/dir_dialog.h"
23 #include "wx/dir_picker_ctrl.h"
24 #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/combine_dcp_job.h"
29 #include "lib/config.h"
30 #include "lib/constants.h"
31 #include "lib/cross.h"
32 #include "lib/job_manager.h"
33 #include "lib/util.h"
34 #include <dcp/combine.h>
35 LIBDCP_DISABLE_WARNINGS
36 #include <wx/filepicker.h>
37 LIBDCP_ENABLE_WARNINGS
38 #include <wx/wx.h>
39 #include <boost/bind/bind.hpp>
40 #include <boost/filesystem.hpp>
41 #include <exception>
42
43
44 using std::dynamic_pointer_cast;
45 using std::exception;
46 using std::make_shared;
47 using std::shared_ptr;
48 using std::string;
49 using std::vector;
50 using boost::optional;
51 #if BOOST_VERSION >= 106100
52 using namespace boost::placeholders;
53 #endif
54
55
56 static string
57 display_string (boost::filesystem::path p, int)
58 {
59         return p.filename().string();
60 }
61
62
63 class DirDialogWrapper : public DirDialog
64 {
65 public:
66         DirDialogWrapper (wxWindow* parent)
67                 : DirDialog (parent, _("Choose a DCP folder"), wxDD_DIR_MUST_EXIST, "AddCombinerInputPath")
68         {
69
70         }
71
72         virtual int ShowModal() override
73         {
74                 return DirDialog::show() ? wxID_OK : wxID_CANCEL;
75         }
76
77         optional<boost::filesystem::path> get () const
78         {
79                 return path();
80         }
81
82         void set (boost::filesystem::path)
83         {
84                 /* Not used */
85         }
86 };
87
88
89 class DOMFrame : public wxFrame
90 {
91 public:
92         explicit DOMFrame (wxString const & title)
93                 : wxFrame (nullptr, -1, title)
94         {
95                 /* Use a panel as the only child of the Frame so that we avoid
96                    the dark-grey background on Windows.
97                 */
98                 auto overall_panel = new wxPanel (this);
99                 auto s = new wxBoxSizer (wxHORIZONTAL);
100                 s->Add (overall_panel, 1, wxEXPAND);
101                 SetSizer (s);
102
103                 vector<EditableListColumn> columns;
104                 columns.push_back(EditableListColumn(_("Input DCP"), 600, true));
105
106                 _input = new EditableList<boost::filesystem::path, DirDialogWrapper>(
107                         overall_panel,
108                         columns,
109                         boost::bind(&DOMFrame::inputs, this),
110                         boost::bind(&DOMFrame::set_inputs, this, _1),
111                         &display_string,
112                         EditableListTitle::VISIBLE,
113                         EditableListButton::NEW | EditableListButton::REMOVE
114                         );
115
116                 auto output = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
117                 output->AddGrowableCol (1, 1);
118
119                 add_label_to_sizer (output, overall_panel, _("Annotation text"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL);
120                 _annotation_text = new wxTextCtrl (overall_panel, wxID_ANY, wxT(""));
121                 output->Add (_annotation_text, 1, wxEXPAND);
122
123                 add_label_to_sizer (output, overall_panel, _("Output DCP folder"), true, 0, wxLEFT | wxRIGHT | wxALIGN_CENTRE_VERTICAL);
124                 _output = new DirPickerCtrl (overall_panel);
125                 output->Add (_output, 1, wxEXPAND);
126
127                 _combine = new Button (overall_panel, _("Combine"));
128
129                 auto sizer = new wxBoxSizer (wxVERTICAL);
130                 sizer->Add (_input, 1, wxALL | wxEXPAND, DCPOMATIC_DIALOG_BORDER);
131                 sizer->Add (output, 0, wxALL | wxEXPAND, DCPOMATIC_DIALOG_BORDER);
132                 sizer->Add (_combine, 0, wxALL | wxALIGN_RIGHT, DCPOMATIC_DIALOG_BORDER);
133                 overall_panel->SetSizer (sizer);
134                 Fit ();
135                 SetSize (768, GetSize().GetHeight() + 32);
136
137                 _combine->Bind (wxEVT_BUTTON, boost::bind(&DOMFrame::combine, this));
138                 _output->Bind (wxEVT_DIRPICKER_CHANGED, boost::bind(&DOMFrame::setup_sensitivity, this));
139
140                 setup_sensitivity ();
141         }
142
143 private:
144         void set_inputs (vector<boost::filesystem::path> inputs)
145         {
146                 _inputs = inputs;
147         }
148
149         vector<boost::filesystem::path> inputs () const
150         {
151                 return _inputs;
152         }
153
154         void combine ()
155         {
156                 using namespace boost::filesystem;
157
158                 path const output = wx_to_std(_output->GetPath());
159
160                 if (is_directory(output) && !is_empty(output)) {
161                         if (!confirm_dialog (
162                                     this,
163                                     std_to_wx (
164                                             String::compose(wx_to_std(_("The directory %1 already exists and is not empty.  "
165                                                                         "Are you sure you want to use it?")),
166                                                             output.string())
167                                             )
168                                     )) {
169                                 return;
170                         }
171                 } else if (is_regular_file(output)) {
172                         error_dialog (
173                                 this,
174                                 String::compose (wx_to_std(_("%1 already exists as a file, so you cannot use it for a DCP.")), output.string())
175                                 );
176                         return;
177                 }
178
179                 auto jm = JobManager::instance ();
180                 jm->add (make_shared<CombineDCPJob>(_inputs, output, wx_to_std(_annotation_text->GetValue())));
181                 bool const ok = display_progress(variant::wx::dcpomatic_combiner(), _("Combining DCPs"));
182                 if (!ok) {
183                         return;
184                 }
185
186                 DCPOMATIC_ASSERT (!jm->get().empty());
187                 auto last = dynamic_pointer_cast<CombineDCPJob> (jm->get().back());
188                 DCPOMATIC_ASSERT (last);
189
190                 if (last->finished_ok()) {
191                         message_dialog (this, _("DCPs combined successfully."));
192                 } else {
193                         auto m = std_to_wx(last->error_summary());
194                         if (!last->error_details().empty()) {
195                                 m += wxString::Format(" (%s)", std_to_wx(last->error_details()));
196                         }
197                         error_dialog (this, m);
198                 }
199         }
200
201         void setup_sensitivity ()
202         {
203                 _combine->Enable (!_output->GetPath().IsEmpty());
204         }
205
206         EditableList<boost::filesystem::path, DirDialogWrapper>* _input;
207         wxTextCtrl* _annotation_text = nullptr;
208         DirPickerCtrl* _output;
209         vector<boost::filesystem::path> _inputs;
210         Button* _combine;
211 };
212
213
214 class App : public wxApp
215 {
216 public:
217         App () {}
218
219         bool OnInit () override
220         {
221                 try {
222                         Config::FailedToLoad.connect(boost::bind(&App::config_failed_to_load, this, _1));
223                         Config::Warning.connect (boost::bind (&App::config_warning, this, _1));
224
225                         SetAppName(variant::wx::dcpomatic_combiner());
226
227                         if (!wxApp::OnInit()) {
228                                 return false;
229                         }
230
231 #ifdef DCPOMATIC_LINUX
232                         unsetenv ("UBUNTU_MENUPROXY");
233 #endif
234
235 #ifdef DCPOMATIC_OSX
236                         make_foreground_application ();
237 #endif
238
239                         dcpomatic_setup_path_encoding ();
240
241                         /* Enable i18n; this will create a Config object
242                            to look for a force-configured language.  This Config
243                            object will be wrong, however, because dcpomatic_setup
244                            hasn't yet been called and there aren't any filters etc.
245                            set up yet.
246                         */
247                         dcpomatic_setup_i18n ();
248
249                         /* Set things up, including filters etc.
250                            which will now be internationalised correctly.
251                         */
252                         dcpomatic_setup ();
253
254                         /* Force the configuration to be re-loaded correctly next
255                            time it is needed.
256                         */
257                         Config::drop ();
258
259                         _frame = new DOMFrame(variant::wx::dcpomatic_combiner());
260                         SetTopWindow (_frame);
261
262                         _frame->Show ();
263
264                         signal_manager = new wxSignalManager (this);
265                         Bind (wxEVT_IDLE, boost::bind(&App::idle, this, _1));
266                 }
267                 catch (exception& e)
268                 {
269                         error_dialog(nullptr, wxString::Format(_("%s could not start (%s)"), variant::wx::dcpomatic_combiner()), std_to_wx(e.what()));
270                         return false;
271                 }
272
273                 return true;
274         }
275
276         void config_failed_to_load(Config::LoadFailure what)
277         {
278                 report_config_load_failure(_frame, what);
279         }
280
281         void config_warning (string m)
282         {
283                 message_dialog (_frame, std_to_wx(m));
284         }
285
286         void idle (wxIdleEvent& ev)
287         {
288                 signal_manager->ui_idle ();
289                 ev.Skip ();
290         }
291
292         void report_exception ()
293         {
294                 try {
295                         throw;
296                 } catch (FileError& e) {
297                         error_dialog (
298                                 0,
299                                 wxString::Format(
300                                         _("An exception occurred: %s (%s)\n\n") + REPORT_PROBLEM,
301                                         std_to_wx (e.what()),
302                                         std_to_wx (e.file().string().c_str ())
303                                         )
304                                 );
305                 } catch (exception& e) {
306                         error_dialog (
307                                 0,
308                                 wxString::Format(
309                                         _("An exception occurred: %s.\n\n") + REPORT_PROBLEM,
310                                         std_to_wx (e.what ())
311                                         )
312                                 );
313                 } catch (...) {
314                         error_dialog (nullptr, _("An unknown exception occurred.") + "  " + REPORT_PROBLEM);
315                 }
316         }
317
318         bool OnExceptionInMainLoop () override
319         {
320                 report_exception ();
321                 /* This will terminate the program */
322                 return false;
323         }
324
325         void OnUnhandledException () override
326         {
327                 report_exception ();
328         }
329
330         DOMFrame* _frame = nullptr;
331 };
332
333 IMPLEMENT_APP (App)