8e6c49294326a6d6edacd9b75e7f72c8f0802338
[dcpomatic.git] / src / wx / content_panel.cc
1 /*
2     Copyright (C) 2012-2018 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21 #include "content_panel.h"
22 #include "wx_util.h"
23 #include "video_panel.h"
24 #include "audio_panel.h"
25 #include "text_panel.h"
26 #include "timing_panel.h"
27 #include "timeline_dialog.h"
28 #include "image_sequence_dialog.h"
29 #include "film_viewer.h"
30 #include "lib/audio_content.h"
31 #include "lib/text_content.h"
32 #include "lib/video_content.h"
33 #include "lib/ffmpeg_content.h"
34 #include "lib/content_factory.h"
35 #include "lib/image_content.h"
36 #include "lib/dcp_content.h"
37 #include "lib/case_insensitive_sorter.h"
38 #include "lib/playlist.h"
39 #include "lib/config.h"
40 #include "lib/log.h"
41 #include "lib/compose.hpp"
42 #include "lib/string_text_file_content.h"
43 #include "lib/string_text_file.h"
44 #include <wx/wx.h>
45 #include <wx/notebook.h>
46 #include <wx/listctrl.h>
47 #include <boost/filesystem.hpp>
48 #include <boost/foreach.hpp>
49 #include <iostream>
50
51 using std::list;
52 using std::string;
53 using std::cout;
54 using std::vector;
55 using std::max;
56 using std::exception;
57 using boost::shared_ptr;
58 using boost::weak_ptr;
59 using boost::dynamic_pointer_cast;
60 using boost::optional;
61
62 #define LOG_GENERAL(...) _film->log()->log (String::compose (__VA_ARGS__), LogEntry::TYPE_GENERAL);
63
64 ContentPanel::ContentPanel (wxNotebook* n, shared_ptr<Film> film, weak_ptr<FilmViewer> viewer)
65         : _video_panel (0)
66         , _audio_panel (0)
67         , _timeline_dialog (0)
68         , _parent (n)
69         , _last_selected_tab (0)
70         , _film (film)
71         , _film_viewer (viewer)
72         , _generally_sensitive (true)
73         , _ignore_deselect (false)
74 {
75         for (int i = 0; i < TEXT_COUNT; ++i) {
76                 _text_panel[i] = 0;
77         }
78
79         _panel = new wxPanel (n);
80         _sizer = new wxBoxSizer (wxVERTICAL);
81         _panel->SetSizer (_sizer);
82
83         _menu = new ContentMenu (_panel);
84
85         {
86                 wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
87
88                 _content = new wxListCtrl (_panel, wxID_ANY, wxDefaultPosition, wxSize (320, 160), wxLC_REPORT | wxLC_NO_HEADER);
89                 _content->DragAcceptFiles (true);
90                 s->Add (_content, 1, wxEXPAND | wxTOP | wxBOTTOM, 6);
91
92                 _content->InsertColumn (0, wxT(""));
93                 _content->SetColumnWidth (0, 512);
94
95                 wxBoxSizer* b = new wxBoxSizer (wxVERTICAL);
96
97                 _add_file = new wxButton (_panel, wxID_ANY, _("Add file(s)..."));
98                 _add_file->SetToolTip (_("Add video, image, sound or subtitle files to the film."));
99                 b->Add (_add_file, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
100
101                 _add_folder = new wxButton (_panel, wxID_ANY, _("Add folder..."));
102                 _add_folder->SetToolTip (_("Add a folder of image files (which will be used as a moving image sequence) or a folder of sound files."));
103                 b->Add (_add_folder, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
104
105                 _add_dcp = new wxButton (_panel, wxID_ANY, _("Add DCP..."));
106                 _add_dcp->SetToolTip (_("Add a DCP."));
107                 b->Add (_add_dcp, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
108
109                 _remove = new wxButton (_panel, wxID_ANY, _("Remove"));
110                 _remove->SetToolTip (_("Remove the selected piece of content from the film."));
111                 b->Add (_remove, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
112
113                 _earlier = new wxButton (_panel, wxID_ANY, _("Earlier"));
114                 _earlier->SetToolTip (_("Move the selected piece of content earlier in the film."));
115                 b->Add (_earlier, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
116
117                 _later = new wxButton (_panel, wxID_ANY, _("Later"));
118                 _later->SetToolTip (_("Move the selected piece of content later in the film."));
119                 b->Add (_later, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
120
121                 _timeline = new wxButton (_panel, wxID_ANY, _("Timeline..."));
122                 _timeline->SetToolTip (_("Open the timeline for the film."));
123                 b->Add (_timeline, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
124
125                 s->Add (b, 0, wxALL, 4);
126
127                 _sizer->Add (s, 0, wxEXPAND | wxALL, 6);
128         }
129
130         _notebook = new wxNotebook (_panel, wxID_ANY);
131         _sizer->Add (_notebook, 1, wxEXPAND | wxTOP, 6);
132
133         _timing_panel = new TimingPanel (this, _film_viewer);
134         _notebook->AddPage (_timing_panel, _("Timing"), false);
135
136         _content->Bind (wxEVT_LIST_ITEM_SELECTED, boost::bind (&ContentPanel::item_selected, this));
137         _content->Bind (wxEVT_LIST_ITEM_DESELECTED, boost::bind (&ContentPanel::item_deselected, this));
138         _content->Bind (wxEVT_LIST_ITEM_RIGHT_CLICK, boost::bind (&ContentPanel::right_click, this, _1));
139         _content->Bind (wxEVT_DROP_FILES, boost::bind (&ContentPanel::files_dropped, this, _1));
140         _add_file->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::add_file_clicked, this));
141         _add_folder->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::add_folder_clicked, this));
142         _add_dcp->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::add_dcp_clicked, this));
143         _remove->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::remove_clicked, this, false));
144         _earlier->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::earlier_clicked, this));
145         _later->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::later_clicked, this));
146         _timeline->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::timeline_clicked, this));
147 }
148
149 ContentList
150 ContentPanel::selected ()
151 {
152         ContentList sel;
153         long int s = -1;
154         while (true) {
155                 s = _content->GetNextItem (s, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
156                 if (s == -1) {
157                         break;
158                 }
159
160                 ContentList cl = _film->content();
161                 if (s < int (cl.size())) {
162                         sel.push_back (cl[s]);
163                 }
164         }
165
166         return sel;
167 }
168
169 ContentList
170 ContentPanel::selected_video ()
171 {
172         ContentList vc;
173
174         BOOST_FOREACH (shared_ptr<Content> i, selected ()) {
175                 if (i->video) {
176                         vc.push_back (i);
177                 }
178         }
179
180         return vc;
181 }
182
183 ContentList
184 ContentPanel::selected_audio ()
185 {
186         ContentList ac;
187
188         BOOST_FOREACH (shared_ptr<Content> i, selected ()) {
189                 if (i->audio) {
190                         ac.push_back (i);
191                 }
192         }
193
194         return ac;
195 }
196
197 ContentList
198 ContentPanel::selected_text ()
199 {
200         ContentList sc;
201
202         BOOST_FOREACH (shared_ptr<Content> i, selected ()) {
203                 if (!i->text.empty()) {
204                         sc.push_back (i);
205                 }
206         }
207
208         return sc;
209 }
210
211 FFmpegContentList
212 ContentPanel::selected_ffmpeg ()
213 {
214         FFmpegContentList sc;
215
216         BOOST_FOREACH (shared_ptr<Content> i, selected ()) {
217                 shared_ptr<FFmpegContent> t = dynamic_pointer_cast<FFmpegContent> (i);
218                 if (t) {
219                         sc.push_back (t);
220                 }
221         }
222
223         return sc;
224 }
225
226 void
227 ContentPanel::film_changed (Film::Property p)
228 {
229         switch (p) {
230         case Film::CONTENT:
231         case Film::CONTENT_ORDER:
232                 setup ();
233                 break;
234         default:
235                 break;
236         }
237
238         BOOST_FOREACH (ContentSubPanel* i, panels()) {
239                 i->film_changed (p);
240         }
241 }
242
243 void
244 ContentPanel::item_deselected ()
245 {
246         /* Maybe this is just a re-click on the same item; if not, _ignore_deselect will stay
247            false and item_deselected_foo will handle the deselection.
248         */
249         _ignore_deselect = false;
250         signal_manager->when_idle (boost::bind (&ContentPanel::item_deselected_idle, this));
251 }
252
253 void
254 ContentPanel::item_deselected_idle ()
255 {
256         if (!_ignore_deselect) {
257                 check_selection ();
258         }
259 }
260
261 void
262 ContentPanel::item_selected ()
263 {
264         _ignore_deselect = true;
265         check_selection ();
266 }
267
268 void
269 ContentPanel::check_selection ()
270 {
271         if (_last_selected == selected()) {
272                 /* This was triggered by a re-build of the view but the selection
273                    did not really change.
274                 */
275                 return;
276         }
277
278         _last_selected = selected ();
279
280         setup_sensitivity ();
281
282         BOOST_FOREACH (ContentSubPanel* i, panels()) {
283                 i->content_selection_changed ();
284         }
285
286         optional<DCPTime> go_to;
287         BOOST_FOREACH (shared_ptr<Content> i, selected()) {
288                 DCPTime p;
289                 p = i->position();
290                 if (dynamic_pointer_cast<StringTextFileContent>(i) && i->paths_valid()) {
291                         /* Rather special case; if we select a text subtitle file jump to its
292                            first subtitle.
293                         */
294                         StringTextFile ts (dynamic_pointer_cast<StringTextFileContent>(i));
295                         if (ts.first()) {
296                                 p += DCPTime(ts.first().get(), _film->active_frame_rate_change(i->position()));
297                         }
298                 }
299                 if (!go_to || p < go_to.get()) {
300                         go_to = p;
301                 }
302         }
303
304         if (go_to && Config::instance()->jump_to_selected() && signal_manager) {
305                 shared_ptr<FilmViewer> fv = _film_viewer.lock ();
306                 DCPOMATIC_ASSERT (fv);
307                 signal_manager->when_idle(boost::bind(&FilmViewer::seek, fv.get(), go_to.get().ceil(_film->video_frame_rate()), true));
308         }
309
310         if (_timeline_dialog) {
311                 _timeline_dialog->set_selection (selected());
312         }
313
314         /* Make required tabs visible */
315
316         if (_notebook->GetPageCount() > 1) {
317                 /* There's more than one tab in the notebook so the current selection could be meaningful
318                    to the user; store it so that we can try to restore it later.
319                 */
320                 _last_selected_tab = 0;
321                 if (_notebook->GetSelection() != wxNOT_FOUND) {
322                         _last_selected_tab = _notebook->GetPage(_notebook->GetSelection());
323                 }
324         }
325
326         bool have_video = false;
327         bool have_audio = false;
328         bool have_text[TEXT_COUNT] = { false, false };
329         BOOST_FOREACH (shared_ptr<Content> i, selected()) {
330                 if (i->video) {
331                         have_video = true;
332                 }
333                 if (i->audio) {
334                         have_audio = true;
335                 }
336                 BOOST_FOREACH (shared_ptr<TextContent> j, i->text) {
337                         have_text[j->original_type()] = true;
338                 }
339         }
340
341         int off = 0;
342
343         if (have_video && !_video_panel) {
344                 _video_panel = new VideoPanel (this);
345                 _notebook->InsertPage (off, _video_panel, _video_panel->name());
346         } else if (!have_video && _video_panel) {
347                 _notebook->DeletePage (off);
348                 _video_panel = 0;
349         }
350
351         if (have_video) {
352                 ++off;
353         }
354
355         if (have_audio && !_audio_panel) {
356                 _audio_panel = new AudioPanel (this);
357                 _notebook->InsertPage (off, _audio_panel, _audio_panel->name());
358         } else if (!have_audio && _audio_panel) {
359                 _notebook->DeletePage (off);
360                 _audio_panel = 0;
361         }
362
363         if (have_audio) {
364                 ++off;
365         }
366
367         for (int i = 0; i < TEXT_COUNT; ++i) {
368                 if (have_text[i] && !_text_panel[i]) {
369                         _text_panel[i] = new TextPanel (this, static_cast<TextType>(i));
370                         _notebook->InsertPage (off, _text_panel[i], _text_panel[i]->name());
371                 } else if (!have_text[i] && _text_panel[i]) {
372                         _notebook->DeletePage (off);
373                         _text_panel[i] = 0;
374                 }
375                 if (have_text[i]) {
376                         ++off;
377                 }
378         }
379
380         /* Set up the tab selection */
381
382         bool done = false;
383         for (size_t i = 0; i < _notebook->GetPageCount(); ++i) {
384                 if (_notebook->GetPage(i) == _last_selected_tab) {
385                         _notebook->SetSelection (i);
386                         done = true;
387                 }
388         }
389
390         if (!done && _notebook->GetPageCount() > 0) {
391                 _notebook->SetSelection (0);
392         }
393
394         setup_sensitivity ();
395         SelectionChanged ();
396 }
397
398 void
399 ContentPanel::add_file_clicked ()
400 {
401         /* This method is also called when Ctrl-A is pressed, so check that our notebook page
402            is visible.
403         */
404         if (_parent->GetCurrentPage() != _panel || !_film) {
405                 return;
406         }
407
408         /* The wxFD_CHANGE_DIR here prevents a `could not set working directory' error 123 on Windows when using
409            non-Latin filenames or paths.
410         */
411         wxFileDialog* d = new wxFileDialog (
412                 _panel,
413                 _("Choose a file or files"),
414                 wxT (""),
415                 wxT (""),
416                 wxT ("All files|*.*|Subtitle files|*.srt;*.xml|Audio files|*.wav;*.w64;*.flac;*.aif;*.aiff"),
417                 wxFD_MULTIPLE | wxFD_CHANGE_DIR
418                 );
419
420         int const r = d->ShowModal ();
421
422         if (r != wxID_OK) {
423                 d->Destroy ();
424                 return;
425         }
426
427         wxArrayString paths;
428         d->GetPaths (paths);
429         list<boost::filesystem::path> path_list;
430         for (unsigned int i = 0; i < paths.GetCount(); ++i) {
431                 path_list.push_back (wx_to_std (paths[i]));
432         }
433         add_files (path_list);
434
435         d->Destroy ();
436 }
437
438 void
439 ContentPanel::add_folder_clicked ()
440 {
441         wxDirDialog* d = new wxDirDialog (_panel, _("Choose a folder"), wxT (""), wxDD_DIR_MUST_EXIST);
442         int r = d->ShowModal ();
443         boost::filesystem::path const path (wx_to_std (d->GetPath ()));
444         d->Destroy ();
445
446         if (r != wxID_OK) {
447                 return;
448         }
449
450         list<shared_ptr<Content> > content;
451
452         try {
453                 content = content_factory (_film, path);
454         } catch (exception& e) {
455                 error_dialog (_parent, e.what());
456                 return;
457         }
458
459         if (content.empty ()) {
460                 error_dialog (_parent, _("No content found in this folder."));
461                 return;
462         }
463
464         BOOST_FOREACH (shared_ptr<Content> i, content) {
465                 shared_ptr<ImageContent> ic = dynamic_pointer_cast<ImageContent> (i);
466                 if (ic) {
467                         ImageSequenceDialog* e = new ImageSequenceDialog (_panel);
468                         r = e->ShowModal ();
469                         float const frame_rate = e->frame_rate ();
470                         e->Destroy ();
471
472                         if (r != wxID_OK) {
473                                 return;
474                         }
475
476                         ic->set_video_frame_rate (frame_rate);
477                 }
478
479                 _film->examine_and_add_content (i);
480         }
481 }
482
483 void
484 ContentPanel::add_dcp_clicked ()
485 {
486         wxDirDialog* d = new wxDirDialog (_panel, _("Choose a DCP folder"), wxT (""), wxDD_DIR_MUST_EXIST);
487         int r = d->ShowModal ();
488         boost::filesystem::path const path (wx_to_std (d->GetPath ()));
489         d->Destroy ();
490
491         if (r != wxID_OK) {
492                 return;
493         }
494
495         try {
496                 _film->examine_and_add_content (shared_ptr<Content> (new DCPContent (_film, path)));
497         } catch (exception& e) {
498                 error_dialog (_parent, e.what());
499         }
500 }
501
502 /** @return true if this remove "click" should be ignored */
503 bool
504 ContentPanel::remove_clicked (bool hotkey)
505 {
506         /* If the method was called because Delete was pressed check that our notebook page
507            is visible and that the content list is focussed.
508         */
509         if (hotkey && (_parent->GetCurrentPage() != _panel || !_content->HasFocus())) {
510                 return true;
511         }
512
513         BOOST_FOREACH (shared_ptr<Content> i, selected ()) {
514                 _film->remove_content (i);
515         }
516
517         check_selection ();
518         return false;
519 }
520
521 void
522 ContentPanel::timeline_clicked ()
523 {
524         if (!_film) {
525                 return;
526         }
527
528         if (_timeline_dialog) {
529                 _timeline_dialog->Destroy ();
530                 _timeline_dialog = 0;
531         }
532
533         _timeline_dialog = new TimelineDialog (this, _film);
534         _timeline_dialog->set_selection (selected());
535         _timeline_dialog->Show ();
536 }
537
538 void
539 ContentPanel::right_click (wxListEvent& ev)
540 {
541         _menu->popup (_film, selected (), TimelineContentViewList (), ev.GetPoint ());
542 }
543
544 /** Set up broad sensitivity based on the type of content that is selected */
545 void
546 ContentPanel::setup_sensitivity ()
547 {
548         _add_file->Enable (_generally_sensitive);
549         _add_folder->Enable (_generally_sensitive);
550         _add_dcp->Enable (_generally_sensitive);
551
552         ContentList selection = selected ();
553         ContentList video_selection = selected_video ();
554         ContentList audio_selection = selected_audio ();
555
556         _remove->Enable   (_generally_sensitive && !selection.empty());
557         _earlier->Enable  (_generally_sensitive && selection.size() == 1);
558         _later->Enable    (_generally_sensitive && selection.size() == 1);
559         _timeline->Enable (_generally_sensitive && _film && !_film->content().empty());
560
561         if (_video_panel) {
562                 _video_panel->Enable (_generally_sensitive && video_selection.size() > 0);
563         }
564         if (_audio_panel) {
565                 _audio_panel->Enable (_generally_sensitive && audio_selection.size() > 0);
566         }
567         for (int i = 0; i < TEXT_COUNT; ++i) {
568                 if (_text_panel[i]) {
569                         _text_panel[i]->Enable (_generally_sensitive && selection.size() == 1 && !selection.front()->text.empty());
570                 }
571         }
572         _timing_panel->Enable   (_generally_sensitive);
573 }
574
575 void
576 ContentPanel::set_film (shared_ptr<Film> film)
577 {
578         if (_audio_panel) {
579                 _audio_panel->set_film (film);
580         }
581
582         _film = film;
583
584         film_changed (Film::CONTENT);
585         film_changed (Film::AUDIO_CHANNELS);
586         check_selection ();
587         setup_sensitivity ();
588 }
589
590 void
591 ContentPanel::set_general_sensitivity (bool s)
592 {
593         _generally_sensitive = s;
594         setup_sensitivity ();
595 }
596
597 void
598 ContentPanel::earlier_clicked ()
599 {
600         ContentList sel = selected ();
601         if (sel.size() == 1) {
602                 _film->move_content_earlier (sel.front ());
603                 check_selection ();
604         }
605 }
606
607 void
608 ContentPanel::later_clicked ()
609 {
610         ContentList sel = selected ();
611         if (sel.size() == 1) {
612                 _film->move_content_later (sel.front ());
613                 check_selection ();
614         }
615 }
616
617 void
618 ContentPanel::set_selection (weak_ptr<Content> wc)
619 {
620         ContentList content = _film->content ();
621         for (size_t i = 0; i < content.size(); ++i) {
622                 if (content[i] == wc.lock ()) {
623                         _content->SetItemState (i, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
624                 } else {
625                         _content->SetItemState (i, 0, wxLIST_STATE_SELECTED);
626                 }
627         }
628 }
629
630 void
631 ContentPanel::set_selection (ContentList cl)
632 {
633         ContentList content = _film->content ();
634         for (size_t i = 0; i < content.size(); ++i) {
635                 if (find(cl.begin(), cl.end(), content[i]) != cl.end()) {
636                         _content->SetItemState (i, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
637                 } else {
638                         _content->SetItemState (i, 0, wxLIST_STATE_SELECTED);
639                 }
640         }
641 }
642
643 void
644 ContentPanel::film_content_changed (int property)
645 {
646         if (
647                 property == ContentProperty::PATH ||
648                 property == DCPContentProperty::NEEDS_ASSETS ||
649                 property == DCPContentProperty::NEEDS_KDM ||
650                 property == DCPContentProperty::NAME
651                 ) {
652
653                 setup ();
654         }
655
656         BOOST_FOREACH (ContentSubPanel* i, panels()) {
657                 i->film_content_changed (property);
658         }
659 }
660
661 void
662 ContentPanel::setup ()
663 {
664         ContentList content = _film->content ();
665
666         Content* selected_content = 0;
667         int const s = _content->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
668         if (s != -1) {
669                 wxListItem item;
670                 item.SetId (s);
671                 item.SetMask (wxLIST_MASK_DATA);
672                 _content->GetItem (item);
673                 selected_content = reinterpret_cast<Content*> (item.GetData ());
674         }
675
676         _content->DeleteAllItems ();
677
678         BOOST_FOREACH (shared_ptr<Content> i, content) {
679                 int const t = _content->GetItemCount ();
680                 bool const valid = i->paths_valid ();
681
682                 /* Temporary debugging for Igor */
683                 BOOST_FOREACH (boost::filesystem::path j, i->paths()) {
684                         LOG_GENERAL ("Check %1 %2 answer %3", j.string(), boost::filesystem::exists(j) ? "yes" : "no", valid ? "yes" : "no");
685                 }
686
687                 shared_ptr<DCPContent> dcp = dynamic_pointer_cast<DCPContent> (i);
688                 bool const needs_kdm = dcp && dcp->needs_kdm ();
689                 bool const needs_assets = dcp && dcp->needs_assets ();
690
691                 wxString s = std_to_wx (i->summary ());
692
693                 if (!valid) {
694                         s = _("MISSING: ") + s;
695                 }
696
697                 if (needs_kdm) {
698                         s = _("NEEDS KDM: ") + s;
699                 }
700
701                 if (needs_assets) {
702                         s = _("NEEDS OV: ") + s;
703                 }
704
705                 wxListItem item;
706                 item.SetId (t);
707                 item.SetText (s);
708                 item.SetData (i.get ());
709                 _content->InsertItem (item);
710
711                 if (i.get() == selected_content) {
712                         _content->SetItemState (t, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
713                 }
714
715                 if (!valid || needs_kdm || needs_assets) {
716                         _content->SetItemTextColour (t, *wxRED);
717                 }
718         }
719
720         if (!selected_content && !content.empty ()) {
721                 /* Select the item of content if none was selected before */
722                 _content->SetItemState (0, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
723         }
724
725         setup_sensitivity ();
726 }
727
728 void
729 ContentPanel::files_dropped (wxDropFilesEvent& event)
730 {
731         if (!_film) {
732                 return;
733         }
734
735         wxString* paths = event.GetFiles ();
736         list<boost::filesystem::path> path_list;
737         for (int i = 0; i < event.GetNumberOfFiles(); i++) {
738                 path_list.push_back (wx_to_std (paths[i]));
739         }
740
741         add_files (path_list);
742 }
743
744 void
745 ContentPanel::add_files (list<boost::filesystem::path> paths)
746 {
747         /* It has been reported that the paths returned from e.g. wxFileDialog are not always sorted;
748            I can't reproduce that, but sort them anyway.  Don't use ImageFilenameSorter as a normal
749            alphabetical sort is expected here.
750         */
751
752         paths.sort (CaseInsensitiveSorter ());
753
754         /* XXX: check for lots of files here and do something */
755
756         try {
757                 BOOST_FOREACH (boost::filesystem::path i, paths) {
758                         BOOST_FOREACH (shared_ptr<Content> j, content_factory (_film, i)) {
759                                 _film->examine_and_add_content (j);
760                         }
761                 }
762         } catch (exception& e) {
763                 error_dialog (_parent, e.what());
764         }
765 }
766
767 list<ContentSubPanel*>
768 ContentPanel::panels () const
769 {
770         list<ContentSubPanel*> p;
771         if (_video_panel) {
772                 p.push_back (_video_panel);
773         }
774         if (_audio_panel) {
775                 p.push_back (_audio_panel);
776         }
777         for (int i = 0; i < TEXT_COUNT; ++i) {
778                 if (_text_panel[i]) {
779                         p.push_back (_text_panel[i]);
780                 }
781         }
782         p.push_back (_timing_panel);
783         return p;
784 }