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