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