Use dcp::filesystem to wrap filesystem calls and fix_long_path
[dcpomatic.git] / src / wx / content_panel.cc
index df6308bea548929f1ba50a9dd0ffe366c00693f3..56c1d0524bd4a2053eaea192be18221e7935daaf 100644 (file)
@@ -22,6 +22,8 @@
 #include "audio_panel.h"
 #include "content_panel.h"
 #include "dcpomatic_button.h"
+#include "dir_dialog.h"
+#include "file_dialog.h"
 #include "film_viewer.h"
 #include "image_sequence_dialog.h"
 #include "text_panel.h"
 #include "lib/dcp_subtitle_decoder.h"
 #include "lib/dcpomatic_log.h"
 #include "lib/ffmpeg_content.h"
+#include "lib/film.h"
 #include "lib/image_content.h"
 #include "lib/log.h"
 #include "lib/playlist.h"
+#include "lib/scope_guard.h"
 #include "lib/string_text_file.h"
 #include "lib/string_text_file_content.h"
 #include "lib/text_content.h"
 #include "lib/video_content.h"
+#include <dcp/filesystem.h>
 #include <dcp/warnings.h>
 LIBDCP_DISABLE_WARNINGS
 #include <wx/display.h>
@@ -90,22 +95,26 @@ public:
        bool OnSashPositionChange(int new_position) override
        {
                /* Try to stop the top bit of the splitter getting so small that buttons disappear */
-               return new_position > 220;
+               auto const ok = new_position > 220;
+               if (ok) {
+                       Config::instance()->set_main_content_divider_sash_position(new_position);
+               }
+               return ok;
        }
 
        void first_shown(wxWindow* top, wxWindow* bottom)
        {
                int const sn = wxDisplay::GetFromWindow(this);
+               /* Fallback for when GetFromWindow fails for reasons that aren't clear */
+               int pos = -600;
                if (sn >= 0) {
                        wxRect const screen = wxDisplay(sn).GetClientArea();
                        /* This is a hack to try and make the content notebook a sensible size; large on big displays but small
                           enough on small displays to leave space for the content area.
                           */
-                       SplitHorizontally(top, bottom, screen.height > 800 ? -600 : -_top_panel_minimum_size);
-               } else {
-                       /* Fallback for when GetFromWindow fails for reasons that aren't clear */
-                       SplitHorizontally(top, bottom, -600);
+                       pos = screen.height > 800 ? -600 : -_top_panel_minimum_size;
                }
+               SplitHorizontally(top, bottom, Config::instance()->main_content_divider_sash_position().get_value_or(pos));
                _first_shown = true;
        }
 
@@ -117,7 +126,7 @@ private:
                        /* The window is now fairly big but the top panel is small; this happens when the DCP-o-matic window
                         * is shrunk and then made larger again.  Try to set a sensible top panel size in this case (#1839).
                         */
-                       SetSashPosition(_top_panel_minimum_size);
+                       SetSashPosition(Config::instance()->main_content_divider_sash_position().get_value_or(_top_panel_minimum_size));
                }
 
                ev.Skip ();
@@ -144,9 +153,9 @@ public:
                vector<boost::filesystem::path> folders;
                for (size_t i = 0; i < filenames.GetCount(); ++i) {
                        auto path = boost::filesystem::path(wx_to_std(filenames[i]));
-                       if (boost::filesystem::is_regular_file(path)) {
+                       if (dcp::filesystem::is_regular_file(path)) {
                                files.push_back(path);
-                       } else if (boost::filesystem::is_directory(path)) {
+                       } else if (dcp::filesystem::is_directory(path)) {
                                if (contains_assetmap(path)) {
                                        dcps.push_back(path);
                                } else {
@@ -175,6 +184,56 @@ private:
 };
 
 
+/** A wxListCtrl that can middle-ellipsize its text */
+class ContentListCtrl : public wxListCtrl
+{
+public:
+       ContentListCtrl(wxWindow* parent)
+               : wxListCtrl(parent, wxID_ANY, wxDefaultPosition, wxSize(320, 160), wxLC_REPORT | wxLC_NO_HEADER | wxLC_VIRTUAL)
+       {
+               _red.SetTextColour(*wxRED);
+       }
+
+       struct Item
+       {
+               wxString text;
+               weak_ptr<Content> content;
+               bool error;
+       };
+
+       void set(vector<Item> const& items)
+       {
+               _items = items;
+               SetItemCount(items.size());
+       }
+
+       wxString OnGetItemText(long item, long) const override
+       {
+               DCPOMATIC_ASSERT(item >= 0 && item < static_cast<long>(_items.size()));
+               wxClientDC dc(const_cast<wxWindow*>(static_cast<wxWindow const*>(this)));
+               return wxControl::Ellipsize(_items[item].text, dc, wxELLIPSIZE_MIDDLE, GetSize().GetWidth());
+       }
+
+       wxListItemAttr* OnGetItemAttr(long item) const override
+       {
+               DCPOMATIC_ASSERT(item >= 0 && item < static_cast<long>(_items.size()));
+               return _items[item].error ? const_cast<wxListItemAttr*>(&_red) : nullptr;
+       }
+
+       weak_ptr<Content> content_at_index(long index)
+       {
+               if (index < 0 || index >= static_cast<long>(_items.size())) {
+                       return {};
+               }
+               return _items[index].content;
+       }
+
+private:
+       std::vector<Item> _items;
+       wxListItemAttr _red;
+};
+
+
 ContentPanel::ContentPanel(wxNotebook* n, shared_ptr<Film> film, FilmViewer& viewer)
        : _parent (n)
        , _film (film)
@@ -191,17 +250,17 @@ ContentPanel::ContentPanel(wxNotebook* n, shared_ptr<Film> film, FilmViewer& vie
        {
                auto s = new wxBoxSizer (wxHORIZONTAL);
 
-               _content = new wxListCtrl (_top_panel, wxID_ANY, wxDefaultPosition, wxSize (320, 160), wxLC_REPORT | wxLC_NO_HEADER);
+               _content = new ContentListCtrl(_top_panel);
                _content->DragAcceptFiles (true);
                s->Add (_content, 1, wxEXPAND | wxTOP | wxBOTTOM, 6);
 
                _content->InsertColumn (0, wxT(""));
-               _content->SetColumnWidth (0, 512);
+               _content->SetColumnWidth(0, 2048);
 
                auto b = new wxBoxSizer (wxVERTICAL);
 
                _add_file = new Button (_top_panel, _("Add file(s)..."));
-               _add_file->SetToolTip (_("Add video, image, sound or subtitle files to the film."));
+               _add_file->SetToolTip(_("Add video, image, sound or subtitle files to the film (Ctrl+A)."));
                b->Add (_add_file, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
 
                _add_folder = new Button (_top_panel, _("Add folder..."));
@@ -213,7 +272,7 @@ ContentPanel::ContentPanel(wxNotebook* n, shared_ptr<Film> film, FilmViewer& vie
                b->Add (_add_dcp, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
 
                _remove = new Button (_top_panel, _("Remove"));
-               _remove->SetToolTip (_("Remove the selected piece of content from the film."));
+               _remove->SetToolTip(_("Remove the selected piece of content from the film (Delete)."));
                b->Add (_remove, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
 
                _earlier = new Button (_top_panel, _("Earlier"));
@@ -225,7 +284,7 @@ ContentPanel::ContentPanel(wxNotebook* n, shared_ptr<Film> film, FilmViewer& vie
                b->Add (_later, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
 
                _timeline = new Button (_top_panel, _("Timeline..."));
-               _timeline->SetToolTip (_("Open the timeline for the film."));
+               _timeline->SetToolTip(_("Open the timeline for the film (Ctrl+T)."));
                b->Add (_timeline, 0, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
 
                s->Add (b, 0, wxALL, 4);
@@ -272,9 +331,9 @@ ContentPanel::selected ()
                        break;
                }
 
-               auto cl = _film->content();
-               if (s < int (cl.size())) {
-                       sel.push_back (cl[s]);
+               auto weak = _content->content_at_index(s);
+               if (auto content = weak.lock()) {
+                       sel.push_back(content);
                }
        }
 
@@ -344,11 +403,11 @@ ContentPanel::selected_ffmpeg ()
 
 
 void
-ContentPanel::film_changed (Film::Property p)
+ContentPanel::film_changed(FilmProperty p)
 {
        switch (p) {
-       case Film::Property::CONTENT:
-       case Film::Property::CONTENT_ORDER:
+       case FilmProperty::CONTENT:
+       case FilmProperty::CONTENT_ORDER:
                setup ();
                break;
        default:
@@ -470,7 +529,7 @@ ContentPanel::check_selection ()
                _video_panel->create ();
        } else if (!have_video && _video_panel) {
                _notebook->DeletePage (off);
-               _video_panel = 0;
+               _video_panel = nullptr;
        }
 
        if (have_video) {
@@ -483,7 +542,7 @@ ContentPanel::check_selection ()
                _audio_panel->create ();
        } else if (!have_audio && _audio_panel) {
                _notebook->DeletePage (off);
-               _audio_panel = 0;
+               _audio_panel = nullptr;
        }
 
        if (have_audio) {
@@ -523,6 +582,17 @@ ContentPanel::check_selection ()
 }
 
 
+optional<boost::filesystem::path>
+ContentPanel::add_files_override_path() const
+{
+       DCPOMATIC_ASSERT(_film->directory());
+       return Config::instance()->default_add_file_location() == Config::DefaultAddFileLocation::SAME_AS_PROJECT
+               ? _film->directory()->parent_path()
+               : boost::optional<boost::filesystem::path>();
+
+}
+
+
 void
 ContentPanel::add_file_clicked ()
 {
@@ -533,56 +603,31 @@ ContentPanel::add_file_clicked ()
                return;
        }
 
-       auto path = Config::instance()->add_files_path();
-
        /* The wxFD_CHANGE_DIR here prevents a `could not set working directory' error 123 on Windows when using
           non-Latin filenames or paths.
        */
-       auto d = new wxFileDialog (
+       FileDialog dialog(
                _splitter,
                _("Choose a file or files"),
-               std_to_wx(path ? path->string() : home_directory().string()),
-               wxT (""),
-               wxT ("All files|*.*|Subtitle files|*.srt;*.xml|Audio files|*.wav;*.w64;*.flac;*.aif;*.aiff"),
-               wxFD_MULTIPLE | wxFD_CHANGE_DIR
+               wxT("All files|*.*|Subtitle files|*.srt;*.xml|Audio files|*.wav;*.w64;*.flac;*.aif;*.aiff"),
+               wxFD_MULTIPLE | wxFD_CHANGE_DIR,
+               "AddFilesPath",
+               add_files_override_path()
                );
 
-       int const r = d->ShowModal ();
-
-       if (r != wxID_OK) {
-               d->Destroy ();
-               return;
-       }
-
-       wxArrayString paths;
-       d->GetPaths (paths);
-       vector<boost::filesystem::path> path_list;
-       for (unsigned int i = 0; i < paths.GetCount(); ++i) {
-               path_list.push_back (wx_to_std(paths[i]));
-       }
-       add_files (path_list);
-
-       if (!path_list.empty()) {
-               Config::instance()->set_add_files_path(path_list[0].parent_path());
+       if (dialog.show()) {
+               add_files(dialog.paths());
        }
-
-       d->Destroy ();
 }
 
 
 void
 ContentPanel::add_folder_clicked ()
 {
-       auto d = new wxDirDialog (_splitter, _("Choose a folder"), wxT(""), wxDD_DIR_MUST_EXIST);
-       int r = d->ShowModal ();
-       boost::filesystem::path const path (wx_to_std (d->GetPath ()));
-       d->Destroy ();
-
-       if (r != wxID_OK) {
-               return;
+       DirDialog dialog(_splitter, _("Choose a folder"), wxDD_DIR_MUST_EXIST, "AddFilesPath", add_files_override_path());
+       if (dialog.show()) {
+               add_folder(dialog.path());
        }
-
-       add_folder(path);
 }
 
 
@@ -606,16 +651,12 @@ ContentPanel::add_folder(boost::filesystem::path folder)
        for (auto i: content) {
                auto ic = dynamic_pointer_cast<ImageContent> (i);
                if (ic) {
-                       auto e = new ImageSequenceDialog (_splitter);
-                       int const r = e->ShowModal();
-                       auto const frame_rate = e->frame_rate ();
-                       e->Destroy ();
+                       ImageSequenceDialog dialog(_splitter);
 
-                       if (r != wxID_OK) {
+                       if (dialog.ShowModal() != wxID_OK) {
                                return;
                        }
-
-                       ic->set_video_frame_rate (frame_rate);
+                       ic->set_video_frame_rate(_film, dialog.frame_rate());
                }
 
                _film->examine_and_add_content (i);
@@ -626,16 +667,10 @@ ContentPanel::add_folder(boost::filesystem::path folder)
 void
 ContentPanel::add_dcp_clicked ()
 {
-       auto d = new wxDirDialog (_splitter, _("Choose a DCP folder"), wxT(""), wxDD_DIR_MUST_EXIST);
-       int r = d->ShowModal ();
-       boost::filesystem::path const path (wx_to_std (d->GetPath ()));
-       d->Destroy ();
-
-       if (r != wxID_OK) {
-               return;
+       DirDialog dialog(_splitter, _("Choose a DCP folder"), wxDD_DIR_MUST_EXIST, "AddFilesPath", add_files_override_path());
+       if (dialog.show()) {
+               add_dcp(dialog.path());
        }
-
-       add_dcp(path);
 }
 
 
@@ -681,16 +716,11 @@ ContentPanel::remove_clicked (bool hotkey)
 void
 ContentPanel::timeline_clicked ()
 {
-       if (!_film) {
+       if (!_film || _film->content().empty()) {
                return;
        }
 
-       if (_timeline_dialog) {
-               _timeline_dialog->Destroy ();
-               _timeline_dialog = nullptr;
-       }
-
-       _timeline_dialog = new TimelineDialog (this, _film, _film_viewer);
+       _timeline_dialog.reset(this, _film, _film_viewer);
        _timeline_dialog->set_selection (selected());
        _timeline_dialog->Show ();
 }
@@ -744,8 +774,8 @@ ContentPanel::set_film (shared_ptr<Film> film)
 
        _film = film;
 
-       film_changed (Film::Property::CONTENT);
-       film_changed (Film::Property::AUDIO_CHANNELS);
+       film_changed(FilmProperty::CONTENT);
+       film_changed(FilmProperty::AUDIO_CHANNELS);
 
        if (_film) {
                check_selection ();
@@ -798,14 +828,16 @@ ContentPanel::set_selection (weak_ptr<Content> wc)
 void
 ContentPanel::set_selection (ContentList cl)
 {
-       _no_check_selection = true;
+       {
+               _no_check_selection = true;
+               ScopeGuard sg = [this]() { _no_check_selection = false; };
 
-       auto content = _film->content ();
-       for (size_t i = 0; i < content.size(); ++i) {
-               set_selected_state(i, find(cl.begin(), cl.end(), content[i]) != cl.end());
+               auto content = _film->content ();
+               for (size_t i = 0; i < content.size(); ++i) {
+                       set_selected_state(i, find(cl.begin(), cl.end(), content[i]) != cl.end());
+               }
        }
 
-       _no_check_selection = false;
        check_selection ();
 }
 
@@ -846,21 +878,11 @@ ContentPanel::setup ()
        }
 
        auto content = _film->content ();
+       auto selection = selected();
 
-       Content* selected_content = nullptr;
-       auto const s = _content->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
-       if (s != -1) {
-               wxListItem item;
-               item.SetId (s);
-               item.SetMask (wxLIST_MASK_DATA);
-               _content->GetItem (item);
-               selected_content = reinterpret_cast<Content*> (item.GetData ());
-       }
-
-       _content->DeleteAllItems ();
+       vector<ContentListCtrl::Item> items;
 
        for (auto i: content) {
-               int const t = _content->GetItemCount ();
                bool const valid = i->paths_valid ();
 
                auto dcp = dynamic_pointer_cast<DCPContent> (i);
@@ -881,24 +903,15 @@ ContentPanel::setup ()
                        s = _("NEEDS OV: ") + s;
                }
 
-               wxListItem item;
-               item.SetId (t);
-               item.SetText (s);
-               item.SetData (i.get ());
-               _content->InsertItem (item);
-
-               if (i.get() == selected_content) {
-                       set_selected_state(t, true);
-               }
-
-               if (!valid || needs_kdm || needs_assets) {
-                       _content->SetItemTextColour (t, *wxRED);
-               }
+               items.push_back({s, i, !valid || needs_kdm || needs_assets});
        }
 
-       if (!selected_content && !content.empty ()) {
-               /* Select the item of content if none was selected before */
+       _content->set(items);
+
+       if (selection.empty() && !content.empty()) {
                set_selected_state(0, true);
+       } else {
+               set_selection(selection);
        }
 
        setup_sensitivity ();