Simplify and fix up selection code for the content list (#2428).
[dcpomatic.git] / src / wx / content_panel.cc
index 83085db5aa57f05920ad1496aac7d75dab71e6f9..0cbb62b54d3744154c079cae22f313d8cba3cb42 100644 (file)
 #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 "timeline_dialog.h"
 #include "timing_panel.h"
 #include "video_panel.h"
+#include "wx_ptr.h"
 #include "wx_util.h"
 #include "lib/audio_content.h"
 #include "lib/case_insensitive_sorter.h"
@@ -43,6 +46,7 @@
 #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"
@@ -50,6 +54,7 @@
 #include <dcp/warnings.h>
 LIBDCP_DISABLE_WARNINGS
 #include <wx/display.h>
+#include <wx/dnd.h>
 #include <wx/listctrl.h>
 #include <wx/notebook.h>
 #include <wx/wx.h>
@@ -72,7 +77,163 @@ using namespace boost::placeholders;
 #endif
 
 
-ContentPanel::ContentPanel (wxNotebook* n, shared_ptr<Film> film, weak_ptr<FilmViewer> viewer)
+class LimitedContentPanelSplitter : public wxSplitterWindow
+{
+public:
+       LimitedContentPanelSplitter(wxWindow* parent)
+               : wxSplitterWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxSP_NOBORDER | wxSP_3DSASH | wxSP_LIVE_UPDATE)
+       {
+               /* This value doesn't really mean much but we just want to stop double-click on the
+                  divider from shrinking the bottom panel (#1601).
+                  */
+               SetMinimumPaneSize(64);
+
+               Bind(wxEVT_SIZE, boost::bind(&LimitedContentPanelSplitter::sized, this, _1));
+       }
+
+       bool OnSashPositionChange(int new_position) override
+       {
+               /* Try to stop the top bit of the splitter getting so small that buttons disappear */
+               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.
+                          */
+                       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;
+       }
+
+private:
+       void sized(wxSizeEvent& ev)
+       {
+               auto const height = GetSize().GetHeight();
+               if (_first_shown && (!_last_height || *_last_height != height) && height > _top_panel_minimum_size && GetSashPosition() < _top_panel_minimum_size) {
+                       /* 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(Config::instance()->main_content_divider_sash_position().get_value_or(_top_panel_minimum_size));
+               }
+
+               ev.Skip ();
+               _last_height = height;
+       }
+
+       bool _first_shown = false;
+       int const _top_panel_minimum_size = 350;
+       boost::optional<int> _last_height;
+};
+
+
+class ContentDropTarget : public wxFileDropTarget
+{
+public:
+       ContentDropTarget(ContentPanel* owner)
+               : _panel(owner)
+       {}
+
+       bool OnDropFiles(wxCoord, wxCoord, wxArrayString const& filenames) override
+       {
+               vector<boost::filesystem::path> files;
+               vector<boost::filesystem::path> dcps;
+               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)) {
+                               files.push_back(path);
+                       } else if (boost::filesystem::is_directory(path)) {
+                               if (contains_assetmap(path)) {
+                                       dcps.push_back(path);
+                               } else {
+                                       folders.push_back(path);
+                               }
+                       }
+               }
+
+               if (!filenames.empty()) {
+                       _panel->add_files(files);
+               }
+
+               for (auto dcp: dcps) {
+                       _panel->add_dcp(dcp);
+               }
+
+               for (auto dir: folders) {
+                       _panel->add_folder(dir);
+               }
+
+               return true;
+       };
+
+private:
+       ContentPanel* _panel;
+};
+
+
+/** 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)
        , _film_viewer (viewer)
@@ -80,7 +241,7 @@ ContentPanel::ContentPanel (wxNotebook* n, shared_ptr<Film> film, weak_ptr<FilmV
        , _ignore_deselect (false)
        , _no_check_selection (false)
 {
-       _splitter = new LimitedSplitter (n);
+       _splitter = new LimitedContentPanelSplitter(n);
        _top_panel = new wxPanel (_splitter);
 
        _menu = new ContentMenu (_splitter, _film_viewer);
@@ -88,12 +249,12 @@ ContentPanel::ContentPanel (wxNotebook* n, shared_ptr<Film> film, weak_ptr<FilmV
        {
                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);
 
@@ -146,6 +307,8 @@ ContentPanel::ContentPanel (wxNotebook* n, shared_ptr<Film> film, weak_ptr<FilmV
        _earlier->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::earlier_clicked, this));
        _later->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::later_clicked, this));
        _timeline->Bind (wxEVT_BUTTON, boost::bind (&ContentPanel::timeline_clicked, this));
+
+       _content->SetDropTarget(new ContentDropTarget(this));
 }
 
 
@@ -167,9 +330,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);
                }
        }
 
@@ -323,9 +486,7 @@ ContentPanel::check_selection ()
        }
 
        if (go_to && Config::instance()->jump_to_selected() && signal_manager) {
-               auto fv = _film_viewer.lock ();
-               DCPOMATIC_ASSERT (fv);
-               signal_manager->when_idle(boost::bind(&FilmViewer::seek, fv.get(), go_to.get().ceil(_film->video_frame_rate()), true));
+               signal_manager->when_idle(boost::bind(&FilmViewer::seek, &_film_viewer, go_to.get().ceil(_film->video_frame_rate()), true));
        }
 
        if (_timeline_dialog) {
@@ -420,6 +581,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 ()
 {
@@ -430,59 +602,41 @@ 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());
        }
+}
 
+
+void
+ContentPanel::add_folder(boost::filesystem::path folder)
+{
        vector<shared_ptr<Content>> content;
 
        try {
-               content = content_factory (path);
+               content = content_factory(folder);
        } catch (exception& e) {
                error_dialog (_parent, e.what());
                return;
@@ -496,16 +650,12 @@ ContentPanel::add_folder_clicked ()
        for (auto i: content) {
                auto ic = dynamic_pointer_cast<ImageContent> (i);
                if (ic) {
-                       auto e = new ImageSequenceDialog (_splitter);
-                       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);
@@ -516,17 +666,18 @@ ContentPanel::add_folder_clicked ()
 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());
        }
+}
+
 
+void
+ContentPanel::add_dcp(boost::filesystem::path dcp)
+{
        try {
-               _film->examine_and_add_content (make_shared<DCPContent>(path));
+               _film->examine_and_add_content(make_shared<DCPContent>(dcp));
        } catch (ProjectFolderError &) {
                error_dialog (
                        _parent,
@@ -536,7 +687,7 @@ ContentPanel::add_dcp_clicked ()
                         )
                        );
        } catch (exception& e) {
-               error_dialog (_parent, e.what());
+               error_dialog(_parent, e.what());
        }
 }
 
@@ -568,12 +719,7 @@ ContentPanel::timeline_clicked ()
                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 ();
 }
@@ -681,14 +827,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 ();
 }
 
@@ -729,21 +877,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);
@@ -764,24 +902,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 ();
@@ -808,6 +937,10 @@ ContentPanel::files_dropped (wxDropFilesEvent& event)
 void
 ContentPanel::add_files (vector<boost::filesystem::path> paths)
 {
+       if (!_film) {
+               return;
+       }
+
        /* It has been reported that the paths returned from e.g. wxFileDialog are not always sorted;
           I can't reproduce that, but sort them anyway.  Don't use ImageFilenameSorter as a normal
           alphabetical sort is expected here.
@@ -857,47 +990,8 @@ ContentPanel::set_selected_state(int item, bool state)
 }
 
 
-LimitedSplitter::LimitedSplitter (wxWindow* parent)
-       : wxSplitterWindow (parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxSP_NOBORDER | wxSP_3DSASH | wxSP_LIVE_UPDATE)
-       , _first_shown (false)
-       , _top_panel_minimum_size (350)
-{
-       /* This value doesn't really mean much but we just want to stop double-click on the
-          divider from shrinking the bottom panel (#1601).
-       */
-       SetMinimumPaneSize (64);
-
-       Bind (wxEVT_SIZE, boost::bind(&LimitedSplitter::sized, this, _1));
-}
-
-
-void
-LimitedSplitter::first_shown (wxWindow* top, wxWindow* bottom)
-{
-       int const sn = wxDisplay::GetFromWindow(this);
-       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);
-       }
-       _first_shown = true;
-}
-
-
-void
-LimitedSplitter::sized (wxSizeEvent& ev)
+wxWindow*
+ContentPanel::window() const
 {
-       if (_first_shown && GetSize().GetHeight() > _top_panel_minimum_size && GetSashPosition() < _top_panel_minimum_size) {
-               /* 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);
-       }
-
-       ev.Skip ();
+       return _splitter;
 }