Added the preliminary GamelistBase and GamelistView classes.

This commit is contained in:
Leon Styhre 2022-01-18 17:14:17 +01:00
parent 50db59a6f6
commit ec0a7ad2f1
5 changed files with 1549 additions and 0 deletions

View file

@ -55,6 +55,8 @@ set(ES_HEADERS
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/IGamelistView.h
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/ISimpleGamelistView.h
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/VideoGamelistView.h
${CMAKE_CURRENT_SOURCE_DIR}/src/views/GamelistBase.h
${CMAKE_CURRENT_SOURCE_DIR}/src/views/GamelistView.h
${CMAKE_CURRENT_SOURCE_DIR}/src/views/SystemView.h
${CMAKE_CURRENT_SOURCE_DIR}/src/views/ViewController.h
)
@ -105,6 +107,8 @@ set(ES_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/IGamelistView.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/ISimpleGamelistView.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/VideoGamelistView.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/views/GamelistBase.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/views/GamelistView.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/views/SystemView.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/views/ViewController.cpp
)

View file

@ -0,0 +1,795 @@
// SPDX-License-Identifier: MIT
//
// EmulationStation Desktop Edition
// GamelistBase.cpp
//
// Gamelist base class with utility functions and other low-level logic.
//
#include "views/GamelistBase.h"
#include "CollectionSystemsManager.h"
#include "FileFilterIndex.h"
#include "UIModeController.h"
#include "guis/GuiGamelistOptions.h"
#include "views/ViewController.h"
GamelistBase::GamelistBase(Window* window, FileData* root)
: GuiComponent {window}
, mRoot {root}
, mList {window}
, mRandomGame {nullptr}
, mLastUpdated(nullptr)
{
}
GamelistBase::~GamelistBase()
{
//
}
void GamelistBase::setCursor(FileData* cursor)
{
if (!mList.setCursor(cursor) && (!cursor->isPlaceHolder())) {
populateList(cursor->getParent()->getChildrenListToDisplay(), cursor->getParent());
mList.setCursor(cursor);
// Update our cursor stack in case our cursor just got set to some folder
// we weren't in before.
if (mCursorStack.empty() || mCursorStack.top() != cursor->getParent()) {
std::stack<FileData*> tmp;
FileData* ptr = cursor->getParent();
while (ptr && ptr != mRoot) {
tmp.push(ptr);
ptr = ptr->getParent();
}
// Flip the stack and put it in mCursorStack.
mCursorStack = std::stack<FileData*>();
while (!tmp.empty()) {
mCursorStack.push(tmp.top());
tmp.pop();
}
}
}
}
bool GamelistBase::input(InputConfig* config, Input input)
{
if (input.value != 0) {
if (config->isMappedTo("a", input)) {
FileData* cursor = getCursor();
if (cursor->getType() == GAME) {
onPauseVideo();
ViewController::getInstance()->cancelViewTransitions();
stopListScrolling();
launch(cursor);
}
else {
// It's a folder.
if (cursor->getChildren().size() > 0) {
ViewController::getInstance()->cancelViewTransitions();
NavigationSounds::getInstance().playThemeNavigationSound(SELECTSOUND);
mCursorStack.push(cursor);
populateList(cursor->getChildrenListToDisplay(), cursor);
FileData* newCursor = nullptr;
std::vector<FileData*> listEntries = cursor->getChildrenListToDisplay();
// Check if there is an entry in the cursor stack history matching any entry
// in the currect folder. If so, select that entry.
for (auto it = mCursorStackHistory.begin(); // Line break.
it != mCursorStackHistory.end(); ++it) {
if (std::find(listEntries.begin(), listEntries.end(), *it) !=
listEntries.end()) {
newCursor = *it;
mCursorStackHistory.erase(it);
break;
}
}
// If there was no match in the cursor history, simply select the first entry.
if (!newCursor)
newCursor = getCursor();
setCursor(newCursor);
if (mRoot->getSystem()->getThemeFolder() == "custom-collections")
updateHelpPrompts();
}
else {
NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND);
}
}
return true;
}
else if (config->isMappedTo("b", input)) {
ViewController::getInstance()->cancelViewTransitions();
if (mCursorStack.size()) {
// Save the position to the cursor stack history.
mCursorStackHistory.push_back(getCursor());
NavigationSounds::getInstance().playThemeNavigationSound(BACKSOUND);
populateList(mCursorStack.top()->getParent()->getChildrenListToDisplay(),
mCursorStack.top()->getParent());
setCursor(mCursorStack.top());
if (mCursorStack.size() > 0)
mCursorStack.pop();
if (mRoot->getSystem()->getThemeFolder() == "custom-collections")
updateHelpPrompts();
}
else {
NavigationSounds::getInstance().playThemeNavigationSound(BACKSOUND);
onPauseVideo();
onFocusLost();
stopListScrolling();
SystemData* systemToView = getCursor()->getSystem();
if (systemToView->isCustomCollection() &&
systemToView->getRootFolder()->getParent())
ViewController::getInstance()->goToSystemView(
systemToView->getRootFolder()->getParent()->getSystem(), true);
else
ViewController::getInstance()->goToSystemView(systemToView, true);
}
return true;
}
else if (config->isMappedTo("x", input)) {
if (getCursor()->getType() == PLACEHOLDER) {
NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND);
return true;
}
else if (config->isMappedTo("x", input) &&
mRoot->getSystem()->getThemeFolder() == "custom-collections" &&
mCursorStack.empty() &&
ViewController::getInstance()->getState().viewing ==
ViewController::GAMELIST) {
NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND);
// Jump to the randomly selected game.
if (mRandomGame) {
stopListScrolling();
ViewController::getInstance()->cancelViewTransitions();
mWindow->startMediaViewer(mRandomGame);
return true;
}
}
else if (mRoot->getSystem()->isGameSystem()) {
stopListScrolling();
ViewController::getInstance()->cancelViewTransitions();
NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND);
mWindow->startMediaViewer(getCursor());
return true;
}
}
else if (config->isMappedLike(getQuickSystemSelectRightButton(), input)) {
if (Settings::getInstance()->getBool("QuickSystemSelect") &&
SystemData::sSystemVector.size() > 1) {
onPauseVideo();
onFocusLost();
stopListScrolling();
ViewController::getInstance()->goToNextGamelist();
return true;
}
}
else if (config->isMappedLike(getQuickSystemSelectLeftButton(), input)) {
if (Settings::getInstance()->getBool("QuickSystemSelect") &&
SystemData::sSystemVector.size() > 1) {
onPauseVideo();
onFocusLost();
stopListScrolling();
ViewController::getInstance()->goToPrevGamelist();
return true;
}
}
else if (Settings::getInstance()->getBool("RandomAddButton") &&
(config->isMappedTo("leftthumbstickclick", input) ||
config->isMappedTo("rightthumbstickclick", input))) {
if (mRoot->getSystem()->isGameSystem() && getCursor()->getType() != PLACEHOLDER) {
stopListScrolling();
// Jump to a random game.
NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND);
FileData* randomGame = getCursor()->getSystem()->getRandomGame(getCursor());
if (randomGame)
setCursor(randomGame);
return true;
}
}
else if (config->isMappedTo("y", input) &&
mRoot->getSystem()->getThemeFolder() == "custom-collections" &&
!CollectionSystemsManager::getInstance()->isEditing() && mCursorStack.empty() &&
ViewController::getInstance()->getState().viewing == ViewController::GAMELIST) {
// Jump to the randomly selected game.
if (mRandomGame) {
NavigationSounds::getInstance().playThemeNavigationSound(SELECTSOUND);
// If there is already an mCursorStackHistory entry for the collection, then
// remove it so we don't get multiple entries.
std::vector<FileData*> listEntries =
mRandomGame->getSystem()->getRootFolder()->getChildrenListToDisplay();
for (auto it = mCursorStackHistory.begin(); it != mCursorStackHistory.end(); ++it) {
if (std::find(listEntries.begin(), listEntries.end(), *it) !=
listEntries.end()) {
mCursorStackHistory.erase(it);
break;
}
}
setCursor(mRandomGame);
updateHelpPrompts();
}
else {
NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND);
}
}
else if (config->isMappedTo("y", input) &&
!Settings::getInstance()->getBool("FavoritesAddButton") &&
!CollectionSystemsManager::getInstance()->isEditing()) {
return true;
}
else if (config->isMappedTo("y", input) &&
!UIModeController::getInstance()->isUIModeKid() &&
!UIModeController::getInstance()->isUIModeKiosk()) {
// Notify the user if attempting to add a custom collection to a custom collection.
if (CollectionSystemsManager::getInstance()->isEditing() &&
mRoot->getSystem()->isGameSystem() && getCursor()->getType() != PLACEHOLDER &&
getCursor()->getParent()->getPath() == "collections") {
NavigationSounds::getInstance().playThemeNavigationSound(FAVORITESOUND);
mWindow->queueInfoPopup("CAN'T ADD CUSTOM COLLECTIONS TO CUSTOM COLLECTIONS", 4000);
}
// Notify the user if attempting to add a placeholder to a custom collection.
if (CollectionSystemsManager::getInstance()->isEditing() &&
mRoot->getSystem()->isGameSystem() && getCursor()->getType() == PLACEHOLDER) {
NavigationSounds::getInstance().playThemeNavigationSound(FAVORITESOUND);
mWindow->queueInfoPopup("CAN'T ADD PLACEHOLDERS TO CUSTOM COLLECTIONS", 4000);
}
else if (mRoot->getSystem()->isGameSystem() && getCursor()->getType() != PLACEHOLDER &&
getCursor()->getParent()->getPath() != "collections") {
if (getCursor()->getType() == GAME || getCursor()->getType() == FOLDER)
NavigationSounds::getInstance().playThemeNavigationSound(FAVORITESOUND);
// When marking or unmarking a game as favorite, don't jump to the new position
// it gets after the gamelist sorting. Instead retain the cursor position in the
// list using the logic below.
FileData* entryToUpdate = getCursor();
SystemData* system = getCursor()->getSystem();
bool favoritesSorting;
bool removedLastFavorite = false;
bool selectLastEntry = false;
bool isEditing = CollectionSystemsManager::getInstance()->isEditing();
bool foldersOnTop = Settings::getInstance()->getBool("FoldersOnTop");
// If the current list only contains folders, then treat it as if the folders
// are not sorted on top, this way the logic should work exactly as for mixed
// lists or files-only lists.
if (getCursor()->getType() == FOLDER && foldersOnTop == true)
foldersOnTop = !getCursor()->getParent()->getOnlyFoldersFlag();
if (mRoot->getSystem()->isCustomCollection() ||
mRoot->getSystem()->getThemeFolder() == "custom-collections")
favoritesSorting = Settings::getInstance()->getBool("FavFirstCustom");
else
favoritesSorting = Settings::getInstance()->getBool("FavoritesFirst");
if (favoritesSorting && mRoot->getSystem()->getName() != "recent" && !isEditing) {
FileData* entryToSelect;
// Add favorite flag.
if (!getCursor()->getFavorite()) {
// If it's a folder and folders are sorted on top, select the current entry.
if (foldersOnTop && getCursor()->getType() == FOLDER) {
entryToSelect = getCursor();
}
// If it's the first entry to be marked as favorite, select the next entry.
else if (getCursor() == getFirstEntry()) {
entryToSelect = getNextEntry();
}
else if (getCursor() == getLastEntry() &&
getPreviousEntry()->getFavorite()) {
entryToSelect = getLastEntry();
selectLastEntry = true;
}
// If we are on the favorite marking boundary, select the next entry.
else if (getCursor()->getFavorite() != getPreviousEntry()->getFavorite()) {
entryToSelect = getNextEntry();
}
// If we mark the second entry as favorite and the first entry is not a
// favorite, then select this entry if they are of the same type.
else if (getPreviousEntry() == getFirstEntry() &&
getCursor()->getType() == getPreviousEntry()->getType()) {
entryToSelect = getPreviousEntry();
}
// For all other scenarios try to select the next entry, and if it doesn't
// exist, select the previous entry.
else {
entryToSelect =
getCursor() != getNextEntry() ? getNextEntry() : getPreviousEntry();
}
}
// Remove favorite flag.
else {
// If it's a folder and folders are sorted on top, select the current entry.
if (foldersOnTop && getCursor()->getType() == FOLDER) {
entryToSelect = getCursor();
}
// If it's the last entry, select the previous entry.
else if (getCursor() == getLastEntry()) {
entryToSelect = getPreviousEntry();
}
// If we are on the favorite marking boundary, select the previous entry,
// unless folders are sorted on top and the previous entry is a folder.
else if (foldersOnTop &&
getCursor()->getFavorite() != getNextEntry()->getFavorite()) {
entryToSelect = getPreviousEntry()->getType() == FOLDER ?
getCursor() :
getPreviousEntry();
}
// If we are on the favorite marking boundary, select the previous entry.
else if (getCursor()->getFavorite() != getNextEntry()->getFavorite()) {
entryToSelect = getPreviousEntry();
}
// For all other scenarios try to select the next entry, and if it doesn't
// exist, select the previous entry.
else {
entryToSelect =
getCursor() != getNextEntry() ? getNextEntry() : getPreviousEntry();
}
// If we removed the last favorite marking, set the flag to jump to the
// first list entry after the sorting has been performed.
if (foldersOnTop && getCursor() == getFirstGameEntry() &&
!getNextEntry()->getFavorite())
removedLastFavorite = true;
else if (getCursor() == getFirstEntry() && !getNextEntry()->getFavorite())
removedLastFavorite = true;
}
setCursor(entryToSelect);
system = entryToUpdate->getSystem();
}
// Marking folders as favorites don't make them part of any collections,
// so it makes more sense to handle it here than to add the function to
// CollectionSystemsManager.
if (entryToUpdate->getType() == FOLDER) {
if (isEditing) {
mWindow->queueInfoPopup("CAN'T ADD FOLDERS TO CUSTOM COLLECTIONS", 4000);
}
else {
MetaDataList* md = &entryToUpdate->getSourceFileData()->metadata;
if (md->get("favorite") == "false") {
md->set("favorite", "true");
mWindow->queueInfoPopup(
"MARKED FOLDER '" +
Utils::String::toUpper(Utils::String::removeParenthesis(
entryToUpdate->getName())) +
"' AS FAVORITE",
4000);
}
else {
md->set("favorite", "false");
mWindow->queueInfoPopup(
"REMOVED FAVORITE MARKING FOR FOLDER '" +
Utils::String::toUpper(Utils::String::removeParenthesis(
entryToUpdate->getName())) +
"'",
4000);
}
}
entryToUpdate->getSourceFileData()->getSystem()->onMetaDataSavePoint();
getCursor()->getParent()->sort(
mRoot->getSortTypeFromString(mRoot->getSortTypeString()),
Settings::getInstance()->getBool("FavoritesFirst"));
ViewController::getInstance()->onFileChanged(getCursor(), false);
// Always jump to the first entry in the gamelist if the last favorite
// was unmarked. We couldn't do this earlier as we didn't have the list
// sorted yet.
if (removedLastFavorite) {
// TEMPORARY
// ViewController::getInstance()
// ->getGamelistView(entryToUpdate->getSystem())
// ->setCursor(ViewController::getInstance()
// ->getGamelistView(entryToUpdate->getSystem())
// ->getFirstEntry());
}
return true;
}
else if (isEditing && entryToUpdate->metadata.get("nogamecount") == "true") {
mWindow->queueInfoPopup("CAN'T ADD ENTRIES THAT ARE NOT COUNTED "
"AS GAMES TO CUSTOM COLLECTIONS",
4000);
}
else if (CollectionSystemsManager::getInstance()->toggleGameInCollection(
entryToUpdate)) {
// As the toggling of the game destroyed this object, we need to get the view
// from ViewController instead of using the reference that existed before the
// destruction. Otherwise we get random crashes.
// TEMPORARY
// IGamelistView* view =
// ViewController::getInstance()->getGamelistView(system).get();
// Jump to the first entry in the gamelist if the last favorite was unmarked.
if (foldersOnTop && removedLastFavorite &&
!entryToUpdate->getSystem()->isCustomCollection()) {
// TEMPORARY
// ViewController::getInstance()
// ->getGamelistView(entryToUpdate->getSystem())
// ->setCursor(ViewController::getInstance()
// ->getGamelistView(entryToUpdate->getSystem())
// ->getFirstGameEntry());
}
else if (removedLastFavorite &&
!entryToUpdate->getSystem()->isCustomCollection()) {
setCursor(getFirstEntry());
// view->setCursor(view->getFirstEntry());
}
else if (selectLastEntry) {
setCursor(getLastEntry());
// view->setCursor(view->getLastEntry());
}
// Display the indication icons which show what games are part of the
// custom collection currently being edited. This is done cheaply using
// onFileChanged() which will trigger populateList().
if (isEditing) {
for (auto it = SystemData::sSystemVector.begin();
it != SystemData::sSystemVector.end(); ++it) {
// TEMPORARY
// ViewController::getInstance()->getGamelistView((*it))->onFileChanged(
// ViewController::getInstance()->getGamelistView((*it))->getCursor(),
// false);
}
}
return true;
}
}
else if (config->isMappedTo("y", input) && getCursor()->isPlaceHolder()) {
NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND);
}
}
}
// return IGamelistView::input(config, input);
// Select button opens GuiGamelistOptions.
if (!UIModeController::getInstance()->isUIModeKid() && // Line break.
config->isMappedTo("back", input) && input.value) {
ViewController::getInstance()->cancelViewTransitions();
stopListScrolling();
mWindow->pushGui(new GuiGamelistOptions(mWindow, this->mRoot->getSystem()));
return true;
}
// Ctrl-R reloads the view when debugging.
else if (Settings::getInstance()->getBool("Debug") &&
config->getDeviceId() == DEVICE_KEYBOARD &&
(SDL_GetModState() & (KMOD_LCTRL | KMOD_RCTRL)) && input.id == SDLK_r &&
input.value != 0) {
LOG(LogDebug) << "IGamelistView::input(): Reloading view";
// TEMPORARY
// ViewController::getInstance()->reloadGamelistView(this, true);
return true;
}
return GuiComponent::input(config, input);
}
void GamelistBase::populateList(const std::vector<FileData*>& files, FileData* firstEntry)
{
mFirstGameEntry = nullptr;
bool favoriteStar {true};
bool isEditing {false};
std::string editingCollection;
std::string inCollectionPrefix;
if (CollectionSystemsManager::getInstance()->isEditing()) {
editingCollection = CollectionSystemsManager::getInstance()->getEditingCollection();
isEditing = true;
}
// Read the settings that control whether a unicode star character should be added
// as a prefix to the game name.
if (files.size() > 0) {
if (files.front()->getSystem()->isCustomCollection())
favoriteStar = Settings::getInstance()->getBool("FavStarCustom");
else
favoriteStar = Settings::getInstance()->getBool("FavoritesStar");
}
mList.clear();
if (files.size() > 0) {
for (auto it = files.cbegin(); it != files.cend(); ++it) {
if (!mFirstGameEntry && (*it)->getType() == GAME)
mFirstGameEntry = (*it);
// Add a leading tick mark icon to the game name if it's part of the custom collection
// currently being edited.
if (isEditing && (*it)->getType() == GAME) {
if (CollectionSystemsManager::getInstance()->inCustomCollection(editingCollection,
(*it))) {
if (Settings::getInstance()->getBool("SpecialCharsASCII"))
inCollectionPrefix = "! ";
else
inCollectionPrefix = ViewController::TICKMARK_CHAR + " ";
}
else {
inCollectionPrefix = "";
}
}
if ((*it)->getFavorite() && favoriteStar &&
mRoot->getSystem()->getName() != "favorites") {
if (Settings::getInstance()->getBool("SpecialCharsASCII"))
mList.add(inCollectionPrefix + "* " + (*it)->getName(), *it,
((*it)->getType() == FOLDER));
else
mList.add(inCollectionPrefix + ViewController::FAVORITE_CHAR + " " +
(*it)->getName(),
*it, ((*it)->getType() == FOLDER));
}
else if ((*it)->getType() == FOLDER && mRoot->getSystem()->getName() != "collections") {
if (Settings::getInstance()->getBool("SpecialCharsASCII"))
mList.add("# " + (*it)->getName(), *it, true);
else
mList.add(ViewController::FOLDER_CHAR + " " + (*it)->getName(), *it, true);
}
else {
mList.add(inCollectionPrefix + (*it)->getName(), *it, ((*it)->getType() == FOLDER));
}
}
}
else {
addPlaceholder(firstEntry);
}
generateGamelistInfo(getCursor(), firstEntry);
generateFirstLetterIndex(files);
}
void GamelistBase::addPlaceholder(FileData* firstEntry)
{
// Empty list, add a placeholder.
FileData* placeholder;
if (firstEntry && firstEntry->getSystem()->isGroupedCustomCollection())
placeholder = firstEntry->getSystem()->getPlaceholder();
else
placeholder = this->mRoot->getSystem()->getPlaceholder();
mList.add(placeholder->getName(), placeholder, (placeholder->getType() == PLACEHOLDER));
}
void GamelistBase::generateFirstLetterIndex(const std::vector<FileData*>& files)
{
std::string firstChar;
bool onlyFavorites {true};
bool onlyFolders {true};
bool hasFavorites {false};
bool hasFolders {false};
bool favoritesSorting {false};
mFirstLetterIndex.clear();
if (files.size() > 0 && files.front()->getSystem()->isCustomCollection())
favoritesSorting = Settings::getInstance()->getBool("FavFirstCustom");
else
favoritesSorting = Settings::getInstance()->getBool("FavoritesFirst");
bool foldersOnTop {Settings::getInstance()->getBool("FoldersOnTop")};
// Find out if there are only favorites and/or only folders in the list.
for (auto it = files.begin(); it != files.end(); ++it) {
if (!((*it)->getFavorite()))
onlyFavorites = false;
if (!((*it)->getType() == FOLDER))
onlyFolders = false;
}
// Build the index.
for (auto it = files.begin(); it != files.end(); ++it) {
if ((*it)->getType() == FOLDER && (*it)->getFavorite() && favoritesSorting &&
!onlyFavorites) {
hasFavorites = true;
}
else if ((*it)->getType() == FOLDER && foldersOnTop && !onlyFolders) {
hasFolders = true;
}
else if ((*it)->getType() == GAME && (*it)->getFavorite() && favoritesSorting &&
!onlyFavorites) {
hasFavorites = true;
}
else {
mFirstLetterIndex.push_back(Utils::String::getFirstCharacter((*it)->getSortName()));
}
}
// Sort and make each entry unique.
std::sort(mFirstLetterIndex.begin(), mFirstLetterIndex.end());
auto last = std::unique(mFirstLetterIndex.begin(), mFirstLetterIndex.end());
mFirstLetterIndex.erase(last, mFirstLetterIndex.end());
// If there are any favorites and/or folders in the list, insert their respective
// Unicode characters at the beginning of the vector.
if (hasFavorites)
mFirstLetterIndex.insert(mFirstLetterIndex.begin(), ViewController::FAVORITE_CHAR);
if (hasFolders)
mFirstLetterIndex.insert(mFirstLetterIndex.begin(), ViewController::FOLDER_CHAR);
}
void GamelistBase::generateGamelistInfo(FileData* cursor, FileData* firstEntry)
{
// Generate data needed for the gamelistInfo field, which is displayed from the
// gamelist interfaces (Detailed/Video/Grid).
mIsFiltered = false;
mIsFolder = false;
FileData* rootFolder {firstEntry->getSystem()->getRootFolder()};
std::pair<unsigned int, unsigned int> gameCount;
FileFilterIndex* idx {rootFolder->getSystem()->getIndex()};
// For the 'recent' collection we need to recount the games as the collection was
// trimmed down to 50 items. If we don't do this, the game count will not be correct
// as it would include all the games prior to trimming.
if (mRoot->getPath() == "recent")
mRoot->countGames(gameCount);
gameCount = rootFolder->getGameCount();
mGameCount = gameCount.first;
mFavoritesGameCount = gameCount.second;
mFilteredGameCount = 0;
mFilteredGameCountAll = 0;
if (idx->isFiltered()) {
mIsFiltered = true;
mFilteredGameCount =
static_cast<unsigned int>(rootFolder->getFilesRecursive(GAME, true, false).size());
// Also count the games that are set to not be counted as games, as the filter may
// apply to such entries as well and this will be indicated with a separate '+ XX'
// in the GamelistInfo field.
mFilteredGameCountAll =
static_cast<unsigned int>(rootFolder->getFilesRecursive(GAME, true, true).size());
}
if (firstEntry->getParent() && firstEntry->getParent()->getType() == FOLDER)
mIsFolder = true;
}
void GamelistBase::remove(FileData* game, bool deleteFile)
{
// Delete the game file on the filesystem.
if (deleteFile)
Utils::FileSystem::removeFile(game->getPath());
FileData* parent {game->getParent()};
// Select next element in list, or previous if none.
if (getCursor() == game) {
std::vector<FileData*> siblings {parent->getChildrenListToDisplay()};
auto gameIter = std::find(siblings.cbegin(), siblings.cend(), game);
unsigned int gamePos {
static_cast<unsigned int>(std::distance(siblings.cbegin(), gameIter))};
if (gameIter != siblings.cend()) {
if ((gamePos + 1) < siblings.size())
setCursor(siblings.at(gamePos + 1));
else if (gamePos > 1)
setCursor(siblings.at(gamePos - 1));
}
}
mList.remove(game);
if (mList.size() == 0)
addPlaceholder(nullptr);
// If a game has been deleted, immediately remove the entry from gamelist.xml
// regardless of the value of the setting SaveGamelistsMode.
game->setDeletionFlag(true);
parent->getSystem()->writeMetaData();
// Remove before repopulating (removes from parent), then update the view.
delete game;
if (deleteFile) {
parent->sort(parent->getSortTypeFromString(parent->getSortTypeString()),
Settings::getInstance()->getBool("FavoritesFirst"));
onFileChanged(parent, false);
}
}
void GamelistBase::removeMedia(FileData* game)
{
std::string systemMediaDir {FileData::getMediaDirectory() + game->getSystem()->getName()};
std::string mediaType;
std::string path;
// Stop the video player, especially important on Windows as the file would otherwise be locked.
onStopVideo();
// If there are no media files left in the directory after the deletion, then remove
// the directory too. Remove any empty parent directories as well.
auto removeEmptyDirFunc = [](std::string systemMediaDir, std::string mediaType,
std::string path) {
std::string parentPath {Utils::FileSystem::getParent(path)};
while (parentPath != systemMediaDir + "/" + mediaType) {
if (Utils::FileSystem::getDirContent(parentPath).size() == 0) {
Utils::FileSystem::removeDirectory(parentPath);
parentPath = Utils::FileSystem::getParent(parentPath);
}
else {
break;
}
}
};
// Remove all game media files on the filesystem.
while (Utils::FileSystem::exists(game->getVideoPath())) {
mediaType = "videos";
path = game->getVideoPath();
Utils::FileSystem::removeFile(path);
removeEmptyDirFunc(systemMediaDir, mediaType, path);
}
while (Utils::FileSystem::exists(game->getMiximagePath())) {
mediaType = "miximages";
path = game->getMiximagePath();
Utils::FileSystem::removeFile(path);
removeEmptyDirFunc(systemMediaDir, mediaType, path);
}
while (Utils::FileSystem::exists(game->getScreenshotPath())) {
mediaType = "screenshots";
path = game->getScreenshotPath();
Utils::FileSystem::removeFile(path);
removeEmptyDirFunc(systemMediaDir, mediaType, path);
}
while (Utils::FileSystem::exists(game->getTitleScreenPath())) {
mediaType = "titlescreens";
path = game->getTitleScreenPath();
Utils::FileSystem::removeFile(path);
removeEmptyDirFunc(systemMediaDir, mediaType, path);
}
while (Utils::FileSystem::exists(game->getCoverPath())) {
mediaType = "covers";
path = game->getCoverPath();
Utils::FileSystem::removeFile(path);
removeEmptyDirFunc(systemMediaDir, mediaType, path);
}
while (Utils::FileSystem::exists(game->getBackCoverPath())) {
mediaType = "backcovers";
path = game->getBackCoverPath();
Utils::FileSystem::removeFile(path);
removeEmptyDirFunc(systemMediaDir, mediaType, path);
}
while (Utils::FileSystem::exists(game->getFanArtPath())) {
mediaType = "fanart";
path = game->getFanArtPath();
Utils::FileSystem::removeFile(path);
removeEmptyDirFunc(systemMediaDir, mediaType, path);
}
while (Utils::FileSystem::exists(game->getMarqueePath())) {
mediaType = "marquees";
path = game->getMarqueePath();
Utils::FileSystem::removeFile(path);
removeEmptyDirFunc(systemMediaDir, mediaType, path);
}
while (Utils::FileSystem::exists(game->get3DBoxPath())) {
mediaType = "3dboxes";
path = game->get3DBoxPath();
Utils::FileSystem::removeFile(path);
removeEmptyDirFunc(systemMediaDir, mediaType, path);
}
while (Utils::FileSystem::exists(game->getPhysicalMediaPath())) {
mediaType = "physicalmedia";
path = game->getPhysicalMediaPath();
Utils::FileSystem::removeFile(path);
removeEmptyDirFunc(systemMediaDir, mediaType, path);
}
while (Utils::FileSystem::exists(game->getThumbnailPath())) {
mediaType = "thumbnails";
path = game->getThumbnailPath();
Utils::FileSystem::removeFile(path);
removeEmptyDirFunc(systemMediaDir, mediaType, path);
}
}

View file

@ -0,0 +1,101 @@
// SPDX-License-Identifier: MIT
//
// EmulationStation Desktop Edition
// GamelistBase.h
//
// Gamelist base class with utility functions and other low-level logic.
//
#ifndef ES_APP_VIEWS_GAMELIST_BASE_H
#define ES_APP_VIEWS_GAMELIST_BASE_H
#include "FileData.h"
#include "GuiComponent.h"
#include "SystemData.h"
#include "ThemeData.h"
#include "Window.h"
#include "components/BadgeComponent.h"
#include "components/DateTimeComponent.h"
#include "components/RatingComponent.h"
#include "components/ScrollableContainer.h"
#include "components/TextComponent.h"
#include "components/TextListComponent.h"
#include <stack>
class GamelistBase : public GuiComponent
{
public:
FileData* getCursor() { return mList.getSelected(); }
void setCursor(FileData*);
bool input(InputConfig* config, Input input) override;
FileData* getNextEntry() { return mList.getNext(); }
FileData* getPreviousEntry() { return mList.getPrevious(); }
FileData* getFirstEntry() { return mList.getFirst(); }
FileData* getLastEntry() { return mList.getLast(); }
FileData* getFirstGameEntry() { return mFirstGameEntry; }
protected:
GamelistBase(Window* window, FileData* root);
~GamelistBase();
// Called when a FileData* is added, has its metadata changed, or is removed.
virtual void onFileChanged(FileData* file, bool reloadGamelist) = 0;
void populateList(const std::vector<FileData*>& files, FileData* firstEntry);
void addPlaceholder(FileData*);
void generateFirstLetterIndex(const std::vector<FileData*>& files);
void generateGamelistInfo(FileData* cursor, FileData* firstEntry);
void remove(FileData* game, bool deleteFile);
void removeMedia(FileData* game);
virtual void launch(FileData* game) = 0;
bool isListScrolling() override { return mList.isScrolling(); }
void stopListScrolling() override { mList.stopScrolling(); }
const std::vector<std::string>& getFirstLetterIndex() { return mFirstLetterIndex; }
std::string getQuickSystemSelectRightButton() { return "right"; }
std::string getQuickSystemSelectLeftButton() { return "left"; }
// These functions are used to retain the folder cursor history, for instance
// during a view reload. The calling function stores the history temporarily.
void copyCursorHistory(std::vector<FileData*>& cursorHistory)
{
cursorHistory = mCursorStackHistory;
}
void populateCursorHistory(std::vector<FileData*>& cursorHistory)
{
mCursorStackHistory = cursorHistory;
}
FileData* mRoot;
TextListComponent<FileData*> mList;
// Points to the first game in the list, i.e. the first entry which is of the type "GAME".
FileData* mFirstGameEntry;
// This game is randomly selected in the grouped custom collections view.
FileData* mRandomGame;
FileData* mLastUpdated;
std::stack<FileData*> mCursorStack;
std::vector<FileData*> mCursorStackHistory;
std::vector<std::string> mFirstLetterIndex;
unsigned int mGameCount;
unsigned int mFavoritesGameCount;
unsigned int mFilteredGameCount;
unsigned int mFilteredGameCountAll;
bool mIsFiltered;
bool mIsFolder;
private:
};
#endif // ES_APP_VIEWS_GAMELIST_BASE_H

View file

@ -0,0 +1,558 @@
// SPDX-License-Identifier: MIT
//
// EmulationStation Desktop Edition
// GamelistView.cpp
//
// Main gamelist logic.
//
#include "views/GamelistView.h"
#include "CollectionSystemsManager.h"
#include "UIModeController.h"
#include "animations/LambdaAnimation.h"
#define FADE_IN_START_OPACITY 0.5f
#define FADE_IN_TIME 650
GamelistView::GamelistView(Window* window, FileData* root)
: GamelistBase {window, root}
, mHeaderText {window}
, mHeaderImage {window}
, mBackground {window}
, mThumbnail {window}
, mMarquee {window}
, mImage {window}
, mLblRating {window}
, mLblReleaseDate {window}
, mLblDeveloper {window}
, mLblPublisher {window}
, mLblGenre {window}
, mLblPlayers {window}
, mLblLastPlayed {window}
, mLblPlayCount {window}
, mRating {window}
, mReleaseDate {window}
, mDeveloper {window}
, mPublisher {window}
, mGenre {window}
, mPlayers {window}
, mLastPlayed {window}
, mPlayCount {window}
, mName {window}
, mBadges {window}
, mDescContainer {window}
, mDescription {window}
, mGamelistInfo {window}
{
mHeaderText.setText(mRoot->getSystem()->getFullName());
}
GamelistView::~GamelistView()
{
//
}
void GamelistView::onFileChanged(FileData* file, bool reloadGamelist)
{
if (reloadGamelist) {
// Might switch to a detailed view.
// TEMPORARY.
// ViewController::getInstance()->reloadGamelistView(this);
return;
}
// We could be tricky here to be efficient;
// but this shouldn't happen very often so we'll just always repopulate.
FileData* cursor {getCursor()};
if (!cursor->isPlaceHolder()) {
populateList(cursor->getParent()->getChildrenListToDisplay(), cursor->getParent());
setCursor(cursor);
}
else {
populateList(mRoot->getChildrenListToDisplay(), mRoot);
setCursor(cursor);
}
}
void GamelistView::onShow()
{
// Reset any Lottie animations.
for (auto extra : mThemeExtras)
extra->resetFileAnimation();
mLastUpdated = nullptr;
GuiComponent::onShow();
updateInfoPanel();
}
void GamelistView::onThemeChanged(const std::shared_ptr<ThemeData>& theme)
{
using namespace ThemeFlags;
mBackground.applyTheme(theme, getName(), "background", ALL);
mHeaderImage.applyTheme(theme, getName(), "logo", ALL);
mHeaderText.applyTheme(theme, getName(), "logoText", ALL);
// Remove old theme extras.
for (auto extra : mThemeExtras) {
removeChild(extra);
delete extra;
}
mThemeExtras.clear();
// Add new theme extras.
mThemeExtras = ThemeData::makeExtras(theme, getName(), mWindow);
for (auto extra : mThemeExtras)
addChild(extra);
if (mHeaderImage.hasImage()) {
removeChild(&mHeaderText);
addChild(&mHeaderImage);
}
else {
addChild(&mHeaderText);
removeChild(&mHeaderImage);
}
mList.applyTheme(theme, getName(), "gamelist", ALL);
mThumbnail.applyTheme(theme, getName(), "md_thumbnail",
POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION | VISIBLE);
mMarquee.applyTheme(theme, getName(), "md_marquee",
POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION | VISIBLE);
mImage.applyTheme(theme, getName(), "md_image",
POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION | VISIBLE);
mName.applyTheme(theme, getName(), "md_name", ALL);
mBadges.applyTheme(theme, getName(), "md_badges", ALL);
initMDLabels();
std::vector<TextComponent*> labels {getMDLabels()};
assert(labels.size() == 8);
std::vector<std::string> lblElements = {
"md_lbl_rating", "md_lbl_releasedate", "md_lbl_developer", "md_lbl_publisher",
"md_lbl_genre", "md_lbl_players", "md_lbl_lastplayed", "md_lbl_playcount"};
for (unsigned int i = 0; i < labels.size(); ++i)
labels[i]->applyTheme(theme, getName(), lblElements[i], ALL);
initMDValues();
std::vector<GuiComponent*> values {getMDValues()};
assert(values.size() == 8);
std::vector<std::string> valElements = {"md_rating", "md_releasedate", "md_developer",
"md_publisher", "md_genre", "md_players",
"md_lastplayed", "md_playcount"};
for (unsigned int i = 0; i < values.size(); ++i)
values[i]->applyTheme(theme, getName(), valElements[i], ALL ^ ThemeFlags::TEXT);
mDescContainer.applyTheme(theme, getName(), "md_description",
POSITION | ThemeFlags::SIZE | Z_INDEX | VISIBLE);
mDescription.setSize(mDescContainer.getSize().x, 0.0f);
mDescription.applyTheme(
theme, getName(), "md_description",
ALL ^ (POSITION | ThemeFlags::SIZE | ThemeFlags::ORIGIN | TEXT | ROTATION));
mGamelistInfo.applyTheme(theme, getName(), "gamelistInfo", ALL ^ ThemeFlags::TEXT);
// If there is no position defined in the theme for gamelistInfo, then hide it.
if (mGamelistInfo.getPosition() == glm::vec3 {})
mGamelistInfo.setVisible(false);
else
mGamelistInfo.setVisible(true);
sortChildren();
}
void GamelistView::update(int deltaTime)
{
// TEMPORARY
// BasicGamelistView::update(deltaTime);
mImage.update(deltaTime);
if (ViewController::getInstance()->getGameLaunchTriggered() && mImage.isAnimationPlaying(0))
mImage.finishAnimation(0);
}
void GamelistView::render(const glm::mat4& parentTrans)
{
glm::mat4 trans {parentTrans * getTransform()};
float scaleX {trans[0].x};
float scaleY {trans[1].y};
glm::ivec2 pos {static_cast<int>(std::round(trans[3].x)),
static_cast<int>(std::round(trans[3].y))};
glm::ivec2 size {static_cast<int>(std::round(mSize.x * scaleX)),
static_cast<int>(std::round(mSize.y * scaleY))};
Renderer::pushClipRect(pos, size);
renderChildren(trans);
Renderer::popClipRect();
}
HelpStyle GamelistView::getHelpStyle()
{
HelpStyle style;
style.applyTheme(mTheme, getName());
return style;
}
std::vector<HelpPrompt> GamelistView::getHelpPrompts()
{
std::vector<HelpPrompt> prompts;
if (Settings::getInstance()->getBool("QuickSystemSelect") &&
SystemData::sSystemVector.size() > 1)
prompts.push_back(HelpPrompt("left/right", "system"));
if (mRoot->getSystem()->getThemeFolder() == "custom-collections" && mCursorStack.empty() &&
ViewController::getInstance()->getState().viewing == ViewController::GAMELIST)
prompts.push_back(HelpPrompt("a", "enter"));
else
prompts.push_back(HelpPrompt("a", "launch"));
prompts.push_back(HelpPrompt("b", "back"));
prompts.push_back(HelpPrompt("x", "view media"));
if (!UIModeController::getInstance()->isUIModeKid())
prompts.push_back(HelpPrompt("back", "options"));
if (mRoot->getSystem()->isGameSystem() && Settings::getInstance()->getBool("RandomAddButton"))
prompts.push_back(HelpPrompt("thumbstickclick", "random"));
if (mRoot->getSystem()->getThemeFolder() == "custom-collections" &&
!CollectionSystemsManager::getInstance()->isEditing() && mCursorStack.empty() &&
ViewController::getInstance()->getState().viewing == ViewController::GAMELIST &&
ViewController::getInstance()->getState().viewstyle != ViewController::BASIC) {
prompts.push_back(HelpPrompt("y", "jump to game"));
}
else if (mRoot->getSystem()->isGameSystem() &&
(mRoot->getSystem()->getThemeFolder() != "custom-collections" ||
!mCursorStack.empty()) &&
!UIModeController::getInstance()->isUIModeKid() &&
!UIModeController::getInstance()->isUIModeKiosk() &&
(Settings::getInstance()->getBool("FavoritesAddButton") ||
CollectionSystemsManager::getInstance()->isEditing())) {
std::string prompt = CollectionSystemsManager::getInstance()->getEditingCollection();
prompts.push_back(HelpPrompt("y", prompt));
}
else if (mRoot->getSystem()->isGameSystem() &&
mRoot->getSystem()->getThemeFolder() == "custom-collections" &&
CollectionSystemsManager::getInstance()->isEditing()) {
std::string prompt = CollectionSystemsManager::getInstance()->getEditingCollection();
prompts.push_back(HelpPrompt("y", prompt));
}
return prompts;
}
void GamelistView::updateInfoPanel()
{
FileData* file {(mList.size() == 0 || mList.isScrolling()) ? nullptr : mList.getSelected()};
// If the game data has already been rendered to the info panel, then skip it this time.
if (file == mLastUpdated)
return;
if (!mList.isScrolling())
mLastUpdated = file;
bool hideMetaDataFields {false};
if (file) {
// Always hide the metadata fields if browsing grouped custom collections.
if (file->getSystem()->isCustomCollection() &&
file->getPath() == file->getSystem()->getName())
hideMetaDataFields = true;
else
hideMetaDataFields = (file->metadata.get("hidemetadata") == "true");
// Always hide the metadata fields for placeholders as well.
if (file->getType() == PLACEHOLDER) {
hideMetaDataFields = true;
mLastUpdated = nullptr;
}
}
// If we're scrolling, hide the metadata fields if the last game had this options set,
// or if we're in the grouped custom collection view.
if (mList.isScrolling())
if ((mLastUpdated && mLastUpdated->metadata.get("hidemetadata") == "true") ||
(mLastUpdated->getSystem()->isCustomCollection() &&
mLastUpdated->getPath() == mLastUpdated->getSystem()->getName()))
hideMetaDataFields = true;
if (hideMetaDataFields) {
mLblRating.setVisible(false);
mRating.setVisible(false);
mLblReleaseDate.setVisible(false);
mReleaseDate.setVisible(false);
mLblDeveloper.setVisible(false);
mDeveloper.setVisible(false);
mLblPublisher.setVisible(false);
mPublisher.setVisible(false);
mLblGenre.setVisible(false);
mGenre.setVisible(false);
mLblPlayers.setVisible(false);
mPlayers.setVisible(false);
mLblLastPlayed.setVisible(false);
mLastPlayed.setVisible(false);
mLblPlayCount.setVisible(false);
mPlayCount.setVisible(false);
mBadges.setVisible(false);
}
else {
mLblRating.setVisible(true);
mRating.setVisible(true);
mLblReleaseDate.setVisible(true);
mReleaseDate.setVisible(true);
mLblDeveloper.setVisible(true);
mDeveloper.setVisible(true);
mLblPublisher.setVisible(true);
mPublisher.setVisible(true);
mLblGenre.setVisible(true);
mGenre.setVisible(true);
mLblPlayers.setVisible(true);
mPlayers.setVisible(true);
mLblLastPlayed.setVisible(true);
mLastPlayed.setVisible(true);
mLblPlayCount.setVisible(true);
mPlayCount.setVisible(true);
mBadges.setVisible(true);
}
bool fadingOut = false;
if (file == nullptr) {
fadingOut = true;
}
else {
// If we're browsing a grouped custom collection, then update the folder metadata
// which will generate a description of three random games and return a pointer to
// the first of these so that we can display its game media.
if (file->getSystem()->isCustomCollection() &&
file->getPath() == file->getSystem()->getName()) {
mRandomGame = CollectionSystemsManager::getInstance()->updateCollectionFolderMetadata(
file->getSystem());
if (mRandomGame) {
mThumbnail.setImage(mRandomGame->getThumbnailPath());
mMarquee.setImage(mRandomGame->getMarqueePath(), false, true);
mImage.setImage(mRandomGame->getImagePath());
}
else {
mThumbnail.setImage("");
mMarquee.setImage("");
mImage.setImage("");
}
}
else {
mThumbnail.setImage(file->getThumbnailPath());
mMarquee.setImage(file->getMarqueePath(), false, true);
mImage.setImage(file->getImagePath());
}
// Populate the gamelistInfo field which shows an icon if a folder has been entered
// as well as the game count for the entire system (total and favorites separately).
// If a filter has been applied, then the number of filtered and total games replaces
// the game counter.
std::string gamelistInfoString;
Alignment infoAlign = mGamelistInfo.getHorizontalAlignment();
if (mIsFolder && infoAlign == ALIGN_RIGHT)
gamelistInfoString = ViewController::FOLDER_CHAR + " ";
if (mIsFiltered) {
if (mFilteredGameCountAll == mFilteredGameCount)
gamelistInfoString += ViewController::FILTER_CHAR + " " +
std::to_string(mFilteredGameCount) + " / " +
std::to_string(mGameCount);
else
gamelistInfoString += ViewController::FILTER_CHAR + " " +
std::to_string(mFilteredGameCount) + " + " +
std::to_string(mFilteredGameCountAll - mFilteredGameCount) +
" / " + std::to_string(mGameCount);
}
else {
gamelistInfoString +=
ViewController::CONTROLLER_CHAR + " " + std::to_string(mGameCount);
if (!(file->getSystem()->isCollection() &&
file->getSystem()->getFullName() == "favorites"))
gamelistInfoString += " " + ViewController::FAVORITE_CHAR + " " +
std::to_string(mFavoritesGameCount);
}
if (mIsFolder && infoAlign != ALIGN_RIGHT)
gamelistInfoString += " " + ViewController::FOLDER_CHAR;
mGamelistInfo.setValue(gamelistInfoString);
// Fade in the game image.
auto func = [this](float t) {
mImage.setOpacity(static_cast<unsigned char>(
glm::mix(static_cast<float>(FADE_IN_START_OPACITY), 1.0f, t) * 255));
};
mImage.setAnimation(new LambdaAnimation(func, FADE_IN_TIME), 0, nullptr, false);
mDescription.setText(file->metadata.get("desc"));
mDescContainer.reset();
mRating.setValue(file->metadata.get("rating"));
mReleaseDate.setValue(file->metadata.get("releasedate"));
mDeveloper.setValue(file->metadata.get("developer"));
mPublisher.setValue(file->metadata.get("publisher"));
mGenre.setValue(file->metadata.get("genre"));
mPlayers.setValue(file->metadata.get("players"));
// Populate the badge slots based on game metadata.
std::vector<BadgeComponent::BadgeInfo> badgeSlots;
for (auto badge : mBadges.getBadgeTypes()) {
BadgeComponent::BadgeInfo badgeInfo;
badgeInfo.badgeType = badge;
if (badge == "controller") {
if (file->metadata.get("controller").compare("") != 0) {
badgeInfo.gameController = file->metadata.get("controller");
badgeSlots.push_back(badgeInfo);
}
}
else if (badge == "altemulator") {
if (file->metadata.get(badge).compare("") != 0)
badgeSlots.push_back(badgeInfo);
}
else {
if (file->metadata.get(badge).compare("true") == 0)
badgeSlots.push_back(badgeInfo);
}
}
mBadges.setBadges(badgeSlots);
mName.setValue(file->metadata.get("name"));
if (file->getType() == GAME) {
if (!hideMetaDataFields) {
mLastPlayed.setValue(file->metadata.get("lastplayed"));
mPlayCount.setValue(file->metadata.get("playcount"));
}
}
else if (file->getType() == FOLDER) {
if (!hideMetaDataFields) {
mLastPlayed.setValue(file->metadata.get("lastplayed"));
mLblPlayCount.setVisible(false);
mPlayCount.setVisible(false);
}
}
fadingOut = false;
}
std::vector<GuiComponent*> comps = getMDValues();
comps.push_back(&mThumbnail);
comps.push_back(&mMarquee);
comps.push_back(&mImage);
comps.push_back(&mDescription);
comps.push_back(&mName);
comps.push_back(&mBadges);
std::vector<TextComponent*> labels = getMDLabels();
comps.insert(comps.cend(), labels.cbegin(), labels.cend());
for (auto it = comps.cbegin(); it != comps.cend(); ++it) {
GuiComponent* comp = *it;
// An animation is playing, then animate if reverse != fadingOut.
// An animation is not playing, then animate if opacity != our target opacity.
if ((comp->isAnimationPlaying(0) && comp->isAnimationReversed(0) != fadingOut) ||
(!comp->isAnimationPlaying(0) && comp->getOpacity() != (fadingOut ? 0 : 255))) {
auto func = [comp](float t) {
comp->setOpacity(static_cast<unsigned char>(glm::mix(0.0f, 1.0f, t) * 255));
};
comp->setAnimation(new LambdaAnimation(func, 150), 0, nullptr, fadingOut);
}
}
}
void GamelistView::initMDLabels()
{
std::vector<TextComponent*> components {getMDLabels()};
const unsigned int colCount {2};
const unsigned int rowCount {static_cast<unsigned int>(components.size() / 2)};
glm::vec3 start {mSize.x * 0.01f, mSize.y * 0.625f, 0.0f};
const float colSize {(mSize.x * 0.48f) / colCount};
const float rowPadding {0.01f * mSize.y};
for (unsigned int i = 0; i < components.size(); ++i) {
const unsigned int row = i % rowCount;
glm::vec3 pos {};
if (row == 0) {
pos = start + glm::vec3 {colSize * (i / rowCount), 0.0f, 0.0f};
}
else {
// Work from the last component.
GuiComponent* lc {components[i - 1]};
pos = lc->getPosition() + glm::vec3 {0.0f, lc->getSize().y + rowPadding, 0.0f};
}
components[i]->setFont(Font::get(FONT_SIZE_SMALL));
components[i]->setPosition(pos);
components[i]->setDefaultZIndex(40.0f);
}
}
void GamelistView::initMDValues()
{
std::vector<TextComponent*> labels {getMDLabels()};
std::vector<GuiComponent*> values {getMDValues()};
std::shared_ptr<Font> defaultFont {Font::get(FONT_SIZE_SMALL)};
mRating.setSize(defaultFont->getHeight() * 5.0f, static_cast<float>(defaultFont->getHeight()));
mReleaseDate.setFont(defaultFont);
mDeveloper.setFont(defaultFont);
mPublisher.setFont(defaultFont);
mGenre.setFont(defaultFont);
mPlayers.setFont(defaultFont);
mLastPlayed.setFont(defaultFont);
mPlayCount.setFont(defaultFont);
float bottom {0.0f};
const float colSize {(mSize.x * 0.48f) / 2.0f};
for (unsigned int i = 0; i < labels.size(); ++i) {
const float heightDiff = (labels[i]->getSize().y - values[i]->getSize().y) / 2.0f;
values[i]->setPosition(labels[i]->getPosition() +
glm::vec3 {labels[i]->getSize().x, heightDiff, 0.0f});
values[i]->setSize(colSize - labels[i]->getSize().x, values[i]->getSize().y);
values[i]->setDefaultZIndex(40.0f);
float testBot = values[i]->getPosition().y + values[i]->getSize().y;
if (testBot > bottom)
bottom = testBot;
}
mDescContainer.setPosition(mDescContainer.getPosition().x, bottom + mSize.y * 0.01f);
mDescContainer.setSize(mDescContainer.getSize().x, mSize.y - mDescContainer.getPosition().y);
}
std::vector<TextComponent*> GamelistView::getMDLabels()
{
std::vector<TextComponent*> ret;
ret.push_back(&mLblRating);
ret.push_back(&mLblReleaseDate);
ret.push_back(&mLblDeveloper);
ret.push_back(&mLblPublisher);
ret.push_back(&mLblGenre);
ret.push_back(&mLblPlayers);
ret.push_back(&mLblLastPlayed);
ret.push_back(&mLblPlayCount);
return ret;
}
std::vector<GuiComponent*> GamelistView::getMDValues()
{
std::vector<GuiComponent*> ret;
ret.push_back(&mRating);
ret.push_back(&mReleaseDate);
ret.push_back(&mDeveloper);
ret.push_back(&mPublisher);
ret.push_back(&mGenre);
ret.push_back(&mPlayers);
ret.push_back(&mLastPlayed);
ret.push_back(&mPlayCount);
return ret;
}

View file

@ -0,0 +1,91 @@
// SPDX-License-Identifier: MIT
//
// EmulationStation Desktop Edition
// GamelistView.h
//
// Main gamelist logic.
//
#ifndef ES_APP_VIEWS_GAMELIST_VIEW_H
#define ES_APP_VIEWS_GAMELIST_VIEW_H
#include "views/GamelistBase.h"
#include "renderers/Renderer.h"
#include "views/ViewController.h"
class GamelistView : public GamelistBase
{
public:
GamelistView(Window* window, FileData* root);
~GamelistView();
// Called when a FileData* is added, has its metadata changed, or is removed.
void onFileChanged(FileData* file, bool reloadGamelist) override;
void onShow() override;
void preloadGamelist() { updateInfoPanel(); }
void launch(FileData* game) override { ViewController::getInstance()->triggerGameLaunch(game); }
std::string getName() const { return "DEPRECATED FUNCTION"; }
const std::shared_ptr<ThemeData> getTheme() const { return mTheme; }
void setTheme(const std::shared_ptr<ThemeData>& theme)
{
mTheme = theme;
onThemeChanged(theme);
}
void onThemeChanged(const std::shared_ptr<ThemeData>& theme);
void update(int deltaTime) override;
void render(const glm::mat4& parentTrans) override;
HelpStyle getHelpStyle() override;
std::vector<HelpPrompt> getHelpPrompts() override;
private:
void updateInfoPanel();
void initMDLabels();
void initMDValues();
std::vector<TextComponent*> getMDLabels();
std::vector<GuiComponent*> getMDValues();
std::shared_ptr<ThemeData> mTheme;
std::vector<GuiComponent*> mThemeExtras;
TextComponent mHeaderText;
ImageComponent mHeaderImage;
ImageComponent mBackground;
ImageComponent mThumbnail;
ImageComponent mMarquee;
ImageComponent mImage;
TextComponent mLblRating;
TextComponent mLblReleaseDate;
TextComponent mLblDeveloper;
TextComponent mLblPublisher;
TextComponent mLblGenre;
TextComponent mLblPlayers;
TextComponent mLblLastPlayed;
TextComponent mLblPlayCount;
RatingComponent mRating;
DateTimeComponent mReleaseDate;
TextComponent mDeveloper;
TextComponent mPublisher;
TextComponent mGenre;
TextComponent mPlayers;
DateTimeComponent mLastPlayed;
TextComponent mPlayCount;
TextComponent mName;
BadgeComponent mBadges;
ScrollableContainer mDescContainer;
TextComponent mDescription;
TextComponent mGamelistInfo;
};
#endif // ES_APP_VIEWS_GAMELIST_VIEW_H