diff --git a/src/emucore/FSNode.cxx b/src/emucore/FSNode.cxx index 5a054652b..e12dc51f8 100644 --- a/src/emucore/FSNode.cxx +++ b/src/emucore/FSNode.cxx @@ -51,7 +51,8 @@ bool FilesystemNode::exists() const } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bool FilesystemNode::getChildren(FSList& fslist, ListMode mode) const +bool FilesystemNode::getChildren(FSList& fslist, ListMode mode, + const NameFilter& filter) const { if (!_realNode || !_realNode->isDirectory()) return false; @@ -89,7 +90,9 @@ bool FilesystemNode::getChildren(FSList& fslist, ListMode mode) const // Force ZIP c'tor to be called AbstractFSNodePtr ptr = FilesystemNodeFactory::create(i->getPath(), FilesystemNodeFactory::Type::ZIP); - fslist.emplace_back(FilesystemNode(ptr)); + FilesystemNode node(ptr); + if (filter(node)) + fslist.emplace_back(node); } else #endif @@ -98,7 +101,9 @@ bool FilesystemNode::getChildren(FSList& fslist, ListMode mode) const if (i->isDirectory()) i->setName(" [" + i->getName() + "]"); - fslist.emplace_back(FilesystemNode(i)); + FilesystemNode node(i); + if (filter(node)) + fslist.emplace_back(node); } } diff --git a/src/emucore/FSNode.hxx b/src/emucore/FSNode.hxx index e4f92af53..7218d5584 100644 --- a/src/emucore/FSNode.hxx +++ b/src/emucore/FSNode.hxx @@ -45,6 +45,8 @@ * we can build upon this. */ +#include + #include "bspf.hxx" class FilesystemNode; @@ -70,6 +72,10 @@ class FilesystemNode */ enum class ListMode { FilesOnly, DirectoriesOnly, All }; + /** Function used to filter the file listing. Returns true if the filename + should be included, else false.*/ + using NameFilter = std::function; + /** * Create a new pathless FilesystemNode. Since there's no path associated * with this node, path-related operations (i.e. exists(), isDirectory(), @@ -127,7 +133,8 @@ class FilesystemNode * @return true if successful, false otherwise (e.g. when the directory * does not exist). */ - bool getChildren(FSList& fslist, ListMode mode = ListMode::DirectoriesOnly) const; + bool getChildren(FSList& fslist, ListMode mode = ListMode::DirectoriesOnly, + const NameFilter& filter = [](const FilesystemNode&){ return true; }) const; /** * Set/get a string representation of the name of the file. This is can be @@ -264,6 +271,7 @@ class AbstractFSNode protected: friend class FilesystemNode; using ListMode = FilesystemNode::ListMode; + using NameFilter = FilesystemNode::NameFilter; public: /** diff --git a/src/gui/BrowserDialog.cxx b/src/gui/BrowserDialog.cxx index c153b45bd..040e7197f 100644 --- a/src/gui/BrowserDialog.cxx +++ b/src/gui/BrowserDialog.cxx @@ -115,7 +115,9 @@ void BrowserDialog::show(const string& startpath, { case FileLoad: _fileList->setListMode(FilesystemNode::ListMode::All); - _fileList->setFileExtension(ext); + _fileList->setNameFilter([&ext](const FilesystemNode& node) { + return BSPF::endsWithIgnoreCase(node.getName(), ext); + }); _selected->setEditable(false); _selected->clearFlags(Widget::FLAG_INVISIBLE); _type->clearFlags(Widget::FLAG_INVISIBLE); @@ -123,7 +125,9 @@ void BrowserDialog::show(const string& startpath, case FileSave: _fileList->setListMode(FilesystemNode::ListMode::All); - _fileList->setFileExtension(ext); + _fileList->setNameFilter([&ext](const FilesystemNode& node) { + return BSPF::endsWithIgnoreCase(node.getName(), ext); + }); _selected->setEditable(false); // FIXME - disable user input for now _selected->clearFlags(Widget::FLAG_INVISIBLE); _type->clearFlags(Widget::FLAG_INVISIBLE); @@ -131,6 +135,7 @@ void BrowserDialog::show(const string& startpath, case Directories: _fileList->setListMode(FilesystemNode::ListMode::DirectoriesOnly); + _fileList->setNameFilter([](const FilesystemNode&) { return true; }); _selected->setEditable(false); _selected->setFlags(Widget::FLAG_INVISIBLE); _type->setFlags(Widget::FLAG_INVISIBLE); diff --git a/src/gui/FileListWidget.cxx b/src/gui/FileListWidget.cxx index 613f80ce2..c9db28035 100644 --- a/src/gui/FileListWidget.cxx +++ b/src/gui/FileListWidget.cxx @@ -20,13 +20,6 @@ #include "bspf.hxx" -/** - TODO: - - extension handling not handled - - add lambda filter to selectively choose files based on pattern - - history of selected folders/files -*/ - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - FileListWidget::FileListWidget(GuiObject* boss, const GUI::Font& font, int x, int y, int w, int h) @@ -36,6 +29,9 @@ FileListWidget::FileListWidget(GuiObject* boss, const GUI::Font& font, { // This widget is special, in that it catches signals and redirects them setTarget(this); + + // By default, all filenames are valid + _filter = [](const FilesystemNode& node) { return true; }; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -71,14 +67,14 @@ void FileListWidget::setDirectory(const FilesystemNode& node, string select) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void FileListWidget::setLocation(const FilesystemNode& node, const string& select) +void FileListWidget::setLocation(const FilesystemNode& node, string select) { _node = node; // Read in the data from the file system (start with an empty list) _fileList.clear(); _fileList.reserve(512); - _node.getChildren(_fileList, _fsmode); + _node.getChildren(_fileList, _fsmode, _filter); // Now fill the list widget with the names from the file list StringList l; @@ -102,10 +98,7 @@ void FileListWidget::selectDirectory() void FileListWidget::selectParent() { if(_node.hasParent()) - { - const string& select = !_history.empty() ? _history.pop() : EmptyString; - setLocation(_node.getParent(), select); - } + setLocation(_node.getParent(), !_history.empty() ? _history.pop() : EmptyString); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/FileListWidget.hxx b/src/gui/FileListWidget.hxx index cebfa6464..04fce87d0 100644 --- a/src/gui/FileListWidget.hxx +++ b/src/gui/FileListWidget.hxx @@ -35,6 +35,9 @@ class CommandSender; Note that the ItemActivated signal is not sent when activating a directory; instead the selection descends into the directory. + + Widgets wishing to enforce their own filename filtering are able + to use a 'NameFilter' as described below. */ class FileListWidget : public StringListWidget { @@ -49,9 +52,10 @@ class FileListWidget : public StringListWidget int x, int y, int w, int h); virtual ~FileListWidget() = default; - /** Determines how to display files/folders */ + /** Determines how to display files/folders; either setDirectory or reload + must be called after any of these are called. */ void setListMode(FilesystemNode::ListMode mode) { _fsmode = mode; } - void setFileExtension(const string& ext) { _extension = ext; } // TODO - re-implement this + void setNameFilter(const FilesystemNode::NameFilter& filter) { _filter = filter; } /** Set initial directory, and optionally select the given item. @@ -69,12 +73,15 @@ class FileListWidget : public StringListWidget void reload(); /** Gets current node(s) */ - const FilesystemNode& selected() const { return _fileList[_selected]; } + const FilesystemNode& selected() { + _selected = BSPF::clamp(_selected, 0u, uInt32(_fileList.size()-1)); + return _fileList[_selected]; + } const FilesystemNode& currentDir() const { return _node; } private: /** Very similar to setDirectory(), but also updates the history */ - void setLocation(const FilesystemNode& node, const string& select = EmptyString); + void setLocation(const FilesystemNode& node, string select = EmptyString); /** Descend into currently selected directory */ void selectDirectory(); @@ -83,12 +90,11 @@ class FileListWidget : public StringListWidget private: FilesystemNode::ListMode _fsmode; + FilesystemNode::NameFilter _filter; FilesystemNode _node; FSList _fileList; Common::FixedStack _history; - - string _extension; uInt32 _selected; private: diff --git a/src/gui/LauncherDialog.cxx b/src/gui/LauncherDialog.cxx index d051f49ac..1b8b3795b 100644 --- a/src/gui/LauncherDialog.cxx +++ b/src/gui/LauncherDialog.cxx @@ -43,45 +43,10 @@ #include "Version.hxx" #include "LauncherDialog.hxx" -/** - TODO: - - show all files / only ROMs - - connect to 'matchPattern' - - create lambda filter to pass these to FileListWidget -*/ -#if 0 - // TODO - rough contents of lambda filter - FSList files; - files.reserve(2048); - myCurrentNode.getChildren(files, FilesystemNode::ListMode::All); - - // Add '[..]' to indicate previous folder - if(myCurrentNode.hasParent()) - myGameList->appendGame(" [..]", "", "", true); - - // Now add the directory entries - bool domatch = myPattern && myPattern->getText() != ""; - for(const auto& f: files) - { - bool isDir = f.isDirectory(); - const string& name = isDir ? (" [" + f.getName() + "]") : f.getName(); - - // Do we want to show only ROMs or all files? - if(!isDir && myShowOnlyROMs && !Bankswitch::isValidRomName(f)) - continue; - - // Skip over files that don't match the pattern in the 'pattern' textbox - if(domatch && !isDir && !matchPattern(name, myPattern->getText())) - continue; - - myGameList->appendGame(name, f.getPath(), "", isDir); - } -#endif - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent, - int x, int y, int w, int h) - : Dialog(osystem, parent, x, y, w, h), +LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent, + int x, int y, int w, int h) + : Dialog(osystem, parent, x, y, w, h), myStartButton(nullptr), myPrevDirButton(nullptr), myOptionsButton(nullptr), @@ -349,6 +314,27 @@ void LauncherDialog::updateUI() loadRomInfo(); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void LauncherDialog::applyFiltering() +{ + myList->setNameFilter( + [&](const FilesystemNode& node) { + if(!node.isDirectory()) + { + // Do we want to show only ROMs or all files? + if(myShowOnlyROMs && !Bankswitch::isValidRomName(node)) + return false; + + // Skip over files that don't match the pattern in the 'pattern' textbox + if(myPattern && myPattern->getText() != "" && + !BSPF::containsIgnoreCase(node.getName(), myPattern->getText())) + return false; + } + return true; + } + ); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void LauncherDialog::loadRomInfo() { @@ -384,47 +370,7 @@ void LauncherDialog::showOnlyROMs(bool state) { myShowOnlyROMs = state; instance().settings().setValue("launcherroms", state); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bool LauncherDialog::matchPattern(const string& s, const string& pattern) const -{ - // This method is modelled after strcasestr, which we don't use - // because it isn't guaranteed to be available everywhere - // The strcasestr uses the KMP algorithm when the comparisons - // reach a certain point, but since we'll be dealing with relatively - // short strings, I think the overhead of building a KMP table - // each time would be slower than the brute force method used here - const char* haystack = s.c_str(); - const char* needle = pattern.c_str(); - - uInt8 b = tolower(*needle); - - needle++; - for(;; haystack++) - { - if(*haystack == '\0') /* No match */ - return false; - - /* The first character matches */ - if(tolower(*haystack) == b) - { - const char* rhaystack = haystack + 1; - const char* rneedle = needle; - - for(;; rhaystack++, rneedle++) - { - if(*rneedle == '\0') /* Found a match */ - return true; - if(*rhaystack == '\0') /* No match */ - return false; - - /* Nothing in this round */ - if(tolower(*rhaystack) != tolower(*rneedle)) - break; - } - } - } + applyFiltering(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -565,6 +511,11 @@ void LauncherDialog::handleCommand(CommandSender* sender, int cmd, myEventHandled = true; break; + case EditableWidget::kChangedCmd: + applyFiltering(); // pattern matching taken care of directly in this method + reload(); + break; + case kQuitCmd: close(); instance().eventHandler().quit(); diff --git a/src/gui/LauncherDialog.hxx b/src/gui/LauncherDialog.hxx index 47e26cbcf..fa1a6a0d2 100644 --- a/src/gui/LauncherDialog.hxx +++ b/src/gui/LauncherDialog.hxx @@ -48,7 +48,6 @@ class LauncherDialog : public Dialog public: // These must be accessible from dialogs created by this class enum { - kAllfilesCmd = 'lalf', // show all files (or ROMs only) kLoadROMCmd = 'STRT', // load currently selected ROM kRomDirChosenCmd = 'romc' // rom dir chosen }; @@ -97,12 +96,12 @@ class LauncherDialog : public Dialog void loadConfig() override; void updateUI(); + void applyFiltering(); void loadRom(); void loadRomInfo(); void handleContextMenu(); void showOnlyROMs(bool state); - bool matchPattern(const string& s, const string& pattern) const; void openSettings(); private: @@ -134,9 +133,10 @@ class LauncherDialog : public Dialog bool myEventHandled; enum { - kPrevDirCmd = 'PRVD', - kOptionsCmd = 'OPTI', - kQuitCmd = 'QUIT' + kAllfilesCmd = 'lalf', // show all files (or ROMs only) + kPrevDirCmd = 'PRVD', + kOptionsCmd = 'OPTI', + kQuitCmd = 'QUIT' }; private: