mirror of
https://github.com/RetroDECK/ES-DE.git
synced 2024-11-22 06:05:38 +00:00
Fully generalized SystemView and GamelistView and rewrote CarouselComponent into a template class.
Also cleaned up some code and fixed an issue where navigation sounds would not play when using the shoulder buttons.
This commit is contained in:
parent
5625f44a0a
commit
3a1c9d41ce
|
@ -25,10 +25,6 @@ set(ES_HEADERS
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/src/UIModeController.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/VolumeControl.h
|
||||
|
||||
# Primary GUI components
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/CarouselComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextListComponent.h
|
||||
|
||||
# GUIs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiAlternativeEmulators.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.h
|
||||
|
@ -76,10 +72,6 @@ set(ES_SOURCES
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/src/UIModeController.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/VolumeControl.cpp
|
||||
|
||||
# Primary GUI components
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/CarouselComponent.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextListComponent.cpp
|
||||
|
||||
# GUIs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiAlternativeEmulators.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.cpp
|
||||
|
|
|
@ -215,9 +215,10 @@ void Screensaver::launchGame()
|
|||
// Launching game
|
||||
ViewController::getInstance()->triggerGameLaunch(mCurrentGame);
|
||||
ViewController::getInstance()->goToGamelist(mCurrentGame->getSystem());
|
||||
GamelistView* view =
|
||||
ViewController::getInstance()->getGamelistView(mCurrentGame->getSystem()).get();
|
||||
GamelistView* view {
|
||||
ViewController::getInstance()->getGamelistView(mCurrentGame->getSystem()).get()};
|
||||
view->setCursor(mCurrentGame);
|
||||
view->stopListScrolling();
|
||||
ViewController::getInstance()->cancelViewTransitions();
|
||||
ViewController::getInstance()->pauseViewVideos();
|
||||
}
|
||||
|
@ -228,9 +229,10 @@ void Screensaver::goToGame()
|
|||
if (mCurrentGame != nullptr) {
|
||||
// Go to the game in the gamelist view, but don't launch it.
|
||||
ViewController::getInstance()->goToGamelist(mCurrentGame->getSystem());
|
||||
GamelistView* view =
|
||||
ViewController::getInstance()->getGamelistView(mCurrentGame->getSystem()).get();
|
||||
GamelistView* view {
|
||||
ViewController::getInstance()->getGamelistView(mCurrentGame->getSystem()).get()};
|
||||
view->setCursor(mCurrentGame);
|
||||
view->stopListScrolling();
|
||||
ViewController::getInstance()->cancelViewTransitions();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// EmulationStation Desktop Edition
|
||||
// CarouselComponent.h
|
||||
//
|
||||
// Carousel.
|
||||
//
|
||||
|
||||
#include "GuiComponent.h"
|
||||
#include "Sound.h"
|
||||
#include "components/IList.h"
|
||||
#include "components/ImageComponent.h"
|
||||
#include "components/TextComponent.h"
|
||||
#include "resources/Font.h"
|
||||
|
||||
class SystemData;
|
||||
|
||||
#ifndef ES_CORE_COMPONENTS_CAROUSEL_COMPONENT_H
|
||||
#define ES_CORE_COMPONENTS_CAROUSEL_COMPONENT_H
|
||||
|
||||
struct CarouselElement {
|
||||
std::shared_ptr<GuiComponent> logo;
|
||||
std::string logoPath;
|
||||
std::string defaultLogoPath;
|
||||
};
|
||||
|
||||
class CarouselComponent : public IList<CarouselElement, SystemData*>
|
||||
{
|
||||
public:
|
||||
CarouselComponent();
|
||||
void addEntry(const std::shared_ptr<ThemeData>& theme, Entry& entry, bool legacyMode);
|
||||
Entry& getEntry(int index) { return mEntries.at(index); }
|
||||
|
||||
enum CarouselType {
|
||||
HORIZONTAL, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0).
|
||||
VERTICAL,
|
||||
VERTICAL_WHEEL,
|
||||
HORIZONTAL_WHEEL
|
||||
};
|
||||
|
||||
int getCursor() { return mCursor; }
|
||||
const CarouselType getType() { return mType; }
|
||||
size_t getNumEntries() { return mEntries.size(); }
|
||||
|
||||
void setCursorChangedCallback(const std::function<void(CursorState state)>& func)
|
||||
{
|
||||
mCursorChangedCallback = func;
|
||||
}
|
||||
void setCancelTransitionsCallback(const std::function<void()>& func)
|
||||
{
|
||||
mCancelTransitionsCallback = func;
|
||||
}
|
||||
|
||||
bool input(InputConfig* config, Input input) override;
|
||||
void update(int deltaTime) override;
|
||||
void render(const glm::mat4& parentTrans) override;
|
||||
|
||||
void applyTheme(const std::shared_ptr<ThemeData>& theme,
|
||||
const std::string& view,
|
||||
const std::string& element,
|
||||
unsigned int properties) override;
|
||||
|
||||
protected:
|
||||
void onCursorChanged(const CursorState& state) override;
|
||||
void onScroll() override
|
||||
{
|
||||
NavigationSounds::getInstance().playThemeNavigationSound(SYSTEMBROWSESOUND);
|
||||
}
|
||||
|
||||
private:
|
||||
Renderer* mRenderer;
|
||||
std::function<void(CursorState state)> mCursorChangedCallback;
|
||||
std::function<void()> mCancelTransitionsCallback;
|
||||
|
||||
float mCamOffset;
|
||||
int mPreviousScrollVelocity;
|
||||
|
||||
CarouselType mType;
|
||||
std::shared_ptr<Font> mFont;
|
||||
unsigned int mTextColor;
|
||||
unsigned int mTextBackgroundColor;
|
||||
std::string mText;
|
||||
float mLineSpacing;
|
||||
Alignment mLogoHorizontalAlignment;
|
||||
Alignment mLogoVerticalAlignment;
|
||||
float mMaxLogoCount;
|
||||
glm::vec2 mLogoSize;
|
||||
float mLogoScale;
|
||||
float mLogoRotation;
|
||||
glm::vec2 mLogoRotationOrigin;
|
||||
unsigned int mCarouselColor;
|
||||
unsigned int mCarouselColorEnd;
|
||||
bool mColorGradientHorizontal;
|
||||
};
|
||||
|
||||
#endif // ES_CORE_COMPONENTS_CAROUSEL_COMPONENT_H
|
|
@ -1,9 +0,0 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// EmulationStation Desktop Edition
|
||||
// TextListComponent.cpp
|
||||
//
|
||||
// Text list used for displaying and navigating the gamelist views.
|
||||
//
|
||||
|
||||
#include "components/TextListComponent.h"
|
|
@ -121,17 +121,18 @@ void GuiMenu::openUIOptions()
|
|||
Scripting::fireEvent("theme-changed", theme_set->getSelected(),
|
||||
Settings::getInstance()->getString("ThemeSet"));
|
||||
Settings::getInstance()->setString("ThemeSet", theme_set->getSelected());
|
||||
CollectionSystemsManager::getInstance()->updateSystemsList();
|
||||
mWindow->setChangedThemeSet();
|
||||
// This is required so that the custom collection system does not disappear
|
||||
// if the user is editing a custom collection when switching theme sets.
|
||||
if (CollectionSystemsManager::getInstance()->isEditing()) {
|
||||
if (CollectionSystemsManager::getInstance()->isEditing())
|
||||
CollectionSystemsManager::getInstance()->exitEditMode();
|
||||
s->setNeedsCollectionsUpdate();
|
||||
}
|
||||
// TODO: Eliminate this extra reload or only execute it when switching from
|
||||
// a legacy theme to a non-legacy theme.
|
||||
ViewController::getInstance()->reloadAll();
|
||||
s->setNeedsSaving();
|
||||
s->setNeedsReloading();
|
||||
s->setNeedsGoToStart();
|
||||
s->setNeedsCollectionsUpdate();
|
||||
s->setInvalidateCachedBackground();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -130,7 +130,7 @@ GuiScraperSearch::GuiScraperSearch(SearchType type, unsigned int scrapeCount)
|
|||
// Result list.
|
||||
mResultList = std::make_shared<ComponentList>();
|
||||
mResultList->setCursorChangedCallback([this](CursorState state) {
|
||||
if (state == CURSOR_STOPPED)
|
||||
if (state == CursorState::CURSOR_STOPPED)
|
||||
updateInfoPane();
|
||||
});
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
GamelistBase::GamelistBase(FileData* root)
|
||||
: mRoot {root}
|
||||
, mPrimary {nullptr}
|
||||
, mRandomGame {nullptr}
|
||||
, mLastUpdated {nullptr}
|
||||
, mGameCount {0}
|
||||
|
@ -25,33 +26,22 @@ GamelistBase::GamelistBase(FileData* root)
|
|||
, mIsFiltered {false}
|
||||
, mIsFolder {false}
|
||||
, mVideoPlaying {false}
|
||||
, mLeftRightAvailable {true}
|
||||
{
|
||||
setSize(Renderer::getScreenWidth(), Renderer::getScreenHeight());
|
||||
|
||||
mList.setSize(mSize.x, mSize.y * 0.8f);
|
||||
mList.setPosition(0.0f, mSize.y * 0.2f);
|
||||
mList.setDefaultZIndex(20.0f);
|
||||
addChild(&mList);
|
||||
|
||||
populateList(root->getChildrenListToDisplay(), root);
|
||||
}
|
||||
|
||||
GamelistBase::~GamelistBase()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
void GamelistBase::setCursor(FileData* cursor)
|
||||
{
|
||||
if (!mList.setCursor(cursor) && (!cursor->isPlaceHolder())) {
|
||||
if (!mPrimary->setCursor(cursor) && (!cursor->isPlaceHolder())) {
|
||||
populateList(cursor->getParent()->getChildrenListToDisplay(), cursor->getParent());
|
||||
mList.setCursor(cursor);
|
||||
mPrimary->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();
|
||||
FileData* ptr {cursor->getParent()};
|
||||
|
||||
while (ptr && ptr != mRoot) {
|
||||
tmp.push(ptr);
|
||||
|
@ -72,7 +62,7 @@ bool GamelistBase::input(InputConfig* config, Input input)
|
|||
{
|
||||
if (input.value != 0) {
|
||||
if (config->isMappedTo("a", input)) {
|
||||
FileData* cursor = getCursor();
|
||||
FileData* cursor {getCursor()};
|
||||
if (cursor->getType() == GAME) {
|
||||
pauseViewVideos();
|
||||
ViewController::getInstance()->cancelViewTransitions();
|
||||
|
@ -87,7 +77,7 @@ bool GamelistBase::input(InputConfig* config, Input input)
|
|||
mCursorStack.push(cursor);
|
||||
populateList(cursor->getChildrenListToDisplay(), cursor);
|
||||
|
||||
FileData* newCursor = nullptr;
|
||||
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.
|
||||
|
@ -105,6 +95,7 @@ bool GamelistBase::input(InputConfig* config, Input input)
|
|||
if (!newCursor)
|
||||
newCursor = getCursor();
|
||||
setCursor(newCursor);
|
||||
stopListScrolling();
|
||||
if (mRoot->getSystem()->getThemeFolder() == "custom-collections")
|
||||
updateHelpPrompts();
|
||||
}
|
||||
|
@ -124,6 +115,7 @@ bool GamelistBase::input(InputConfig* config, Input input)
|
|||
populateList(mCursorStack.top()->getParent()->getChildrenListToDisplay(),
|
||||
mCursorStack.top()->getParent());
|
||||
setCursor(mCursorStack.top());
|
||||
stopListScrolling();
|
||||
if (mCursorStack.size() > 0)
|
||||
mCursorStack.pop();
|
||||
if (mRoot->getSystem()->getThemeFolder() == "custom-collections")
|
||||
|
@ -173,7 +165,7 @@ bool GamelistBase::input(InputConfig* config, Input input)
|
|||
}
|
||||
}
|
||||
else if (config->isMappedLike(getQuickSystemSelectRightButton(), input)) {
|
||||
if (Settings::getInstance()->getBool("QuickSystemSelect") &&
|
||||
if (mLeftRightAvailable && Settings::getInstance()->getBool("QuickSystemSelect") &&
|
||||
SystemData::sSystemVector.size() > 1) {
|
||||
muteViewVideos();
|
||||
onFocusLost();
|
||||
|
@ -183,7 +175,7 @@ bool GamelistBase::input(InputConfig* config, Input input)
|
|||
}
|
||||
}
|
||||
else if (config->isMappedLike(getQuickSystemSelectLeftButton(), input)) {
|
||||
if (Settings::getInstance()->getBool("QuickSystemSelect") &&
|
||||
if (mLeftRightAvailable && Settings::getInstance()->getBool("QuickSystemSelect") &&
|
||||
SystemData::sSystemVector.size() > 1) {
|
||||
muteViewVideos();
|
||||
onFocusLost();
|
||||
|
@ -199,7 +191,7 @@ bool GamelistBase::input(InputConfig* config, Input input)
|
|||
stopListScrolling();
|
||||
// Jump to a random game.
|
||||
NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND);
|
||||
FileData* randomGame = getCursor()->getSystem()->getRandomGame(getCursor());
|
||||
FileData* randomGame {getCursor()->getSystem()->getRandomGame(getCursor())};
|
||||
if (randomGame)
|
||||
setCursor(randomGame);
|
||||
return true;
|
||||
|
@ -214,8 +206,8 @@ bool GamelistBase::input(InputConfig* config, Input input)
|
|||
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();
|
||||
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()) {
|
||||
|
@ -224,6 +216,7 @@ bool GamelistBase::input(InputConfig* config, Input input)
|
|||
}
|
||||
}
|
||||
setCursor(mRandomGame);
|
||||
stopListScrolling();
|
||||
updateHelpPrompts();
|
||||
}
|
||||
else {
|
||||
|
@ -258,13 +251,13 @@ bool GamelistBase::input(InputConfig* config, Input input)
|
|||
// 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();
|
||||
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");
|
||||
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.
|
||||
|
@ -412,8 +405,8 @@ bool GamelistBase::input(InputConfig* config, Input input)
|
|||
// 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.
|
||||
GamelistView* view =
|
||||
ViewController::getInstance()->getGamelistView(system).get();
|
||||
GamelistView* 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()) {
|
||||
|
@ -428,7 +421,7 @@ bool GamelistBase::input(InputConfig* config, Input input)
|
|||
setCursor(getFirstEntry());
|
||||
view->setCursor(view->getFirstEntry());
|
||||
}
|
||||
else if (selectLastEntry && mList.size() > 0) {
|
||||
else if (selectLastEntry && mPrimary->size() > 0) {
|
||||
setCursor(getLastEntry());
|
||||
view->setCursor(view->getLastEntry());
|
||||
}
|
||||
|
@ -498,45 +491,65 @@ void GamelistBase::populateList(const std::vector<FileData*>& files, FileData* f
|
|||
favoriteStar = Settings::getInstance()->getBool("FavoritesStar");
|
||||
}
|
||||
|
||||
mList.clear();
|
||||
if (mPrimary != nullptr)
|
||||
mPrimary->clear();
|
||||
|
||||
auto theme = mRoot->getSystem()->getTheme();
|
||||
std::string name;
|
||||
unsigned int color {0};
|
||||
|
||||
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 (mCarousel != nullptr) {
|
||||
CarouselComponent<FileData*>::Entry carouselEntry;
|
||||
carouselEntry.name = (*it)->getName();
|
||||
carouselEntry.object = *it;
|
||||
carouselEntry.data.logoPath = (*it)->getMarqueePath();
|
||||
mCarousel->addEntry(carouselEntry, theme);
|
||||
}
|
||||
|
||||
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));
|
||||
if (mTextList != nullptr) {
|
||||
TextListComponent<FileData*>::Entry textListEntry;
|
||||
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"))
|
||||
name = inCollectionPrefix + "* " + (*it)->getName();
|
||||
else
|
||||
name = inCollectionPrefix + ViewController::FAVORITE_CHAR + " " +
|
||||
(*it)->getName();
|
||||
}
|
||||
else if ((*it)->getType() == FOLDER &&
|
||||
mRoot->getSystem()->getName() != "collections") {
|
||||
if (Settings::getInstance()->getBool("SpecialCharsASCII"))
|
||||
name = "# " + (*it)->getName();
|
||||
else
|
||||
name = ViewController::FOLDER_CHAR + " " + (*it)->getName();
|
||||
}
|
||||
else {
|
||||
name = inCollectionPrefix + (*it)->getName();
|
||||
}
|
||||
color = (*it)->getType() == FOLDER;
|
||||
textListEntry.name = name;
|
||||
textListEntry.object = *it;
|
||||
textListEntry.data.colorId = color;
|
||||
mTextList->addEntry(textListEntry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -551,14 +564,26 @@ void GamelistBase::populateList(const std::vector<FileData*>& files, FileData* f
|
|||
void GamelistBase::addPlaceholder(FileData* firstEntry)
|
||||
{
|
||||
// Empty list, add a placeholder.
|
||||
FileData* placeholder;
|
||||
FileData* placeholder {nullptr};
|
||||
|
||||
if (firstEntry && firstEntry->getSystem()->isGroupedCustomCollection())
|
||||
placeholder = firstEntry->getSystem()->getPlaceholder();
|
||||
else
|
||||
placeholder = this->mRoot->getSystem()->getPlaceholder();
|
||||
|
||||
mList.add(placeholder->getName(), placeholder, (placeholder->getType() == PLACEHOLDER));
|
||||
if (mTextList != nullptr) {
|
||||
TextListComponent<FileData*>::Entry textListEntry;
|
||||
textListEntry.name = placeholder->getName();
|
||||
textListEntry.object = placeholder;
|
||||
textListEntry.data.colorId = 1;
|
||||
mTextList->addEntry(textListEntry);
|
||||
}
|
||||
if (mCarousel != nullptr) {
|
||||
CarouselComponent<FileData*>::Entry carouselEntry;
|
||||
carouselEntry.name = placeholder->getName();
|
||||
carouselEntry.object = placeholder;
|
||||
mCarousel->addEntry(carouselEntry, mRoot->getSystem()->getTheme());
|
||||
}
|
||||
}
|
||||
|
||||
void GamelistBase::generateFirstLetterIndex(const std::vector<FileData*>& files)
|
||||
|
@ -679,9 +704,10 @@ void GamelistBase::remove(FileData* game, bool deleteFile)
|
|||
setCursor(siblings.at(gamePos - 1));
|
||||
}
|
||||
}
|
||||
mList.remove(game);
|
||||
|
||||
if (mList.size() == 0)
|
||||
mPrimary->remove(game);
|
||||
|
||||
if (mPrimary->size() == 0)
|
||||
addPlaceholder(nullptr);
|
||||
|
||||
// If a game has been deleted, immediately remove the entry from gamelist.xml
|
||||
|
|
|
@ -21,23 +21,24 @@
|
|||
#include "components/RatingComponent.h"
|
||||
#include "components/ScrollableContainer.h"
|
||||
#include "components/TextComponent.h"
|
||||
#include "components/TextListComponent.h"
|
||||
#include "components/VideoFFmpegComponent.h"
|
||||
#include "components/primary/CarouselComponent.h"
|
||||
#include "components/primary/TextListComponent.h"
|
||||
|
||||
#include <stack>
|
||||
|
||||
class GamelistBase : public GuiComponent
|
||||
{
|
||||
public:
|
||||
FileData* getCursor() { return mList.getSelected(); }
|
||||
FileData* getCursor() { return mPrimary->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* getNextEntry() { return mPrimary->getNext(); }
|
||||
FileData* getPreviousEntry() { return mPrimary->getPrevious(); }
|
||||
FileData* getFirstEntry() { return mPrimary->getFirst(); }
|
||||
FileData* getLastEntry() { return mPrimary->getLast(); }
|
||||
FileData* getFirstGameEntry() { return mFirstGameEntry; }
|
||||
|
||||
// These functions are used to retain the folder cursor history, for instance
|
||||
|
@ -58,9 +59,10 @@ public:
|
|||
|
||||
const std::vector<std::string>& getFirstLetterIndex() { return mFirstLetterIndex; }
|
||||
|
||||
void stopListScrolling() override { mPrimary->stopScrolling(); }
|
||||
|
||||
protected:
|
||||
GamelistBase(FileData* root);
|
||||
~GamelistBase();
|
||||
|
||||
// Called when a FileData* is added, has its metadata changed, or is removed.
|
||||
virtual void onFileChanged(FileData* file, bool reloadGamelist) = 0;
|
||||
|
@ -72,14 +74,15 @@ protected:
|
|||
|
||||
virtual void launch(FileData* game) = 0;
|
||||
|
||||
bool isListScrolling() override { return mList.isScrolling(); }
|
||||
void stopListScrolling() override { mList.stopScrolling(); }
|
||||
bool isListScrolling() override { return mPrimary->isScrolling(); }
|
||||
|
||||
std::string getQuickSystemSelectRightButton() { return "right"; }
|
||||
std::string getQuickSystemSelectLeftButton() { return "left"; }
|
||||
|
||||
FileData* mRoot;
|
||||
TextListComponent<FileData*> mList;
|
||||
std::unique_ptr<CarouselComponent<FileData*>> mCarousel;
|
||||
std::unique_ptr<TextListComponent<FileData*>> mTextList;
|
||||
PrimaryComponent<FileData*>* mPrimary;
|
||||
|
||||
// Points to the first game in the list, i.e. the first entry which is of the type "GAME".
|
||||
FileData* mFirstGameEntry;
|
||||
|
@ -100,6 +103,7 @@ protected:
|
|||
bool mIsFiltered;
|
||||
bool mIsFolder;
|
||||
bool mVideoPlaying;
|
||||
bool mLeftRightAvailable;
|
||||
|
||||
private:
|
||||
};
|
||||
|
|
|
@ -67,7 +67,7 @@ void GamelistView::legacyPopulateFields()
|
|||
mImageComponents.back()->setThemeMetadata("image_md_image");
|
||||
mImageComponents.back()->setOrigin(0.5f, 0.5f);
|
||||
mImageComponents.back()->setPosition(mSize.x * 0.25f,
|
||||
mList.getPosition().y + mSize.y * 0.2125f);
|
||||
mPrimary->getPosition().y + mSize.y * 0.2125f);
|
||||
mImageComponents.back()->setMaxSize(mSize.x * (0.50f - 2.0f * padding), mSize.y * 0.4f);
|
||||
mImageComponents.back()->setDefaultZIndex(30.0f);
|
||||
mImageComponents.back()->setScrollFadeIn(true);
|
||||
|
@ -79,7 +79,7 @@ void GamelistView::legacyPopulateFields()
|
|||
mVideoComponents.back()->setThemeMetadata("video_md_video");
|
||||
mVideoComponents.back()->setOrigin(0.5f, 0.5f);
|
||||
mVideoComponents.back()->setPosition(mSize.x * 0.25f,
|
||||
mList.getPosition().y + mSize.y * 0.2125f);
|
||||
mPrimary->getPosition().y + mSize.y * 0.2125f);
|
||||
mVideoComponents.back()->setSize(mSize.x * (0.5f - 2.0f * padding), mSize.y * 0.4f);
|
||||
mVideoComponents.back()->setDefaultZIndex(30.0f);
|
||||
mVideoComponents.back()->setScrollFadeIn(true);
|
||||
|
@ -87,10 +87,11 @@ void GamelistView::legacyPopulateFields()
|
|||
addChild(mVideoComponents.back().get());
|
||||
}
|
||||
|
||||
mList.setPosition(mSize.x * (0.50f + padding), mList.getPosition().y);
|
||||
mList.setSize(mSize.x * (0.50f - padding), mList.getSize().y);
|
||||
mList.setAlignment(TextListComponent<FileData*>::ALIGN_LEFT);
|
||||
mList.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); });
|
||||
mPrimary->setPosition(mSize.x * (0.50f + padding), mPrimary->getPosition().y);
|
||||
mPrimary->setSize(mSize.x * (0.50f - padding), mPrimary->getSize().y);
|
||||
mPrimary->setAlignment(TextListComponent<FileData*>::PrimaryAlignment::ALIGN_LEFT);
|
||||
mPrimary->setCursorChangedCallback(
|
||||
[&](const CursorState& state) { legacyUpdateInfoPanel(state); });
|
||||
|
||||
// Metadata labels + values.
|
||||
mTextComponents.push_back(std::make_unique<TextComponent>());
|
||||
|
@ -210,6 +211,11 @@ void GamelistView::legacyPopulateFields()
|
|||
|
||||
void GamelistView::legacyOnThemeChanged(const std::shared_ptr<ThemeData>& theme)
|
||||
{
|
||||
if (mTextList == nullptr) {
|
||||
mTextList = std::make_unique<TextListComponent<FileData*>>();
|
||||
mPrimary = mTextList.get();
|
||||
}
|
||||
|
||||
legacyPopulateFields();
|
||||
|
||||
using namespace ThemeFlags;
|
||||
|
@ -230,7 +236,11 @@ void GamelistView::legacyOnThemeChanged(const std::shared_ptr<ThemeData>& theme)
|
|||
for (auto extra : mThemeExtras)
|
||||
addChild(extra);
|
||||
|
||||
mList.applyTheme(theme, getName(), "textlist_gamelist", ALL);
|
||||
mPrimary->setSize(mSize.x, mSize.y * 0.8f);
|
||||
mPrimary->setPosition(0.0f, mSize.y * 0.2f);
|
||||
mPrimary->setDefaultZIndex(50.0f);
|
||||
mPrimary->applyTheme(theme, getName(), "textlist_gamelist", ALL);
|
||||
addChild(mPrimary);
|
||||
|
||||
mImageComponents[LegacyImage::MD_THUMBNAIL]->applyTheme(
|
||||
theme, getName(), mImageComponents[LegacyImage::MD_THUMBNAIL]->getThemeMetadata(), ALL);
|
||||
|
@ -306,19 +316,22 @@ void GamelistView::legacyOnThemeChanged(const std::shared_ptr<ThemeData>& theme)
|
|||
container->setVisible(false);
|
||||
}
|
||||
|
||||
populateList(mRoot->getChildrenListToDisplay(), mRoot);
|
||||
sortChildren();
|
||||
mHelpStyle.applyTheme(mTheme, getName());
|
||||
}
|
||||
|
||||
void GamelistView::legacyUpdateInfoPanel()
|
||||
void GamelistView::legacyUpdateInfoPanel(const CursorState& state)
|
||||
{
|
||||
FileData* file {(mList.size() == 0 || mList.isScrolling()) ? nullptr : mList.getSelected()};
|
||||
FileData* file {(mPrimary->size() > 0 && state == CursorState::CURSOR_STOPPED) ?
|
||||
mPrimary->getSelected() :
|
||||
nullptr};
|
||||
|
||||
// If the game data has already been rendered to the info panel, then skip it this time.
|
||||
if (file == mLastUpdated)
|
||||
return;
|
||||
|
||||
if (!mList.isScrolling())
|
||||
if (state == CursorState::CURSOR_STOPPED)
|
||||
mLastUpdated = file;
|
||||
|
||||
bool hideMetaDataFields {false};
|
||||
|
@ -340,7 +353,7 @@ void GamelistView::legacyUpdateInfoPanel()
|
|||
|
||||
// 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 (mPrimary->isScrolling()) {
|
||||
if ((mLastUpdated && mLastUpdated->metadata.get("hidemetadata") == "true") ||
|
||||
(mLastUpdated->getSystem()->isCustomCollection() &&
|
||||
mLastUpdated->getPath() == mLastUpdated->getSystem()->getName()))
|
||||
|
@ -566,10 +579,6 @@ void GamelistView::legacyUpdateInfoPanel()
|
|||
|
||||
for (auto it = comps.cbegin(); it != comps.cend(); ++it) {
|
||||
GuiComponent* comp {*it};
|
||||
if (!fadingOut && !comp->isAnimationPlaying(0)) {
|
||||
comp->setOpacity(1.0f);
|
||||
continue;
|
||||
}
|
||||
// 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) ||
|
||||
|
@ -578,6 +587,9 @@ void GamelistView::legacyUpdateInfoPanel()
|
|||
comp->setAnimation(new LambdaAnimation(func, 150), 0, nullptr, fadingOut);
|
||||
}
|
||||
}
|
||||
|
||||
if (state == CursorState::CURSOR_SCROLLING)
|
||||
mLastUpdated = nullptr;
|
||||
}
|
||||
|
||||
void GamelistView::legacyUpdate(int deltaTime)
|
||||
|
|
|
@ -26,13 +26,6 @@ GamelistView::GamelistView(FileData* root)
|
|||
|
||||
if (mLegacyMode)
|
||||
return;
|
||||
|
||||
const float padding {0.01f};
|
||||
|
||||
mList.setPosition(mSize.x * (0.50f + padding), mList.getPosition().y);
|
||||
mList.setSize(mSize.x * (0.50f - padding), mList.getSize().y);
|
||||
mList.setAlignment(TextListComponent<FileData*>::ALIGN_LEFT);
|
||||
mList.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); });
|
||||
}
|
||||
|
||||
GamelistView::~GamelistView()
|
||||
|
@ -79,9 +72,11 @@ void GamelistView::onShow()
|
|||
GuiComponent::onShow();
|
||||
|
||||
if (mLegacyMode)
|
||||
legacyUpdateInfoPanel();
|
||||
legacyUpdateInfoPanel(CursorState::CURSOR_STOPPED);
|
||||
else
|
||||
updateInfoPanel();
|
||||
updateInfoPanel(CursorState::CURSOR_STOPPED);
|
||||
|
||||
mPrimary->finishAnimation(0);
|
||||
}
|
||||
|
||||
void GamelistView::onTransition()
|
||||
|
@ -111,6 +106,44 @@ void GamelistView::onThemeChanged(const std::shared_ptr<ThemeData>& theme)
|
|||
|
||||
if (mTheme->hasView("gamelist")) {
|
||||
for (auto& element : mTheme->getViewElements("gamelist").elements) {
|
||||
if (element.second.type == "textlist" || element.second.type == "carousel") {
|
||||
if (element.second.type == "carousel" && mTextList != nullptr) {
|
||||
LOG(LogWarning) << "SystemView::populate(): Multiple primary components "
|
||||
<< "defined, skipping <carousel> configuration entry";
|
||||
continue;
|
||||
}
|
||||
if (element.second.type == "textlist" && mCarousel != nullptr) {
|
||||
LOG(LogWarning) << "SystemView::populate(): Multiple primary components "
|
||||
<< "defined, skipping <textlist> configuration entry";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (element.second.type == "textlist") {
|
||||
if (mTextList == nullptr) {
|
||||
mTextList = std::make_unique<TextListComponent<FileData*>>();
|
||||
mPrimary = mTextList.get();
|
||||
}
|
||||
mPrimary->setPosition(0.2f, mSize.y * 0.2f);
|
||||
mPrimary->setSize(mSize.x * 0.7f, mSize.y * 0.6f);
|
||||
mPrimary->setAlignment(TextListComponent<FileData*>::PrimaryAlignment::ALIGN_LEFT);
|
||||
mPrimary->setCursorChangedCallback(
|
||||
[&](const CursorState& state) { updateInfoPanel(state); });
|
||||
mPrimary->setDefaultZIndex(50.0f);
|
||||
mPrimary->setZIndex(50.0f);
|
||||
mPrimary->applyTheme(theme, "gamelist", element.first, ALL);
|
||||
addChild(mPrimary);
|
||||
}
|
||||
if (element.second.type == "carousel") {
|
||||
if (mCarousel == nullptr) {
|
||||
mCarousel = std::make_unique<CarouselComponent<FileData*>>();
|
||||
mPrimary = mCarousel.get();
|
||||
}
|
||||
mPrimary->setCursorChangedCallback(
|
||||
[&](const CursorState& state) { updateInfoPanel(state); });
|
||||
mPrimary->setDefaultZIndex(50.0f);
|
||||
mPrimary->applyTheme(theme, "gamelist", element.first, ALL);
|
||||
addChild(mPrimary);
|
||||
}
|
||||
if (element.second.type == "image") {
|
||||
mImageComponents.push_back(std::make_unique<ImageComponent>());
|
||||
mImageComponents.back()->setDefaultZIndex(30.0f);
|
||||
|
@ -211,11 +244,31 @@ void GamelistView::onThemeChanged(const std::shared_ptr<ThemeData>& theme)
|
|||
addChild(mRatingComponents.back().get());
|
||||
}
|
||||
}
|
||||
|
||||
if (mPrimary == nullptr) {
|
||||
mTextList = std::make_unique<TextListComponent<FileData*>>();
|
||||
mPrimary = mTextList.get();
|
||||
mPrimary->setPosition(0.2f, mSize.y * 0.2f);
|
||||
mPrimary->setSize(mSize.x * 0.7f, mSize.y * 0.6f);
|
||||
mPrimary->setAlignment(TextListComponent<FileData*>::PrimaryAlignment::ALIGN_LEFT);
|
||||
mPrimary->setCursorChangedCallback(
|
||||
[&](const CursorState& state) { updateInfoPanel(state); });
|
||||
mPrimary->setDefaultZIndex(50.0f);
|
||||
mPrimary->setZIndex(50.0f);
|
||||
mPrimary->applyTheme(theme, "gamelist", "", ALL);
|
||||
addChild(mPrimary);
|
||||
}
|
||||
|
||||
mHelpStyle.applyTheme(mTheme, "gamelist");
|
||||
populateList(mRoot->getChildrenListToDisplay(), mRoot);
|
||||
}
|
||||
|
||||
mList.setDefaultZIndex(50.0f);
|
||||
mList.applyTheme(theme, "gamelist", "textlist_gamelist", ALL);
|
||||
// Disable quick system select if the primary component uses the left and right buttons.
|
||||
if (mCarousel != nullptr) {
|
||||
if (mCarousel->getType() == CarouselComponent<FileData*>::CarouselType::HORIZONTAL ||
|
||||
mCarousel->getType() == CarouselComponent<FileData*>::CarouselType::HORIZONTAL_WHEEL)
|
||||
mLeftRightAvailable = false;
|
||||
}
|
||||
|
||||
sortChildren();
|
||||
}
|
||||
|
@ -259,7 +312,7 @@ std::vector<HelpPrompt> GamelistView::getHelpPrompts()
|
|||
std::vector<HelpPrompt> prompts;
|
||||
|
||||
if (Settings::getInstance()->getBool("QuickSystemSelect") &&
|
||||
SystemData::sSystemVector.size() > 1)
|
||||
SystemData::sSystemVector.size() > 1 && mLeftRightAvailable)
|
||||
prompts.push_back(HelpPrompt("left/right", "system"));
|
||||
|
||||
if (mRoot->getSystem()->getThemeFolder() == "custom-collections" && mCursorStack.empty() &&
|
||||
|
@ -301,20 +354,22 @@ std::vector<HelpPrompt> GamelistView::getHelpPrompts()
|
|||
return prompts;
|
||||
}
|
||||
|
||||
void GamelistView::updateInfoPanel()
|
||||
void GamelistView::updateInfoPanel(const CursorState& state)
|
||||
{
|
||||
if (mLegacyMode) {
|
||||
legacyUpdateInfoPanel();
|
||||
legacyUpdateInfoPanel(state);
|
||||
return;
|
||||
}
|
||||
|
||||
FileData* file {(mList.size() == 0 || mList.isScrolling()) ? nullptr : mList.getSelected()};
|
||||
FileData* file {(mPrimary->size() > 0 && state == CursorState::CURSOR_STOPPED) ?
|
||||
mPrimary->getSelected() :
|
||||
nullptr};
|
||||
|
||||
// If the game data has already been rendered to the info panel, then skip it this time.
|
||||
if (file == mLastUpdated)
|
||||
return;
|
||||
|
||||
if (!mList.isScrolling())
|
||||
if (state == CursorState::CURSOR_STOPPED)
|
||||
mLastUpdated = file;
|
||||
|
||||
bool hideMetaDataFields {false};
|
||||
|
@ -336,7 +391,7 @@ void GamelistView::updateInfoPanel()
|
|||
|
||||
// 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 (state == CursorState::CURSOR_SCROLLING) {
|
||||
if ((mLastUpdated && mLastUpdated->metadata.get("hidemetadata") == "true") ||
|
||||
(mLastUpdated->getSystem()->isCustomCollection() &&
|
||||
mLastUpdated->getPath() == mLastUpdated->getSystem()->getName()))
|
||||
|
@ -686,10 +741,6 @@ void GamelistView::updateInfoPanel()
|
|||
|
||||
for (auto it = comps.cbegin(); it != comps.cend(); ++it) {
|
||||
GuiComponent* comp {*it};
|
||||
if (!fadingOut && !comp->isAnimationPlaying(0)) {
|
||||
comp->setOpacity(1.0f);
|
||||
continue;
|
||||
}
|
||||
// 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) ||
|
||||
|
@ -698,6 +749,9 @@ void GamelistView::updateInfoPanel()
|
|||
comp->setAnimation(new LambdaAnimation(func, 150), 0, nullptr, fadingOut);
|
||||
}
|
||||
}
|
||||
|
||||
if (state == CursorState::CURSOR_SCROLLING)
|
||||
mLastUpdated = nullptr;
|
||||
}
|
||||
|
||||
void GamelistView::setGameImage(FileData* file, GuiComponent* comp)
|
||||
|
|
|
@ -25,7 +25,7 @@ public:
|
|||
void onShow() override;
|
||||
void onTransition() override;
|
||||
|
||||
void preloadGamelist() { updateInfoPanel(); }
|
||||
void preloadGamelist() { updateInfoPanel(CursorState::CURSOR_STOPPED); }
|
||||
void launch(FileData* game) override { ViewController::getInstance()->triggerGameLaunch(game); }
|
||||
|
||||
std::string getName() const
|
||||
|
@ -91,13 +91,13 @@ public:
|
|||
std::vector<HelpPrompt> getHelpPrompts() override;
|
||||
|
||||
private:
|
||||
void updateInfoPanel();
|
||||
void updateInfoPanel(const CursorState& state);
|
||||
void setGameImage(FileData* file, GuiComponent* comp);
|
||||
|
||||
// Legacy (backward compatibility) functions.
|
||||
void legacyPopulateFields();
|
||||
void legacyOnThemeChanged(const std::shared_ptr<ThemeData>& theme);
|
||||
void legacyUpdateInfoPanel();
|
||||
void legacyUpdateInfoPanel(const CursorState& state);
|
||||
void legacyUpdate(int deltaTime);
|
||||
void legacyInitMDLabels();
|
||||
void legacyInitMDValues();
|
||||
|
|
|
@ -23,6 +23,8 @@
|
|||
|
||||
SystemView::SystemView()
|
||||
: mRenderer {Renderer::getInstance()}
|
||||
, mPrimary {nullptr}
|
||||
, mPrimaryType {PrimaryType::CAROUSEL}
|
||||
, mCamOffset {0.0f}
|
||||
, mFadeOpacity {0.0f}
|
||||
, mPreviousScrollVelocity {0}
|
||||
|
@ -34,20 +36,6 @@ SystemView::SystemView()
|
|||
, mFadeTransitions {false}
|
||||
{
|
||||
setSize(Renderer::getScreenWidth(), Renderer::getScreenHeight());
|
||||
|
||||
mCarousel = std::make_unique<CarouselComponent>();
|
||||
mCarousel->setCursorChangedCallback([&](const CursorState& state) { onCursorChanged(state); });
|
||||
mCarousel->setCancelTransitionsCallback([&] {
|
||||
ViewController::getInstance()->cancelViewTransitions();
|
||||
mNavigated = true;
|
||||
if (mSystemElements.size() > 1) {
|
||||
for (auto& anim : mSystemElements[mCarousel->getCursor()].lottieAnimComponents)
|
||||
anim->setPauseAnimation(true);
|
||||
for (auto& anim : mSystemElements[mCarousel->getCursor()].GIFAnimComponents)
|
||||
anim->setPauseAnimation(true);
|
||||
}
|
||||
});
|
||||
|
||||
populate();
|
||||
}
|
||||
|
||||
|
@ -65,29 +53,29 @@ SystemView::~SystemView()
|
|||
|
||||
void SystemView::onTransition()
|
||||
{
|
||||
for (auto& anim : mSystemElements[mCarousel->getCursor()].lottieAnimComponents)
|
||||
for (auto& anim : mSystemElements[mPrimary->getCursor()].lottieAnimComponents)
|
||||
anim->setPauseAnimation(true);
|
||||
|
||||
for (auto& anim : mSystemElements[mCarousel->getCursor()].GIFAnimComponents)
|
||||
for (auto& anim : mSystemElements[mPrimary->getCursor()].GIFAnimComponents)
|
||||
anim->setPauseAnimation(true);
|
||||
}
|
||||
|
||||
void SystemView::goToSystem(SystemData* system, bool animate)
|
||||
{
|
||||
mCarousel->setCursor(system);
|
||||
mPrimary->setCursor(system);
|
||||
|
||||
for (auto& selector : mSystemElements[mCarousel->getCursor()].gameSelectors) {
|
||||
for (auto& selector : mSystemElements[mPrimary->getCursor()].gameSelectors) {
|
||||
if (selector->getGameSelection() == GameSelectorComponent::GameSelection::RANDOM)
|
||||
selector->setNeedsRefresh();
|
||||
}
|
||||
|
||||
for (auto& video : mSystemElements[mCarousel->getCursor()].videoComponents)
|
||||
for (auto& video : mSystemElements[mPrimary->getCursor()].videoComponents)
|
||||
video->setStaticVideo();
|
||||
|
||||
for (auto& anim : mSystemElements[mCarousel->getCursor()].lottieAnimComponents)
|
||||
for (auto& anim : mSystemElements[mPrimary->getCursor()].lottieAnimComponents)
|
||||
anim->resetFileAnimation();
|
||||
|
||||
for (auto& anim : mSystemElements[mCarousel->getCursor()].GIFAnimComponents)
|
||||
for (auto& anim : mSystemElements[mPrimary->getCursor()].GIFAnimComponents)
|
||||
anim->resetFileAnimation();
|
||||
|
||||
updateGameSelectors();
|
||||
|
@ -111,9 +99,9 @@ bool SystemView::input(InputConfig* config, Input input)
|
|||
}
|
||||
|
||||
if (config->isMappedTo("a", input)) {
|
||||
mCarousel->stopScrolling();
|
||||
mPrimary->stopScrolling();
|
||||
pauseViewVideos();
|
||||
ViewController::getInstance()->goToGamelist(mCarousel->getSelected());
|
||||
ViewController::getInstance()->goToGamelist(mPrimary->getSelected());
|
||||
NavigationSounds::getInstance().playThemeNavigationSound(SELECTSOUND);
|
||||
return true;
|
||||
}
|
||||
|
@ -122,7 +110,7 @@ bool SystemView::input(InputConfig* config, Input input)
|
|||
config->isMappedTo("rightthumbstickclick", input))) {
|
||||
// Get a random system and jump to it.
|
||||
NavigationSounds::getInstance().playThemeNavigationSound(SYSTEMBROWSESOUND);
|
||||
mCarousel->setCursor(SystemData::getRandomSystem(mCarousel->getSelected()));
|
||||
mPrimary->setCursor(SystemData::getRandomSystem(mPrimary->getSelected()));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -138,23 +126,23 @@ bool SystemView::input(InputConfig* config, Input input)
|
|||
}
|
||||
}
|
||||
|
||||
return mCarousel->input(config, input);
|
||||
return mPrimary->input(config, input);
|
||||
}
|
||||
|
||||
void SystemView::update(int deltaTime)
|
||||
{
|
||||
if (!mCarousel->isAnimationPlaying(0))
|
||||
if (!mPrimary->isAnimationPlaying(0))
|
||||
mMaxFade = false;
|
||||
|
||||
mCarousel->update(deltaTime);
|
||||
mPrimary->update(deltaTime);
|
||||
|
||||
for (auto& video : mSystemElements[mCarousel->getCursor()].videoComponents)
|
||||
for (auto& video : mSystemElements[mPrimary->getCursor()].videoComponents)
|
||||
video->update(deltaTime);
|
||||
|
||||
for (auto& anim : mSystemElements[mCarousel->getCursor()].lottieAnimComponents)
|
||||
for (auto& anim : mSystemElements[mPrimary->getCursor()].lottieAnimComponents)
|
||||
anim->update(deltaTime);
|
||||
|
||||
for (auto& anim : mSystemElements[mCarousel->getCursor()].GIFAnimComponents)
|
||||
for (auto& anim : mSystemElements[mPrimary->getCursor()].GIFAnimComponents)
|
||||
anim->update(deltaTime);
|
||||
|
||||
GuiComponent::update(deltaTime);
|
||||
|
@ -162,7 +150,7 @@ void SystemView::update(int deltaTime)
|
|||
|
||||
void SystemView::render(const glm::mat4& parentTrans)
|
||||
{
|
||||
if (mCarousel->getNumEntries() == 0)
|
||||
if (mPrimary->getNumEntries() == 0)
|
||||
return; // Nothing to render.
|
||||
|
||||
bool fade {false};
|
||||
|
@ -174,7 +162,7 @@ void SystemView::render(const glm::mat4& parentTrans)
|
|||
renderElements(parentTrans, false);
|
||||
glm::mat4 trans {getTransform() * parentTrans};
|
||||
|
||||
mCarousel->render(trans);
|
||||
mPrimary->render(trans);
|
||||
|
||||
// For legacy themes the carousel is always rendered on top of all other elements.
|
||||
if (!mLegacyMode && !fade)
|
||||
|
@ -191,11 +179,16 @@ void SystemView::onThemeChanged(const std::shared_ptr<ThemeData>& /*theme*/)
|
|||
std::vector<HelpPrompt> SystemView::getHelpPrompts()
|
||||
{
|
||||
std::vector<HelpPrompt> prompts;
|
||||
if (mCarousel->getType() == CarouselComponent::VERTICAL ||
|
||||
mCarousel->getType() == CarouselComponent::VERTICAL_WHEEL)
|
||||
if (mCarousel != nullptr) {
|
||||
if (mCarousel->getType() == CarouselComponent<SystemData*>::CarouselType::VERTICAL ||
|
||||
mCarousel->getType() == CarouselComponent<SystemData*>::CarouselType::VERTICAL_WHEEL)
|
||||
prompts.push_back(HelpPrompt("up/down", "choose"));
|
||||
else
|
||||
prompts.push_back(HelpPrompt("left/right", "choose"));
|
||||
}
|
||||
else if (mTextList != nullptr) {
|
||||
prompts.push_back(HelpPrompt("up/down", "choose"));
|
||||
else
|
||||
prompts.push_back(HelpPrompt("left/right", "choose"));
|
||||
}
|
||||
|
||||
prompts.push_back(HelpPrompt("a", "select"));
|
||||
|
||||
|
@ -209,9 +202,9 @@ std::vector<HelpPrompt> SystemView::getHelpPrompts()
|
|||
return prompts;
|
||||
}
|
||||
|
||||
void SystemView::onCursorChanged(const CursorState& /*state*/)
|
||||
void SystemView::onCursorChanged(const CursorState& state)
|
||||
{
|
||||
int cursor {mCarousel->getCursor()};
|
||||
int cursor {mPrimary->getCursor()};
|
||||
|
||||
for (auto& selector : mSystemElements[cursor].gameSelectors) {
|
||||
if (selector->getGameSelection() == GameSelectorComponent::GameSelection::RANDOM)
|
||||
|
@ -221,34 +214,41 @@ void SystemView::onCursorChanged(const CursorState& /*state*/)
|
|||
for (auto& video : mSystemElements[cursor].videoComponents)
|
||||
video->setStaticVideo();
|
||||
|
||||
for (auto& anim : mSystemElements[mCarousel->getCursor()].lottieAnimComponents)
|
||||
for (auto& anim : mSystemElements[mPrimary->getCursor()].lottieAnimComponents)
|
||||
anim->resetFileAnimation();
|
||||
|
||||
for (auto& anim : mSystemElements[mCarousel->getCursor()].GIFAnimComponents)
|
||||
for (auto& anim : mSystemElements[mPrimary->getCursor()].GIFAnimComponents)
|
||||
anim->resetFileAnimation();
|
||||
|
||||
updateGameSelectors();
|
||||
startViewVideos();
|
||||
updateHelpPrompts();
|
||||
|
||||
int scrollVelocity {mCarousel->getScrollingVelocity()};
|
||||
int scrollVelocity {mPrimary->getScrollingVelocity()};
|
||||
float startPos {mCamOffset};
|
||||
float posMax {static_cast<float>(mCarousel->getNumEntries())};
|
||||
float posMax {static_cast<float>(mPrimary->getNumEntries())};
|
||||
float target {static_cast<float>(cursor)};
|
||||
float endPos {target};
|
||||
|
||||
// Find the shortest path to the target.
|
||||
float endPos {target}; // Directly.
|
||||
float dist {fabs(endPos - startPos)};
|
||||
if (mPreviousScrollVelocity > 0 && scrollVelocity == 0 && mCamOffset > posMax - 1.0f)
|
||||
startPos = 0.0f;
|
||||
|
||||
if (fabs(target + posMax - startPos - scrollVelocity) < dist)
|
||||
endPos = target + posMax; // Loop around the end (0 -> max).
|
||||
if (fabs(target - posMax - startPos - scrollVelocity) < dist)
|
||||
endPos = target - posMax; // Loop around the start (max - 1 -> -1).
|
||||
if (mPrimaryType == PrimaryType::CAROUSEL) {
|
||||
// Find the shortest path to the target.
|
||||
float dist {fabs(endPos - startPos)};
|
||||
|
||||
if (fabs(target + posMax - startPos - scrollVelocity) < dist)
|
||||
endPos = target + posMax; // Loop around the end (0 -> max).
|
||||
if (fabs(target - posMax - startPos - scrollVelocity) < dist)
|
||||
endPos = target - posMax; // Loop around the start (max - 1 -> -1).
|
||||
}
|
||||
|
||||
// Make sure transitions do not animate in reverse.
|
||||
bool changedDirection {false};
|
||||
if (mPreviousScrollVelocity != 0 && mPreviousScrollVelocity != scrollVelocity)
|
||||
changedDirection = true;
|
||||
if (mPreviousScrollVelocity != 0 && mPreviousScrollVelocity != scrollVelocity) {
|
||||
if (scrollVelocity > 0 && startPos + scrollVelocity < posMax)
|
||||
changedDirection = true;
|
||||
}
|
||||
|
||||
if (!changedDirection && scrollVelocity > 0 && endPos < startPos)
|
||||
endPos = endPos + posMax;
|
||||
|
@ -374,8 +374,27 @@ void SystemView::populate()
|
|||
std::string logoPath;
|
||||
std::string defaultLogoPath;
|
||||
|
||||
if (mLegacyMode && mViewNeedsReload)
|
||||
if (mLegacyMode && mViewNeedsReload) {
|
||||
if (mCarousel == nullptr) {
|
||||
mCarousel = std::make_unique<CarouselComponent<SystemData*>>();
|
||||
mPrimary = mCarousel.get();
|
||||
mPrimary->setDefaultZIndex(50.0f);
|
||||
mPrimary->setCursorChangedCallback(
|
||||
[&](const CursorState& state) { onCursorChanged(state); });
|
||||
mPrimary->setCancelTransitionsCallback([&] {
|
||||
ViewController::getInstance()->cancelViewTransitions();
|
||||
mNavigated = true;
|
||||
if (mSystemElements.size() > 1) {
|
||||
for (auto& anim :
|
||||
mSystemElements[mPrimary->getCursor()].lottieAnimComponents)
|
||||
anim->setPauseAnimation(true);
|
||||
for (auto& anim : mSystemElements[mPrimary->getCursor()].GIFAnimComponents)
|
||||
anim->setPauseAnimation(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
legacyApplyTheme(theme);
|
||||
}
|
||||
|
||||
if (mLegacyMode) {
|
||||
SystemViewElements elements;
|
||||
|
@ -404,12 +423,51 @@ void SystemView::populate()
|
|||
ThemeFlags::ALL);
|
||||
elements.gameSelectors.back()->setNeedsRefresh();
|
||||
}
|
||||
if (element.second.type == "carousel") {
|
||||
mCarousel->applyTheme(theme, "system", element.first, ThemeFlags::ALL);
|
||||
if (element.second.has("logo"))
|
||||
logoPath = element.second.get<std::string>("logo");
|
||||
if (element.second.has("defaultLogo"))
|
||||
defaultLogoPath = element.second.get<std::string>("defaultLogo");
|
||||
if (element.second.type == "textlist" || element.second.type == "carousel") {
|
||||
if (element.second.type == "carousel" && mTextList != nullptr) {
|
||||
LOG(LogWarning)
|
||||
<< "SystemView::populate(): Multiple primary components "
|
||||
<< "defined, skipping <carousel> configuration entry";
|
||||
continue;
|
||||
}
|
||||
if (element.second.type == "textlist" && mCarousel != nullptr) {
|
||||
LOG(LogWarning)
|
||||
<< "SystemView::populate(): Multiple primary components "
|
||||
<< "defined, skipping <textlist> configuration entry";
|
||||
continue;
|
||||
}
|
||||
if (element.second.type == "carousel" && mCarousel == nullptr) {
|
||||
mCarousel = std::make_unique<CarouselComponent<SystemData*>>();
|
||||
mPrimary = mCarousel.get();
|
||||
mPrimaryType = PrimaryType::CAROUSEL;
|
||||
}
|
||||
else if (element.second.type == "textlist" && mTextList == nullptr) {
|
||||
mTextList = std::make_unique<TextListComponent<SystemData*>>();
|
||||
mPrimary = mTextList.get();
|
||||
mPrimaryType = PrimaryType::TEXTLIST;
|
||||
}
|
||||
mPrimary->setDefaultZIndex(50.0f);
|
||||
mPrimary->applyTheme(theme, "system", element.first, ThemeFlags::ALL);
|
||||
mPrimary->setCursorChangedCallback(
|
||||
[&](const CursorState& state) { onCursorChanged(state); });
|
||||
mPrimary->setCancelTransitionsCallback([&] {
|
||||
ViewController::getInstance()->cancelViewTransitions();
|
||||
mNavigated = true;
|
||||
if (mSystemElements.size() > 1) {
|
||||
for (auto& anim :
|
||||
mSystemElements[mPrimary->getCursor()].lottieAnimComponents)
|
||||
anim->setPauseAnimation(true);
|
||||
for (auto& anim :
|
||||
mSystemElements[mPrimary->getCursor()].GIFAnimComponents)
|
||||
anim->setPauseAnimation(true);
|
||||
}
|
||||
});
|
||||
if (mCarousel != nullptr) {
|
||||
if (element.second.has("logo"))
|
||||
logoPath = element.second.get<std::string>("logo");
|
||||
if (element.second.has("defaultLogo"))
|
||||
defaultLogoPath = element.second.get<std::string>("defaultLogo");
|
||||
}
|
||||
}
|
||||
else if (element.second.type == "image") {
|
||||
elements.imageComponents.emplace_back(std::make_unique<ImageComponent>());
|
||||
|
@ -500,10 +558,6 @@ void SystemView::populate()
|
|||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Apply default carousel configuration.
|
||||
mCarousel->applyTheme(theme, "system", "", ThemeFlags::ALL);
|
||||
}
|
||||
|
||||
std::stable_sort(
|
||||
elements.children.begin(), elements.children.end(),
|
||||
|
@ -523,13 +577,41 @@ void SystemView::populate()
|
|||
mSystemElements.back().helpStyle.applyTheme(theme, "system");
|
||||
}
|
||||
|
||||
CarouselComponent::Entry entry;
|
||||
entry.name = it->getName();
|
||||
entry.object = it;
|
||||
entry.data.logoPath = logoPath;
|
||||
entry.data.defaultLogoPath = defaultLogoPath;
|
||||
if (mPrimary == nullptr) {
|
||||
mCarousel = std::make_unique<CarouselComponent<SystemData*>>();
|
||||
mPrimary = mCarousel.get();
|
||||
mPrimaryType = PrimaryType::CAROUSEL;
|
||||
mPrimary->setDefaultZIndex(50.0f);
|
||||
mPrimary->applyTheme(theme, "system", "", ThemeFlags::ALL);
|
||||
mPrimary->setCursorChangedCallback(
|
||||
[&](const CursorState& state) { onCursorChanged(state); });
|
||||
mPrimary->setCancelTransitionsCallback([&] {
|
||||
ViewController::getInstance()->cancelViewTransitions();
|
||||
mNavigated = true;
|
||||
if (mSystemElements.size() > 1) {
|
||||
for (auto& anim : mSystemElements[mPrimary->getCursor()].lottieAnimComponents)
|
||||
anim->setPauseAnimation(true);
|
||||
for (auto& anim : mSystemElements[mPrimary->getCursor()].GIFAnimComponents)
|
||||
anim->setPauseAnimation(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mCarousel->addEntry(theme, entry, mLegacyMode);
|
||||
if (mCarousel != nullptr) {
|
||||
CarouselComponent<SystemData*>::Entry entry;
|
||||
entry.name = it->getName();
|
||||
entry.object = it;
|
||||
entry.data.logoPath = logoPath;
|
||||
entry.data.defaultLogoPath = defaultLogoPath;
|
||||
mCarousel->addEntry(entry, theme);
|
||||
}
|
||||
if (mTextList != nullptr) {
|
||||
TextListComponent<SystemData*>::Entry entry;
|
||||
entry.name = it->getFullName();
|
||||
entry.object = it;
|
||||
entry.data.colorId = 0;
|
||||
mTextList->addEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& elements : mSystemElements) {
|
||||
|
@ -545,7 +627,7 @@ void SystemView::populate()
|
|||
}
|
||||
}
|
||||
|
||||
if (mCarousel->getNumEntries() == 0) {
|
||||
if (mPrimary->getNumEntries() == 0) {
|
||||
// Something is wrong, there is not a single system to show, check if UI mode is not full.
|
||||
if (!UIModeController::getInstance()->isUIModeFull()) {
|
||||
Settings::getInstance()->setString("UIMode", "full");
|
||||
|
@ -562,21 +644,21 @@ void SystemView::populate()
|
|||
void SystemView::updateGameCount()
|
||||
{
|
||||
std::pair<unsigned int, unsigned int> gameCount =
|
||||
mCarousel->getSelected()->getDisplayedGameCount();
|
||||
mPrimary->getSelected()->getDisplayedGameCount();
|
||||
std::stringstream ss;
|
||||
std::stringstream ssGames;
|
||||
std::stringstream ssFavorites;
|
||||
bool games {false};
|
||||
|
||||
if (!mCarousel->getSelected()->isGameSystem()) {
|
||||
if (!mPrimary->getSelected()->isGameSystem()) {
|
||||
ss << "Configuration";
|
||||
}
|
||||
else if (mCarousel->getSelected()->isCollection() &&
|
||||
(mCarousel->getSelected()->getName() == "favorites")) {
|
||||
else if (mPrimary->getSelected()->isCollection() &&
|
||||
(mPrimary->getSelected()->getName() == "favorites")) {
|
||||
ss << gameCount.first << " Game" << (gameCount.first == 1 ? " " : "s");
|
||||
}
|
||||
else if (mCarousel->getSelected()->isCollection() &&
|
||||
(mCarousel->getSelected()->getName() == "recent")) {
|
||||
else if (mPrimary->getSelected()->isCollection() &&
|
||||
(mPrimary->getSelected()->getName() == "recent")) {
|
||||
// The "recent" gamelist has probably been trimmed after sorting, so we'll cap it at
|
||||
// its maximum limit of 50 games.
|
||||
ss << (gameCount.first > 50 ? 50 : gameCount.first) << " Game"
|
||||
|
@ -594,7 +676,7 @@ void SystemView::updateGameCount()
|
|||
mLegacySystemInfo->setText(ss.str());
|
||||
}
|
||||
else {
|
||||
for (auto& gameCount : mSystemElements[mCarousel->getCursor()].gameCountComponents) {
|
||||
for (auto& gameCount : mSystemElements[mPrimary->getCursor()].gameCountComponents) {
|
||||
if (gameCount->getThemeSystemdata() == "gamecount") {
|
||||
gameCount->setValue(ss.str());
|
||||
}
|
||||
|
@ -619,7 +701,7 @@ void SystemView::updateGameSelectors()
|
|||
if (mLegacyMode)
|
||||
return;
|
||||
|
||||
int cursor {mCarousel->getCursor()};
|
||||
int cursor {mPrimary->getCursor()};
|
||||
|
||||
if (mSystemElements[cursor].gameSelectors.size() == 0)
|
||||
return;
|
||||
|
@ -1052,10 +1134,13 @@ void SystemView::legacyApplyTheme(const std::shared_ptr<ThemeData>& theme)
|
|||
else
|
||||
mViewNeedsReload = true;
|
||||
|
||||
mCarousel->applyTheme(theme, "system", "carousel_systemcarousel", ThemeFlags::ALL);
|
||||
if (mCarousel != nullptr)
|
||||
mPrimary->applyTheme(theme, "system", "carousel_systemcarousel", ThemeFlags::ALL);
|
||||
else if (mTextList != nullptr)
|
||||
mPrimary->applyTheme(theme, "system", "textlist_gamelist", ThemeFlags::ALL);
|
||||
|
||||
mLegacySystemInfo->setSize(mSize.x, mLegacySystemInfo->getFont()->getLetterHeight() * 2.2f);
|
||||
mLegacySystemInfo->setPosition(0.0f, mCarousel->getPosition().y + mCarousel->getSize().y);
|
||||
mLegacySystemInfo->setPosition(0.0f, mPrimary->getPosition().y + mPrimary->getSize().y);
|
||||
mLegacySystemInfo->setBackgroundColor(0xDDDDDDD8);
|
||||
mLegacySystemInfo->setRenderBackground(true);
|
||||
mLegacySystemInfo->setFont(
|
||||
|
@ -1076,33 +1161,41 @@ void SystemView::renderElements(const glm::mat4& parentTrans, bool abovePrimary)
|
|||
{
|
||||
glm::mat4 trans {getTransform() * parentTrans};
|
||||
|
||||
const float primaryZIndex {mCarousel->getZIndex()};
|
||||
const float primaryZIndex {mPrimary->getZIndex()};
|
||||
|
||||
int renderLeft {static_cast<int>(mCamOffset)};
|
||||
int renderRight {static_cast<int>(mCamOffset)};
|
||||
int renderBefore {static_cast<int>(mCamOffset)};
|
||||
int renderAfter {static_cast<int>(mCamOffset)};
|
||||
|
||||
// If we're transitioning then also render the previous and next systems.
|
||||
if (mCarousel->isAnimationPlaying(0)) {
|
||||
renderLeft -= 1;
|
||||
renderRight += 1;
|
||||
if (mPrimary->isAnimationPlaying(0)) {
|
||||
renderBefore -= 1;
|
||||
renderAfter += 1;
|
||||
}
|
||||
|
||||
for (int i = renderLeft; i <= renderRight; ++i) {
|
||||
for (int i = renderBefore; i <= renderAfter; ++i) {
|
||||
int index {i};
|
||||
while (index < 0)
|
||||
index += static_cast<int>(mCarousel->getNumEntries());
|
||||
while (index >= static_cast<int>(mCarousel->getNumEntries()))
|
||||
index -= static_cast<int>(mCarousel->getNumEntries());
|
||||
index += static_cast<int>(mPrimary->getNumEntries());
|
||||
while (index >= static_cast<int>(mPrimary->getNumEntries()))
|
||||
index -= static_cast<int>(mPrimary->getNumEntries());
|
||||
|
||||
if (mCarousel->isAnimationPlaying(0) || index == mCarousel->getCursor()) {
|
||||
if (mPrimary->isAnimationPlaying(0) || index == mPrimary->getCursor()) {
|
||||
glm::mat4 elementTrans {trans};
|
||||
if (mCarousel->getType() == CarouselComponent::HORIZONTAL ||
|
||||
mCarousel->getType() == CarouselComponent::HORIZONTAL_WHEEL)
|
||||
elementTrans = glm::translate(elementTrans,
|
||||
glm::vec3 {(i - mCamOffset) * mSize.x, 0.0f, 0.0f});
|
||||
else
|
||||
if (mCarousel != nullptr) {
|
||||
if (mCarousel->getType() ==
|
||||
CarouselComponent<SystemData*>::CarouselType::HORIZONTAL ||
|
||||
mCarousel->getType() ==
|
||||
CarouselComponent<SystemData*>::CarouselType::HORIZONTAL_WHEEL)
|
||||
elementTrans = glm::translate(
|
||||
elementTrans, glm::vec3 {(i - mCamOffset) * mSize.x, 0.0f, 0.0f});
|
||||
else
|
||||
elementTrans = glm::translate(
|
||||
elementTrans, glm::vec3 {0.0f, (i - mCamOffset) * mSize.y, 0.0f});
|
||||
}
|
||||
else if (mTextList != nullptr) {
|
||||
elementTrans = glm::translate(elementTrans,
|
||||
glm::vec3 {0.0f, (i - mCamOffset) * mSize.y, 0.0f});
|
||||
}
|
||||
|
||||
mRenderer->pushClipRect(
|
||||
glm::ivec2 {static_cast<int>(glm::round(elementTrans[3].x)),
|
||||
|
|
|
@ -13,42 +13,26 @@
|
|||
#include "GuiComponent.h"
|
||||
#include "Sound.h"
|
||||
#include "SystemData.h"
|
||||
#include "components/CarouselComponent.h"
|
||||
#include "components/DateTimeComponent.h"
|
||||
#include "components/GIFAnimComponent.h"
|
||||
#include "components/GameSelectorComponent.h"
|
||||
#include "components/LottieAnimComponent.h"
|
||||
#include "components/RatingComponent.h"
|
||||
#include "components/TextComponent.h"
|
||||
#include "components/TextListComponent.h"
|
||||
#include "components/VideoFFmpegComponent.h"
|
||||
#include "components/primary/CarouselComponent.h"
|
||||
#include "components/primary/TextListComponent.h"
|
||||
#include "resources/Font.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
class SystemData;
|
||||
|
||||
struct SystemViewElements {
|
||||
HelpStyle helpStyle;
|
||||
std::string name;
|
||||
std::string fullName;
|
||||
std::vector<std::unique_ptr<GameSelectorComponent>> gameSelectors;
|
||||
std::vector<GuiComponent*> legacyExtras;
|
||||
std::vector<GuiComponent*> children;
|
||||
|
||||
std::vector<std::unique_ptr<ImageComponent>> imageComponents;
|
||||
std::vector<std::unique_ptr<VideoFFmpegComponent>> videoComponents;
|
||||
std::vector<std::unique_ptr<LottieAnimComponent>> lottieAnimComponents;
|
||||
std::vector<std::unique_ptr<GIFAnimComponent>> GIFAnimComponents;
|
||||
std::vector<std::unique_ptr<TextComponent>> gameCountComponents;
|
||||
std::vector<std::unique_ptr<TextComponent>> textComponents;
|
||||
std::vector<std::unique_ptr<DateTimeComponent>> dateTimeComponents;
|
||||
std::vector<std::unique_ptr<RatingComponent>> ratingComponents;
|
||||
};
|
||||
|
||||
class SystemView : public GuiComponent
|
||||
{
|
||||
public:
|
||||
using PrimaryType = PrimaryComponent<SystemData*>::PrimaryType;
|
||||
|
||||
SystemView();
|
||||
~SystemView();
|
||||
|
||||
|
@ -59,47 +43,49 @@ public:
|
|||
void update(int deltaTime) override;
|
||||
void render(const glm::mat4& parentTrans) override;
|
||||
|
||||
bool isScrolling() { return mCarousel->isScrolling(); }
|
||||
void stopScrolling() { mCarousel->stopScrolling(); }
|
||||
bool isSystemAnimationPlaying(unsigned char slot)
|
||||
{
|
||||
return mCarousel->isAnimationPlaying(slot);
|
||||
}
|
||||
bool isScrolling() { return mPrimary->isScrolling(); }
|
||||
void stopScrolling() { mPrimary->stopScrolling(); }
|
||||
bool isSystemAnimationPlaying(unsigned char slot) { return mPrimary->isAnimationPlaying(slot); }
|
||||
void finishSystemAnimation(unsigned char slot)
|
||||
{
|
||||
finishAnimation(slot);
|
||||
mCarousel->finishAnimation(slot);
|
||||
mPrimary->finishAnimation(slot);
|
||||
}
|
||||
|
||||
CarouselComponent::CarouselType getCarouselType() { return mCarousel->getType(); }
|
||||
SystemData* getFirstSystem() { return mCarousel->getFirst(); }
|
||||
PrimaryComponent<SystemData*>::PrimaryType getPrimaryType() { return mPrimaryType; }
|
||||
CarouselComponent<SystemData*>::CarouselType getCarouselType()
|
||||
{
|
||||
return (mCarousel != nullptr) ? mCarousel->getType() :
|
||||
CarouselComponent<SystemData*>::CarouselType::NO_CAROUSEL;
|
||||
}
|
||||
SystemData* getFirstSystem() { return mPrimary->getFirst(); }
|
||||
|
||||
void startViewVideos() override
|
||||
{
|
||||
for (auto& video : mSystemElements[mCarousel->getCursor()].videoComponents)
|
||||
for (auto& video : mSystemElements[mPrimary->getCursor()].videoComponents)
|
||||
video->startVideoPlayer();
|
||||
}
|
||||
void stopViewVideos() override
|
||||
{
|
||||
for (auto& video : mSystemElements[mCarousel->getCursor()].videoComponents)
|
||||
for (auto& video : mSystemElements[mPrimary->getCursor()].videoComponents)
|
||||
video->stopVideoPlayer();
|
||||
}
|
||||
void pauseViewVideos() override
|
||||
{
|
||||
for (auto& video : mSystemElements[mCarousel->getCursor()].videoComponents) {
|
||||
for (auto& video : mSystemElements[mPrimary->getCursor()].videoComponents) {
|
||||
video->pauseVideoPlayer();
|
||||
}
|
||||
}
|
||||
void muteViewVideos() override
|
||||
{
|
||||
for (auto& video : mSystemElements[mCarousel->getCursor()].videoComponents)
|
||||
for (auto& video : mSystemElements[mPrimary->getCursor()].videoComponents)
|
||||
video->muteVideoPlayer();
|
||||
}
|
||||
|
||||
void onThemeChanged(const std::shared_ptr<ThemeData>& theme);
|
||||
|
||||
std::vector<HelpPrompt> getHelpPrompts() override;
|
||||
HelpStyle getHelpStyle() override { return mSystemElements[mCarousel->getCursor()].helpStyle; }
|
||||
HelpStyle getHelpStyle() override { return mSystemElements[mPrimary->getCursor()].helpStyle; }
|
||||
|
||||
protected:
|
||||
void onCursorChanged(const CursorState& state);
|
||||
|
@ -111,10 +97,31 @@ private:
|
|||
void legacyApplyTheme(const std::shared_ptr<ThemeData>& theme);
|
||||
void renderElements(const glm::mat4& parentTrans, bool abovePrimary);
|
||||
|
||||
struct SystemViewElements {
|
||||
HelpStyle helpStyle;
|
||||
std::string name;
|
||||
std::string fullName;
|
||||
std::vector<std::unique_ptr<GameSelectorComponent>> gameSelectors;
|
||||
std::vector<GuiComponent*> legacyExtras;
|
||||
std::vector<GuiComponent*> children;
|
||||
|
||||
std::vector<std::unique_ptr<ImageComponent>> imageComponents;
|
||||
std::vector<std::unique_ptr<VideoFFmpegComponent>> videoComponents;
|
||||
std::vector<std::unique_ptr<LottieAnimComponent>> lottieAnimComponents;
|
||||
std::vector<std::unique_ptr<GIFAnimComponent>> GIFAnimComponents;
|
||||
std::vector<std::unique_ptr<TextComponent>> gameCountComponents;
|
||||
std::vector<std::unique_ptr<TextComponent>> textComponents;
|
||||
std::vector<std::unique_ptr<DateTimeComponent>> dateTimeComponents;
|
||||
std::vector<std::unique_ptr<RatingComponent>> ratingComponents;
|
||||
};
|
||||
|
||||
Renderer* mRenderer;
|
||||
std::unique_ptr<CarouselComponent> mCarousel;
|
||||
std::unique_ptr<CarouselComponent<SystemData*>> mCarousel;
|
||||
std::unique_ptr<TextListComponent<SystemData*>> mTextList;
|
||||
std::unique_ptr<TextComponent> mLegacySystemInfo;
|
||||
std::vector<SystemViewElements> mSystemElements;
|
||||
PrimaryComponent<SystemData*>* mPrimary;
|
||||
PrimaryType mPrimaryType;
|
||||
|
||||
float mCamOffset;
|
||||
float mFadeOpacity;
|
||||
|
|
|
@ -355,19 +355,33 @@ void ViewController::goToSystemView(SystemData* system, bool playTransition)
|
|||
if (applicationStartup) {
|
||||
mCamera = glm::translate(mCamera, -mCurrentView->getPosition());
|
||||
if (Settings::getInstance()->getString("TransitionStyle") == "slide") {
|
||||
if (getSystemListView()->getCarouselType() == CarouselComponent::HORIZONTAL ||
|
||||
getSystemListView()->getCarouselType() == CarouselComponent::HORIZONTAL_WHEEL)
|
||||
if (getSystemListView()->getPrimaryType() == SystemView::PrimaryType::CAROUSEL) {
|
||||
if (getSystemListView()->getCarouselType() ==
|
||||
CarouselComponent<SystemData*>::CarouselType::HORIZONTAL ||
|
||||
getSystemListView()->getCarouselType() ==
|
||||
CarouselComponent<SystemData*>::CarouselType::HORIZONTAL_WHEEL)
|
||||
mCamera[3].y += Renderer::getScreenHeight();
|
||||
else
|
||||
mCamera[3].x -= Renderer::getScreenWidth();
|
||||
}
|
||||
else if (getSystemListView()->getPrimaryType() == SystemView::PrimaryType::TEXTLIST) {
|
||||
mCamera[3].y += Renderer::getScreenHeight();
|
||||
else
|
||||
mCamera[3].x -= Renderer::getScreenWidth();
|
||||
}
|
||||
updateHelpPrompts();
|
||||
}
|
||||
else if (Settings::getInstance()->getString("TransitionStyle") == "fade") {
|
||||
if (getSystemListView()->getCarouselType() == CarouselComponent::HORIZONTAL ||
|
||||
getSystemListView()->getCarouselType() == CarouselComponent::HORIZONTAL_WHEEL)
|
||||
if (getSystemListView()->getPrimaryType() == SystemView::PrimaryType::CAROUSEL) {
|
||||
if (getSystemListView()->getCarouselType() ==
|
||||
CarouselComponent<SystemData*>::CarouselType::HORIZONTAL ||
|
||||
getSystemListView()->getCarouselType() ==
|
||||
CarouselComponent<SystemData*>::CarouselType::HORIZONTAL_WHEEL)
|
||||
mCamera[3].y += Renderer::getScreenHeight();
|
||||
else
|
||||
mCamera[3].x += Renderer::getScreenWidth();
|
||||
}
|
||||
else if (getSystemListView()->getPrimaryType() == SystemView::PrimaryType::TEXTLIST) {
|
||||
mCamera[3].y += Renderer::getScreenHeight();
|
||||
else
|
||||
mCamera[3].x += Renderer::getScreenWidth();
|
||||
}
|
||||
}
|
||||
else {
|
||||
updateHelpPrompts();
|
||||
|
@ -415,9 +429,9 @@ void ViewController::goToPrevGamelist()
|
|||
|
||||
void ViewController::goToGamelist(SystemData* system)
|
||||
{
|
||||
bool wrapFirstToLast = false;
|
||||
bool wrapLastToFirst = false;
|
||||
bool slideTransitions = false;
|
||||
bool wrapFirstToLast {false};
|
||||
bool wrapLastToFirst {false};
|
||||
bool slideTransitions {false};
|
||||
|
||||
if (mCurrentView != nullptr)
|
||||
mCurrentView->onTransition();
|
||||
|
@ -521,6 +535,7 @@ void ViewController::goToGamelist(SystemData* system)
|
|||
}
|
||||
|
||||
mCurrentView = getGamelistView(system);
|
||||
mCurrentView->finishAnimation(0);
|
||||
|
||||
// Application startup animation, if starting in a gamelist rather than in the system view.
|
||||
if (mState.viewing == NOTHING) {
|
||||
|
|
|
@ -31,7 +31,12 @@ set(CORE_HEADERS
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/src/animations/LambdaAnimation.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/animations/MoveCameraAnimation.h
|
||||
|
||||
# GUI components
|
||||
# Primary GUI components
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/primary/CarouselComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/primary/PrimaryComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/primary/TextListComponent.h
|
||||
|
||||
# Secondary GUI components
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/AnimatedImageComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/BadgeComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/BusyComponent.h
|
||||
|
@ -111,7 +116,7 @@ set(CORE_SOURCES
|
|||
# Animations
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/animations/AnimationController.cpp
|
||||
|
||||
# GUI components
|
||||
# Secondary GUI components
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/AnimatedImageComponent.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/BadgeComponent.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/BusyComponent.cpp
|
||||
|
|
|
@ -240,7 +240,7 @@ void Window::input(InputConfig* config, Input input)
|
|||
// up. So scale it to full size so it won't be stuck at a smaller size when returning
|
||||
// from the submenu.
|
||||
mTopScale = 1.0f;
|
||||
GuiComponent* menu = mGuiStack.back();
|
||||
GuiComponent* menu {mGuiStack.back()};
|
||||
glm::vec2 menuCenter {menu->getCenter()};
|
||||
menu->setOrigin(0.5f, 0.5f);
|
||||
menu->setPosition(menuCenter.x, menuCenter.y, 0.0f);
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
#define TOTAL_HORIZONTAL_PADDING_PX 20.0f
|
||||
|
||||
ComponentList::ComponentList()
|
||||
: IList<ComponentListRow, void*> {LIST_SCROLL_STYLE_SLOW, LIST_NEVER_LOOP}
|
||||
: IList<ComponentListRow, void*> {LIST_SCROLL_STYLE_SLOW, ListLoopType::LIST_NEVER_LOOP}
|
||||
, mRenderer {Renderer::getInstance()}
|
||||
, mFocused {false}
|
||||
, mSetupCompleted {false}
|
||||
|
@ -53,7 +53,7 @@ void ComponentList::addRow(const ComponentListRow& row, bool setCursorHere)
|
|||
|
||||
if (setCursorHere) {
|
||||
mCursor = static_cast<int>(mEntries.size()) - 1;
|
||||
onCursorChanged(CURSOR_STOPPED);
|
||||
onCursorChanged(CursorState::CURSOR_STOPPED);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
#include "GuiComponent.h"
|
||||
#include "Log.h"
|
||||
#include "Settings.h"
|
||||
#include "ThemeData.h"
|
||||
|
||||
class GameSelectorComponent : public GuiComponent
|
||||
|
|
|
@ -13,14 +13,15 @@
|
|||
#include "components/ImageComponent.h"
|
||||
#include "utils/StringUtil.h"
|
||||
|
||||
enum CursorState {
|
||||
enum class CursorState {
|
||||
CURSOR_STOPPED, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0).
|
||||
CURSOR_SCROLLING
|
||||
};
|
||||
|
||||
enum ListLoopType {
|
||||
enum class ListLoopType {
|
||||
LIST_ALWAYS_LOOP, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0).
|
||||
LIST_PAUSE_AT_END,
|
||||
LIST_PAUSE_AT_END_ON_JUMP,
|
||||
LIST_NEVER_LOOP
|
||||
};
|
||||
|
||||
|
@ -57,7 +58,7 @@ const ScrollTierList LIST_SCROLL_STYLE_SLOW = {
|
|||
};
|
||||
// clang-format on
|
||||
|
||||
template <typename EntryData, typename UserData> class IList : public GuiComponent
|
||||
template <typename EntryData, typename UserData> class IList : public virtual GuiComponent
|
||||
{
|
||||
public:
|
||||
struct Entry {
|
||||
|
@ -67,6 +68,10 @@ public:
|
|||
};
|
||||
|
||||
protected:
|
||||
Window* mWindow;
|
||||
std::vector<Entry> mEntries;
|
||||
const ScrollTierList& mTierList;
|
||||
const ListLoopType mLoopType;
|
||||
int mCursor;
|
||||
int mScrollTier;
|
||||
int mScrollVelocity;
|
||||
|
@ -76,32 +81,23 @@ protected:
|
|||
float mTitleOverlayOpacity;
|
||||
unsigned int mTitleOverlayColor;
|
||||
|
||||
const ScrollTierList& mTierList;
|
||||
const ListLoopType mLoopType;
|
||||
|
||||
std::vector<Entry> mEntries;
|
||||
Window* mWindow;
|
||||
|
||||
public:
|
||||
IList(const ScrollTierList& tierList = LIST_SCROLL_STYLE_QUICK,
|
||||
const ListLoopType& loopType = LIST_PAUSE_AT_END)
|
||||
: mTierList {tierList}
|
||||
const ListLoopType& loopType = ListLoopType::LIST_PAUSE_AT_END)
|
||||
: mWindow {Window::getInstance()}
|
||||
, mTierList {tierList}
|
||||
, mLoopType {loopType}
|
||||
, mWindow {Window::getInstance()}
|
||||
, mCursor {0}
|
||||
, mScrollTier {0}
|
||||
, mScrollVelocity {0}
|
||||
, mScrollTierAccumulator {0}
|
||||
, mScrollCursorAccumulator {0}
|
||||
, mTitleOverlayOpacity {0.0f}
|
||||
, mTitleOverlayColor {0xFFFFFF00}
|
||||
{
|
||||
mCursor = 0;
|
||||
mScrollTier = 0;
|
||||
mScrollVelocity = 0;
|
||||
mScrollTierAccumulator = 0;
|
||||
mScrollCursorAccumulator = 0;
|
||||
|
||||
mTitleOverlayOpacity = 0.0f;
|
||||
mTitleOverlayColor = 0xFFFFFF00;
|
||||
}
|
||||
|
||||
bool isScrolling() const { return (mScrollVelocity != 0 && mScrollTier > 0); }
|
||||
|
||||
int getScrollingVelocity() { return mScrollVelocity; }
|
||||
const bool isScrolling() const { return (mScrollVelocity != 0 && mScrollTier > 0); }
|
||||
|
||||
void stopScrolling()
|
||||
{
|
||||
|
@ -109,15 +105,17 @@ public:
|
|||
|
||||
listInput(0);
|
||||
if (mScrollVelocity == 0)
|
||||
onCursorChanged(CURSOR_STOPPED);
|
||||
onCursorChanged(CursorState::CURSOR_STOPPED);
|
||||
}
|
||||
|
||||
const int getScrollingVelocity() const { return mScrollVelocity; }
|
||||
|
||||
void clear()
|
||||
{
|
||||
mEntries.clear();
|
||||
mCursor = 0;
|
||||
listInput(0);
|
||||
onCursorChanged(CURSOR_STOPPED);
|
||||
onCursorChanged(CursorState::CURSOR_STOPPED);
|
||||
}
|
||||
|
||||
const std::string& getSelectedName()
|
||||
|
@ -166,16 +164,15 @@ public:
|
|||
{
|
||||
assert(it != mEntries.cend());
|
||||
mCursor = it - mEntries.cbegin();
|
||||
onCursorChanged(CURSOR_STOPPED);
|
||||
onCursorChanged(CursorState::CURSOR_STOPPED);
|
||||
}
|
||||
|
||||
// Returns true if successful (select is in our list), false if not.
|
||||
bool setCursor(const UserData& obj)
|
||||
{
|
||||
for (auto it = mEntries.cbegin(); it != mEntries.cend(); ++it) {
|
||||
if ((*it).object == obj) {
|
||||
mCursor = static_cast<int>(it - mEntries.cbegin());
|
||||
onCursorChanged(CURSOR_STOPPED);
|
||||
onCursorChanged(CursorState::CURSOR_STOPPED);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -205,7 +202,7 @@ protected:
|
|||
{
|
||||
if (mCursor > 0 && it - mEntries.cbegin() <= mCursor) {
|
||||
--mCursor;
|
||||
onCursorChanged(CURSOR_STOPPED);
|
||||
onCursorChanged(CursorState::CURSOR_STOPPED);
|
||||
}
|
||||
|
||||
mEntries.erase(it);
|
||||
|
@ -214,7 +211,7 @@ protected:
|
|||
bool listFirstRow()
|
||||
{
|
||||
mCursor = 0;
|
||||
onCursorChanged(CURSOR_STOPPED);
|
||||
onCursorChanged(CursorState::CURSOR_STOPPED);
|
||||
onScroll();
|
||||
return true;
|
||||
}
|
||||
|
@ -222,7 +219,7 @@ protected:
|
|||
bool listLastRow()
|
||||
{
|
||||
mCursor = static_cast<int>(mEntries.size()) - 1;
|
||||
onCursorChanged(CURSOR_STOPPED);
|
||||
onCursorChanged(CursorState::CURSOR_STOPPED);
|
||||
onScroll();
|
||||
return true;
|
||||
}
|
||||
|
@ -257,7 +254,7 @@ protected:
|
|||
// We delay scrolling until after scroll tier has updated so isScrolling() returns
|
||||
// accurately during onCursorChanged callbacks. We don't just do scroll tier first
|
||||
// because it would not catch the scrollDelay == tier length case.
|
||||
int scrollCount = 0;
|
||||
int scrollCount {0};
|
||||
while (mScrollCursorAccumulator >= mTierList.tiers[mScrollTier].scrollDelay) {
|
||||
mScrollCursorAccumulator -= mTierList.tiers[mScrollTier].scrollDelay;
|
||||
++scrollCount;
|
||||
|
@ -275,45 +272,47 @@ protected:
|
|||
scroll(mScrollVelocity);
|
||||
}
|
||||
|
||||
void listRenderTitleOverlay(const glm::mat4& /*trans*/)
|
||||
void listRenderTitleOverlay(const glm::mat4&)
|
||||
{
|
||||
if (!Settings::getInstance()->getBool("ListScrollOverlay"))
|
||||
return;
|
||||
if constexpr (std::is_same_v<UserData, FileData*>) {
|
||||
if (!Settings::getInstance()->getBool("ListScrollOverlay"))
|
||||
return;
|
||||
|
||||
if (size() == 0 || mTitleOverlayOpacity == 0.0f) {
|
||||
mWindow->renderListScrollOverlay(0.0f, "");
|
||||
return;
|
||||
}
|
||||
if (size() == 0 || mTitleOverlayOpacity == 0.0f) {
|
||||
mWindow->renderListScrollOverlay(0.0f, "");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string titleIndex;
|
||||
bool favoritesSorting;
|
||||
std::string titleIndex;
|
||||
bool favoritesSorting;
|
||||
|
||||
if (getSelected()->getSystem()->isCustomCollection())
|
||||
favoritesSorting = Settings::getInstance()->getBool("FavFirstCustom");
|
||||
else
|
||||
favoritesSorting = Settings::getInstance()->getBool("FavoritesFirst");
|
||||
if (getSelected()->getSystem()->isCustomCollection())
|
||||
favoritesSorting = Settings::getInstance()->getBool("FavFirstCustom");
|
||||
else
|
||||
favoritesSorting = Settings::getInstance()->getBool("FavoritesFirst");
|
||||
|
||||
if (favoritesSorting && getSelected()->getFavorite()) {
|
||||
if (favoritesSorting && getSelected()->getFavorite()) {
|
||||
#if defined(_MSC_VER) // MSVC compiler.
|
||||
titleIndex = Utils::String::wideStringToString(L"\uF005");
|
||||
titleIndex = Utils::String::wideStringToString(L"\uF005");
|
||||
#else
|
||||
titleIndex = "\uF005";
|
||||
titleIndex = "\uF005";
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
titleIndex = getSelected()->getName();
|
||||
if (titleIndex.size()) {
|
||||
titleIndex[0] = toupper(titleIndex[0]);
|
||||
if (titleIndex.size() > 1) {
|
||||
titleIndex = titleIndex.substr(0, 2);
|
||||
titleIndex[1] = tolower(titleIndex[1]);
|
||||
}
|
||||
else {
|
||||
titleIndex = getSelected()->getName();
|
||||
if (titleIndex.size()) {
|
||||
titleIndex[0] = toupper(titleIndex[0]);
|
||||
if (titleIndex.size() > 1) {
|
||||
titleIndex = titleIndex.substr(0, 2);
|
||||
titleIndex[1] = tolower(titleIndex[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The actual rendering takes place in Window to make sure that the overlay is placed on
|
||||
// top of all GUI elements but below the info popups and GPU statistics overlay.
|
||||
mWindow->renderListScrollOverlay(mTitleOverlayOpacity, titleIndex);
|
||||
// The actual rendering takes place in Window to make sure that the overlay is placed on
|
||||
// top of all GUI elements but below the info popups and GPU statistics overlay.
|
||||
mWindow->renderListScrollOverlay(mTitleOverlayOpacity, titleIndex);
|
||||
}
|
||||
}
|
||||
|
||||
void scroll(int amt)
|
||||
|
@ -321,14 +320,23 @@ protected:
|
|||
if (mScrollVelocity == 0 || size() < 2)
|
||||
return;
|
||||
|
||||
int cursor = mCursor + amt;
|
||||
int absAmt = amt < 0 ? -amt : amt;
|
||||
int cursor {mCursor + amt};
|
||||
int absAmt {amt < 0 ? -amt : amt};
|
||||
|
||||
// Stop at the end if we've been holding down the button for a long time or
|
||||
// we're scrolling faster than one item at a time (e.g. page up/down).
|
||||
// Otherwise, loop around.
|
||||
if ((mLoopType == LIST_PAUSE_AT_END && (mScrollTier > 0 || absAmt > 1)) ||
|
||||
mLoopType == LIST_NEVER_LOOP) {
|
||||
bool stopScroll {false};
|
||||
|
||||
// Depending on the loop type we'll either pause at the ends if holding a navigation
|
||||
// button, or we'll only stop if it's a quick jump key (should or trigger button) that
|
||||
// is held, or we never loop.
|
||||
if (mLoopType == ListLoopType::LIST_PAUSE_AT_END && (mScrollTier > 0 || absAmt > 1))
|
||||
stopScroll = true;
|
||||
else if (mLoopType == ListLoopType::LIST_PAUSE_AT_END_ON_JUMP && abs(mScrollVelocity) > 1 &&
|
||||
(mScrollTier > 0 || absAmt > 1))
|
||||
stopScroll = true;
|
||||
else if (mLoopType == ListLoopType::LIST_NEVER_LOOP)
|
||||
stopScroll = true;
|
||||
|
||||
if (stopScroll) {
|
||||
if (cursor < 0) {
|
||||
cursor = 0;
|
||||
mScrollVelocity = 0;
|
||||
|
@ -351,10 +359,11 @@ protected:
|
|||
onScroll();
|
||||
|
||||
mCursor = cursor;
|
||||
onCursorChanged((mScrollTier > 0) ? CURSOR_SCROLLING : CURSOR_STOPPED);
|
||||
onCursorChanged((mScrollTier > 0) ? CursorState::CURSOR_SCROLLING :
|
||||
CursorState::CURSOR_STOPPED);
|
||||
}
|
||||
|
||||
virtual void onCursorChanged(const CursorState& /*state*/) {}
|
||||
virtual void onCursorChanged(const CursorState&) {}
|
||||
virtual void onScroll() {}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,22 +1,147 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// EmulationStation Desktop Edition
|
||||
// CarouselComponent.cpp
|
||||
// CarouselComponent.h
|
||||
//
|
||||
// Carousel.
|
||||
//
|
||||
|
||||
#include "components/CarouselComponent.h"
|
||||
#ifndef ES_CORE_COMPONENTS_CAROUSEL_COMPONENT_H
|
||||
#define ES_CORE_COMPONENTS_CAROUSEL_COMPONENT_H
|
||||
|
||||
#include "Log.h"
|
||||
#include "Sound.h"
|
||||
#include "animations/LambdaAnimation.h"
|
||||
#include "components/IList.h"
|
||||
#include "components/primary/PrimaryComponent.h"
|
||||
#include "resources/Font.h"
|
||||
|
||||
CarouselComponent::CarouselComponent()
|
||||
: IList<CarouselElement, SystemData*> {LIST_SCROLL_STYLE_SLOW, LIST_ALWAYS_LOOP}
|
||||
namespace
|
||||
{
|
||||
struct CarouselElement {
|
||||
std::shared_ptr<GuiComponent> logo;
|
||||
std::string logoPath;
|
||||
std::string defaultLogoPath;
|
||||
};
|
||||
}; // namespace
|
||||
|
||||
template <typename T>
|
||||
class CarouselComponent : public PrimaryComponent<T>, protected IList<CarouselElement, T>
|
||||
{
|
||||
using List = IList<CarouselElement, T>;
|
||||
|
||||
protected:
|
||||
using List::mCursor;
|
||||
using List::mEntries;
|
||||
using List::mScrollVelocity;
|
||||
using List::mSize;
|
||||
using List::mWindow;
|
||||
|
||||
using GuiComponent::mDefaultZIndex;
|
||||
using GuiComponent::mOrigin;
|
||||
using GuiComponent::mPosition;
|
||||
using GuiComponent::mZIndex;
|
||||
|
||||
public:
|
||||
using Entry = typename IList<CarouselElement, T>::Entry;
|
||||
|
||||
enum class CarouselType {
|
||||
HORIZONTAL, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0).
|
||||
VERTICAL,
|
||||
VERTICAL_WHEEL,
|
||||
HORIZONTAL_WHEEL,
|
||||
NO_CAROUSEL
|
||||
};
|
||||
|
||||
CarouselComponent();
|
||||
|
||||
void addEntry(Entry& entry, const std::shared_ptr<ThemeData>& theme = nullptr);
|
||||
Entry& getEntry(int index) { return mEntries.at(index); }
|
||||
const CarouselType getType() { return mType; }
|
||||
|
||||
void setCursorChangedCallback(const std::function<void(CursorState state)>& func) override
|
||||
{
|
||||
mCursorChangedCallback = func;
|
||||
}
|
||||
void setCancelTransitionsCallback(const std::function<void()>& func) override
|
||||
{
|
||||
mCancelTransitionsCallback = func;
|
||||
}
|
||||
|
||||
bool input(InputConfig* config, Input input) override;
|
||||
void update(int deltaTime) override;
|
||||
void render(const glm::mat4& parentTrans) override;
|
||||
void applyTheme(const std::shared_ptr<ThemeData>& theme,
|
||||
const std::string& view,
|
||||
const std::string& element,
|
||||
unsigned int properties) override;
|
||||
|
||||
private:
|
||||
void onCursorChanged(const CursorState& state) override;
|
||||
void onScroll() override
|
||||
{
|
||||
NavigationSounds::getInstance().playThemeNavigationSound(SYSTEMBROWSESOUND);
|
||||
}
|
||||
|
||||
bool isScrolling() const override { return List::isScrolling(); }
|
||||
void stopScrolling() override
|
||||
{
|
||||
List::stopScrolling();
|
||||
// Only finish the animation if we're in the gamelist view.
|
||||
if constexpr (std::is_same_v<T, FileData*>)
|
||||
GuiComponent::finishAnimation(0);
|
||||
}
|
||||
const int getScrollingVelocity() override { return List::getScrollingVelocity(); }
|
||||
void clear() override { List::clear(); }
|
||||
const T& getSelected() const override { return List::getSelected(); }
|
||||
const T& getNext() const override { return List::getNext(); }
|
||||
const T& getPrevious() const override { return List::getPrevious(); }
|
||||
const T& getFirst() const override { return List::getFirst(); }
|
||||
const T& getLast() const override { return List::getLast(); }
|
||||
bool setCursor(const T& obj) override { return List::setCursor(obj); }
|
||||
bool remove(const T& obj) override { return List::remove(obj); }
|
||||
int size() const override { return List::size(); }
|
||||
|
||||
int getCursor() override { return mCursor; }
|
||||
const size_t getNumEntries() override { return mEntries.size(); }
|
||||
|
||||
Renderer* mRenderer;
|
||||
std::function<void(CursorState state)> mCursorChangedCallback;
|
||||
std::function<void()> mCancelTransitionsCallback;
|
||||
|
||||
float mEntryCamOffset;
|
||||
int mPreviousScrollVelocity;
|
||||
bool mTriggerJump;
|
||||
|
||||
CarouselType mType;
|
||||
std::shared_ptr<Font> mFont;
|
||||
unsigned int mTextColor;
|
||||
unsigned int mTextBackgroundColor;
|
||||
std::string mText;
|
||||
float mLineSpacing;
|
||||
Alignment mLogoHorizontalAlignment;
|
||||
Alignment mLogoVerticalAlignment;
|
||||
float mMaxLogoCount;
|
||||
glm::vec2 mLogoSize;
|
||||
float mLogoScale;
|
||||
float mLogoRotation;
|
||||
glm::vec2 mLogoRotationOrigin;
|
||||
unsigned int mCarouselColor;
|
||||
unsigned int mCarouselColorEnd;
|
||||
bool mColorGradientHorizontal;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
CarouselComponent<T>::CarouselComponent()
|
||||
: IList<CarouselElement, T> {LIST_SCROLL_STYLE_SLOW,
|
||||
(std::is_same_v<T, SystemData*> ?
|
||||
ListLoopType::LIST_ALWAYS_LOOP :
|
||||
ListLoopType::LIST_PAUSE_AT_END_ON_JUMP)}
|
||||
, mRenderer {Renderer::getInstance()}
|
||||
, mCamOffset {0.0f}
|
||||
, mEntryCamOffset {0.0f}
|
||||
, mPreviousScrollVelocity {0}
|
||||
, mType {HORIZONTAL}
|
||||
, mTriggerJump {false}
|
||||
, mType {CarouselType::HORIZONTAL}
|
||||
, mFont {Font::get(FONT_SIZE_LARGE)}
|
||||
, mTextColor {0x000000FF}
|
||||
, mTextBackgroundColor {0xFFFFFF00}
|
||||
|
@ -34,10 +159,11 @@ CarouselComponent::CarouselComponent()
|
|||
{
|
||||
}
|
||||
|
||||
void CarouselComponent::addEntry(const std::shared_ptr<ThemeData>& theme,
|
||||
Entry& entry,
|
||||
bool legacyMode)
|
||||
template <typename T>
|
||||
void CarouselComponent<T>::addEntry(Entry& entry, const std::shared_ptr<ThemeData>& theme)
|
||||
{
|
||||
bool legacyMode {theme->isLegacyTheme()};
|
||||
|
||||
// Make logo.
|
||||
if (legacyMode) {
|
||||
const ThemeData::ThemeElement* logoElem {
|
||||
|
@ -96,8 +222,10 @@ void CarouselComponent::addEntry(const std::shared_ptr<ThemeData>& theme,
|
|||
}
|
||||
if (!legacyMode) {
|
||||
text->setLineSpacing(mLineSpacing);
|
||||
if (mText != "")
|
||||
text->setValue(mText);
|
||||
if constexpr (std::is_same_v<T, SystemData*>) {
|
||||
if (mText != "")
|
||||
text->setValue(mText);
|
||||
}
|
||||
text->setColor(mTextColor);
|
||||
text->setBackgroundColor(mTextBackgroundColor);
|
||||
text->setRenderBackground(true);
|
||||
|
@ -126,64 +254,116 @@ void CarouselComponent::addEntry(const std::shared_ptr<ThemeData>& theme,
|
|||
glm::vec2 denormalized {mLogoSize * entry.data.logo->getOrigin()};
|
||||
entry.data.logo->setPosition(glm::vec3 {denormalized.x, denormalized.y, 0.0f});
|
||||
|
||||
add(entry);
|
||||
List::add(entry);
|
||||
}
|
||||
|
||||
bool CarouselComponent::input(InputConfig* config, Input input)
|
||||
template <typename T> bool CarouselComponent<T>::input(InputConfig* config, Input input)
|
||||
{
|
||||
if (input.value != 0) {
|
||||
switch (mType) {
|
||||
case VERTICAL:
|
||||
case VERTICAL_WHEEL:
|
||||
case CarouselType::VERTICAL:
|
||||
case CarouselType::VERTICAL_WHEEL:
|
||||
if (config->isMappedLike("up", input)) {
|
||||
if (mCancelTransitionsCallback)
|
||||
mCancelTransitionsCallback();
|
||||
listInput(-1);
|
||||
List::listInput(-1);
|
||||
return true;
|
||||
}
|
||||
if (config->isMappedLike("down", input)) {
|
||||
if (mCancelTransitionsCallback)
|
||||
mCancelTransitionsCallback();
|
||||
listInput(1);
|
||||
List::listInput(1);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case HORIZONTAL:
|
||||
case HORIZONTAL_WHEEL:
|
||||
case CarouselType::HORIZONTAL:
|
||||
case CarouselType::HORIZONTAL_WHEEL:
|
||||
default:
|
||||
if (config->isMappedLike("left", input)) {
|
||||
if (mCancelTransitionsCallback)
|
||||
mCancelTransitionsCallback();
|
||||
listInput(-1);
|
||||
List::listInput(-1);
|
||||
return true;
|
||||
}
|
||||
if (config->isMappedLike("right", input)) {
|
||||
if (mCancelTransitionsCallback)
|
||||
mCancelTransitionsCallback();
|
||||
listInput(1);
|
||||
List::listInput(1);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if constexpr (std::is_same_v<T, FileData*>) {
|
||||
if (config->isMappedLike("leftshoulder", input)) {
|
||||
if (getCursor() == 0) {
|
||||
if (!NavigationSounds::getInstance().isPlayingThemeNavigationSound(SCROLLSOUND))
|
||||
NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND);
|
||||
return true;
|
||||
}
|
||||
if (mCancelTransitionsCallback)
|
||||
mCancelTransitionsCallback();
|
||||
List::listInput(-10);
|
||||
return true;
|
||||
}
|
||||
if (config->isMappedLike("rightshoulder", input)) {
|
||||
if (getCursor() == static_cast<int>(mEntries.size()) - 1) {
|
||||
if (!NavigationSounds::getInstance().isPlayingThemeNavigationSound(SCROLLSOUND))
|
||||
NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND);
|
||||
return true;
|
||||
}
|
||||
if (mCancelTransitionsCallback)
|
||||
mCancelTransitionsCallback();
|
||||
List::listInput(10);
|
||||
return true;
|
||||
}
|
||||
if (config->isMappedLike("lefttrigger", input)) {
|
||||
mTriggerJump = true;
|
||||
if (mCancelTransitionsCallback)
|
||||
mCancelTransitionsCallback();
|
||||
return this->listFirstRow();
|
||||
}
|
||||
if (config->isMappedLike("righttrigger", input)) {
|
||||
mTriggerJump = true;
|
||||
if (mCancelTransitionsCallback)
|
||||
mCancelTransitionsCallback();
|
||||
return this->listLastRow();
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (config->isMappedLike("left", input) || config->isMappedLike("right", input) ||
|
||||
config->isMappedLike("up", input) || config->isMappedLike("down", input)) {
|
||||
listInput(0);
|
||||
if constexpr (std::is_same_v<T, FileData*>) {
|
||||
if (config->isMappedLike("up", input) || config->isMappedLike("down", input) ||
|
||||
config->isMappedLike("left", input) || config->isMappedLike("right", input) ||
|
||||
config->isMappedLike("leftshoulder", input) ||
|
||||
config->isMappedLike("rightshoulder", input) ||
|
||||
config->isMappedLike("lefttrigger", input) ||
|
||||
config->isMappedLike("righttrigger", input)) {
|
||||
onCursorChanged(CursorState::CURSOR_STOPPED);
|
||||
List::listInput(0);
|
||||
mTriggerJump = false;
|
||||
}
|
||||
}
|
||||
if constexpr (std::is_same_v<T, SystemData*>) {
|
||||
if (config->isMappedLike("up", input) || config->isMappedLike("down", input) ||
|
||||
config->isMappedLike("left", input) || config->isMappedLike("right", input))
|
||||
List::listInput(0);
|
||||
}
|
||||
}
|
||||
|
||||
return GuiComponent::input(config, input);
|
||||
}
|
||||
|
||||
void CarouselComponent::update(int deltaTime)
|
||||
template <typename T> void CarouselComponent<T>::update(int deltaTime)
|
||||
{
|
||||
listUpdate(deltaTime);
|
||||
List::listUpdate(deltaTime);
|
||||
GuiComponent::update(deltaTime);
|
||||
}
|
||||
|
||||
void CarouselComponent::render(const glm::mat4& parentTrans)
|
||||
template <typename T> void CarouselComponent<T>::render(const glm::mat4& parentTrans)
|
||||
{
|
||||
if (mEntries.size() == 0)
|
||||
return;
|
||||
|
||||
glm::mat4 carouselTrans {parentTrans};
|
||||
carouselTrans = glm::translate(carouselTrans, glm::vec3 {mPosition.x, mPosition.y, 0.0f});
|
||||
carouselTrans = glm::translate(
|
||||
|
@ -210,15 +390,15 @@ void CarouselComponent::render(const glm::mat4& parentTrans)
|
|||
float yOff {0.0f};
|
||||
|
||||
switch (mType) {
|
||||
case HORIZONTAL_WHEEL:
|
||||
case VERTICAL_WHEEL:
|
||||
xOff = std::round((mSize.x - mLogoSize.x) / 2.0f - (mCamOffset * logoSpacing.y));
|
||||
case CarouselType::HORIZONTAL_WHEEL:
|
||||
case CarouselType::VERTICAL_WHEEL:
|
||||
xOff = std::round((mSize.x - mLogoSize.x) / 2.0f - (mEntryCamOffset * logoSpacing.y));
|
||||
yOff = (mSize.y - mLogoSize.y) / 2.0f;
|
||||
break;
|
||||
case VERTICAL:
|
||||
case CarouselType::VERTICAL:
|
||||
logoSpacing.y =
|
||||
((mSize.y - (mLogoSize.y * mMaxLogoCount)) / (mMaxLogoCount)) + mLogoSize.y;
|
||||
yOff = (mSize.y - mLogoSize.y) / 2.0f - (mCamOffset * logoSpacing.y);
|
||||
yOff = (mSize.y - mLogoSize.y) / 2.0f - (mEntryCamOffset * logoSpacing.y);
|
||||
if (mLogoHorizontalAlignment == ALIGN_LEFT)
|
||||
xOff = mLogoSize.x / 10.0f;
|
||||
else if (mLogoHorizontalAlignment == ALIGN_RIGHT)
|
||||
|
@ -226,11 +406,11 @@ void CarouselComponent::render(const glm::mat4& parentTrans)
|
|||
else
|
||||
xOff = (mSize.x - mLogoSize.x) / 2.0f;
|
||||
break;
|
||||
case HORIZONTAL:
|
||||
case CarouselType::HORIZONTAL:
|
||||
default:
|
||||
logoSpacing.x =
|
||||
((mSize.x - (mLogoSize.x * mMaxLogoCount)) / (mMaxLogoCount)) + mLogoSize.x;
|
||||
xOff = std::round((mSize.x - mLogoSize.x) / 2.0f - (mCamOffset * logoSpacing.x));
|
||||
xOff = std::round((mSize.x - mLogoSize.x) / 2.0f - (mEntryCamOffset * logoSpacing.x));
|
||||
if (mLogoVerticalAlignment == ALIGN_TOP)
|
||||
yOff = mLogoSize.y / 10.0f;
|
||||
else if (mLogoVerticalAlignment == ALIGN_BOTTOM)
|
||||
|
@ -240,7 +420,7 @@ void CarouselComponent::render(const glm::mat4& parentTrans)
|
|||
break;
|
||||
}
|
||||
|
||||
int center {static_cast<int>(mCamOffset)};
|
||||
int center {static_cast<int>(mEntryCamOffset)};
|
||||
int logoInclusion {static_cast<int>(std::ceil(mMaxLogoCount / 2.0f))};
|
||||
bool singleEntry {mEntries.size() == 1};
|
||||
|
||||
|
@ -263,7 +443,7 @@ void CarouselComponent::render(const glm::mat4& parentTrans)
|
|||
logoTrans = glm::translate(
|
||||
logoTrans, glm::vec3 {i * logoSpacing.x + xOff, i * logoSpacing.y + yOff, 0.0f});
|
||||
|
||||
float distance {i - mCamOffset};
|
||||
float distance {i - mEntryCamOffset};
|
||||
|
||||
float scale {1.0f + ((mLogoScale - 1.0f) * (1.0f - fabsf(distance)))};
|
||||
scale = std::min(mLogoScale, std::max(1.0f, scale));
|
||||
|
@ -278,16 +458,16 @@ void CarouselComponent::render(const glm::mat4& parentTrans)
|
|||
if (comp == nullptr)
|
||||
continue;
|
||||
|
||||
if (mType == VERTICAL_WHEEL || mType == HORIZONTAL_WHEEL) {
|
||||
if (mType == CarouselType::VERTICAL_WHEEL || mType == CarouselType::HORIZONTAL_WHEEL) {
|
||||
comp->setRotationDegrees(mLogoRotation * distance);
|
||||
comp->setRotationOrigin(mLogoRotationOrigin);
|
||||
}
|
||||
|
||||
// When running at lower resolutions, prevent the scale-down to go all the way to the
|
||||
// minimum value. This avoids potential single-pixel alignment issues when the logo
|
||||
// can't be vertically placed exactly in the middle of the carousel. Although the
|
||||
// problem theoretically exists at all resolutions, it's not visble at around 1080p
|
||||
// and above.
|
||||
// When running at lower resolutions, prevent the scale-down to go all the way to
|
||||
// the minimum value. This avoids potential single-pixel alignment issues when the
|
||||
// logo can't be vertically placed exactly in the middle of the carousel. Although
|
||||
// the problem theoretically exists at all resolutions, it's not visble at around
|
||||
// 1080p and above.
|
||||
if (std::min(Renderer::getScreenWidth(), Renderer::getScreenHeight()) < 1080.0f)
|
||||
scale = glm::clamp(scale, 1.0f / mLogoScale + 0.01f, 1.0f);
|
||||
|
||||
|
@ -298,10 +478,11 @@ void CarouselComponent::render(const glm::mat4& parentTrans)
|
|||
mRenderer->popClipRect();
|
||||
}
|
||||
|
||||
void CarouselComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
|
||||
const std::string& view,
|
||||
const std::string& element,
|
||||
unsigned int properties)
|
||||
template <typename T>
|
||||
void CarouselComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme,
|
||||
const std::string& view,
|
||||
const std::string& element,
|
||||
unsigned int properties)
|
||||
{
|
||||
using namespace ThemeFlags;
|
||||
const ThemeData::ThemeElement* elem {theme->getElement(view, element, "carousel")};
|
||||
|
@ -312,7 +493,7 @@ void CarouselComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
|
|||
mPosition.y = floorf(0.5f * (Renderer::getScreenHeight() - mSize.y));
|
||||
mCarouselColor = 0xFFFFFFD8;
|
||||
mCarouselColorEnd = 0xFFFFFFD8;
|
||||
mDefaultZIndex = 50.0f;
|
||||
mZIndex = mDefaultZIndex;
|
||||
mText = "";
|
||||
|
||||
if (!elem)
|
||||
|
@ -321,22 +502,22 @@ void CarouselComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
|
|||
if (elem->has("type")) {
|
||||
const std::string type {elem->get<std::string>("type")};
|
||||
if (type == "horizontal") {
|
||||
mType = HORIZONTAL;
|
||||
mType = CarouselType::HORIZONTAL;
|
||||
}
|
||||
else if (type == "horizontal_wheel") {
|
||||
mType = HORIZONTAL_WHEEL;
|
||||
mType = CarouselType::HORIZONTAL_WHEEL;
|
||||
}
|
||||
else if (type == "vertical") {
|
||||
mType = VERTICAL;
|
||||
mType = CarouselType::VERTICAL;
|
||||
}
|
||||
else if (type == "vertical_wheel") {
|
||||
mType = VERTICAL_WHEEL;
|
||||
mType = CarouselType::VERTICAL_WHEEL;
|
||||
}
|
||||
else {
|
||||
LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property "
|
||||
"<type> defined as \""
|
||||
<< type << "\"";
|
||||
mType = HORIZONTAL;
|
||||
mType = CarouselType::HORIZONTAL;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -395,10 +576,10 @@ void CarouselComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
|
|||
|
||||
if (elem->has("logoHorizontalAlignment")) {
|
||||
const std::string alignment {elem->get<std::string>("logoHorizontalAlignment")};
|
||||
if (alignment == "left" && mType != HORIZONTAL) {
|
||||
if (alignment == "left" && mType != CarouselType::HORIZONTAL) {
|
||||
mLogoHorizontalAlignment = ALIGN_LEFT;
|
||||
}
|
||||
else if (alignment == "right" && mType != HORIZONTAL) {
|
||||
else if (alignment == "right" && mType != CarouselType::HORIZONTAL) {
|
||||
mLogoHorizontalAlignment = ALIGN_RIGHT;
|
||||
}
|
||||
else if (alignment == "center") {
|
||||
|
@ -414,10 +595,10 @@ void CarouselComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
|
|||
|
||||
if (elem->has("logoVerticalAlignment")) {
|
||||
const std::string alignment {elem->get<std::string>("logoVerticalAlignment")};
|
||||
if (alignment == "top" && mType != VERTICAL) {
|
||||
if (alignment == "top" && mType != CarouselType::VERTICAL) {
|
||||
mLogoVerticalAlignment = ALIGN_TOP;
|
||||
}
|
||||
else if (alignment == "bottom" && mType != VERTICAL) {
|
||||
else if (alignment == "bottom" && mType != CarouselType::VERTICAL) {
|
||||
mLogoVerticalAlignment = ALIGN_BOTTOM;
|
||||
}
|
||||
else if (alignment == "center") {
|
||||
|
@ -434,19 +615,19 @@ void CarouselComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
|
|||
// Legacy themes only.
|
||||
if (elem->has("logoAlignment")) {
|
||||
const std::string alignment {elem->get<std::string>("logoAlignment")};
|
||||
if (alignment == "left" && mType != HORIZONTAL) {
|
||||
if (alignment == "left" && mType != CarouselType::HORIZONTAL) {
|
||||
mLogoHorizontalAlignment = ALIGN_LEFT;
|
||||
mLogoVerticalAlignment = ALIGN_CENTER;
|
||||
}
|
||||
else if (alignment == "right" && mType != HORIZONTAL) {
|
||||
else if (alignment == "right" && mType != CarouselType::HORIZONTAL) {
|
||||
mLogoHorizontalAlignment = ALIGN_RIGHT;
|
||||
mLogoVerticalAlignment = ALIGN_CENTER;
|
||||
}
|
||||
else if (alignment == "top" && mType != VERTICAL) {
|
||||
else if (alignment == "top" && mType != CarouselType::VERTICAL) {
|
||||
mLogoVerticalAlignment = ALIGN_TOP;
|
||||
mLogoHorizontalAlignment = ALIGN_CENTER;
|
||||
}
|
||||
else if (alignment == "bottom" && mType != VERTICAL) {
|
||||
else if (alignment == "bottom" && mType != CarouselType::VERTICAL) {
|
||||
mLogoVerticalAlignment = ALIGN_BOTTOM;
|
||||
mLogoHorizontalAlignment = ALIGN_CENTER;
|
||||
}
|
||||
|
@ -507,20 +688,28 @@ void CarouselComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
|
|||
mRenderer->getScreenHeight() * 1.5f);
|
||||
}
|
||||
|
||||
void CarouselComponent::onCursorChanged(const CursorState& state)
|
||||
template <typename T> void CarouselComponent<T>::onCursorChanged(const CursorState& state)
|
||||
{
|
||||
float startPos {mCamOffset};
|
||||
float startPos {mEntryCamOffset};
|
||||
float posMax {static_cast<float>(mEntries.size())};
|
||||
float target {static_cast<float>(mCursor)};
|
||||
|
||||
// Find the shortest path to the target.
|
||||
float endPos {target}; // Directly.
|
||||
float dist {fabsf(endPos - startPos)};
|
||||
|
||||
if (fabsf(target + posMax - startPos - mScrollVelocity) < dist)
|
||||
endPos = target + posMax; // Loop around the end (0 -> max).
|
||||
if (fabsf(target - posMax - startPos - mScrollVelocity) < dist)
|
||||
endPos = target - posMax; // Loop around the start (max - 1 -> -1).
|
||||
if (mPreviousScrollVelocity > 0 && mScrollVelocity == 0 && mEntryCamOffset > posMax - 1.0f)
|
||||
startPos = 0.0f;
|
||||
|
||||
// If quick jumping to the start or end of the list using the trigger button when in
|
||||
// the gamelist view, then always animate in the requested direction.
|
||||
if (!mTriggerJump) {
|
||||
float dist {fabsf(endPos - startPos)};
|
||||
|
||||
if (fabsf(target + posMax - startPos - mScrollVelocity) < dist)
|
||||
endPos = target + posMax; // Loop around the end (0 -> max).
|
||||
if (fabsf(target - posMax - startPos - mScrollVelocity) < dist)
|
||||
endPos = target - posMax; // Loop around the start (max - 1 -> -1).
|
||||
}
|
||||
|
||||
// Make sure there are no reverse jumps between logos.
|
||||
bool changedDirection {false};
|
||||
|
@ -536,11 +725,11 @@ void CarouselComponent::onCursorChanged(const CursorState& state)
|
|||
if (mScrollVelocity != 0)
|
||||
mPreviousScrollVelocity = mScrollVelocity;
|
||||
|
||||
// No need to animate transition, we're not going anywhere (probably mEntries.size() == 1).
|
||||
if (endPos == mCamOffset)
|
||||
// No need to animate transition, we're not going anywhere.
|
||||
if (endPos == mEntryCamOffset)
|
||||
return;
|
||||
|
||||
Animation* anim = new LambdaAnimation(
|
||||
Animation* anim {new LambdaAnimation(
|
||||
[this, startPos, endPos, posMax](float t) {
|
||||
t -= 1;
|
||||
float f {glm::mix(startPos, endPos, t * t * t + 1)};
|
||||
|
@ -549,12 +738,14 @@ void CarouselComponent::onCursorChanged(const CursorState& state)
|
|||
if (f >= posMax)
|
||||
f -= posMax;
|
||||
|
||||
mCamOffset = f;
|
||||
mEntryCamOffset = f;
|
||||
},
|
||||
500);
|
||||
500)};
|
||||
|
||||
setAnimation(anim, 0, nullptr, false, 0);
|
||||
GuiComponent::setAnimation(anim, 0, nullptr, false, 0);
|
||||
|
||||
if (mCursorChangedCallback)
|
||||
mCursorChangedCallback(state);
|
||||
}
|
||||
|
||||
#endif // ES_CORE_COMPONENTS_CAROUSEL_COMPONENT_H
|
50
es-core/src/components/primary/PrimaryComponent.h
Normal file
50
es-core/src/components/primary/PrimaryComponent.h
Normal file
|
@ -0,0 +1,50 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// EmulationStation Desktop Edition
|
||||
// PrimaryComponent.h
|
||||
//
|
||||
// Base class for the primary components (carousel and textlist).
|
||||
//
|
||||
|
||||
#ifndef ES_CORE_COMPONENTS_PRIMARY_COMPONENT_H
|
||||
#define ES_CORE_COMPONENTS_PRIMARY_COMPONENT_H
|
||||
|
||||
template <typename T> class PrimaryComponent : public virtual GuiComponent
|
||||
{
|
||||
public:
|
||||
enum class PrimaryType {
|
||||
CAROUSEL, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0).
|
||||
TEXTLIST
|
||||
};
|
||||
|
||||
enum class PrimaryAlignment {
|
||||
ALIGN_LEFT, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0).
|
||||
ALIGN_CENTER,
|
||||
ALIGN_RIGHT
|
||||
};
|
||||
|
||||
// IList functions.
|
||||
virtual bool isScrolling() const = 0;
|
||||
virtual void stopScrolling() = 0;
|
||||
virtual const int getScrollingVelocity() = 0;
|
||||
virtual void clear() = 0;
|
||||
virtual const T& getSelected() const = 0;
|
||||
virtual const T& getNext() const = 0;
|
||||
virtual const T& getPrevious() const = 0;
|
||||
virtual const T& getFirst() const = 0;
|
||||
virtual const T& getLast() const = 0;
|
||||
virtual bool setCursor(const T& obj) = 0;
|
||||
virtual bool remove(const T& obj) = 0;
|
||||
virtual int size() const = 0;
|
||||
|
||||
// Functions used by all primary components.
|
||||
virtual void setCancelTransitionsCallback(const std::function<void()>& func) = 0;
|
||||
virtual void setCursorChangedCallback(const std::function<void(CursorState state)>& func) = 0;
|
||||
virtual int getCursor() = 0;
|
||||
virtual const size_t getNumEntries() = 0;
|
||||
|
||||
// Functions used by some primary components.
|
||||
virtual void setAlignment(PrimaryAlignment align) {};
|
||||
};
|
||||
|
||||
#endif // ES_CORE_COMPONENTS_PRIMARY_COMPONENT_H
|
|
@ -12,35 +12,39 @@
|
|||
#include "Log.h"
|
||||
#include "Sound.h"
|
||||
#include "components/IList.h"
|
||||
#include "components/primary/PrimaryComponent.h"
|
||||
#include "resources/Font.h"
|
||||
#include "utils/StringUtil.h"
|
||||
|
||||
#include <memory>
|
||||
namespace
|
||||
{
|
||||
struct TextListData {
|
||||
unsigned int colorId;
|
||||
std::shared_ptr<TextCache> textCache;
|
||||
};
|
||||
}; // namespace
|
||||
|
||||
class TextCache;
|
||||
|
||||
struct TextListData {
|
||||
unsigned int colorId;
|
||||
std::shared_ptr<TextCache> textCache;
|
||||
};
|
||||
|
||||
// A scrollable text list supporting multiple row colors.
|
||||
template <typename T> class TextListComponent : public IList<TextListData, T>
|
||||
template <typename T>
|
||||
class TextListComponent : public PrimaryComponent<T>, private IList<TextListData, T>
|
||||
{
|
||||
using List = IList<TextListData, T>;
|
||||
|
||||
protected:
|
||||
using List::mCursor;
|
||||
using List::mEntries;
|
||||
using List::mScrollVelocity;
|
||||
using List::mSize;
|
||||
using List::mWindow;
|
||||
|
||||
public:
|
||||
using GuiComponent::setColor;
|
||||
using List::size;
|
||||
using Entry = typename IList<TextListData, T>::Entry;
|
||||
using PrimaryAlignment = typename PrimaryComponent<T>::PrimaryAlignment;
|
||||
using GuiComponent::setColor;
|
||||
|
||||
TextListComponent();
|
||||
|
||||
void addEntry(Entry& entry, const std::shared_ptr<ThemeData>& theme = nullptr);
|
||||
|
||||
bool input(InputConfig* config, Input input) override;
|
||||
void update(int deltaTime) override;
|
||||
void render(const glm::mat4& parentTrans) override;
|
||||
|
@ -49,24 +53,16 @@ public:
|
|||
const std::string& element,
|
||||
unsigned int properties) override;
|
||||
|
||||
void add(const std::string& name, const T& obj, unsigned int colorId);
|
||||
void setAlignment(PrimaryAlignment align) override { mAlignment = align; }
|
||||
|
||||
enum Alignment {
|
||||
ALIGN_LEFT, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0).
|
||||
ALIGN_CENTER,
|
||||
ALIGN_RIGHT
|
||||
};
|
||||
|
||||
void setAlignment(Alignment align)
|
||||
{
|
||||
// Set alignment.
|
||||
mAlignment = align;
|
||||
}
|
||||
|
||||
void setCursorChangedCallback(const std::function<void(CursorState state)>& func)
|
||||
void setCursorChangedCallback(const std::function<void(CursorState state)>& func) override
|
||||
{
|
||||
mCursorChangedCallback = func;
|
||||
}
|
||||
void setCancelTransitionsCallback(const std::function<void()>& func) override
|
||||
{
|
||||
mCancelTransitionsCallback = func;
|
||||
}
|
||||
|
||||
void setFont(const std::shared_ptr<Font>& font)
|
||||
{
|
||||
|
@ -135,16 +131,37 @@ protected:
|
|||
void onCursorChanged(const CursorState& state) override;
|
||||
|
||||
private:
|
||||
bool isScrolling() const override { return List::isScrolling(); }
|
||||
void stopScrolling() override { List::stopScrolling(); }
|
||||
const int getScrollingVelocity() override { return List::getScrollingVelocity(); }
|
||||
void clear() override { List::clear(); }
|
||||
const T& getSelected() const override { return List::getSelected(); }
|
||||
const T& getNext() const override { return List::getNext(); }
|
||||
const T& getPrevious() const override { return List::getPrevious(); }
|
||||
const T& getFirst() const override { return List::getFirst(); }
|
||||
const T& getLast() const override { return List::getLast(); }
|
||||
bool setCursor(const T& obj) override { return List::setCursor(obj); }
|
||||
bool remove(const T& obj) override { return List::remove(obj); }
|
||||
int size() const override { return List::size(); }
|
||||
|
||||
int getCursor() override { return mCursor; }
|
||||
const size_t getNumEntries() override { return mEntries.size(); }
|
||||
|
||||
Renderer* mRenderer;
|
||||
std::function<void()> mCancelTransitionsCallback;
|
||||
float mCamOffset;
|
||||
int mPreviousScrollVelocity;
|
||||
|
||||
int mLoopOffset;
|
||||
int mLoopOffset2;
|
||||
int mLoopTime;
|
||||
bool mLoopScroll;
|
||||
|
||||
Alignment mAlignment;
|
||||
PrimaryAlignment mAlignment;
|
||||
float mHorizontalMargin;
|
||||
|
||||
std::function<void(CursorState state)> mCursorChangedCallback;
|
||||
ImageComponent mSelectorImage;
|
||||
|
||||
std::shared_ptr<Font> mFont;
|
||||
bool mUppercase;
|
||||
|
@ -159,34 +176,162 @@ private:
|
|||
unsigned int mSelectedColor;
|
||||
static const unsigned int COLOR_ID_COUNT = 2;
|
||||
unsigned int mColors[COLOR_ID_COUNT];
|
||||
|
||||
ImageComponent mSelectorImage;
|
||||
};
|
||||
|
||||
template <typename T> TextListComponent<T>::TextListComponent()
|
||||
template <typename T>
|
||||
TextListComponent<T>::TextListComponent()
|
||||
: IList<TextListData, T> {(std::is_same_v<T, SystemData*> ? LIST_SCROLL_STYLE_SLOW :
|
||||
LIST_SCROLL_STYLE_QUICK),
|
||||
ListLoopType::LIST_PAUSE_AT_END}
|
||||
, mRenderer {Renderer::getInstance()}
|
||||
, mCamOffset {0.0f}
|
||||
, mPreviousScrollVelocity {0}
|
||||
, mLoopOffset {0}
|
||||
, mLoopOffset2 {0}
|
||||
, mLoopTime {0}
|
||||
, mLoopScroll {false}
|
||||
, mAlignment {PrimaryAlignment::ALIGN_CENTER}
|
||||
, mHorizontalMargin {0.0f}
|
||||
, mFont {Font::get(FONT_SIZE_MEDIUM)}
|
||||
, mUppercase {false}
|
||||
, mLowercase {false}
|
||||
, mCapitalize {false}
|
||||
, mLineSpacing {1.5f}
|
||||
, mSelectorHeight {mFont->getSize() * 1.5f}
|
||||
, mSelectorOffsetY {0.0f}
|
||||
, mSelectorColor {0x000000FF}
|
||||
, mSelectorColorEnd {0x000000FF}
|
||||
, mSelectorColorGradientHorizontal {true}
|
||||
, mSelectedColor {0}
|
||||
, mColors {0x0000FFFF, 0x00FF00FF}
|
||||
{
|
||||
mRenderer = Renderer::getInstance();
|
||||
mLoopOffset = 0;
|
||||
mLoopOffset2 = 0;
|
||||
mLoopTime = 0;
|
||||
mLoopScroll = false;
|
||||
}
|
||||
|
||||
mHorizontalMargin = 0.0f;
|
||||
mAlignment = ALIGN_CENTER;
|
||||
template <typename T>
|
||||
void TextListComponent<T>::addEntry(Entry& entry, const std::shared_ptr<ThemeData>& theme)
|
||||
{
|
||||
List::add(entry);
|
||||
}
|
||||
|
||||
mFont = Font::get(FONT_SIZE_MEDIUM);
|
||||
mUppercase = false;
|
||||
mLowercase = false;
|
||||
mCapitalize = false;
|
||||
mLineSpacing = 1.5f;
|
||||
mSelectorHeight = mFont->getSize() * 1.5f;
|
||||
mSelectorOffsetY = 0;
|
||||
mSelectorColor = 0x000000FF;
|
||||
mSelectorColorEnd = 0x000000FF;
|
||||
mSelectorColorGradientHorizontal = true;
|
||||
mSelectedColor = 0;
|
||||
mColors[0] = 0x0000FFFF;
|
||||
mColors[1] = 0x00FF00FF;
|
||||
template <typename T> bool TextListComponent<T>::input(InputConfig* config, Input input)
|
||||
{
|
||||
if (size() > 0) {
|
||||
if (input.value != 0) {
|
||||
if (config->isMappedLike("up", input)) {
|
||||
if (mCancelTransitionsCallback)
|
||||
mCancelTransitionsCallback();
|
||||
List::listInput(-1);
|
||||
return true;
|
||||
}
|
||||
if (config->isMappedLike("down", input)) {
|
||||
if (mCancelTransitionsCallback)
|
||||
mCancelTransitionsCallback();
|
||||
List::listInput(1);
|
||||
return true;
|
||||
}
|
||||
if (config->isMappedLike("leftshoulder", input)) {
|
||||
if (getCursor() == 0) {
|
||||
if (!NavigationSounds::getInstance().isPlayingThemeNavigationSound(SCROLLSOUND))
|
||||
NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND);
|
||||
return true;
|
||||
}
|
||||
if (mCancelTransitionsCallback)
|
||||
mCancelTransitionsCallback();
|
||||
List::listInput(-10);
|
||||
return true;
|
||||
}
|
||||
if (config->isMappedLike("rightshoulder", input)) {
|
||||
if (getCursor() == static_cast<int>(mEntries.size()) - 1) {
|
||||
if (!NavigationSounds::getInstance().isPlayingThemeNavigationSound(SCROLLSOUND))
|
||||
NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND);
|
||||
return true;
|
||||
}
|
||||
if (mCancelTransitionsCallback)
|
||||
mCancelTransitionsCallback();
|
||||
List::listInput(10);
|
||||
return true;
|
||||
}
|
||||
if (config->isMappedLike("righttrigger", input)) {
|
||||
if (getCursor() == static_cast<int>(mEntries.size()) - 1) {
|
||||
if (!NavigationSounds::getInstance().isPlayingThemeNavigationSound(SCROLLSOUND))
|
||||
NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND);
|
||||
return true;
|
||||
}
|
||||
if (mCancelTransitionsCallback)
|
||||
mCancelTransitionsCallback();
|
||||
return this->listLastRow();
|
||||
}
|
||||
if (config->isMappedLike("lefttrigger", input)) {
|
||||
if (getCursor() == 0) {
|
||||
if (!NavigationSounds::getInstance().isPlayingThemeNavigationSound(SCROLLSOUND))
|
||||
NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND);
|
||||
return true;
|
||||
}
|
||||
if (mCancelTransitionsCallback)
|
||||
mCancelTransitionsCallback();
|
||||
return this->listFirstRow();
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (config->isMappedLike("up", input) || config->isMappedLike("down", input) ||
|
||||
config->isMappedLike("leftshoulder", input) ||
|
||||
config->isMappedLike("rightshoulder", input) ||
|
||||
config->isMappedLike("lefttrigger", input) ||
|
||||
config->isMappedLike("righttrigger", input)) {
|
||||
if constexpr (std::is_same_v<T, SystemData*>)
|
||||
List::listInput(0);
|
||||
else
|
||||
List::stopScrolling();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return GuiComponent::input(config, input);
|
||||
}
|
||||
|
||||
template <typename T> void TextListComponent<T>::update(int deltaTime)
|
||||
{
|
||||
List::listUpdate(deltaTime);
|
||||
|
||||
if (mWindow->isScreensaverActive() || !mWindow->getAllowTextScrolling())
|
||||
List::stopScrolling();
|
||||
|
||||
if (!isScrolling() && size() > 0) {
|
||||
// Always reset the loop offsets.
|
||||
mLoopOffset = 0;
|
||||
mLoopOffset2 = 0;
|
||||
|
||||
// If we're not scrolling and this object's text exceeds our size, then loop it.
|
||||
const float textLength {mFont
|
||||
->sizeText(Utils::String::toUpper(
|
||||
mEntries.at(static_cast<unsigned int>(mCursor)).name))
|
||||
.x};
|
||||
const float limit {mSize.x - mHorizontalMargin * 2.0f};
|
||||
|
||||
if (textLength > limit) {
|
||||
// Loop the text.
|
||||
const float speed {mFont->sizeText("ABCDEFGHIJKLMNOPQRSTUVWXYZ").x * 0.247f};
|
||||
const float delay {3000.0f};
|
||||
const float scrollLength {textLength};
|
||||
const float returnLength {speed * 1.5f};
|
||||
const float scrollTime {(scrollLength * 1000.0f) / speed};
|
||||
const float returnTime {(returnLength * 1000.0f) / speed};
|
||||
const int maxTime {static_cast<int>(delay + scrollTime + returnTime)};
|
||||
|
||||
mLoopTime += deltaTime;
|
||||
while (mLoopTime > maxTime)
|
||||
mLoopTime -= maxTime;
|
||||
|
||||
mLoopOffset = static_cast<int>(Utils::Math::loop(delay, scrollTime + returnTime,
|
||||
static_cast<float>(mLoopTime),
|
||||
scrollLength + returnLength));
|
||||
|
||||
if (mLoopOffset > (scrollLength - (limit - returnLength)))
|
||||
mLoopOffset2 = static_cast<int>(mLoopOffset - (scrollLength + returnLength));
|
||||
}
|
||||
}
|
||||
|
||||
GuiComponent::update(deltaTime);
|
||||
}
|
||||
|
||||
template <typename T> void TextListComponent<T>::render(const glm::mat4& parentTrans)
|
||||
|
@ -259,7 +404,7 @@ template <typename T> void TextListComponent<T>::render(const glm::mat4& parentT
|
|||
static_cast<int>(std::round(dim.y))});
|
||||
|
||||
for (int i = startEntry; i < listCutoff; ++i) {
|
||||
typename IList<TextListData, T>::Entry& entry {mEntries.at(static_cast<unsigned int>(i))};
|
||||
Entry& entry {mEntries.at(i)};
|
||||
|
||||
unsigned int color;
|
||||
if (mCursor == i && mSelectedColor)
|
||||
|
@ -282,28 +427,33 @@ template <typename T> void TextListComponent<T>::render(const glm::mat4& parentT
|
|||
std::unique_ptr<TextCache>(font->buildTextCache(entry.name, 0, 0, 0x000000FF));
|
||||
}
|
||||
|
||||
// If a game is marked as hidden, lower the text opacity a lot.
|
||||
// If a game is marked to not be counted, lower the opacity a moderate amount.
|
||||
if (entry.object->getHidden())
|
||||
entry.data.textCache->setColor(color & 0xFFFFFF44);
|
||||
else if (!entry.object->getCountAsGame())
|
||||
entry.data.textCache->setColor(color & 0xFFFFFF77);
|
||||
else
|
||||
if constexpr (std::is_same_v<T, FileData*>) {
|
||||
// If a game is marked as hidden, lower the text opacity a lot.
|
||||
// If a game is marked to not be counted, lower the opacity a moderate amount.
|
||||
if (entry.object->getHidden())
|
||||
entry.data.textCache->setColor(color & 0xFFFFFF44);
|
||||
else if (!entry.object->getCountAsGame())
|
||||
entry.data.textCache->setColor(color & 0xFFFFFF77);
|
||||
else
|
||||
entry.data.textCache->setColor(color);
|
||||
}
|
||||
else {
|
||||
entry.data.textCache->setColor(color);
|
||||
}
|
||||
|
||||
glm::vec3 offset {0.0f, y, 0.0f};
|
||||
|
||||
switch (mAlignment) {
|
||||
case ALIGN_LEFT:
|
||||
case PrimaryAlignment::ALIGN_LEFT:
|
||||
offset.x = mHorizontalMargin;
|
||||
break;
|
||||
case ALIGN_CENTER:
|
||||
case PrimaryAlignment::ALIGN_CENTER:
|
||||
offset.x =
|
||||
static_cast<float>((mSize.x - entry.data.textCache->metrics.size.x) / 2.0f);
|
||||
if (offset.x < mHorizontalMargin)
|
||||
offset.x = mHorizontalMargin;
|
||||
break;
|
||||
case ALIGN_RIGHT:
|
||||
case PrimaryAlignment::ALIGN_RIGHT:
|
||||
offset.x = (mSize.x - entry.data.textCache->metrics.size.x);
|
||||
offset.x -= mHorizontalMargin;
|
||||
if (offset.x < mHorizontalMargin)
|
||||
|
@ -340,122 +490,11 @@ template <typename T> void TextListComponent<T>::render(const glm::mat4& parentT
|
|||
y += entrySize;
|
||||
}
|
||||
mRenderer->popClipRect();
|
||||
List::listRenderTitleOverlay(trans);
|
||||
if constexpr (std::is_same_v<T, FileData*>)
|
||||
List::listRenderTitleOverlay(trans);
|
||||
GuiComponent::renderChildren(trans);
|
||||
}
|
||||
|
||||
template <typename T> bool TextListComponent<T>::input(InputConfig* config, Input input)
|
||||
{
|
||||
if (size() > 0) {
|
||||
if (input.value != 0) {
|
||||
if (config->isMappedLike("down", input)) {
|
||||
List::listInput(1);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (config->isMappedLike("up", input)) {
|
||||
List::listInput(-1);
|
||||
return true;
|
||||
}
|
||||
if (config->isMappedLike("rightshoulder", input)) {
|
||||
List::listInput(10);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (config->isMappedLike("leftshoulder", input)) {
|
||||
List::listInput(-10);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (config->isMappedLike("righttrigger", input)) {
|
||||
return this->listLastRow();
|
||||
}
|
||||
|
||||
if (config->isMappedLike("lefttrigger", input)) {
|
||||
return this->listFirstRow();
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (config->isMappedLike("down", input) || config->isMappedLike("up", input) ||
|
||||
config->isMappedLike("rightshoulder", input) ||
|
||||
config->isMappedLike("leftshoulder", input) ||
|
||||
config->isMappedLike("lefttrigger", input) ||
|
||||
config->isMappedLike("righttrigger", input))
|
||||
List::stopScrolling();
|
||||
}
|
||||
}
|
||||
|
||||
return GuiComponent::input(config, input);
|
||||
}
|
||||
|
||||
template <typename T> void TextListComponent<T>::update(int deltaTime)
|
||||
{
|
||||
List::listUpdate(deltaTime);
|
||||
|
||||
if (mWindow->isScreensaverActive() || !mWindow->getAllowTextScrolling())
|
||||
List::stopScrolling();
|
||||
|
||||
if (!List::isScrolling() && size() > 0) {
|
||||
// Always reset the loop offsets.
|
||||
mLoopOffset = 0;
|
||||
mLoopOffset2 = 0;
|
||||
|
||||
// If we're not scrolling and this object's text exceeds our size, then loop it.
|
||||
const float textLength {mFont
|
||||
->sizeText(Utils::String::toUpper(
|
||||
mEntries.at(static_cast<unsigned int>(mCursor)).name))
|
||||
.x};
|
||||
const float limit {mSize.x - mHorizontalMargin * 2.0f};
|
||||
|
||||
if (textLength > limit) {
|
||||
// Loop the text.
|
||||
const float speed {mFont->sizeText("ABCDEFGHIJKLMNOPQRSTUVWXYZ").x * 0.247f};
|
||||
const float delay {3000.0f};
|
||||
const float scrollLength {textLength};
|
||||
const float returnLength {speed * 1.5f};
|
||||
const float scrollTime {(scrollLength * 1000.0f) / speed};
|
||||
const float returnTime {(returnLength * 1000.0f) / speed};
|
||||
const int maxTime {static_cast<int>(delay + scrollTime + returnTime)};
|
||||
|
||||
mLoopTime += deltaTime;
|
||||
while (mLoopTime > maxTime)
|
||||
mLoopTime -= maxTime;
|
||||
|
||||
mLoopOffset = static_cast<int>(Utils::Math::loop(delay, scrollTime + returnTime,
|
||||
static_cast<float>(mLoopTime),
|
||||
scrollLength + returnLength));
|
||||
|
||||
if (mLoopOffset > (scrollLength - (limit - returnLength)))
|
||||
mLoopOffset2 = static_cast<int>(mLoopOffset - (scrollLength + returnLength));
|
||||
}
|
||||
}
|
||||
|
||||
GuiComponent::update(deltaTime);
|
||||
}
|
||||
|
||||
// List management stuff.
|
||||
template <typename T>
|
||||
void TextListComponent<T>::add(const std::string& name, const T& obj, unsigned int color)
|
||||
{
|
||||
assert(color < COLOR_ID_COUNT);
|
||||
|
||||
typename IList<TextListData, T>::Entry entry;
|
||||
entry.name = name;
|
||||
entry.object = obj;
|
||||
entry.data.colorId = color;
|
||||
static_cast<IList<TextListData, T>*>(this)->add(entry);
|
||||
}
|
||||
|
||||
template <typename T> void TextListComponent<T>::onCursorChanged(const CursorState& state)
|
||||
{
|
||||
mLoopOffset = 0;
|
||||
mLoopOffset2 = 0;
|
||||
mLoopTime = 0;
|
||||
|
||||
if (mCursorChangedCallback)
|
||||
mCursorChangedCallback(state);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void TextListComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme,
|
||||
const std::string& view,
|
||||
|
@ -508,11 +547,11 @@ void TextListComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme,
|
|||
if (elem->has("horizontalAlignment")) {
|
||||
const std::string& str {elem->get<std::string>("horizontalAlignment")};
|
||||
if (str == "left")
|
||||
setAlignment(ALIGN_LEFT);
|
||||
setAlignment(PrimaryAlignment::ALIGN_LEFT);
|
||||
else if (str == "center")
|
||||
setAlignment(ALIGN_CENTER);
|
||||
setAlignment(PrimaryAlignment::ALIGN_CENTER);
|
||||
else if (str == "right")
|
||||
setAlignment(ALIGN_RIGHT);
|
||||
setAlignment(PrimaryAlignment::ALIGN_RIGHT);
|
||||
else
|
||||
LOG(LogWarning) << "TextListComponent: Invalid theme configuration, property "
|
||||
"<horizontalAlignment> defined as \""
|
||||
|
@ -522,11 +561,11 @@ void TextListComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme,
|
|||
else if (elem->has("alignment")) {
|
||||
const std::string& str {elem->get<std::string>("alignment")};
|
||||
if (str == "left")
|
||||
setAlignment(ALIGN_LEFT);
|
||||
setAlignment(PrimaryAlignment::ALIGN_LEFT);
|
||||
else if (str == "center")
|
||||
setAlignment(ALIGN_CENTER);
|
||||
setAlignment(PrimaryAlignment::ALIGN_CENTER);
|
||||
else if (str == "right")
|
||||
setAlignment(ALIGN_RIGHT);
|
||||
setAlignment(PrimaryAlignment::ALIGN_RIGHT);
|
||||
else
|
||||
LOG(LogWarning) << "TextListComponent: Invalid theme configuration, property "
|
||||
"<alignment> defined as \""
|
||||
|
@ -590,4 +629,35 @@ void TextListComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme,
|
|||
}
|
||||
}
|
||||
|
||||
template <typename T> void TextListComponent<T>::onCursorChanged(const CursorState& state)
|
||||
{
|
||||
mLoopOffset = 0;
|
||||
mLoopOffset2 = 0;
|
||||
mLoopTime = 0;
|
||||
|
||||
if constexpr (std::is_same_v<T, SystemData*>) {
|
||||
float startPos {mCamOffset};
|
||||
float posMax {static_cast<float>(mEntries.size())};
|
||||
float endPos {static_cast<float>(mCursor)};
|
||||
|
||||
Animation* anim {new LambdaAnimation(
|
||||
[this, startPos, endPos, posMax](float t) {
|
||||
t -= 1;
|
||||
float f {glm::mix(startPos, endPos, t * t * t + 1)};
|
||||
if (f < 0)
|
||||
f += posMax;
|
||||
if (f >= posMax)
|
||||
f -= posMax;
|
||||
|
||||
mCamOffset = f;
|
||||
},
|
||||
500)};
|
||||
|
||||
GuiComponent::setAnimation(anim, 0, nullptr, false, 0);
|
||||
}
|
||||
|
||||
if (mCursorChangedCallback)
|
||||
mCursorChangedCallback(state);
|
||||
}
|
||||
|
||||
#endif // ES_CORE_COMPONENTS_TEXT_LIST_COMPONENT_H
|
Loading…
Reference in a new issue