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