bfbaf7fe292a8b90e393eb845e76bfbfafddbba0
[dcpomatic.git] / src / wx / content_panel.cc
1 /*
2     Copyright (C) 2012-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 "audio_panel.h"
23 #include "content_panel.h"
24 #include "cucumber_bridge.h"
25 #include "cucumber_registry.h"
26 #include "dcpomatic_button.h"
27 #include "dir_dialog.h"
28 #include "file_dialog.h"
29 #include "film_viewer.h"
30 #include "image_sequence_dialog.h"
31 #include "text_panel.h"
32 #include "timeline_dialog.h"
33 #include "timing_panel.h"
34 #include "video_panel.h"
35 #include "wx_util.h"
36 #include "lib/audio_content.h"
37 #include "lib/case_insensitive_sorter.h"
38 #include "lib/compose.hpp"
39 #include "lib/config.h"
40 #include "lib/content_factory.h"
41 #include "lib/cross.h"
42 #include "lib/dcp_content.h"
43 #include "lib/dcp_subtitle_content.h"
44 #include "lib/dcp_subtitle_decoder.h"
45 #include "lib/dcpomatic_log.h"
46 #include "lib/ffmpeg_content.h"
47 #include "lib/film.h"
48 #include "lib/film_util.h"
49 #include "lib/image_content.h"
50 #include "lib/job_manager.h"
51 #include "lib/log.h"
52 #include "lib/playlist.h"
53 #include "lib/scope_guard.h"
54 #include "lib/string_text_file.h"
55 #include "lib/string_text_file_content.h"
56 #include "lib/text_content.h"
57 #include "lib/video_content.h"
58 #include <dcp/filesystem.h>
59 #include <dcp/warnings.h>
60 LIBDCP_DISABLE_WARNINGS
61 #include <wx/display.h>
62 #include <wx/dnd.h>
63 #include <wx/listctrl.h>
64 #include <wx/notebook.h>
65 #include <wx/wx.h>
66 LIBDCP_ENABLE_WARNINGS
67 #include <boost/filesystem.hpp>
68
69
70 using std::dynamic_pointer_cast;
71 using std::exception;
72 using std::list;
73 using std::make_shared;
74 using std::shared_ptr;
75 using std::string;
76 using std::vector;
77 using std::weak_ptr;
78 using boost::optional;
79 using namespace dcpomatic;
80 #if BOOST_VERSION >= 106100
81 using namespace boost::placeholders;
82 #endif
83
84
85 class LimitedContentPanelSplitter : public wxSplitterWindow
86 {
87 public:
88         LimitedContentPanelSplitter(wxWindow* parent)
89                 : wxSplitterWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxSP_NOBORDER | wxSP_3DSASH | wxSP_LIVE_UPDATE)
90         {
91                 /* This value doesn't really mean much but we just want to stop double-click on the
92                    divider from shrinking the bottom panel (#1601).
93                    */
94                 SetMinimumPaneSize(64);
95
96                 Bind(wxEVT_SIZE, boost::bind(&LimitedContentPanelSplitter::sized, this, _1));
97         }
98
99         bool OnSashPositionChange(int new_position) override
100         {
101                 /* Try to stop the top bit of the splitter getting so small that buttons disappear */
102                 auto const ok = new_position > 220;
103                 if (ok) {
104                         Config::instance()->set_main_content_divider_sash_position(new_position);
105                 }
106                 return ok;
107         }
108
109         void first_shown(wxWindow* top, wxWindow* bottom)
110         {
111                 int const sn = wxDisplay::GetFromWindow(this);
112                 /* Fallback for when GetFromWindow fails for reasons that aren't clear */
113                 int pos = -600;
114                 if (sn >= 0) {
115                         wxRect const screen = wxDisplay(sn).GetClientArea();
116                         /* This is a hack to try and make the content notebook a sensible size; large on big displays but small
117                            enough on small displays to leave space for the content area.
118                            */
119                         pos = screen.height > 800 ? -600 : -_top_panel_minimum_size;
120                 }
121                 SplitHorizontally(top, bottom, Config::instance()->main_content_divider_sash_position().get_value_or(pos));
122                 _first_shown = true;
123         }
124
125 private:
126         void sized(wxSizeEvent& ev)
127         {
128                 auto const height = GetSize().GetHeight();
129                 if (_first_shown && (!_last_height || *_last_height != height) && height > _top_panel_minimum_size && GetSashPosition() < _top_panel_minimum_size) {
130                         /* The window is now fairly big but the top panel is small; this happens when the DCP-o-matic window
131                          * is shrunk and then made larger again.  Try to set a sensible top panel size in this case (#1839).
132                          */
133                         SetSashPosition(Config::instance()->main_content_divider_sash_position().get_value_or(_top_panel_minimum_size));
134                 }
135
136                 ev.Skip ();
137                 _last_height = height;
138         }
139
140         bool _first_shown = false;
141         int const _top_panel_minimum_size = 350;
142         boost::optional<int> _last_height;
143 };
144
145
146 class ContentDropTarget : public wxFileDropTarget
147 {
148 public:
149         ContentDropTarget(ContentPanel* owner)
150                 : _panel(owner)
151         {}
152
153         bool OnDropFiles(wxCoord, wxCoord, wxArrayString const& filenames) override
154         {
155                 vector<boost::filesystem::path> files;
156                 vector<boost::filesystem::path> dcps;
157                 vector<boost::filesystem::path> folders;
158                 for (size_t i = 0; i < filenames.GetCount(); ++i) {
159                         auto path = boost::filesystem::path(wx_to_std(filenames[i]));
160                         if (dcp::filesystem::is_regular_file(path)) {
161                                 files.push_back(path);
162                         } else if (dcp::filesystem::is_directory(path)) {
163                                 if (contains_assetmap(path)) {
164                                         dcps.push_back(path);
165                                 } else {
166                                         folders.push_back(path);
167                                 }
168                         }
169                 }
170
171                 if (!filenames.empty()) {
172                         _panel->add_files(files);
173                 }
174
175                 for (auto dcp: dcps) {
176                         _panel->add_dcp(dcp);
177                 }
178
179                 for (auto dir: folders) {
180                         _panel->add_folder(dir);
181                 }
182
183                 return true;
184         };
185
186 private:
187         ContentPanel* _panel;
188 };
189
190
191 /** A wxListCtrl that can middle-ellipsize its text */
192 class ContentListCtrl : public wxListCtrl
193 {
194 public:
195         ContentListCtrl(wxWindow* parent)
196                 : wxListCtrl(parent, wxID_ANY, wxDefaultPosition, wxSize(320, 160), wxLC_REPORT | wxLC_NO_HEADER | wxLC_VIRTUAL)
197         {
198                 _red.SetTextColour(*wxRED);
199         }
200
201         struct Item
202         {
203                 wxString text;
204                 weak_ptr<Content> content;
205                 bool error;
206         };
207
208         void set(vector<Item> const& items)
209         {
210                 _items = items;
211                 SetItemCount(items.size());
212         }
213
214         wxString OnGetItemText(long item, long) const override
215         {
216                 DCPOMATIC_ASSERT(item >= 0 && item < static_cast<long>(_items.size()));
217                 wxClientDC dc(const_cast<wxWindow*>(static_cast<wxWindow const*>(this)));
218                 return wxControl::Ellipsize(_items[item].text, dc, wxELLIPSIZE_MIDDLE, GetSize().GetWidth());
219         }
220
221         wxListItemAttr* OnGetItemAttr(long item) const override
222         {
223                 DCPOMATIC_ASSERT(item >= 0 && item < static_cast<long>(_items.size()));
224                 return _items[item].error ? const_cast<wxListItemAttr*>(&_red) : nullptr;
225         }
226
227         weak_ptr<Content> content_at_index(long index)
228         {
229                 if (index < 0 || index >= static_cast<long>(_items.size())) {
230                         return {};
231                 }
232                 return _items[index].content;
233         }
234
235 private:
236         std::vector<Item> _items;
237         wxListItemAttr _red;
238 };
239
240
241 ContentPanel::ContentPanel(wxNotebook* n, shared_ptr<Film> film, FilmViewer& viewer)
242         : _parent (n)
243         , _film (film)
244         , _film_viewer (viewer)
245         , _generally_sensitive (true)
246         , _ignore_deselect (false)
247         , _no_check_selection (false)
248 {
249         _splitter = new LimitedContentPanelSplitter(n);
250         _top_panel = new wxPanel (_splitter);
251
252         _menu = new ContentMenu (_splitter, _film_viewer);
253
254         {
255                 auto s = new wxBoxSizer (wxHORIZONTAL);
256
257                 _content = new ContentListCtrl(_top_panel);
258                 _content->DragAcceptFiles (true);
259                 s->Add (_content, 1, wxEXPAND | wxTOP | wxBOTTOM, 6);
260
261                 _content->InsertColumn (0, wxT(""));
262                 _content->SetColumnWidth(0, 2048);
263
264                 auto b = new wxBoxSizer (wxVERTICAL);
265
266                 _add_file = new Button (_top_panel, _("Add file(s)..."));
267                 _add_file->SetToolTip(_("Add video, image, sound or subtitle files to the film (Ctrl+A)."));
268                 b->Add (_add_file, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
269
270                 _add_folder = new Button (_top_panel, _("Add folder..."));
271                 _add_folder->SetToolTip (_("Add a folder of image files (which will be used as a moving image sequence) or a folder of sound files."));
272                 b->Add (_add_folder, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
273
274                 _add_dcp = new Button (_top_panel, _("Add DCP..."));
275                 _add_dcp->SetToolTip (_("Add a DCP."));
276                 b->Add (_add_dcp, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
277
278                 _remove = new Button (_top_panel, _("Remove"));
279                 _remove->SetToolTip(_("Remove the selected piece of content from the film (Delete)."));
280                 b->Add (_remove, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
281
282                 _earlier = new Button (_top_panel, _("Earlier"));
283                 _earlier->SetToolTip (_("Move the selected piece of content earlier in the film."));
284                 b->Add (_earlier, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
285
286                 _later = new Button (_top_panel, _("Later"));
287                 _later->SetToolTip (_("Move the selected piece of content later in the film."));
288                 b->Add (_later, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
289
290                 _timeline = new Button (_top_panel, _("Timeline..."));
291                 _timeline->SetToolTip(_("Open the timeline for the film (Ctrl+T)."));
292                 b->Add (_timeline, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
293
294                 s->Add (b, 0, wxALL, 4);
295                 _top_panel->SetSizer (s);
296         }
297
298         _notebook = new wxNotebook (_splitter, wxID_ANY);
299
300         _timing_panel = new TimingPanel (this, _film_viewer);
301         _notebook->AddPage (_timing_panel, _("Timing"), false);
302         _timing_panel->create ();
303
304         _content->Bind (wxEVT_LIST_ITEM_SELECTED, boost::bind (&ContentPanel::item_selected, this));
305         _content->Bind (wxEVT_LIST_ITEM_DESELECTED, boost::bind (&ContentPanel::item_deselected, this));
306         _content->Bind (wxEVT_LIST_ITEM_RIGHT_CLICK, boost::bind (&ContentPanel::right_click, this, _1));
307         _content->Bind (wxEVT_DROP_FILES, boost::bind (&ContentPanel::files_dropped, this, _1));
308         _add_file->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::add_file_clicked, this));
309         _add_folder->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::add_folder_clicked, this));
310         _add_dcp->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::add_dcp_clicked, this));
311         _remove->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::remove_clicked, this, false));
312         _earlier->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::earlier_clicked, this));
313         _later->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::later_clicked, this));
314         _timeline->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::timeline_clicked, this));
315
316         _content->SetDropTarget(new ContentDropTarget(this));
317 }
318
319
320 void
321 ContentPanel::first_shown ()
322 {
323         _splitter->first_shown (_top_panel, _notebook);
324 }
325
326
327 ContentList
328 ContentPanel::selected ()
329 {
330         ContentList sel;
331         long int s = -1;
332         while (true) {
333                 s = _content->GetNextItem (s, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
334                 if (s == -1) {
335                         break;
336                 }
337
338                 auto weak = _content->content_at_index(s);
339                 if (auto content = weak.lock()) {
340                         sel.push_back(content);
341                 }
342         }
343
344         return sel;
345 }
346
347
348 ContentList
349 ContentPanel::selected_video ()
350 {
351         ContentList vc;
352
353         for (auto i: selected()) {
354                 if (i->video) {
355                         vc.push_back (i);
356                 }
357         }
358
359         return vc;
360 }
361
362
363 ContentList
364 ContentPanel::selected_audio ()
365 {
366         ContentList ac;
367
368         for (auto i: selected()) {
369                 if (i->audio) {
370                         ac.push_back (i);
371                 }
372         }
373
374         return ac;
375 }
376
377
378 ContentList
379 ContentPanel::selected_text ()
380 {
381         ContentList sc;
382
383         for (auto i: selected()) {
384                 if (!i->text.empty()) {
385                         sc.push_back (i);
386                 }
387         }
388
389         return sc;
390 }
391
392
393 FFmpegContentList
394 ContentPanel::selected_ffmpeg ()
395 {
396         FFmpegContentList sc;
397
398         for (auto i: selected()) {
399                 auto t = dynamic_pointer_cast<FFmpegContent> (i);
400                 if (t) {
401                         sc.push_back (t);
402                 }
403         }
404
405         return sc;
406 }
407
408
409 void
410 ContentPanel::film_changed(FilmProperty p)
411 {
412         switch (p) {
413         case FilmProperty::CONTENT:
414         case FilmProperty::CONTENT_ORDER:
415                 setup ();
416                 break;
417         default:
418                 break;
419         }
420
421         for (auto i: panels()) {
422                 i->film_changed (p);
423         }
424 }
425
426
427 void
428 ContentPanel::item_deselected ()
429 {
430         /* Maybe this is just a re-click on the same item; if not, _ignore_deselect will stay
431            false and item_deselected_foo will handle the deselection.
432         */
433         _ignore_deselect = false;
434         signal_manager->when_idle (boost::bind (&ContentPanel::item_deselected_idle, this));
435 }
436
437
438 void
439 ContentPanel::item_deselected_idle ()
440 {
441         if (!_ignore_deselect) {
442                 check_selection ();
443         }
444 }
445
446
447 void
448 ContentPanel::item_selected ()
449 {
450         _ignore_deselect = true;
451         check_selection ();
452 }
453
454
455 void
456 ContentPanel::check_selection ()
457 {
458         if (_no_check_selection) {
459                 return;
460         }
461
462         setup_sensitivity ();
463
464         for (auto i: panels()) {
465                 i->content_selection_changed ();
466         }
467
468         optional<DCPTime> go_to;
469         for (auto content: selected()) {
470                 if (content->paths_valid()) {
471                         auto position = content->position();
472                         if (auto text_content = dynamic_pointer_cast<StringTextFileContent>(content)) {
473                                 /* Rather special case; if we select a text subtitle file jump to its
474                                    first subtitle.
475                                 */
476                                 StringTextFile ts(text_content);
477                                 if (auto first = ts.first()) {
478                                         position += DCPTime(first.get(), _film->active_frame_rate_change(content->position()));
479                                 }
480                         } else if (auto dcp_content = dynamic_pointer_cast<DCPSubtitleContent>(content)) {
481                                 /* Do the same for DCP subtitles */
482                                 DCPSubtitleDecoder ts(_film, dcp_content);
483                                 if (auto first = ts.first()) {
484                                         position += DCPTime(first.get(), _film->active_frame_rate_change(content->position()));
485                                 }
486                         }
487                         if (!go_to || position < go_to.get()) {
488                                 go_to = position;
489                         }
490                 }
491         }
492
493         if (go_to && Config::instance()->jump_to_selected() && signal_manager) {
494                 signal_manager->when_idle(boost::bind(&FilmViewer::seek, &_film_viewer, go_to.get().ceil(_film->video_frame_rate()), true));
495         }
496
497         if (_timeline_dialog) {
498                 _timeline_dialog->set_selection (selected());
499         }
500
501         /* Make required tabs visible */
502
503         if (_notebook->GetPageCount() > 1) {
504                 /* There's more than one tab in the notebook so the current selection could be meaningful
505                    to the user; store it so that we can try to restore it later.
506                 */
507                 _last_selected_tab = 0;
508                 if (_notebook->GetSelection() != wxNOT_FOUND) {
509                         _last_selected_tab = _notebook->GetPage(_notebook->GetSelection());
510                 }
511         }
512
513         bool have_video = false;
514         bool have_audio = false;
515         EnumIndexedVector<bool, TextType> have_text;
516         for (auto i: selected()) {
517                 if (i->video) {
518                         have_video = true;
519                 }
520                 if (i->audio) {
521                         have_audio = true;
522                 }
523                 for (auto j: i->text) {
524                         have_text[static_cast<int>(j->original_type())] = true;
525                 }
526         }
527
528         int off = 0;
529
530         if (have_video && !_video_panel) {
531                 _video_panel = new VideoPanel (this);
532                 _notebook->InsertPage (off, _video_panel, _video_panel->name());
533                 _video_panel->create ();
534         } else if (!have_video && _video_panel) {
535                 _notebook->DeletePage (off);
536                 _video_panel = nullptr;
537         }
538
539         if (have_video) {
540                 ++off;
541         }
542
543         if (have_audio && !_audio_panel) {
544                 _audio_panel = new AudioPanel (this);
545                 _notebook->InsertPage (off, _audio_panel, _audio_panel->name());
546                 _audio_panel->create ();
547         } else if (!have_audio && _audio_panel) {
548                 _notebook->DeletePage (off);
549                 _audio_panel = nullptr;
550         }
551
552         if (have_audio) {
553                 ++off;
554         }
555
556         for (int i = 0; i < static_cast<int>(TextType::COUNT); ++i) {
557                 if (have_text[i] && !_text_panel[i]) {
558                         _text_panel[i] = new TextPanel (this, static_cast<TextType>(i));
559                         _notebook->InsertPage (off, _text_panel[i], _text_panel[i]->name());
560                         _text_panel[i]->create ();
561                 } else if (!have_text[i] && _text_panel[i]) {
562                         _notebook->DeletePage (off);
563                         _text_panel[i] = nullptr;
564                 }
565                 if (have_text[i]) {
566                         ++off;
567                 }
568         }
569
570         /* Set up the tab selection */
571
572         auto done = false;
573         for (size_t i = 0; i < _notebook->GetPageCount(); ++i) {
574                 if (_notebook->GetPage(i) == _last_selected_tab) {
575                         _notebook->SetSelection (i);
576                         done = true;
577                 }
578         }
579
580         if (!done && _notebook->GetPageCount() > 0) {
581                 _notebook->SetSelection (0);
582         }
583
584         setup_sensitivity ();
585         SelectionChanged ();
586 }
587
588
589 void
590 ContentPanel::add_file_clicked ()
591 {
592         /* This method is also called when Ctrl-A is pressed, so check that our notebook page
593            is visible.
594         */
595         if (_parent->GetCurrentPage() != _splitter || !_film) {
596                 return;
597         }
598
599         /* The wxFD_CHANGE_DIR here prevents a `could not set working directory' error 123 on Windows when using
600            non-Latin filenames or paths.
601         */
602         FileDialog dialog(
603                 _splitter,
604                 _("Choose a file or files"),
605                 wxT("All files|*.*|Subtitle files|*.srt;*.xml|Audio files|*.wav;*.w64;*.flac;*.aif;*.aiff"),
606                 wxFD_MULTIPLE | wxFD_CHANGE_DIR,
607                 "AddFilesPath",
608                 add_files_override_path(_film)
609                 );
610
611         if (dialog.show()) {
612                 add_files(dialog.paths());
613         }
614 }
615
616
617 void
618 ContentPanel::add_folder_clicked ()
619 {
620         DirDialog dialog(_splitter, _("Choose a folder"), wxDD_DIR_MUST_EXIST, "AddFilesPath", add_files_override_path(_film));
621         if (dialog.show()) {
622                 add_folder(dialog.path());
623         }
624 }
625
626
627 void
628 ContentPanel::add_folder(boost::filesystem::path folder)
629 {
630         vector<shared_ptr<Content>> content;
631
632         try {
633                 content = content_factory(folder);
634         } catch (exception& e) {
635                 error_dialog (_parent, e.what());
636                 return;
637         }
638
639         if (content.empty ()) {
640                 error_dialog (_parent, _("No content found in this folder."));
641                 return;
642         }
643
644         for (auto i: content) {
645                 auto ic = dynamic_pointer_cast<ImageContent> (i);
646                 if (ic) {
647                         ImageSequenceDialog dialog(_splitter);
648
649                         if (dialog.ShowModal() != wxID_OK) {
650                                 return;
651                         }
652                         ic->set_video_frame_rate(_film, dialog.frame_rate());
653                 }
654
655                 _film->examine_and_add_content (i);
656         }
657 }
658
659
660 void
661 ContentPanel::add_dcp_clicked ()
662 {
663         DirDialog dialog(_splitter, _("Choose a DCP folder"), wxDD_DIR_MUST_EXIST, "AddFilesPath", add_files_override_path(_film));
664         if (dialog.show()) {
665                 add_dcp(dialog.path());
666         }
667 }
668
669
670 void
671 ContentPanel::add_dcp(boost::filesystem::path dcp)
672 {
673         try {
674                 _film->examine_and_add_content(make_shared<DCPContent>(dcp));
675         } catch (ProjectFolderError &) {
676                 error_dialog (
677                         _parent,
678                         _(
679                                 "This looks like a DCP-o-matic project folder, which cannot be added to a different project.  "
680                                 "Choose the DCP folder inside the DCP-o-matic project folder if that's what you want to import."
681                          )
682                         );
683         } catch (exception& e) {
684                 error_dialog(_parent, e.what());
685         }
686 }
687
688
689 /** @return true if this remove "click" should be ignored */
690 bool
691 ContentPanel::remove_clicked (bool hotkey)
692 {
693         /* If the method was called because Delete was pressed check that our notebook page
694            is visible and that the content list is focused.
695         */
696         if (hotkey && (_parent->GetCurrentPage() != _splitter || !_content->HasFocus())) {
697                 return true;
698         }
699
700         for (auto i: selected ()) {
701                 _film->remove_content (i);
702         }
703
704         check_selection ();
705         return false;
706 }
707
708
709 void
710 ContentPanel::timeline_clicked ()
711 {
712         if (!_film || _film->content().empty()) {
713                 return;
714         }
715
716         _timeline_dialog.reset(this, _film, _film_viewer);
717         _timeline_dialog->set_selection (selected());
718         _timeline_dialog->Show ();
719 }
720
721
722 void
723 ContentPanel::right_click (wxListEvent& ev)
724 {
725         _menu->popup (_film, selected (), TimelineContentViewList (), ev.GetPoint ());
726 }
727
728
729 /** Set up broad sensitivity based on the type of content that is selected */
730 void
731 ContentPanel::setup_sensitivity ()
732 {
733         _add_file->Enable (_generally_sensitive);
734         _add_folder->Enable (_generally_sensitive);
735         _add_dcp->Enable (_generally_sensitive);
736
737         auto selection = selected ();
738         auto video_selection = selected_video ();
739         auto audio_selection = selected_audio ();
740
741         _remove->Enable   (_generally_sensitive && !selection.empty());
742         _earlier->Enable  (_generally_sensitive && selection.size() == 1);
743         _later->Enable    (_generally_sensitive && selection.size() == 1);
744         _timeline->Enable (_generally_sensitive && _film && !_film->content().empty());
745
746         if (_video_panel) {
747                 _video_panel->Enable (_generally_sensitive && video_selection.size() > 0);
748         }
749         if (_audio_panel) {
750                 _audio_panel->Enable (_generally_sensitive && audio_selection.size() > 0);
751         }
752         for (auto text: _text_panel) {
753                 if (text) {
754                         text->Enable(_generally_sensitive && selection.size() == 1 && !selection.front()->text.empty());
755                 }
756         }
757         _timing_panel->Enable (_generally_sensitive);
758 }
759
760
761 void
762 ContentPanel::set_film (shared_ptr<Film> film)
763 {
764         if (_audio_panel) {
765                 _audio_panel->set_film (film);
766         }
767
768         _film = film;
769
770         film_changed(FilmProperty::CONTENT);
771         film_changed(FilmProperty::AUDIO_CHANNELS);
772
773         if (_film) {
774                 check_selection ();
775         }
776
777         setup_sensitivity ();
778 }
779
780
781 void
782 ContentPanel::set_general_sensitivity (bool s)
783 {
784         _generally_sensitive = s;
785         setup_sensitivity ();
786 }
787
788
789 void
790 ContentPanel::earlier_clicked ()
791 {
792         auto sel = selected ();
793         if (sel.size() == 1) {
794                 _film->move_content_earlier (sel.front ());
795                 check_selection ();
796         }
797 }
798
799
800 void
801 ContentPanel::later_clicked ()
802 {
803         auto sel = selected ();
804         if (sel.size() == 1) {
805                 _film->move_content_later (sel.front ());
806                 check_selection ();
807         }
808 }
809
810
811 void
812 ContentPanel::set_selection (weak_ptr<Content> wc)
813 {
814         auto content = _film->content ();
815         for (size_t i = 0; i < content.size(); ++i) {
816                 set_selected_state(i, content[i] == wc.lock());
817         }
818 }
819
820
821 void
822 ContentPanel::set_selection (ContentList cl)
823 {
824         {
825                 _no_check_selection = true;
826                 ScopeGuard sg = [this]() { _no_check_selection = false; };
827
828                 auto content = _film->content ();
829                 for (size_t i = 0; i < content.size(); ++i) {
830                         set_selected_state(i, find(cl.begin(), cl.end(), content[i]) != cl.end());
831                 }
832         }
833
834         check_selection ();
835 }
836
837
838 void
839 ContentPanel::select_all ()
840 {
841         set_selection (_film->content());
842 }
843
844
845 void
846 ContentPanel::film_content_changed (int property)
847 {
848         if (
849                 property == ContentProperty::PATH ||
850                 property == DCPContentProperty::NEEDS_ASSETS ||
851                 property == DCPContentProperty::NEEDS_KDM ||
852                 property == DCPContentProperty::NAME
853                 ) {
854
855                 setup ();
856         }
857
858         for (auto i: panels()) {
859                 i->film_content_changed (property);
860         }
861 }
862
863
864 static
865 wxString
866 text_for_content(shared_ptr<Content> content, bool& alert)
867 {
868         bool const valid = content->paths_valid();
869
870         auto dcp = dynamic_pointer_cast<DCPContent>(content);
871         bool const needs_kdm = dcp && dcp->needs_kdm();
872         bool const needs_assets = dcp && dcp->needs_assets();
873
874         auto s = std_to_wx(content->summary());
875
876         if (!valid) {
877                 s = _("MISSING: ") + s;
878         }
879
880         if (needs_kdm) {
881                 s = _("NEEDS KDM: ") + s;
882         }
883
884         if (needs_assets) {
885                 s = _("NEEDS OV: ") + s;
886         }
887
888         alert = !valid || needs_kdm || needs_assets;
889
890         return s;
891 }
892
893
894 void
895 ContentPanel::setup ()
896 {
897         if (!_film) {
898                 _content->DeleteAllItems ();
899                 setup_sensitivity ();
900                 return;
901         }
902
903         auto content = _film->content ();
904         auto selection = selected();
905
906         vector<ContentListCtrl::Item> items;
907
908         for (auto i: content) {
909                 int const t = _content->GetItemCount ();
910                 bool alert = false;
911                 wxString const s = text_for_content(i, alert);
912                 items.push_back({s, i, alert});
913
914                 wxListItem item;
915                 item.SetId (t);
916                 item.SetText (s);
917                 item.SetData (i.get ());
918                 _content->InsertItem (item);
919
920                 if (i.get() == selected_content) {
921                         _content->SetItemState (t, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
922                 }
923
924                 if (alert) {
925                         _content->SetItemTextColour (t, *wxRED);
926                 }
927         }
928
929         _content->set(items);
930
931         if (selection.empty() && !content.empty()) {
932                 set_selected_state(0, true);
933         } else {
934                 set_selection(selection);
935         }
936
937         setup_sensitivity ();
938 }
939
940
941 void
942 ContentPanel::files_dropped (wxDropFilesEvent& event)
943 {
944         if (!_film) {
945                 return;
946         }
947
948         auto paths = event.GetFiles ();
949         vector<boost::filesystem::path> path_list;
950         for (int i = 0; i < event.GetNumberOfFiles(); i++) {
951                 path_list.push_back (wx_to_std(paths[i]));
952         }
953
954         add_files (path_list);
955 }
956
957
958 void
959 ContentPanel::add_files (vector<boost::filesystem::path> paths)
960 {
961         if (!_film) {
962                 return;
963         }
964
965         /* It has been reported that the paths returned from e.g. wxFileDialog are not always sorted;
966            I can't reproduce that, but sort them anyway.  Don't use ImageFilenameSorter as a normal
967            alphabetical sort is expected here.
968         */
969
970         std::sort (paths.begin(), paths.end(), CaseInsensitiveSorter());
971
972         /* XXX: check for lots of files here and do something */
973
974         try {
975                 for (auto i: paths) {
976                         for (auto j: content_factory(i)) {
977                                 _film->examine_and_add_content (j);
978                         }
979                 }
980         } catch (exception& e) {
981                 error_dialog (_parent, e.what());
982         }
983 }
984
985
986 list<ContentSubPanel*>
987 ContentPanel::panels () const
988 {
989         list<ContentSubPanel*> p;
990         if (_video_panel) {
991                 p.push_back (_video_panel);
992         }
993         if (_audio_panel) {
994                 p.push_back (_audio_panel);
995         }
996         for (auto text: _text_panel) {
997                 if (text) {
998                         p.push_back(text);
999                 }
1000         }
1001         p.push_back (_timing_panel);
1002         return p;
1003 }
1004
1005
1006 void
1007 ContentPanel::set_selected_state(int item, bool state)
1008 {
1009         _content->SetItemState(item, state ? wxLIST_STATE_SELECTED : 0, wxLIST_STATE_SELECTED);
1010         _content->SetItemState(item, state ? wxLIST_STATE_FOCUSED : 0, wxLIST_STATE_FOCUSED);
1011 }
1012
1013
1014 wxWindow*
1015 ContentPanel::window() const
1016 {
1017         return _splitter;
1018 }
1019
1020
1021 void
1022 ContentPanel::cucumber_add_content_file (string filename)
1023 {
1024         add_files ({filename});
1025         auto jm = JobManager::instance ();
1026         while (jm->work_to_do()) {
1027                 while (signal_manager->ui_idle()) {}
1028                 dcpomatic_sleep_seconds (1);
1029         }
1030         while (signal_manager->ui_idle()) {}
1031 }
1032
1033
1034 /** This may not be called from the GUI thread */
1035 std::string
1036 ContentPanel::cucumber_get_content_list ()
1037 {
1038         /* The playlist method that we end up calling has a mutex, so this should be ok */
1039         std::string s;
1040         for (auto content: _film->content()) {
1041                 bool alert;
1042                 s += wx_to_std (text_for_content(content, alert)) + "\n";
1043         }
1044         return s;
1045 }