Fix crash when adding content via Ctrl-A with no film loaded.
[dcpomatic.git] / src / wx / content_panel.cc
1 /*
2     Copyright (C) 2012-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_panel.h"
22 #include "wx_util.h"
23 #include "video_panel.h"
24 #include "audio_panel.h"
25 #include "subtitle_panel.h"
26 #include "timing_panel.h"
27 #include "timeline_dialog.h"
28 #include "image_sequence_dialog.h"
29 #include "film_viewer.h"
30 #include "lib/audio_content.h"
31 #include "lib/subtitle_content.h"
32 #include "lib/video_content.h"
33 #include "lib/ffmpeg_content.h"
34 #include "lib/content_factory.h"
35 #include "lib/image_content.h"
36 #include "lib/dcp_content.h"
37 #include "lib/case_insensitive_sorter.h"
38 #include "lib/playlist.h"
39 #include "lib/config.h"
40 #include <wx/wx.h>
41 #include <wx/notebook.h>
42 #include <wx/listctrl.h>
43 #include <boost/filesystem.hpp>
44 #include <boost/foreach.hpp>
45 #include <iostream>
46
47 using std::list;
48 using std::string;
49 using std::cout;
50 using std::vector;
51 using std::exception;
52 using boost::shared_ptr;
53 using boost::weak_ptr;
54 using boost::dynamic_pointer_cast;
55 using boost::optional;
56
57 ContentPanel::ContentPanel (wxNotebook* n, boost::shared_ptr<Film> film, FilmViewer* viewer)
58         : _timeline_dialog (0)
59         , _parent (n)
60         , _film (film)
61         , _film_viewer (viewer)
62         , _generally_sensitive (true)
63 {
64         _panel = new wxPanel (n);
65         _sizer = new wxBoxSizer (wxVERTICAL);
66         _panel->SetSizer (_sizer);
67
68         _menu = new ContentMenu (_panel);
69
70         {
71                 wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
72
73                 _content = new wxListCtrl (_panel, wxID_ANY, wxDefaultPosition, wxSize (320, 160), wxLC_REPORT | wxLC_NO_HEADER);
74                 _content->DragAcceptFiles (true);
75                 s->Add (_content, 1, wxEXPAND | wxTOP | wxBOTTOM, 6);
76
77                 _content->InsertColumn (0, wxT(""));
78                 _content->SetColumnWidth (0, 512);
79
80                 wxBoxSizer* b = new wxBoxSizer (wxVERTICAL);
81
82                 _add_file = new wxButton (_panel, wxID_ANY, _("Add file(s)..."));
83                 _add_file->SetToolTip (_("Add video, image, sound or subtitle files to the film."));
84                 b->Add (_add_file, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
85
86                 _add_folder = new wxButton (_panel, wxID_ANY, _("Add folder..."));
87                 _add_folder->SetToolTip (_("Add a folder of image files (which will be used as a moving image sequence) or a folder of sound files."));
88                 b->Add (_add_folder, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
89
90                 _add_dcp = new wxButton (_panel, wxID_ANY, _("Add DCP..."));
91                 _add_dcp->SetToolTip (_("Add a DCP."));
92                 b->Add (_add_dcp, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
93
94                 _remove = new wxButton (_panel, wxID_ANY, _("Remove"));
95                 _remove->SetToolTip (_("Remove the selected piece of content from the film."));
96                 b->Add (_remove, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
97
98                 _earlier = new wxButton (_panel, wxID_ANY, _("Earlier"));
99                 _earlier->SetToolTip (_("Move the selected piece of content earlier in the film."));
100                 b->Add (_earlier, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
101
102                 _later = new wxButton (_panel, wxID_ANY, _("Later"));
103                 _later->SetToolTip (_("Move the selected piece of content later in the film."));
104                 b->Add (_later, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
105
106                 _timeline = new wxButton (_panel, wxID_ANY, _("Timeline..."));
107                 _timeline->SetToolTip (_("Open the timeline for the film."));
108                 b->Add (_timeline, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
109
110                 s->Add (b, 0, wxALL, 4);
111
112                 _sizer->Add (s, 0, wxEXPAND | wxALL, 6);
113         }
114
115         _notebook = new wxNotebook (_panel, wxID_ANY);
116         _sizer->Add (_notebook, 1, wxEXPAND | wxTOP, 6);
117
118         _video_panel = new VideoPanel (this);
119         _panels.push_back (_video_panel);
120         _audio_panel = new AudioPanel (this);
121         _panels.push_back (_audio_panel);
122         _subtitle_panel = new SubtitlePanel (this);
123         _panels.push_back (_subtitle_panel);
124         _timing_panel = new TimingPanel (this, _film_viewer);
125         _panels.push_back (_timing_panel);
126
127         _content->Bind (wxEVT_LIST_ITEM_SELECTED, boost::bind (&ContentPanel::selection_changed, this));
128         _content->Bind (wxEVT_LIST_ITEM_DESELECTED, boost::bind (&ContentPanel::selection_changed, this));
129         _content->Bind (wxEVT_LIST_ITEM_RIGHT_CLICK, boost::bind (&ContentPanel::right_click, this, _1));
130         _content->Bind (wxEVT_DROP_FILES, boost::bind (&ContentPanel::files_dropped, this, _1));
131         _add_file->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::add_file_clicked, this));
132         _add_folder->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::add_folder_clicked, this));
133         _add_dcp->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::add_dcp_clicked, this));
134         _remove->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::remove_clicked, this, false));
135         _earlier->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::earlier_clicked, this));
136         _later->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::later_clicked, this));
137         _timeline->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::timeline_clicked, this));
138 }
139
140 ContentList
141 ContentPanel::selected ()
142 {
143         ContentList sel;
144         long int s = -1;
145         while (true) {
146                 s = _content->GetNextItem (s, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
147                 if (s == -1) {
148                         break;
149                 }
150
151                 if (s < int (_film->content().size ())) {
152                         sel.push_back (_film->content()[s]);
153                 }
154         }
155
156         return sel;
157 }
158
159 ContentList
160 ContentPanel::selected_video ()
161 {
162         ContentList vc;
163
164         BOOST_FOREACH (shared_ptr<Content> i, selected ()) {
165                 if (i->video) {
166                         vc.push_back (i);
167                 }
168         }
169
170         return vc;
171 }
172
173 ContentList
174 ContentPanel::selected_audio ()
175 {
176         ContentList ac;
177
178         BOOST_FOREACH (shared_ptr<Content> i, selected ()) {
179                 if (i->audio) {
180                         ac.push_back (i);
181                 }
182         }
183
184         return ac;
185 }
186
187 ContentList
188 ContentPanel::selected_subtitle ()
189 {
190         ContentList sc;
191
192         BOOST_FOREACH (shared_ptr<Content> i, selected ()) {
193                 if (i->subtitle) {
194                         sc.push_back (i);
195                 }
196         }
197
198         return sc;
199 }
200
201 FFmpegContentList
202 ContentPanel::selected_ffmpeg ()
203 {
204         FFmpegContentList sc;
205
206         BOOST_FOREACH (shared_ptr<Content> i, selected ()) {
207                 shared_ptr<FFmpegContent> t = dynamic_pointer_cast<FFmpegContent> (i);
208                 if (t) {
209                         sc.push_back (t);
210                 }
211         }
212
213         return sc;
214 }
215
216 void
217 ContentPanel::film_changed (Film::Property p)
218 {
219         switch (p) {
220         case Film::CONTENT:
221         case Film::CONTENT_ORDER:
222                 setup ();
223                 break;
224         default:
225                 break;
226         }
227
228         BOOST_FOREACH (ContentSubPanel* i, _panels) {
229                 i->film_changed (p);
230         }
231 }
232
233 void
234 ContentPanel::selection_changed ()
235 {
236         if (_last_selected == selected()) {
237                 /* This was triggered by a re-build of the view but the selection
238                    did not really change.
239                 */
240                 return;
241         }
242
243         _last_selected = selected ();
244
245         setup_sensitivity ();
246
247         BOOST_FOREACH (ContentSubPanel* i, _panels) {
248                 i->content_selection_changed ();
249         }
250
251         optional<DCPTime> go_to;
252         BOOST_FOREACH (shared_ptr<Content> i, selected ()) {
253                 if (!go_to || i->position() < go_to.get()) {
254                         go_to = i->position ();
255                 }
256         }
257
258         if (go_to && Config::instance()->jump_to_selected ()) {
259                 _film_viewer->set_position (go_to.get ());
260         }
261
262         if (_timeline_dialog) {
263                 _timeline_dialog->set_selection (selected ());
264         }
265 }
266
267 void
268 ContentPanel::add_file_clicked ()
269 {
270         /* This method is also called when Ctrl-A is pressed, so check that our notebook page
271            is visible.
272         */
273         if (_parent->GetCurrentPage() != _panel || !_film) {
274                 return;
275         }
276
277         /* The wxFD_CHANGE_DIR here prevents a `could not set working directory' error 123 on Windows when using
278            non-Latin filenames or paths.
279         */
280         wxFileDialog* d = new wxFileDialog (
281                 _panel,
282                 _("Choose a file or files"),
283                 wxT (""),
284                 wxT (""),
285                 wxT ("All files|*.*|Subtitle files|*.srt;*.xml|Audio files|*.wav;*.w64;*.flac;*.aif;*.aiff"),
286                 wxFD_MULTIPLE | wxFD_CHANGE_DIR
287                 );
288
289         int const r = d->ShowModal ();
290
291         if (r != wxID_OK) {
292                 d->Destroy ();
293                 return;
294         }
295
296         wxArrayString paths;
297         d->GetPaths (paths);
298         list<boost::filesystem::path> path_list;
299         for (unsigned int i = 0; i < paths.GetCount(); ++i) {
300                 path_list.push_back (wx_to_std (paths[i]));
301         }
302         add_files (path_list);
303
304         d->Destroy ();
305 }
306
307 void
308 ContentPanel::add_folder_clicked ()
309 {
310         wxDirDialog* d = new wxDirDialog (_panel, _("Choose a folder"), wxT (""), wxDD_DIR_MUST_EXIST);
311         int r = d->ShowModal ();
312         boost::filesystem::path const path (wx_to_std (d->GetPath ()));
313         d->Destroy ();
314
315         if (r != wxID_OK) {
316                 return;
317         }
318
319         list<shared_ptr<Content> > content;
320
321         try {
322                 content = content_factory (_film, path);
323         } catch (exception& e) {
324                 error_dialog (_parent, e.what());
325                 return;
326         }
327
328         if (content.empty ()) {
329                 error_dialog (_parent, _("No content found in this folder."));
330                 return;
331         }
332
333         BOOST_FOREACH (shared_ptr<Content> i, content) {
334                 shared_ptr<ImageContent> ic = dynamic_pointer_cast<ImageContent> (i);
335                 if (ic) {
336                         ImageSequenceDialog* e = new ImageSequenceDialog (_panel);
337                         r = e->ShowModal ();
338                         float const frame_rate = e->frame_rate ();
339                         e->Destroy ();
340
341                         if (r != wxID_OK) {
342                                 return;
343                         }
344
345                         ic->set_video_frame_rate (frame_rate);
346                 }
347
348                 _film->examine_and_add_content (i);
349         }
350 }
351
352 void
353 ContentPanel::add_dcp_clicked ()
354 {
355         wxDirDialog* d = new wxDirDialog (_panel, _("Choose a DCP folder"), wxT (""), wxDD_DIR_MUST_EXIST);
356         int r = d->ShowModal ();
357         boost::filesystem::path const path (wx_to_std (d->GetPath ()));
358         d->Destroy ();
359
360         if (r != wxID_OK) {
361                 return;
362         }
363
364         try {
365                 _film->examine_and_add_content (shared_ptr<Content> (new DCPContent (_film, path)));
366         } catch (exception& e) {
367                 error_dialog (_parent, e.what());
368         }
369 }
370
371 /** @return true if this remove "click" should be ignored */
372 bool
373 ContentPanel::remove_clicked (bool hotkey)
374 {
375         /* If the method was called because Delete was pressed check that our notebook page
376            is visible and that the content list is focussed.
377         */
378         if (hotkey && (_parent->GetCurrentPage() != _panel || !_content->HasFocus())) {
379                 return true;
380         }
381
382         BOOST_FOREACH (shared_ptr<Content> i, selected ()) {
383                 _film->remove_content (i);
384         }
385
386         selection_changed ();
387         return false;
388 }
389
390 void
391 ContentPanel::timeline_clicked ()
392 {
393         if (_timeline_dialog) {
394                 _timeline_dialog->Destroy ();
395                 _timeline_dialog = 0;
396         }
397
398         _timeline_dialog = new TimelineDialog (this, _film);
399         _timeline_dialog->Show ();
400 }
401
402 void
403 ContentPanel::right_click (wxListEvent& ev)
404 {
405         _menu->popup (_film, selected (), TimelineContentViewList (), ev.GetPoint ());
406 }
407
408 /** Set up broad sensitivity based on the type of content that is selected */
409 void
410 ContentPanel::setup_sensitivity ()
411 {
412         _add_file->Enable (_generally_sensitive);
413         _add_folder->Enable (_generally_sensitive);
414         _add_dcp->Enable (_generally_sensitive);
415
416         ContentList selection = selected ();
417         ContentList video_selection = selected_video ();
418         ContentList audio_selection = selected_audio ();
419
420         _remove->Enable   (!selection.empty() && _generally_sensitive);
421         _earlier->Enable  (selection.size() == 1 && _generally_sensitive);
422         _later->Enable    (selection.size() == 1 && _generally_sensitive);
423         _timeline->Enable (!_film->content().empty() && _generally_sensitive);
424
425         _video_panel->Enable    (video_selection.size() > 0 && _generally_sensitive);
426         _audio_panel->Enable    (audio_selection.size() > 0 && _generally_sensitive);
427         _subtitle_panel->Enable (selection.size() == 1 && selection.front()->subtitle && _generally_sensitive);
428         _timing_panel->Enable   (_generally_sensitive);
429 }
430
431 void
432 ContentPanel::set_film (shared_ptr<Film> film)
433 {
434         _audio_panel->set_film (film);
435
436         _film = film;
437
438         film_changed (Film::CONTENT);
439         film_changed (Film::AUDIO_CHANNELS);
440         selection_changed ();
441         setup_sensitivity ();
442 }
443
444 void
445 ContentPanel::set_general_sensitivity (bool s)
446 {
447         _generally_sensitive = s;
448
449         _content->Enable (s);
450         _add_file->Enable (s);
451         _add_folder->Enable (s);
452         _add_dcp->Enable (s);
453         _remove->Enable (s);
454         _earlier->Enable (s);
455         _later->Enable (s);
456         _timeline->Enable (s);
457
458         /* Set the panels in the content notebook */
459         BOOST_FOREACH (ContentSubPanel* i, _panels) {
460                 i->Enable (s);
461         }
462 }
463
464 void
465 ContentPanel::earlier_clicked ()
466 {
467         ContentList sel = selected ();
468         if (sel.size() == 1) {
469                 _film->move_content_earlier (sel.front ());
470                 selection_changed ();
471         }
472 }
473
474 void
475 ContentPanel::later_clicked ()
476 {
477         ContentList sel = selected ();
478         if (sel.size() == 1) {
479                 _film->move_content_later (sel.front ());
480                 selection_changed ();
481         }
482 }
483
484 void
485 ContentPanel::set_selection (weak_ptr<Content> wc)
486 {
487         ContentList content = _film->content ();
488         for (size_t i = 0; i < content.size(); ++i) {
489                 if (content[i] == wc.lock ()) {
490                         _content->SetItemState (i, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
491                 } else {
492                         _content->SetItemState (i, 0, wxLIST_STATE_SELECTED);
493                 }
494         }
495 }
496
497 void
498 ContentPanel::set_selection (ContentList cl)
499 {
500         ContentList content = _film->content ();
501         for (size_t i = 0; i < content.size(); ++i) {
502                 if (find(cl.begin(), cl.end(), content[i]) != cl.end()) {
503                         _content->SetItemState (i, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
504                 } else {
505                         _content->SetItemState (i, 0, wxLIST_STATE_SELECTED);
506                 }
507         }
508 }
509
510 void
511 ContentPanel::film_content_changed (int property)
512 {
513         if (
514                 property == ContentProperty::PATH ||
515                 property == DCPContentProperty::NEEDS_ASSETS ||
516                 property == DCPContentProperty::NEEDS_KDM ||
517                 property == DCPContentProperty::NAME
518                 ) {
519
520                 setup ();
521         }
522
523         BOOST_FOREACH (ContentSubPanel* i, _panels) {
524                 i->film_content_changed (property);
525         }
526 }
527
528 void
529 ContentPanel::setup ()
530 {
531         ContentList content = _film->content ();
532
533         Content* selected_content = 0;
534         int const s = _content->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
535         if (s != -1) {
536                 wxListItem item;
537                 item.SetId (s);
538                 item.SetMask (wxLIST_MASK_DATA);
539                 _content->GetItem (item);
540                 selected_content = reinterpret_cast<Content*> (item.GetData ());
541         }
542
543         _content->DeleteAllItems ();
544
545         BOOST_FOREACH (shared_ptr<Content> i, content) {
546                 int const t = _content->GetItemCount ();
547                 bool const valid = i->paths_valid ();
548                 shared_ptr<DCPContent> dcp = dynamic_pointer_cast<DCPContent> (i);
549                 bool const needs_kdm = dcp && dcp->needs_kdm ();
550                 bool const needs_assets = dcp && dcp->needs_assets ();
551
552                 wxString s = std_to_wx (i->summary ());
553
554                 if (!valid) {
555                         s = _("MISSING: ") + s;
556                 }
557
558                 if (needs_kdm) {
559                         s = _("NEEDS KDM: ") + s;
560                 }
561
562                 if (needs_assets) {
563                         s = _("NEEDS OV: ") + s;
564                 }
565
566                 wxListItem item;
567                 item.SetId (t);
568                 item.SetText (s);
569                 item.SetData (i.get ());
570                 _content->InsertItem (item);
571
572                 if (i.get() == selected_content) {
573                         _content->SetItemState (t, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
574                 }
575
576                 if (!valid || needs_kdm || needs_assets) {
577                         _content->SetItemTextColour (t, *wxRED);
578                 }
579         }
580
581         if (!selected_content && !content.empty ()) {
582                 /* Select the item of content if none was selected before */
583                 _content->SetItemState (0, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
584         }
585 }
586
587 void
588 ContentPanel::files_dropped (wxDropFilesEvent& event)
589 {
590         if (!_film) {
591                 return;
592         }
593
594         wxString* paths = event.GetFiles ();
595         list<boost::filesystem::path> path_list;
596         for (int i = 0; i < event.GetNumberOfFiles(); i++) {
597                 path_list.push_back (wx_to_std (paths[i]));
598         }
599
600         add_files (path_list);
601 }
602
603 void
604 ContentPanel::add_files (list<boost::filesystem::path> paths)
605 {
606         /* It has been reported that the paths returned from e.g. wxFileDialog are not always sorted;
607            I can't reproduce that, but sort them anyway.  Don't use ImageFilenameSorter as a normal
608            alphabetical sort is expected here.
609         */
610
611         paths.sort (CaseInsensitiveSorter ());
612
613         /* XXX: check for lots of files here and do something */
614
615         BOOST_FOREACH (boost::filesystem::path i, paths) {
616                 BOOST_FOREACH (shared_ptr<Content> j, content_factory (_film, i)) {
617                         _film->examine_and_add_content (j);
618                 }
619         }
620 }