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