Work around visual glitch when clicking on an already-selected
[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, boost::shared_ptr<Film> film, 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                 signal_manager->when_idle(boost::bind(&FilmViewer::set_position, _film_viewer, go_to.get().ceil(_film->video_frame_rate())));
306         }
307
308         if (_timeline_dialog) {
309                 _timeline_dialog->set_selection (selected());
310         }
311
312         /* Make required tabs visible */
313
314         if (_notebook->GetPageCount() > 1) {
315                 /* There's more than one tab in the notebook so the current selection could be meaningful
316                    to the user; store it so that we can try to restore it later.
317                 */
318                 _last_selected_tab = 0;
319                 if (_notebook->GetSelection() != wxNOT_FOUND) {
320                         _last_selected_tab = _notebook->GetPage(_notebook->GetSelection());
321                 }
322         }
323
324         bool have_video = false;
325         bool have_audio = false;
326         bool have_text[TEXT_COUNT] = { false, false };
327         BOOST_FOREACH (shared_ptr<Content> i, selected()) {
328                 if (i->video) {
329                         have_video = true;
330                 }
331                 if (i->audio) {
332                         have_audio = true;
333                 }
334                 BOOST_FOREACH (shared_ptr<TextContent> j, i->text) {
335                         have_text[j->original_type()] = true;
336                 }
337         }
338
339         int off = 0;
340
341         if (have_video && !_video_panel) {
342                 _video_panel = new VideoPanel (this);
343                 _notebook->InsertPage (off, _video_panel, _video_panel->name());
344         } else if (!have_video && _video_panel) {
345                 _notebook->DeletePage (off);
346                 _video_panel = 0;
347         }
348
349         if (have_video) {
350                 ++off;
351         }
352
353         if (have_audio && !_audio_panel) {
354                 _audio_panel = new AudioPanel (this);
355                 _notebook->InsertPage (off, _audio_panel, _audio_panel->name());
356         } else if (!have_audio && _audio_panel) {
357                 _notebook->DeletePage (off);
358                 _audio_panel = 0;
359         }
360
361         if (have_audio) {
362                 ++off;
363         }
364
365         for (int i = 0; i < TEXT_COUNT; ++i) {
366                 if (have_text[i] && !_text_panel[i]) {
367                         _text_panel[i] = new TextPanel (this, static_cast<TextType>(i));
368                         _notebook->InsertPage (off, _text_panel[i], _text_panel[i]->name());
369                 } else if (!have_text[i] && _text_panel[i]) {
370                         _notebook->DeletePage (off);
371                         _text_panel[i] = 0;
372                 }
373                 if (have_text[i]) {
374                         ++off;
375                 }
376         }
377
378         /* Set up the tab selection */
379
380         bool done = false;
381         for (size_t i = 0; i < _notebook->GetPageCount(); ++i) {
382                 if (_notebook->GetPage(i) == _last_selected_tab) {
383                         _notebook->SetSelection (i);
384                         done = true;
385                 }
386         }
387
388         if (!done && _notebook->GetPageCount() > 0) {
389                 _notebook->SetSelection (0);
390         }
391
392         setup_sensitivity ();
393         SelectionChanged ();
394 }
395
396 void
397 ContentPanel::add_file_clicked ()
398 {
399         /* This method is also called when Ctrl-A is pressed, so check that our notebook page
400            is visible.
401         */
402         if (_parent->GetCurrentPage() != _panel || !_film) {
403                 return;
404         }
405
406         /* The wxFD_CHANGE_DIR here prevents a `could not set working directory' error 123 on Windows when using
407            non-Latin filenames or paths.
408         */
409         wxFileDialog* d = new wxFileDialog (
410                 _panel,
411                 _("Choose a file or files"),
412                 wxT (""),
413                 wxT (""),
414                 wxT ("All files|*.*|Subtitle files|*.srt;*.xml|Audio files|*.wav;*.w64;*.flac;*.aif;*.aiff"),
415                 wxFD_MULTIPLE | wxFD_CHANGE_DIR
416                 );
417
418         int const r = d->ShowModal ();
419
420         if (r != wxID_OK) {
421                 d->Destroy ();
422                 return;
423         }
424
425         wxArrayString paths;
426         d->GetPaths (paths);
427         list<boost::filesystem::path> path_list;
428         for (unsigned int i = 0; i < paths.GetCount(); ++i) {
429                 path_list.push_back (wx_to_std (paths[i]));
430         }
431         add_files (path_list);
432
433         d->Destroy ();
434 }
435
436 void
437 ContentPanel::add_folder_clicked ()
438 {
439         wxDirDialog* d = new wxDirDialog (_panel, _("Choose a folder"), wxT (""), wxDD_DIR_MUST_EXIST);
440         int r = d->ShowModal ();
441         boost::filesystem::path const path (wx_to_std (d->GetPath ()));
442         d->Destroy ();
443
444         if (r != wxID_OK) {
445                 return;
446         }
447
448         list<shared_ptr<Content> > content;
449
450         try {
451                 content = content_factory (_film, path);
452         } catch (exception& e) {
453                 error_dialog (_parent, e.what());
454                 return;
455         }
456
457         if (content.empty ()) {
458                 error_dialog (_parent, _("No content found in this folder."));
459                 return;
460         }
461
462         BOOST_FOREACH (shared_ptr<Content> i, content) {
463                 shared_ptr<ImageContent> ic = dynamic_pointer_cast<ImageContent> (i);
464                 if (ic) {
465                         ImageSequenceDialog* e = new ImageSequenceDialog (_panel);
466                         r = e->ShowModal ();
467                         float const frame_rate = e->frame_rate ();
468                         e->Destroy ();
469
470                         if (r != wxID_OK) {
471                                 return;
472                         }
473
474                         ic->set_video_frame_rate (frame_rate);
475                 }
476
477                 _film->examine_and_add_content (i);
478         }
479 }
480
481 void
482 ContentPanel::add_dcp_clicked ()
483 {
484         wxDirDialog* d = new wxDirDialog (_panel, _("Choose a DCP folder"), wxT (""), wxDD_DIR_MUST_EXIST);
485         int r = d->ShowModal ();
486         boost::filesystem::path const path (wx_to_std (d->GetPath ()));
487         d->Destroy ();
488
489         if (r != wxID_OK) {
490                 return;
491         }
492
493         try {
494                 _film->examine_and_add_content (shared_ptr<Content> (new DCPContent (_film, path)));
495         } catch (exception& e) {
496                 error_dialog (_parent, e.what());
497         }
498 }
499
500 /** @return true if this remove "click" should be ignored */
501 bool
502 ContentPanel::remove_clicked (bool hotkey)
503 {
504         /* If the method was called because Delete was pressed check that our notebook page
505            is visible and that the content list is focussed.
506         */
507         if (hotkey && (_parent->GetCurrentPage() != _panel || !_content->HasFocus())) {
508                 return true;
509         }
510
511         BOOST_FOREACH (shared_ptr<Content> i, selected ()) {
512                 _film->remove_content (i);
513         }
514
515         check_selection ();
516         return false;
517 }
518
519 void
520 ContentPanel::timeline_clicked ()
521 {
522         if (!_film) {
523                 return;
524         }
525
526         if (_timeline_dialog) {
527                 _timeline_dialog->Destroy ();
528                 _timeline_dialog = 0;
529         }
530
531         _timeline_dialog = new TimelineDialog (this, _film);
532         _timeline_dialog->set_selection (selected());
533         _timeline_dialog->Show ();
534 }
535
536 void
537 ContentPanel::right_click (wxListEvent& ev)
538 {
539         _menu->popup (_film, selected (), TimelineContentViewList (), ev.GetPoint ());
540 }
541
542 /** Set up broad sensitivity based on the type of content that is selected */
543 void
544 ContentPanel::setup_sensitivity ()
545 {
546         _add_file->Enable (_generally_sensitive);
547         _add_folder->Enable (_generally_sensitive);
548         _add_dcp->Enable (_generally_sensitive);
549
550         ContentList selection = selected ();
551         ContentList video_selection = selected_video ();
552         ContentList audio_selection = selected_audio ();
553
554         _remove->Enable   (_generally_sensitive && !selection.empty());
555         _earlier->Enable  (_generally_sensitive && selection.size() == 1);
556         _later->Enable    (_generally_sensitive && selection.size() == 1);
557         _timeline->Enable (_generally_sensitive && _film && !_film->content().empty());
558
559         if (_video_panel) {
560                 _video_panel->Enable (_generally_sensitive && video_selection.size() > 0);
561         }
562         if (_audio_panel) {
563                 _audio_panel->Enable (_generally_sensitive && audio_selection.size() > 0);
564         }
565         for (int i = 0; i < TEXT_COUNT; ++i) {
566                 if (_text_panel[i]) {
567                         _text_panel[i]->Enable (_generally_sensitive && selection.size() == 1 && !selection.front()->text.empty());
568                 }
569         }
570         _timing_panel->Enable   (_generally_sensitive);
571 }
572
573 void
574 ContentPanel::set_film (shared_ptr<Film> film)
575 {
576         if (_audio_panel) {
577                 _audio_panel->set_film (film);
578         }
579
580         _film = film;
581
582         film_changed (Film::CONTENT);
583         film_changed (Film::AUDIO_CHANNELS);
584         check_selection ();
585         setup_sensitivity ();
586 }
587
588 void
589 ContentPanel::set_general_sensitivity (bool s)
590 {
591         _generally_sensitive = s;
592         setup_sensitivity ();
593 }
594
595 void
596 ContentPanel::earlier_clicked ()
597 {
598         ContentList sel = selected ();
599         if (sel.size() == 1) {
600                 _film->move_content_earlier (sel.front ());
601                 check_selection ();
602         }
603 }
604
605 void
606 ContentPanel::later_clicked ()
607 {
608         ContentList sel = selected ();
609         if (sel.size() == 1) {
610                 _film->move_content_later (sel.front ());
611                 check_selection ();
612         }
613 }
614
615 void
616 ContentPanel::set_selection (weak_ptr<Content> wc)
617 {
618         ContentList content = _film->content ();
619         for (size_t i = 0; i < content.size(); ++i) {
620                 if (content[i] == wc.lock ()) {
621                         _content->SetItemState (i, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
622                 } else {
623                         _content->SetItemState (i, 0, wxLIST_STATE_SELECTED);
624                 }
625         }
626 }
627
628 void
629 ContentPanel::set_selection (ContentList cl)
630 {
631         ContentList content = _film->content ();
632         for (size_t i = 0; i < content.size(); ++i) {
633                 if (find(cl.begin(), cl.end(), content[i]) != cl.end()) {
634                         _content->SetItemState (i, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
635                 } else {
636                         _content->SetItemState (i, 0, wxLIST_STATE_SELECTED);
637                 }
638         }
639 }
640
641 void
642 ContentPanel::film_content_changed (int property)
643 {
644         if (
645                 property == ContentProperty::PATH ||
646                 property == DCPContentProperty::NEEDS_ASSETS ||
647                 property == DCPContentProperty::NEEDS_KDM ||
648                 property == DCPContentProperty::NAME
649                 ) {
650
651                 setup ();
652         }
653
654         BOOST_FOREACH (ContentSubPanel* i, panels()) {
655                 i->film_content_changed (property);
656         }
657 }
658
659 void
660 ContentPanel::setup ()
661 {
662         ContentList content = _film->content ();
663
664         Content* selected_content = 0;
665         int const s = _content->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
666         if (s != -1) {
667                 wxListItem item;
668                 item.SetId (s);
669                 item.SetMask (wxLIST_MASK_DATA);
670                 _content->GetItem (item);
671                 selected_content = reinterpret_cast<Content*> (item.GetData ());
672         }
673
674         _content->DeleteAllItems ();
675
676         BOOST_FOREACH (shared_ptr<Content> i, content) {
677                 int const t = _content->GetItemCount ();
678                 bool const valid = i->paths_valid ();
679
680                 /* Temporary debugging for Igor */
681                 BOOST_FOREACH (boost::filesystem::path j, i->paths()) {
682                         LOG_GENERAL ("Check %1 %2 answer %3", j.string(), boost::filesystem::exists(j) ? "yes" : "no", valid ? "yes" : "no");
683                 }
684
685                 shared_ptr<DCPContent> dcp = dynamic_pointer_cast<DCPContent> (i);
686                 bool const needs_kdm = dcp && dcp->needs_kdm ();
687                 bool const needs_assets = dcp && dcp->needs_assets ();
688
689                 wxString s = std_to_wx (i->summary ());
690
691                 if (!valid) {
692                         s = _("MISSING: ") + s;
693                 }
694
695                 if (needs_kdm) {
696                         s = _("NEEDS KDM: ") + s;
697                 }
698
699                 if (needs_assets) {
700                         s = _("NEEDS OV: ") + s;
701                 }
702
703                 wxListItem item;
704                 item.SetId (t);
705                 item.SetText (s);
706                 item.SetData (i.get ());
707                 _content->InsertItem (item);
708
709                 if (i.get() == selected_content) {
710                         _content->SetItemState (t, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
711                 }
712
713                 if (!valid || needs_kdm || needs_assets) {
714                         _content->SetItemTextColour (t, *wxRED);
715                 }
716         }
717
718         if (!selected_content && !content.empty ()) {
719                 /* Select the item of content if none was selected before */
720                 _content->SetItemState (0, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
721         }
722
723         setup_sensitivity ();
724 }
725
726 void
727 ContentPanel::files_dropped (wxDropFilesEvent& event)
728 {
729         if (!_film) {
730                 return;
731         }
732
733         wxString* paths = event.GetFiles ();
734         list<boost::filesystem::path> path_list;
735         for (int i = 0; i < event.GetNumberOfFiles(); i++) {
736                 path_list.push_back (wx_to_std (paths[i]));
737         }
738
739         add_files (path_list);
740 }
741
742 void
743 ContentPanel::add_files (list<boost::filesystem::path> paths)
744 {
745         /* It has been reported that the paths returned from e.g. wxFileDialog are not always sorted;
746            I can't reproduce that, but sort them anyway.  Don't use ImageFilenameSorter as a normal
747            alphabetical sort is expected here.
748         */
749
750         paths.sort (CaseInsensitiveSorter ());
751
752         /* XXX: check for lots of files here and do something */
753
754         try {
755                 BOOST_FOREACH (boost::filesystem::path i, paths) {
756                         BOOST_FOREACH (shared_ptr<Content> j, content_factory (_film, i)) {
757                                 _film->examine_and_add_content (j);
758                         }
759                 }
760         } catch (exception& e) {
761                 error_dialog (_parent, e.what());
762         }
763 }
764
765 list<ContentSubPanel*>
766 ContentPanel::panels () const
767 {
768         list<ContentSubPanel*> p;
769         if (_video_panel) {
770                 p.push_back (_video_panel);
771         }
772         if (_audio_panel) {
773                 p.push_back (_audio_panel);
774         }
775         for (int i = 0; i < TEXT_COUNT; ++i) {
776                 if (_text_panel[i]) {
777                         p.push_back (_text_panel[i]);
778                 }
779         }
780         p.push_back (_timing_panel);
781         return p;
782 }