Apply 9d49a592519fc7f0141e2a1a9fee0043776952ac; fix drag-and-drop and sort image...
[dcpomatic.git] / src / wx / content_panel.cc
1 /*
2     Copyright (C) 2012-2015 Carl Hetherington <cth@carlh.net>
3
4     This program is free software; you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation; either version 2 of the License, or
7     (at your option) any later version.
8
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU General Public License
15     along with this program; if not, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
18 */
19
20 #include "lib/audio_content.h"
21 #include "lib/subtitle_content.h"
22 #include "lib/video_content.h"
23 #include "lib/ffmpeg_content.h"
24 #include "lib/content_factory.h"
25 #include "lib/image_content.h"
26 #include "lib/dcp_content.h"
27 #include "lib/playlist.h"
28 #include "content_panel.h"
29 #include "wx_util.h"
30 #include "video_panel.h"
31 #include "audio_panel.h"
32 #include "subtitle_panel.h"
33 #include "timing_panel.h"
34 #include "timeline_dialog.h"
35 #include "image_sequence_dialog.h"
36 #include <wx/wx.h>
37 #include <wx/notebook.h>
38 #include <wx/listctrl.h>
39 #include <boost/filesystem.hpp>
40
41 #include "lib/image_filename_sorter.cc"
42
43 using std::list;
44 using std::string;
45 using std::cout;
46 using boost::shared_ptr;
47 using boost::weak_ptr;
48 using boost::dynamic_pointer_cast;
49
50 ContentPanel::ContentPanel (wxNotebook* n, boost::shared_ptr<Film> f)
51         : _timeline_dialog (0)
52         , _film (f)
53         , _generally_sensitive (true)
54 {
55         _panel = new wxPanel (n);
56         _sizer = new wxBoxSizer (wxVERTICAL);
57         _panel->SetSizer (_sizer);
58
59         _menu = new ContentMenu (_panel);
60
61         {
62                 wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
63                 
64                 _content = new wxListCtrl (_panel, wxID_ANY, wxDefaultPosition, wxSize (320, 160), wxLC_REPORT | wxLC_NO_HEADER);
65                 _content->DragAcceptFiles (true);
66                 s->Add (_content, 1, wxEXPAND | wxTOP | wxBOTTOM, 6);
67
68                 _content->InsertColumn (0, wxT(""));
69                 _content->SetColumnWidth (0, 512);
70
71                 wxBoxSizer* b = new wxBoxSizer (wxVERTICAL);
72                 
73                 _add_file = new wxButton (_panel, wxID_ANY, _("Add file(s)..."));
74                 _add_file->SetToolTip (_("Add video, image or sound files to the film."));
75                 b->Add (_add_file, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
76                 
77                 _add_folder = new wxButton (_panel, wxID_ANY, _("Add folder..."));
78                 _add_folder->SetToolTip (_("Add a folder of image files (which will be used as a moving image sequence) or a DCP."));
79                 b->Add (_add_folder, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
80                 
81                 _remove = new wxButton (_panel, wxID_ANY, _("Remove"));
82                 _remove->SetToolTip (_("Remove the selected piece of content from the film."));
83                 b->Add (_remove, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
84                 
85                 _earlier = new wxButton (_panel, wxID_ANY, _("Up"));
86                 _earlier->SetToolTip (_("Move the selected piece of content earlier in the film."));
87                 b->Add (_earlier, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
88                 
89                 _later = new wxButton (_panel, wxID_ANY, _("Down"));
90                 _later->SetToolTip (_("Move the selected piece of content later in the film."));
91                 b->Add (_later, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
92                 
93                 _timeline = new wxButton (_panel, wxID_ANY, _("Timeline..."));
94                 _timeline->SetToolTip (_("Open the timeline for the film."));
95                 b->Add (_timeline, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
96
97                 s->Add (b, 0, wxALL, 4);
98
99                 _sizer->Add (s, 0, wxEXPAND | wxALL, 6);
100         }
101
102         _notebook = new wxNotebook (_panel, wxID_ANY);
103         _sizer->Add (_notebook, 1, wxEXPAND | wxTOP, 6);
104
105         _video_panel = new VideoPanel (this);
106         _panels.push_back (_video_panel);
107         _audio_panel = new AudioPanel (this);
108         _panels.push_back (_audio_panel);
109         _subtitle_panel = new SubtitlePanel (this);
110         _panels.push_back (_subtitle_panel);
111         _timing_panel = new TimingPanel (this);
112         _panels.push_back (_timing_panel);
113
114         _content->Bind (wxEVT_COMMAND_LIST_ITEM_SELECTED, boost::bind (&ContentPanel::selection_changed, this));
115         _content->Bind (wxEVT_COMMAND_LIST_ITEM_DESELECTED, boost::bind (&ContentPanel::selection_changed, this));
116         _content->Bind (wxEVT_COMMAND_LIST_ITEM_RIGHT_CLICK, boost::bind (&ContentPanel::right_click, this, _1));
117         _content->Bind (wxEVT_DROP_FILES, boost::bind (&ContentPanel::files_dropped, this, _1));
118         _add_file->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&ContentPanel::add_file_clicked, this));
119         _add_folder->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&ContentPanel::add_folder_clicked, this));
120         _remove->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&ContentPanel::remove_clicked, this));
121         _earlier->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&ContentPanel::earlier_clicked, this));
122         _later->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&ContentPanel::later_clicked, this));
123         _timeline->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&ContentPanel::timeline_clicked, this));
124 }
125
126 ContentList
127 ContentPanel::selected ()
128 {
129         ContentList sel;
130         long int s = -1;
131         while (true) {
132                 s = _content->GetNextItem (s, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
133                 if (s == -1) {
134                         break;
135                 }
136
137                 if (s < int (_film->content().size ())) {
138                         sel.push_back (_film->content()[s]);
139                 }
140         }
141
142         return sel;
143 }
144
145 VideoContentList
146 ContentPanel::selected_video ()
147 {
148         ContentList c = selected ();
149         VideoContentList vc;
150         
151         for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
152                 shared_ptr<VideoContent> t = dynamic_pointer_cast<VideoContent> (*i);
153                 if (t) {
154                         vc.push_back (t);
155                 }
156         }
157
158         return vc;
159 }
160
161 AudioContentList
162 ContentPanel::selected_audio ()
163 {
164         ContentList c = selected ();
165         AudioContentList ac;
166         
167         for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
168                 shared_ptr<AudioContent> t = dynamic_pointer_cast<AudioContent> (*i);
169                 if (t) {
170                         ac.push_back (t);
171                 }
172         }
173
174         return ac;
175 }
176
177 SubtitleContentList
178 ContentPanel::selected_subtitle ()
179 {
180         ContentList c = selected ();
181         SubtitleContentList sc;
182         
183         for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
184                 shared_ptr<SubtitleContent> t = dynamic_pointer_cast<SubtitleContent> (*i);
185                 if (t) {
186                         sc.push_back (t);
187                 }
188         }
189
190         return sc;
191 }
192
193 FFmpegContentList
194 ContentPanel::selected_ffmpeg ()
195 {
196         ContentList c = selected ();
197         FFmpegContentList sc;
198         
199         for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
200                 shared_ptr<FFmpegContent> t = dynamic_pointer_cast<FFmpegContent> (*i);
201                 if (t) {
202                         sc.push_back (t);
203                 }
204         }
205
206         return sc;
207 }
208
209 void
210 ContentPanel::film_changed (Film::Property p)
211 {
212         switch (p) {
213         case Film::CONTENT:
214                 setup ();
215                 break;
216         default:
217                 break;
218         }
219
220         for (list<ContentSubPanel*>::iterator i = _panels.begin(); i != _panels.end(); ++i) {
221                 (*i)->film_changed (p);
222         }
223 }       
224
225 void
226 ContentPanel::selection_changed ()
227 {
228         setup_sensitivity ();
229
230         for (list<ContentSubPanel*>::iterator i = _panels.begin(); i != _panels.end(); ++i) {
231                 (*i)->content_selection_changed ();
232         }
233 }
234
235 void
236 ContentPanel::add_file_clicked ()
237 {
238         /* The wxFD_CHANGE_DIR here prevents a `could not set working directory' error 123 on Windows when using
239            non-Latin filenames or paths.
240         */
241         wxFileDialog* d = new wxFileDialog (_panel, _("Choose a file or files"), wxT (""), wxT (""), wxT ("*.*"), wxFD_MULTIPLE | wxFD_CHANGE_DIR);
242         int const r = d->ShowModal ();
243
244         if (r != wxID_OK) {
245                 d->Destroy ();
246                 return;
247         }
248
249         wxArrayString paths;
250         d->GetPaths (paths);
251         list<boost::filesystem::path> path_list;
252         for (unsigned int i = 0; i < paths.GetCount(); ++i) {
253                 path_list.push_back (wx_to_std (paths[i]));
254         }
255         add_files (path_list);
256
257         d->Destroy ();
258 }
259
260 void
261 ContentPanel::add_folder_clicked ()
262 {
263         wxDirDialog* d = new wxDirDialog (_panel, _("Choose a folder"), wxT (""), wxDD_DIR_MUST_EXIST);
264         int r = d->ShowModal ();
265         boost::filesystem::path const path (wx_to_std (d->GetPath ()));
266         d->Destroy ();
267         
268         if (r != wxID_OK) {
269                 return;
270         }
271
272         /* Guess if this is a DCP or a set of images: read the first ten filenames and if they
273            are all valid image files we assume it is a set of images.
274         */
275
276         bool is_dcp = false;
277         int read = 0;
278         for (boost::filesystem::directory_iterator i(path); i != boost::filesystem::directory_iterator() && read < 10; ++i, ++read) {
279                 if (!boost::filesystem::is_regular_file (i->path()) || !valid_image_file (i->path())) {
280                         is_dcp = true;
281                 }
282         }
283
284         if (is_dcp) {
285                 try {
286                         shared_ptr<DCPContent> content (new DCPContent (_film, path));
287                         _film->examine_and_add_content (content);
288                 } catch (...) {
289                         error_dialog (_panel, _("Could not find a DCP in that folder."));
290                 }
291         } else {
292                 
293                 ImageSequenceDialog* e = new ImageSequenceDialog (_panel);
294                 r = e->ShowModal ();
295                 float const frame_rate = e->frame_rate ();
296                 e->Destroy ();
297
298                 if (r != wxID_OK) {
299                         return;
300                 }
301
302                 shared_ptr<Content> content;
303                 
304                 try {
305                         shared_ptr<ImageContent> content (new ImageContent (_film, path));
306                         content->set_video_frame_rate (frame_rate);
307                         _film->examine_and_add_content (content);
308                 } catch (...) {
309                         error_dialog (_panel, _("Could not find any images in that folder"));
310                         return;
311                 }
312         }
313 }
314
315 void
316 ContentPanel::remove_clicked ()
317 {
318         ContentList c = selected ();
319         if (c.size() == 1) {
320                 _film->remove_content (c.front ());
321         }
322
323         selection_changed ();
324 }
325
326 void
327 ContentPanel::timeline_clicked ()
328 {
329         if (_timeline_dialog) {
330                 _timeline_dialog->Destroy ();
331                 _timeline_dialog = 0;
332         }
333         
334         _timeline_dialog = new TimelineDialog (this, _film);
335         _timeline_dialog->Show ();
336 }
337
338 void
339 ContentPanel::right_click (wxListEvent& ev)
340 {
341         _menu->popup (_film, selected (), TimelineContentViewList (), ev.GetPoint ());
342 }
343
344 /** Set up broad sensitivity based on the type of content that is selected */
345 void
346 ContentPanel::setup_sensitivity ()
347 {
348         _add_file->Enable (_generally_sensitive);
349         _add_folder->Enable (_generally_sensitive);
350
351         ContentList selection = selected ();
352         VideoContentList video_selection = selected_video ();
353         AudioContentList audio_selection = selected_audio ();
354
355         _remove->Enable   (selection.size() == 1 && _generally_sensitive);
356         _earlier->Enable  (selection.size() == 1 && _generally_sensitive);
357         _later->Enable    (selection.size() == 1 && _generally_sensitive);
358         _timeline->Enable (!_film->content().empty() && _generally_sensitive);
359
360         _video_panel->Enable    (video_selection.size() > 0 && _generally_sensitive);
361         _audio_panel->Enable    (audio_selection.size() > 0 && _generally_sensitive);
362         _subtitle_panel->Enable (selection.size() == 1 && dynamic_pointer_cast<SubtitleContent> (selection.front()) && _generally_sensitive);
363         _timing_panel->Enable   (selection.size() == 1 && _generally_sensitive);
364 }
365
366 void
367 ContentPanel::set_film (shared_ptr<Film> f)
368 {
369         _film = f;
370
371         film_changed (Film::CONTENT);
372         film_changed (Film::AUDIO_CHANNELS);
373         selection_changed ();
374 }
375
376 void
377 ContentPanel::set_general_sensitivity (bool s)
378 {
379         _generally_sensitive = s;
380
381         _content->Enable (s);
382         _add_file->Enable (s);
383         _add_folder->Enable (s);
384         _remove->Enable (s);
385         _earlier->Enable (s);
386         _later->Enable (s);
387         _timeline->Enable (s);
388
389         /* Set the panels in the content notebook */
390         for (list<ContentSubPanel*>::iterator i = _panels.begin(); i != _panels.end(); ++i) {
391                 (*i)->Enable (s);
392         }
393 }
394
395 void
396 ContentPanel::earlier_clicked ()
397 {
398         ContentList sel = selected ();
399         if (sel.size() == 1) {
400                 _film->move_content_earlier (sel.front ());
401                 selection_changed ();
402         }
403 }
404
405 void
406 ContentPanel::later_clicked ()
407 {
408         ContentList sel = selected ();
409         if (sel.size() == 1) {
410                 _film->move_content_later (sel.front ());
411                 selection_changed ();
412         }
413 }
414
415 void
416 ContentPanel::set_selection (weak_ptr<Content> wc)
417 {
418         ContentList content = _film->content ();
419         for (size_t i = 0; i < content.size(); ++i) {
420                 if (content[i] == wc.lock ()) {
421                         _content->SetItemState (i, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
422                 } else {
423                         _content->SetItemState (i, 0, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED);
424                 }
425         }
426 }
427
428 void
429 ContentPanel::film_content_changed (int property)
430 {
431         if (property == ContentProperty::PATH || property == ContentProperty::POSITION || property == DCPContentProperty::CAN_BE_PLAYED) {
432                 setup ();
433         }
434                 
435         for (list<ContentSubPanel*>::iterator i = _panels.begin(); i != _panels.end(); ++i) {
436                 (*i)->film_content_changed (property);
437         }
438 }
439
440 void
441 ContentPanel::setup ()
442 {
443         string selected_summary;
444         int const s = _content->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
445         if (s != -1) {
446                 selected_summary = wx_to_std (_content->GetItemText (s));
447         }
448         
449         _content->DeleteAllItems ();
450
451         ContentList content = _film->content ();
452         sort (content.begin(), content.end(), ContentSorter ());
453
454         for (ContentList::iterator i = content.begin(); i != content.end(); ++i) {
455                 int const t = _content->GetItemCount ();
456                 bool const valid = (*i)->paths_valid ();
457                 shared_ptr<DCPContent> dcp = dynamic_pointer_cast<DCPContent> (*i);
458                 bool const needs_kdm = dcp && !dcp->can_be_played ();
459
460                 string s = (*i)->summary ();
461                 
462                 if (!valid) {
463                         s = _("MISSING: ") + s;
464                 }
465
466                 if (needs_kdm) {
467                         s = _("NEEDS KDM: ") + s;
468                 }
469
470                 _content->InsertItem (t, std_to_wx (s));
471
472                 if ((*i)->summary() == selected_summary) {
473                         _content->SetItemState (t, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
474                 }
475
476                 if (!valid || needs_kdm) {
477                         _content->SetItemTextColour (t, *wxRED);
478                 }
479         }
480
481         if (selected_summary.empty () && !content.empty ()) {
482                 /* Select the item of content if none was selected before */
483                 _content->SetItemState (0, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
484         }
485 }
486
487 void
488 ContentPanel::files_dropped (wxDropFilesEvent& event)
489 {
490         cout << "SHIT!\n";
491         
492         if (!_film) {
493                 return;
494         }
495         
496         wxString* paths = event.GetFiles ();
497         list<boost::filesystem::path> path_list;
498         for (int i = 0; i < event.GetNumberOfFiles(); i++) {
499                 path_list.push_back (wx_to_std (paths[i]));
500         }
501
502         add_files (path_list);
503 }
504
505 void
506 ContentPanel::add_files (list<boost::filesystem::path> paths)
507 {
508         /* It has been reported that the paths returned from e.g. wxFileDialog are not always sorted;
509            I can't reproduce that, but sort them anyway.
510         */
511         
512         paths.sort (ImageFilenameSorter ());
513
514         /* XXX: check for lots of files here and do something */
515
516         for (list<boost::filesystem::path>::const_iterator i = paths.begin(); i != paths.end(); ++i) {
517                 shared_ptr<Content> c = content_factory (_film, *i);
518                 shared_ptr<ImageContent> ic = dynamic_pointer_cast<ImageContent> (c);
519                 if (ic) {
520                         ic->set_video_frame_rate (24);
521                 }
522                 _film->examine_and_add_content (c);
523         }
524 }