added tracking of user favorites, recently played and most popular games

added virtual directories for selecting tracked games
extended launcher context menu and shortcuts
This commit is contained in:
Thomas Jentzsch 2021-11-28 17:33:54 +01:00
parent 083cf78797
commit 00609a3a7a
15 changed files with 1315 additions and 167 deletions

View File

@ -14,12 +14,16 @@
6.6 to 6.? 6.6 to 6.?
* Reworked the file launcher:
- Added tracking of user favorites, recently played and most popular
games.
- Added virtual directories for selecting tracked games.
- Added icons for files and directories.
- Added option to show/hide file extensions.
- Extended context menu and shortcuts.
* Added option to toggle autofire mode. * Added option to toggle autofire mode.
* Added icons to file lists.
* Added option to show/hide file extensions.
-Have fun! -Have fun!

View File

@ -2273,6 +2273,11 @@
<td>Control + H</td> <td>Control + H</td>
<td>Control + H</td> <td>Control + H</td>
</tr> </tr>
<tr>
<td>Toggle favorite</td>
<td>Control + F</td>
<td>Control + F</td>
</tr>
<tr> <tr>
<td>Reload ROM listing</td> <td>Reload ROM listing</td>
<td>Control + R</td> <td>Control + R</td>

View File

@ -157,6 +157,11 @@ Settings::Settings()
setPermanent("launcherextensions", "false"); setPermanent("launcherextensions", "false");
setPermanent("romviewer", "1"); setPermanent("romviewer", "1");
setPermanent("lastrom", ""); setPermanent("lastrom", "");
setPermanent("favoriteroms", "");
setPermanent("recentroms", "");
setPermanent("maxrecentroms", "20");
setPermanent("popularroms", "");
setPermanent("altsorting", "false");
// UI-related options // UI-related options
#ifdef DEBUGGER_SUPPORT #ifdef DEBUGGER_SUPPORT
@ -594,6 +599,8 @@ void Settings::usage() const
<< " -launcherroms <1|0> Show only ROMs in the launcher (vs. all files)\n" << " -launcherroms <1|0> Show only ROMs in the launcher (vs. all files)\n"
<< " -launchersubdirs <0|1> Show files from subdirectories too\n" << " -launchersubdirs <0|1> Show files from subdirectories too\n"
<< " -launcherextensions <0|1> Display file extensions in launcher\n" << " -launcherextensions <0|1> Display file extensions in launcher\n"
<< " -altsorting <0|1> Alternative sorting in virtual folders\n"
<< " -maxrecentroms <number> Number of ROMs tracked in 'Recently played'\n"
<< " -romdir <dir> Set the path where the ROM launcher will start\n" << " -romdir <dir> Set the path where the ROM launcher will start\n"
<< " -followlauncher <0|1> Default ROM path follows launcher navigation\n" << " -followlauncher <0|1> Default ROM path follows launcher navigation\n"
<< " -userdir <dir> Set the path to save user files to\n" << " -userdir <dir> Set the path to save user files to\n"

View File

@ -0,0 +1,264 @@
//============================================================================
//
// SSSS tt lll lll
// SS SS tt ll ll
// SS tttttt eeee ll ll aaaa
// SSSS tt ee ee ll ll aa
// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator"
// SS SS tt ee ll ll aa aa
// SSSS ttt eeeee llll llll aaaaa
//
// Copyright (c) 1995-2021 by Bradford W. Mott, Stephen Anthony
// and the Stella Team
//
// See the file "License.txt" for information on usage and redistribution of
// this file, and for a DISCLAIMER OF ALL WARRANTIES.
//============================================================================
#include "FSNode.hxx"
#include "json_lib.hxx"
#include "Settings.hxx"
#include "FavoritesManager.hxx"
using json = nlohmann::json;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FavoritesManager::FavoritesManager(Settings& settings)
: mySettings(settings)
{
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FavoritesManager::load()
{
myMaxRecent = mySettings.getInt("maxrecentroms");
// User Favorites
myUserSet.clear();
const string& serializedUser = mySettings.getString("favoriteroms");
if(!serializedUser.empty())
{
const json& jUser = json::parse(serializedUser);
for(const auto& u : jUser)
{
const string& path = u.get<string>();
addUser(path);
}
}
// Recently Played
myRecentList.clear();
const string& serializedRecent = mySettings.getString("recentroms");
if(!serializedRecent.empty())
{
const json& jRecent = json::parse(serializedRecent);
for(const auto& r : jRecent)
{
const string& path = r.get<string>();
addRecent(path);
}
}
// Most Popular
myPopularMap.clear();
const string& serializedPopular = mySettings.getString("popularroms");
if(!serializedPopular.empty())
{
const json& jPopular = json::parse(serializedPopular);
for(const auto& p : jPopular)
{
const string& path = p[0].get<string>();
const uInt32 count = p[1].get<uInt32>();
myPopularMap.emplace(path, count);
}
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FavoritesManager::save()
{
// User Favorites
json jUser = json::array();
for(const auto& path : myUserSet)
jUser.push_back(path);
mySettings.setValue("favoriteroms", jUser.dump(2));
// Recently Played
json jRecent = json::array();
for(const auto& path : myRecentList)
jRecent.push_back(path);
mySettings.setValue("recentroms", jRecent.dump(2));
// Most Popular
json jPopular = json::array();
for(const auto& path : myPopularMap)
jPopular.push_back(path);
mySettings.setValue("popularroms", jPopular.dump(2));
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FavoritesManager::addUser(const string& path)
{
myUserSet.emplace(path);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FavoritesManager::removeUser(const string& path)
{
myUserSet.erase(path);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool FavoritesManager::toggleUser(const string& path)
{
bool favorize = !existsUser(path);
if(favorize)
addUser(path);
else
removeUser(path);
return favorize;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool FavoritesManager::existsUser(const string& path) const
{
return myUserSet.find(path) != myUserSet.end();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const FavoritesManager::UserList& FavoritesManager::userList() const
{
// Return newest to oldest
static UserList sortedList;
sortedList.clear();
sortedList.assign(myUserSet.begin(), myUserSet.end());
if(!mySettings.getBool("altsorting"))
std::sort(sortedList.begin(), sortedList.end(),
[](const string& a, const string& b)
{
// Sort without path
FilesystemNode aNode(a);
FilesystemNode bNode(b);
return BSPF::compareIgnoreCase(aNode.getName(), bNode.getName()) < 0;
});
return sortedList;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FavoritesManager::update(const string& path)
{
addRecent(path);
incPopular(path);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FavoritesManager::addRecent(const string& path)
{
auto it = std::find(myRecentList.begin(), myRecentList.end(), path);
// Always remove existing before adding at the end again
if(it != myRecentList.end())
myRecentList.erase(it);
myRecentList.emplace_back(path);
// Limit size
while(myRecentList.size() > myMaxRecent)
myRecentList.erase(myRecentList.begin());
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const FavoritesManager::RecentList& FavoritesManager::recentList() const
{
static RecentList sortedList;
bool sortByName = mySettings.getBool("altsorting");
sortedList.clear();
if(sortByName)
{
sortedList.assign(myRecentList.begin(), myRecentList.end());
std::sort(sortedList.begin(), sortedList.end(),
[](const string& a, const string& b)
{
// Sort alphabetical, without path
FilesystemNode aNode(a);
FilesystemNode bNode(b);
return BSPF::compareIgnoreCase(aNode.getName(), bNode.getName()) < 0;
});
}
else
// sort newest to oldest
sortedList.assign(myRecentList.rbegin(), myRecentList.rend());
return sortedList;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FavoritesManager::incPopular(const string& path)
{
static constexpr uInt32 scale = 100;
static constexpr double factor = 0.7;
static constexpr uInt32 max_popular = scale;
static constexpr uInt32 min_popular = max_popular * factor;
auto increased = myPopularMap.find(path);
if(increased != myPopularMap.end())
increased->second += scale;
else
{
// Limit number of entries and age data
if(myPopularMap.size() >= max_popular)
{
PopularList sortedList = sortedPopularList(); // sorted by frequency!
for(auto item = sortedList.cbegin(); item != sortedList.cend(); ++item)
{
auto entry = myPopularMap.find(item->first);
if(entry != myPopularMap.end())
{
//if(item - sortedList.cbegin() <= min_popular)
if(entry->second >= scale * (1.0 - factor))
entry->second *= factor; // age data
else
myPopularMap.erase(entry); // remove least popular
}
}
}
myPopularMap.emplace(path, scale);
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const FavoritesManager::PopularList& FavoritesManager::popularList() const
{
return sortedPopularList(mySettings.getBool("altsorting"));
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const FavoritesManager::PopularList& FavoritesManager::sortedPopularList(bool sortByName) const
{
// Return most to least popular or sorted by name
static PopularList sortedList;
sortedList.clear();
sortedList.assign(myPopularMap.begin(), myPopularMap.end());
std::sort(sortedList.begin(), sortedList.end(),
[sortByName](const PopularType& a, const PopularType& b)
{
// 1. sort by most popular
if(!sortByName && a.second != b.second)
return a.second > b.second;
// 2. Sort alphabetical, without path
FilesystemNode aNode(a.first);
FilesystemNode bNode(b.first);
return BSPF::compareIgnoreCase(aNode.getName(), bNode.getName()) < 0;
});
return sortedList;
}

View File

@ -0,0 +1,88 @@
//============================================================================
//
// SSSS tt lll lll
// SS SS tt ll ll
// SS tttttt eeee ll ll aaaa
// SSSS tt ee ee ll ll aa
// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator"
// SS SS tt ee ll ll aa aa
// SSSS ttt eeeee llll llll aaaaa
//
// Copyright (c) 1995-2021 by Bradford W. Mott, Stephen Anthony
// and the Stella Team
//
// See the file "License.txt" for information on usage and redistribution of
// this file, and for a DISCLAIMER OF ALL WARRANTIES.
//============================================================================
#ifndef FAVORITES_MANAGER_HXX
#define FAVORITES_MANAGER_HXX
class Settings;
#include "bspf.hxx"
#include <map>
#include <unordered_set>
/**
Manages user defined favorites, recently played ROMs and most popular ROMs.
@author Thomas Jentzsch
*/
class FavoritesManager
{
public:
using UserList = std::vector<string>;
using RecentList = std::vector<string>;
using PopularType = std::pair<string, uInt32>;
using PopularList = std::vector<PopularType>;
FavoritesManager::FavoritesManager(Settings& settings);
void load();
void save();
// User favorites
void addUser(const string& path);
void removeUser(const string& path);
bool toggleUser(const string& path);
bool existsUser(const string& path) const;
const UserList& userList() const;
void update(const string& path);
// Recently played
const RecentList& recentList() const;
// Most popular
const PopularList& popularList() const;
private:
using PopularMap = std::map<string, uInt32>;
using UserSet = std::unordered_set<string>;
UserSet myUserSet;
RecentList myRecentList;
PopularMap myPopularMap;
uInt32 myMaxRecent{10};
Settings& mySettings;
private:
void addRecent(const string& path);
void incPopular(const string& path);
const PopularList& sortedPopularList(bool sortByName = false) const;
private:
// Following constructors and assignment operators not supported
FavoritesManager() = delete;
FavoritesManager(const FavoritesManager&) = delete;
FavoritesManager(FavoritesManager&&) = delete;
FavoritesManager& operator=(const FavoritesManager&) = delete;
FavoritesManager& operator=(FavoritesManager&&) = delete;
};
#endif

View File

@ -43,7 +43,7 @@ void FileListWidget::setDirectory(const FilesystemNode& node,
_node = node; _node = node;
// We always want a directory listing // We always want a directory listing
if(!_node.isDirectory() && _node.hasParent()) if(!isDirectory(_node) && _node.hasParent())
{ {
_selectedFile = _node.getName(); _selectedFile = _node.getName();
_node = _node.getParent(); _node = _node.getParent();
@ -83,6 +83,67 @@ void FileListWidget::setLocation(const FilesystemNode& 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();
getChildren(isCancelled);
// Now fill the list widget with the names from the file list,
// even if cancelled
StringList list;
size_t orgLen = _node.getShortPath().length();
_dirList.clear();
_iconTypeList.clear();
for(const auto& file : _fileList)
{
const string& path = file.getShortPath();
const string& name = file.getName();
const string& displayName = _showFileExtensions ? name : file.getNameWithExt(EmptyString);
// display only relative path in tooltip
if(path.length() >= orgLen && !fullPathToolTip())
_dirList.push_back(path.substr(orgLen));
else
_dirList.push_back(path);
if(file.isDirectory())
{
if(BSPF::endsWithIgnoreCase(name, ".zip"))
{
list.push_back(displayName);
_iconTypeList.push_back(IconType::zip);
}
else
{
list.push_back(name);
if(name == "..")
_iconTypeList.push_back(IconType::updir);
else
_iconTypeList.push_back(IconType::directory);
}
}
else
{
list.push_back(displayName);
_iconTypeList.push_back(romIconType(file));
}
}
extendLists(list);
setList(list);
setSelected(select);
ListWidget::recalc();
progress().close();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool FileListWidget::isDirectory(const FilesystemNode& node) const
{
return node.isDirectory();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FileListWidget::getChildren(const FilesystemNode::CancelCheck& isCancelled)
{
if(_includeSubDirs) if(_includeSubDirs)
{ {
// Actually this could become HUGE // Actually this could become HUGE
@ -94,53 +155,15 @@ void FileListWidget::setLocation(const FilesystemNode& node,
_fileList.reserve(0x200); _fileList.reserve(0x200);
_node.getChildren(_fileList, _fsmode, _filter, false, true, isCancelled); _node.getChildren(_fileList, _fsmode, _filter, false, true, isCancelled);
} }
}
// Now fill the list widget with the names from the file list, // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// even if cancelled FileListWidget::IconType FileListWidget::romIconType(const FilesystemNode& file) const
StringList l; {
size_t orgLen = _node.getShortPath().length(); if(file.isFile() && Bankswitch::isValidRomName(file.getName()))
return IconType::rom;
_dirList.clear(); else
_iconList.clear(); return IconType::unknown;
for(const auto& file : _fileList)
{
const string& path = file.getShortPath();
const string& name = file.getName();
const string& displayName = _showFileExtensions ? name : file.getNameWithExt(EmptyString);
// display only relative path in tooltip
if(path.length() >= orgLen)
_dirList.push_back(path.substr(orgLen));
else
_dirList.push_back(path);
if(file.isDirectory())
{
if(BSPF::endsWithIgnoreCase(name, ".zip"))
{
l.push_back(displayName);
_iconList.push_back(IconType::zip);
}
else
{
l.push_back(name);
_iconList.push_back(IconType::directory);
}
}
else
{
l.push_back(displayName);
if(file.isFile() && Bankswitch::isValidRomName(name))
_iconList.push_back(IconType::rom);
else
_iconList.push_back(IconType::unknown);
}
}
setList(l);
setSelected(select);
ListWidget::recalc();
progress().close();
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -160,12 +183,16 @@ void FileListWidget::selectParent()
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FileListWidget::reload() void FileListWidget::reload()
{ {
if(_node.exists()) if(isDirectory(_node))
{ {
if(_showFileExtensions || selected().isDirectory()) _selectedFile = selected().getName();
_selectedFile = selected().getName(); setLocation(_node, _selectedFile);
else }
_selectedFile = selected().getNameWithExt(EmptyString); else if(_node.exists())
{
_selectedFile = _showFileExtensions
? selected().getName()
: selected().getNameWithExt(EmptyString);
setLocation(_node, _selectedFile); setLocation(_node, _selectedFile);
} }
} }
@ -205,7 +232,10 @@ bool FileListWidget::handleText(char text)
if(BSPF::startsWithIgnoreCase(i, _quickSelectStr)) if(BSPF::startsWithIgnoreCase(i, _quickSelectStr))
// Select directories when the first character is uppercase // Select directories when the first character is uppercase
if((std::isupper(_quickSelectStr[0]) != 0) == if((std::isupper(_quickSelectStr[0]) != 0) ==
(_iconList[selectedItem] == IconType::directory)) (_iconTypeList[selectedItem] == IconType::directory
|| _iconTypeList[selectedItem] == IconType::favdir
|| _iconTypeList[selectedItem] == IconType::recentdir
|| _iconTypeList[selectedItem] == IconType::popdir))
break; break;
selectedItem++; selectedItem++;
} }
@ -233,7 +263,7 @@ void FileListWidget::handleCommand(CommandSender* sender, int cmd, int data, int
case ListWidget::kActivatedCmd: case ListWidget::kActivatedCmd:
case ListWidget::kDoubleClickedCmd: case ListWidget::kDoubleClickedCmd:
_selected = data; _selected = data;
if(selected().isDirectory()) if(isDirectory(selected())/* || !selected().exists()*/)
{ {
if(selected().getName() == "..") if(selected().getName() == "..")
selectParent(); selectParent();
@ -269,6 +299,22 @@ void FileListWidget::handleCommand(CommandSender* sender, int cmd, int data, int
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
int FileListWidget::drawIcon(int i, int x, int y, ColorId color) int FileListWidget::drawIcon(int i, int x, int y, ColorId color)
{ {
const bool smallIcon = iconWidth() < 24;
const Icon* icon = getIcon(i);
const int iconGap = smallIcon ? 2 : 3;
FBSurface& s = _boss->dialog().surface();
s.drawBitmap(icon->data(), x + 1 + iconGap,
y + (_lineHeight - static_cast<int>(icon->size())) / 2,
color, iconWidth() - iconGap * 2, static_cast<int>(icon->size()));
return iconWidth();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const FileListWidget::Icon* FileListWidget::getIcon(int i) const
{
#if 1
static const Icon unknown_small = { static const Icon unknown_small = {
0b00111111'1100000, 0b00111111'1100000,
0b00100000'0110000, 0b00100000'0110000,
@ -342,8 +388,38 @@ int FileListWidget::drawIcon(int i, int x, int y, ColorId color)
0b10001111'1110001, 0b10001111'1110001,
0b10000000'0000001, 0b10000000'0000001,
0b11111111'1111111 0b11111111'1111111
}; };
static const Icon up_small = {
//0b00000100'0000000,
//0b00001110'0000000,
//0b00011111'0000000,
//0b00111111'1000000,
//0b01111111'1100000,
//0b11111111'1110000,
//0b00001110'0000000,
//0b00001110'0000000,
//0b00001110'0000000,
//0b00001111'0000000,
//0b00001111'1111111,
//0b00000111'1111111,
//0b00000011'1111111,
//0b00000000'0000000,
0b11111000'0000000,
0b11111100'0000000,
0b11111111'1111111,
0b10000000'0000001,
0b10000011'1000001,
0b10000111'1100001,
0b10001111'1110001,
0b10011111'1111001,
0b10000011'1000001,
0b10000011'1000001,
0b10000011'1000001,
0b10000011'1000001,
0b11111111'1111111
};
static const Icon unknown_large = { static const Icon unknown_large = {
0b00111'11111111'11000000, 0b00111'11111111'11000000,
0b00111'11111111'11100000, 0b00111'11111111'11100000,
@ -420,52 +496,59 @@ int FileListWidget::drawIcon(int i, int x, int y, ColorId color)
0b111111'11111111'1111111, 0b111111'11111111'1111111,
0b111111'11111111'1111111, 0b111111'11111111'1111111,
0b110000'00000000'0000011, 0b110000'00000000'0000011,
0b110000'00000000'0000011, 0b110001'11111111'1100011,
0b110000'11111111'1000011, 0b110001'11111111'1100011,
0b110000'11111111'1000011, 0b110001'11111111'1100011,
0b110000'00000011'0000011, 0b110000'00000111'1000011,
0b110000'00000110'0000011, 0b110000'00001111'0000011,
0b110000'00001100'0000011, 0b110000'00011110'0000011,
0b110000'00011000'0000011, 0b110000'00111100'0000011,
0b110000'00110000'0000011, 0b110000'01111000'0000011,
0b110000'01100000'0000011, 0b110000'11110000'0000011,
0b110000'11111111'1000011, 0b110001'11111111'1100011,
0b110000'11111111'1000011, 0b110001'11111111'1100011,
0b110000'00000000'0000011, 0b110001'11111111'1100011,
0b110000'00000000'0000011, 0b110000'00000000'0000011,
0b111111'11111111'1111111, 0b111111'11111111'1111111,
0b111111'11111111'1111111 0b111111'11111111'1111111
}; };
static const Icon up_large = {
0b111111'10000000'0000000,
0b111111'11000000'0000000,
0b111111'11100000'0000000,
0b111111'11111111'1111111,
0b111111'11111111'1111111,
0b110000'00000000'0000011,
0b110000'00011100'0000011,
0b110000'00111110'0000011,
0b110000'01111111'0000011,
0b110000'11111111'1000011,
0b110001'11111111'1100011,
0b110011'11111111'1110011,
0b110011'11111111'1110011,
0b110000'00111110'0000011,
0b110000'00111110'0000011,
0b110000'00111110'0000011,
0b110000'00111110'0000011,
0b110000'00111110'0000011,
0b110000'00000000'0000011,
0b111111'11111111'1111111,
0b111111'11111111'1111111
};
#endif
static const Icon* small_icons[int(IconType::numTypes)] = {
&unknown_small, &rom_small, &directory_small, &zip_small, &up_small
};
static const Icon* large_icons[int(IconType::numTypes)] = {
&unknown_large, &rom_large, &directory_large, &zip_large, &up_large,
};
const bool smallIcon = iconWidth() < 24; const bool smallIcon = iconWidth() < 24;
const int iconGap = smallIcon ? 2 : 3; const int iconType = int(_iconTypeList[i]);
const Icon* icon{nullptr}; assert(iconType < 5);
switch(_iconList[i])
{
case IconType::rom:
icon = smallIcon ? &rom_small: &rom_large;
break;
case IconType::directory: return smallIcon ? small_icons[iconType] : large_icons[iconType];
icon = smallIcon ? &directory_small : &directory_large;
break;
case IconType::zip:
icon = smallIcon ? &zip_small : &zip_large;
break;
default:
icon = smallIcon ? &unknown_small : &unknown_large;
break;
}
FBSurface& s = _boss->dialog().surface();
s.drawBitmap(icon->data(), x + 1 + iconGap,
y + (_lineHeight - static_cast<int>(icon->size())) / 2,
color, iconWidth() - iconGap * 2, static_cast<int>(icon->size()));
return iconWidth();
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

View File

@ -47,6 +47,7 @@ class FileListWidget : public StringListWidget
ItemChanged = 'FLic', // Entry in the list is changed (single-click, etc) ItemChanged = 'FLic', // Entry in the list is changed (single-click, etc)
ItemActivated = 'FLac' // Entry in the list is activated (double-click, etc) ItemActivated = 'FLac' // Entry in the list is activated (double-click, etc)
}; };
using IconTypeFilter = std::function<bool(const FilesystemNode& node)>;
public: public:
FileListWidget(GuiObject* boss, const GUI::Font& font, FileListWidget(GuiObject* boss, const GUI::Font& font,
@ -99,16 +100,39 @@ class FileListWidget : public StringListWidget
ProgressDialog& progress(); ProgressDialog& progress();
void incProgress(); void incProgress();
private: protected:
enum class IconType { enum class IconType {
unknown, unknown,
rom, rom,
directory, directory,
zip zip,
updir,
numTypes,
favorite = numTypes,
favdir,
recentdir,
popdir,
numLauncherTypes = popdir - numTypes + 1
}; };
using IconTypeList = std::vector<IconType>; using IconTypeList = std::vector<IconType>;
using Icon = uIntArray; using Icon = uIntArray;
protected:
virtual bool isDirectory(const FilesystemNode& node) const;
virtual void getChildren(const FilesystemNode::CancelCheck& isCancelled);
virtual void extendLists(StringList& list) { };
virtual IconType romIconType(const FilesystemNode& file) const;
virtual const Icon* getIcon(int i) const;
int iconWidth() const;
virtual bool fullPathToolTip() const { return false; }
protected:
FilesystemNode _node;
FSList _fileList;
FilesystemNode::NameFilter _filter;
StringList _dirList;
IconTypeList _iconTypeList;
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);
@ -116,19 +140,12 @@ class FileListWidget : public StringListWidget
bool handleText(char text) override; bool handleText(char text) override;
void handleCommand(CommandSender* sender, int cmd, int data, int id) override; void handleCommand(CommandSender* sender, int cmd, int data, int id) override;
int drawIcon(int i, int x, int y, ColorId color) override; int drawIcon(int i, int x, int y, ColorId color) override;
int iconWidth() const;
private: private:
FilesystemNode::ListMode _fsmode{FilesystemNode::ListMode::All}; FilesystemNode::ListMode _fsmode{FilesystemNode::ListMode::All};
FilesystemNode::NameFilter _filter;
FilesystemNode _node;
FSList _fileList;
bool _includeSubDirs{false}; bool _includeSubDirs{false};
bool _showFileExtensions{true}; bool _showFileExtensions{true};
StringList _dirList;
IconTypeList _iconList;
Common::FixedStack<string> _history; Common::FixedStack<string> _history;
uInt32 _selected{0}; uInt32 _selected{0};
string _selectedFile; string _selectedFile;

View File

@ -23,6 +23,7 @@
#include "Dialog.hxx" #include "Dialog.hxx"
#include "EditTextWidget.hxx" #include "EditTextWidget.hxx"
#include "FileListWidget.hxx" #include "FileListWidget.hxx"
#include "LauncherFileListWidget.hxx"
#include "FSNode.hxx" #include "FSNode.hxx"
#include "MD5.hxx" #include "MD5.hxx"
#include "OptionsDialog.hxx" #include "OptionsDialog.hxx"
@ -211,9 +212,10 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent,
if(romWidth > 0) romWidth += HBORDER; if(romWidth > 0) romWidth += HBORDER;
int listWidth = _w - (romWidth > 0 ? romWidth + fontWidth : 0) - HBORDER * 2; int listWidth = _w - (romWidth > 0 ? romWidth + fontWidth : 0) - HBORDER * 2;
xpos = HBORDER; ypos += lineHeight + VGAP; xpos = HBORDER; ypos += lineHeight + VGAP;
myList = new FileListWidget(this, _font, xpos, ypos, listWidth, listHeight); myList = new LauncherFileListWidget(this, _font, xpos, ypos, listWidth, listHeight);
myList->setEditable(false); myList->setEditable(false);
myList->setListMode(FilesystemNode::ListMode::All); myList->setListMode(FilesystemNode::ListMode::All);
wid.push_back(myList); wid.push_back(myList);
// Add ROM info area (if enabled) // Add ROM info area (if enabled)
@ -301,10 +303,11 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent,
myList->progress().setMessage(" Filtering files" + ELLIPSIS + " "); 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"); myShowOnlyROMs = instance().settings().getBool("launcherroms");
showOnlyROMs(onlyROMs); //showOnlyROMs(onlyROMs);
if(myAllFiles) if(myAllFiles)
myAllFiles->setState(!onlyROMs); myAllFiles->setState(!myShowOnlyROMs);
applyFiltering();
setHelpAnchor("ROMInfo"); setHelpAnchor("ROMInfo");
} }
@ -348,11 +351,9 @@ const FilesystemNode& LauncherDialog::currentDir() const
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::reload() void LauncherDialog::reload()
{ {
bool subDirs = instance().settings().getBool("launchersubdirs");
bool extensions = instance().settings().getBool("launcherextensions"); bool extensions = instance().settings().getBool("launcherextensions");
myMD5List.clear(); myMD5List.clear();
myList->setIncludeSubDirs(subDirs);
myList->setShowFileExtensions(extensions); myList->setShowFileExtensions(extensions);
myList->reload(); myList->reload();
myPendingReload = false; myPendingReload = false;
@ -372,34 +373,37 @@ void LauncherDialog::loadConfig()
{ {
// Should we use a temporary directory specified on the commandline, or the // Should we use a temporary directory specified on the commandline, or the
// default one specified by the settings? // default one specified by the settings?
const string& tmpromdir = instance().settings().getString("tmpromdir"); Settings& settings = instance().settings();
const string& tmpromdir = settings.getString("tmpromdir");
const string& romdir = tmpromdir != "" ? tmpromdir : const string& romdir = tmpromdir != "" ? tmpromdir :
instance().settings().getString("romdir"); settings.getString("romdir");
const string& version = instance().settings().getString("stella.version"); const string& version = settings.getString("stella.version");
// Show "What's New" message when a new version of Stella is run for the first time // Show "What's New" message when a new version of Stella is run for the first time
if(version != STELLA_VERSION) if(version != STELLA_VERSION)
{ {
openWhatsNew(); openWhatsNew();
instance().settings().setValue("stella.version", STELLA_VERSION); settings.setValue("stella.version", STELLA_VERSION);
} }
bool subDirs = instance().settings().getBool("launchersubdirs"); bool subDirs = settings.getBool("launchersubdirs");
bool extensions = instance().settings().getBool("launcherextensions"); bool extensions = settings.getBool("launcherextensions");
if (mySubDirs) mySubDirs->setState(subDirs); if (mySubDirs) mySubDirs->setState(subDirs);
myList->setIncludeSubDirs(subDirs); myList->setIncludeSubDirs(subDirs);
myList->setShowFileExtensions(extensions); myList->setShowFileExtensions(extensions);
// Favorites
myList->loadFavorites();
// Assume that if the list is empty, this is the first time that loadConfig() // Assume that if the list is empty, this is the first time that loadConfig()
// has been called (and we should reload the list) // has been called (and we should reload the list)
if(myList->getList().empty()) if(myList->getList().empty())
{ {
FilesystemNode node(romdir == "" ? "~" : romdir); FilesystemNode node(romdir == "" ? "~" : romdir);
if(!(node.exists() && node.isDirectory())) if(!myList->isDirectory(node))
node = FilesystemNode("~"); node = FilesystemNode("~");
myList->setDirectory(node, instance().settings().getString("lastrom")); myList->setDirectory(node, settings.getString("lastrom"));
updateUI(); updateUI();
} }
Dialog::setFocus(getFocusList()[mySelectedItem]); Dialog::setFocus(getFocusList()[mySelectedItem]);
@ -413,10 +417,15 @@ void LauncherDialog::loadConfig()
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::saveConfig() void LauncherDialog::saveConfig()
{ {
if (mySubDirs) Settings& settings = instance().settings();
instance().settings().setValue("launchersubdirs", mySubDirs->getState());
if(instance().settings().getBool("followlauncher")) if(mySubDirs)
instance().settings().setValue("romdir", myList->currentDir().getShortPath()); settings.setValue("launchersubdirs", mySubDirs->getState());
if(settings.getBool("followlauncher"))
settings.setValue("romdir", myList->currentDir().getShortPath());
// Favorites
myList->saveFavorites();
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -617,25 +626,36 @@ void LauncherDialog::loadRomInfo()
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::handleContextMenu() void LauncherDialog::handleContextMenu()
{ {
const string& cmd = menu().getSelectedTag().toString(); const string& cmd = contextMenu().getSelectedTag().toString();
if(cmd == "override") if(cmd == "favorite")
myList->toggleUserFavorite();
else if(cmd == "override")
openGlobalProps(); openGlobalProps();
else if(cmd == "extensions")
toggleExtensions();
else if(cmd == "sorting")
toggleSorting();
else if(cmd == "showall")
toggleShowAll();
else if(cmd == "subdirs")
toggleSubDirs();
else if(cmd == "reload") else if(cmd == "reload")
reload(); reload();
else if(cmd == "highscores") else if(cmd == "highscores")
openHighScores(); openHighScores();
else if(cmd == "options")
openSettings();
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ContextMenu& LauncherDialog::menu() ContextMenu& LauncherDialog::contextMenu()
{ {
if(myMenu == nullptr) if(myContextMenu == nullptr)
// Create (empty) context menu for ROM list options // Create (empty) context menu for ROM list options
myMenu = make_unique<ContextMenu>(this, _font, EmptyVarList); myContextMenu = make_unique<ContextMenu>(this, _font, EmptyVarList);
return *myContextMenu;
return *myMenu;
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -658,8 +678,16 @@ void LauncherDialog::handleKeyDown(StellaKey key, StellaMod mod, bool repeated)
handled = true; handled = true;
switch(key) switch(key)
{ {
case KBDK_P: case KBDK_A:
openGlobalProps(); toggleShowAll();
break;
case KBDK_D:
toggleSubDirs();
break;
case KBDK_F:
myList->toggleUserFavorite();
break; break;
case KBDK_H: case KBDK_H:
@ -667,19 +695,25 @@ void LauncherDialog::handleKeyDown(StellaKey key, StellaMod mod, bool repeated)
openHighScores(); openHighScores();
break; break;
case KBDK_O:
openSettings();
break;
case KBDK_P:
openGlobalProps();
break;
case KBDK_R: case KBDK_R:
reload(); reload();
break; break;
case KBDK_X: case KBDK_S:
{ toggleSorting();
bool extensions = !instance().settings().getBool("launcherextensions"); break;
instance().settings().setValue("launcherextensions", extensions); case KBDK_X:
myList->setShowFileExtensions(extensions); toggleExtensions();
reload();
break; break;
}
default: default:
handled = false; handled = false;
@ -736,7 +770,7 @@ void LauncherDialog::handleJoyUp(int stick, int button)
if (button == 1 && (e == Event::UIOK || e == Event::NoType) && if (button == 1 && (e == Event::UIOK || e == Event::NoType) &&
!currentNode().isDirectory() && Bankswitch::isValidRomName(currentNode())) !currentNode().isDirectory() && Bankswitch::isValidRomName(currentNode()))
openGlobalProps(); openGlobalProps();
if (button == 3 && (e == Event::Event::UITabPrev || e == Event::NoType)) if (button == 3 && (e == Event::UITabPrev || e == Event::NoType))
openSettings(); openSettings();
else if (!myEventHandled) else if (!myEventHandled)
Dialog::handleJoyUp(stick, button); Dialog::handleJoyUp(stick, button);
@ -779,18 +813,7 @@ void LauncherDialog::handleMouseDown(int x, int y, MouseButton b, int clickCount
&& x + getAbsX() >= myList->getLeft() && x + getAbsX() <= myList->getRight() && x + getAbsX() >= myList->getLeft() && x + getAbsX() <= myList->getRight()
&& y + getAbsY() >= myList->getTop() && y + getAbsY() <= myList->getBottom()) && y + getAbsY() >= myList->getTop() && y + getAbsY() <= myList->getBottom())
{ {
// Dynamically create context menu for ROM list options openContextMenu(x, y);
VariantList items;
if(!currentNode().isDirectory() && Bankswitch::isValidRomName(currentNode()))
VarList::push_back(items, " Power-on options" + ELLIPSIS + " Ctrl+P", "override");
if(instance().highScores().enabled())
VarList::push_back(items, " High scores" + ELLIPSIS + " Ctrl+H", "highscores");
VarList::push_back(items, " Reload listing Ctrl+R ", "reload");
menu().addItems(items);
// Add menu at current x,y mouse location
menu().show(x + getAbsX(), y + getAbsY(), surface().dstRect());
} }
else else
Dialog::handleMouseDown(x, y, b, clickCount); Dialog::handleMouseDown(x, y, b, clickCount);
@ -813,7 +836,7 @@ void LauncherDialog::handleCommand(CommandSender* sender, int cmd,
break; break;
case kLoadROMCmd: case kLoadROMCmd:
if(myList->selected().isDirectory()) if(myList->isDirectory(myList->selected()))
{ {
if(myList->selected().getName() == "..") if(myList->selected().getName() == "..")
myList->selectParent(); myList->selectParent();
@ -823,6 +846,9 @@ void LauncherDialog::handleCommand(CommandSender* sender, int cmd,
} }
[[fallthrough]]; [[fallthrough]];
case FileListWidget::ItemActivated: case FileListWidget::ItemActivated:
// Assumes that the ROM will be loaded successfully, has to be done
// before saving the config.
myList->updateFavorites();
saveConfig(); saveConfig();
loadRom(); loadRom();
break; break;
@ -840,8 +866,8 @@ void LauncherDialog::handleCommand(CommandSender* sender, int cmd,
break; break;
case ListWidget::kLongButtonPressCmd: case ListWidget::kLongButtonPressCmd:
if (!currentNode().isDirectory() && Bankswitch::isValidRomName(currentNode())) if(!currentNode().isDirectory() && Bankswitch::isValidRomName(currentNode()))
openGlobalProps(); openContextMenu();
myEventHandled = true; myEventHandled = true;
break; break;
@ -878,7 +904,7 @@ void LauncherDialog::handleCommand(CommandSender* sender, int cmd,
{ {
FilesystemNode node(romDir); FilesystemNode node(romDir);
if(!(node.exists() && node.isDirectory())) if(!myList->isDirectory(node))
node = FilesystemNode("~"); node = FilesystemNode("~");
myList->setDirectory(node); myList->setDirectory(node);
@ -930,6 +956,117 @@ void LauncherDialog::setDefaultDir()
instance().settings().setValue("romdir", myList->currentDir().getShortPath()); instance().settings().setValue("romdir", myList->currentDir().getShortPath());
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::toggleShowAll()
{
myShowOnlyROMs = !instance().settings().getBool("launcherroms");
instance().settings().setValue("launcherroms", myShowOnlyROMs);
if(myAllFiles)
myAllFiles->setState(!myShowOnlyROMs);
applyFiltering();
reload();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::toggleSubDirs()
{
bool subdirs = !instance().settings().getBool("launchersubdirs");
instance().settings().setValue("launchersubdirs", subdirs);
if(mySubDirs)
mySubDirs->setState(subdirs);
myList->setIncludeSubDirs(subdirs);
reload();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::toggleExtensions()
{
bool extensions = !instance().settings().getBool("launcherextensions");
instance().settings().setValue("launcherextensions", extensions);
myList->setShowFileExtensions(extensions);
reload();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::toggleSorting()
{
if(myList->inVirtualDir())
{
// Toggle between normal and alternative sorting of virtual directories
bool altSorting = !instance().settings().getBool("altsorting");
instance().settings().setValue("altsorting", altSorting);
reload();
}
}
void LauncherDialog::addContextItem(VariantList& items, const string& label,
const string& shortcut, const string& key)
{
const string pad = " ";
if(myUseMinimalUI)
VarList::push_back(items, " " + label + " ", key);
else
VarList::push_back(items, " " + label + pad.substr(0, 24 - label.length())
+ shortcut + " ", key);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::openContextMenu(int x, int y)
{
if(x < 0 || y < 0)
{
// Determine position from currently selected list item
x = myList->getLeft() + myList->getWidth() / 2;
y = myList->getTop() + (myList->getSelected() - myList->currentPos() + 1) * _font.getLineHeight();
}
// Dynamically create context menu for ROM list options
VariantList items;
// TODO: remove subdirs and show all from GUI
if(!currentNode().isDirectory() && Bankswitch::isValidRomName(currentNode()))
{
addContextItem(items, myList->isUserFavorite(myList->selected().getPath())
? "Remove from favorites"
: "Add to favorites", "Ctrl+F", "favorite");
addContextItem(items, "Power-on options" + ELLIPSIS, "Ctrl+P", "override");
if(instance().highScores().enabled())
addContextItem(items, "High scores" + ELLIPSIS, "Ctrl+H", "highscores");
}
if(myUseMinimalUI)
addContextItem(items, "Options" + ELLIPSIS, "Ctrl+O", "options");
else
{
addContextItem(items, instance().settings().getBool("launcherextensions")
? "Disable file extensions"
: "Enable file extensions", "Ctrl+X", "extensions");
if(myList->inVirtualDir())
addContextItem(items, instance().settings().getBool("altsorting")
? "Normal sorting"
: "Alternative sorting", "Ctrl+S", "sorting");
else
{
addContextItem(items, instance().settings().getBool("launcherroms")
? "Show all files"
: "Show only ROMs", "Ctrl+A", "showall");
addContextItem(items, instance().settings().getBool("launchersubdirs")
? "Exclude subdirectories"
: "Include subdirectories", "Ctrl+D", "subdirs");
}
addContextItem(items, "Reload listing", "Ctrl+R", "reload");
}
contextMenu().addItems(items);
// Add menu at current x,y mouse location
contextMenu().show(x + getAbsX(), y + getAbsY(), surface().dstRect());
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherDialog::openGlobalProps() void LauncherDialog::openGlobalProps()
{ {

View File

@ -25,9 +25,10 @@ class DialogContainer;
class OSystem; class OSystem;
class Properties; class Properties;
class EditTextWidget; class EditTextWidget;
class FileListWidget; class LauncherFileListWidget;
class RomInfoWidget; class RomInfoWidget;
class StaticTextWidget; class StaticTextWidget;
namespace Common { namespace Common {
struct Size; struct Size;
} }
@ -36,10 +37,12 @@ namespace GUI {
} }
#include <unordered_map> #include <unordered_map>
#include <unordered_set>
#include "bspf.hxx" #include "bspf.hxx"
#include "Dialog.hxx" #include "Dialog.hxx"
#include "FSNode.hxx" #include "FSNode.hxx"
#include "Variant.hxx"
class LauncherDialog : public Dialog class LauncherDialog : public Dialog
{ {
@ -50,6 +53,7 @@ class LauncherDialog : public Dialog
kRomDirChosenCmd = 'romc', // ROM dir chosen kRomDirChosenCmd = 'romc', // ROM dir chosen
kExtChangedCmd = 'extc' // File extension display changed kExtChangedCmd = 'extc' // File extension display changed
}; };
using FileList = std::unordered_set<string>;
public: public:
LauncherDialog(OSystem& osystem, DialogContainer& parent, LauncherDialog(OSystem& osystem, DialogContainer& parent,
@ -153,16 +157,23 @@ class LauncherDialog : public Dialog
void handleContextMenu(); void handleContextMenu();
void showOnlyROMs(bool state); void showOnlyROMs(bool state);
void setDefaultDir(); void setDefaultDir();
void toggleShowAll();
void toggleSubDirs();
void toggleExtensions();
void toggleSorting();
void addContextItem(VariantList& items, const string& label,
const string& shortcut, const string& key);
void openContextMenu(int x = -1, int y = -1);
void openGlobalProps(); void openGlobalProps();
void openSettings(); void openSettings();
void openHighScores(); void openHighScores();
void openWhatsNew(); void openWhatsNew();
ContextMenu& menu(); ContextMenu& contextMenu();
private: private:
unique_ptr<Dialog> myDialog; unique_ptr<Dialog> myDialog;
unique_ptr<ContextMenu> myMenu; unique_ptr<ContextMenu> myContextMenu;
// automatically sized font for ROM info viewer // automatically sized font for ROM info viewer
unique_ptr<GUI::Font> myROMInfoFont; unique_ptr<GUI::Font> myROMInfoFont;
@ -172,7 +183,7 @@ class LauncherDialog : public Dialog
CheckboxWidget* mySubDirs{nullptr}; CheckboxWidget* mySubDirs{nullptr};
StaticTextWidget* myRomCount{nullptr}; StaticTextWidget* myRomCount{nullptr};
FileListWidget* myList{nullptr}; LauncherFileListWidget* myList{nullptr};
StaticTextWidget* myDirLabel{nullptr}; StaticTextWidget* myDirLabel{nullptr};
EditTextWidget* myDir{nullptr}; EditTextWidget* myDir{nullptr};
@ -182,8 +193,8 @@ class LauncherDialog : public Dialog
ButtonWidget* myOptionsButton{nullptr}; ButtonWidget* myOptionsButton{nullptr};
ButtonWidget* myQuitButton{nullptr}; ButtonWidget* myQuitButton{nullptr};
// FIXME - NOT USED StaticTextWidget* myRomLink{nullptr};
RomInfoWidget* myRomInfoWidget{nullptr}; RomInfoWidget* myRomInfoWidget{nullptr};
std::unordered_map<string,string> myMD5List; std::unordered_map<string,string> myMD5List;
int mySelectedItem{0}; int mySelectedItem{0};

View File

@ -0,0 +1,445 @@
//============================================================================
//
// SSSS tt lll lll
// SS SS tt ll ll
// SS tttttt eeee ll ll aaaa
// SSSS tt ee ee ll ll aa
// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator"
// SS SS tt ee ll ll aa aa
// SSSS ttt eeeee llll llll aaaaa
//
// Copyright (c) 1995-2021 by Bradford W. Mott, Stephen Anthony
// and the Stella Team
//
// See the file "License.txt" for information on usage and redistribution of
// this file, and for a DISCLAIMER OF ALL WARRANTIES.
//============================================================================
#include "Bankswitch.hxx"
#include "FavoritesManager.hxx"
#include "OSystem.hxx"
#include "ProgressDialog.hxx"
#include "Settings.hxx"
#include "LauncherFileListWidget.hxx"
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
LauncherFileListWidget::LauncherFileListWidget(GuiObject* boss, const GUI::Font& font,
int x, int y, int w, int h)
: FileListWidget(boss, font, x, y, w, h)
{
// This widget is special, in that it catches signals and redirects them
setTarget(this);
myFavorites = make_unique<FavoritesManager>(instance().settings());
myRomDir = instance().settings().getString("romdir");
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool LauncherFileListWidget::isDirectory(const FilesystemNode& node) const
{
bool isDir = node.isDirectory();
// Check for virtual directories
if(!isDir && !node.exists())
return node.getName() == user_name
|| node.getName() == recent_name
|| node.getName() == popular_name;
return isDir;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherFileListWidget::getChildren(const FilesystemNode::CancelCheck& isCancelled)
{
// TODO:
// + remove virtual folders in virtual folders
// + tooltips (incl. subdirs)
// + always add (after remove) recent ROMs
// + age recently played (e.g. reduce all regularly, WHEN? HOW MUCH?)
// + mark virtual dir when returning from it
// + "lastrom"
// + uppercase search
// + change sort order
// + move subdirs & all files into popup menu
// + no all files option in virtual folders
// + missing large icons
// + Settings.cxx doc
// + display only in ROM path folder
// - remove subdirs & all files from GUI
// - doc (settings, hotkeys, popup, launcher, virtual folders)
if(_node.exists() || !_node.hasParent())
{
myInVirtualDir = false;
FileListWidget::getChildren(isCancelled);
}
else
{
myInVirtualDir = true;
FilesystemNode parent(_node.getParent());
parent.setName("..");
_fileList.emplace_back(parent);
const string& name = _node.getName();
if(name == user_name)
{
for(auto& item : myFavorites->userList())
{
FilesystemNode node(item);
if(_filter(node))
_fileList.emplace_back(node);
}
}
else if(name == popular_name)
{
for(auto& item : myFavorites->popularList())
{
FilesystemNode node(item.first);
if(_filter(node))
_fileList.emplace_back(node);
}
}
else if(name == recent_name)
{
for(auto& item : myFavorites->recentList())
{
FilesystemNode node(item);
if(_filter(node))
_fileList.emplace_back(node);
}
}
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherFileListWidget::addFolder(StringList& list, int& offset, const string& name, IconType icon)
{
_fileList.insert(_fileList.begin() + offset,
FilesystemNode(_node.getPath() + name));
list.insert(list.begin() + offset, name);
_dirList.insert(_dirList.begin() + offset, "");
_iconTypeList.insert((_iconTypeList.begin() + offset), icon);
++offset;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherFileListWidget::extendLists(StringList& list)
{
// Only show virtual dirs in "romdir". Except if
// "romdir" is virtual or "romdir" is a ZIP
// Then show virtual dirs in parent dir of "romdir".
if(myRomDir == instance().settings().getString("romdir")
&& (myInVirtualDir || BSPF::endsWithIgnoreCase(_node.getPath(), ".zip")))
myRomDir = _node.getParent().getPath();
if(_node.getPath() == myRomDir)
{
// Add virtual directories behind ".."
int offset = _fileList.begin()->getName() == ".." ? 1 : 0;
if(myFavorites->userList().size())
addFolder(list, offset, user_name, IconType::favdir);
if(myFavorites->popularList().size())
addFolder(list, offset, popular_name, IconType::popdir);
if(myFavorites->recentList().size())
addFolder(list, offset, recent_name, IconType::recentdir);
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherFileListWidget::loadFavorites()
{
myFavorites->load();
for(const auto& path : myFavorites->userList())
userFavor(path);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherFileListWidget::saveFavorites()
{
myFavorites->save();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherFileListWidget::updateFavorites()
{
myFavorites->update(selected().getPath());
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool LauncherFileListWidget::isUserFavorite(const string& path) const
{
return myFavorites->existsUser(path);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherFileListWidget::toggleUserFavorite()
{
if(!selected().isDirectory() && Bankswitch::isValidRomName(selected()))
{
bool isUserFavorite = myFavorites->toggleUser(selected().getPath());
userFavor(selected().getPath(), isUserFavorite);
// Redraw file list
setDirty();
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void LauncherFileListWidget::userFavor(const string& path, bool isUserFavorite)
{
int pos = 0;
for(const auto& file : _fileList)
{
if(file.getPath() == path)
break;
pos++;
}
if(pos < _iconTypeList.size())
_iconTypeList[pos] = isUserFavorite ? IconType::favorite : IconType::rom;
}
FileListWidget::IconType LauncherFileListWidget::romIconType(const FilesystemNode& file) const
{
if(file.isFile() && Bankswitch::isValidRomName(file.getName()))
return isUserFavorite(file.getPath()) ? IconType::favorite : IconType::rom;
else
return IconType::unknown;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const FileListWidget::Icon* LauncherFileListWidget::getIcon(int i) const
{
static const Icon favorite_small = {
//0b0000001'11000000,
//0b0000011'01100000,
//0b0000010'00100000,
//0b0000110'00110000,
//0b0000100'00010000,
//0b1111100'00011111,
//0b1000000'00000001,
//0b1100000'00000011,
//0b0110000'00000110,
//0b0011000'00001100,
//0b0010000'00000100,
//0b0110000'10000110,
//0b0100011'01100010,
//0b0101110'00111010,
//0b0111000'00001110,
0b0000001'10000000,
0b0000011'11000000,
0b0000010'01000000,
0b0000110'01100000,
0b0111100'00111100,
0b1100000'00000110,
0b0110000'00001100,
0b0011000'00011000,
0b0001100'00110000,
0b0011000'00011000,
0b0010001'10001000,
0b0110111'11101100,
0b0111100'00111100,
0b0011000'00011000,
};
static const Icon favdir_small = {
//0b11111000'0000000,
//0b11111100'0000000,
//0b11111111'1111111,
//0b10000000'0000001,
//0b10000001'0000001,
//0b10000011'1000001,
//0b10001111'1110001,
//0b10000111'1100001,
//0b10000011'1000001,
//0b10000111'1100001,
//0b10001100'0110001,
//0b10000000'0000001,
//0b11111111'1111111
0b11111000'0000000,
0b11111100'0000000,
0b11111101'0111111,
0b10000011'1000001,
0b10000011'1000001,
0b10000111'1100001,
0b10111111'1111101,
0b10011111'1111001,
0b10001111'1110001,
0b10000111'1100001,
0b10001111'1110001,
0b10011100'0111001,
0b11011000'0011011
};
static const Icon recent_small = {
0b11111000'0000000,
0b11111100'0000000,
0b11111111'1111111,
0b10000011'1000001,
0b10001110'1110001,
0b10001110'1110001,
0b10011110'1111001,
0b10011110'0111001,
0b10011111'0011001,
0b10001111'1110001,
0b10001111'1110001,
0b10000011'1000001,
0b11111111'1111111
};
static const Icon popular_small = {
0b11111000'0000000,
0b11111100'0000000,
0b11111111'1111111,
0b10000000'0000001,
0b10001100'0110001,
0b10011110'1111001,
0b10011111'1111001,
0b10011111'1111001,
0b10001111'1110001,
0b10000111'1100001,
0b10000011'1000001,
0b10000001'0000001,
0b11111111'1111111
};
static const Icon favorite_large = {
//0b0000000'0001000'0000000,
//0b0000000'0011100'0000000,
//0b0000000'0011100'0000000,
//0b0000000'0110110'0000000,
//0b0000000'0110110'0000000,
//0b0000000'0110110'0000000,
//0b0000000'1100011'0000000,
//0b0111111'1000001'1111110,
//0b1111111'0000000'1111111,
//0b0110000'0000000'0000110,
//0b0011000'0000000'0001100,
//0b0001100'0000000'0011000,
//0b0000110'0000000'0110000,
//0b0000011'0000000'1100000,
//0b0000111'0000000'1110000,
//0b0000110'0001000'0110000,
//0b0001100'0011100'0011000,
//0b0001100'1110111'0011000,
//0b0011001'1000001'1001100,
//0b0011111'0000000'1111100,
//0b0001100'0000000'0011000
0b0000000'0001000'0000000,
0b0000000'0011100'0000000,
0b0000000'0011100'0000000,
0b0000000'0111110'0000000,
0b0000000'0111110'0000000,
0b0000000'0110110'0000000,
0b0000000'1100011'0000000,
0b0111111'1100011'1111110,
0b1111111'1000001'1111111,
0b0111000'0000000'0001110,
0b0011100'0000000'0011100,
0b0001110'0000000'0111000,
0b0000111'0000000'1110000,
0b0000011'1000001'1100000,
0b0000111'0000000'1110000,
0b0000111'0011100'1110000,
0b0001110'0111110'0111000,
0b0001100'1110111'0011000,
0b0011111'1000001'1111100,
0b0011111'0000000'1111100,
0b0001100'0000000'0011000
};
static const Icon favdir_large = {
0b111111'10000000'0000000,
0b111111'11000000'0000000,
0b111111'11100000'0000000,
0b111111'11111111'1111111,
0b111111'11111111'1111111,
0b110000'00001000'0000011,
0b110000'00011100'0000011,
0b110000'00011100'0000011,
0b110000'00111110'0000011,
0b110001'11111111'1100011,
0b110011'11111111'1110011,
0b110001'11111111'1100011,
0b110000'11111111'1000011,
0b110000'01111111'0000011,
0b110000'11111111'1000011,
0b110001'11110111'1100011,
0b110001'11100011'1100011,
0b110000'11000001'1000011,
0b110000'00000000'0000011,
0b111111'11111111'1111111,
0b111111'11111111'1111111
};
static const Icon recent_large = {
0b111111'10000000'0000000,
0b111111'11000000'0000000,
0b111111'11100000'0000000,
0b111111'11111111'1111111,
0b111111'11111111'1111111,
0b110000'00000000'0000011,
0b110000'00111110'0000011,
0b110000'11110111'1000011,
0b110001'11110111'1100011,
0b110001'11110111'1100011,
0b110011'11110111'1110011,
0b110011'11110111'1110011,
0b110011'11110011'1110011,
0b110011'11111001'1110011,
0b110001'11111100'1100011,
0b110001'11111111'1100011,
0b110000'11111111'1000011,
0b110000'00111110'0000011,
0b110000'00000000'0000011,
0b111111'11111111'1111111,
0b111111'11111111'1111111
};
static const Icon popular_large = {
0b111111'10000000'0000000,
0b111111'11000000'0000000,
0b111111'11100000'0000000,
0b111111'11111111'1111111,
0b111111'11111111'1111111,
0b110000'00000000'0000011,
0b110000'00000000'0000011,
0b110000'11100011'1000011,
0b110001'11110111'1100011,
0b110011'11111111'1110011,
0b110011'11111111'1110011,
0b110011'11111111'1110011,
0b110001'11111111'1100011,
0b110000'11111111'1000011,
0b110000'01111111'0000011,
0b110000'00111110'0000011,
0b110000'00011100'0000011,
0b110000'00001000'0000011,
0b110000'00000000'0000011,
0b111111'11111111'1111111,
0b111111'11111111'1111111
};
static const Icon* small_icons[int(IconType::numLauncherTypes)] = {
&favorite_small, &favdir_small, &recent_small, &popular_small
};
static const Icon* large_icons[int(IconType::numLauncherTypes)] = {
&favorite_large, &favdir_large, &recent_large, &popular_large
};
if(int(_iconTypeList[i]) < int(IconType::numTypes))
return FileListWidget::getIcon(i);
const bool smallIcon = iconWidth() < 24;
const int iconType = int(_iconTypeList[i]) - int(IconType::numTypes);
assert(iconType < int(IconType::numLauncherTypes));
return smallIcon ? small_icons[iconType] : large_icons[iconType];
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const string LauncherFileListWidget::user_name = "Favorites";
const string LauncherFileListWidget::recent_name = "Recently Played";
const string LauncherFileListWidget::popular_name = "Most Popular";

View File

@ -0,0 +1,70 @@
//============================================================================
//
// SSSS tt lll lll
// SS SS tt ll ll
// SS tttttt eeee ll ll aaaa
// SSSS tt ee ee ll ll aa
// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator"
// SS SS tt ee ll ll aa aa
// SSSS ttt eeeee llll llll aaaaa
//
// Copyright (c) 1995-2021 by Bradford W. Mott, Stephen Anthony
// and the Stella Team
//
// See the file "License.txt" for information on usage and redistribution of
// this file, and for a DISCLAIMER OF ALL WARRANTIES.
//============================================================================
#ifndef LAUNCHER_FILE_LIST_WIDGET_HXX
#define LAUNCHER_FILE_LIST_WIDGET_HXX
class FavoritesManager;
class FilesystemNode;
class ProgressDialog;
class Settings;
#include "FileListWidget.hxx"
/**
Specialization of the general FileListWidget which procides support for
user defined favorites, recently played ROMs and most popular ROMs.
@author Thomas Jentzsch
*/
class LauncherFileListWidget : public FileListWidget
{
public:
LauncherFileListWidget(GuiObject* boss, const GUI::Font& font,
int x, int y, int w, int h);
~LauncherFileListWidget() override = default;
void loadFavorites();
void saveFavorites();
void updateFavorites();
bool isUserFavorite(const string& path) const;
void toggleUserFavorite();
bool isDirectory(const FilesystemNode& node) const override;
bool inVirtualDir() const { return myInVirtualDir; }
private:
static const string user_name;
static const string recent_name;
static const string popular_name;
unique_ptr<FavoritesManager> myFavorites;
bool myInVirtualDir{false};
string myRomDir;
private:
void getChildren(const FilesystemNode::CancelCheck& isCancelled) override;
void userFavor(const string& path, bool enable = true);
void addFolder(StringList& list, int& offset, const string& name, IconType icon);
void extendLists(StringList& list) override;
IconType romIconType(const FilesystemNode& file) const override;
const Icon* getIcon(int i) const override;
bool fullPathToolTip() const override { return myInVirtualDir; }
};
#endif

View File

@ -18,7 +18,6 @@
#include "bspf.hxx" #include "bspf.hxx"
#include "Dialog.hxx" #include "Dialog.hxx"
#include "FBSurface.hxx" #include "FBSurface.hxx"
#include "Settings.hxx"
#include "ScrollBarWidget.hxx" #include "ScrollBarWidget.hxx"
#include "StringListWidget.hxx" #include "StringListWidget.hxx"

View File

@ -16,6 +16,7 @@ MODULE_OBJS := \
src/gui/EditTextWidget.o \ src/gui/EditTextWidget.o \
src/gui/EmulationDialog.o \ src/gui/EmulationDialog.o \
src/gui/EventMappingWidget.o \ src/gui/EventMappingWidget.o \
src/gui/FavoritesManager.o \
src/gui/FileListWidget.o \ src/gui/FileListWidget.o \
src/gui/Font.o \ src/gui/Font.o \
src/gui/GameInfoDialog.o \ src/gui/GameInfoDialog.o \
@ -27,6 +28,7 @@ MODULE_OBJS := \
src/gui/InputTextDialog.o \ src/gui/InputTextDialog.o \
src/gui/JoystickDialog.o \ src/gui/JoystickDialog.o \
src/gui/LauncherDialog.o \ src/gui/LauncherDialog.o \
src/gui/LauncherFileListWidget.o \
src/gui/Launcher.o \ src/gui/Launcher.o \
src/gui/ListWidget.o \ src/gui/ListWidget.o \
src/gui/LoggerDialog.o \ src/gui/LoggerDialog.o \

View File

@ -907,9 +907,11 @@
<ClCompile Include="..\gui\ColorWidget.cxx" /> <ClCompile Include="..\gui\ColorWidget.cxx" />
<ClCompile Include="..\gui\DeveloperDialog.cxx" /> <ClCompile Include="..\gui\DeveloperDialog.cxx" />
<ClCompile Include="..\gui\EmulationDialog.cxx" /> <ClCompile Include="..\gui\EmulationDialog.cxx" />
<ClCompile Include="..\gui\FavoritesManager.cxx" />
<ClCompile Include="..\gui\FileListWidget.cxx" /> <ClCompile Include="..\gui\FileListWidget.cxx" />
<ClCompile Include="..\gui\HighScoresDialog.cxx" /> <ClCompile Include="..\gui\HighScoresDialog.cxx" />
<ClCompile Include="..\gui\HighScoresMenu.cxx" /> <ClCompile Include="..\gui\HighScoresMenu.cxx" />
<ClCompile Include="..\gui\LauncherFileListWidget.cxx" />
<ClCompile Include="..\gui\PlusRomsMenu.cxx" /> <ClCompile Include="..\gui\PlusRomsMenu.cxx" />
<ClCompile Include="..\gui\JoystickDialog.cxx" /> <ClCompile Include="..\gui\JoystickDialog.cxx" />
<ClCompile Include="..\gui\LoggerDialog.cxx" /> <ClCompile Include="..\gui\LoggerDialog.cxx" />
@ -2111,9 +2113,11 @@
<ClInclude Include="..\gui\ConsoleMediumFont.hxx" /> <ClInclude Include="..\gui\ConsoleMediumFont.hxx" />
<ClInclude Include="..\gui\DeveloperDialog.hxx" /> <ClInclude Include="..\gui\DeveloperDialog.hxx" />
<ClInclude Include="..\gui\EmulationDialog.hxx" /> <ClInclude Include="..\gui\EmulationDialog.hxx" />
<ClInclude Include="..\gui\FavoritesManager.hxx" />
<ClInclude Include="..\gui\FileListWidget.hxx" /> <ClInclude Include="..\gui\FileListWidget.hxx" />
<ClInclude Include="..\gui\HighScoresDialog.hxx" /> <ClInclude Include="..\gui\HighScoresDialog.hxx" />
<ClInclude Include="..\gui\HighScoresMenu.hxx" /> <ClInclude Include="..\gui\HighScoresMenu.hxx" />
<ClInclude Include="..\gui\LauncherFileListWidget.hxx" />
<ClInclude Include="..\gui\PlusRomsMenu.hxx" /> <ClInclude Include="..\gui\PlusRomsMenu.hxx" />
<ClInclude Include="..\gui\JoystickDialog.hxx" /> <ClInclude Include="..\gui\JoystickDialog.hxx" />
<ClInclude Include="..\gui\LoggerDialog.hxx" /> <ClInclude Include="..\gui\LoggerDialog.hxx" />

View File

@ -1119,6 +1119,12 @@
<ClCompile Include="..\common\DevSettingsHandler.cxx"> <ClCompile Include="..\common\DevSettingsHandler.cxx">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="..\gui\LauncherFileListWidget.cxx">
<Filter>Source Files\gui</Filter>
</ClCompile>
<ClCompile Include="..\gui\FavoritesManager.cxx">
<Filter>Source Files\gui</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="..\common\bspf.hxx"> <ClInclude Include="..\common\bspf.hxx">
@ -2300,6 +2306,12 @@
<ClInclude Include="..\common\DevSettingsHandler.hxx"> <ClInclude Include="..\common\DevSettingsHandler.hxx">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="..\gui\LauncherFileListWidget.hxx">
<Filter>Header Files\gui</Filter>
</ClInclude>
<ClInclude Include="..\gui\FavoritesManager.hxx">
<Filter>Header Files\gui</Filter>
</ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="stella.ico"> <None Include="stella.ico">