mirror of https://github.com/stella-emu/stella.git
All functionality now restored to FileListWidget (pattern matching, ROMs only, etc).
This now uses a lambda function, which is more versatile. Eventually we may even add regular expressions.
This commit is contained in:
parent
712cfbaeb1
commit
ad9b0e6e75
|
@ -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())
|
if (!_realNode || !_realNode->isDirectory())
|
||||||
return false;
|
return false;
|
||||||
|
@ -89,7 +90,9 @@ bool FilesystemNode::getChildren(FSList& fslist, ListMode mode) const
|
||||||
// Force ZIP c'tor to be called
|
// Force ZIP c'tor to be called
|
||||||
AbstractFSNodePtr ptr = FilesystemNodeFactory::create(i->getPath(),
|
AbstractFSNodePtr ptr = FilesystemNodeFactory::create(i->getPath(),
|
||||||
FilesystemNodeFactory::Type::ZIP);
|
FilesystemNodeFactory::Type::ZIP);
|
||||||
fslist.emplace_back(FilesystemNode(ptr));
|
FilesystemNode node(ptr);
|
||||||
|
if (filter(node))
|
||||||
|
fslist.emplace_back(node);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
#endif
|
#endif
|
||||||
|
@ -98,7 +101,9 @@ bool FilesystemNode::getChildren(FSList& fslist, ListMode mode) const
|
||||||
if (i->isDirectory())
|
if (i->isDirectory())
|
||||||
i->setName(" [" + i->getName() + "]");
|
i->setName(" [" + i->getName() + "]");
|
||||||
|
|
||||||
fslist.emplace_back(FilesystemNode(i));
|
FilesystemNode node(i);
|
||||||
|
if (filter(node))
|
||||||
|
fslist.emplace_back(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,8 @@
|
||||||
* we can build upon this.
|
* we can build upon this.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
#include "bspf.hxx"
|
#include "bspf.hxx"
|
||||||
|
|
||||||
class FilesystemNode;
|
class FilesystemNode;
|
||||||
|
@ -70,6 +72,10 @@ class FilesystemNode
|
||||||
*/
|
*/
|
||||||
enum class ListMode { FilesOnly, DirectoriesOnly, All };
|
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<bool(const FilesystemNode& node)>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new pathless FilesystemNode. Since there's no path associated
|
* Create a new pathless FilesystemNode. Since there's no path associated
|
||||||
* with this node, path-related operations (i.e. exists(), isDirectory(),
|
* 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
|
* @return true if successful, false otherwise (e.g. when the directory
|
||||||
* does not exist).
|
* 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
|
* Set/get a string representation of the name of the file. This is can be
|
||||||
|
@ -264,6 +271,7 @@ class AbstractFSNode
|
||||||
protected:
|
protected:
|
||||||
friend class FilesystemNode;
|
friend class FilesystemNode;
|
||||||
using ListMode = FilesystemNode::ListMode;
|
using ListMode = FilesystemNode::ListMode;
|
||||||
|
using NameFilter = FilesystemNode::NameFilter;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -115,7 +115,9 @@ void BrowserDialog::show(const string& startpath,
|
||||||
{
|
{
|
||||||
case FileLoad:
|
case FileLoad:
|
||||||
_fileList->setListMode(FilesystemNode::ListMode::All);
|
_fileList->setListMode(FilesystemNode::ListMode::All);
|
||||||
_fileList->setFileExtension(ext);
|
_fileList->setNameFilter([&ext](const FilesystemNode& node) {
|
||||||
|
return BSPF::endsWithIgnoreCase(node.getName(), ext);
|
||||||
|
});
|
||||||
_selected->setEditable(false);
|
_selected->setEditable(false);
|
||||||
_selected->clearFlags(Widget::FLAG_INVISIBLE);
|
_selected->clearFlags(Widget::FLAG_INVISIBLE);
|
||||||
_type->clearFlags(Widget::FLAG_INVISIBLE);
|
_type->clearFlags(Widget::FLAG_INVISIBLE);
|
||||||
|
@ -123,7 +125,9 @@ void BrowserDialog::show(const string& startpath,
|
||||||
|
|
||||||
case FileSave:
|
case FileSave:
|
||||||
_fileList->setListMode(FilesystemNode::ListMode::All);
|
_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->setEditable(false); // FIXME - disable user input for now
|
||||||
_selected->clearFlags(Widget::FLAG_INVISIBLE);
|
_selected->clearFlags(Widget::FLAG_INVISIBLE);
|
||||||
_type->clearFlags(Widget::FLAG_INVISIBLE);
|
_type->clearFlags(Widget::FLAG_INVISIBLE);
|
||||||
|
@ -131,6 +135,7 @@ void BrowserDialog::show(const string& startpath,
|
||||||
|
|
||||||
case Directories:
|
case Directories:
|
||||||
_fileList->setListMode(FilesystemNode::ListMode::DirectoriesOnly);
|
_fileList->setListMode(FilesystemNode::ListMode::DirectoriesOnly);
|
||||||
|
_fileList->setNameFilter([](const FilesystemNode&) { return true; });
|
||||||
_selected->setEditable(false);
|
_selected->setEditable(false);
|
||||||
_selected->setFlags(Widget::FLAG_INVISIBLE);
|
_selected->setFlags(Widget::FLAG_INVISIBLE);
|
||||||
_type->setFlags(Widget::FLAG_INVISIBLE);
|
_type->setFlags(Widget::FLAG_INVISIBLE);
|
||||||
|
|
|
@ -20,13 +20,6 @@
|
||||||
|
|
||||||
#include "bspf.hxx"
|
#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,
|
FileListWidget::FileListWidget(GuiObject* boss, const GUI::Font& font,
|
||||||
int x, int y, int w, int h)
|
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
|
// This widget is special, in that it catches signals and redirects them
|
||||||
setTarget(this);
|
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;
|
_node = node;
|
||||||
|
|
||||||
// Read in the data from the file system (start with an empty list)
|
// Read in the data from the file system (start with an empty list)
|
||||||
_fileList.clear();
|
_fileList.clear();
|
||||||
_fileList.reserve(512);
|
_fileList.reserve(512);
|
||||||
_node.getChildren(_fileList, _fsmode);
|
_node.getChildren(_fileList, _fsmode, _filter);
|
||||||
|
|
||||||
// Now fill the list widget with the names from the file list
|
// Now fill the list widget with the names from the file list
|
||||||
StringList l;
|
StringList l;
|
||||||
|
@ -102,10 +98,7 @@ void FileListWidget::selectDirectory()
|
||||||
void FileListWidget::selectParent()
|
void FileListWidget::selectParent()
|
||||||
{
|
{
|
||||||
if(_node.hasParent())
|
if(_node.hasParent())
|
||||||
{
|
setLocation(_node.getParent(), !_history.empty() ? _history.pop() : EmptyString);
|
||||||
const string& select = !_history.empty() ? _history.pop() : EmptyString;
|
|
||||||
setLocation(_node.getParent(), select);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
|
@ -35,6 +35,9 @@ class CommandSender;
|
||||||
|
|
||||||
Note that the ItemActivated signal is not sent when activating a
|
Note that the ItemActivated signal is not sent when activating a
|
||||||
directory; instead the selection descends into the directory.
|
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
|
class FileListWidget : public StringListWidget
|
||||||
{
|
{
|
||||||
|
@ -49,9 +52,10 @@ class FileListWidget : public StringListWidget
|
||||||
int x, int y, int w, int h);
|
int x, int y, int w, int h);
|
||||||
virtual ~FileListWidget() = default;
|
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 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.
|
Set initial directory, and optionally select the given item.
|
||||||
|
@ -69,12 +73,15 @@ class FileListWidget : public StringListWidget
|
||||||
void reload();
|
void reload();
|
||||||
|
|
||||||
/** Gets current node(s) */
|
/** 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; }
|
const FilesystemNode& currentDir() const { return _node; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/** Very similar to setDirectory(), but also updates the history */
|
/** 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 */
|
/** Descend into currently selected directory */
|
||||||
void selectDirectory();
|
void selectDirectory();
|
||||||
|
@ -83,12 +90,11 @@ class FileListWidget : public StringListWidget
|
||||||
|
|
||||||
private:
|
private:
|
||||||
FilesystemNode::ListMode _fsmode;
|
FilesystemNode::ListMode _fsmode;
|
||||||
|
FilesystemNode::NameFilter _filter;
|
||||||
FilesystemNode _node;
|
FilesystemNode _node;
|
||||||
FSList _fileList;
|
FSList _fileList;
|
||||||
|
|
||||||
Common::FixedStack<string> _history;
|
Common::FixedStack<string> _history;
|
||||||
|
|
||||||
string _extension;
|
|
||||||
uInt32 _selected;
|
uInt32 _selected;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
@ -43,45 +43,10 @@
|
||||||
#include "Version.hxx"
|
#include "Version.hxx"
|
||||||
#include "LauncherDialog.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,
|
LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent,
|
||||||
int x, int y, int w, int h)
|
int x, int y, int w, int h)
|
||||||
: Dialog(osystem, parent, x, y, w, h),
|
: Dialog(osystem, parent, x, y, w, h),
|
||||||
myStartButton(nullptr),
|
myStartButton(nullptr),
|
||||||
myPrevDirButton(nullptr),
|
myPrevDirButton(nullptr),
|
||||||
myOptionsButton(nullptr),
|
myOptionsButton(nullptr),
|
||||||
|
@ -349,6 +314,27 @@ void LauncherDialog::updateUI()
|
||||||
loadRomInfo();
|
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()
|
void LauncherDialog::loadRomInfo()
|
||||||
{
|
{
|
||||||
|
@ -384,47 +370,7 @@ void LauncherDialog::showOnlyROMs(bool state)
|
||||||
{
|
{
|
||||||
myShowOnlyROMs = state;
|
myShowOnlyROMs = state;
|
||||||
instance().settings().setValue("launcherroms", state);
|
instance().settings().setValue("launcherroms", state);
|
||||||
}
|
applyFiltering();
|
||||||
|
|
||||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
@ -565,6 +511,11 @@ void LauncherDialog::handleCommand(CommandSender* sender, int cmd,
|
||||||
myEventHandled = true;
|
myEventHandled = true;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case EditableWidget::kChangedCmd:
|
||||||
|
applyFiltering(); // pattern matching taken care of directly in this method
|
||||||
|
reload();
|
||||||
|
break;
|
||||||
|
|
||||||
case kQuitCmd:
|
case kQuitCmd:
|
||||||
close();
|
close();
|
||||||
instance().eventHandler().quit();
|
instance().eventHandler().quit();
|
||||||
|
|
|
@ -48,7 +48,6 @@ class LauncherDialog : public Dialog
|
||||||
public:
|
public:
|
||||||
// These must be accessible from dialogs created by this class
|
// These must be accessible from dialogs created by this class
|
||||||
enum {
|
enum {
|
||||||
kAllfilesCmd = 'lalf', // show all files (or ROMs only)
|
|
||||||
kLoadROMCmd = 'STRT', // load currently selected ROM
|
kLoadROMCmd = 'STRT', // load currently selected ROM
|
||||||
kRomDirChosenCmd = 'romc' // rom dir chosen
|
kRomDirChosenCmd = 'romc' // rom dir chosen
|
||||||
};
|
};
|
||||||
|
@ -97,12 +96,12 @@ class LauncherDialog : public Dialog
|
||||||
|
|
||||||
void loadConfig() override;
|
void loadConfig() override;
|
||||||
void updateUI();
|
void updateUI();
|
||||||
|
void applyFiltering();
|
||||||
|
|
||||||
void loadRom();
|
void loadRom();
|
||||||
void loadRomInfo();
|
void loadRomInfo();
|
||||||
void handleContextMenu();
|
void handleContextMenu();
|
||||||
void showOnlyROMs(bool state);
|
void showOnlyROMs(bool state);
|
||||||
bool matchPattern(const string& s, const string& pattern) const;
|
|
||||||
void openSettings();
|
void openSettings();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
@ -134,9 +133,10 @@ class LauncherDialog : public Dialog
|
||||||
bool myEventHandled;
|
bool myEventHandled;
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
kPrevDirCmd = 'PRVD',
|
kAllfilesCmd = 'lalf', // show all files (or ROMs only)
|
||||||
kOptionsCmd = 'OPTI',
|
kPrevDirCmd = 'PRVD',
|
||||||
kQuitCmd = 'QUIT'
|
kOptionsCmd = 'OPTI',
|
||||||
|
kQuitCmd = 'QUIT'
|
||||||
};
|
};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
Loading…
Reference in New Issue