More renaming.
[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/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/plain_text_content.h"
43 #include "lib/plain_text.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<PlainTextContent>(i) && i->paths_valid()) {
262                         /* Rather special case; if we select a text subtitle file jump to its
263                            first subtitle.
264                         */
265                         PlainText ts (dynamic_pointer_cast<PlainTextContent>(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 (!_film) {
413                 return;
414         }
415
416         if (_timeline_dialog) {
417                 _timeline_dialog->Destroy ();
418                 _timeline_dialog = 0;
419         }
420
421         _timeline_dialog = new TimelineDialog (this, _film);
422         _timeline_dialog->Show ();
423 }
424
425 void
426 ContentPanel::right_click (wxListEvent& ev)
427 {
428         _menu->popup (_film, selected (), TimelineContentViewList (), ev.GetPoint ());
429 }
430
431 /** Set up broad sensitivity based on the type of content that is selected */
432 void
433 ContentPanel::setup_sensitivity ()
434 {
435         _add_file->Enable (_generally_sensitive);
436         _add_folder->Enable (_generally_sensitive);
437         _add_dcp->Enable (_generally_sensitive);
438
439         ContentList selection = selected ();
440         ContentList video_selection = selected_video ();
441         ContentList audio_selection = selected_audio ();
442
443         _remove->Enable   (_generally_sensitive && !selection.empty());
444         _earlier->Enable  (_generally_sensitive && selection.size() == 1);
445         _later->Enable    (_generally_sensitive && selection.size() == 1);
446         _timeline->Enable (_generally_sensitive && _film && !_film->content().empty());
447
448         _video_panel->Enable    (_generally_sensitive && video_selection.size() > 0);
449         _audio_panel->Enable    (_generally_sensitive && audio_selection.size() > 0);
450         _subtitle_panel->Enable (_generally_sensitive && selection.size() == 1 && selection.front()->subtitle);
451         _timing_panel->Enable   (_generally_sensitive);
452 }
453
454 void
455 ContentPanel::set_film (shared_ptr<Film> film)
456 {
457         _audio_panel->set_film (film);
458
459         _film = film;
460
461         film_changed (Film::CONTENT);
462         film_changed (Film::AUDIO_CHANNELS);
463         selection_changed ();
464         setup_sensitivity ();
465 }
466
467 void
468 ContentPanel::set_general_sensitivity (bool s)
469 {
470         _generally_sensitive = s;
471         setup_sensitivity ();
472 }
473
474 void
475 ContentPanel::earlier_clicked ()
476 {
477         ContentList sel = selected ();
478         if (sel.size() == 1) {
479                 _film->move_content_earlier (sel.front ());
480                 selection_changed ();
481         }
482 }
483
484 void
485 ContentPanel::later_clicked ()
486 {
487         ContentList sel = selected ();
488         if (sel.size() == 1) {
489                 _film->move_content_later (sel.front ());
490                 selection_changed ();
491         }
492 }
493
494 void
495 ContentPanel::set_selection (weak_ptr<Content> wc)
496 {
497         ContentList content = _film->content ();
498         for (size_t i = 0; i < content.size(); ++i) {
499                 if (content[i] == wc.lock ()) {
500                         _content->SetItemState (i, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
501                 } else {
502                         _content->SetItemState (i, 0, wxLIST_STATE_SELECTED);
503                 }
504         }
505 }
506
507 void
508 ContentPanel::set_selection (ContentList cl)
509 {
510         ContentList content = _film->content ();
511         for (size_t i = 0; i < content.size(); ++i) {
512                 if (find(cl.begin(), cl.end(), content[i]) != cl.end()) {
513                         _content->SetItemState (i, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
514                 } else {
515                         _content->SetItemState (i, 0, wxLIST_STATE_SELECTED);
516                 }
517         }
518 }
519
520 void
521 ContentPanel::film_content_changed (int property)
522 {
523         if (
524                 property == ContentProperty::PATH ||
525                 property == DCPContentProperty::NEEDS_ASSETS ||
526                 property == DCPContentProperty::NEEDS_KDM ||
527                 property == DCPContentProperty::NAME
528                 ) {
529
530                 setup ();
531         }
532
533         BOOST_FOREACH (ContentSubPanel* i, _panels) {
534                 i->film_content_changed (property);
535         }
536 }
537
538 void
539 ContentPanel::setup ()
540 {
541         ContentList content = _film->content ();
542
543         Content* selected_content = 0;
544         int const s = _content->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
545         if (s != -1) {
546                 wxListItem item;
547                 item.SetId (s);
548                 item.SetMask (wxLIST_MASK_DATA);
549                 _content->GetItem (item);
550                 selected_content = reinterpret_cast<Content*> (item.GetData ());
551         }
552
553         _content->DeleteAllItems ();
554
555         BOOST_FOREACH (shared_ptr<Content> i, content) {
556                 int const t = _content->GetItemCount ();
557                 bool const valid = i->paths_valid ();
558
559                 /* Temporary debugging for Igor */
560                 BOOST_FOREACH (boost::filesystem::path j, i->paths()) {
561                         LOG_GENERAL ("Check %1 %2 answer %3", j.string(), boost::filesystem::exists(j) ? "yes" : "no", valid ? "yes" : "no");
562                 }
563
564                 shared_ptr<DCPContent> dcp = dynamic_pointer_cast<DCPContent> (i);
565                 bool const needs_kdm = dcp && dcp->needs_kdm ();
566                 bool const needs_assets = dcp && dcp->needs_assets ();
567
568                 wxString s = std_to_wx (i->summary ());
569
570                 if (!valid) {
571                         s = _("MISSING: ") + s;
572                 }
573
574                 if (needs_kdm) {
575                         s = _("NEEDS KDM: ") + s;
576                 }
577
578                 if (needs_assets) {
579                         s = _("NEEDS OV: ") + s;
580                 }
581
582                 wxListItem item;
583                 item.SetId (t);
584                 item.SetText (s);
585                 item.SetData (i.get ());
586                 _content->InsertItem (item);
587
588                 if (i.get() == selected_content) {
589                         _content->SetItemState (t, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
590                 }
591
592                 if (!valid || needs_kdm || needs_assets) {
593                         _content->SetItemTextColour (t, *wxRED);
594                 }
595         }
596
597         if (!selected_content && !content.empty ()) {
598                 /* Select the item of content if none was selected before */
599                 _content->SetItemState (0, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
600         }
601
602         setup_sensitivity ();
603 }
604
605 void
606 ContentPanel::files_dropped (wxDropFilesEvent& event)
607 {
608         if (!_film) {
609                 return;
610         }
611
612         wxString* paths = event.GetFiles ();
613         list<boost::filesystem::path> path_list;
614         for (int i = 0; i < event.GetNumberOfFiles(); i++) {
615                 path_list.push_back (wx_to_std (paths[i]));
616         }
617
618         add_files (path_list);
619 }
620
621 void
622 ContentPanel::add_files (list<boost::filesystem::path> paths)
623 {
624         /* It has been reported that the paths returned from e.g. wxFileDialog are not always sorted;
625            I can't reproduce that, but sort them anyway.  Don't use ImageFilenameSorter as a normal
626            alphabetical sort is expected here.
627         */
628
629         paths.sort (CaseInsensitiveSorter ());
630
631         /* XXX: check for lots of files here and do something */
632
633         try {
634                 BOOST_FOREACH (boost::filesystem::path i, paths) {
635                         BOOST_FOREACH (shared_ptr<Content> j, content_factory (_film, i)) {
636                                 _film->examine_and_add_content (j);
637                         }
638                 }
639         } catch (exception& e) {
640                 error_dialog (_parent, e.what());
641         }
642 }