mirror of https://github.com/stella-emu/stella.git
added subdirectory search to launcher
enhanced ProgressDialog
This commit is contained in:
parent
4c97ec89c9
commit
1219fe0d2c
|
@ -76,9 +76,62 @@ bool FilesystemNode::exists() const
|
||||||
return _realNode ? _realNode->exists() : false;
|
return _realNode ? _realNode->exists() : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
bool FilesystemNode::getAllChildren(FSList& fslist, ListMode mode,
|
||||||
|
const NameFilter& filter,
|
||||||
|
bool includeParentDirectory) const
|
||||||
|
{
|
||||||
|
if(getChildren(fslist, mode, filter, includeParentDirectory))
|
||||||
|
{
|
||||||
|
// Sort only once at the end
|
||||||
|
#if defined(ZIP_SUPPORT)
|
||||||
|
// before sorting, replace single file ZIP archive names with contained file names
|
||||||
|
// because they are displayed using their contained file names
|
||||||
|
for(auto& i : fslist)
|
||||||
|
{
|
||||||
|
if(BSPF::endsWithIgnoreCase(i.getPath(), ".zip"))
|
||||||
|
{
|
||||||
|
FilesystemNodeZIP zipNode(i.getPath());
|
||||||
|
|
||||||
|
i.setName(zipNode.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
std::sort(fslist.begin(), fslist.end(),
|
||||||
|
[](const FilesystemNode& node1, const FilesystemNode& node2)
|
||||||
|
{
|
||||||
|
if(node1.isDirectory() != node2.isDirectory())
|
||||||
|
return node1.isDirectory();
|
||||||
|
else
|
||||||
|
return BSPF::compareIgnoreCase(node1.getName(), node2.getName()) < 0;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
#if defined(ZIP_SUPPORT)
|
||||||
|
// After sorting replace zip files with zip nodes
|
||||||
|
for(auto& i : fslist)
|
||||||
|
{
|
||||||
|
if(BSPF::endsWithIgnoreCase(i.getPath(), ".zip"))
|
||||||
|
{
|
||||||
|
// Force ZIP c'tor to be called
|
||||||
|
AbstractFSNodePtr ptr = FilesystemNodeFactory::create(i.getPath(),
|
||||||
|
FilesystemNodeFactory::Type::ZIP);
|
||||||
|
FilesystemNode zipNode(ptr);
|
||||||
|
i = zipNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
bool FilesystemNode::getChildren(FSList& fslist, ListMode mode,
|
bool FilesystemNode::getChildren(FSList& fslist, ListMode mode,
|
||||||
const NameFilter& filter,
|
const NameFilter& filter,
|
||||||
|
bool includeChildDirectories,
|
||||||
bool includeParentDirectory) const
|
bool includeParentDirectory) const
|
||||||
{
|
{
|
||||||
if (!_realNode || !_realNode->isDirectory())
|
if (!_realNode || !_realNode->isDirectory())
|
||||||
|
@ -90,12 +143,15 @@ bool FilesystemNode::getChildren(FSList& fslist, ListMode mode,
|
||||||
if (!_realNode->getChildren(tmp, mode))
|
if (!_realNode->getChildren(tmp, mode))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// when incuding child directories, everything must be sorted once at the end
|
||||||
|
if(!includeChildDirectories)
|
||||||
|
{
|
||||||
#if defined(ZIP_SUPPORT)
|
#if defined(ZIP_SUPPORT)
|
||||||
// before sorting, replace single file ZIP archive names with contained file names
|
// before sorting, replace single file ZIP archive names with contained file names
|
||||||
// because they are displayed using their contained file names
|
// because they are displayed using their contained file names
|
||||||
for (auto& i : tmp)
|
for(auto& i : tmp)
|
||||||
{
|
{
|
||||||
if (BSPF::endsWithIgnoreCase(i->getPath(), ".zip"))
|
if(BSPF::endsWithIgnoreCase(i->getPath(), ".zip"))
|
||||||
{
|
{
|
||||||
FilesystemNodeZIP node(i->getPath());
|
FilesystemNodeZIP node(i->getPath());
|
||||||
|
|
||||||
|
@ -104,15 +160,16 @@ bool FilesystemNode::getChildren(FSList& fslist, ListMode mode,
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
std::sort(tmp.begin(), tmp.end(),
|
std::sort(tmp.begin(), tmp.end(),
|
||||||
[](const AbstractFSNodePtr& node1, const AbstractFSNodePtr& node2)
|
[](const AbstractFSNodePtr& node1, const AbstractFSNodePtr& node2)
|
||||||
{
|
{
|
||||||
if (node1->isDirectory() != node2->isDirectory())
|
if(node1->isDirectory() != node2->isDirectory())
|
||||||
return node1->isDirectory();
|
return node1->isDirectory();
|
||||||
else
|
else
|
||||||
return BSPF::compareIgnoreCase(node1->getName(), node2->getName()) < 0;
|
return BSPF::compareIgnoreCase(node1->getName(), node2->getName()) < 0;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Add parent node, if it is valid to do so
|
// Add parent node, if it is valid to do so
|
||||||
if (includeParentDirectory && hasParent())
|
if (includeParentDirectory && hasParent())
|
||||||
|
@ -130,21 +187,44 @@ bool FilesystemNode::getChildren(FSList& fslist, ListMode mode,
|
||||||
{
|
{
|
||||||
// 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);
|
||||||
FilesystemNode node(ptr);
|
FilesystemNode zipNode(ptr);
|
||||||
if (filter(node))
|
|
||||||
fslist.emplace_back(node);
|
if(filter(zipNode))
|
||||||
|
{
|
||||||
|
if(!includeChildDirectories)
|
||||||
|
fslist.emplace_back(zipNode);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// filter by zip node but add file node
|
||||||
|
FilesystemNode node(i);
|
||||||
|
fslist.emplace_back(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
#endif
|
#endif
|
||||||
{
|
{
|
||||||
// Make directories stand out
|
// Make directories stand out
|
||||||
if (i->isDirectory())
|
if(i->isDirectory())
|
||||||
i->setName(" [" + i->getName() + "]");
|
i->setName(" [" + i->getName() + "]");
|
||||||
|
|
||||||
FilesystemNode node(i);
|
FilesystemNode node(i);
|
||||||
if (filter(node))
|
|
||||||
fslist.emplace_back(node);
|
if(includeChildDirectories)
|
||||||
|
{
|
||||||
|
if(i->isDirectory())
|
||||||
|
node.getChildren(fslist, mode, filter, includeChildDirectories, false);
|
||||||
|
else
|
||||||
|
// do not add directories in this mode
|
||||||
|
if(filter(node))
|
||||||
|
fslist.emplace_back(node);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(filter(node))
|
||||||
|
fslist.emplace_back(node);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -114,6 +114,17 @@ class FilesystemNode
|
||||||
*/
|
*/
|
||||||
bool exists() const;
|
bool exists() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a list of child nodes of this and all sub-directories. If called on a node
|
||||||
|
* that does not represent a directory, false is returned.
|
||||||
|
*
|
||||||
|
* @return true if successful, false otherwise (e.g. when the directory
|
||||||
|
* does not exist).
|
||||||
|
*/
|
||||||
|
bool getAllChildren(FSList& fslist, ListMode mode = ListMode::DirectoriesOnly,
|
||||||
|
const NameFilter& filter = [](const FilesystemNode&) { return true; },
|
||||||
|
bool includeParentDirectory = true) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a list of child nodes of this directory node. If called on a node
|
* Return a list of child nodes of this directory node. If called on a node
|
||||||
* that does not represent a directory, false is returned.
|
* that does not represent a directory, false is returned.
|
||||||
|
@ -123,6 +134,7 @@ class FilesystemNode
|
||||||
*/
|
*/
|
||||||
bool getChildren(FSList& fslist, ListMode mode = ListMode::DirectoriesOnly,
|
bool getChildren(FSList& fslist, ListMode mode = ListMode::DirectoriesOnly,
|
||||||
const NameFilter& filter = [](const FilesystemNode&){ return true; },
|
const NameFilter& filter = [](const FilesystemNode&){ return true; },
|
||||||
|
bool includeChildDirectories = false,
|
||||||
bool includeParentDirectory = true) const;
|
bool includeParentDirectory = true) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -273,8 +285,8 @@ class FilesystemNode
|
||||||
string getPathWithExt(const string& ext) const;
|
string getPathWithExt(const string& ext) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
AbstractFSNodePtr _realNode;
|
|
||||||
explicit FilesystemNode(const AbstractFSNodePtr& realNode);
|
explicit FilesystemNode(const AbstractFSNodePtr& realNode);
|
||||||
|
AbstractFSNodePtr _realNode;
|
||||||
void setPath(const string& path);
|
void setPath(const string& path);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,8 @@ class ContextMenu : public Dialog, public CommandSender
|
||||||
const VariantList& items, int cmd = 0, int width = 0);
|
const VariantList& items, int cmd = 0, int width = 0);
|
||||||
~ContextMenu() override = default;
|
~ContextMenu() override = default;
|
||||||
|
|
||||||
|
bool isShading() const override { return false; }
|
||||||
|
|
||||||
/** Set the parent widget's ID */
|
/** Set the parent widget's ID */
|
||||||
void setID(uInt32 id) { _id = id; }
|
void setID(uInt32 id) { _id = id; }
|
||||||
|
|
||||||
|
|
|
@ -255,12 +255,11 @@ void Dialog::render()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dialog is still on top if e.g a dialog without title is opened
|
// A dialog is still on top if a non-shading dialog (e.g. ContextMenu)
|
||||||
// (e.g. ContextMenu)
|
// is opended above it.
|
||||||
bool onTop = parent().myDialogStack.top() == this
|
bool onTop = parent().myDialogStack.top() == this
|
||||||
|| (parent().myDialogStack.get(parent().myDialogStack.size() - 2) == this
|
|| (parent().myDialogStack.get(parent().myDialogStack.size() - 2) == this
|
||||||
&& !parent().myDialogStack.top()->hasTitle());
|
&& !parent().myDialogStack.top()->isShading());
|
||||||
//&& typeid(*parent().myDialogStack.top()) == typeid(ContextMenu))
|
|
||||||
|
|
||||||
if(!onTop)
|
if(!onTop)
|
||||||
{
|
{
|
||||||
|
|
|
@ -94,6 +94,8 @@ class Dialog : public GuiObject
|
||||||
void setTitle(const string& title);
|
void setTitle(const string& title);
|
||||||
bool hasTitle() { return !_title.empty(); }
|
bool hasTitle() { return !_title.empty(); }
|
||||||
|
|
||||||
|
virtual bool isShading() const { return true; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Determine the maximum width/height of a dialog based on the minimum
|
Determine the maximum width/height of a dialog based on the minimum
|
||||||
allowable bounds, also taking into account the current window size.
|
allowable bounds, also taking into account the current window size.
|
||||||
|
|
|
@ -171,7 +171,7 @@ void DialogContainer::removeDialog()
|
||||||
{
|
{
|
||||||
if(!myDialogStack.empty())
|
if(!myDialogStack.empty())
|
||||||
{
|
{
|
||||||
cerr << "remove dialog " << typeid(*myDialogStack.top()).name() << endl;
|
//cerr << "remove dialog " << typeid(*myDialogStack.top()).name() << endl;
|
||||||
myDialogStack.pop();
|
myDialogStack.pop();
|
||||||
|
|
||||||
// Inform the frame buffer that it has to render all surfaces
|
// Inform the frame buffer that it has to render all surfaces
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
#include "ScrollBarWidget.hxx"
|
#include "ScrollBarWidget.hxx"
|
||||||
#include "FileListWidget.hxx"
|
#include "FileListWidget.hxx"
|
||||||
#include "TimerManager.hxx"
|
#include "TimerManager.hxx"
|
||||||
|
#include "ProgressDialog.hxx"
|
||||||
|
|
||||||
#include "bspf.hxx"
|
#include "bspf.hxx"
|
||||||
|
|
||||||
|
@ -72,22 +73,37 @@ void FileListWidget::setDirectory(const FilesystemNode& node,
|
||||||
void FileListWidget::setLocation(const FilesystemNode& node,
|
void FileListWidget::setLocation(const FilesystemNode& node,
|
||||||
const string& select)
|
const string& select)
|
||||||
{
|
{
|
||||||
|
progress().resetProgress();
|
||||||
|
progress().open();
|
||||||
|
|
||||||
_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);
|
|
||||||
_node.getChildren(_fileList, _fsmode, _filter);
|
if(_includeSubDirs)
|
||||||
|
{
|
||||||
|
// Actually this could become HUGE
|
||||||
|
_fileList.reserve(0x2000);
|
||||||
|
_node.getAllChildren(_fileList, _fsmode, _filter);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_fileList.reserve(0x200);
|
||||||
|
_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;
|
||||||
for(const auto& file: _fileList)
|
for(const auto& file : _fileList)
|
||||||
l.push_back(file.getName());
|
l.push_back(file.getName());
|
||||||
|
|
||||||
setList(l);
|
setList(l);
|
||||||
setSelected(select);
|
setSelected(select);
|
||||||
|
|
||||||
ListWidget::recalc();
|
ListWidget::recalc();
|
||||||
|
|
||||||
|
progress().close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
@ -114,6 +130,21 @@ void FileListWidget::reload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
ProgressDialog& FileListWidget::progress()
|
||||||
|
{
|
||||||
|
if(myProgressDialog == nullptr)
|
||||||
|
myProgressDialog = make_unique<ProgressDialog>(this, _font, "", false);
|
||||||
|
|
||||||
|
return *myProgressDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileListWidget::incProgress()
|
||||||
|
{
|
||||||
|
if(_includeSubDirs)
|
||||||
|
progress().incProgress();
|
||||||
|
}
|
||||||
|
|
||||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
bool FileListWidget::handleText(char text)
|
bool FileListWidget::handleText(char text)
|
||||||
{
|
{
|
||||||
|
@ -202,3 +233,5 @@ void FileListWidget::handleCommand(CommandSender* sender, int cmd, int data, int
|
||||||
|
|
||||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
uInt64 FileListWidget::_QUICK_SELECT_DELAY = 300;
|
uInt64 FileListWidget::_QUICK_SELECT_DELAY = 300;
|
||||||
|
|
||||||
|
unique_ptr<ProgressDialog> FileListWidget::myProgressDialog{nullptr};
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
#define FILE_LIST_WIDGET_HXX
|
#define FILE_LIST_WIDGET_HXX
|
||||||
|
|
||||||
class CommandSender;
|
class CommandSender;
|
||||||
|
class ProgressDialog;
|
||||||
|
|
||||||
#include "FSNode.hxx"
|
#include "FSNode.hxx"
|
||||||
#include "Stack.hxx"
|
#include "Stack.hxx"
|
||||||
|
@ -59,12 +60,16 @@ class FileListWidget : public StringListWidget
|
||||||
_filter = filter;
|
_filter = filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When enabled, all subdirectories will be searched too.
|
||||||
|
void setIncludeSubDirs(bool enable) { _includeSubDirs = enable; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Set initial directory, and optionally select the given item.
|
Set initial directory, and optionally select the given item.
|
||||||
|
|
||||||
@param node The directory to display. If this is a file, its parent
|
@param node The directory to display. If this is a file, its parent
|
||||||
will instead be used, and the file will be selected
|
will instead be used, and the file will be selected
|
||||||
@param select An optional entry to select (if applicable)
|
@param select An optional entry to select (if applicable)
|
||||||
|
@param recursive Recursively list sub-directories too
|
||||||
*/
|
*/
|
||||||
void setDirectory(const FilesystemNode& node,
|
void setDirectory(const FilesystemNode& node,
|
||||||
const string& select = EmptyString);
|
const string& select = EmptyString);
|
||||||
|
@ -84,6 +89,12 @@ class FileListWidget : public StringListWidget
|
||||||
|
|
||||||
static void setQuickSelectDelay(uInt64 time) { _QUICK_SELECT_DELAY = time; }
|
static void setQuickSelectDelay(uInt64 time) { _QUICK_SELECT_DELAY = time; }
|
||||||
|
|
||||||
|
ProgressDialog& progress();
|
||||||
|
void incProgress();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
static unique_ptr<ProgressDialog> myProgressDialog;
|
||||||
|
|
||||||
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);
|
void setLocation(const FilesystemNode& node, const string& select);
|
||||||
|
@ -99,6 +110,7 @@ class FileListWidget : public StringListWidget
|
||||||
FilesystemNode::NameFilter _filter;
|
FilesystemNode::NameFilter _filter;
|
||||||
FilesystemNode _node;
|
FilesystemNode _node;
|
||||||
FSList _fileList;
|
FSList _fileList;
|
||||||
|
bool _includeSubDirs{false};
|
||||||
|
|
||||||
Common::FixedStack<string> _history;
|
Common::FixedStack<string> _history;
|
||||||
uInt32 _selected{0};
|
uInt32 _selected{0};
|
||||||
|
|
|
@ -15,6 +15,9 @@
|
||||||
// this file, and for a DISCLAIMER OF ALL WARRANTIES.
|
// this file, and for a DISCLAIMER OF ALL WARRANTIES.
|
||||||
//============================================================================
|
//============================================================================
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// - abort current file list reload when typing
|
||||||
|
|
||||||
#include "bspf.hxx"
|
#include "bspf.hxx"
|
||||||
#include "Bankswitch.hxx"
|
#include "Bankswitch.hxx"
|
||||||
#include "BrowserDialog.hxx"
|
#include "BrowserDialog.hxx"
|
||||||
|
@ -29,6 +32,7 @@
|
||||||
#include "GlobalPropsDialog.hxx"
|
#include "GlobalPropsDialog.hxx"
|
||||||
#include "StellaSettingsDialog.hxx"
|
#include "StellaSettingsDialog.hxx"
|
||||||
#include "WhatsNewDialog.hxx"
|
#include "WhatsNewDialog.hxx"
|
||||||
|
#include "ProgressDialog.hxx"
|
||||||
#include "MessageBox.hxx"
|
#include "MessageBox.hxx"
|
||||||
#include "ToolTip.hxx"
|
#include "ToolTip.hxx"
|
||||||
#include "OSystem.hxx"
|
#include "OSystem.hxx"
|
||||||
|
@ -73,25 +77,71 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent,
|
||||||
buttonHeight = myUseMinimalUI ? lineHeight - VGAP * 2: lineHeight * 1.25,
|
buttonHeight = myUseMinimalUI ? lineHeight - VGAP * 2: lineHeight * 1.25,
|
||||||
buttonWidth = (_w - 2 * HBORDER - BUTTON_GAP * (4 - 1));
|
buttonWidth = (_w - 2 * HBORDER - BUTTON_GAP * (4 - 1));
|
||||||
|
|
||||||
int xpos = HBORDER, ypos = VBORDER, lwidth = 0, lwidth2 = 0;
|
int xpos = HBORDER, ypos = VBORDER;
|
||||||
WidgetArray wid;
|
WidgetArray wid;
|
||||||
string lblRom = "Select a ROM from the list" + ELLIPSIS;
|
string lblSelect = "Select a ROM from the list" + ELLIPSIS;
|
||||||
|
string lblAllFiles = "Show all files";
|
||||||
const string& lblFilter = "Filter";
|
const string& lblFilter = "Filter";
|
||||||
const string& lblAllFiles = "Show all files";
|
string lblSubDirs = "Incl. subdirectories";
|
||||||
const string& lblFound = "XXXX items found";
|
string lblFound = "12345 items found";
|
||||||
|
|
||||||
tooltip().setFont(font);
|
tooltip().setFont(font);
|
||||||
|
|
||||||
lwidth = font.getStringWidth(lblRom);
|
int lwSelect = font.getStringWidth(lblSelect);
|
||||||
lwidth2 = font.getStringWidth(lblAllFiles) + CheckboxWidget::boxSize(font);
|
int cwAllFiles = font.getStringWidth(lblAllFiles) + CheckboxWidget::prefixSize(font);
|
||||||
int lwidth3 = font.getStringWidth(lblFilter);
|
int lwFilter = font.getStringWidth(lblFilter);
|
||||||
int lwidth4 = font.getStringWidth(lblFound);
|
int cwSubDirs = font.getStringWidth(lblSubDirs) + CheckboxWidget::prefixSize(font);
|
||||||
|
int lwFound = font.getStringWidth(lblFound);
|
||||||
|
int wTotal = HBORDER * 2 + lwSelect + cwAllFiles + lwFilter + cwSubDirs + lwFound
|
||||||
|
+ EditTextWidget::calcWidth(font, "123456") + LBL_GAP * 7;
|
||||||
|
bool noSelect = false;
|
||||||
|
|
||||||
if(w < HBORDER * 2 + lwidth + lwidth2 + lwidth3 + lwidth4 + fontWidth * 6 + LBL_GAP * 8)
|
if(w < wTotal)
|
||||||
{
|
{
|
||||||
// make sure there is space for at least 6 characters in the filter field
|
// make sure there is space for at least 6 characters in the filter field
|
||||||
lblRom = "Select a ROM" + ELLIPSIS;
|
lblSelect = "Select a ROM" + ELLIPSIS;
|
||||||
lwidth = font.getStringWidth(lblRom);
|
int lwSelectShort = font.getStringWidth(lblSelect);
|
||||||
|
|
||||||
|
wTotal -= lwSelect - lwSelectShort;
|
||||||
|
lwSelect = lwSelectShort;
|
||||||
|
}
|
||||||
|
if(w < wTotal)
|
||||||
|
{
|
||||||
|
// make sure there is space for at least 6 characters in the filter field
|
||||||
|
lblSubDirs = "Subdir.";
|
||||||
|
int cwSubDirsShort = font.getStringWidth(lblSubDirs) + CheckboxWidget::prefixSize(font);
|
||||||
|
|
||||||
|
wTotal -= cwSubDirs - cwSubDirsShort;
|
||||||
|
cwSubDirs = cwSubDirsShort;
|
||||||
|
}
|
||||||
|
if(w < wTotal)
|
||||||
|
{
|
||||||
|
// make sure there is space for at least 6 characters in the filter field
|
||||||
|
lblAllFiles = "All files";
|
||||||
|
int cwAllFilesShort = font.getStringWidth(lblAllFiles) + CheckboxWidget::prefixSize(font);
|
||||||
|
|
||||||
|
wTotal -= cwAllFiles - cwAllFilesShort;
|
||||||
|
cwAllFiles = cwAllFilesShort;
|
||||||
|
}
|
||||||
|
if(w < wTotal)
|
||||||
|
{
|
||||||
|
// make sure there is space for at least 6 characters in the filter field
|
||||||
|
lblFound = "12345 found";
|
||||||
|
int lwFoundShort = font.getStringWidth(lblFound);
|
||||||
|
|
||||||
|
wTotal -= lwFound - lwFoundShort;
|
||||||
|
lwFound = lwFoundShort;
|
||||||
|
myShortCount = true;
|
||||||
|
}
|
||||||
|
if(w < wTotal)
|
||||||
|
{
|
||||||
|
// make sure there is space for at least 6 characters in the filter field
|
||||||
|
lblSelect = "";
|
||||||
|
int lwSelectShort = font.getStringWidth(lblSelect);
|
||||||
|
|
||||||
|
wTotal -= lwSelect - lwSelectShort;
|
||||||
|
lwSelect = lwSelectShort;
|
||||||
|
noSelect = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(myUseMinimalUI)
|
if(myUseMinimalUI)
|
||||||
|
@ -108,31 +158,51 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show the header
|
// Show the header
|
||||||
new StaticTextWidget(this, font, xpos, ypos, lblRom);
|
new StaticTextWidget(this, font, xpos, ypos, lblSelect);
|
||||||
// Shop the files counter
|
// Shop the files counter
|
||||||
xpos = _w - HBORDER - lwidth4;
|
xpos = _w - HBORDER - lwFound;
|
||||||
myRomCount = new StaticTextWidget(this, font, xpos, ypos,
|
myRomCount = new StaticTextWidget(this, font, xpos, ypos,
|
||||||
lwidth4, fontHeight,
|
lwFound, fontHeight,
|
||||||
"", TextAlign::Right);
|
"", TextAlign::Right);
|
||||||
|
|
||||||
// Add filter that can narrow the results shown in the listing
|
// Add filter that can narrow the results shown in the listing
|
||||||
// It has to fit between both labels
|
// It has to fit between both labels
|
||||||
if(!myUseMinimalUI && w >= 640)
|
if(!myUseMinimalUI && w >= 640)
|
||||||
{
|
{
|
||||||
int fwidth = std::min(15 * fontWidth, xpos - lwidth3 - lwidth2 - lwidth - HBORDER - LBL_GAP * 8);
|
int fwFilter = std::min(EditTextWidget::calcWidth(font, "123456789012345"),
|
||||||
|
xpos - cwSubDirs - lwFilter - cwAllFiles
|
||||||
|
- lwSelect - HBORDER - LBL_GAP * (noSelect ? 5 : 7));
|
||||||
|
|
||||||
|
// Show the subdirectories checkbox
|
||||||
|
xpos -= cwSubDirs + LBL_GAP;
|
||||||
|
mySubDirs = new CheckboxWidget(this, font, xpos, ypos, lblSubDirs, kSubDirsCmd);
|
||||||
|
mySubDirs->setEnabled(false);
|
||||||
|
ostringstream tip;
|
||||||
|
tip << "Search files in subdirectories too.\n"
|
||||||
|
<< "Filter must have at least " << MIN_SUBDIRS_CHARS << " chars.";
|
||||||
|
mySubDirs->setToolTip(tip.str());
|
||||||
|
|
||||||
// Show the filter input field
|
// Show the filter input field
|
||||||
xpos -= fwidth + LBL_GAP;
|
xpos -= fwFilter + LBL_GAP;
|
||||||
myPattern = new EditTextWidget(this, font, xpos, ypos - 2, fwidth, lineHeight, "");
|
myPattern = new EditTextWidget(this, font, xpos, ypos - 2, fwFilter, lineHeight, "");
|
||||||
myPattern->setToolTip("Enter filter text to reduce file list.");
|
myPattern->setToolTip("Enter filter text to reduce file list.\n"
|
||||||
|
"Use '*' and '?' as wildcards.");
|
||||||
|
|
||||||
// Show the "Filter" label
|
// Show the "Filter" label
|
||||||
xpos -= lwidth3 + LBL_GAP;
|
xpos -= lwFilter + LBL_GAP;
|
||||||
new StaticTextWidget(this, font, xpos, ypos, lblFilter);
|
new StaticTextWidget(this, font, xpos, ypos, lblFilter);
|
||||||
|
|
||||||
// Show the checkbox for all files
|
// Show the checkbox for all files
|
||||||
xpos -= lwidth2 + LBL_GAP * 3;
|
if(noSelect)
|
||||||
|
xpos = HBORDER;
|
||||||
|
else
|
||||||
|
xpos -= cwAllFiles + LBL_GAP * 2;
|
||||||
myAllFiles = new CheckboxWidget(this, font, xpos, ypos, lblAllFiles, kAllfilesCmd);
|
myAllFiles = new CheckboxWidget(this, font, xpos, ypos, lblAllFiles, kAllfilesCmd);
|
||||||
myAllFiles->setToolTip("Uncheck to show ROM files only.");
|
myAllFiles->setToolTip("Uncheck to show ROM files only.");
|
||||||
|
|
||||||
wid.push_back(myAllFiles);
|
wid.push_back(myAllFiles);
|
||||||
wid.push_back(myPattern);
|
wid.push_back(myPattern);
|
||||||
|
wid.push_back(mySubDirs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add list with game titles
|
// Add list with game titles
|
||||||
|
@ -167,11 +237,11 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent,
|
||||||
|
|
||||||
// Add textfield to show current directory
|
// Add textfield to show current directory
|
||||||
xpos = HBORDER;
|
xpos = HBORDER;
|
||||||
ypos += myList->getHeight() + VGAP * 2;
|
ypos += myList->getHeight() + VGAP;
|
||||||
lwidth = font.getStringWidth("Path") + LBL_GAP;
|
lwSelect = font.getStringWidth("Path") + LBL_GAP;
|
||||||
myDirLabel = new StaticTextWidget(this, font, xpos, ypos+2, lwidth, fontHeight,
|
myDirLabel = new StaticTextWidget(this, font, xpos, ypos+2, lwSelect, fontHeight,
|
||||||
"Path", TextAlign::Left);
|
"Path", TextAlign::Left);
|
||||||
xpos += lwidth;
|
xpos += lwSelect;
|
||||||
myDir = new EditTextWidget(this, font, xpos, ypos, _w - xpos - HBORDER, lineHeight, "");
|
myDir = new EditTextWidget(this, font, xpos, ypos, _w - xpos - HBORDER, lineHeight, "");
|
||||||
myDir->setEditable(false, true);
|
myDir->setEditable(false, true);
|
||||||
myDir->clearFlags(Widget::FLAG_RETAIN_FOCUS);
|
myDir->clearFlags(Widget::FLAG_RETAIN_FOCUS);
|
||||||
|
@ -236,9 +306,13 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent,
|
||||||
myGlobalProps = make_unique<GlobalPropsDialog>(this,
|
myGlobalProps = make_unique<GlobalPropsDialog>(this,
|
||||||
myUseMinimalUI ? osystem.frameBuffer().launcherFont() : osystem.frameBuffer().font());
|
myUseMinimalUI ? osystem.frameBuffer().launcherFont() : osystem.frameBuffer().font());
|
||||||
|
|
||||||
|
// since we cannot know how many files there are, use are really high value here
|
||||||
|
myList->progress().setRange(0, 50000, 5);
|
||||||
|
myList->progress().setMessage(" Filtering files" + ELLIPSIS + " ");
|
||||||
|
|
||||||
// Do we show only ROMs or all files?
|
// Do we show only ROMs or all files?
|
||||||
bool onlyROMs = instance().settings().getBool("launcherroms");
|
bool onlyROMs = instance().settings().getBool("launcherroms");
|
||||||
showOnlyROMs(onlyROMs);
|
|
||||||
if(myAllFiles)
|
if(myAllFiles)
|
||||||
myAllFiles->setState(!onlyROMs);
|
myAllFiles->setState(!onlyROMs);
|
||||||
}
|
}
|
||||||
|
@ -341,7 +415,7 @@ void LauncherDialog::updateUI()
|
||||||
|
|
||||||
// Indicate how many files were found
|
// Indicate how many files were found
|
||||||
ostringstream buf;
|
ostringstream buf;
|
||||||
buf << (myList->getList().size() - 1) << " items found";
|
buf << (myList->getList().size() - 1) << (myShortCount ? " found" : " items found");
|
||||||
myRomCount->setLabel(buf.str());
|
myRomCount->setLabel(buf.str());
|
||||||
|
|
||||||
// Update ROM info UI item
|
// Update ROM info UI item
|
||||||
|
@ -422,6 +496,7 @@ void LauncherDialog::applyFiltering()
|
||||||
{
|
{
|
||||||
myList->setNameFilter(
|
myList->setNameFilter(
|
||||||
[&](const FilesystemNode& node) {
|
[&](const FilesystemNode& node) {
|
||||||
|
myList->incProgress();
|
||||||
if(!node.isDirectory())
|
if(!node.isDirectory())
|
||||||
{
|
{
|
||||||
// Do we want to show only ROMs or all files?
|
// Do we want to show only ROMs or all files?
|
||||||
|
@ -664,6 +739,11 @@ void LauncherDialog::handleCommand(CommandSender* sender, int cmd,
|
||||||
reload();
|
reload();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case kSubDirsCmd:
|
||||||
|
myList->setIncludeSubDirs(mySubDirs->getState());
|
||||||
|
reload();
|
||||||
|
break;
|
||||||
|
|
||||||
case kLoadROMCmd:
|
case kLoadROMCmd:
|
||||||
case FileListWidget::ItemActivated:
|
case FileListWidget::ItemActivated:
|
||||||
saveConfig();
|
saveConfig();
|
||||||
|
@ -689,9 +769,15 @@ void LauncherDialog::handleCommand(CommandSender* sender, int cmd,
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EditableWidget::kChangedCmd:
|
case EditableWidget::kChangedCmd:
|
||||||
|
{
|
||||||
|
bool subAllowed = myPattern->getText().length() >= MIN_SUBDIRS_CHARS;
|
||||||
|
|
||||||
|
mySubDirs->setEnabled(subAllowed);
|
||||||
|
myList->setIncludeSubDirs(mySubDirs->getState() && subAllowed);
|
||||||
applyFiltering(); // pattern matching taken care of directly in this method
|
applyFiltering(); // pattern matching taken care of directly in this method
|
||||||
reload();
|
reload();
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case kQuitCmd:
|
case kQuitCmd:
|
||||||
saveConfig();
|
saveConfig();
|
||||||
|
|
|
@ -100,6 +100,7 @@ class LauncherDialog : public Dialog
|
||||||
static constexpr int MIN_ROMINFO_CHARS = 30;
|
static constexpr int MIN_ROMINFO_CHARS = 30;
|
||||||
static constexpr int MIN_ROMINFO_ROWS = 7; // full lines
|
static constexpr int MIN_ROMINFO_ROWS = 7; // full lines
|
||||||
static constexpr int MIN_ROMINFO_LINES = 4; // extra lines
|
static constexpr int MIN_ROMINFO_LINES = 4; // extra lines
|
||||||
|
static constexpr int MIN_SUBDIRS_CHARS = 3; // minimum filter chars for subdirectory search
|
||||||
|
|
||||||
void setPosition() override { positionAt(0); }
|
void setPosition() override { positionAt(0); }
|
||||||
void handleKeyDown(StellaKey key, StellaMod mod, bool repeated) override;
|
void handleKeyDown(StellaKey key, StellaMod mod, bool repeated) override;
|
||||||
|
@ -169,19 +170,22 @@ class LauncherDialog : public Dialog
|
||||||
// automatically sized font for ROM info viewer
|
// automatically sized font for ROM info viewer
|
||||||
unique_ptr<GUI::Font> myROMInfoFont;
|
unique_ptr<GUI::Font> myROMInfoFont;
|
||||||
|
|
||||||
ButtonWidget* myStartButton{nullptr};
|
CheckboxWidget* myAllFiles{nullptr};
|
||||||
ButtonWidget* myPrevDirButton{nullptr};
|
EditTextWidget* myPattern{nullptr};
|
||||||
ButtonWidget* myOptionsButton{nullptr};
|
CheckboxWidget* mySubDirs{nullptr};
|
||||||
ButtonWidget* myQuitButton{nullptr};
|
StaticTextWidget* myRomCount{nullptr};
|
||||||
|
|
||||||
FileListWidget* myList{nullptr};
|
FileListWidget* myList{nullptr};
|
||||||
|
|
||||||
StaticTextWidget* myDirLabel{nullptr};
|
StaticTextWidget* myDirLabel{nullptr};
|
||||||
EditTextWidget* myDir{nullptr};
|
EditTextWidget* myDir{nullptr};
|
||||||
StaticTextWidget* myRomCount{nullptr};
|
|
||||||
EditTextWidget* myPattern{nullptr};
|
|
||||||
CheckboxWidget* myAllFiles{nullptr};
|
|
||||||
|
|
||||||
RomInfoWidget* myRomInfoWidget{nullptr};
|
ButtonWidget* myStartButton{nullptr};
|
||||||
|
ButtonWidget* myPrevDirButton{nullptr};
|
||||||
|
ButtonWidget* myOptionsButton{nullptr};
|
||||||
|
ButtonWidget* myQuitButton{nullptr};
|
||||||
|
|
||||||
|
RomInfoWidget* myRomInfoWidget{nullptr};
|
||||||
std::unordered_map<string,string> myMD5List;
|
std::unordered_map<string,string> myMD5List;
|
||||||
|
|
||||||
int mySelectedItem{0};
|
int mySelectedItem{0};
|
||||||
|
@ -189,9 +193,11 @@ class LauncherDialog : public Dialog
|
||||||
bool myShowOnlyROMs{false};
|
bool myShowOnlyROMs{false};
|
||||||
bool myUseMinimalUI{false};
|
bool myUseMinimalUI{false};
|
||||||
bool myEventHandled{false};
|
bool myEventHandled{false};
|
||||||
|
bool myShortCount{false};
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
kAllfilesCmd = 'lalf', // show all files (or ROMs only)
|
kAllfilesCmd = 'lalf', // show all files (or ROMs only)
|
||||||
|
kSubDirsCmd = 'lred',
|
||||||
kPrevDirCmd = 'PRVD',
|
kPrevDirCmd = 'PRVD',
|
||||||
kOptionsCmd = 'OPTI',
|
kOptionsCmd = 'OPTI',
|
||||||
kQuitCmd = 'QUIT'
|
kQuitCmd = 'QUIT'
|
||||||
|
|
|
@ -26,8 +26,9 @@
|
||||||
|
|
||||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
ProgressDialog::ProgressDialog(GuiObject* boss, const GUI::Font& font,
|
ProgressDialog::ProgressDialog(GuiObject* boss, const GUI::Font& font,
|
||||||
const string& message)
|
const string& message, bool openDialog)
|
||||||
: Dialog(boss->instance(), boss->parent())
|
: Dialog(boss->instance(), boss->parent()),
|
||||||
|
myFont(font)
|
||||||
{
|
{
|
||||||
const int fontWidth = font.getMaxCharWidth(),
|
const int fontWidth = font.getMaxCharWidth(),
|
||||||
fontHeight = font.getFontHeight(),
|
fontHeight = font.getFontHeight(),
|
||||||
|
@ -52,13 +53,23 @@ ProgressDialog::ProgressDialog(GuiObject* boss, const GUI::Font& font,
|
||||||
mySlider->setMinValue(1);
|
mySlider->setMinValue(1);
|
||||||
mySlider->setMaxValue(100);
|
mySlider->setMaxValue(100);
|
||||||
|
|
||||||
open();
|
if(openDialog)
|
||||||
|
open();
|
||||||
}
|
}
|
||||||
|
|
||||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
void ProgressDialog::setMessage(const string& message)
|
void ProgressDialog::setMessage(const string& message)
|
||||||
{
|
{
|
||||||
|
const int fontWidth = myFont.getMaxCharWidth(),
|
||||||
|
HBORDER = fontWidth * 1.25;
|
||||||
|
const int lwidth = myFont.getStringWidth(message);
|
||||||
|
|
||||||
|
// Recalculate real dimensions
|
||||||
|
_w = HBORDER * 2 + lwidth;
|
||||||
|
|
||||||
|
myMessage->setWidth(lwidth);
|
||||||
myMessage->setLabel(message);
|
myMessage->setLabel(message);
|
||||||
|
mySlider->setWidth(lwidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
@ -68,17 +79,25 @@ void ProgressDialog::setRange(int start, int finish, int step)
|
||||||
myFinish = finish;
|
myFinish = finish;
|
||||||
myStep = int((step / 100.0) * (myFinish - myStart + 1));
|
myStep = int((step / 100.0) * (myFinish - myStart + 1));
|
||||||
|
|
||||||
mySlider->setMinValue(myStart);
|
mySlider->setMinValue(myStart + myStep);
|
||||||
mySlider->setMaxValue(myFinish);
|
mySlider->setMaxValue(myFinish);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
void ProgressDialog::resetProgress()
|
||||||
|
{
|
||||||
|
myProgress = myStepProgress = 0;
|
||||||
|
mySlider->setValue(0);
|
||||||
|
}
|
||||||
|
|
||||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
void ProgressDialog::setProgress(int progress)
|
void ProgressDialog::setProgress(int progress)
|
||||||
{
|
{
|
||||||
// Only increase the progress bar if we have arrived at a new step
|
// Only increase the progress bar if we have arrived at a new step
|
||||||
if(progress - mySlider->getValue() > myStep)
|
if(progress - myStepProgress >= myStep)
|
||||||
{
|
{
|
||||||
mySlider->setValue(progress);
|
myStepProgress = progress;
|
||||||
|
mySlider->setValue(progress % (myFinish - myStart + 1));
|
||||||
|
|
||||||
// Since this dialog is usually called in a tight loop that doesn't
|
// Since this dialog is usually called in a tight loop that doesn't
|
||||||
// yield, we need to manually tell the framebuffer that a redraw is
|
// yield, we need to manually tell the framebuffer that a redraw is
|
||||||
|
@ -88,3 +107,9 @@ void ProgressDialog::setProgress(int progress)
|
||||||
instance().frameBuffer().update();
|
instance().frameBuffer().update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
void ProgressDialog::incProgress()
|
||||||
|
{
|
||||||
|
setProgress(++myProgress);
|
||||||
|
}
|
||||||
|
|
|
@ -23,23 +23,29 @@ class StaticTextWidget;
|
||||||
class SliderWidget;
|
class SliderWidget;
|
||||||
|
|
||||||
#include "bspf.hxx"
|
#include "bspf.hxx"
|
||||||
|
#include "Dialog.hxx"
|
||||||
|
|
||||||
class ProgressDialog : public Dialog
|
class ProgressDialog : public Dialog
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ProgressDialog(GuiObject* boss, const GUI::Font& font,
|
ProgressDialog(GuiObject* boss, const GUI::Font& font,
|
||||||
const string& message);
|
const string& message, bool openDialog = true);
|
||||||
~ProgressDialog() override = default;
|
~ProgressDialog() override = default;
|
||||||
|
|
||||||
void setMessage(const string& message);
|
void setMessage(const string& message);
|
||||||
void setRange(int begin, int end, int step);
|
void setRange(int begin, int end, int step);
|
||||||
|
void resetProgress();
|
||||||
void setProgress(int progress);
|
void setProgress(int progress);
|
||||||
|
void incProgress();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
const GUI::Font& myFont;
|
||||||
StaticTextWidget* myMessage{nullptr};
|
StaticTextWidget* myMessage{nullptr};
|
||||||
SliderWidget* mySlider{nullptr};
|
SliderWidget* mySlider{nullptr};
|
||||||
|
|
||||||
int myStart{0}, myFinish{0}, myStep{0};
|
int myStart{0}, myFinish{0}, myStep{0};
|
||||||
|
int myProgress{0};
|
||||||
|
int myStepProgress{0};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Following constructors and assignment operators not supported
|
// Following constructors and assignment operators not supported
|
||||||
|
|
|
@ -179,5 +179,5 @@ void ToolTip::show(const string& tip)
|
||||||
void ToolTip::render()
|
void ToolTip::render()
|
||||||
{
|
{
|
||||||
if(myTipShown)
|
if(myTipShown)
|
||||||
mySurface->render(), cerr << " render tooltip" << endl;
|
mySurface->render(); // , cerr << " render tooltip" << endl;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue