Cleanup: use ScopeGuard.
[dcpomatic.git] / src / tools / dcpomatic_kdm.cc
1 /*
2     Copyright (C) 2015-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/about_dialog.h"
23 #include "wx/dcpomatic_button.h"
24 #include "wx/editable_list.h"
25 #include "wx/file_picker_ctrl.h"
26 #include "wx/full_config_dialog.h"
27 #include "wx/job_view_dialog.h"
28 #include "wx/kdm_output_panel.h"
29 #include "wx/kdm_timing_panel.h"
30 #include "wx/nag_dialog.h"
31 #include "wx/new_dkdm_folder_dialog.h"
32 #include "wx/report_problem_dialog.h"
33 #include "wx/screens_panel.h"
34 #include "wx/static_text.h"
35 #include "wx/wx_signal_manager.h"
36 #include "wx/wx_util.h"
37 #include "lib/cinema.h"
38 #include "lib/collator.h"
39 #include "lib/compose.hpp"
40 #include "lib/constants.h"
41 #include "lib/config.h"
42 #include "lib/cross.h"
43 #include "lib/dcpomatic_log.h"
44 #include "lib/dkdm_wrapper.h"
45 #include "lib/exceptions.h"
46 #include "lib/file_log.h"
47 #include "lib/job_manager.h"
48 #include "lib/kdm_with_metadata.h"
49 #include "lib/scope_guard.h"
50 #include "lib/screen.h"
51 #include "lib/send_kdm_email_job.h"
52 #include <dcp/encrypted_kdm.h>
53 #include <dcp/decrypted_kdm.h>
54 #include <dcp/exceptions.h>
55 #include <dcp/warnings.h>
56 LIBDCP_DISABLE_WARNINGS
57 #include <wx/filepicker.h>
58 #include <wx/preferences.h>
59 #include <wx/splash.h>
60 #include <wx/srchctrl.h>
61 #include <wx/treectrl.h>
62 #include <wx/wx.h>
63 LIBDCP_ENABLE_WARNINGS
64 #ifdef __WXOSX__
65 #include <ApplicationServices/ApplicationServices.h>
66 #endif
67 #include <boost/bind/bind.hpp>
68 #include <unordered_set>
69
70 #ifdef check
71 #undef check
72 #endif
73
74
75 using std::exception;
76 using std::list;
77 using std::make_shared;
78 using std::map;
79 using std::pair;
80 using std::shared_ptr;
81 using std::string;
82 using std::unordered_set;
83 using std::vector;
84 using boost::bind;
85 using boost::optional;
86 using boost::ref;
87 using std::dynamic_pointer_cast;
88 #if BOOST_VERSION >= 106100
89 using namespace boost::placeholders;
90 #endif
91 using namespace dcpomatic;
92
93
94 enum {
95         ID_help_report_a_problem = 1,
96 };
97
98
99 class DOMFrame : public wxFrame
100 {
101 public:
102         explicit DOMFrame (wxString const & title)
103                 : wxFrame (nullptr, -1, title)
104                 , _config_dialog (nullptr)
105                 , _job_view (nullptr)
106         {
107 #if defined(DCPOMATIC_WINDOWS)
108                 if (Config::instance()->win32_console ()) {
109                         AllocConsole();
110
111                         HANDLE handle_out = GetStdHandle(STD_OUTPUT_HANDLE);
112                         int hCrt = _open_osfhandle((intptr_t) handle_out, _O_TEXT);
113                         FILE* hf_out = _fdopen(hCrt, "w");
114                         setvbuf(hf_out, NULL, _IONBF, 1);
115                         *stdout = *hf_out;
116
117                         HANDLE handle_in = GetStdHandle(STD_INPUT_HANDLE);
118                         hCrt = _open_osfhandle((intptr_t) handle_in, _O_TEXT);
119                         FILE* hf_in = _fdopen(hCrt, "r");
120                         setvbuf(hf_in, NULL, _IONBF, 128);
121                         *stdin = *hf_in;
122
123                         std::cout << "DCP-o-matic KDM creator is starting." << "\n";
124                 }
125 #endif
126
127                 auto bar = new wxMenuBar;
128                 setup_menu (bar);
129                 SetMenuBar (bar);
130
131                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_exit, this),             wxID_EXIT);
132                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::edit_preferences, this),      wxID_PREFERENCES);
133                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::help_about, this),            wxID_ABOUT);
134                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::help_report_a_problem, this), ID_help_report_a_problem);
135
136                 /* Use a panel as the only child of the Frame so that we avoid
137                    the dark-grey background on Windows.
138                 */
139                 auto overall_panel = new wxPanel (this, wxID_ANY);
140                 auto main_sizer = new wxBoxSizer (wxHORIZONTAL);
141
142                 auto horizontal = new wxBoxSizer (wxHORIZONTAL);
143                 auto left = new wxBoxSizer (wxVERTICAL);
144                 auto right = new wxBoxSizer (wxVERTICAL);
145
146                 horizontal->Add (left, 1, wxEXPAND | wxRIGHT, DCPOMATIC_SIZER_X_GAP * 2);
147                 horizontal->Add (right, 1, wxEXPAND);
148
149                 wxFont subheading_font (*wxNORMAL_FONT);
150                 subheading_font.SetWeight (wxFONTWEIGHT_BOLD);
151
152                 auto h = new StaticText (overall_panel, _("Screens"));
153                 h->SetFont (subheading_font);
154                 left->Add (h, 0, wxBOTTOM, DCPOMATIC_SIZER_Y_GAP);
155                 _screens = new ScreensPanel (overall_panel);
156                 left->Add (_screens, 1, wxEXPAND | wxBOTTOM, DCPOMATIC_SIZER_Y_GAP);
157
158                 /// TRANSLATORS: translate the word "Timing" here; do not include the "KDM|" prefix
159                 h = new StaticText (overall_panel, S_("KDM|Timing"));
160                 h->SetFont (subheading_font);
161                 right->Add (h);
162                 _timing = new KDMTimingPanel (overall_panel);
163                 right->Add (_timing, 0, wxALL, DCPOMATIC_SIZER_Y_GAP);
164
165                 h = new StaticText (overall_panel, _("DKDM"));
166                 h->SetFont (subheading_font);
167                 right->Add (h, 0, wxTOP, DCPOMATIC_SIZER_Y_GAP * 2);
168
169                 _dkdm_search = new wxSearchCtrl(overall_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(200, search_ctrl_height()));
170 #ifndef __WXGTK3__
171                 /* The cancel button seems to be strangely broken in GTK3; clicking on it twice sometimes works */
172                 _dkdm_search->ShowCancelButton (true);
173 #endif
174
175                 right->Add(_dkdm_search, 0, wxTOP | wxBOTTOM, DCPOMATIC_SIZER_Y_GAP);
176
177                 auto dkdm_sizer = new wxBoxSizer (wxHORIZONTAL);
178                 _dkdm = new wxTreeCtrl (
179                         overall_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTR_HIDE_ROOT | wxTR_HAS_BUTTONS | wxTR_LINES_AT_ROOT
180                 );
181                 dkdm_sizer->Add(_dkdm, 1, wxEXPAND | wxBOTTOM, DCPOMATIC_SIZER_Y_GAP);
182                 wxBoxSizer* dkdm_buttons = new wxBoxSizer(wxVERTICAL);
183                 _add_dkdm = new Button (overall_panel, _("Add..."));
184                 dkdm_buttons->Add (_add_dkdm, 0, wxALL | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
185                 _add_dkdm_folder = new Button (overall_panel, _("Add folder..."));
186                 dkdm_buttons->Add (_add_dkdm_folder, 0, wxALL | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
187                 _remove_dkdm = new Button (overall_panel, _("Remove"));
188                 dkdm_buttons->Add (_remove_dkdm, 0, wxALL | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
189                 _export_dkdm = new Button (overall_panel, _("Export..."));
190                 dkdm_buttons->Add (_export_dkdm, 0, wxALL | wxEXPAND, DCPOMATIC_BUTTON_STACK_GAP);
191                 dkdm_sizer->Add (dkdm_buttons, 0, wxEXPAND | wxALL, DCPOMATIC_SIZER_GAP);
192                 right->Add (dkdm_sizer, 1, wxEXPAND | wxBOTTOM, DCPOMATIC_SIZER_Y_GAP);
193
194                 update_dkdm_view();
195
196                 h = new StaticText (overall_panel, _("Output"));
197                 h->SetFont (subheading_font);
198                 right->Add (h, 0, wxTOP, DCPOMATIC_SIZER_Y_GAP * 2);
199                 _output = new KDMOutputPanel (overall_panel);
200                 right->Add (_output, 0, wxALL, DCPOMATIC_SIZER_Y_GAP);
201
202                 _create = new Button (overall_panel, _("Create KDMs"));
203                 right->Add (_create, 0, wxALL, DCPOMATIC_SIZER_GAP);
204
205                 main_sizer->Add (horizontal, 1, wxALL | wxEXPAND, DCPOMATIC_DIALOG_BORDER);
206                 overall_panel->SetSizer (main_sizer);
207
208                 /* Instantly save any config changes when using a DCP-o-matic GUI */
209                 Config::instance()->Changed.connect (boost::bind (&Config::write, Config::instance ()));
210
211                 _screens->ScreensChanged.connect (boost::bind (&DOMFrame::setup_sensitivity, this));
212                 _create->Bind (wxEVT_BUTTON, bind (&DOMFrame::create_kdms, this));
213                 _dkdm->Bind(wxEVT_TREE_SEL_CHANGED, boost::bind(&DOMFrame::dkdm_selection_changed, this));
214                 _dkdm->Bind (wxEVT_TREE_BEGIN_DRAG, boost::bind (&DOMFrame::dkdm_begin_drag, this, _1));
215                 _dkdm->Bind (wxEVT_TREE_END_DRAG, boost::bind (&DOMFrame::dkdm_end_drag, this, _1));
216                 _dkdm->Bind(wxEVT_TREE_ITEM_EXPANDED, boost::bind(&DOMFrame::dkdm_expanded, this, _1));
217                 _dkdm->Bind(wxEVT_TREE_ITEM_COLLAPSED, boost::bind(&DOMFrame::dkdm_collapsed, this, _1));
218                 _add_dkdm->Bind (wxEVT_BUTTON, bind (&DOMFrame::add_dkdm_clicked, this));
219                 _add_dkdm_folder->Bind (wxEVT_BUTTON, bind (&DOMFrame::add_dkdm_folder_clicked, this));
220                 _remove_dkdm->Bind (wxEVT_BUTTON, bind (&DOMFrame::remove_dkdm_clicked, this));
221                 _export_dkdm->Bind (wxEVT_BUTTON, bind (&DOMFrame::export_dkdm_clicked, this));
222                 _dkdm_search->Bind(wxEVT_TEXT, boost::bind(&DOMFrame::dkdm_search_changed, this));
223
224                 setup_sensitivity ();
225
226                 dcpomatic_log = make_shared<FileLog>(State::write_path("kdm.log"));
227         }
228
229 private:
230         void file_exit ()
231         {
232                 /* false here allows the close handler to veto the close request */
233                 Close (false);
234         }
235
236         void edit_preferences ()
237         {
238                 if (!_config_dialog) {
239                         _config_dialog = create_full_config_dialog ();
240                 }
241                 _config_dialog->Show (this);
242         }
243
244         void help_about ()
245         {
246                 auto d = new AboutDialog (this);
247                 d->ShowModal ();
248                 d->Destroy ();
249         }
250
251         void help_report_a_problem ()
252         {
253                 auto d = new ReportProblemDialog (this, shared_ptr<Film>());
254                 if (d->ShowModal () == wxID_OK) {
255                         d->report ();
256                 }
257                 d->Destroy ();
258         }
259
260         void setup_menu (wxMenuBar* m)
261         {
262                 auto file = new wxMenu;
263
264 #ifdef __WXOSX__
265                 file->Append (wxID_EXIT, _("&Exit"));
266 #else
267                 file->Append (wxID_EXIT, _("&Quit"));
268 #endif
269
270 #ifdef __WXOSX__
271                 file->Append (wxID_PREFERENCES, _("&Preferences...\tCtrl-P"));
272 #else
273                 wxMenu* edit = new wxMenu;
274                 edit->Append (wxID_PREFERENCES, _("&Preferences...\tCtrl-P"));
275 #endif
276
277                 wxMenu* help = new wxMenu;
278 #ifdef __WXOSX__
279                 help->Append (wxID_ABOUT, _("About DCP-o-matic"));
280 #else
281                 help->Append (wxID_ABOUT, _("About"));
282 #endif
283                 help->Append (ID_help_report_a_problem, _("Report a problem..."));
284
285                 m->Append (file, _("&File"));
286 #ifndef __WXOSX__
287                 m->Append (edit, _("&Edit"));
288 #endif
289                 m->Append (help, _("&Help"));
290         }
291
292         bool confirm_overwrite (boost::filesystem::path path)
293         {
294                 return confirm_dialog (
295                         this,
296                         wxString::Format (_("File %s already exists.  Do you want to overwrite it?"), std_to_wx(path.string()).data())
297                         );
298         }
299
300         /** @id if not nullptr this is filled in with the wxTreeItemId of the selection */
301         shared_ptr<DKDMBase> selected_dkdm (wxTreeItemId* id = nullptr) const
302         {
303                 wxArrayTreeItemIds selections;
304                 _dkdm->GetSelections (selections);
305                 if (selections.GetCount() != 1) {
306                         if (id) {
307                                 *id = 0;
308                         }
309                         return {};
310                 }
311
312                 if (id) {
313                         *id = selections[0];
314                 }
315
316                 auto i = _dkdm_id.find (selections[0]);
317                 if (i == _dkdm_id.end()) {
318                         return {};
319                 }
320
321                 return i->second;
322         }
323
324         void create_kdms ()
325         {
326                 try {
327                         auto dkdm_base = selected_dkdm ();
328                         if (!dkdm_base) {
329                                 return;
330                         }
331
332                         list<KDMWithMetadataPtr> kdms;
333                         string title;
334
335                         auto dkdm = std::dynamic_pointer_cast<DKDM>(dkdm_base);
336                         if (dkdm) {
337
338                                 /* Decrypt the DKDM */
339                                 dcp::DecryptedKDM decrypted (dkdm->dkdm(), Config::instance()->decryption_chain()->key().get());
340                                 title = decrypted.content_title_text ();
341
342                                 /* This is the signer for our new KDMs */
343                                 auto signer = Config::instance()->signer_chain ();
344                                 if (!signer->valid ()) {
345                                         throw InvalidSignerError ();
346                                 }
347
348                                 for (auto i: _screens->screens()) {
349
350                                         if (!i->recipient) {
351                                                 continue;
352                                         }
353
354                                         dcp::LocalTime begin(_timing->from(), dcp::UTCOffset(i->cinema->utc_offset_hour(), i->cinema->utc_offset_minute()));
355                                         dcp::LocalTime end(_timing->until(), dcp::UTCOffset(i->cinema->utc_offset_hour(), i->cinema->utc_offset_minute()));
356
357                                         /* Make an empty KDM */
358                                         dcp::DecryptedKDM kdm (
359                                                 begin,
360                                                 end,
361                                                 decrypted.annotation_text().get_value_or (""),
362                                                 title,
363                                                 dcp::LocalTime().as_string()
364                                                 );
365
366                                         /* Add keys from the DKDM */
367                                         for (auto const& j: decrypted.keys()) {
368                                                 kdm.add_key (j);
369                                         }
370
371                                         auto const encrypted = kdm.encrypt(
372                                                         signer, i->recipient.get(), i->trusted_device_thumbprints(), _output->formulation(),
373                                                         !_output->forensic_mark_video(), _output->forensic_mark_audio() ? boost::optional<int>() : 0
374                                                         );
375
376                                         dcp::NameFormat::Map name_values;
377                                         name_values['c'] = i->cinema->name;
378                                         name_values['s'] = i->name;
379                                         name_values['f'] = title;
380                                         name_values['b'] = begin.date() + " " + begin.time_of_day(true, false);
381                                         name_values['e'] = end.date() + " " + end.time_of_day(true, false);
382                                         name_values['i'] = encrypted.cpl_id ();
383
384                                         /* Encrypt */
385                                         kdms.push_back (make_shared<KDMWithMetadata>(name_values, i->cinema.get(), i->cinema->emails, encrypted));
386                                 }
387                         }
388
389                         if (kdms.empty()) {
390                                 return;
391                         }
392
393                         auto result = _output->make (
394                                 kdms, title, bind (&DOMFrame::confirm_overwrite, this, _1)
395                                 );
396
397                         if (result.first) {
398                                 JobManager::instance()->add (result.first);
399                                 if (_job_view) {
400                                         _job_view->Destroy ();
401                                         _job_view = 0;
402                                 }
403                                 _job_view = new JobViewDialog (this, _("Send KDM emails"), result.first);
404                                 _job_view->ShowModal ();
405                         }
406
407                         if (result.second > 0) {
408                                 /* XXX: proper plural form support in wxWidgets? */
409                                 wxString s = result.second == 1 ? _("%d KDM written to %s") : _("%d KDMs written to %s");
410                                 message_dialog (
411                                         this,
412                                         wxString::Format (s, result.second, std_to_wx(_output->directory().string()).data())
413                                         );
414                         }
415                 } catch (dcp::NotEncryptedError& e) {
416                         error_dialog (this, _("CPL's content is not encrypted."));
417                 } catch (exception& e) {
418                         error_dialog (this, std_to_wx(e.what()));
419                 } catch (...) {
420                         error_dialog (this, _("An unknown exception occurred."));
421                 }
422         }
423
424         void setup_sensitivity ()
425         {
426                 _screens->setup_sensitivity ();
427                 _output->setup_sensitivity ();
428                 wxArrayTreeItemIds sel;
429                 _dkdm->GetSelections (sel);
430                 auto group = dynamic_pointer_cast<DKDMGroup>(selected_dkdm());
431                 auto dkdm = dynamic_pointer_cast<DKDM>(selected_dkdm());
432                 _create->Enable (!_screens->screens().empty() && sel.GetCount() > 0 && dkdm);
433                 _remove_dkdm->Enable (sel.GetCount() > 0 && (!group || group->name() != "root"));
434                 _export_dkdm->Enable (sel.GetCount() > 0 && dkdm);
435         }
436
437         void dkdm_selection_changed()
438         {
439                 _selected_dkdm = selected_dkdm();
440                 setup_sensitivity();
441         }
442
443         void dkdm_expanded(wxTreeEvent& ev)
444         {
445                 if (_ignore_expand) {
446                         return;
447                 }
448
449                 auto iter = _dkdm_id.find(ev.GetItem());
450                 if (iter != _dkdm_id.end()) {
451                         _expanded_dkdm_groups.insert(iter->second);
452                 }
453         }
454
455         void dkdm_collapsed(wxTreeEvent& ev)
456         {
457                 auto iter = _dkdm_id.find(ev.GetItem());
458                 if (iter != _dkdm_id.end()) {
459                         _expanded_dkdm_groups.erase(iter->second);
460                 }
461         }
462
463         void dkdm_begin_drag (wxTreeEvent& ev)
464         {
465                 ev.Allow ();
466         }
467
468         void dkdm_end_drag (wxTreeEvent& ev)
469         {
470                 auto from = _dkdm_id.find (_dkdm->GetSelection ());
471                 auto to = _dkdm_id.find (ev.GetItem ());
472                 if (from == _dkdm_id.end() || to == _dkdm_id.end() || from->first == to->first) {
473                         return;
474                 }
475
476                 auto group = dynamic_pointer_cast<DKDMGroup> (to->second);
477                 if (!group) {
478                         group = to->second->parent();
479                 }
480
481                 DCPOMATIC_ASSERT (group);
482                 DCPOMATIC_ASSERT (from->second->parent ());
483
484                 from->second->parent()->remove (from->second);
485                 add_dkdm(from->second, group, dynamic_pointer_cast<DKDM>(to->second));
486
487                 update_dkdm_view();
488         }
489
490         void add_dkdm_clicked ()
491         {
492                 auto d = new wxFileDialog(
493                         this,
494                         _("Select DKDM file"),
495                         wxEmptyString,
496                         wxEmptyString,
497                         wxT("XML files|*.xml|All files|*.*"),
498                         wxFD_MULTIPLE
499                         );
500
501                 ScopeGuard sg = [d]() { d->Destroy(); };
502
503                 if (d->ShowModal() != wxID_OK) {
504                         return;
505                 }
506
507                 auto chain = Config::instance()->decryption_chain();
508                 DCPOMATIC_ASSERT (chain->key());
509
510                 wxArrayString paths;
511                 d->GetPaths(paths);
512                 for (unsigned int i = 0; i < paths.GetCount(); ++i) {
513                         try {
514                                 dcp::EncryptedKDM ekdm(dcp::file_to_string(wx_to_std(paths[i]), MAX_KDM_SIZE));
515                                 /* Decrypt the DKDM to make sure that we can */
516                                 dcp::DecryptedKDM dkdm(ekdm, chain->key().get());
517
518                                 auto new_dkdm = make_shared<DKDM>(ekdm);
519                                 auto group = dynamic_pointer_cast<DKDMGroup> (selected_dkdm());
520                                 if (!group) {
521                                         group = Config::instance()->dkdms ();
522                                 }
523                                 add_dkdm(new_dkdm, group);
524                         } catch (dcp::KDMFormatError& e) {
525                                 error_dialog (
526                                         this,
527                                         _("Could not read file as a KDM.  Perhaps it is badly formatted, or not a KDM at all."),
528                                         std_to_wx(e.what())
529                                         );
530                                 return;
531                         } catch (dcp::KDMDecryptionError &) {
532                                 error_dialog (
533                                         this,
534                                         _("Could not decrypt the DKDM.  Perhaps it was not created with the correct certificate.")
535                                         );
536                         } catch (dcp::MiscError& e) {
537                                 error_dialog (
538                                         this,
539                                         _("Could not read file as a KDM.  It is much too large.  Make sure you are loading a DKDM (XML) file."),
540                                         std_to_wx(e.what())
541                                         );
542                         }
543                 }
544                 update_dkdm_view();
545         }
546
547         void add_dkdm_folder_clicked ()
548         {
549                 auto d = new NewDKDMFolderDialog (this);
550                 if (d->ShowModal() == wxID_OK) {
551                         auto new_dkdm = make_shared<DKDMGroup>(wx_to_std(d->get()));
552                         auto parent = dynamic_pointer_cast<DKDMGroup>(selected_dkdm());
553                         if (!parent) {
554                                 parent = Config::instance()->dkdms ();
555                         }
556                         add_dkdm(new_dkdm, parent);
557                         update_dkdm_view();
558                 }
559                 d->Destroy ();
560         }
561
562         void update_dkdm_view()
563         {
564                 _dkdm->DeleteAllItems();
565                 _dkdm_id.clear();
566                 add_dkdm_to_view(Config::instance()->dkdms());
567                 if (_selected_dkdm) {
568                         auto selection_in_id_map = std::find_if(_dkdm_id.begin(), _dkdm_id.end(), [this](pair<wxTreeItemId, shared_ptr<DKDMBase>> const& entry) {
569                                 return entry.second == _selected_dkdm;
570                         });
571                         if (selection_in_id_map != _dkdm_id.end()) {
572                                 _dkdm->SelectItem(selection_in_id_map->first);
573                         }
574                 }
575         }
576
577         /** @return true if this thing or any of its children match a search string */
578         bool matches(shared_ptr<DKDMBase> base, string const& search)
579         {
580                 if (search.empty()) {
581                         return true;
582                 }
583
584                 auto name = base->name();
585                 transform(name.begin(), name.end(), name.begin(), ::tolower);
586                 if (name.find(search) != string::npos) {
587                         return true;
588                 }
589
590                 auto group = dynamic_pointer_cast<DKDMGroup>(base);
591                 if (!group) {
592                         return false;
593                 }
594
595                 auto const children = group->children();
596                 return std::any_of(children.begin(), children.end(), [this, search](shared_ptr<DKDMBase> child) {
597                         return matches(child, search);
598                 });
599         }
600
601         /** Add DKDMs to the view that match the current search */
602         void add_dkdm_to_view(shared_ptr<DKDMBase> base)
603         {
604                 auto search = wx_to_std(_dkdm_search->GetValue());
605                 transform(search.begin(), search.end(), search.begin(), ::tolower);
606
607                 optional<wxTreeItemId> group_to_expand;
608
609                 if (!base->parent()) {
610                         /* This is the root group */
611                         _dkdm_id[_dkdm->AddRoot("root")] = base;
612                 } else {
613                         /* Add base to the view */
614                         wxTreeItemId added;
615                         auto parent_id = dkdm_to_id(base->parent());
616                         added = _dkdm->AppendItem(parent_id, std_to_wx(base->name()));
617                         /* Expand the group (later) if it matches the search or it was manually expanded */
618                         if (!search.empty() || _expanded_dkdm_groups.find(base) != _expanded_dkdm_groups.end()) {
619                                 group_to_expand = added;
620                         }
621                         _dkdm_id[added] = base;
622                 }
623
624                 /* Add children */
625                 auto group = dynamic_pointer_cast<DKDMGroup>(base);
626                 if (group) {
627                         auto children = group->children();
628                         children.sort(
629                                 [this](shared_ptr<DKDMBase> a, shared_ptr<DKDMBase> b) { return _collator.compare(a->name(), b->name()) < 0; }
630                         );
631
632                         for (auto i: children) {
633                                 if (matches(i, search)) {
634                                         add_dkdm_to_view(i);
635                                 }
636                         }
637                 }
638
639                 if (group_to_expand) {
640                         _ignore_expand = true;
641                         _dkdm->Expand(*group_to_expand);
642                         _ignore_expand = false;
643                 }
644         }
645
646         /** @param group Group to add dkdm to */
647         void add_dkdm(shared_ptr<DKDMBase> dkdm, shared_ptr<DKDMGroup> group, shared_ptr<DKDM> previous = shared_ptr<DKDM>())
648         {
649                 group->add (dkdm, previous);
650                 /* We're messing with a Config-owned object here, so tell it that something has changed.
651                    This isn't nice.
652                 */
653                 Config::instance()->changed ();
654         }
655
656         wxTreeItemId dkdm_to_id (shared_ptr<DKDMBase> dkdm)
657         {
658                 for (auto const& i: _dkdm_id) {
659                         if (i.second == dkdm) {
660                                 return i.first;
661                         }
662                 }
663                 DCPOMATIC_ASSERT (false);
664         }
665
666         void remove_dkdm_clicked ()
667         {
668                 auto removed = selected_dkdm ();
669                 if (!removed) {
670                         return;
671                 }
672
673                 if (NagDialog::maybe_nag (
674                             this, Config::NAG_DELETE_DKDM,
675                             _("You are about to remove a DKDM.  This will make it impossible to decrypt the DCP that the DKDM was made for, and it cannot be undone.  "
676                               "Are you sure?"),
677                             true)) {
678                         return;
679                 }
680
681                 _dkdm->Delete (dkdm_to_id (removed));
682                 auto dkdms = Config::instance()->dkdms ();
683                 dkdms->remove (removed);
684                 Config::instance()->changed ();
685         }
686
687         void export_dkdm_clicked ()
688         {
689                 auto removed = selected_dkdm ();
690                 if (!removed) {
691                         return;
692                 }
693
694                 auto dkdm = dynamic_pointer_cast<DKDM>(removed);
695                 if (!dkdm) {
696                         return;
697                 }
698
699                 auto d = new wxFileDialog (
700                         this, _("Select DKDM File"), wxEmptyString, wxEmptyString, wxT("XML files (*.xml)|*.xml"),
701                         wxFD_SAVE | wxFD_OVERWRITE_PROMPT
702                         );
703
704                 if (d->ShowModal() == wxID_OK) {
705                         dkdm->dkdm().as_xml(wx_to_std(d->GetPath()));
706                 }
707                 d->Destroy ();
708         }
709
710         void dkdm_search_changed()
711         {
712                 update_dkdm_view();
713         }
714
715         wxPreferencesEditor* _config_dialog;
716         ScreensPanel* _screens;
717         KDMTimingPanel* _timing;
718         wxTreeCtrl* _dkdm;
719         wxSearchCtrl* _dkdm_search;
720         typedef std::map<wxTreeItemId, std::shared_ptr<DKDMBase>> DKDMMap;
721         DKDMMap _dkdm_id;
722         /* Keep a separate track of the selected DKDM so that when a search happens, and some things
723          * get removed from the view, we can restore the selection when they are re-added.
724          */
725         shared_ptr<DKDMBase> _selected_dkdm;
726         /* Keep expanded groups for the same reason */
727         unordered_set<shared_ptr<DKDMBase>> _expanded_dkdm_groups;
728         /* true if we are "artificially" expanding a group because it contains something found
729          * in a search.
730          */
731         bool _ignore_expand = false;
732         wxButton* _add_dkdm;
733         wxButton* _add_dkdm_folder;
734         wxButton* _remove_dkdm;
735         wxButton* _export_dkdm;
736         wxButton* _create;
737         KDMOutputPanel* _output;
738         JobViewDialog* _job_view;
739         Collator _collator;
740 };
741
742
743 /** @class App
744  *  @brief The magic App class for wxWidgets.
745  */
746 class App : public wxApp
747 {
748 public:
749         App ()
750                 : wxApp ()
751                 , _frame (nullptr)
752         {}
753
754 private:
755
756         bool OnInit () override
757         {
758                 wxSplashScreen* splash = nullptr;
759
760                 try {
761                         wxInitAllImageHandlers ();
762
763                         Config::FailedToLoad.connect(boost::bind(&App::config_failed_to_load, this, _1));
764                         Config::Warning.connect (boost::bind (&App::config_warning, this, _1));
765
766                         splash = maybe_show_splash ();
767
768                         SetAppName (_("DCP-o-matic KDM Creator"));
769
770                         if (!wxApp::OnInit()) {
771                                 return false;
772                         }
773
774 #ifdef DCPOMATIC_LINUX
775                         unsetenv ("UBUNTU_MENUPROXY");
776 #endif
777
778 #ifdef DCPOMATIC_OSX
779                         make_foreground_application ();
780 #endif
781
782                         dcpomatic_setup_path_encoding ();
783
784                         /* Enable i18n; this will create a Config object
785                            to look for a force-configured language.  This Config
786                            object will be wrong, however, because dcpomatic_setup
787                            hasn't yet been called and there aren't any filters etc.
788                            set up yet.
789                         */
790                         dcpomatic_setup_i18n ();
791
792                         /* Set things up, including filters etc.
793                            which will now be internationalised correctly.
794                         */
795                         dcpomatic_setup ();
796
797                         /* Force the configuration to be re-loaded correctly next
798                            time it is needed.
799                         */
800                         Config::drop ();
801
802                         _frame = new DOMFrame (_("DCP-o-matic KDM Creator"));
803                         SetTopWindow (_frame);
804                         _frame->Maximize ();
805                         if (splash) {
806                                 splash->Destroy ();
807                                 splash = nullptr;
808                         }
809                         _frame->Show ();
810
811                         signal_manager = new wxSignalManager (this);
812                         Bind (wxEVT_IDLE, boost::bind (&App::idle, this));
813                 }
814                 catch (exception& e)
815                 {
816                         if (splash) {
817                                 splash->Destroy ();
818                         }
819                         error_dialog (0, _("DCP-o-matic could not start"), std_to_wx(e.what()));
820                 }
821
822                 return true;
823         }
824
825         /* An unhandled exception has occurred inside the main event loop */
826         bool OnExceptionInMainLoop () override
827         {
828                 try {
829                         throw;
830                 } catch (FileError& e) {
831                         error_dialog (
832                                 0,
833                                 wxString::Format (
834                                         _("An exception occurred: %s (%s)\n\n") + REPORT_PROBLEM,
835                                         std_to_wx (e.what()),
836                                         std_to_wx (e.file().string().c_str ())
837                                         )
838                                 );
839                 } catch (exception& e) {
840                         error_dialog (
841                                 nullptr,
842                                 wxString::Format (
843                                         _("An exception occurred: %s.\n\n") + " " + REPORT_PROBLEM,
844                                         std_to_wx(e.what())
845                                         )
846                                 );
847                 } catch (...) {
848                         error_dialog (0, _("An unknown exception occurred.") + "  " + REPORT_PROBLEM);
849                 }
850
851                 /* This will terminate the program */
852                 return false;
853         }
854
855         void OnUnhandledException () override
856         {
857                 error_dialog (nullptr, _("An unknown exception occurred.") + "  " + REPORT_PROBLEM);
858         }
859
860         void idle ()
861         {
862                 signal_manager->ui_idle ();
863         }
864
865         void config_failed_to_load(Config::LoadFailure what)
866         {
867                 report_config_load_failure(_frame, what);
868         }
869
870         void config_warning (string m)
871         {
872                 message_dialog (_frame, std_to_wx(m));
873         }
874
875         DOMFrame* _frame;
876 };
877
878 IMPLEMENT_APP (App)