Attempt to fix crash on hitting enter in the crop control on Windows (#1009).
[dcpomatic.git] / src / wx / content_menu.cc
1 /*
2     Copyright (C) 2013-2016 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 #include "content_menu.h"
22 #include "repeat_dialog.h"
23 #include "wx_util.h"
24 #include "timeline_video_content_view.h"
25 #include "timeline_audio_content_view.h"
26 #include "content_properties_dialog.h"
27 #include "lib/playlist.h"
28 #include "lib/film.h"
29 #include "lib/image_content.h"
30 #include "lib/content_factory.h"
31 #include "lib/examine_content_job.h"
32 #include "lib/job_manager.h"
33 #include "lib/exceptions.h"
34 #include "lib/dcp_content.h"
35 #include "lib/dcp_examiner.h"
36 #include "lib/ffmpeg_content.h"
37 #include "lib/audio_content.h"
38 #include <dcp/cpl.h>
39 #include <dcp/exceptions.h>
40 #include <wx/wx.h>
41 #include <wx/dirdlg.h>
42 #include <boost/foreach.hpp>
43 #include <iostream>
44
45 using std::cout;
46 using std::vector;
47 using std::exception;
48 using std::list;
49 using boost::shared_ptr;
50 using boost::weak_ptr;
51 using boost::dynamic_pointer_cast;
52
53 enum {
54         /* Start at 256 so we can have IDs on _cpl_menu from 1 to 255 */
55         ID_repeat = 256,
56         ID_join,
57         ID_find_missing,
58         ID_properties,
59         ID_re_examine,
60         ID_kdm,
61         ID_ov,
62         ID_choose_cpl,
63         ID_remove
64 };
65
66 ContentMenu::ContentMenu (wxWindow* p)
67         : _menu (new wxMenu)
68         , _parent (p)
69         , _pop_up_open (false)
70 {
71         _repeat = _menu->Append (ID_repeat, _("Repeat..."));
72         _join = _menu->Append (ID_join, _("Join"));
73         _find_missing = _menu->Append (ID_find_missing, _("Find missing..."));
74         _properties = _menu->Append (ID_properties, _("Properties..."));
75         _re_examine = _menu->Append (ID_re_examine, _("Re-examine..."));
76         _menu->AppendSeparator ();
77         _kdm = _menu->Append (ID_kdm, _("Add KDM..."));
78         _ov = _menu->Append (ID_ov, _("Add OV..."));
79         _cpl_menu = new wxMenu ();
80         _choose_cpl = _menu->Append (ID_choose_cpl, _("Choose CPL..."), _cpl_menu);
81         _menu->AppendSeparator ();
82         _remove = _menu->Append (ID_remove, _("Remove"));
83
84         _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&ContentMenu::repeat, this), ID_repeat);
85         _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&ContentMenu::join, this), ID_join);
86         _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&ContentMenu::find_missing, this), ID_find_missing);
87         _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&ContentMenu::properties, this), ID_properties);
88         _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&ContentMenu::re_examine, this), ID_re_examine);
89         _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&ContentMenu::kdm, this), ID_kdm);
90         _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&ContentMenu::ov, this), ID_ov);
91         _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&ContentMenu::remove, this), ID_remove);
92
93         _parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::cpl_selected, this, _1), 1, ID_repeat - 1);
94 }
95
96 ContentMenu::~ContentMenu ()
97 {
98         delete _menu;
99
100         BOOST_FOREACH (boost::signals2::connection& i, _job_connections) {
101                 i.disconnect ();
102         }
103 }
104
105 void
106 ContentMenu::popup (weak_ptr<Film> film, ContentList c, TimelineContentViewList v, wxPoint p)
107 {
108         _film = film;
109         _content = c;
110         _views = v;
111
112         int const N = _cpl_menu->GetMenuItemCount();
113         for (int i = 1; i <= N; ++i) {
114                 _cpl_menu->Delete (i);
115         }
116
117         _repeat->Enable (!_content.empty ());
118
119         int n = 0;
120         BOOST_FOREACH (shared_ptr<Content> i, _content) {
121                 if (dynamic_pointer_cast<FFmpegContent> (i)) {
122                         ++n;
123                 }
124         }
125
126         _join->Enable (n > 1);
127
128         _find_missing->Enable (_content.size() == 1 && !_content.front()->paths_valid ());
129         _properties->Enable (_content.size() == 1);
130         _re_examine->Enable (!_content.empty ());
131
132         if (_content.size() == 1) {
133                 shared_ptr<DCPContent> dcp = dynamic_pointer_cast<DCPContent> (_content.front ());
134                 if (dcp) {
135                         _kdm->Enable (dcp->encrypted ());
136                         _ov->Enable (dcp->needs_assets ());
137                         try {
138                                 DCPExaminer ex (dcp);
139                                 list<shared_ptr<dcp::CPL> > cpls = ex.cpls ();
140                                 _choose_cpl->Enable (cpls.size() > 1);
141                                 /* We can't have 0 as a menu item ID on OS X */
142                                 int id = 1;
143                                 BOOST_FOREACH (shared_ptr<dcp::CPL> i, cpls) {
144                                         wxMenuItem* item = _cpl_menu->AppendCheckItem (
145                                                 id++,
146                                                 wxString::Format (
147                                                         "%s (%s)",
148                                                         std_to_wx(i->annotation_text()).data(),
149                                                         std_to_wx(i->id()).data()
150                                                         )
151                                                 );
152                                         item->Check (dcp->cpl() && dcp->cpl() == i->id());
153                                 }
154                         } catch (dcp::DCPReadError) {
155                                 /* The DCP is probably missing */
156                         }
157                 } else {
158                         _kdm->Enable (false);
159                         _ov->Enable (false);
160                         _choose_cpl->Enable (false);
161                 }
162         } else {
163                 _kdm->Enable (false);
164         }
165
166         _remove->Enable (!_content.empty ());
167
168         _pop_up_open = true;
169         _parent->PopupMenu (_menu, p);
170         _pop_up_open = false;
171 }
172
173 void
174 ContentMenu::repeat ()
175 {
176         if (_content.empty ()) {
177                 return;
178         }
179
180         RepeatDialog* d = new RepeatDialog (_parent);
181         if (d->ShowModal() != wxID_OK) {
182                 d->Destroy ();
183                 return;
184         }
185
186         shared_ptr<Film> film = _film.lock ();
187         if (!film) {
188                 return;
189         }
190
191         film->repeat_content (_content, d->number ());
192         d->Destroy ();
193
194         _content.clear ();
195         _views.clear ();
196 }
197
198 void
199 ContentMenu::join ()
200 {
201         vector<shared_ptr<Content> > fc;
202         BOOST_FOREACH (shared_ptr<Content> i, _content) {
203                 shared_ptr<FFmpegContent> f = dynamic_pointer_cast<FFmpegContent> (i);
204                 if (f) {
205                         fc.push_back (f);
206                 }
207         }
208
209         DCPOMATIC_ASSERT (fc.size() > 1);
210
211         shared_ptr<Film> film = _film.lock ();
212         if (!film) {
213                 return;
214         }
215
216         try {
217                 shared_ptr<FFmpegContent> joined (new FFmpegContent (film, fc));
218                 film->remove_content (_content);
219                 film->examine_and_add_content (joined);
220         } catch (JoinError& e) {
221                 error_dialog (_parent, std_to_wx (e.what ()));
222         }
223 }
224
225 void
226 ContentMenu::remove ()
227 {
228         if (_content.empty ()) {
229                 return;
230         }
231
232         shared_ptr<Film> film = _film.lock ();
233         if (!film) {
234                 return;
235         }
236
237         /* We are removing from the timeline if _views is not empty */
238         bool handled = false;
239         if (!_views.empty ()) {
240                 /* Special case: we only remove FFmpegContent if its video view is selected;
241                    if not, and its audio view is selected, we unmap the audio.
242                 */
243                 BOOST_FOREACH (shared_ptr<Content> i, _content) {
244                         shared_ptr<FFmpegContent> fc = dynamic_pointer_cast<FFmpegContent> (i);
245                         if (!fc) {
246                                 continue;
247                         }
248
249                         shared_ptr<TimelineVideoContentView> video;
250                         shared_ptr<TimelineAudioContentView> audio;
251
252                         BOOST_FOREACH (shared_ptr<TimelineContentView> j, _views) {
253                                 shared_ptr<TimelineVideoContentView> v = dynamic_pointer_cast<TimelineVideoContentView> (j);
254                                 shared_ptr<TimelineAudioContentView> a = dynamic_pointer_cast<TimelineAudioContentView> (j);
255                                 if (v && v->content() == fc) {
256                                         video = v;
257                                 } else if (a && a->content() == fc) {
258                                         audio = a;
259                                 }
260                         }
261
262                         if (!video && audio) {
263                                 AudioMapping m = fc->audio->mapping ();
264                                 m.unmap_all ();
265                                 fc->audio->set_mapping (m);
266                                 handled = true;
267                         }
268                 }
269         }
270
271         if (!handled) {
272                 film->remove_content (_content);
273         }
274
275         _content.clear ();
276         _views.clear ();
277 }
278
279 void
280 ContentMenu::find_missing ()
281 {
282         if (_content.size() != 1) {
283                 return;
284         }
285
286         shared_ptr<const Film> film = _film.lock ();
287         if (!film) {
288                 return;
289         }
290
291         shared_ptr<Content> content;
292
293         /* XXX: a bit nasty */
294         shared_ptr<ImageContent> ic = dynamic_pointer_cast<ImageContent> (_content.front ());
295         shared_ptr<DCPContent> dc = dynamic_pointer_cast<DCPContent> (_content.front ());
296
297         int r = wxID_CANCEL;
298         boost::filesystem::path path;
299
300         if ((ic && !ic->still ()) || dc) {
301                 wxDirDialog* d = new wxDirDialog (0, _("Choose a folder"), wxT (""), wxDD_DIR_MUST_EXIST);
302                 r = d->ShowModal ();
303                 path = wx_to_std (d->GetPath ());
304                 d->Destroy ();
305         } else {
306                 wxFileDialog* d = new wxFileDialog (0, _("Choose a file"), wxT (""), wxT (""), wxT ("*.*"), wxFD_MULTIPLE);
307                 r = d->ShowModal ();
308                 path = wx_to_std (d->GetPath ());
309                 d->Destroy ();
310         }
311
312         if (r == wxID_OK) {
313                 content = content_factory (film, path);
314         }
315
316         if (!content) {
317                 return;
318         }
319
320         shared_ptr<Job> j (new ExamineContentJob (film, content));
321
322         _job_connections.push_back (
323                 j->Finished.connect (
324                         bind (
325                                 &ContentMenu::maybe_found_missing,
326                                 this,
327                                 boost::weak_ptr<Job> (j),
328                                 boost::weak_ptr<Content> (_content.front ()),
329                                 boost::weak_ptr<Content> (content)
330                                 )
331                         )
332                 );
333
334         JobManager::instance()->add (j);
335 }
336
337 void
338 ContentMenu::re_examine ()
339 {
340         shared_ptr<Film> film = _film.lock ();
341         if (!film) {
342                 return;
343         }
344
345         BOOST_FOREACH (shared_ptr<Content> i, _content) {
346                 JobManager::instance()->add (shared_ptr<Job> (new ExamineContentJob (film, i)));
347         }
348 }
349
350 void
351 ContentMenu::maybe_found_missing (weak_ptr<Job> j, weak_ptr<Content> oc, weak_ptr<Content> nc)
352 {
353         shared_ptr<Job> job = j.lock ();
354         if (!job || !job->finished_ok ()) {
355                 return;
356         }
357
358         shared_ptr<Content> old_content = oc.lock ();
359         shared_ptr<Content> new_content = nc.lock ();
360         DCPOMATIC_ASSERT (old_content);
361         DCPOMATIC_ASSERT (new_content);
362
363         if (new_content->digest() != old_content->digest()) {
364                 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."));
365                 return;
366         }
367
368         old_content->set_path (new_content->path (0));
369 }
370
371 void
372 ContentMenu::kdm ()
373 {
374         DCPOMATIC_ASSERT (!_content.empty ());
375         shared_ptr<DCPContent> dcp = dynamic_pointer_cast<DCPContent> (_content.front ());
376         DCPOMATIC_ASSERT (dcp);
377
378         wxFileDialog* d = new wxFileDialog (_parent, _("Select KDM"));
379
380         if (d->ShowModal() == wxID_OK) {
381                 try {
382                         dcp->add_kdm (dcp::EncryptedKDM (dcp::file_to_string (wx_to_std (d->GetPath ()))));
383                 } catch (exception& e) {
384                         error_dialog (_parent, wxString::Format (_("Could not load KDM (%s)"), e.what ()));
385                         d->Destroy ();
386                         return;
387                 }
388
389                 shared_ptr<Film> film = _film.lock ();
390                 DCPOMATIC_ASSERT (film);
391                 JobManager::instance()->add (shared_ptr<Job> (new ExamineContentJob (film, dcp)));
392         }
393
394         d->Destroy ();
395 }
396
397 void
398 ContentMenu::ov ()
399 {
400         DCPOMATIC_ASSERT (!_content.empty ());
401         shared_ptr<DCPContent> dcp = dynamic_pointer_cast<DCPContent> (_content.front ());
402         DCPOMATIC_ASSERT (dcp);
403
404         wxDirDialog* d = new wxDirDialog (_parent, _("Select OV"));
405
406         if (d->ShowModal() == wxID_OK) {
407                 dcp->add_ov (wx_to_std (d->GetPath ()));
408                 shared_ptr<Film> film = _film.lock ();
409                 DCPOMATIC_ASSERT (film);
410                 JobManager::instance()->add (shared_ptr<Job> (new ExamineContentJob (film, dcp)));
411         }
412
413         d->Destroy ();
414 }
415
416 void
417 ContentMenu::properties ()
418 {
419         ContentPropertiesDialog* d = new ContentPropertiesDialog (_parent, _content.front ());
420         d->ShowModal ();
421         d->Destroy ();
422 }
423
424 void
425 ContentMenu::cpl_selected (wxCommandEvent& ev)
426 {
427         if (!_pop_up_open) {
428                 return;
429         }
430
431         DCPOMATIC_ASSERT (!_content.empty ());
432         shared_ptr<DCPContent> dcp = dynamic_pointer_cast<DCPContent> (_content.front ());
433         DCPOMATIC_ASSERT (dcp);
434
435         DCPExaminer ex (dcp);
436         list<shared_ptr<dcp::CPL> > cpls = ex.cpls ();
437         DCPOMATIC_ASSERT (ev.GetId() > 0);
438         DCPOMATIC_ASSERT (ev.GetId() <= int (cpls.size()));
439
440         list<shared_ptr<dcp::CPL> >::const_iterator i = cpls.begin ();
441         for (int j = 0; j < ev.GetId() - 1; ++j) {
442                 ++i;
443         }
444
445         dcp->set_cpl ((*i)->id ());
446         shared_ptr<Film> film = _film.lock ();
447         DCPOMATIC_ASSERT (film);
448         JobManager::instance()->add (shared_ptr<Job> (new ExamineContentJob (film, dcp)));
449 }