diff --git a/Changes.txt b/Changes.txt index 82fb3b6db..bd42bb88b 100644 --- a/Changes.txt +++ b/Changes.txt @@ -14,12 +14,16 @@ 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 icons to file lists. - - * Added option to show/hide file extensions. - -Have fun! diff --git a/docs/index.html b/docs/index.html index 387746583..805c130f5 100644 --- a/docs/index.html +++ b/docs/index.html @@ -2273,6 +2273,11 @@ Control + H Control + H + + Toggle favorite + Control + F + Control + F + Reload ROM listing Control + R diff --git a/src/emucore/Settings.cxx b/src/emucore/Settings.cxx index dca0364fc..d3ee3583a 100644 --- a/src/emucore/Settings.cxx +++ b/src/emucore/Settings.cxx @@ -157,6 +157,11 @@ Settings::Settings() setPermanent("launcherextensions", "false"); setPermanent("romviewer", "1"); setPermanent("lastrom", ""); + setPermanent("favoriteroms", ""); + setPermanent("recentroms", ""); + setPermanent("maxrecentroms", "20"); + setPermanent("popularroms", ""); + setPermanent("altsorting", "false"); // UI-related options #ifdef DEBUGGER_SUPPORT @@ -594,6 +599,8 @@ void Settings::usage() const << " -launcherroms <1|0> Show only ROMs in the launcher (vs. all files)\n" << " -launchersubdirs <0|1> Show files from subdirectories too\n" << " -launcherextensions <0|1> Display file extensions in launcher\n" + << " -altsorting <0|1> Alternative sorting in virtual folders\n" + << " -maxrecentroms Number of ROMs tracked in 'Recently played'\n" << " -romdir Set the path where the ROM launcher will start\n" << " -followlauncher <0|1> Default ROM path follows launcher navigation\n" << " -userdir Set the path to save user files to\n" diff --git a/src/gui/FavoritesManager.cxx b/src/gui/FavoritesManager.cxx new file mode 100644 index 000000000..36ba57581 --- /dev/null +++ b/src/gui/FavoritesManager.cxx @@ -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(); + 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(); + 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(); + const uInt32 count = p[1].get(); + 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; +} \ No newline at end of file diff --git a/src/gui/FavoritesManager.hxx b/src/gui/FavoritesManager.hxx new file mode 100644 index 000000000..13c020446 --- /dev/null +++ b/src/gui/FavoritesManager.hxx @@ -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 +#include + +/** + Manages user defined favorites, recently played ROMs and most popular ROMs. + + @author Thomas Jentzsch +*/ + +class FavoritesManager +{ + public: + using UserList = std::vector; + using RecentList = std::vector; + using PopularType = std::pair; + using PopularList = std::vector; + + 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; + using UserSet = std::unordered_set; + + 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 \ No newline at end of file diff --git a/src/gui/FileListWidget.cxx b/src/gui/FileListWidget.cxx index a991c4c5a..23916139d 100644 --- a/src/gui/FileListWidget.cxx +++ b/src/gui/FileListWidget.cxx @@ -43,7 +43,7 @@ void FileListWidget::setDirectory(const FilesystemNode& node, _node = node; // We always want a directory listing - if(!_node.isDirectory() && _node.hasParent()) + if(!isDirectory(_node) && _node.hasParent()) { _selectedFile = _node.getName(); _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) _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) { // Actually this could become HUGE @@ -94,53 +155,15 @@ void FileListWidget::setLocation(const FilesystemNode& node, _fileList.reserve(0x200); _node.getChildren(_fileList, _fsmode, _filter, false, true, isCancelled); } +} - // Now fill the list widget with the names from the file list, - // even if cancelled - StringList l; - size_t orgLen = _node.getShortPath().length(); - - _dirList.clear(); - _iconList.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) - _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(); +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +FileListWidget::IconType FileListWidget::romIconType(const FilesystemNode& file) const +{ + if(file.isFile() && Bankswitch::isValidRomName(file.getName())) + return IconType::rom; + else + return IconType::unknown; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -160,12 +183,16 @@ void FileListWidget::selectParent() // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void FileListWidget::reload() { - if(_node.exists()) + if(isDirectory(_node)) { - if(_showFileExtensions || selected().isDirectory()) - _selectedFile = selected().getName(); - else - _selectedFile = selected().getNameWithExt(EmptyString); + _selectedFile = selected().getName(); + setLocation(_node, _selectedFile); + } + else if(_node.exists()) + { + _selectedFile = _showFileExtensions + ? selected().getName() + : selected().getNameWithExt(EmptyString); setLocation(_node, _selectedFile); } } @@ -205,7 +232,10 @@ bool FileListWidget::handleText(char text) if(BSPF::startsWithIgnoreCase(i, _quickSelectStr)) // Select directories when the first character is uppercase 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; selectedItem++; } @@ -233,7 +263,7 @@ void FileListWidget::handleCommand(CommandSender* sender, int cmd, int data, int case ListWidget::kActivatedCmd: case ListWidget::kDoubleClickedCmd: _selected = data; - if(selected().isDirectory()) + if(isDirectory(selected())/* || !selected().exists()*/) { if(selected().getName() == "..") 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) { + 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(icon->size())) / 2, + color, iconWidth() - iconGap * 2, static_cast(icon->size())); + + return iconWidth(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +const FileListWidget::Icon* FileListWidget::getIcon(int i) const +{ +#if 1 static const Icon unknown_small = { 0b00111111'1100000, 0b00100000'0110000, @@ -342,8 +388,38 @@ int FileListWidget::drawIcon(int i, int x, int y, ColorId color) 0b10001111'1110001, 0b10000000'0000001, 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 = { 0b00111'11111111'11000000, 0b00111'11111111'11100000, @@ -420,52 +496,59 @@ int FileListWidget::drawIcon(int i, int x, int y, ColorId color) 0b111111'11111111'1111111, 0b111111'11111111'1111111, 0b110000'00000000'0000011, - 0b110000'00000000'0000011, - 0b110000'11111111'1000011, - 0b110000'11111111'1000011, - 0b110000'00000011'0000011, - 0b110000'00000110'0000011, - 0b110000'00001100'0000011, - 0b110000'00011000'0000011, - 0b110000'00110000'0000011, - 0b110000'01100000'0000011, - 0b110000'11111111'1000011, - 0b110000'11111111'1000011, - 0b110000'00000000'0000011, + 0b110001'11111111'1100011, + 0b110001'11111111'1100011, + 0b110001'11111111'1100011, + 0b110000'00000111'1000011, + 0b110000'00001111'0000011, + 0b110000'00011110'0000011, + 0b110000'00111100'0000011, + 0b110000'01111000'0000011, + 0b110000'11110000'0000011, + 0b110001'11111111'1100011, + 0b110001'11111111'1100011, + 0b110001'11111111'1100011, 0b110000'00000000'0000011, 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 int iconGap = smallIcon ? 2 : 3; + const int iconType = int(_iconTypeList[i]); - const Icon* icon{nullptr}; - switch(_iconList[i]) - { - case IconType::rom: - icon = smallIcon ? &rom_small: &rom_large; - break; + assert(iconType < 5); - case IconType::directory: - 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(icon->size())) / 2, - color, iconWidth() - iconGap * 2, static_cast(icon->size())); - - return iconWidth(); + return smallIcon ? small_icons[iconType] : large_icons[iconType]; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/gui/FileListWidget.hxx b/src/gui/FileListWidget.hxx index 17bdc4d9a..d64e95606 100644 --- a/src/gui/FileListWidget.hxx +++ b/src/gui/FileListWidget.hxx @@ -47,6 +47,7 @@ class FileListWidget : public StringListWidget ItemChanged = 'FLic', // Entry in the list is changed (single-click, etc) ItemActivated = 'FLac' // Entry in the list is activated (double-click, etc) }; + using IconTypeFilter = std::function; public: FileListWidget(GuiObject* boss, const GUI::Font& font, @@ -99,16 +100,39 @@ class FileListWidget : public StringListWidget ProgressDialog& progress(); void incProgress(); - private: + protected: enum class IconType { unknown, rom, directory, - zip + zip, + updir, + numTypes, + favorite = numTypes, + favdir, + recentdir, + popdir, + numLauncherTypes = popdir - numTypes + 1 }; using IconTypeList = std::vector; 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: /** Very similar to setDirectory(), but also updates the history */ void setLocation(const FilesystemNode& node, const string& select); @@ -116,19 +140,12 @@ class FileListWidget : public StringListWidget bool handleText(char text) override; void handleCommand(CommandSender* sender, int cmd, int data, int id) override; int drawIcon(int i, int x, int y, ColorId color) override; - int iconWidth() const; private: FilesystemNode::ListMode _fsmode{FilesystemNode::ListMode::All}; - FilesystemNode::NameFilter _filter; - FilesystemNode _node; - FSList _fileList; bool _includeSubDirs{false}; bool _showFileExtensions{true}; - StringList _dirList; - IconTypeList _iconList; - Common::FixedStack _history; uInt32 _selected{0}; string _selectedFile; diff --git a/src/gui/LauncherDialog.cxx b/src/gui/LauncherDialog.cxx index 21408b87c..7aacad097 100644 --- a/src/gui/LauncherDialog.cxx +++ b/src/gui/LauncherDialog.cxx @@ -23,6 +23,7 @@ #include "Dialog.hxx" #include "EditTextWidget.hxx" #include "FileListWidget.hxx" +#include "LauncherFileListWidget.hxx" #include "FSNode.hxx" #include "MD5.hxx" #include "OptionsDialog.hxx" @@ -211,9 +212,10 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent, if(romWidth > 0) romWidth += HBORDER; int listWidth = _w - (romWidth > 0 ? romWidth + fontWidth : 0) - HBORDER * 2; 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->setListMode(FilesystemNode::ListMode::All); + wid.push_back(myList); // Add ROM info area (if enabled) @@ -301,10 +303,11 @@ LauncherDialog::LauncherDialog(OSystem& osystem, DialogContainer& parent, myList->progress().setMessage(" Filtering files" + ELLIPSIS + " "); // Do we show only ROMs or all files? - bool onlyROMs = instance().settings().getBool("launcherroms"); - showOnlyROMs(onlyROMs); + myShowOnlyROMs = instance().settings().getBool("launcherroms"); + //showOnlyROMs(onlyROMs); if(myAllFiles) - myAllFiles->setState(!onlyROMs); + myAllFiles->setState(!myShowOnlyROMs); + applyFiltering(); setHelpAnchor("ROMInfo"); } @@ -348,11 +351,9 @@ const FilesystemNode& LauncherDialog::currentDir() const // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void LauncherDialog::reload() { - bool subDirs = instance().settings().getBool("launchersubdirs"); bool extensions = instance().settings().getBool("launcherextensions"); myMD5List.clear(); - myList->setIncludeSubDirs(subDirs); myList->setShowFileExtensions(extensions); myList->reload(); myPendingReload = false; @@ -372,34 +373,37 @@ void LauncherDialog::loadConfig() { // Should we use a temporary directory specified on the commandline, or the // 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 : - instance().settings().getString("romdir"); - const string& version = instance().settings().getString("stella.version"); + settings.getString("romdir"); + const string& version = settings.getString("stella.version"); // Show "What's New" message when a new version of Stella is run for the first time if(version != STELLA_VERSION) { openWhatsNew(); - instance().settings().setValue("stella.version", STELLA_VERSION); + settings.setValue("stella.version", STELLA_VERSION); } - bool subDirs = instance().settings().getBool("launchersubdirs"); - bool extensions = instance().settings().getBool("launcherextensions"); + bool subDirs = settings.getBool("launchersubdirs"); + bool extensions = settings.getBool("launcherextensions"); if (mySubDirs) mySubDirs->setState(subDirs); myList->setIncludeSubDirs(subDirs); myList->setShowFileExtensions(extensions); + // Favorites + myList->loadFavorites(); // Assume that if the list is empty, this is the first time that loadConfig() // has been called (and we should reload the list) if(myList->getList().empty()) { FilesystemNode node(romdir == "" ? "~" : romdir); - if(!(node.exists() && node.isDirectory())) + if(!myList->isDirectory(node)) node = FilesystemNode("~"); - myList->setDirectory(node, instance().settings().getString("lastrom")); + myList->setDirectory(node, settings.getString("lastrom")); updateUI(); } Dialog::setFocus(getFocusList()[mySelectedItem]); @@ -413,10 +417,15 @@ void LauncherDialog::loadConfig() // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void LauncherDialog::saveConfig() { - if (mySubDirs) - instance().settings().setValue("launchersubdirs", mySubDirs->getState()); - if(instance().settings().getBool("followlauncher")) - instance().settings().setValue("romdir", myList->currentDir().getShortPath()); + Settings& settings = instance().settings(); + + if(mySubDirs) + 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() { - 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(); + else if(cmd == "extensions") + toggleExtensions(); + else if(cmd == "sorting") + toggleSorting(); + else if(cmd == "showall") + toggleShowAll(); + else if(cmd == "subdirs") + toggleSubDirs(); else if(cmd == "reload") reload(); else if(cmd == "highscores") 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 - myMenu = make_unique(this, _font, EmptyVarList); + myContextMenu = make_unique(this, _font, EmptyVarList); - - return *myMenu; + return *myContextMenu; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -658,8 +678,16 @@ void LauncherDialog::handleKeyDown(StellaKey key, StellaMod mod, bool repeated) handled = true; switch(key) { - case KBDK_P: - openGlobalProps(); + case KBDK_A: + toggleShowAll(); + break; + + case KBDK_D: + toggleSubDirs(); + break; + + case KBDK_F: + myList->toggleUserFavorite(); break; case KBDK_H: @@ -667,19 +695,25 @@ void LauncherDialog::handleKeyDown(StellaKey key, StellaMod mod, bool repeated) openHighScores(); break; + case KBDK_O: + openSettings(); + break; + + case KBDK_P: + openGlobalProps(); + break; + case KBDK_R: reload(); break; - case KBDK_X: - { - bool extensions = !instance().settings().getBool("launcherextensions"); - - instance().settings().setValue("launcherextensions", extensions); - myList->setShowFileExtensions(extensions); - reload(); + case KBDK_S: + toggleSorting(); + break; + + case KBDK_X: + toggleExtensions(); break; - } default: handled = false; @@ -736,7 +770,7 @@ void LauncherDialog::handleJoyUp(int stick, int button) if (button == 1 && (e == Event::UIOK || e == Event::NoType) && !currentNode().isDirectory() && Bankswitch::isValidRomName(currentNode())) openGlobalProps(); - if (button == 3 && (e == Event::Event::UITabPrev || e == Event::NoType)) + if (button == 3 && (e == Event::UITabPrev || e == Event::NoType)) openSettings(); else if (!myEventHandled) 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() && y + getAbsY() >= myList->getTop() && y + getAbsY() <= myList->getBottom()) { - // Dynamically create context menu for ROM list options - 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()); + openContextMenu(x, y); } else Dialog::handleMouseDown(x, y, b, clickCount); @@ -813,7 +836,7 @@ void LauncherDialog::handleCommand(CommandSender* sender, int cmd, break; case kLoadROMCmd: - if(myList->selected().isDirectory()) + if(myList->isDirectory(myList->selected())) { if(myList->selected().getName() == "..") myList->selectParent(); @@ -823,6 +846,9 @@ void LauncherDialog::handleCommand(CommandSender* sender, int cmd, } [[fallthrough]]; case FileListWidget::ItemActivated: + // Assumes that the ROM will be loaded successfully, has to be done + // before saving the config. + myList->updateFavorites(); saveConfig(); loadRom(); break; @@ -840,8 +866,8 @@ void LauncherDialog::handleCommand(CommandSender* sender, int cmd, break; case ListWidget::kLongButtonPressCmd: - if (!currentNode().isDirectory() && Bankswitch::isValidRomName(currentNode())) - openGlobalProps(); + if(!currentNode().isDirectory() && Bankswitch::isValidRomName(currentNode())) + openContextMenu(); myEventHandled = true; break; @@ -878,7 +904,7 @@ void LauncherDialog::handleCommand(CommandSender* sender, int cmd, { FilesystemNode node(romDir); - if(!(node.exists() && node.isDirectory())) + if(!myList->isDirectory(node)) node = FilesystemNode("~"); myList->setDirectory(node); @@ -930,6 +956,117 @@ void LauncherDialog::setDefaultDir() 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() { diff --git a/src/gui/LauncherDialog.hxx b/src/gui/LauncherDialog.hxx index 55a439731..21142a5f7 100644 --- a/src/gui/LauncherDialog.hxx +++ b/src/gui/LauncherDialog.hxx @@ -25,9 +25,10 @@ class DialogContainer; class OSystem; class Properties; class EditTextWidget; -class FileListWidget; +class LauncherFileListWidget; class RomInfoWidget; class StaticTextWidget; + namespace Common { struct Size; } @@ -36,10 +37,12 @@ namespace GUI { } #include +#include #include "bspf.hxx" #include "Dialog.hxx" #include "FSNode.hxx" +#include "Variant.hxx" class LauncherDialog : public Dialog { @@ -50,6 +53,7 @@ class LauncherDialog : public Dialog kRomDirChosenCmd = 'romc', // ROM dir chosen kExtChangedCmd = 'extc' // File extension display changed }; + using FileList = std::unordered_set; public: LauncherDialog(OSystem& osystem, DialogContainer& parent, @@ -153,16 +157,23 @@ class LauncherDialog : public Dialog void handleContextMenu(); void showOnlyROMs(bool state); 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 openSettings(); void openHighScores(); void openWhatsNew(); - ContextMenu& menu(); + ContextMenu& contextMenu(); private: unique_ptr myDialog; - unique_ptr myMenu; + unique_ptr myContextMenu; // automatically sized font for ROM info viewer unique_ptr myROMInfoFont; @@ -172,7 +183,7 @@ class LauncherDialog : public Dialog CheckboxWidget* mySubDirs{nullptr}; StaticTextWidget* myRomCount{nullptr}; - FileListWidget* myList{nullptr}; + LauncherFileListWidget* myList{nullptr}; StaticTextWidget* myDirLabel{nullptr}; EditTextWidget* myDir{nullptr}; @@ -182,8 +193,8 @@ class LauncherDialog : public Dialog ButtonWidget* myOptionsButton{nullptr}; ButtonWidget* myQuitButton{nullptr}; -// FIXME - NOT USED StaticTextWidget* myRomLink{nullptr}; RomInfoWidget* myRomInfoWidget{nullptr}; + std::unordered_map myMD5List; int mySelectedItem{0}; diff --git a/src/gui/LauncherFileListWidget.cxx b/src/gui/LauncherFileListWidget.cxx new file mode 100644 index 000000000..bed7ba0f6 --- /dev/null +++ b/src/gui/LauncherFileListWidget.cxx @@ -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(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"; diff --git a/src/gui/LauncherFileListWidget.hxx b/src/gui/LauncherFileListWidget.hxx new file mode 100644 index 000000000..f8ecf35fd --- /dev/null +++ b/src/gui/LauncherFileListWidget.hxx @@ -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 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 \ No newline at end of file diff --git a/src/gui/StringListWidget.cxx b/src/gui/StringListWidget.cxx index 32696c54e..9e097297a 100644 --- a/src/gui/StringListWidget.cxx +++ b/src/gui/StringListWidget.cxx @@ -18,7 +18,6 @@ #include "bspf.hxx" #include "Dialog.hxx" #include "FBSurface.hxx" -#include "Settings.hxx" #include "ScrollBarWidget.hxx" #include "StringListWidget.hxx" diff --git a/src/gui/module.mk b/src/gui/module.mk index 3759b367c..65c7dcec9 100644 --- a/src/gui/module.mk +++ b/src/gui/module.mk @@ -16,6 +16,7 @@ MODULE_OBJS := \ src/gui/EditTextWidget.o \ src/gui/EmulationDialog.o \ src/gui/EventMappingWidget.o \ + src/gui/FavoritesManager.o \ src/gui/FileListWidget.o \ src/gui/Font.o \ src/gui/GameInfoDialog.o \ @@ -27,6 +28,7 @@ MODULE_OBJS := \ src/gui/InputTextDialog.o \ src/gui/JoystickDialog.o \ src/gui/LauncherDialog.o \ + src/gui/LauncherFileListWidget.o \ src/gui/Launcher.o \ src/gui/ListWidget.o \ src/gui/LoggerDialog.o \ diff --git a/src/windows/Stella.vcxproj b/src/windows/Stella.vcxproj index 8741d4ac9..ba3da17d8 100755 --- a/src/windows/Stella.vcxproj +++ b/src/windows/Stella.vcxproj @@ -615,7 +615,7 @@ - + @@ -907,9 +907,11 @@ + + @@ -1780,7 +1782,7 @@ - + @@ -2111,9 +2113,11 @@ + + diff --git a/src/windows/Stella.vcxproj.filters b/src/windows/Stella.vcxproj.filters index 9af38e18f..69f23f3e1 100644 --- a/src/windows/Stella.vcxproj.filters +++ b/src/windows/Stella.vcxproj.filters @@ -1119,6 +1119,12 @@ Source Files + + Source Files\gui + + + Source Files\gui + @@ -2300,6 +2306,12 @@ Header Files + + Header Files\gui + + + Header Files\gui +