6c635db0b02c7ac6e5c29de9cea8aa95433aad1e
[dcpomatic.git] / src / wx / content_menu.cc
1 /*
2     Copyright (C) 2013-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 "content_menu.h"
23 #include "repeat_dialog.h"
24 #include "wx_util.h"
25 #include "timeline_video_content_view.h"
26 #include "timeline_audio_content_view.h"
27 #include "content_properties_dialog.h"
28 #include "content_advanced_dialog.h"
29 #include "lib/playlist.h"
30 #include "lib/film.h"
31 #include "lib/image_content.h"
32 #include "lib/content_factory.h"
33 #include "lib/examine_content_job.h"
34 #include "lib/job_manager.h"
35 #include "lib/exceptions.h"
36 #include "lib/dcp_content.h"
37 #include "lib/dcp_examiner.h"
38 #include "lib/ffmpeg_content.h"
39 #include "lib/audio_content.h"
40 #include "lib/config.h"
41 #include "lib/copy_dcp_details_to_film.h"
42 #include <dcp/cpl.h>
43 #include <dcp/exceptions.h>
44 #include <dcp/decrypted_kdm.h>
45 #include <wx/wx.h>
46 #include <wx/dirdlg.h>
47 #include <iostream>
48
49
50 using std::cout;
51 using std::vector;
52 using std::exception;
53 using std::list;
54 using std::shared_ptr;
55 using std::weak_ptr;
56 using std::dynamic_pointer_cast;
57 using std::make_shared;
58 using boost::optional;
59 #if BOOST_VERSION >= 106100
60 using namespace boost::placeholders;
61 #endif
62
63
64 enum {
65         /* Start at 256 so we can have IDs on _cpl_menu from 1 to 255 */
66         ID_repeat = 256,
67         ID_join,
68         ID_find_missing,
69         ID_properties,
70         ID_advanced,
71         ID_re_examine,
72         ID_kdm,
73         ID_ov,
74         ID_choose_cpl,
75         ID_set_dcp_settings,
76         ID_remove
77 };
78
79
80 ContentMenu::ContentMenu (wxWindow* p)
81         : _menu (new wxMenu)
82         , _parent (p)
83         , _pop_up_open (false)
84 {
85         _repeat = _menu->Append (ID_repeat, _("Repeat..."));
86         _join = _menu->Append (ID_join, _("Join"));
87         _find_missing = _menu->Append (ID_find_missing, _("Find missing..."));
88         _properties = _menu->Append (ID_properties, _("Properties..."));
89         _advanced = _menu->Append (ID_advanced, _("Advanced settings..."));
90         _re_examine = _menu->Append (ID_re_examine, _("Re-examine..."));
91         _menu->AppendSeparator ();
92         _kdm = _menu->Append (ID_kdm, _("Add KDM..."));
93         _ov = _menu->Append (ID_ov, _("Add OV..."));
94         _cpl_menu = new wxMenu ();
95         _choose_cpl = _menu->Append (ID_choose_cpl, _("Choose CPL..."), _cpl_menu);
96         _set_dcp_settings = _menu->Append (ID_set_dcp_settings, _("Set project DCP settings from this DCP"));
97         _menu->AppendSeparator ();
98         _remove = _menu->Append (ID_remove, _("Remove"));
99
100         _parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::repeat, this), ID_repeat);
101         _parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::join, this), ID_join);
102         _parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::find_missing, this), ID_find_missing);
103         _parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::properties, this), ID_properties);
104         _parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::advanced, this), ID_advanced);
105         _parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::re_examine, this), ID_re_examine);
106         _parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::kdm, this), ID_kdm);
107         _parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::ov, this), ID_ov);
108         _parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::set_dcp_settings, this), ID_set_dcp_settings);
109         _parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::remove, this), ID_remove);
110         _parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::cpl_selected, this, _1), 1, ID_repeat - 1);
111 }
112
113 void
114 ContentMenu::popup (weak_ptr<Film> film, ContentList c, TimelineContentViewList v, wxPoint p)
115 {
116         _film = film;
117         _content = c;
118         _views = v;
119
120         int const N = _cpl_menu->GetMenuItemCount();
121         for (int i = 1; i <= N; ++i) {
122                 _cpl_menu->Delete (i);
123         }
124
125         _repeat->Enable (!_content.empty ());
126
127         int n = 0;
128         for (auto i: _content) {
129                 if (dynamic_pointer_cast<FFmpegContent> (i)) {
130                         ++n;
131                 }
132         }
133
134         _join->Enable (n > 1);
135
136         _find_missing->Enable (_content.size() == 1 && !_content.front()->paths_valid ());
137         _properties->Enable (_content.size() == 1);
138         _advanced->Enable (_content.size() == 1);
139         _re_examine->Enable (!_content.empty ());
140
141         if (_content.size() == 1) {
142                 auto dcp = dynamic_pointer_cast<DCPContent> (_content.front());
143                 if (dcp) {
144                         _kdm->Enable (dcp->encrypted ());
145                         _ov->Enable (dcp->needs_assets ());
146                         _set_dcp_settings->Enable (static_cast<bool>(dcp));
147                         try {
148                                 DCPExaminer ex (dcp, true);
149                                 auto cpls = ex.cpls ();
150                                 _choose_cpl->Enable (cpls.size() > 1);
151                                 /* We can't have 0 as a menu item ID on OS X */
152                                 int id = 1;
153                                 for (auto i: cpls) {
154                                         auto item = _cpl_menu->AppendRadioItem (
155                                                 id++,
156                                                 wxString::Format (
157                                                         "%s (%s)",
158                                                         std_to_wx(i->annotation_text().get_value_or("")).data(),
159                                                         std_to_wx(i->id()).data()
160                                                         )
161                                                 );
162                                         item->Check (dcp->cpl() && dcp->cpl() == i->id());
163                                 }
164                         } catch (dcp::ReadError &) {
165                                 /* The DCP is probably missing */
166                         } catch (dcp::KDMDecryptionError &) {
167                                 /* We have an incorrect KDM */
168                         } catch (KDMError &) {
169                                 /* We have an incorrect KDM */
170                         }
171                 } else {
172                         _kdm->Enable (false);
173                         _ov->Enable (false);
174                         _choose_cpl->Enable (false);
175                         _set_dcp_settings->Enable (false);
176                 }
177         } else {
178                 _kdm->Enable (false);
179                 _set_dcp_settings->Enable (false);
180         }
181
182         _remove->Enable (!_content.empty ());
183
184         _pop_up_open = true;
185         _parent->PopupMenu (_menu, p);
186         _pop_up_open = false;
187 }
188
189
190 void
191 ContentMenu::set_dcp_settings ()
192 {
193         auto film = _film.lock ();
194         if (!film) {
195                 return;
196         }
197
198         DCPOMATIC_ASSERT (_content.size() == 1);
199         auto dcp = dynamic_pointer_cast<DCPContent>(_content.front());
200         DCPOMATIC_ASSERT (dcp);
201         copy_dcp_details_to_film (dcp, film);
202 }
203
204
205 void
206 ContentMenu::repeat ()
207 {
208         if (_content.empty ()) {
209                 return;
210         }
211
212         auto d = new RepeatDialog (_parent);
213         if (d->ShowModal() != wxID_OK) {
214                 d->Destroy ();
215                 return;
216         }
217
218         auto film = _film.lock ();
219         if (!film) {
220                 return;
221         }
222
223         film->repeat_content (_content, d->number ());
224         d->Destroy ();
225
226         _content.clear ();
227         _views.clear ();
228 }
229
230
231 void
232 ContentMenu::join ()
233 {
234         vector<shared_ptr<Content>> fc;
235         for (auto i: _content) {
236                 auto f = dynamic_pointer_cast<FFmpegContent> (i);
237                 if (f) {
238                         fc.push_back (f);
239                 }
240         }
241
242         DCPOMATIC_ASSERT (fc.size() > 1);
243
244         auto film = _film.lock ();
245         if (!film) {
246                 return;
247         }
248
249         try {
250                 auto joined = make_shared<FFmpegContent>(fc);
251                 film->remove_content (_content);
252                 film->examine_and_add_content (joined);
253         } catch (JoinError& e) {
254                 error_dialog (_parent, std_to_wx (e.what ()));
255         }
256 }
257
258
259 void
260 ContentMenu::remove ()
261 {
262         if (_content.empty ()) {
263                 return;
264         }
265
266         auto film = _film.lock ();
267         if (!film) {
268                 return;
269         }
270
271         /* We are removing from the timeline if _views is not empty */
272         bool handled = false;
273         if (!_views.empty ()) {
274                 /* Special case: we only remove FFmpegContent if its video view is selected;
275                    if not, and its audio view is selected, we unmap the audio.
276                 */
277                 for (auto i: _content) {
278                         auto fc = dynamic_pointer_cast<FFmpegContent> (i);
279                         if (!fc) {
280                                 continue;
281                         }
282
283                         shared_ptr<TimelineVideoContentView> video;
284                         shared_ptr<TimelineAudioContentView> audio;
285
286                         for (auto j: _views) {
287                                 auto v = dynamic_pointer_cast<TimelineVideoContentView>(j);
288                                 auto a = dynamic_pointer_cast<TimelineAudioContentView>(j);
289                                 if (v && v->content() == fc) {
290                                         video = v;
291                                 } else if (a && a->content() == fc) {
292                                         audio = a;
293                                 }
294                         }
295
296                         if (!video && audio) {
297                                 auto m = fc->audio->mapping ();
298                                 m.unmap_all ();
299                                 fc->audio->set_mapping (m);
300                                 handled = true;
301                         }
302                 }
303         }
304
305         if (!handled) {
306                 film->remove_content (_content);
307         }
308
309         _content.clear ();
310         _views.clear ();
311 }
312
313
314 void
315 ContentMenu::find_missing ()
316 {
317         if (_content.size() != 1) {
318                 return;
319         }
320
321         auto film = _film.lock ();
322         if (!film) {
323                 return;
324         }
325
326         /* XXX: a bit nasty */
327         auto ic = dynamic_pointer_cast<ImageContent> (_content.front());
328         auto dc = dynamic_pointer_cast<DCPContent> (_content.front());
329
330         int r = wxID_CANCEL;
331         boost::filesystem::path path;
332
333         if ((ic && !ic->still ()) || dc) {
334                 auto d = new wxDirDialog (0, _("Choose a folder"), wxT (""), wxDD_DIR_MUST_EXIST);
335                 r = d->ShowModal ();
336                 path = wx_to_std (d->GetPath ());
337                 d->Destroy ();
338         } else {
339                 auto d = new wxFileDialog (0, _("Choose a file"), wxT (""), wxT (""), wxT ("*.*"));
340                 r = d->ShowModal ();
341                 path = wx_to_std (d->GetPath ());
342                 d->Destroy ();
343         }
344
345         list<shared_ptr<Content>> content;
346
347         if (r == wxID_OK) {
348                 if (dc) {
349                         content.push_back (make_shared<DCPContent>(path));
350                 } else {
351                         content = content_factory (path);
352                 }
353         }
354
355         if (content.empty ()) {
356                 return;
357         }
358
359         for (auto i: content) {
360                 auto j = make_shared<ExamineContentJob>(film, i);
361
362                 j->Finished.connect (
363                         bind (
364                                 &ContentMenu::maybe_found_missing,
365                                 this,
366                                 std::weak_ptr<Job> (j),
367                                 std::weak_ptr<Content> (_content.front ()),
368                                 std::weak_ptr<Content> (i)
369                                 )
370                         );
371
372                 JobManager::instance()->add (j);
373         }
374 }
375
376 void
377 ContentMenu::re_examine ()
378 {
379         auto film = _film.lock ();
380         if (!film) {
381                 return;
382         }
383
384         for (auto i: _content) {
385                 JobManager::instance()->add (shared_ptr<Job> (new ExamineContentJob (film, i)));
386         }
387 }
388
389 void
390 ContentMenu::maybe_found_missing (weak_ptr<Job> j, weak_ptr<Content> oc, weak_ptr<Content> nc)
391 {
392         auto job = j.lock ();
393         if (!job || !job->finished_ok ()) {
394                 return;
395         }
396
397         auto old_content = oc.lock ();
398         auto new_content = nc.lock ();
399         DCPOMATIC_ASSERT (old_content);
400         DCPOMATIC_ASSERT (new_content);
401
402         if (new_content->digest() != old_content->digest()) {
403                 error_dialog (0, _("The content file(s) you specified are not the same as those that are missing.  Either try again with the correct content file or remove the missing content."));
404                 return;
405         }
406
407         old_content->set_paths (new_content->paths());
408 }
409
410 void
411 ContentMenu::kdm ()
412 {
413         DCPOMATIC_ASSERT (!_content.empty ());
414         auto dcp = dynamic_pointer_cast<DCPContent> (_content.front());
415         DCPOMATIC_ASSERT (dcp);
416
417         auto d = new wxFileDialog (_parent, _("Select KDM"));
418
419         if (d->ShowModal() == wxID_OK) {
420                 optional<dcp::EncryptedKDM> kdm;
421                 try {
422                         kdm = dcp::EncryptedKDM (dcp::file_to_string(wx_to_std(d->GetPath()), MAX_KDM_SIZE));
423                 } catch (exception& e) {
424                         error_dialog (_parent, _("Could not load KDM"), std_to_wx(e.what()));
425                         d->Destroy ();
426                         return;
427                 }
428
429                 /* Try to decrypt it to get an early preview of any errors */
430                 try {
431                         decrypt_kdm_with_helpful_error (*kdm);
432                 } catch (KDMError& e) {
433                         error_dialog (_parent, std_to_wx(e.summary()), std_to_wx(e.detail()));
434                         return;
435                 } catch (exception& e) {
436                         error_dialog (_parent, e.what());
437                         return;
438                 }
439
440                 DCPExaminer ex (dcp, true);
441
442                 bool kdm_matches_any_cpl = false;
443                 for (auto i: ex.cpls()) {
444                         if (i->id() == kdm->cpl_id()) {
445                                 kdm_matches_any_cpl = true;
446                         }
447                 }
448
449                 bool kdm_matches_selected_cpl = dcp->cpl() || kdm->cpl_id() == dcp->cpl().get();
450
451                 if (!kdm_matches_any_cpl) {
452                         error_dialog (_parent, _("This KDM was not made for this DCP.  You will need a different one."));
453                         return;
454                 }
455
456                 if (!kdm_matches_selected_cpl && kdm_matches_any_cpl) {
457                         message_dialog (_parent, _("This KDM was made for one of the CPLs in this DCP, but not the currently selected one.  To play the currently-selected CPL you will need a different KDM."));
458                 }
459
460                 dcp->add_kdm (*kdm);
461
462                 auto film = _film.lock ();
463                 DCPOMATIC_ASSERT (film);
464                 JobManager::instance()->add (make_shared<ExamineContentJob>(film, dcp));
465         }
466
467         d->Destroy ();
468 }
469
470 void
471 ContentMenu::ov ()
472 {
473         DCPOMATIC_ASSERT (!_content.empty ());
474         auto dcp = dynamic_pointer_cast<DCPContent> (_content.front());
475         DCPOMATIC_ASSERT (dcp);
476
477         auto d = new wxDirDialog (_parent, _("Select OV"));
478
479         if (d->ShowModal() == wxID_OK) {
480                 dcp->add_ov (wx_to_std (d->GetPath()));
481                 shared_ptr<Film> film = _film.lock ();
482                 DCPOMATIC_ASSERT (film);
483                 JobManager::instance()->add (make_shared<ExamineContentJob>(film, dcp));
484         }
485
486         d->Destroy ();
487 }
488
489 void
490 ContentMenu::properties ()
491 {
492         auto film = _film.lock ();
493         DCPOMATIC_ASSERT (film);
494         auto d = new ContentPropertiesDialog (_parent, film, _content.front());
495         d->ShowModal ();
496         d->Destroy ();
497 }
498
499
500 void
501 ContentMenu::advanced ()
502 {
503         auto d = new ContentAdvancedDialog (_parent, _content.front());
504         d->ShowModal ();
505         d->Destroy ();
506 }
507
508
509 void
510 ContentMenu::cpl_selected (wxCommandEvent& ev)
511 {
512         if (!_pop_up_open) {
513                 return;
514         }
515
516         DCPOMATIC_ASSERT (!_content.empty ());
517         auto dcp = dynamic_pointer_cast<DCPContent> (_content.front());
518         DCPOMATIC_ASSERT (dcp);
519
520         DCPExaminer ex (dcp, true);
521         auto cpls = ex.cpls ();
522         DCPOMATIC_ASSERT (ev.GetId() > 0);
523         DCPOMATIC_ASSERT (ev.GetId() <= int (cpls.size()));
524
525         auto i = cpls.begin ();
526         for (int j = 0; j < ev.GetId() - 1; ++j) {
527                 ++i;
528         }
529
530         dcp->set_cpl ((*i)->id ());
531         auto film = _film.lock ();
532         DCPOMATIC_ASSERT (film);
533         JobManager::instance()->add (make_shared<ExamineContentJob>(film, dcp));
534 }