ce9c66a3ed437bb9fc07b0b55019861e52f9280e
[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 "caption_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/caption_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/text_caption_file_content.h"
43 #include "lib/text_caption_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         : _timeline_dialog (0)
66         , _parent (n)
67         , _last_selected_tab (0)
68         , _film (film)
69         , _film_viewer (viewer)
70         , _generally_sensitive (true)
71 {
72         _panel = new wxPanel (n);
73         _sizer = new wxBoxSizer (wxVERTICAL);
74         _panel->SetSizer (_sizer);
75
76         _menu = new ContentMenu (_panel);
77
78         {
79                 wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
80
81                 _content = new wxListCtrl (_panel, wxID_ANY, wxDefaultPosition, wxSize (320, 160), wxLC_REPORT | wxLC_NO_HEADER);
82                 _content->DragAcceptFiles (true);
83                 s->Add (_content, 1, wxEXPAND | wxTOP | wxBOTTOM, 6);
84
85                 _content->InsertColumn (0, wxT(""));
86                 _content->SetColumnWidth (0, 512);
87
88                 wxBoxSizer* b = new wxBoxSizer (wxVERTICAL);
89
90                 _add_file = new wxButton (_panel, wxID_ANY, _("Add file(s)..."));
91                 _add_file->SetToolTip (_("Add video, image, sound or subtitle files to the film."));
92                 b->Add (_add_file, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
93
94                 _add_folder = new wxButton (_panel, wxID_ANY, _("Add folder..."));
95                 _add_folder->SetToolTip (_("Add a folder of image files (which will be used as a moving image sequence) or a folder of sound files."));
96                 b->Add (_add_folder, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
97
98                 _add_dcp = new wxButton (_panel, wxID_ANY, _("Add DCP..."));
99                 _add_dcp->SetToolTip (_("Add a DCP."));
100                 b->Add (_add_dcp, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
101
102                 _remove = new wxButton (_panel, wxID_ANY, _("Remove"));
103                 _remove->SetToolTip (_("Remove the selected piece of content from the film."));
104                 b->Add (_remove, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
105
106                 _earlier = new wxButton (_panel, wxID_ANY, _("Earlier"));
107                 _earlier->SetToolTip (_("Move the selected piece of content earlier in the film."));
108                 b->Add (_earlier, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
109
110                 _later = new wxButton (_panel, wxID_ANY, _("Later"));
111                 _later->SetToolTip (_("Move the selected piece of content later in the film."));
112                 b->Add (_later, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
113
114                 _timeline = new wxButton (_panel, wxID_ANY, _("Timeline..."));
115                 _timeline->SetToolTip (_("Open the timeline for the film."));
116                 b->Add (_timeline, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
117
118                 s->Add (b, 0, wxALL, 4);
119
120                 _sizer->Add (s, 0, wxEXPAND | wxALL, 6);
121         }
122
123         _notebook = new wxNotebook (_panel, wxID_ANY);
124         _sizer->Add (_notebook, 1, wxEXPAND | wxTOP, 6);
125
126         _video_panel = new VideoPanel (this);
127         _panels.push_back (_video_panel);
128         _audio_panel = new AudioPanel (this);
129         _panels.push_back (_audio_panel);
130         for (int i = 0; i < CAPTION_COUNT; ++i) {
131                 _caption_panel[i] = new CaptionPanel (this, static_cast<CaptionType>(i));
132                 _panels.push_back (_caption_panel[i]);
133         }
134         _timing_panel = new TimingPanel (this, _film_viewer);
135         _panels.push_back (_timing_panel);
136
137         _content->Bind (wxEVT_LIST_ITEM_SELECTED, boost::bind (&ContentPanel::selection_changed, this));
138         _content->Bind (wxEVT_LIST_ITEM_DESELECTED, boost::bind (&ContentPanel::selection_changed, this));
139         _content->Bind (wxEVT_LIST_ITEM_RIGHT_CLICK, boost::bind (&ContentPanel::right_click, this, _1));
140         _content->Bind (wxEVT_DROP_FILES, boost::bind (&ContentPanel::files_dropped, this, _1));
141         _add_file->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::add_file_clicked, this));
142         _add_folder->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::add_folder_clicked, this));
143         _add_dcp->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::add_dcp_clicked, this));
144         _remove->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::remove_clicked, this, false));
145         _earlier->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::earlier_clicked, this));
146         _later->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::later_clicked, this));
147         _timeline->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::timeline_clicked, this));
148 }
149
150 ContentList
151 ContentPanel::selected ()
152 {
153         ContentList sel;
154         long int s = -1;
155         while (true) {
156                 s = _content->GetNextItem (s, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
157                 if (s == -1) {
158                         break;
159                 }
160
161                 if (s < int (_film->content().size ())) {
162                         sel.push_back (_film->content()[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_caption ()
199 {
200         ContentList sc;
201
202         BOOST_FOREACH (shared_ptr<Content> i, selected ()) {
203                 if (!i->caption.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::selection_changed ()
245 {
246         if (_last_selected == selected()) {
247                 /* This was triggered by a re-build of the view but the selection
248                    did not really change.
249                 */
250                 return;
251         }
252
253         _last_selected = selected ();
254
255         setup_sensitivity ();
256
257         BOOST_FOREACH (ContentSubPanel* i, _panels) {
258                 i->content_selection_changed ();
259         }
260
261         optional<DCPTime> go_to;
262         BOOST_FOREACH (shared_ptr<Content> i, selected()) {
263                 DCPTime p;
264                 p = i->position();
265                 if (dynamic_pointer_cast<TextCaptionFileContent>(i) && i->paths_valid()) {
266                         /* Rather special case; if we select a text subtitle file jump to its
267                            first subtitle.
268                         */
269                         TextCaptionFile ts (dynamic_pointer_cast<TextCaptionFileContent>(i));
270                         if (ts.first()) {
271                                 p += DCPTime(ts.first().get(), _film->active_frame_rate_change(i->position()));
272                         }
273                 }
274                 if (!go_to || p < go_to.get()) {
275                         go_to = p;
276                 }
277         }
278
279         if (go_to && Config::instance()->jump_to_selected ()) {
280                 _film_viewer->set_position (go_to.get().ceil(_film->video_frame_rate()));
281         }
282
283         if (_timeline_dialog) {
284                 _timeline_dialog->set_selection (selected());
285         }
286
287         /* Make required tabs visible */
288
289         if (_notebook->GetPageCount() > 1) {
290                 /* There's more than one tab in the notebook so the current selection could be meaningful
291                    to the user; store it so that we can try to restore it later.
292                 */
293                 _last_selected_tab = 0;
294                 if (_notebook->GetSelection() != wxNOT_FOUND) {
295                         _last_selected_tab = _notebook->GetPage(_notebook->GetSelection());
296                 }
297         }
298
299         bool have_video = false;
300         bool have_audio = false;
301         bool have_caption[CAPTION_COUNT] = { false, false };
302         BOOST_FOREACH (shared_ptr<Content> i, selected()) {
303                 if (i->video) {
304                         have_video = true;
305                 }
306                 if (i->audio) {
307                         have_audio = true;
308                 }
309                 BOOST_FOREACH (shared_ptr<CaptionContent> j, i->caption) {
310                         have_caption[j->original_type()] = true;
311                 }
312         }
313
314         bool video_panel = false;
315         bool audio_panel = false;
316         bool caption_panel[CAPTION_COUNT] = { false, false };
317         for (size_t i = 0; i < _notebook->GetPageCount(); ++i) {
318                 if (_notebook->GetPage(i) == _video_panel) {
319                         video_panel = true;
320                 } else if (_notebook->GetPage(i) == _audio_panel) {
321                         audio_panel = true;
322                 }
323                 for (int j = 0; j < CAPTION_COUNT; ++j) {
324                         if (_notebook->GetPage(i) == _caption_panel[j]) {
325                                 caption_panel[j] = true;
326                         }
327                 }
328         }
329
330         int off = 0;
331
332         if (have_video != video_panel) {
333                 if (video_panel) {
334                         _notebook->RemovePage (off);
335                 }
336                 if (have_video) {
337                         _notebook->InsertPage (off, _video_panel, _video_panel->name());
338                 }
339         }
340
341         if (have_video) {
342                 ++off;
343         }
344
345         if (have_audio != audio_panel) {
346                 if (audio_panel) {
347                         _notebook->RemovePage (off);
348                 }
349                 if (have_audio) {
350                         _notebook->InsertPage (off, _audio_panel, _audio_panel->name());
351                 }
352         }
353
354         if (have_audio) {
355                 ++off;
356         }
357
358         for (int i = 0; i < CAPTION_COUNT; ++i) {
359                 if (have_caption[i] != caption_panel[i]) {
360                         if (caption_panel[i]) {
361                                 _notebook->RemovePage (off);
362                         }
363                         if (have_caption[i]) {
364                                 _notebook->InsertPage (off, _caption_panel[i], _caption_panel[i]->name());
365                         }
366                 }
367                 if (have_caption[i]) {
368                         ++off;
369                 }
370         }
371
372         /* Set up the tab selection */
373
374         bool done = false;
375         for (size_t i = 0; i < _notebook->GetPageCount(); ++i) {
376                 if (_notebook->GetPage(i) == _last_selected_tab) {
377                         _notebook->SetSelection (i);
378                         done = true;
379                 }
380         }
381
382         if (!done) {
383                 _notebook->SetSelection (0);
384         }
385
386         SelectionChanged ();
387 }
388
389 void
390 ContentPanel::add_file_clicked ()
391 {
392         /* This method is also called when Ctrl-A is pressed, so check that our notebook page
393            is visible.
394         */
395         if (_parent->GetCurrentPage() != _panel || !_film) {
396                 return;
397         }
398
399         /* The wxFD_CHANGE_DIR here prevents a `could not set working directory' error 123 on Windows when using
400            non-Latin filenames or paths.
401         */
402         wxFileDialog* d = new wxFileDialog (
403                 _panel,
404                 _("Choose a file or files"),
405                 wxT (""),
406                 wxT (""),
407                 wxT ("All files|*.*|Subtitle files|*.srt;*.xml|Audio files|*.wav;*.w64;*.flac;*.aif;*.aiff"),
408                 wxFD_MULTIPLE | wxFD_CHANGE_DIR
409                 );
410
411         int const r = d->ShowModal ();
412
413         if (r != wxID_OK) {
414                 d->Destroy ();
415                 return;
416         }
417
418         wxArrayString paths;
419         d->GetPaths (paths);
420         list<boost::filesystem::path> path_list;
421         for (unsigned int i = 0; i < paths.GetCount(); ++i) {
422                 path_list.push_back (wx_to_std (paths[i]));
423         }
424         add_files (path_list);
425
426         d->Destroy ();
427 }
428
429 void
430 ContentPanel::add_folder_clicked ()
431 {
432         wxDirDialog* d = new wxDirDialog (_panel, _("Choose a folder"), wxT (""), wxDD_DIR_MUST_EXIST);
433         int r = d->ShowModal ();
434         boost::filesystem::path const path (wx_to_std (d->GetPath ()));
435         d->Destroy ();
436
437         if (r != wxID_OK) {
438                 return;
439         }
440
441         list<shared_ptr<Content> > content;
442
443         try {
444                 content = content_factory (_film, path);
445         } catch (exception& e) {
446                 error_dialog (_parent, e.what());
447                 return;
448         }
449
450         if (content.empty ()) {
451                 error_dialog (_parent, _("No content found in this folder."));
452                 return;
453         }
454
455         BOOST_FOREACH (shared_ptr<Content> i, content) {
456                 shared_ptr<ImageContent> ic = dynamic_pointer_cast<ImageContent> (i);
457                 if (ic) {
458                         ImageSequenceDialog* e = new ImageSequenceDialog (_panel);
459                         r = e->ShowModal ();
460                         float const frame_rate = e->frame_rate ();
461                         e->Destroy ();
462
463                         if (r != wxID_OK) {
464                                 return;
465                         }
466
467                         ic->set_video_frame_rate (frame_rate);
468                 }
469
470                 _film->examine_and_add_content (i);
471         }
472 }
473
474 void
475 ContentPanel::add_dcp_clicked ()
476 {
477         wxDirDialog* d = new wxDirDialog (_panel, _("Choose a DCP folder"), wxT (""), wxDD_DIR_MUST_EXIST);
478         int r = d->ShowModal ();
479         boost::filesystem::path const path (wx_to_std (d->GetPath ()));
480         d->Destroy ();
481
482         if (r != wxID_OK) {
483                 return;
484         }
485
486         try {
487                 _film->examine_and_add_content (shared_ptr<Content> (new DCPContent (_film, path)));
488         } catch (exception& e) {
489                 error_dialog (_parent, e.what());
490         }
491 }
492
493 /** @return true if this remove "click" should be ignored */
494 bool
495 ContentPanel::remove_clicked (bool hotkey)
496 {
497         /* If the method was called because Delete was pressed check that our notebook page
498            is visible and that the content list is focussed.
499         */
500         if (hotkey && (_parent->GetCurrentPage() != _panel || !_content->HasFocus())) {
501                 return true;
502         }
503
504         BOOST_FOREACH (shared_ptr<Content> i, selected ()) {
505                 _film->remove_content (i);
506         }
507
508         selection_changed ();
509         return false;
510 }
511
512 void
513 ContentPanel::timeline_clicked ()
514 {
515         if (!_film) {
516                 return;
517         }
518
519         if (_timeline_dialog) {
520                 _timeline_dialog->Destroy ();
521                 _timeline_dialog = 0;
522         }
523
524         _timeline_dialog = new TimelineDialog (this, _film);
525         _timeline_dialog->Show ();
526 }
527
528 void
529 ContentPanel::right_click (wxListEvent& ev)
530 {
531         _menu->popup (_film, selected (), TimelineContentViewList (), ev.GetPoint ());
532 }
533
534 /** Set up broad sensitivity based on the type of content that is selected */
535 void
536 ContentPanel::setup_sensitivity ()
537 {
538         _add_file->Enable (_generally_sensitive);
539         _add_folder->Enable (_generally_sensitive);
540         _add_dcp->Enable (_generally_sensitive);
541
542         ContentList selection = selected ();
543         ContentList video_selection = selected_video ();
544         ContentList audio_selection = selected_audio ();
545
546         _remove->Enable   (_generally_sensitive && !selection.empty());
547         _earlier->Enable  (_generally_sensitive && selection.size() == 1);
548         _later->Enable    (_generally_sensitive && selection.size() == 1);
549         _timeline->Enable (_generally_sensitive && _film && !_film->content().empty());
550
551         _video_panel->Enable    (_generally_sensitive && video_selection.size() > 0);
552         _audio_panel->Enable    (_generally_sensitive && audio_selection.size() > 0);
553         for (int i = 0; i < CAPTION_COUNT; ++i) {
554                 _caption_panel[i]->Enable  (_generally_sensitive && selection.size() == 1 && !selection.front()->caption.empty());
555         }
556         _timing_panel->Enable   (_generally_sensitive);
557 }
558
559 void
560 ContentPanel::set_film (shared_ptr<Film> film)
561 {
562         _audio_panel->set_film (film);
563
564         _film = film;
565
566         film_changed (Film::CONTENT);
567         film_changed (Film::AUDIO_CHANNELS);
568         selection_changed ();
569         setup_sensitivity ();
570 }
571
572 void
573 ContentPanel::set_general_sensitivity (bool s)
574 {
575         _generally_sensitive = s;
576         setup_sensitivity ();
577 }
578
579 void
580 ContentPanel::earlier_clicked ()
581 {
582         ContentList sel = selected ();
583         if (sel.size() == 1) {
584                 _film->move_content_earlier (sel.front ());
585                 selection_changed ();
586         }
587 }
588
589 void
590 ContentPanel::later_clicked ()
591 {
592         ContentList sel = selected ();
593         if (sel.size() == 1) {
594                 _film->move_content_later (sel.front ());
595                 selection_changed ();
596         }
597 }
598
599 void
600 ContentPanel::set_selection (weak_ptr<Content> wc)
601 {
602         ContentList content = _film->content ();
603         for (size_t i = 0; i < content.size(); ++i) {
604                 if (content[i] == wc.lock ()) {
605                         _content->SetItemState (i, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
606                 } else {
607                         _content->SetItemState (i, 0, wxLIST_STATE_SELECTED);
608                 }
609         }
610 }
611
612 void
613 ContentPanel::set_selection (ContentList cl)
614 {
615         ContentList content = _film->content ();
616         for (size_t i = 0; i < content.size(); ++i) {
617                 if (find(cl.begin(), cl.end(), content[i]) != cl.end()) {
618                         _content->SetItemState (i, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
619                 } else {
620                         _content->SetItemState (i, 0, wxLIST_STATE_SELECTED);
621                 }
622         }
623 }
624
625 void
626 ContentPanel::film_content_changed (int property)
627 {
628         if (
629                 property == ContentProperty::PATH ||
630                 property == DCPContentProperty::NEEDS_ASSETS ||
631                 property == DCPContentProperty::NEEDS_KDM ||
632                 property == DCPContentProperty::NAME
633                 ) {
634
635                 setup ();
636         }
637
638         BOOST_FOREACH (ContentSubPanel* i, _panels) {
639                 i->film_content_changed (property);
640         }
641 }
642
643 void
644 ContentPanel::setup ()
645 {
646         ContentList content = _film->content ();
647
648         Content* selected_content = 0;
649         int const s = _content->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
650         if (s != -1) {
651                 wxListItem item;
652                 item.SetId (s);
653                 item.SetMask (wxLIST_MASK_DATA);
654                 _content->GetItem (item);
655                 selected_content = reinterpret_cast<Content*> (item.GetData ());
656         }
657
658         _content->DeleteAllItems ();
659
660         BOOST_FOREACH (shared_ptr<Content> i, content) {
661                 int const t = _content->GetItemCount ();
662                 bool const valid = i->paths_valid ();
663
664                 /* Temporary debugging for Igor */
665                 BOOST_FOREACH (boost::filesystem::path j, i->paths()) {
666                         LOG_GENERAL ("Check %1 %2 answer %3", j.string(), boost::filesystem::exists(j) ? "yes" : "no", valid ? "yes" : "no");
667                 }
668
669                 shared_ptr<DCPContent> dcp = dynamic_pointer_cast<DCPContent> (i);
670                 bool const needs_kdm = dcp && dcp->needs_kdm ();
671                 bool const needs_assets = dcp && dcp->needs_assets ();
672
673                 wxString s = std_to_wx (i->summary ());
674
675                 if (!valid) {
676                         s = _("MISSING: ") + s;
677                 }
678
679                 if (needs_kdm) {
680                         s = _("NEEDS KDM: ") + s;
681                 }
682
683                 if (needs_assets) {
684                         s = _("NEEDS OV: ") + s;
685                 }
686
687                 wxListItem item;
688                 item.SetId (t);
689                 item.SetText (s);
690                 item.SetData (i.get ());
691                 _content->InsertItem (item);
692
693                 if (i.get() == selected_content) {
694                         _content->SetItemState (t, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
695                 }
696
697                 if (!valid || needs_kdm || needs_assets) {
698                         _content->SetItemTextColour (t, *wxRED);
699                 }
700         }
701
702         if (!selected_content && !content.empty ()) {
703                 /* Select the item of content if none was selected before */
704                 _content->SetItemState (0, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
705         }
706
707         setup_sensitivity ();
708 }
709
710 void
711 ContentPanel::files_dropped (wxDropFilesEvent& event)
712 {
713         if (!_film) {
714                 return;
715         }
716
717         wxString* paths = event.GetFiles ();
718         list<boost::filesystem::path> path_list;
719         for (int i = 0; i < event.GetNumberOfFiles(); i++) {
720                 path_list.push_back (wx_to_std (paths[i]));
721         }
722
723         add_files (path_list);
724 }
725
726 void
727 ContentPanel::add_files (list<boost::filesystem::path> paths)
728 {
729         /* It has been reported that the paths returned from e.g. wxFileDialog are not always sorted;
730            I can't reproduce that, but sort them anyway.  Don't use ImageFilenameSorter as a normal
731            alphabetical sort is expected here.
732         */
733
734         paths.sort (CaseInsensitiveSorter ());
735
736         /* XXX: check for lots of files here and do something */
737
738         try {
739                 BOOST_FOREACH (boost::filesystem::path i, paths) {
740                         BOOST_FOREACH (shared_ptr<Content> j, content_factory (_film, i)) {
741                                 _film->examine_and_add_content (j);
742                         }
743                 }
744         } catch (exception& e) {
745                 error_dialog (_parent, e.what());
746         }
747 }