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