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