Round seek on jump-to-selected to work around problems with sub-frame seeks and the...
[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 "subtitle_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/subtitle_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_subtitle_content.h"
43 #include "lib/text_subtitle.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::exception;
56 using boost::shared_ptr;
57 using boost::weak_ptr;
58 using boost::dynamic_pointer_cast;
59 using boost::optional;
60
61 #define LOG_GENERAL(...) _film->log()->log (String::compose (__VA_ARGS__), LogEntry::TYPE_GENERAL);
62
63 ContentPanel::ContentPanel (wxNotebook* n, boost::shared_ptr<Film> film, FilmViewer* viewer)
64         : _timeline_dialog (0)
65         , _parent (n)
66         , _film (film)
67         , _film_viewer (viewer)
68         , _generally_sensitive (true)
69 {
70         _panel = new wxPanel (n);
71         _sizer = new wxBoxSizer (wxVERTICAL);
72         _panel->SetSizer (_sizer);
73
74         _menu = new ContentMenu (_panel);
75
76         {
77                 wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
78
79                 _content = new wxListCtrl (_panel, wxID_ANY, wxDefaultPosition, wxSize (320, 160), wxLC_REPORT | wxLC_NO_HEADER);
80                 _content->DragAcceptFiles (true);
81                 s->Add (_content, 1, wxEXPAND | wxTOP | wxBOTTOM, 6);
82
83                 _content->InsertColumn (0, wxT(""));
84                 _content->SetColumnWidth (0, 512);
85
86                 wxBoxSizer* b = new wxBoxSizer (wxVERTICAL);
87
88                 _add_file = new wxButton (_panel, wxID_ANY, _("Add file(s)..."));
89                 _add_file->SetToolTip (_("Add video, image, sound or subtitle files to the film."));
90                 b->Add (_add_file, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
91
92                 _add_folder = new wxButton (_panel, wxID_ANY, _("Add folder..."));
93                 _add_folder->SetToolTip (_("Add a folder of image files (which will be used as a moving image sequence) or a folder of sound files."));
94                 b->Add (_add_folder, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
95
96                 _add_dcp = new wxButton (_panel, wxID_ANY, _("Add DCP..."));
97                 _add_dcp->SetToolTip (_("Add a DCP."));
98                 b->Add (_add_dcp, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
99
100                 _remove = new wxButton (_panel, wxID_ANY, _("Remove"));
101                 _remove->SetToolTip (_("Remove the selected piece of content from the film."));
102                 b->Add (_remove, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
103
104                 _earlier = new wxButton (_panel, wxID_ANY, _("Earlier"));
105                 _earlier->SetToolTip (_("Move the selected piece of content earlier in the film."));
106                 b->Add (_earlier, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
107
108                 _later = new wxButton (_panel, wxID_ANY, _("Later"));
109                 _later->SetToolTip (_("Move the selected piece of content later in the film."));
110                 b->Add (_later, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
111
112                 _timeline = new wxButton (_panel, wxID_ANY, _("Timeline..."));
113                 _timeline->SetToolTip (_("Open the timeline for the film."));
114                 b->Add (_timeline, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
115
116                 s->Add (b, 0, wxALL, 4);
117
118                 _sizer->Add (s, 0, wxEXPAND | wxALL, 6);
119         }
120
121         _notebook = new wxNotebook (_panel, wxID_ANY);
122         _sizer->Add (_notebook, 1, wxEXPAND | wxTOP, 6);
123
124         _video_panel = new VideoPanel (this);
125         _panels.push_back (_video_panel);
126         _audio_panel = new AudioPanel (this);
127         _panels.push_back (_audio_panel);
128         _subtitle_panel = new SubtitlePanel (this);
129         _panels.push_back (_subtitle_panel);
130         _timing_panel = new TimingPanel (this, _film_viewer);
131         _panels.push_back (_timing_panel);
132
133         _content->Bind (wxEVT_LIST_ITEM_SELECTED, boost::bind (&ContentPanel::selection_changed, this));
134         _content->Bind (wxEVT_LIST_ITEM_DESELECTED, boost::bind (&ContentPanel::selection_changed, this));
135         _content->Bind (wxEVT_LIST_ITEM_RIGHT_CLICK, boost::bind (&ContentPanel::right_click, this, _1));
136         _content->Bind (wxEVT_DROP_FILES, boost::bind (&ContentPanel::files_dropped, this, _1));
137         _add_file->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::add_file_clicked, this));
138         _add_folder->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::add_folder_clicked, this));
139         _add_dcp->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::add_dcp_clicked, this));
140         _remove->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::remove_clicked, this, false));
141         _earlier->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::earlier_clicked, this));
142         _later->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::later_clicked, this));
143         _timeline->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::timeline_clicked, this));
144 }
145
146 ContentList
147 ContentPanel::selected ()
148 {
149         ContentList sel;
150         long int s = -1;
151         while (true) {
152                 s = _content->GetNextItem (s, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
153                 if (s == -1) {
154                         break;
155                 }
156
157                 if (s < int (_film->content().size ())) {
158                         sel.push_back (_film->content()[s]);
159                 }
160         }
161
162         return sel;
163 }
164
165 ContentList
166 ContentPanel::selected_video ()
167 {
168         ContentList vc;
169
170         BOOST_FOREACH (shared_ptr<Content> i, selected ()) {
171                 if (i->video) {
172                         vc.push_back (i);
173                 }
174         }
175
176         return vc;
177 }
178
179 ContentList
180 ContentPanel::selected_audio ()
181 {
182         ContentList ac;
183
184         BOOST_FOREACH (shared_ptr<Content> i, selected ()) {
185                 if (i->audio) {
186                         ac.push_back (i);
187                 }
188         }
189
190         return ac;
191 }
192
193 ContentList
194 ContentPanel::selected_subtitle ()
195 {
196         ContentList sc;
197
198         BOOST_FOREACH (shared_ptr<Content> i, selected ()) {
199                 if (i->subtitle) {
200                         sc.push_back (i);
201                 }
202         }
203
204         return sc;
205 }
206
207 FFmpegContentList
208 ContentPanel::selected_ffmpeg ()
209 {
210         FFmpegContentList sc;
211
212         BOOST_FOREACH (shared_ptr<Content> i, selected ()) {
213                 shared_ptr<FFmpegContent> t = dynamic_pointer_cast<FFmpegContent> (i);
214                 if (t) {
215                         sc.push_back (t);
216                 }
217         }
218
219         return sc;
220 }
221
222 void
223 ContentPanel::film_changed (Film::Property p)
224 {
225         switch (p) {
226         case Film::CONTENT:
227         case Film::CONTENT_ORDER:
228                 setup ();
229                 break;
230         default:
231                 break;
232         }
233
234         BOOST_FOREACH (ContentSubPanel* i, _panels) {
235                 i->film_changed (p);
236         }
237 }
238
239 void
240 ContentPanel::selection_changed ()
241 {
242         if (_last_selected == selected()) {
243                 /* This was triggered by a re-build of the view but the selection
244                    did not really change.
245                 */
246                 return;
247         }
248
249         _last_selected = selected ();
250
251         setup_sensitivity ();
252
253         BOOST_FOREACH (ContentSubPanel* i, _panels) {
254                 i->content_selection_changed ();
255         }
256
257         optional<DCPTime> go_to;
258         BOOST_FOREACH (shared_ptr<Content> i, selected ()) {
259                 DCPTime p;
260                 p = i->position();
261                 if (dynamic_pointer_cast<TextSubtitleContent>(i)) {
262                         /* Rather special case; if we select a text subtitle file jump to its
263                            first subtitle.
264                         */
265                         TextSubtitle ts (dynamic_pointer_cast<TextSubtitleContent>(i));
266                         if (ts.first()) {
267                                 p += DCPTime(ts.first().get(), _film->active_frame_rate_change(i->position()));
268                         }
269                 }
270                 if (!go_to || p < go_to.get()) {
271                         go_to = p;
272                 }
273         }
274
275         if (go_to && Config::instance()->jump_to_selected ()) {
276                 _film_viewer->set_position (go_to.get().ceil(_film->video_frame_rate()));
277         }
278
279         if (_timeline_dialog) {
280                 _timeline_dialog->set_selection (selected ());
281         }
282
283         SelectionChanged ();
284 }
285
286 void
287 ContentPanel::add_file_clicked ()
288 {
289         /* This method is also called when Ctrl-A is pressed, so check that our notebook page
290            is visible.
291         */
292         if (_parent->GetCurrentPage() != _panel || !_film) {
293                 return;
294         }
295
296         /* The wxFD_CHANGE_DIR here prevents a `could not set working directory' error 123 on Windows when using
297            non-Latin filenames or paths.
298         */
299         wxFileDialog* d = new wxFileDialog (
300                 _panel,
301                 _("Choose a file or files"),
302                 wxT (""),
303                 wxT (""),
304                 wxT ("All files|*.*|Subtitle files|*.srt;*.xml|Audio files|*.wav;*.w64;*.flac;*.aif;*.aiff"),
305                 wxFD_MULTIPLE | wxFD_CHANGE_DIR
306                 );
307
308         int const r = d->ShowModal ();
309
310         if (r != wxID_OK) {
311                 d->Destroy ();
312                 return;
313         }
314
315         wxArrayString paths;
316         d->GetPaths (paths);
317         list<boost::filesystem::path> path_list;
318         for (unsigned int i = 0; i < paths.GetCount(); ++i) {
319                 path_list.push_back (wx_to_std (paths[i]));
320         }
321         add_files (path_list);
322
323         d->Destroy ();
324 }
325
326 void
327 ContentPanel::add_folder_clicked ()
328 {
329         wxDirDialog* d = new wxDirDialog (_panel, _("Choose a folder"), wxT (""), wxDD_DIR_MUST_EXIST);
330         int r = d->ShowModal ();
331         boost::filesystem::path const path (wx_to_std (d->GetPath ()));
332         d->Destroy ();
333
334         if (r != wxID_OK) {
335                 return;
336         }
337
338         list<shared_ptr<Content> > content;
339
340         try {
341                 content = content_factory (_film, path);
342         } catch (exception& e) {
343                 error_dialog (_parent, e.what());
344                 return;
345         }
346
347         if (content.empty ()) {
348                 error_dialog (_parent, _("No content found in this folder."));
349                 return;
350         }
351
352         BOOST_FOREACH (shared_ptr<Content> i, content) {
353                 shared_ptr<ImageContent> ic = dynamic_pointer_cast<ImageContent> (i);
354                 if (ic) {
355                         ImageSequenceDialog* e = new ImageSequenceDialog (_panel);
356                         r = e->ShowModal ();
357                         float const frame_rate = e->frame_rate ();
358                         e->Destroy ();
359
360                         if (r != wxID_OK) {
361                                 return;
362                         }
363
364                         ic->set_video_frame_rate (frame_rate);
365                 }
366
367                 _film->examine_and_add_content (i);
368         }
369 }
370
371 void
372 ContentPanel::add_dcp_clicked ()
373 {
374         wxDirDialog* d = new wxDirDialog (_panel, _("Choose a DCP folder"), wxT (""), wxDD_DIR_MUST_EXIST);
375         int r = d->ShowModal ();
376         boost::filesystem::path const path (wx_to_std (d->GetPath ()));
377         d->Destroy ();
378
379         if (r != wxID_OK) {
380                 return;
381         }
382
383         try {
384                 _film->examine_and_add_content (shared_ptr<Content> (new DCPContent (_film, path)));
385         } catch (exception& e) {
386                 error_dialog (_parent, e.what());
387         }
388 }
389
390 /** @return true if this remove "click" should be ignored */
391 bool
392 ContentPanel::remove_clicked (bool hotkey)
393 {
394         /* If the method was called because Delete was pressed check that our notebook page
395            is visible and that the content list is focussed.
396         */
397         if (hotkey && (_parent->GetCurrentPage() != _panel || !_content->HasFocus())) {
398                 return true;
399         }
400
401         BOOST_FOREACH (shared_ptr<Content> i, selected ()) {
402                 _film->remove_content (i);
403         }
404
405         selection_changed ();
406         return false;
407 }
408
409 void
410 ContentPanel::timeline_clicked ()
411 {
412         if (_timeline_dialog) {
413                 _timeline_dialog->Destroy ();
414                 _timeline_dialog = 0;
415         }
416
417         _timeline_dialog = new TimelineDialog (this, _film);
418         _timeline_dialog->Show ();
419 }
420
421 void
422 ContentPanel::right_click (wxListEvent& ev)
423 {
424         _menu->popup (_film, selected (), TimelineContentViewList (), ev.GetPoint ());
425 }
426
427 /** Set up broad sensitivity based on the type of content that is selected */
428 void
429 ContentPanel::setup_sensitivity ()
430 {
431         _add_file->Enable (_generally_sensitive);
432         _add_folder->Enable (_generally_sensitive);
433         _add_dcp->Enable (_generally_sensitive);
434
435         ContentList selection = selected ();
436         ContentList video_selection = selected_video ();
437         ContentList audio_selection = selected_audio ();
438
439         _remove->Enable   (_generally_sensitive && !selection.empty());
440         _earlier->Enable  (_generally_sensitive && selection.size() == 1);
441         _later->Enable    (_generally_sensitive && selection.size() == 1);
442         _timeline->Enable (_generally_sensitive && _film && !_film->content().empty());
443
444         _video_panel->Enable    (_generally_sensitive && video_selection.size() > 0);
445         _audio_panel->Enable    (_generally_sensitive && audio_selection.size() > 0);
446         _subtitle_panel->Enable (_generally_sensitive && selection.size() == 1 && selection.front()->subtitle);
447         _timing_panel->Enable   (_generally_sensitive);
448 }
449
450 void
451 ContentPanel::set_film (shared_ptr<Film> film)
452 {
453         _audio_panel->set_film (film);
454
455         _film = film;
456
457         film_changed (Film::CONTENT);
458         film_changed (Film::AUDIO_CHANNELS);
459         selection_changed ();
460         setup_sensitivity ();
461 }
462
463 void
464 ContentPanel::set_general_sensitivity (bool s)
465 {
466         _generally_sensitive = s;
467         setup_sensitivity ();
468 }
469
470 void
471 ContentPanel::earlier_clicked ()
472 {
473         ContentList sel = selected ();
474         if (sel.size() == 1) {
475                 _film->move_content_earlier (sel.front ());
476                 selection_changed ();
477         }
478 }
479
480 void
481 ContentPanel::later_clicked ()
482 {
483         ContentList sel = selected ();
484         if (sel.size() == 1) {
485                 _film->move_content_later (sel.front ());
486                 selection_changed ();
487         }
488 }
489
490 void
491 ContentPanel::set_selection (weak_ptr<Content> wc)
492 {
493         ContentList content = _film->content ();
494         for (size_t i = 0; i < content.size(); ++i) {
495                 if (content[i] == wc.lock ()) {
496                         _content->SetItemState (i, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
497                 } else {
498                         _content->SetItemState (i, 0, wxLIST_STATE_SELECTED);
499                 }
500         }
501 }
502
503 void
504 ContentPanel::set_selection (ContentList cl)
505 {
506         ContentList content = _film->content ();
507         for (size_t i = 0; i < content.size(); ++i) {
508                 if (find(cl.begin(), cl.end(), content[i]) != cl.end()) {
509                         _content->SetItemState (i, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
510                 } else {
511                         _content->SetItemState (i, 0, wxLIST_STATE_SELECTED);
512                 }
513         }
514 }
515
516 void
517 ContentPanel::film_content_changed (int property)
518 {
519         if (
520                 property == ContentProperty::PATH ||
521                 property == DCPContentProperty::NEEDS_ASSETS ||
522                 property == DCPContentProperty::NEEDS_KDM ||
523                 property == DCPContentProperty::NAME
524                 ) {
525
526                 setup ();
527         }
528
529         BOOST_FOREACH (ContentSubPanel* i, _panels) {
530                 i->film_content_changed (property);
531         }
532 }
533
534 void
535 ContentPanel::setup ()
536 {
537         ContentList content = _film->content ();
538
539         Content* selected_content = 0;
540         int const s = _content->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
541         if (s != -1) {
542                 wxListItem item;
543                 item.SetId (s);
544                 item.SetMask (wxLIST_MASK_DATA);
545                 _content->GetItem (item);
546                 selected_content = reinterpret_cast<Content*> (item.GetData ());
547         }
548
549         _content->DeleteAllItems ();
550
551         BOOST_FOREACH (shared_ptr<Content> i, content) {
552                 int const t = _content->GetItemCount ();
553                 bool const valid = i->paths_valid ();
554
555                 /* Temporary debugging for Igor */
556                 BOOST_FOREACH (boost::filesystem::path j, i->paths()) {
557                         LOG_GENERAL ("Check %1 %2 answer %3", j.string(), boost::filesystem::exists(j) ? "yes" : "no", valid ? "yes" : "no");
558                 }
559
560                 shared_ptr<DCPContent> dcp = dynamic_pointer_cast<DCPContent> (i);
561                 bool const needs_kdm = dcp && dcp->needs_kdm ();
562                 bool const needs_assets = dcp && dcp->needs_assets ();
563
564                 wxString s = std_to_wx (i->summary ());
565
566                 if (!valid) {
567                         s = _("MISSING: ") + s;
568                 }
569
570                 if (needs_kdm) {
571                         s = _("NEEDS KDM: ") + s;
572                 }
573
574                 if (needs_assets) {
575                         s = _("NEEDS OV: ") + s;
576                 }
577
578                 wxListItem item;
579                 item.SetId (t);
580                 item.SetText (s);
581                 item.SetData (i.get ());
582                 _content->InsertItem (item);
583
584                 if (i.get() == selected_content) {
585                         _content->SetItemState (t, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
586                 }
587
588                 if (!valid || needs_kdm || needs_assets) {
589                         _content->SetItemTextColour (t, *wxRED);
590                 }
591         }
592
593         if (!selected_content && !content.empty ()) {
594                 /* Select the item of content if none was selected before */
595                 _content->SetItemState (0, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
596         }
597
598         setup_sensitivity ();
599 }
600
601 void
602 ContentPanel::files_dropped (wxDropFilesEvent& event)
603 {
604         if (!_film) {
605                 return;
606         }
607
608         wxString* paths = event.GetFiles ();
609         list<boost::filesystem::path> path_list;
610         for (int i = 0; i < event.GetNumberOfFiles(); i++) {
611                 path_list.push_back (wx_to_std (paths[i]));
612         }
613
614         add_files (path_list);
615 }
616
617 void
618 ContentPanel::add_files (list<boost::filesystem::path> paths)
619 {
620         /* It has been reported that the paths returned from e.g. wxFileDialog are not always sorted;
621            I can't reproduce that, but sort them anyway.  Don't use ImageFilenameSorter as a normal
622            alphabetical sort is expected here.
623         */
624
625         paths.sort (CaseInsensitiveSorter ());
626
627         /* XXX: check for lots of files here and do something */
628
629         try {
630                 BOOST_FOREACH (boost::filesystem::path i, paths) {
631                         BOOST_FOREACH (shared_ptr<Content> j, content_factory (_film, i)) {
632                                 _film->examine_and_add_content (j);
633                         }
634                 }
635         } catch (exception& e) {
636                 error_dialog (_parent, e.what());
637         }
638 }