From 5bedb7a8fabf780ce65522fb866025142c799b58 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sun, 6 Nov 2022 21:28:15 +0100 Subject: [PATCH 001/101] Small refactoring of CarouselComponent and TextListComponent. --- es-core/src/components/primary/CarouselComponent.h | 8 ++++---- es-core/src/components/primary/TextListComponent.h | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/es-core/src/components/primary/CarouselComponent.h b/es-core/src/components/primary/CarouselComponent.h index 5051d31da..2acc06723 100644 --- a/es-core/src/components/primary/CarouselComponent.h +++ b/es-core/src/components/primary/CarouselComponent.h @@ -66,14 +66,14 @@ public: return mLetterCaseGroupedCollections; } - void setCursorChangedCallback(const std::function& func) override - { - mCursorChangedCallback = func; - } void setCancelTransitionsCallback(const std::function& func) override { mCancelTransitionsCallback = func; } + void setCursorChangedCallback(const std::function& func) override + { + mCursorChangedCallback = func; + } bool input(InputConfig* config, Input input) override; void update(int deltaTime) override; diff --git a/es-core/src/components/primary/TextListComponent.h b/es-core/src/components/primary/TextListComponent.h index 16d537b3f..42cfa8378 100644 --- a/es-core/src/components/primary/TextListComponent.h +++ b/es-core/src/components/primary/TextListComponent.h @@ -57,14 +57,14 @@ public: void setAlignment(PrimaryAlignment align) override { mAlignment = align; } - void setCursorChangedCallback(const std::function& func) override - { - mCursorChangedCallback = func; - } void setCancelTransitionsCallback(const std::function& func) override { mCancelTransitionsCallback = func; } + void setCursorChangedCallback(const std::function& func) override + { + mCursorChangedCallback = func; + } void setFont(const std::shared_ptr& font) { @@ -111,6 +111,7 @@ private: Renderer* mRenderer; std::function mCancelTransitionsCallback; + std::function mCursorChangedCallback; float mCamOffset; int mPreviousScrollVelocity; @@ -122,7 +123,6 @@ private: PrimaryAlignment mAlignment; float mHorizontalMargin; - std::function mCursorChangedCallback; ImageComponent mSelectorImage; std::shared_ptr mFont; From 4e54508e45ba3c99813a54c75df1e08d90db9f36 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sun, 6 Nov 2022 21:34:03 +0100 Subject: [PATCH 002/101] Added a GridComponent skeleton. --- es-app/src/views/SystemView.h | 1 + es-core/CMakeLists.txt | 1 + .../src/components/primary/GridComponent.h | 70 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 es-core/src/components/primary/GridComponent.h diff --git a/es-app/src/views/SystemView.h b/es-app/src/views/SystemView.h index 462dd1c81..26a15442d 100644 --- a/es-app/src/views/SystemView.h +++ b/es-app/src/views/SystemView.h @@ -21,6 +21,7 @@ #include "components/TextComponent.h" #include "components/VideoFFmpegComponent.h" #include "components/primary/CarouselComponent.h" +#include "components/primary/GridComponent.h" #include "components/primary/TextListComponent.h" #include "resources/Font.h" diff --git a/es-core/CMakeLists.txt b/es-core/CMakeLists.txt index 40cc69038..59b07a8cf 100644 --- a/es-core/CMakeLists.txt +++ b/es-core/CMakeLists.txt @@ -33,6 +33,7 @@ set(CORE_HEADERS # Primary GUI components ${CMAKE_CURRENT_SOURCE_DIR}/src/components/primary/CarouselComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/primary/GridComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/primary/PrimaryComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/primary/TextListComponent.h diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h new file mode 100644 index 000000000..770a86d9a --- /dev/null +++ b/es-core/src/components/primary/GridComponent.h @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// GridComponent.h +// +// Grid, usable in both the system and gamelist views. +// + +#ifndef ES_CORE_COMPONENTS_GRID_COMPONENT_H +#define ES_CORE_COMPONENTS_GRID_COMPONENT_H + +#include "components/IList.h" +#include "components/primary/PrimaryComponent.h" + +struct GridEntry { + std::shared_ptr item; + std::string itemPath; + std::string defaultItemPath; +}; + +template +class GridComponent : public PrimaryComponent, protected IList +{ + using List = IList; + +protected: + using List::mCursor; + using List::mEntries; + +public: + GridComponent(); + + void setCancelTransitionsCallback(const std::function& func) override + { + mCancelTransitionsCallback = func; + } + void setCursorChangedCallback(const std::function& func) override + { + mCursorChangedCallback = func; + } + int getCursor() override { return mCursor; } + const size_t getNumEntries() override { return mEntries.size(); } + const bool getFadeAbovePrimary() const override { return mFadeAbovePrimary; } + const LetterCase getLetterCase() const override { return mLetterCase; } + virtual const LetterCase getLetterCaseCollections() const = 0; + virtual const LetterCase getLetterCaseGroupedCollections() const = 0; + +private: + Renderer* mRenderer; + std::function mCancelTransitionsCallback; + std::function mCursorChangedCallback; + + bool mFadeAbovePrimary; + LetterCase mLetterCase; + LetterCase mLetterCaseCollections; + LetterCase mLetterCaseGroupedCollections; +}; + +template +GridComponent::GridComponent() + : IList {} + , mRenderer {Renderer::getInstance()} + , mFadeAbovePrimary {false} + , mLetterCase {LetterCase::NONE} + , mLetterCaseCollections {LetterCase::NONE} + , mLetterCaseGroupedCollections {LetterCase::NONE} +{ +} + +#endif // ES_CORE_COMPONENTS_GRID_COMPONENT_H From c73ff02012295ae3b2e7c5fe2234b554b2693028 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sun, 6 Nov 2022 21:35:36 +0100 Subject: [PATCH 003/101] Updated the file header comments for CarouselComponent and TextListComponent. --- es-core/src/components/primary/CarouselComponent.h | 2 +- es-core/src/components/primary/TextListComponent.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/es-core/src/components/primary/CarouselComponent.h b/es-core/src/components/primary/CarouselComponent.h index 2acc06723..d88ce0484 100644 --- a/es-core/src/components/primary/CarouselComponent.h +++ b/es-core/src/components/primary/CarouselComponent.h @@ -3,7 +3,7 @@ // EmulationStation Desktop Edition // CarouselComponent.h // -// Carousel. +// Carousel, usable in both the system and gamelist views. // #ifndef ES_CORE_COMPONENTS_CAROUSEL_COMPONENT_H diff --git a/es-core/src/components/primary/TextListComponent.h b/es-core/src/components/primary/TextListComponent.h index 42cfa8378..a658d1ed2 100644 --- a/es-core/src/components/primary/TextListComponent.h +++ b/es-core/src/components/primary/TextListComponent.h @@ -3,7 +3,7 @@ // EmulationStation Desktop Edition // TextListComponent.h // -// Text list used for displaying and navigating the gamelist views. +// Text list, usable in both the system and gamelist views. // #ifndef ES_CORE_COMPONENTS_TEXT_LIST_COMPONENT_H From b4338a3fb744c02ae82e8009adc540fe6f1d55e3 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Mon, 7 Nov 2022 23:58:22 +0100 Subject: [PATCH 004/101] Refactored some image resizing functions. --- es-app/src/guis/GuiMetaDataEd.cpp | 3 +-- es-core/src/GuiComponent.h | 4 ++-- es-core/src/components/AnimatedImageComponent.cpp | 7 +++---- es-core/src/components/ImageComponent.cpp | 6 +++--- es-core/src/components/ImageComponent.h | 6 +----- es-core/src/components/SwitchComponent.h | 1 - es-core/src/components/VideoComponent.h | 6 ------ es-core/src/components/VideoFFmpegComponent.cpp | 4 ++-- es-core/src/components/VideoFFmpegComponent.h | 2 +- 9 files changed, 13 insertions(+), 26 deletions(-) diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index 40593ba15..4104af36f 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -149,8 +149,7 @@ GuiMetaDataEd::GuiMetaDataEd(MetaDataList* md, case MD_BOOL: { ed = std::make_shared(); // Make the switches slightly smaller. - glm::vec2 switchSize {ed->getSize() * 0.9f}; - ed->setResize(ceilf(switchSize.x), switchSize.y); + ed->setSize(glm::ceil(ed->getSize() * 0.9f)); ed->setChangedColor(ICONCOLOR_USERMARKED); row.addElement(ed, false, true); diff --git a/es-core/src/GuiComponent.h b/es-core/src/GuiComponent.h index 827bb57bc..8727af152 100644 --- a/es-core/src/GuiComponent.h +++ b/es-core/src/GuiComponent.h @@ -104,8 +104,8 @@ public: virtual glm::vec2 getSize() const { return mSize; } void setSize(const glm::vec2& size) { setSize(size.x, size.y); } void setSize(const float w, const float h); - virtual void setResize(float width, float height) {} - virtual void setResize(float width, float height, bool rasterize) {} + virtual void setResize(const float width, const float height) {} + virtual void setResize(const glm::vec2& size, bool rasterize = true) {} virtual void onSizeChanged() {} virtual glm::vec2 getRotationSize() const { return getSize(); } diff --git a/es-core/src/components/AnimatedImageComponent.cpp b/es-core/src/components/AnimatedImageComponent.cpp index b1745954d..4f530cc5a 100644 --- a/es-core/src/components/AnimatedImageComponent.cpp +++ b/es-core/src/components/AnimatedImageComponent.cpp @@ -32,7 +32,7 @@ void AnimatedImageComponent::load(const AnimationDef* def) } auto img = std::unique_ptr(new ImageComponent); - img->setResize(mSize.x, mSize.y); + img->setResize(mSize); img->setImage(std::string(def->frames[i].path), false); mFrames.push_back(ImageFrame(std::move(img), def->frames[i].time)); @@ -53,9 +53,8 @@ void AnimatedImageComponent::reset() void AnimatedImageComponent::onSizeChanged() { - for (auto it = mFrames.cbegin(); it != mFrames.cend(); ++it) { - it->first->setResize(mSize.x, mSize.y); - } + for (auto it = mFrames.cbegin(); it != mFrames.cend(); ++it) + it->first->setResize(mSize); } void AnimatedImageComponent::update(int deltaTime) diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index fd9c142c4..9922485b8 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -123,16 +123,16 @@ void ImageComponent::setImage(const std::shared_ptr& texture, b resize(); } -void ImageComponent::setResize(float width, float height) +void ImageComponent::setResize(const float width, const float height) { mTargetSize = glm::vec2 {width, height}; mTargetIsMax = false; resize(); } -void ImageComponent::setResize(float width, float height, bool rasterize) +void ImageComponent::setResize(const glm::vec2& size, bool rasterize) { - mTargetSize = glm::vec2 {width, height}; + mTargetSize = size; mTargetIsMax = false; resize(rasterize); } diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index 2a8eb2e8c..54d0d6e63 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -39,11 +39,7 @@ public: // Can be set before or after an image is loaded. // setMaxSize() and setResize() are mutually exclusive. void setResize(const float width, const float height) override; - void setResize(const glm::vec2& size, bool rasterize = true) - { - setResize(size.x, size.y, rasterize); - } - void setResize(const float width, const float height, bool rasterize) override; + void setResize(const glm::vec2& size, bool rasterize = true) override; // Resize the image to be as large as possible but fit within a box of this size. // Can be set before or after an image is loaded. diff --git a/es-core/src/components/SwitchComponent.h b/es-core/src/components/SwitchComponent.h index 653f3d13a..60843e59a 100644 --- a/es-core/src/components/SwitchComponent.h +++ b/es-core/src/components/SwitchComponent.h @@ -22,7 +22,6 @@ public: void render(const glm::mat4& parentTrans) override; void onSizeChanged() override { mImage.setSize(mSize); } - void setResize(float width, float height) override { setSize(width, height); } bool getState() const { return mState; } void setState(bool state); std::string getValue() const override; diff --git a/es-core/src/components/VideoComponent.h b/es-core/src/components/VideoComponent.h index 8072930a0..d214dead5 100644 --- a/es-core/src/components/VideoComponent.h +++ b/es-core/src/components/VideoComponent.h @@ -76,12 +76,6 @@ public: void update(int deltaTime) override; - // Resize the video to fit this size. If one axis is zero, scale that axis to maintain - // aspect ratio. If both are non-zero, potentially break the aspect ratio. If both are - // zero, no resizing. This can be set before or after a video is loaded. - // setMaxSize() and setResize() are mutually exclusive. - virtual void setResize(float width, float height) override = 0; - // Resize the video to be as large as possible but fit within a box of this size. // This can be set before or after a video is loaded. // Never breaks the aspect ratio. setMaxSize() and setResize() are mutually exclusive. diff --git a/es-core/src/components/VideoFFmpegComponent.cpp b/es-core/src/components/VideoFFmpegComponent.cpp index 05a80b638..b78994535 100644 --- a/es-core/src/components/VideoFFmpegComponent.cpp +++ b/es-core/src/components/VideoFFmpegComponent.cpp @@ -60,12 +60,12 @@ VideoFFmpegComponent::VideoFFmpegComponent() { } -void VideoFFmpegComponent::setResize(float width, float height) +void VideoFFmpegComponent::setResize(const float width, const float height) { // This resize function is used when stretching videos to full screen in the video screensaver. mTargetSize = glm::vec2 {width, height}; mTargetIsMax = false; - mStaticImage.setResize(width, height); + mStaticImage.setResize(mTargetSize); resize(); } diff --git a/es-core/src/components/VideoFFmpegComponent.h b/es-core/src/components/VideoFFmpegComponent.h index 240fd2a6a..e4f69147d 100644 --- a/es-core/src/components/VideoFFmpegComponent.h +++ b/es-core/src/components/VideoFFmpegComponent.h @@ -40,7 +40,7 @@ public: // aspect ratio. If both are non-zero, potentially break the aspect ratio. If both are // zero, no resizing. This can be set before or after a video is loaded. // setMaxSize() and setResize() are mutually exclusive. - void setResize(float width, float height) override; + void setResize(const float width, const float height) override; // Resize the video to be as large as possible but fit within a box of this size. // This can be set before or after a video is loaded. From 319992a0f78f1f5c44a553c26a33c7d2621e730b Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Tue, 8 Nov 2022 21:44:42 +0100 Subject: [PATCH 005/101] Changed the include guard names for the primary components. --- es-core/src/components/primary/CarouselComponent.h | 6 +++--- es-core/src/components/primary/PrimaryComponent.h | 6 +++--- es-core/src/components/primary/TextListComponent.h | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/es-core/src/components/primary/CarouselComponent.h b/es-core/src/components/primary/CarouselComponent.h index d88ce0484..6ab01846b 100644 --- a/es-core/src/components/primary/CarouselComponent.h +++ b/es-core/src/components/primary/CarouselComponent.h @@ -6,8 +6,8 @@ // Carousel, usable in both the system and gamelist views. // -#ifndef ES_CORE_COMPONENTS_CAROUSEL_COMPONENT_H -#define ES_CORE_COMPONENTS_CAROUSEL_COMPONENT_H +#ifndef ES_CORE_COMPONENTS_PRIMARY_CAROUSEL_COMPONENT_H +#define ES_CORE_COMPONENTS_PRIMARY_CAROUSEL_COMPONENT_H #include "Sound.h" #include "animations/LambdaAnimation.h" @@ -1346,4 +1346,4 @@ template void CarouselComponent::onCursorChanged(const CursorSta mCursorChangedCallback(state); } -#endif // ES_CORE_COMPONENTS_CAROUSEL_COMPONENT_H +#endif // ES_CORE_COMPONENTS_PRIMARY_CAROUSEL_COMPONENT_H diff --git a/es-core/src/components/primary/PrimaryComponent.h b/es-core/src/components/primary/PrimaryComponent.h index 9df90c1dd..03ad7c7a9 100644 --- a/es-core/src/components/primary/PrimaryComponent.h +++ b/es-core/src/components/primary/PrimaryComponent.h @@ -6,8 +6,8 @@ // Base class for the primary components (carousel and textlist). // -#ifndef ES_CORE_COMPONENTS_PRIMARY_COMPONENT_H -#define ES_CORE_COMPONENTS_PRIMARY_COMPONENT_H +#ifndef ES_CORE_COMPONENTS_PRIMARY_PRIMARY_COMPONENT_H +#define ES_CORE_COMPONENTS_PRIMARY_PRIMARY_COMPONENT_H template class PrimaryComponent : public virtual GuiComponent { @@ -52,4 +52,4 @@ public: virtual void setAlignment(PrimaryAlignment align) {} }; -#endif // ES_CORE_COMPONENTS_PRIMARY_COMPONENT_H +#endif // ES_CORE_COMPONENTS_PRIMARY_PRIMARY_COMPONENT_H diff --git a/es-core/src/components/primary/TextListComponent.h b/es-core/src/components/primary/TextListComponent.h index a658d1ed2..abebebd24 100644 --- a/es-core/src/components/primary/TextListComponent.h +++ b/es-core/src/components/primary/TextListComponent.h @@ -6,8 +6,8 @@ // Text list, usable in both the system and gamelist views. // -#ifndef ES_CORE_COMPONENTS_TEXT_LIST_COMPONENT_H -#define ES_CORE_COMPONENTS_TEXT_LIST_COMPONENT_H +#ifndef ES_CORE_COMPONENTS_PRIMARY_TEXT_LIST_COMPONENT_H +#define ES_CORE_COMPONENTS_PRIMARY_TEXT_LIST_COMPONENT_H #include "Log.h" #include "Sound.h" @@ -723,4 +723,4 @@ template void TextListComponent::onCursorChanged(const CursorSta mCursorChangedCallback(state); } -#endif // ES_CORE_COMPONENTS_TEXT_LIST_COMPONENT_H +#endif // ES_CORE_COMPONENTS_PRIMARY_TEXT_LIST_COMPONENT_H From 3fd18ec2ee05b93a834b92eae10b121b9041a164 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Fri, 11 Nov 2022 18:05:59 +0100 Subject: [PATCH 006/101] Changed a function name in FlexboxComponent. --- es-core/src/components/FlexboxComponent.cpp | 4 ++-- es-core/src/components/FlexboxComponent.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/es-core/src/components/FlexboxComponent.cpp b/es-core/src/components/FlexboxComponent.cpp index 822bd55cf..52ea8278a 100644 --- a/es-core/src/components/FlexboxComponent.cpp +++ b/es-core/src/components/FlexboxComponent.cpp @@ -38,7 +38,7 @@ void FlexboxComponent::render(const glm::mat4& parentTrans) return; if (!mLayoutValid) - computeLayout(); + calculateLayout(); glm::mat4 trans {parentTrans * getTransform()}; mRenderer->setMatrix(trans); @@ -82,7 +82,7 @@ void FlexboxComponent::setItemMargin(glm::vec2 value) mLayoutValid = false; } -void FlexboxComponent::computeLayout() +void FlexboxComponent::calculateLayout() { // If we're not clamping itemMargin to a reasonable value, all kinds of weird rendering // issues could occur. diff --git a/es-core/src/components/FlexboxComponent.h b/es-core/src/components/FlexboxComponent.h index e525808cc..6cd3e0712 100644 --- a/es-core/src/components/FlexboxComponent.h +++ b/es-core/src/components/FlexboxComponent.h @@ -78,7 +78,7 @@ public: private: // Calculate flexbox layout. - void computeLayout(); + void calculateLayout(); Renderer* mRenderer; std::vector& mItems; From 74ac8393e322d154eaf2f4b92081817919d40c41 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Fri, 11 Nov 2022 18:11:31 +0100 Subject: [PATCH 007/101] Small refactoring of AnimationController. --- es-core/src/animations/AnimationController.cpp | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/es-core/src/animations/AnimationController.cpp b/es-core/src/animations/AnimationController.cpp index cee72e5b0..f951802ca 100644 --- a/es-core/src/animations/AnimationController.cpp +++ b/es-core/src/animations/AnimationController.cpp @@ -9,6 +9,7 @@ #include "animations/AnimationController.h" #include "animations/Animation.h" +#include "utils/MathUtil.h" AnimationController::AnimationController(Animation* anim, int delay, @@ -37,16 +38,10 @@ bool AnimationController::update(int deltaTime) if (mTime < 0) // Are we still in delay? return false; - float t = static_cast(mTime) / mAnimation->getDuration(); + float animTime {glm::clamp(static_cast(mTime) / mAnimation->getDuration(), 0.0f, 1.0f)}; + mAnimation->apply(mReverse ? 1.0f - animTime : animTime); - if (t > 1.0f) - t = 1.0f; - else if (t < 0.0f) - t = 0.0f; - - mAnimation->apply(mReverse ? 1.0f - t : t); - - if (t == 1.0f) + if (animTime == 1.0f) return true; return false; From 12f2142c0333642c05bea575efe26582cd1b87ee Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Fri, 11 Nov 2022 18:16:51 +0100 Subject: [PATCH 008/101] Improved the animation interpolation in CarouselComponent and TextListComponent. Also eliminated an unnecessary cursor stop function call. --- .../components/primary/CarouselComponent.h | 22 +++++++++++++--- .../components/primary/TextListComponent.h | 25 ++++++++++++++++--- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/es-core/src/components/primary/CarouselComponent.h b/es-core/src/components/primary/CarouselComponent.h index 6ab01846b..ba0b13817 100644 --- a/es-core/src/components/primary/CarouselComponent.h +++ b/es-core/src/components/primary/CarouselComponent.h @@ -527,7 +527,8 @@ template bool CarouselComponent::input(InputConfig* config, Inpu config->isMappedLike("rightshoulder", input) || config->isMappedLike("lefttrigger", input) || config->isMappedLike("righttrigger", input)) { - onCursorChanged(CursorState::CURSOR_STOPPED); + if (isScrolling()) + onCursorChanged(CursorState::CURSOR_STOPPED); List::listInput(0); mTriggerJump = false; } @@ -1326,11 +1327,24 @@ template void CarouselComponent::onCursorChanged(const CursorSta mPositiveDirection = false; mEntryCamTarget = endPos; + float animTime {380}; + float timeDiff {1.0f}; + + // If startPos is inbetween two positions then reduce the time slightly as the distance will + // be shorter meaning the animation would play for too long if not compensated for. + if (mScrollVelocity == 1) + timeDiff = endPos - startPos; + else if (mScrollVelocity == -1) + timeDiff = startPos - endPos; + + if (timeDiff != 1.0f) + animTime = glm::clamp(std::fabs(glm::mix(0.0f, 380.0f, timeDiff * 1.5f)), 200.0f, 380.0f); Animation* anim {new LambdaAnimation( [this, startPos, endPos, posMax](float t) { - t -= 1; - float f {glm::mix(startPos, endPos, t * t * t + 1)}; + // Non-linear interpolation. + t = 1.0f - (1.0f - t) * (1.0f - t); + float f {(endPos * t) + (startPos * (1.0f - t))}; if (f < 0) f += posMax; if (f >= posMax) @@ -1338,7 +1352,7 @@ template void CarouselComponent::onCursorChanged(const CursorSta mEntryCamOffset = f; }, - 500)}; + static_cast(animTime))}; GuiComponent::setAnimation(anim, 0, nullptr, false, 0); diff --git a/es-core/src/components/primary/TextListComponent.h b/es-core/src/components/primary/TextListComponent.h index abebebd24..c64f8c614 100644 --- a/es-core/src/components/primary/TextListComponent.h +++ b/es-core/src/components/primary/TextListComponent.h @@ -241,7 +241,9 @@ template bool TextListComponent::input(InputConfig* config, Inpu List::listInput(0); } else { - List::stopScrolling(); + if (isScrolling()) + onCursorChanged(CursorState::CURSOR_STOPPED); + List::listInput(0); } } } @@ -703,10 +705,25 @@ template void TextListComponent::onCursorChanged(const CursorSta float posMax {static_cast(mEntries.size())}; float endPos {static_cast(mCursor)}; + float animTime {380}; + float timeDiff {1.0f}; + + // If startPos is inbetween two positions then reduce the time slightly as the distance will + // be shorter meaning the animation would play for too long if not compensated for. + if (mScrollVelocity == 1) + timeDiff = endPos - startPos; + else if (mScrollVelocity == -1) + timeDiff = startPos - endPos; + + if (timeDiff != 1.0f) + animTime = + glm::clamp(std::fabs(glm::mix(0.0f, 380.0f, timeDiff * 1.5f)), 200.0f, 380.0f); + Animation* anim {new LambdaAnimation( [this, startPos, endPos, posMax](float t) { - t -= 1; - float f {glm::mix(startPos, endPos, t * t * t + 1)}; + // Non-linear interpolation. + t = 1.0f - (1.0f - t) * (1.0f - t); + float f {(endPos * t) + (startPos * (1.0f - t))}; if (f < 0) f += posMax; if (f >= posMax) @@ -714,7 +731,7 @@ template void TextListComponent::onCursorChanged(const CursorSta mCamOffset = f; }, - 500)}; + static_cast(animTime))}; GuiComponent::setAnimation(anim, 0, nullptr, false, 0); } From 48111ce5e4ec145d4589b1d6806de0b429f5feb5 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sat, 12 Nov 2022 14:08:53 +0100 Subject: [PATCH 009/101] Added basic GridComponent functionality and integration. --- es-app/src/views/GamelistBase.cpp | 49 +- es-app/src/views/GamelistBase.h | 2 + es-app/src/views/GamelistView.cpp | 65 ++- es-app/src/views/SystemView.cpp | 91 ++-- es-app/src/views/SystemView.h | 1 + es-core/src/ThemeData.cpp | 34 ++ es-core/src/components/IList.h | 6 + .../components/primary/CarouselComponent.h | 5 +- .../src/components/primary/GridComponent.h | 485 +++++++++++++++++- .../src/components/primary/PrimaryComponent.h | 1 + .../components/primary/TextListComponent.h | 4 +- 11 files changed, 675 insertions(+), 68 deletions(-) diff --git a/es-app/src/views/GamelistBase.cpp b/es-app/src/views/GamelistBase.cpp index b99e08bf2..d03e4bf99 100644 --- a/es-app/src/views/GamelistBase.cpp +++ b/es-app/src/views/GamelistBase.cpp @@ -568,15 +568,19 @@ void GamelistBase::populateList(const std::vector& files, FileData* f auto theme = mRoot->getSystem()->getTheme(); std::string name; - std::string carouselItemType; - std::string carouselDefaultItem; + std::string defaultItem; if (mCarousel != nullptr) { - carouselItemType = mCarousel->getItemType(); - carouselDefaultItem = mCarousel->getDefaultItem(); - if (!ResourceManager::getInstance().fileExists(carouselDefaultItem)) - carouselDefaultItem = ""; + defaultItem = mCarousel->getDefaultItem(); + if (!ResourceManager::getInstance().fileExists(defaultItem)) + defaultItem = ""; } + else if (mGrid != nullptr) { + defaultItem = mGrid->getDefaultItem(); + if (!ResourceManager::getInstance().fileExists(defaultItem)) + defaultItem = ""; + } + if (files.size() > 0) { for (auto it = files.cbegin(); it != files.cend(); ++it) { @@ -593,8 +597,6 @@ void GamelistBase::populateList(const std::vector& files, FileData* f } if (mCarousel != nullptr) { - assert(carouselItemType != ""); - CarouselComponent::Entry carouselEntry; carouselEntry.name = (*it)->getName(); carouselEntry.object = *it; @@ -606,13 +608,29 @@ void GamelistBase::populateList(const std::vector& files, FileData* f else if (letterCase == LetterCase::CAPITALIZED) carouselEntry.name = Utils::String::toCapitalized(carouselEntry.name); - if (carouselDefaultItem != "") - carouselEntry.data.defaultItemPath = carouselDefaultItem; + if (defaultItem != "") + carouselEntry.data.defaultItemPath = defaultItem; mCarousel->addEntry(carouselEntry, theme); } + else if (mGrid != nullptr) { + GridComponent::Entry gridEntry; + gridEntry.name = (*it)->getName(); + gridEntry.object = *it; - if (mTextList != nullptr) { + if (letterCase == LetterCase::UPPERCASE) + gridEntry.name = Utils::String::toUpper(gridEntry.name); + else if (letterCase == LetterCase::LOWERCASE) + gridEntry.name = Utils::String::toLower(gridEntry.name); + else if (letterCase == LetterCase::CAPITALIZED) + gridEntry.name = Utils::String::toCapitalized(gridEntry.name); + + if (defaultItem != "") + gridEntry.data.defaultItemPath = defaultItem; + + mGrid->addEntry(gridEntry, theme); + } + else if (mTextList != nullptr) { TextListComponent::Entry textListEntry; std::string indicators {mTextList->getIndicators()}; std::string collectionIndicators {mTextList->getCollectionIndicators()}; @@ -717,13 +735,20 @@ void GamelistBase::addPlaceholder(FileData* firstEntry) textListEntry.data.entryType = TextListEntryType::SECONDARY; mTextList->addEntry(textListEntry); } - if (mCarousel != nullptr) { + else if (mCarousel != nullptr) { CarouselComponent::Entry carouselEntry; carouselEntry.name = placeholder->getName(); letterCaseFunc(carouselEntry.name); carouselEntry.object = placeholder; mCarousel->addEntry(carouselEntry, mRoot->getSystem()->getTheme()); } + else if (mGrid != nullptr) { + GridComponent::Entry gridEntry; + gridEntry.name = placeholder->getName(); + letterCaseFunc(gridEntry.name); + gridEntry.object = placeholder; + mGrid->addEntry(gridEntry, mRoot->getSystem()->getTheme()); + } } void GamelistBase::generateFirstLetterIndex(const std::vector& files) diff --git a/es-app/src/views/GamelistBase.h b/es-app/src/views/GamelistBase.h index 046a2d478..59b188c29 100644 --- a/es-app/src/views/GamelistBase.h +++ b/es-app/src/views/GamelistBase.h @@ -23,6 +23,7 @@ #include "components/TextComponent.h" #include "components/VideoFFmpegComponent.h" #include "components/primary/CarouselComponent.h" +#include "components/primary/GridComponent.h" #include "components/primary/TextListComponent.h" #include @@ -90,6 +91,7 @@ protected: FileData* mRoot; std::unique_ptr> mCarousel; + std::unique_ptr> mGrid; std::unique_ptr> mTextList; PrimaryComponent* mPrimary; diff --git a/es-app/src/views/GamelistView.cpp b/es-app/src/views/GamelistView.cpp index 76a3f0582..a529cac68 100644 --- a/es-app/src/views/GamelistView.cpp +++ b/es-app/src/views/GamelistView.cpp @@ -114,15 +114,24 @@ void GamelistView::onThemeChanged(const std::shared_ptr& 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) { + if (element.second.type == "carousel" || element.second.type == "grid" || + element.second.type == "textlist") { + if (element.second.type == "carousel" && + (mGrid != nullptr || mTextList != nullptr)) { LOG(LogWarning) << "SystemView::populate(): Multiple primary components " - << "defined, skipping configuration entry"; + << "defined, skipping carousel configuration entry"; continue; } - if (element.second.type == "textlist" && mCarousel != nullptr) { + if (element.second.type == "grid" && + (mCarousel != nullptr || mTextList != nullptr)) { LOG(LogWarning) << "SystemView::populate(): Multiple primary components " - << "defined, skipping configuration entry"; + << "defined, skipping grid configuration entry"; + continue; + } + if (element.second.type == "textlist" && + (mCarousel != nullptr || mGrid != nullptr)) { + LOG(LogWarning) << "SystemView::populate(): Multiple primary components " + << "defined, skipping textlist configuration entry"; continue; } } @@ -154,10 +163,11 @@ void GamelistView::onThemeChanged(const std::shared_ptr& theme) mCarousel->setItemType(itemType); } else { - LOG(LogWarning) - << "GamelistView::onThemeChanged(): Invalid theme configuration, " - " property defined as \"" - << itemType << "\""; + LOG(LogWarning) << "GamelistView::onThemeChanged(): Invalid theme " + "configuration, carousel property \"itemType\" " + "for element \"" + << element.first.substr(9) << "\" defined as \"" + << itemType << "\""; mCarousel->setItemType("marquee"); } } @@ -174,6 +184,40 @@ void GamelistView::onThemeChanged(const std::shared_ptr& theme) mPrimary->applyTheme(theme, "gamelist", element.first, ALL); addChild(mPrimary); } + if (element.second.type == "grid") { + if (mGrid == nullptr) { + mGrid = std::make_unique>(); + if (element.second.has("itemType")) { + const std::string itemType {element.second.get("itemType")}; + if (itemType == "marquee" || itemType == "cover" || + itemType == "backcover" || itemType == "3dbox" || + itemType == "physicalmedia" || itemType == "screenshot" || + itemType == "titlescreen" || itemType == "miximage" || + itemType == "fanart" || itemType == "none") { + mGrid->setItemType(itemType); + } + else { + LOG(LogWarning) << "GamelistView::onThemeChanged(): Invalid theme " + "configuration, grid property \"itemType\" " + "for element \"" + << element.first.substr(5) << "\" defined as \"" + << itemType << "\""; + mGrid->setItemType("marquee"); + } + } + else { + mGrid->setItemType("marquee"); + } + if (element.second.has("defaultItem")) + mGrid->setDefaultItem(element.second.get("defaultItem")); + mPrimary = mGrid.get(); + } + mPrimary->setCursorChangedCallback( + [&](const CursorState& state) { updateView(state); }); + mPrimary->setDefaultZIndex(50.0f); + mPrimary->applyTheme(theme, "gamelist", element.first, ALL); + addChild(mPrimary); + } if (element.second.type == "image") { // If this is the startup system, then forceload the images to avoid texture pop-in. if (isStartupSystem) @@ -331,6 +375,9 @@ void GamelistView::onThemeChanged(const std::shared_ptr& theme) mCarousel->getType() == CarouselComponent::CarouselType::HORIZONTAL_WHEEL) mLeftRightAvailable = false; } + else if (mGrid != nullptr) { + mLeftRightAvailable = false; + } for (auto& video : mStaticVideoComponents) { if (video->hasStaticVideo()) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 8b6830d5e..850d45c0e 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -145,9 +145,6 @@ bool SystemView::input(InputConfig* config, Input input) void SystemView::update(int deltaTime) { - if (!mPrimary->isAnimationPlaying(0)) - mMaxFade = false; - mPrimary->update(deltaTime); for (auto& video : mSystemElements[mPrimary->getCursor()].videoComponents) { @@ -295,19 +292,24 @@ void SystemView::onCursorChanged(const CursorState& state) Animation* anim; + float animTime {380.0f}; + float timeDiff {1.0f}; + + // If startPos is inbetween two positions then reduce the time slightly as the distance will + // be shorter meaning the animation would play for too long if not compensated for. + if (scrollVelocity == 1) + timeDiff = endPos - startPos; + else if (scrollVelocity == -1) + timeDiff = startPos - endPos; + + if (timeDiff != 1.0f) + animTime = + glm::clamp(std::fabs(glm::mix(0.0f, animTime, timeDiff * 1.5f)), 200.0f, animTime); + if (transitionStyle == "fade") { float startFade {mFadeOpacity}; anim = new LambdaAnimation( [this, startFade, startPos, endPos, posMax](float t) { - t -= 1; - float f {glm::mix(startPos, endPos, t * t * t + 1.0f)}; - if (f < 0.0f) - f += posMax; - if (f >= posMax) - f -= posMax; - - t += 1; - if (t < 0.3f) mFadeOpacity = glm::mix(0.0f, 1.0f, glm::clamp(t / 0.2f + startFade, 0.0f, 1.0f)); @@ -319,7 +321,7 @@ void SystemView::onCursorChanged(const CursorState& state) if (t > 0.5f) mCamOffset = endPos; - if (t >= 0.7f && t != 1.0f) + if (mNavigated && t >= 0.7f && t != 1.0f) mMaxFade = true; // Update the game count when the entire animation has been completed. @@ -328,15 +330,16 @@ void SystemView::onCursorChanged(const CursorState& state) updateGameCount(); } }, - 500); + static_cast(animTime * 1.3f)); } else if (transitionStyle == "slide") { mUpdatedGameCount = false; anim = new LambdaAnimation( [this, startPos, endPos, posMax](float t) { - t -= 1; - float f {glm::mix(startPos, endPos, t * t * t + 1.0f)}; - if (f < 0.0f) + // Non-linear interpolation. + t = 1.0f - (1.0f - t) * (1.0f - t); + float f {(endPos * t) + (startPos * (1.0f - t))}; + if (f < 0) f += posMax; if (f >= posMax) f -= posMax; @@ -362,23 +365,13 @@ void SystemView::onCursorChanged(const CursorState& state) updateGameCount(); } }, - 500); + static_cast(animTime)); } else { // Instant. updateGameCount(); anim = new LambdaAnimation( - [this, startPos, endPos, posMax](float t) { - t -= 1; - float f {glm::mix(startPos, endPos, t * t * t + 1.0f)}; - if (f < 0.0f) - f += posMax; - if (f >= posMax) - f -= posMax; - - mCamOffset = endPos; - }, - 500); + [this, startPos, endPos, posMax](float t) { mCamOffset = endPos; }, animTime); } setAnimation(anim, 0, nullptr, false, 0); @@ -458,17 +451,27 @@ void SystemView::populate() ThemeFlags::ALL); elements.gameSelectors.back()->setNeedsRefresh(); } - if (element.second.type == "textlist" || element.second.type == "carousel") { - if (element.second.type == "carousel" && mTextList != nullptr) { + if (element.second.type == "carousel" || element.second.type == "grid" || + element.second.type == "textlist") { + if (element.second.type == "carousel" && + (mGrid != nullptr || mTextList != nullptr)) { LOG(LogWarning) << "SystemView::populate(): Multiple primary components " - << "defined, skipping configuration entry"; + << "defined, skipping carousel configuration entry"; continue; } - if (element.second.type == "textlist" && mCarousel != nullptr) { + if (element.second.type == "grid" && + (mCarousel != nullptr || mTextList != nullptr)) { LOG(LogWarning) << "SystemView::populate(): Multiple primary components " - << "defined, skipping configuration entry"; + << "defined, skipping grid configuration entry"; + continue; + } + if (element.second.type == "textlist" && + (mCarousel != nullptr || mGrid != nullptr)) { + LOG(LogWarning) + << "SystemView::populate(): Multiple primary components " + << "defined, skipping textlist configuration entry"; continue; } if (element.second.type == "carousel" && mCarousel == nullptr) { @@ -476,6 +479,11 @@ void SystemView::populate() mPrimary = mCarousel.get(); mPrimaryType = PrimaryType::CAROUSEL; } + else if (element.second.type == "grid" && mGrid == nullptr) { + mGrid = std::make_unique>(); + mPrimary = mGrid.get(); + mPrimaryType = PrimaryType::GRID; + } else if (element.second.type == "textlist" && mTextList == nullptr) { mTextList = std::make_unique>(); mPrimary = mTextList.get(); @@ -497,7 +505,7 @@ void SystemView::populate() anim->setPauseAnimation(true); } }); - if (mCarousel != nullptr) { + if (mCarousel != nullptr || mGrid != nullptr) { if (element.second.has("staticItem")) itemPath = element.second.get("staticItem"); if (element.second.has("defaultItem")) @@ -675,6 +683,15 @@ void SystemView::populate() entry.data.defaultItemPath = defaultItemPath; mCarousel->addEntry(entry, theme); } + else if (mGrid != nullptr) { + GridComponent::Entry entry; + entry.name = it->getFullName(); + letterCaseFunc(entry.name); + entry.object = it; + entry.data.itemPath = itemPath; + entry.data.defaultItemPath = defaultItemPath; + mGrid->addEntry(entry, theme); + } else if (mTextList != nullptr) { TextListComponent::Entry entry; entry.name = it->getFullName(); @@ -1268,6 +1285,10 @@ void SystemView::renderElements(const glm::mat4& parentTrans, bool abovePrimary) elementTrans, glm::round(glm::vec3 {0.0f, (i - mCamOffset) * mSize.y, 0.0f})); } + else if (mGrid != nullptr) { + elementTrans = glm::translate( + elementTrans, glm::round(glm::vec3 {0.0f, (i - mCamOffset) * mSize.y, 0.0f})); + } else if (mTextList != nullptr) { elementTrans = glm::translate( elementTrans, glm::round(glm::vec3 {0.0f, (i - mCamOffset) * mSize.y, 0.0f})); diff --git a/es-app/src/views/SystemView.h b/es-app/src/views/SystemView.h index 26a15442d..6040fca27 100644 --- a/es-app/src/views/SystemView.h +++ b/es-app/src/views/SystemView.h @@ -126,6 +126,7 @@ private: Renderer* mRenderer; std::unique_ptr> mCarousel; + std::unique_ptr> mGrid; std::unique_ptr> mTextList; std::unique_ptr mLegacySystemInfo; std::vector mSystemElements; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 26aae7104..06f54ca12 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -322,6 +322,40 @@ std::map> {"fadeAbovePrimary", BOOLEAN}, {"zIndex", FLOAT}, {"legacyZIndexMode", STRING}}}, // For backward compatibility with legacy themes. + {"grid", + {{"pos", NORMALIZED_PAIR}, + {"size", NORMALIZED_PAIR}, + {"origin", NORMALIZED_PAIR}, + {"columns", UNSIGNED_INTEGER}, + {"staticItem", PATH}, + {"itemType", STRING}, + {"defaultItem", PATH}, + {"itemSize", NORMALIZED_PAIR}, + {"itemScale", FLOAT}, + {"itemSpacing", NORMALIZED_PAIR}, + {"itemTransitions", STRING}, + {"itemHorizontalAlignment", STRING}, + {"itemVerticalAlignment", STRING}, + {"horizontalMargins", NORMALIZED_PAIR}, + {"verticalMargins", NORMALIZED_PAIR}, + {"horizontalOffset", FLOAT}, + {"verticalOffset", FLOAT}, + {"unfocusedItemOpacity", FLOAT}, + {"edgeScaleInwards", BOOLEAN}, + {"color", COLOR}, + {"colorEnd", COLOR}, + {"gradientType", STRING}, + {"text", STRING}, + {"textColor", COLOR}, + {"textBackgroundColor", COLOR}, + {"fontPath", PATH}, + {"fontSize", FLOAT}, + {"letterCase", STRING}, + {"letterCaseCollections", STRING}, + {"letterCaseGroupedCollections", STRING}, + {"lineSpacing", FLOAT}, + {"fadeAbovePrimary", BOOLEAN}, + {"zIndex", FLOAT}}}, {"textlist", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, diff --git a/es-core/src/components/IList.h b/es-core/src/components/IList.h index e8300ee6d..888a66042 100644 --- a/es-core/src/components/IList.h +++ b/es-core/src/components/IList.h @@ -73,6 +73,7 @@ protected: const ScrollTierList& mTierList; const ListLoopType mLoopType; int mCursor; + int mLastCursor; int mScrollTier; int mScrollVelocity; int mScrollTierAccumulator; @@ -88,6 +89,7 @@ public: , mTierList {tierList} , mLoopType {loopType} , mCursor {0} + , mLastCursor {0} , mScrollTier {0} , mScrollVelocity {0} , mScrollTierAccumulator {0} @@ -213,6 +215,7 @@ protected: bool listFirstRow() { + mLastCursor = mCursor; mCursor = 0; onCursorChanged(CursorState::CURSOR_STOPPED); onScroll(); @@ -221,6 +224,7 @@ protected: bool listLastRow() { + mLastCursor = mCursor; mCursor = static_cast(mEntries.size()) - 1; onCursorChanged(CursorState::CURSOR_STOPPED); onScroll(); @@ -323,6 +327,8 @@ protected: if (mScrollVelocity == 0 || size() < 2) return; + mLastCursor = mCursor; + int cursor {mCursor + amt}; int absAmt {amt < 0 ? -amt : amt}; diff --git a/es-core/src/components/primary/CarouselComponent.h b/es-core/src/components/primary/CarouselComponent.h index ba0b13817..373649929 100644 --- a/es-core/src/components/primary/CarouselComponent.h +++ b/es-core/src/components/primary/CarouselComponent.h @@ -1327,7 +1327,7 @@ template void CarouselComponent::onCursorChanged(const CursorSta mPositiveDirection = false; mEntryCamTarget = endPos; - float animTime {380}; + float animTime {380.0f}; float timeDiff {1.0f}; // If startPos is inbetween two positions then reduce the time slightly as the distance will @@ -1338,7 +1338,8 @@ template void CarouselComponent::onCursorChanged(const CursorSta timeDiff = startPos - endPos; if (timeDiff != 1.0f) - animTime = glm::clamp(std::fabs(glm::mix(0.0f, 380.0f, timeDiff * 1.5f)), 200.0f, 380.0f); + animTime = + glm::clamp(std::fabs(glm::mix(0.0f, animTime, timeDiff * 1.5f)), 200.0f, animTime); Animation* anim {new LambdaAnimation( [this, startPos, endPos, posMax](float t) { diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index 770a86d9a..972188f53 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -6,8 +6,8 @@ // Grid, usable in both the system and gamelist views. // -#ifndef ES_CORE_COMPONENTS_GRID_COMPONENT_H -#define ES_CORE_COMPONENTS_GRID_COMPONENT_H +#ifndef ES_CORE_COMPONENTS_PRIMARY_GRID_COMPONENT_H +#define ES_CORE_COMPONENTS_PRIMARY_GRID_COMPONENT_H #include "components/IList.h" #include "components/primary/PrimaryComponent.h" @@ -26,10 +26,19 @@ class GridComponent : public PrimaryComponent, protected IList protected: using List::mCursor; using List::mEntries; + using List::mLastCursor; + using List::mScrollVelocity; + using List::mSize; public: + using Entry = typename IList::Entry; + GridComponent(); + void addEntry(Entry& entry, const std::shared_ptr& theme); + void updateEntry(Entry& entry, const std::shared_ptr& theme); + void onDemandTextureLoad() override; + void setCancelTransitionsCallback(const std::function& func) override { mCancelTransitionsCallback = func; @@ -42,29 +51,489 @@ public: const size_t getNumEntries() override { return mEntries.size(); } const bool getFadeAbovePrimary() const override { return mFadeAbovePrimary; } const LetterCase getLetterCase() const override { return mLetterCase; } - virtual const LetterCase getLetterCaseCollections() const = 0; - virtual const LetterCase getLetterCaseGroupedCollections() const = 0; + const LetterCase getLetterCaseCollections() const override { return mLetterCaseCollections; } + const LetterCase getLetterCaseGroupedCollections() const override + { + return mLetterCaseGroupedCollections; + } + const std::string& getItemType() { return mItemType; } + void setItemType(std::string itemType) { mItemType = itemType; } + const std::string& getDefaultItem() { return mDefaultItem; } + void setDefaultItem(std::string defaultItem) { mDefaultItem = defaultItem; } + 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& theme, + const std::string& view, + const std::string& element, + unsigned int properties) override; private: + void calculateLayout(); + + void onCursorChanged(const CursorState& state) override; + 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(); } + Renderer* mRenderer; std::function mCancelTransitionsCallback; std::function mCursorChangedCallback; - bool mFadeAbovePrimary; + std::string mItemType; + std::string mDefaultItem; + float mEntryOffset; + float mEntryCamTarget; + float mTransitionFactor; + std::shared_ptr mFont; + + unsigned int mColumns; + glm::vec2 mItemSize; + float mItemScale; + glm::vec2 mItemSpacing; + bool mInstantItemTransitions; + float mUnfocusedItemOpacity; + unsigned int mTextColor; + unsigned int mTextBackgroundColor; LetterCase mLetterCase; LetterCase mLetterCaseCollections; LetterCase mLetterCaseGroupedCollections; + float mLineSpacing; + bool mFadeAbovePrimary; + int mPreviousScrollVelocity; + bool mPositiveDirection; + bool mGamelistView; + bool mLayoutValid; + bool mRowJump; }; template GridComponent::GridComponent() - : IList {} + : IList {LIST_SCROLL_STYLE_SLOW, ListLoopType::LIST_PAUSE_AT_END} , mRenderer {Renderer::getInstance()} - , mFadeAbovePrimary {false} + , mEntryOffset {0.0f} + , mEntryCamTarget {0.0f} + , mTransitionFactor {1.0f} + , mFont {Font::get(FONT_SIZE_LARGE)} + , mColumns {5} + , mItemSize {glm::vec2 {Renderer::getScreenWidth() * 0.15f, + Renderer::getScreenHeight() * 0.25f}} + , mItemScale {1.2f} + , mItemSpacing {glm::vec2 {Renderer::getScreenWidth() * 0.02f, + Renderer::getScreenHeight() * 0.02f}} + , mInstantItemTransitions {false} + , mUnfocusedItemOpacity {1.0f} + , mTextColor {0x000000FF} + , mTextBackgroundColor {0xFFFFFF00} , mLetterCase {LetterCase::NONE} , mLetterCaseCollections {LetterCase::NONE} , mLetterCaseGroupedCollections {LetterCase::NONE} + , mLineSpacing {1.5f} + , mFadeAbovePrimary {false} + , mPreviousScrollVelocity {0} + , mPositiveDirection {false} + , mGamelistView {std::is_same_v ? true : false} + , mLayoutValid {false} + , mRowJump {false} { } -#endif // ES_CORE_COMPONENTS_GRID_COMPONENT_H +template +void GridComponent::addEntry(Entry& entry, const std::shared_ptr& theme) +{ + bool dynamic {true}; + + if (!mGamelistView) + dynamic = false; + + if (entry.data.itemPath != "" && + ResourceManager::getInstance().fileExists(entry.data.itemPath)) { + auto item = std::make_shared(false, dynamic); + item->setLinearInterpolation(true); + item->setMipmapping(true); + item->setMaxSize(mItemSize); + item->setImage(entry.data.itemPath); + item->applyTheme(theme, "system", "", ThemeFlags::ALL); + item->setOrigin(0.5f, 0.5f); + item->setRotateByTargetSize(true); + entry.data.item = item; + } + else if (entry.data.defaultItemPath != "" && + ResourceManager::getInstance().fileExists(entry.data.defaultItemPath)) { + auto defaultItem = std::make_shared(false, dynamic); + defaultItem->setLinearInterpolation(true); + defaultItem->setMipmapping(true); + defaultItem->setMaxSize(mItemSize); + defaultItem->setImage(entry.data.defaultItemPath); + defaultItem->applyTheme(theme, "system", "", ThemeFlags::ALL); + defaultItem->setOrigin(0.5f, 0.5f); + defaultItem->setRotateByTargetSize(true); + entry.data.item = defaultItem; + } + + if (!entry.data.item) { + // If no item image is present, add item text as fallback. + auto text = std::make_shared( + entry.name, mFont, 0x000000FF, Alignment::ALIGN_CENTER, Alignment::ALIGN_CENTER, + glm::vec3 {0.0f, 0.0f, 0.0f}, mItemSize, 0x00000000); + text->setOrigin(0.5f, 0.5f); + text->setLineSpacing(mLineSpacing); + if (!mGamelistView) + text->setValue(entry.name); + text->setColor(mTextColor); + text->setBackgroundColor(mTextBackgroundColor); + text->setRenderBackground(true); + + entry.data.item = text; + } + + List::add(entry); +} + +template +void GridComponent::updateEntry(Entry& entry, const std::shared_ptr& theme) +{ + if (entry.data.itemPath != "") { + auto item = std::make_shared(false, true); + item->setLinearInterpolation(true); + item->setMipmapping(true); + item->setMaxSize(mItemSize); + item->setImage(entry.data.itemPath); + item->applyTheme(theme, "system", "", ThemeFlags::ALL); + item->setOrigin(0.5f, 0.5f); + item->setRotateByTargetSize(true); + entry.data.item = item; + } + else { + return; + } +} + +template void GridComponent::onDemandTextureLoad() +{ + if constexpr (std::is_same_v) { + const int numEntries {static_cast(mEntries.size())}; + + // TODO: Currently loads every item every time. + for (int i {0}; i < size(); ++i) { + int cursor {i}; + + while (cursor < 0) + cursor += numEntries; + while (cursor >= numEntries) + cursor -= numEntries; + + auto& entry = mEntries.at(cursor); + + if (entry.data.itemPath == "") { + FileData* game {entry.object}; + + if (mItemType == "" || mItemType == "marquee") + entry.data.itemPath = game->getMarqueePath(); + else if (mItemType == "cover") + entry.data.itemPath = game->getCoverPath(); + else if (mItemType == "backcover") + entry.data.itemPath = game->getBackCoverPath(); + else if (mItemType == "3dbox") + entry.data.itemPath = game->get3DBoxPath(); + else if (mItemType == "physicalmedia") + entry.data.itemPath = game->getPhysicalMediaPath(); + else if (mItemType == "screenshot") + entry.data.itemPath = game->getScreenshotPath(); + else if (mItemType == "titlescreen") + entry.data.itemPath = game->getTitleScreenPath(); + else if (mItemType == "miximage") + entry.data.itemPath = game->getMiximagePath(); + else if (mItemType == "fanart") + entry.data.itemPath = game->getFanArtPath(); + else if (mItemType == "none") // Display the game name as text. + return; + + auto theme = game->getSystem()->getTheme(); + updateEntry(entry, theme); + } + } + } +} + +template bool GridComponent::input(InputConfig* config, Input input) +{ + if (size() > 0) { + if (input.value != 0) { + mRowJump = false; + + if (config->isMappedLike("left", input)) { + if (mCancelTransitionsCallback) + mCancelTransitionsCallback(); + List::listInput(-1); + return true; + } + if (config->isMappedLike("right", input)) { + if (mCancelTransitionsCallback) + mCancelTransitionsCallback(); + List::listInput(1); + return true; + } + if (config->isMappedLike("up", input)) { + if (mCancelTransitionsCallback) + mCancelTransitionsCallback(); + mRowJump = true; + List::listInput(-mColumns); + return true; + } + if (config->isMappedLike("down", input)) { + if (mCancelTransitionsCallback) + mCancelTransitionsCallback(); + mRowJump = true; + List::listInput(mColumns); + return true; + } + if (config->isMappedLike("lefttrigger", input)) { + if (getCursor() == 0) + return true; + if (mCancelTransitionsCallback) + mCancelTransitionsCallback(); + return this->listFirstRow(); + } + if (config->isMappedLike("righttrigger", input)) { + if (getCursor() == static_cast(mEntries.size()) - 1) + return 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) || + config->isMappedLike("lefttrigger", input) || + config->isMappedLike("righttrigger", input)) { + if constexpr (std::is_same_v) { + if (isScrolling()) + onCursorChanged(CursorState::CURSOR_STOPPED); + List::listInput(0); + } + else { + if (isScrolling()) + onCursorChanged(CursorState::CURSOR_STOPPED); + List::listInput(0); + } + } + } + } + + return GuiComponent::input(config, input); +} + +template void GridComponent::update(int deltaTime) +{ + if (!mLayoutValid) + calculateLayout(); + + List::listUpdate(deltaTime); + GuiComponent::update(deltaTime); +} + +template void GridComponent::render(const glm::mat4& parentTrans) +{ + int numEntries {static_cast(mEntries.size())}; + if (numEntries == 0) + return; + + glm::mat4 trans {parentTrans * List::getTransform()}; + mRenderer->setMatrix(trans); + + // In image debug mode, draw a green rectangle covering the entire grid area. + if (Settings::getInstance()->getBool("DebugImage")) + mRenderer->drawRect(0.0f, 0.0f, mSize.x, mSize.y, 0x00FF0033, 0x00FF0033); + + for (size_t i {0}; i < mEntries.size(); ++i) { + float opacity {mUnfocusedItemOpacity}; + float scale {1.0f}; + + if (i == static_cast(mCursor)) { + scale = glm::mix(1.0f, mItemScale, mTransitionFactor); + opacity = 1.0f - glm::mix(mUnfocusedItemOpacity, 0.0f, mTransitionFactor); + } + else if (i == static_cast(mLastCursor)) { + scale = glm::mix(mItemScale, 1.0f, mTransitionFactor); + opacity = glm::mix(1.0f, mUnfocusedItemOpacity, mTransitionFactor); + } + + mEntries.at(i).data.item->setScale(scale); + mEntries.at(i).data.item->setOpacity(opacity); + mEntries.at(i).data.item->render(trans); + mEntries.at(i).data.item->setScale(1.0f); + mEntries.at(i).data.item->setOpacity(1.0f); + } + + GuiComponent::renderChildren(trans); +} + +template +void GridComponent::applyTheme(const std::shared_ptr& theme, + const std::string& view, + const std::string& element, + unsigned int properties) +{ + GuiComponent::applyTheme(theme, view, element, properties); + using namespace ThemeFlags; + const ThemeData::ThemeElement* elem {theme->getElement(view, element, "grid")}; + + if (!elem) + return; + + if (elem->has("columns")) + mColumns = glm::clamp(elem->get("columns"), 0u, 100u); + + if (elem->has("itemSize")) { + const glm::vec2 itemSize {glm::clamp(elem->get("itemSize"), 0.05f, 1.0f)}; + mItemSize = itemSize * glm::vec2(Renderer::getScreenWidth(), Renderer::getScreenHeight()); + } + + if (elem->has("itemScale")) + mItemScale = glm::clamp(elem->get("itemScale"), 0.5f, 2.0f); + + if (elem->has("itemTransitions")) { + const std::string& itemTransitions {elem->get("itemTransitions")}; + if (itemTransitions == "scale") { + mInstantItemTransitions = false; + } + else if (itemTransitions == "instant") { + mInstantItemTransitions = true; + } + else { + mInstantItemTransitions = false; + LOG(LogWarning) << "GridComponent: Invalid theme configuration, property " + "\"itemTransitions\" for element \"" + << element.substr(5) << "\" defined as \"" << itemTransitions << "\""; + } + } + + if (elem->has("itemSpacing")) { + const glm::vec2 itemSpacing {glm::clamp(elem->get("itemSpacing"), 0.0f, 0.1f)}; + mItemSpacing = + itemSpacing * glm::vec2(Renderer::getScreenWidth(), Renderer::getScreenHeight()); + } + + if (elem->has("unfocusedItemOpacity")) + mUnfocusedItemOpacity = glm::clamp(elem->get("unfocusedItemOpacity"), 0.1f, 1.0f); +} + +template void GridComponent::onCursorChanged(const CursorState& state) +{ + float startPos {mEntryOffset}; + float posMax {static_cast(mEntries.size())}; + float target {static_cast(mCursor)}; + + // Find the shortest path to the target. + float endPos {target}; // Directly. + + if (mPreviousScrollVelocity > 0 && mScrollVelocity == 0 && mEntryOffset > posMax - 1.0f) + startPos = 0.0f; + + float dist {std::fabs(endPos - startPos)}; + + if (std::fabs(target + posMax - startPos - mScrollVelocity) < dist) + endPos = target + posMax; // Loop around the end (0 -> max). + if (std::fabs(target - posMax - startPos - mScrollVelocity) < dist) + endPos = target - posMax; // Loop around the start (max - 1 -> -1). + + // Make sure there are no reverse jumps between items. + bool changedDirection {false}; + if (mPreviousScrollVelocity != 0 && mPreviousScrollVelocity != mScrollVelocity) + changedDirection = true; + + if (!changedDirection && mScrollVelocity > 0 && endPos < startPos) + endPos = endPos + posMax; + + if (!changedDirection && mScrollVelocity < 0 && endPos > startPos) + endPos = endPos - posMax; + + if (mScrollVelocity != 0) + mPreviousScrollVelocity = mScrollVelocity; + + // Needed to make sure that overlapping items are renderered correctly. + if (startPos > endPos) + mPositiveDirection = true; + else + mPositiveDirection = false; + + mEntryCamTarget = endPos; + float animTime {250.0f}; + float timeDiff {1.0f}; + + // If startPos is inbetween two positions then reduce the time slightly as the distance will + // be shorter meaning the animation would play for too long if not compensated for. + if (mScrollVelocity == 1) + timeDiff = endPos - startPos; + else if (mScrollVelocity == -1) + timeDiff = startPos - endPos; + + if (timeDiff != 1.0f) + animTime = + glm::clamp(std::fabs(glm::mix(0.0f, animTime, timeDiff * 1.5f)), 180.0f, animTime); + + Animation* anim {new LambdaAnimation( + [this, startPos, endPos, posMax](float t) { + // Non-linear interpolation. + t = 1.0f - (1.0f - t) * (1.0f - t); + float f {(endPos * t) + (startPos * (1.0f - t))}; + if (f < 0) + f += posMax; + if (f >= posMax) + f -= posMax; + + mEntryOffset = f; + + if (mInstantItemTransitions) { + mTransitionFactor = 1.0f; + } + else { + // Linear interpolation. + mTransitionFactor = t; + // Non-linear interpolation doesn't seem to be a good match for this component. + // mTransitionFactor = {(1.0f * t) + (0.0f * (1.0f - t))}; + } + }, + static_cast(animTime))}; + + GuiComponent::setAnimation(anim, 0, nullptr, false, 0); + + if (mCursorChangedCallback) + mCursorChangedCallback(state); +} + +template void GridComponent::calculateLayout() +{ + assert(!mEntries.empty()); + + unsigned int columnCount {0}; + unsigned int rowCount {0}; + + for (auto& entry : mEntries) { + entry.data.item->setPosition(glm::vec3 { + (mItemSize.x * columnCount) + (mItemSize.x * 0.5f) + mItemSpacing.x * columnCount, + (mItemSize.y * rowCount) + (mItemSize.y * 0.5f) + mItemSpacing.y * rowCount, 0.0f}); + if (columnCount == mColumns - 1) { + ++rowCount; + columnCount = 0; + continue; + } + + ++columnCount; + } + + mLayoutValid = true; +} + +#endif // ES_CORE_COMPONENTS_PRIMARY_GRID_COMPONENT_H diff --git a/es-core/src/components/primary/PrimaryComponent.h b/es-core/src/components/primary/PrimaryComponent.h index 03ad7c7a9..7d28a0c92 100644 --- a/es-core/src/components/primary/PrimaryComponent.h +++ b/es-core/src/components/primary/PrimaryComponent.h @@ -14,6 +14,7 @@ template class PrimaryComponent : public virtual GuiComponent public: enum class PrimaryType { CAROUSEL, + GRID, TEXTLIST }; diff --git a/es-core/src/components/primary/TextListComponent.h b/es-core/src/components/primary/TextListComponent.h index c64f8c614..cadfc08e7 100644 --- a/es-core/src/components/primary/TextListComponent.h +++ b/es-core/src/components/primary/TextListComponent.h @@ -705,7 +705,7 @@ template void TextListComponent::onCursorChanged(const CursorSta float posMax {static_cast(mEntries.size())}; float endPos {static_cast(mCursor)}; - float animTime {380}; + float animTime {380.0f}; float timeDiff {1.0f}; // If startPos is inbetween two positions then reduce the time slightly as the distance will @@ -717,7 +717,7 @@ template void TextListComponent::onCursorChanged(const CursorSta if (timeDiff != 1.0f) animTime = - glm::clamp(std::fabs(glm::mix(0.0f, 380.0f, timeDiff * 1.5f)), 200.0f, 380.0f); + glm::clamp(std::fabs(glm::mix(0.0f, animTime, timeDiff * 1.5f)), 200.0f, animTime); Animation* anim {new LambdaAnimation( [this, startPos, endPos, posMax](float t) { From ed6b23a2c6784e55c754b4dcbd0565ed5d0deb20 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sun, 13 Nov 2022 11:56:18 +0100 Subject: [PATCH 010/101] Fixed an issue where the default textlist size and position properties were not set for the system view. Also moved some default value assignments to the TextListComponent itself. --- es-app/src/views/GamelistView.cpp | 6 ------ es-core/src/components/primary/CarouselComponent.h | 6 +++--- es-core/src/components/primary/TextListComponent.h | 7 +++++++ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/es-app/src/views/GamelistView.cpp b/es-app/src/views/GamelistView.cpp index a529cac68..97cbf24b5 100644 --- a/es-app/src/views/GamelistView.cpp +++ b/es-app/src/views/GamelistView.cpp @@ -140,9 +140,6 @@ void GamelistView::onThemeChanged(const std::shared_ptr& theme) mTextList = std::make_unique>(); mPrimary = mTextList.get(); } - mPrimary->setPosition(0.0f, mSize.y * 0.1f); - mPrimary->setSize(mSize.x, mSize.y * 0.8f); - mPrimary->setAlignment(TextListComponent::PrimaryAlignment::ALIGN_LEFT); mPrimary->setCursorChangedCallback( [&](const CursorState& state) { updateView(state); }); mPrimary->setDefaultZIndex(50.0f); @@ -357,9 +354,6 @@ void GamelistView::onThemeChanged(const std::shared_ptr& theme) if (mPrimary == nullptr) { mTextList = std::make_unique>(); mPrimary = mTextList.get(); - mPrimary->setPosition(0.0f, mSize.y * 0.1f); - mPrimary->setSize(mSize.x, mSize.y * 0.8f); - mPrimary->setAlignment(TextListComponent::PrimaryAlignment::ALIGN_LEFT); mPrimary->setCursorChangedCallback([&](const CursorState& state) { updateView(state); }); mPrimary->setDefaultZIndex(50.0f); mPrimary->setZIndex(50.0f); diff --git a/es-core/src/components/primary/CarouselComponent.h b/es-core/src/components/primary/CarouselComponent.h index 373649929..128f1cfb4 100644 --- a/es-core/src/components/primary/CarouselComponent.h +++ b/es-core/src/components/primary/CarouselComponent.h @@ -914,9 +914,6 @@ void CarouselComponent::applyTheme(const std::shared_ptr& theme, const std::string& element, unsigned int properties) { - using namespace ThemeFlags; - const ThemeData::ThemeElement* elem {theme->getElement(view, element, "carousel")}; - mSize.x = Renderer::getScreenWidth(); mSize.y = Renderer::getScreenHeight() * 0.23240f; GuiComponent::mPosition.x = 0.0f; @@ -925,6 +922,9 @@ void CarouselComponent::applyTheme(const std::shared_ptr& theme, mCarouselColorEnd = 0xFFFFFFD8; mZIndex = mDefaultZIndex; + using namespace ThemeFlags; + const ThemeData::ThemeElement* elem {theme->getElement(view, element, "carousel")}; + if (!elem) return; diff --git a/es-core/src/components/primary/TextListComponent.h b/es-core/src/components/primary/TextListComponent.h index cadfc08e7..34b25f1ca 100644 --- a/es-core/src/components/primary/TextListComponent.h +++ b/es-core/src/components/primary/TextListComponent.h @@ -482,7 +482,14 @@ void TextListComponent::applyTheme(const std::shared_ptr& theme, const std::string& element, unsigned int properties) { + mSize.x = Renderer::getScreenWidth(); + mSize.y = Renderer::getScreenHeight() * 0.8f; + GuiComponent::mPosition.x = 0.0f; + GuiComponent::mPosition.y = Renderer::getScreenHeight() * 0.1; + setAlignment(PrimaryAlignment::ALIGN_LEFT); + GuiComponent::applyTheme(theme, view, element, properties); + using namespace ThemeFlags; const ThemeData::ThemeElement* elem {theme->getElement(view, element, "textlist")}; From d14983d4aea4be80621ebd8c9420e864e34629e9 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sun, 13 Nov 2022 12:09:18 +0100 Subject: [PATCH 011/101] Documentation update. --- CHANGELOG.md | 1 + THEMES-DEV.md | 1415 +++++++++++++++++++++++++------------------------ 2 files changed, 729 insertions(+), 687 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91d6740eb..291363796 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -169,6 +169,7 @@ * Moved all Platform functions to the utility namespace instead of using the global namespace * Implemented proper XML attribute support in ThemeData that eliminates the risk of name collisions * Added size restrictions to images and fonts so incorrect theme configuration would not lead to crashes or excessive memory utilization +* Made animations on the carousel better looking by using a new non-linear interpolation method * Migrated the carousel code from SystemView to a separate new CarouselComponent * Changed the carousel properties to be more generic by renaming "logo" to "item", e.g. itemSize, maxItemCount etc. * Added the properties "itemsBeforeCenter" and "itemsAfterCenter" to define entries for carousels of the wheel type diff --git a/THEMES-DEV.md b/THEMES-DEV.md index 3e4ae02c4..6d0cff341 100644 --- a/THEMES-DEV.md +++ b/THEMES-DEV.md @@ -824,6 +824,7 @@ These are the default zIndex values per element type: | gamelistinfo | 45 | | rating | 45 | | carousel | 50 | +| grid | 50 | | textlist | 50 | The `helpsystem` element does not really have a zIndex value and is always rendered on top of all other elements. @@ -977,697 +978,28 @@ For any given step, the configuration is parsed in the exact order that it's def ## Property data types -* NORMALIZED_PAIR - two decimals, in the range [0..1], delimited by a space. For example, `0.25 0.5`. Most commonly used for position (x and y coordinates) and size (width and height). -* NORMALIZED_RECT - four decimals, in the range [0..1], delimited by a space. For example, `0.25 0.5 0.10 0.30`. Most commonly used for padding to store top, left, bottom and right coordinates. -* PATH - a path. If the first character is a `~`, it will be expanded into the environment variable for the home path (`$HOME` for Unix and macOS or `%HOMEPATH%` for Windows) unless overridden using the --home command line option. f the first character is a `.`, it will be expanded to the theme file's directory, allowing you to specify resources relative to the theme file, like so: `./../core/fonts/myfont.ttf`. -* BOOLEAN - `true`/`1` or `false`/`0`. -* COLOR - a hexadecimal RGB or RGBA color (6 or 8 digits). If 6 digits, will assume the alpha channel is `FF` (completely opaque). -* UNSIGNED_INTEGER - an unsigned integer. -* FLOAT - a decimal. -* STRING - a string of text. +* NORMALIZED_PAIR - two decimal values delimited by a space, for example `0.25 0.5` +* NORMALIZED_RECT - four decimal values delimited by a space, for example `0.25 0.5 0.10 0.30` +* PATH - path to a resource. If the first character is a tilde (`~`) then it will be expanded to the user's home directory (`$HOME` for Unix and macOS and `%HOMEPATH%` for Windows) unless overridden using the --home command line option. If the first character is a dot (`.`) then the resource will be searched for relative to the location of the theme file, for example `./myfont.ttf` or `./../core/fonts/myfont.ttf` +* BOOLEAN - `true`/`1` or `false`/`0` +* COLOR - a hexadecimal RGB or RGBA color value consisting of 6 or 8 digits. If a 6 digit value is used then the alpha channel will be set to `FF` (completely opaque) +* UNSIGNED_INTEGER - an unsigned integer value +* FLOAT - a decimal value +* STRING - a string of text ## Element types and their properties -Common to almost all elements is a `pos` and `size` property of the NORMALIZED_PAIR type. They are normalized in terms of their "parent" object's size; 99% of the time this is just the size of the screen. In this case, `0 0` would correspond to the top left corner, and `1 1` the bottom right corner (a positive Y value points further down). You can also use numbers outside of the [0..1] range if you want to place an element partially off-screen. +There are three groups of elements available for use which are named _primary_, _secondary_ and _special_. They are all covered in detail below. -The order in which you define properties does not matter and you only need to define the ones where you want to override the default values. +Common to almost all elements are `pos` and `size` properties of the NORMALIZED_PAIR type. They are normalized in terms of their parent's size. Most of the time this is just the size of the screen. In this case, `0 0` would correspond to the top left corner, and `1 1` the bottom right corner (a positive Y value points further down). You can also use numbers outside the 0 to 1 range if you want to place an element partially off-screen. -### image +The order in which you define properties for a given element does not matter and you only need to define a property if you want to override its default value. If a property is defined multiple times then the latest entry will override any previous occurances. -Displays a raster image or a scalable vector graphics (SVG) image. +### Primary elements -Supported views: -* `system ` -* `gamelist` +Elements from this group can only occur once per view (for a certain variant) and they govern basic behavior and functionality like controller input and navigation. -Instances per view: -* `unlimited` - -Properties: -* `pos` - type: NORMALIZED_PAIR -* `size` - type: NORMALIZED_PAIR - - If only one axis is specified (and the other is zero), then the other axis will be automatically calculated in accordance with the image's aspect ratio. Setting both axes to 0 is an error and the size will be clamped to `0.001 0.001` in this case. - - Minimum value per axis is `0.001` and maximum value per axis is `3`. If specifying a value outside the allowed range then no attempt will be made to preserve the aspect ratio. -* `maxSize` - type: NORMALIZED_PAIR - - The image will be resized as large as possible so that it fits within this size while maintaining its aspect ratio. Use this instead of `size` when you don't know what kind of image you're using so it doesn't get grossly oversized on one axis (e.g. with a game's image metadata). Although this property is possible to combine with the `tile` property that does not make a whole lot of sense, instead use the `size` property for tiled images. - - Minimum value per axis is `0.001` and maximum value per axis is `3` -* `origin` - type: NORMALIZED_PAIR - - Where on the element `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the element exactly in the middle of the screen. If the position and size attributes are themeable, origin is implied. - - Minimum value per axis is `0` and maximum value per axis is `1` - - Default is `0 0` -* `rotation` - type: FLOAT - - Angle in degrees that the image should be rotated. Positive values will rotate clockwise, negative values will rotate counterclockwise. - - Default is `0` -* `rotationOrigin` - type: NORMALIZED_PAIR - - Point around which the image will be rotated. - - Minimum value per axis is `0` and maximum value per axis is `1` - - Default is `0.5 0.5` -* `path` - type: PATH - - Explicit path to an image file. Most common extensions are supported (including .jpg, .png, and unanimated .gif). If `imageType` is also defined then this will take precedence as these two properties are not intended to be used together. If you need a fallback image in case of missing game media, use the `default` property instead. -* `default` - type: PATH - - Path to a default image file. The default image will be displayed when the selected game does not have an image of the type defined by the `imageType` property (i.e. this `default` property does nothing unless a valid `imageType` property has been set). It's also applied to any custom collection that does not contain any games when browsing the grouped custom collections system. -* `imageType` - type: STRING - - This displays a game image of a certain media type. Multiple types can be defined, in which case the entries should be delimited by commas or by whitespace characters (tabs, spaces or line breaks). The media will be searched for in the order that the entries have been defined. If no image is found, then the space will be left blank unless the `default` property has been set. To use this property from the `system` view, you will first need to add a `gameselector` element. Defining duplicate values is considered an error and will result in the property getting ignored. - - Valid values: - - `image` - This will look for a `miximage`, and if that is not found `screenshot` is tried next, then `titlescreen` and finally `cover`. This is just a convenient shortcut and it's equivalent to explicitly defining `miximage, screenshot, titlescreen, cover` - - `miximage` - This will look for a miximage. - - `marquee` - This will look for a marquee (wheel) image. - - `screenshot` - This will look for a screenshot image. - - `titlescreen` - This will look for a title screen image. - - `cover` - This will look for a box front cover image. - - `backcover` - This will look for a box back cover image. - - `3dbox` - This will look for a 3D box image. - - `physicalmedia` - This will look for a physical media image. - - `fanart` - This will look for a fan art image. -* `metadataElement` - type: BOOLEAN - - By default game metadata and media are faded out during gamelist fast-scrolling and text metadata fields, ratings and badges are hidden when enabling the _Hide metadata fields_ setting for a game entry. Using this property it's possible to explicitly define additional image elements that should be treated as if they were game media files. This is for example useful for hiding and fading out image elements that are used as indicator icons for the various metadata types like genre, publisher, players etc. It's however not possible to do the opposite, i.e. to disable this functionality for the default game media types as that would break basic application behavior. - - Default is `false` -* `gameselector` - type: STRING - - If more than one gameselector element has been defined, this property makes it possible to state which one to use. If multiple gameselector elements have been defined and this property is missing then the first entry will be chosen and a warning message will be logged. If only a single gameselector has been defined, this property is ignored. The value of this property must match the `name` attribute value of the gameselector element. This property is only needed for the `system` view and only if the `imageType` property is utilized. -* `tile` - type: BOOLEAN - - If true, the image will be tiled instead of stretched to fit its size. Useful for backgrounds. - - Default is `false` -* `tileSize` - type: NORMALIZED_PAIR - - Size of the individual images making up the tile as opposed to the overall size for the element which is defined by the `size` property. If only one axis is specified (and the other is zero), then the other axis will be automatically calculated in accordance with the image's aspect ratio. Setting both axes to 0 is an error and tiling will be disabled in this case. If this property is omitted, then the size will be set to the actual image dimensions. For SVG images this means whatever canvas size has been defined inside the file. - - Minimum value per axis is `0` and maximum value per axis is `1`. -* `tileHorizontalAlignment` - type: STRING - - If the images making up the tiled texture do not match precisely with the edges of the overall element, then this property can be used to define the alignment on the horizontal axis. - - Valid values are `left` or `right` - - Default is `left` -* `tileVerticalAlignment` - type: STRING - - If the images making up the tiled texture do not match precisely with the edges of the overall element, then this property can be used to define the alignment on the vertical axis. - - Valid values are `top` or `bottom` - - Default is `bottom` -* `interpolation` - type: STRING - - Interpolation method to use when scaling. Nearest neighbor (`nearest`) preserves sharp pixels and linear filtering (`linear`) makes the image smoother. This property has limited effect on scalable vector graphics (SVG) images unless rotation is applied. - - Valid values are `nearest` or `linear` - - Default is `nearest` -* `color` - type: COLOR - - Multiply each pixel's color by this color. For example, an all-white image with `FF0000` would become completely red. You can also control the transparency of an image with `FFFFFFAA` - keeping all the pixels their normal color and only affecting the alpha channel. -* `colorEnd` - type: COLOR - - Works exactly in the same way as `color` but can be set as the end color to apply a color shift gradient to the image. -* `gradientType` - type: STRING - - The direction to apply the color shift gradient if both `color` and `colorEnd` have been defined. - - Valid values are `horizontal` or `vertical` - - Default is `horizontal` -* `scrollFadeIn` - type: BOOLEAN - - If enabled, a short fade-in animation will be applied when scrolling through games in the gamelist view. This usually looks best if used for the main game image. - - Default is `false` -* `opacity` - type: FLOAT - - Controls the level of transparency. If set to `0` the element will be disabled. - - Minimum value is `0` and maximum value is `1` - - Default is `1` -* `saturation` - type: FLOAT - - Controls the level of color saturation. - - Minimum value is `0` (grayscale) and maximum value is `1` (original file saturation). - - Default is `1` -* `visible` - type: BOOLEAN - - If set to false, the element will be disabled. This is equivalent to setting `opacity` to `0` - - Default is `true` -* `zIndex` - type: FLOAT - - z-index value for element. Elements will be rendered in order of zIndex value from low to high. - - Default is `30` - -### video - -Plays a video and provides support for displaying a static image for a defined time period before starting the video player. Although an unlimited number of videos could in theory be defined per view it's recommended to keep it at a single instance as playing videos takes a lot of CPU resources. But if still going for multiple videos, make sure to use the `audio` property to disable audio on all but one video as ES-DE currently has no audio mixing capabilities so the sound would not play correctly. To use videos in the `system` view, you either need to set a static video using the `path` property, or you need to create a `gameselector` element so game videos can be used. - -Supported views: -* `system ` -* `gamelist` - -Instances per view: -* `unlimited` (but recommended to keep at a single instance) - -Properties: -* `pos` - type: NORMALIZED_PAIR -* `size` - type: NORMALIZED_PAIR - - If only one axis is specified (and the other is zero), then the other will be automatically calculated in accordance with the static image's aspect ratio and the video's aspect ratio. Setting both axes to 0 is an error and the size will be clamped to `0.01 0.01` in this case. - - Minimum value per axis is `0.01` and maximum value per axis is `2`. If specifying a value outside the allowed range then no attempt will be made to preserve the aspect ratio. -* `maxSize` - type: NORMALIZED_PAIR - - The static image and video will be resized as large as possible so that they fit within this size while maintaining their aspect ratios. Use this instead of `size` when you don't know what kind of video you're using so it doesn't get grossly oversized on one axis (e.g. with a game's video metadata). - - Minimum value per axis is `0.01` and maximum value per axis is `2` -* `origin` - type: NORMALIZED_PAIR - - Where on the element `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the element exactly in the middle of the screen. If the position and size attributes are themeable, origin is implied. - - Minimum value per axis is `0` and maximum value per axis is `1` - - Default is `0 0` -* `path` - type: PATH - - Path to a video file. Setting a value for this property will make the video static, i.e. any `imageType`, `gameselector` and `default` properties will be ignored. -* `default` - type: PATH - - Path to a default video file. The default video will be played when the selected game does not have a video. This property is also applied to any custom collection that does not contain any games when browsing the grouped custom collections system. -* `defaultImage` - type: PATH - - Path to a default image file. The default image will be displayed when the selected game does not have an image of the type defined by the `imageType` property (i.e. this `default` property does nothing unless a `imageType` property has been set). It's also applied to any custom collection that does not contain any games when browsing the grouped custom collections system. -* `imageType` - type: STRING - - This displays a game image of a certain media type. Multiple types can be defined, in which case the entries should be delimited by commas or by whitespace characters (tabs, spaces or line breaks). The media will be searched for in the order that the entries have been defined. If no image is found, then the space will be left blank unless the `default` property has been set. To use this property from the `system` view, you will first need to add a `gameselector` element. If `delay` is set to zero, then this property is ignored. Defining duplicate values is considered an error and will also result in the property getting ignored. - - Valid values: - - `image` - This will look for a `miximage`, and if that is not found `screenshot` is tried next, then `titlescreen` and finally `cover`. This is just a convenient shortcut and it's equivalent to explicitly defining `miximage, screenshot, titlescreen, cover` - - `miximage` - This will look for a miximage. - - `marquee` - This will look for a marquee (wheel) image. - - `screenshot` - This will look for a screenshot image. - - `titlescreen` - This will look for a title screen image. - - `cover` - This will look for a box front cover image. - - `backcover` - This will look for a box back cover image. - - `3dbox` - This will look for a 3D box image. - - `physicalmedia` - This will look for a physical media image. - - `fanart` - This will look for a fan art image. -* `metadataElement` - type: BOOLEAN - - By default game metadata and media are faded out during gamelist fast-scrolling and text metadata fields, ratings and badges are hidden when enabling the _Hide metadata fields_ setting for a game entry. Using this property it's possible to explicitly define static video elements that should be treated as if they were game media files. This property is ignored if `path` is not set. - - Default is `false` -* `gameselector` - type: STRING - - If more than one gameselector element has been defined, this property makes it possible to state which one to use. If multiple gameselector elements have been defined and this property is missing then the first entry will be chosen and a warning message will be logged. If only a single gameselector has been defined, this property is ignored. The value of this property must match the `name` attribute value of the gameselector element. -* `audio` - type: BOOLEAN - - Whether to enable or disable audio playback for the video. For static videos in the gamelist view it's strongly recommended to set this to `false` if there is also a separate video element playing game videos. - - Default is `true` -* `interpolation` - type: STRING - - Interpolation method to use when scaling raster images. Nearest neighbor (`nearest`) preserves sharp pixels and linear filtering (`linear`) makes the image smoother. Note that this property only affects the static image, not the video scaling. This property also has no effect on scalable vector graphics (SVG) images. - - Valid values are `nearest` or `linear` - - Default is `nearest` -* `pillarboxes` - type: BOOLEAN - - Whether to render black pillarboxes (and to a lesses extent letterboxes) for videos with aspect ratios where this is applicable. This is for instance useful for arcade game videos in vertical orientation. - - Default is `true` -* `pillarboxThreshold` - type: NORMALIZED_PAIR - - Normally it doesn't look very good to add really narrow pillarboxes or letterboxes, so by default they are skipped if the actual video size is not reaching a threshold value as compared to the overall defined video area size. By modifying this property it's possible to control that threshold, as for some theme designs it will look better with the consistency of always rendering the pillarboxes/letterboxes even if they are narrow. To clarify, the default X axis value of 0.85 means that if the video width is 85% or less as compared to the X axis defined by the `size` property, then pillarboxes will be rendered. So setting the `pillarboxThreshold` value to `1 1` will always apply pillarboxes/letterboxes regardless of the video file dimension. - - Minimum value per axis is `0.2` and maximum value per axis is `1` - - Default is `0.85 0.90` -* `scanlines` - type: BOOLEAN - - Whether to use a shader to render scanlines. - - Default is `false` -* `delay` - type: FLOAT - - Delay in seconds before video will start playing. During the delay period the game image defined via the `imageType` property will be displayed. If that property is not set, then the `delay` property will be ignored. - - Minimum value is `0` and maximum value is `15` - - Default is `1.5` -* `fadeInTime` - type: FLOAT - - Time in seconds to fade in the video from pure black. This is completely unrelated to the `scrollFadeIn` property. Note that if this is set to zero it may seem as if the property doesn't work correctly as many ScreenScraper videos have a fade-in baked into the actual video stream. Setting this property to lower than 0.3 seconds or so is generally a bad idea for videos that don't have a fade-in baked in as transitions from the static image will then look like a bad jump cut. - - Minimum value is `0` and maximum value is `8` - - Default is `1` -* `scrollFadeIn` - type: BOOLEAN - - If enabled, a short fade-in animation will be applied when scrolling through games in the gamelist view. This animation is only applied to images and not to actual videos, so if no image metadata has been defined then this property has no effect. For this to work correctly the `delay` property also needs to be set. - - Default is `false` -* `opacity` - type: FLOAT - - Controls the level of transparency. If set to `0` the element will be disabled. - - Minimum value is `0` and maximum value is `1` - - Default is `1` -* `saturation` - type: FLOAT - - Controls the level of color saturation. This affects both the static image and the video stream. - - Minimum value is `0` (grayscale) and maximum value is `1` (original file saturation). - - Default is `1` -* `visible` - type: BOOLEAN - - If set to false, the element will be disabled. This is equivalent to setting `opacity` to `0` - - Default is `true` -* `zIndex` - type: FLOAT - - z-index value for element. Elements will be rendered in order of zIndex value from low to high. - - Default is `30` - -### animation - -GIF and Lottie (vector graphics) animations. The type of animation is automatically selected based on the file extension with `.gif` for GIF animations and `.json` for Lottie animations. Note that Lottie animations take a lot of memory and CPU resources if scaled up to large sizes so it's adviced to not add too many of them to the same view and to not make them too large. GIF animations on the other hand are not as demanding except if they're really long and/or high-resolution. - -Supported views: -* `system ` -* `gamelist` - -Instances per view: -* `unlimited` - -Properties: -* `pos` - type: NORMALIZED_PAIR -* `size` - type: NORMALIZED_PAIR - - If only one axis is specified (and the other is zero), the other will be automatically calculated in accordance with the animation's aspect ratio. Note that this is sometimes not entirely accurate as some animations contain invalid size information. -* `origin` - type: NORMALIZED_PAIR - - Where on the element `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the element exactly in the middle of the screen. If the position and size attributes are themeable, origin is implied. - - Minimum value per axis is `0` and maximum value per axis is `1` - - Default is `0 0` -* `rotation` - type: FLOAT - - Angle in degrees that the animation should be rotated. Positive values will rotate clockwise, negative values will rotate counterclockwise. - - Default is `0` -* `rotationOrigin` - type: NORMALIZED_PAIR - - Point around which the animation will be rotated. - - Minimum value per axis is `0` and maximum value per axis is `1` - - Default is `0.5 0.5` -* `metadataElement` - type: BOOLEAN - - By default game metadata and media are faded out during gamelist fast-scrolling and text metadata fields, ratings and badges are hidden when enabling the _Hide metadata fields_ setting for a game entry. Using this property it's possible to explicitly define animation elements that should be treated as if they were game media files. This is for example useful for hiding and fading out animations that are used as indicators for the various metadata types like genre, publisher, players etc. - - Default is `false` -* `path` - type: PATH - - Path to the animation file. Only the .json extension is supported. -* `speed` - type: FLOAT. - - The relative speed at which to play the animation. - - Minimum value is `0.2` and maximum value is `3` - - Default is `1` -* `direction` - type: STRING - - The direction that the animation should be played. Valid values are `normal` (forwards), `reverse` (backwards), `alternate` (bouncing forwards/backwards) and `alternateReverse` (bouncing backwards/forwards, i.e. starting with playing backwards). - - Default is `normal` -* `keepAspectRatio` - type: BOOLEAN. - - If true, aspect ratio will be preserved. If false, animation will stretch to the defined size. Note that setting to `false` is incompatible with only defining one of the axes for the `size` element. - - Default is `true` -* `interpolation` - type: STRING - - Interpolation method to use when scaling GIF animations. Nearest neighbor (`nearest`) preserves sharp pixels and linear filtering (`linear`) makes the image smoother. This property has no effect on Lottie animations. - - Valid values are `nearest` or `linear` - - Default is `nearest` -* `opacity` - type: FLOAT - - Controls the level of transparency. If set to `0` the element will be disabled. - - Minimum value is `0` and maximum value is `1` - - Default is `1` -* `saturation` - type: FLOAT - - Controls the level of color saturation. - - Minimum value is `0` (grayscale) and maximum value is `1` (original file saturation). - - Default is `1` -* `visible` - type: BOOLEAN - - If set to false, the element will be disabled. This is equivalent to setting `opacity` to `0` - - Default is `true` -* `zIndex` - type: FLOAT - - z-index value for element. Elements will be rendered in order of zIndex value from low to high. - - Default is `35` - -### badges - -Displays graphical symbols representing a number of metadata fields for the currently selected game. It's strongly recommended to use the same image dimensions for all badges as varying aspect ratios will lead to alignment issues. For the controller images it's recommended to keep to the square canvas size used by the default bundled graphics as otherwise sizing and placement will be inconsistent (unless all controller graphic files are customized of course). - -Supported views: -* `gamelist` - -Instances per view: -* `unlimited` - -Properties: -* `pos` - type: NORMALIZED_PAIR -* `size` - type: NORMALIZED_PAIR - - Possible combinations: - - `w h` - Dimensions of the badges container. The badges will be scaled to fit within these dimensions. - - Minimum value per axis is `0.03` and maximum value per axis is `1` - - Default is `0.15 0.20` -* `origin` - type: NORMALIZED_PAIR - - Where on the element `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the element exactly in the middle of the screen. If the position and size attributes are themeable, origin is implied. - - Minimum value per axis is `0` and maximum value per axis is `1` - - Default is `0 0` -* `rotation` - type: FLOAT - - Angle in degrees that the badges should be rotated. Positive values will rotate clockwise, negative values will rotate counterclockwise. - - Default is `0` -* `rotationOrigin` - type: NORMALIZED_PAIR - - Point around which the image will be rotated. - - Minimum value per axis is `0` and maximum value per axis is `1` - - Default is `0.5 0.5`. -* `horizontalAlignment` - type: STRING. - - Valid values are `left` or `right` -* `direction` - type: STRING - - Valid values are "row" or "column". Controls the primary layout direction (line axis) for the badges. Lines will fill up in the specified direction. - - Default is `row` -* `lines` - type: UNSIGNED_INTEGER - - The number of lines available. - - Default is `3` -* `itemsPerLine` - type: UNSIGNED_INTEGER - - Number of badges that fit on a line. When more badges are available a new line will be started. - - Default is `4` -* `itemMargin` - type: NORMALIZED_PAIR - - The horizontal and vertical margins between badges - `x y` - - If one of the axis is set to `-1` the margin of the other axis (in pixels) will be used, which makes it possible to get identical spacing between all items regardless of screen aspect ratio. - - Minimum value per axis is `0` and maximum value per axis is `0.2` - - Default is `0.01 0.01`. -* `slots` - type: STRING - - The badge types that should be displayed. Specified as a list of strings delimited by commas or by whitespace characters (tabs, spaces or line breaks). The order in which they are defined will be followed when placing badges on screen. Available badges are: - - `collection` - Will be shown when editing a custom collection and the current entry is part of that collection. - - `folder` - Will be shown when the current entry is a folder. If a folder link has been setup, then a configurable link icon will overlay this badge. - - `favorite` - Will be shown when the game is marked as favorite. - - `completed` - Will be shown when the game is marked as completed. - - `kidgame` - Will be shown when the game is marked as a kids game. - - `broken` - Will be shown when the game is marked as broken. - - `controller` - Will be shown and overlaid by the corresponding controller icon if a controller type has been selected for the game (using the metadata editor or via scraping). - - `altemulator` - Will be shown when an alternative emulator is setup for the game. - - `all` - Including this value will enable all badges. If some badges have been added already they will be shown in the order they were defined and the remaining ones will be added at the end, in the order listed above. Using the `all` value can be used as a way to future-proof the theme, because if additional badges are added in future ES-DE releases, no theme updates would be needed to accomodate them. Just make sure to include space for a few extra badges in the layout, and increase the `lines` and `itemsPerLine` accordingly. -* `controllerPos` - type: NORMALIZED_PAIR - - The position of the controller icon relative to the parent `controller` badge. - - Minimum value per axis is `-1` and maximum value per axis is `2` - - Default is `0.5 0.5` which centers the controller icon on the badge. -* `controllerSize` - type: FLOAT - - The size of the controller icon relative to the parent `controller` badge. - - Setting the value to `1` sizes the icon to the same width as the parent badge. The image aspect ratio is always maintained. - - Minimum value is `0.1` and maximum value is `2` - - Default is `0.5` -* `customBadgeIcon` - type: PATH - - A badge icon override. Specify the badge type in the attribute `badge`. The available badges are the ones listed above. -* `customControllerIcon` - type: PATH - - A controller icon override. Specify the controller type in the attribute `controller`. - - These are the available types: - - `gamepad_generic`, - `gamepad_nintendo_nes`, - `gamepad_nintendo_snes`, - `gamepad_nintendo_64`, - `gamepad_playstation`, - `gamepad_sega_md_3_buttons`, - `gamepad_sega_md_6_buttons`, - `gamepad_xbox`, - `joystick_generic`, - `joystick_arcade_no_buttons`, - `joystick_arcade_1_button`, - `joystick_arcade_2_buttons`, - `joystick_arcade_3_buttons`, - `joystick_arcade_4_buttons`, - `joystick_arcade_5_buttons`, - `joystick_arcade_6_buttons`, - `keyboard_generic`, - `keyboard_and_mouse_generic`, - `mouse_generic`, - `mouse_amiga`, - `lightgun_generic`, - `lightgun_nintendo`, - `steering_wheel_generic`, - `flight_stick_generic`, - `spinner_generic`, - `trackball_generic`, - `wii_remote_nintendo`, - `wii_remote_and_nunchuk_nintendo`, - `joycon_left_or_right_nintendo`, - `joycon_pair_nintendo`, - `xbox_kinect`, - `unknown` -* `folderLinkPos` - type: NORMALIZED_PAIR - - The position of the folder link icon relative to the parent `folder` badge. - - Minimum value per axis is `-1` and maximum value per axis is `2` - - Default is `0.5 0.5` which centers the folder link icon on the badge. -* `folderLinkSize` - type: FLOAT - - The size of the folder link icon relative to the parent `folder` badge. - - Setting the value to `1` sizes the icon to the same width as the parent badge. The image aspect ratio is always maintained. - - Minimum value is `0.1` and maximum value is `1` - - Default is `0.5` -* `customFolderLinkIcon` - type: PATH - - Folder link icon override. -* `opacity` - type: FLOAT - - Controls the level of transparency. If set to `0` the element will be disabled. - - Minimum value is `0` and maximum value is `1` - - Default is `1` -* `visible` - type: BOOLEAN - - If set to false, the element will be disabled. This is equivalent to setting `opacity` to `0` - - Default is `true` -* `zIndex` - type: FLOAT - - z-index value for element. Elements will be rendered in order of zIndex value from low to high. - - Default is `35` - -### text - -Displays text. This can be literal strings or values based on game metadata or system variables, as described below. For the `gamelist` view it's also possible to place the text inside a scrollable container which is for example useful for longer texts like the game descriptions. - -Supported views: -* `system` -* `gamelist` - -Instances per view: -* `unlimited` - -Properties: -* `pos` - type: NORMALIZED_PAIR -* `size` - type: NORMALIZED_PAIR - - Possible combinations: - - `0 0` - automatically size so text fits on one line (expanding horizontally). - - `w 0` - automatically wrap text so it doesn't go beyond `w` (expanding vertically). - - `w h` - works like a "text box". If `h` is non-zero and `h` <= `fontSize` (implying it should be a single line of text), text that goes beyond `w` will be truncated with an elipses (...). -* `origin` - type: NORMALIZED_PAIR - - Where on the element `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the element exactly in the middle of the screen. If the position and size attributes are themeable, origin is implied. - - Minimum value per axis is `0` and maximum value per axis is `1` - - Default is `0 0` -* `rotation` - type: FLOAT - - Angle in degrees that the text should be rotated. Positive values will rotate clockwise, negative values will rotate counterclockwise. Rotation is not possible if the `container` property has been set to true. - - Default is `0` -* `rotationOrigin` - type: NORMALIZED_PAIR - - Point around which the text will be rotated. - - Minimum value per axis is `0` and maximum value per axis is `1` - - Default is `0.5 0.5` -* `text` - type: STRING - - A string literal to display. -* `systemdata` - type: STRING - - This translates to some system data including values defined in es_systems.xml as well as some statistics. This property can only be used in the `system` view and you can only define a single value per element. - - Valid values: - - `name` - Short system name as defined in es_systems.xml. - - `fullname` - Full system name as defined in es_systems.xml. - - `gamecount` - Number of games available for the system. Number of favorites are printed inside brackets if applicable. - - `gamecount_games` - Number of games available for the system. Does not print the favorites count. - - `gamecount_favorites` - Number of favorite games for the system, may be blank if favorites are not applicable. -* `metadata` - type: STRING - - This translates to the metadata values that are available for the game. To use this property from the `system` view, you will first need to add a `gameselector` element. You can only define a single metadata value per text element. - - Valid values: - - `name` - Game name. - - `description` - Game description. Should be combined with the `container` property in most cases. - - `rating` - The numerical representation of the game rating, for example `3` or `4.5`. - - `developer` - Developer. - - `publisher` - Publisher. - - `genre` - Genre. - - `players` - The number of players. - - `favorite` - Whether the game is a favorite. Will be printed as either `yes` or `no`. - - `completed` - Whether the game has been completed. Will be printed as either `yes` or `no`. - - `kidgame` - Whether the game is suitable for children. Will be printed as either `yes` or `no`. - - `broken` - Whether the game is broken/not working. Will be printed as either `yes` or `no`. - - `playcount` - How many times the game has been played. - - `controller` - The controller for the game. Will be blank if none has been selected. - - `altemulator` - The alternative emulator for the game. Will be blank if none has been selected. -* `metadataElement` - type: BOOLEAN - - By default game metadata and media are faded out during gamelist fast-scrolling and text metadata fields, ratings and badges are hidden when enabling the _Hide metadata fields_ setting for a game entry. Using this property it's possible to explicitly define additional text elements that should be treated as if they were game metadata entries. This is for example useful for hiding and fading out text labels for the various metadata types like genre, publisher, players etc. Note that it's not possible to disable the metadata hiding functionality for the default metadata fields as that would break basic application behavior. Also note that there is a slight exception to the hiding logic for text containers with the metadata value set to `description`. In this case the element is by default not hidden when enabling the _Hide metadata fields_ setting. To also hide such containers, set this property to true. - - Default is `false` -* `gameselector` - type: STRING - - If more than one gameselector element has been defined, this property makes it possible to state which one to use. If multiple gameselector elements have been defined and this property is missing then the first entry will be chosen and a warning message will be logged. If only a single gameselector has been defined, this property is ignored. The value of this property must match the `name` attribute value of the gameselector element. This property is only needed for the `system` view and only if the `metadata` property is utilized. -* `container` - type: BOOLEAN - - Whether the text should be placed inside a scrollable container. Only available for the `gamelist` view. - - Default is `false` -* `containerVerticalSnap` - type: BOOLEAN - - Whether the text should be vertically snapped to the font height. With this property enabled the container will have its height reduced as needed so that only complete rows of text are displayed at the start and end positions. This will not affect the "real" size of the container as set by the `size` property which means that the overall element placement will still be predictable if a vertical origin other than zero is used. - - Default is `true` -* `containerScrollSpeed` - type: FLOAT - - A base speed is automatically calculated based on the container and font sizes, so this property applies relative to the auto-calculated value. - - Minimum value is `0.1` and maximum value is `10` - - Default is `1` -* `containerStartDelay` - type: FLOAT - - Delay in seconds before scrolling starts. Note that the text fade-in animation that plays when resetting from the end position will cause a slight delay even if this property is set to zero. - - Minimum value is `0` and maximum value is `10` - - Default is `4.5` -* `containerResetDelay` - type: FLOAT - - Delay in seconds before resetting to the start position after reaching the scrolling end position. - - Minimum value is `0` and maximum value is `20` - - Default is `7` -* `fontPath` - type: PATH - - Path to a TrueType font (.ttf). -* `fontSize` - type: FLOAT - - Size of the font as a percentage of screen height (e.g. for a value of `0.1`, the text's height would be 10% of the screen height). This calculation is based on the reference 'S' character so other glyphs may not fill this area, or they may exceed this area. - - Minimum value is `0.001` and maximum value is `1.5`. Note that when running at a really low resolution, the minimum value may get clamped to a larger relative size. The font is allowed to overflow the height of the element by 100%, i.e. `fontSize` can be set to twice that of the y axis of the `size` property. Any value above that will be clamped. - - Default is `0.045` -* `horizontalAlignment` - type: STRING - - Controls alignment on the X axis. - - Valid values are `left`, `center` or `right` - - Default is `left` -* `verticalAlignment` - type: STRING - - Controls alignment on the Y axis. - - Valid values are `top`, `center` or `bottom` - - Default is `center` -* `color` - type: COLOR -* `backgroundColor` - type: COLOR -* `letterCase` - type: STRING - - Valid values are `none`, `uppercase`, `lowercase` or `capitalize` - - Default is `none` (original letter case is retained) -* `lineSpacing` - type: FLOAT - - Controls the space between lines (as a multiple of the font height). Due to the imprecise nature of typefaces where certain glyphs (characters) may exceed the requested font size, it's recommended to keep this value at around `1.1` or higher for multi-line text fields. This way overlapping glyphs or characters being cut off at the top or bottom will be prevented. - - Minimum value is `0.5` and maximum value is `3` - - Default is `1.5` -* `opacity` - type: FLOAT - - Controls the level of transparency. If set to `0` the element will be disabled. - - Minimum value is `0` and maximum value is `1` - - Default is `1` -* `visible` - type: BOOLEAN - - If set to false, the element will be disabled. This is equivalent to setting `opacity` to `0` - - Default is `true` -* `zIndex` - type: FLOAT - - z-index value for element. Elements will be rendered in order of zIndex value from low to high. - - Default is `40` - -### datetime - -Displays a date and time as a text string. The format is ISO 8601 (YYYY-MM-DD) by default, but this can be changed using the `format` property. The text _unknown_ will be shown by default if there is no time stamp available. If the property `displayRelative` has been set, the text will be shown as _never_ in case of no time stamp. - -Supported views: -* `system` -* `gamelist` - -Instances per view: -* `unlimited` - -Properties: -* `pos` - type: NORMALIZED_PAIR -* `size` - type: NORMALIZED_PAIR - - Possible combinations: - - `0 0` - automatically size so text fits on one line (expanding horizontally). - - `w 0` - automatically wrap text so it doesn't go beyond `w` (expanding vertically). - - `w h` - works like a "text box". If `h` is non-zero and `h` <= `fontSize` (implying it should be a single line of text), text that goes beyond `w` will be truncated with an elipses (...). -* `origin` - type: NORMALIZED_PAIR - - Where on the element `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the element exactly in the middle of the screen. If the position and size attributes are themeable, origin is implied. - - Minimum value per axis is `0` and maximum value per axis is `1` - - Default is `0 0` -* `rotation` - type: FLOAT - - Angle in degrees that the text should be rotated. Positive values will rotate clockwise, negative values will rotate counterclockwise. - - Default is `0` -* `rotationOrigin` - type: NORMALIZED_PAIR - - Point around which the text will be rotated. - - Minimum value per axis is `0` and maximum value per axis is `1` - - Default is `0.5 0.5`. -* `metadata` - type: STRING - - This displays the metadata values that are available for the game. If an invalid metadata field is defined, the text "unknown" will be printed. To use this property from the `system` view, you will first need to add a `gameselector` element. You can only define a single metadata value per datetime element. - - Valid values: - - `releasedate` - The release date of the game. - - `lastplayed` - The time the game was last played. This will be displayed as a value relative to the current date and time by default, but can be overridden using the `displayRelative` property. -* `gameselector` - type: STRING - - If more than one gameselector element has been defined, this property makes it possible to state which one to use. If multiple gameselector elements have been defined and this property is missing then the first entry will be chosen and a warning message will be logged. If only a single gameselector has been defined, this property is ignored. The value of this property must match the `name` attribute value of the gameselector element. This property is only needed for the `system` view and only if the `metadata` property is utilized. -* `fontPath` - type: PATH - - Path to a TrueType font (.ttf). -* `fontSize` - type: FLOAT - - Size of the font as a percentage of screen height (e.g. for a value of `0.1`, the text's height would be 10% of the screen height). This calculation is based on the reference 'S' character so other glyphs may not fill this area, or they may exceed this area. - - Minimum value is `0.001` and maximum value is `1.5`. Note that when running at a really low resolution, the minimum value may get clamped to a larger relative size. The font is allowed to overflow the height of the element by 100%, i.e. `fontSize` can be set to twice that of the y axis of the `size` property. Any value above that will be clamped. - - Default is `0.045` -* `horizontalAlignment` - type: STRING - - Controls alignment on the X axis. - - Valid values are `left`, `center` or `right` - - Default is `left` -* `verticalAlignment` - type: STRING - - Controls alignment on the Y axis. - - Valid values are `top`, `center` or `bottom` - - Default is `center` -* `color` - type: COLOR -* `backgroundColor` - type: COLOR -* `letterCase` - type: STRING - - Valid values are `none`, `uppercase`, `lowercase` or `capitalize` - - Default is `none` (original letter case is retained) -* `lineSpacing` - type: FLOAT - - Controls the space between lines (as a multiple of font height). - - Minimum value is `0.5` and maximum value is `3` - - Default is `1.5` -* `format` - type: STRING - - Specifies the date and time format. Has no effect if `displayRelative` has been set to true. - - %Y: The year, including the century (1900) - - %m: The month number [01,12] - - %d: The day of the month [01,31] - - %H: The hour (24-hour clock) [00,23] - - %M: The minute [00,59] - - %S: The second [00,59] -* `displayRelative` - type: BOOLEAN. - - Renders the datetime as a relative string (e.g. 'x days ago'). - - Default is `false` -* `opacity` - type: FLOAT - - Controls the level of transparency. If set to `0` the element will be disabled. - - Minimum value is `0` and maximum value is `1` - - Default is `1` -* `visible` - type: BOOLEAN - - If set to false, the element will be disabled. This is equivalent to setting `opacity` to `0` - - Default is `true` -* `zIndex` - type: FLOAT - - z-index value for element. Elements will be rendered in order of zIndex value from low to high. - - Default is `40` - -### gamelistinfo - -Displays the game count (all games as well as favorites), any applied filters, and a folder icon if a folder has been entered. If this text is left aligned or center aligned, the folder icon will be placed to the right of the other information, and if it's right aligned, the folder icon will be placed to the left. - -Supported views: -* `gamelist` - -Instances per view: -* `unlimited` - -Properties: -* `pos` - type: NORMALIZED_PAIR -* `size` - type: NORMALIZED_PAIR - - Possible combinations: - - `0 0` - automatically size so text fits on one line (expanding horizontally). - - `w 0` - automatically wrap text so it doesn't go beyond `w` (expanding vertically). - - `w h` - works like a "text box". If `h` is non-zero and `h` <= `fontSize` (implying it should be a single line of text), text that goes beyond `w` will be truncated with an elipses (...). -* `origin` - type: NORMALIZED_PAIR - - Where on the element `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the element exactly in the middle of the screen. If the position and size attributes are themeable, origin is implied. - - Minimum value per axis is `0` and maximum value per axis is `1` - - Default is `0 0` -* `rotation` - type: FLOAT - - Angle in degrees that the text should be rotated. Positive values will rotate clockwise, negative values will rotate counterclockwise. - - Default is `0` -* `rotationOrigin` - type: NORMALIZED_PAIR - - Point around which the element will be rotated. - - Minimum value per axis is `0` and maximum value per axis is `1` - - Default is `0.5 0.5` -* `fontPath` - type: PATH - - Path to a TrueType font (.ttf). -* `fontSize` - type: FLOAT - - Size of the font as a percentage of screen height (e.g. for a value of `0.1`, the text's height would be 10% of the screen height). This calculation is based on the reference 'S' character so other glyphs may not fill this area, or they may exceed this area. - - Minimum value is `0.001` and maximum value is `1.5`. Note that when running at a really low resolution, the minimum value may get clamped to a larger relative size. The font is allowed to overflow the height of the element by 100%, i.e. `fontSize` can be set to twice that of the y axis of the `size` property. Any value above that will be clamped. - - Default is `0.045` -* `horizontalAlignment` - type: STRING - - Controls alignment on the X axis. - - Valid values are `left`, `center` or `right` - - Default is `left` -* `verticalAlignment` - type: STRING - - Controls alignment on the Y axis. - - Valid values are `top`, `center` or `bottom` - - Default is `center` -* `color` - type: COLOR -* `backgroundColor` - type: COLOR -* `opacity` - type: FLOAT - - Controls the level of transparency. If set to `0` the element will be disabled. - - Minimum value is `0` and maximum value is `1` - - Default is `1` -* `visible` - type: BOOLEAN - - If set to false, the element will be disabled. This is equivalent to setting `opacity` to `0` - - Default is `true` -* `zIndex` - type: FLOAT - - z-index value for element. Elements will be rendered in order of zIndex value from low to high. - - Default is `45` - -### rating - -Displays a graphical representation of the game rating, from 0 to 5. - -To display game ratings in the `system` view, you first need to create a `gameselector` element. - -Supported views: -* `system` -* `gamelist` - -Instances per view: -* `unlimited` - -Properties: -* `pos` - type: NORMALIZED_PAIR -* `size` - type: NORMALIZED_PAIR - - These values are mutually exclusive, if an X axis value is defined then the element will be sized based on this, and if an Y axis value is defined then the element will be sized based on that. If both the X and Y axis values are defined then the Y axis value will take precedence and the X axis value will be ignored. This makes sure that the image aspect ratio is always maintained. - - Minimum value per axis is `0.01` and maximum value for the X axis is `1` and maximum value for the Y axis is `0.5` - - Default is `0 0.06` -* `origin` - type: NORMALIZED_PAIR - - Where on the element `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the element exactly in the middle of the screen. If the position and size attributes are themeable, origin is implied. - - Minimum value per axis is `0` and maximum value per axis is `1` - - Default is `0 0` -* `rotation` - type: FLOAT - - Angle in degrees that the rating should be rotated. Positive values will rotate clockwise, negative values will rotate counterclockwise. - - Default is `0` -* `rotationOrigin` - type: NORMALIZED_PAIR - - Point around which the rating will be rotated. - - Minimum value per axis is `0` and maximum value per axis is `1` - - Default is `0.5 0.5` -* `gameselector` - type: STRING - - If more than one gameselector element has been defined, this property makes it possible to state which one to use. If multiple gameselector elements have been defined and this property is missing then the first entry will be chosen and a warning message will be logged. If only a single gameselector has been defined, this property is ignored. The value of this property must match the `name` attribute value of the gameselector element. This property is only needed for the `system` view. -* `interpolation` - type: STRING - - Interpolation method to use when scaling the images. Nearest neighbor (`nearest`) preserves sharp pixels and linear filtering (`linear`) makes the image smoother. The effect of this property is primarily visible for raster graphic images, but it has a limited effect also when using scalable vector graphics (SVG) images, and even more so if rotation is applied. - - Valid values are `nearest` or `linear` - - Default is `nearest` -* `color` - type: COLOR - - Multiply each pixel's color by this color. For example, an all-white image with `FF0000` would become completely red. You can also control the transparency of an image with `FFFFFFAA` - keeping all the pixels their normal color and only affecting the alpha channel. - - Default is `FFFFFFFF` -* `filledPath` - type: PATH - - Path to the "filled" rating image. Any aspect ratio is supported. Note that there is no explicit padding property, so to add spaces between each icon simply make the image content smaller on the canvas. The images should always be centered on the canvas or otherwise the filledPath and unfilledPath textures will not align properly for all rating values. Most common file extensions are supported (including .svg, .jpg, .png, and unanimated .gif). -* `unfilledPath` - type: PATH - - Path to the "unfilled" rating image. Any aspect ratio is supported. Note that there is no explicit padding property, so to add spaces between each icon simply make the image content smaller on the canvas. The images should always be centered on the canvas or otherwise the filledPath and unfilledPath textures will not align properly for all rating values. Most common file extensions are supported (including .svg, .jpg, .png, and unanimated .gif). -* `overlay` - type: BOOLEAN - - Whether to overlay the filledPath image on top of the unfilledPath image. If this property is set to false, then the unfilledPath image will only be rendered to the right of the rating value cut position. This property is useful for avoiding image aliasing artifacts that could otherwise occur when combining some rating images. It can also help with avoiding some inconsistent fade-out animations. - - Default is `true` -* `opacity` - type: FLOAT - - Controls the level of transparency. If set to `0` the element will be disabled. - - Minimum value is `0` and maximum value is `1` - - Default is `1` -* `visible` - type: BOOLEAN - - If set to false, the element will be disabled. This is equivalent to setting `opacity` to `0` - - Default is `true` -* `zIndex` - type: FLOAT - - z-index value for element. Elements will be rendered in order of zIndex value from low to high. - - Default is `45` - -### carousel +#### carousel A carousel for navigating and selecting games or systems. @@ -1827,7 +1159,33 @@ Properties: - z-index value for element. Elements will be rendered in order of zIndex value from low to high. - Default is `50` -### textlist +#### grid + +**The grid component is currently in active development which means that properties and values may change without prior warning up until the final 2.0.0 release.** + +An X*Y grid for navigating and selecting games or systems using the left/right and up/down buttons. + +Supported views: +* `system` +* `gamelist` + +Instances per view: +* `single` + +Properties: +* `pos` - type: NORMALIZED_PAIR + - Default is `0 0.1` +* `size` - type: NORMALIZED_PAIR + - Default is `1 0.8` +* `origin` - type: NORMALIZED_PAIR + - Where on the element `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the textlist exactly in the middle of the screen. If the position and size attributes are themeable, origin is implied. + - Minimum value per axis is `0` and maximum value per axis is `1` + - Default is `0 0` +* `zIndex` - type: FLOAT + - z-index value for element. Elements will be rendered in order of zIndex value from low to high. + - Default is `50` + +#### textlist A text list for navigating and selecting games or systems. @@ -1840,7 +1198,7 @@ Instances per view: Properties: * `pos` - type: NORMALIZED_PAIR - - Default is `0 0.2` + - Default is `0 0.1` * `size` - type: NORMALIZED_PAIR - Default is `1 0.8` * `origin` - type: NORMALIZED_PAIR @@ -1920,7 +1278,690 @@ Properties: - z-index value for element. Elements will be rendered in order of zIndex value from low to high. - Default is `50` -### gameselector +### Secondary elements + +Elements from this group can occur an unlimited number of times and they take care of displaying the bulk of the theme configuration such as text, images, videos, animations etc. + +#### image + +Displays a raster image or a scalable vector graphics (SVG) image. + +Supported views: +* `system ` +* `gamelist` + +Instances per view: +* `unlimited` + +Properties: +* `pos` - type: NORMALIZED_PAIR +* `size` - type: NORMALIZED_PAIR + - If only one axis is specified (and the other is zero), then the other axis will be automatically calculated in accordance with the image's aspect ratio. Setting both axes to 0 is an error and the size will be clamped to `0.001 0.001` in this case. + - Minimum value per axis is `0.001` and maximum value per axis is `3`. If specifying a value outside the allowed range then no attempt will be made to preserve the aspect ratio. +* `maxSize` - type: NORMALIZED_PAIR + - The image will be resized as large as possible so that it fits within this size while maintaining its aspect ratio. Use this instead of `size` when you don't know what kind of image you're using so it doesn't get grossly oversized on one axis (e.g. with a game's image metadata). Although this property is possible to combine with the `tile` property that does not make a whole lot of sense, instead use the `size` property for tiled images. + - Minimum value per axis is `0.001` and maximum value per axis is `3` +* `origin` - type: NORMALIZED_PAIR + - Where on the element `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the element exactly in the middle of the screen. If the position and size attributes are themeable, origin is implied. + - Minimum value per axis is `0` and maximum value per axis is `1` + - Default is `0 0` +* `rotation` - type: FLOAT + - Angle in degrees that the image should be rotated. Positive values will rotate clockwise, negative values will rotate counterclockwise. + - Default is `0` +* `rotationOrigin` - type: NORMALIZED_PAIR + - Point around which the image will be rotated. + - Minimum value per axis is `0` and maximum value per axis is `1` + - Default is `0.5 0.5` +* `path` - type: PATH + - Explicit path to an image file. Most common extensions are supported (including .jpg, .png, and unanimated .gif). If `imageType` is also defined then this will take precedence as these two properties are not intended to be used together. If you need a fallback image in case of missing game media, use the `default` property instead. +* `default` - type: PATH + - Path to a default image file. The default image will be displayed when the selected game does not have an image of the type defined by the `imageType` property (i.e. this `default` property does nothing unless a valid `imageType` property has been set). It's also applied to any custom collection that does not contain any games when browsing the grouped custom collections system. +* `imageType` - type: STRING + - This displays a game image of a certain media type. Multiple types can be defined, in which case the entries should be delimited by commas or by whitespace characters (tabs, spaces or line breaks). The media will be searched for in the order that the entries have been defined. If no image is found, then the space will be left blank unless the `default` property has been set. To use this property from the `system` view, you will first need to add a `gameselector` element. Defining duplicate values is considered an error and will result in the property getting ignored. + - Valid values: + - `image` - This will look for a `miximage`, and if that is not found `screenshot` is tried next, then `titlescreen` and finally `cover`. This is just a convenient shortcut and it's equivalent to explicitly defining `miximage, screenshot, titlescreen, cover` + - `miximage` - This will look for a miximage. + - `marquee` - This will look for a marquee (wheel) image. + - `screenshot` - This will look for a screenshot image. + - `titlescreen` - This will look for a title screen image. + - `cover` - This will look for a box front cover image. + - `backcover` - This will look for a box back cover image. + - `3dbox` - This will look for a 3D box image. + - `physicalmedia` - This will look for a physical media image. + - `fanart` - This will look for a fan art image. +* `metadataElement` - type: BOOLEAN + - By default game metadata and media are faded out during gamelist fast-scrolling and text metadata fields, ratings and badges are hidden when enabling the _Hide metadata fields_ setting for a game entry. Using this property it's possible to explicitly define additional image elements that should be treated as if they were game media files. This is for example useful for hiding and fading out image elements that are used as indicator icons for the various metadata types like genre, publisher, players etc. It's however not possible to do the opposite, i.e. to disable this functionality for the default game media types as that would break basic application behavior. + - Default is `false` +* `gameselector` - type: STRING + - If more than one gameselector element has been defined, this property makes it possible to state which one to use. If multiple gameselector elements have been defined and this property is missing then the first entry will be chosen and a warning message will be logged. If only a single gameselector has been defined, this property is ignored. The value of this property must match the `name` attribute value of the gameselector element. This property is only needed for the `system` view and only if the `imageType` property is utilized. +* `tile` - type: BOOLEAN + - If true, the image will be tiled instead of stretched to fit its size. Useful for backgrounds. + - Default is `false` +* `tileSize` - type: NORMALIZED_PAIR + - Size of the individual images making up the tile as opposed to the overall size for the element which is defined by the `size` property. If only one axis is specified (and the other is zero), then the other axis will be automatically calculated in accordance with the image's aspect ratio. Setting both axes to 0 is an error and tiling will be disabled in this case. If this property is omitted, then the size will be set to the actual image dimensions. For SVG images this means whatever canvas size has been defined inside the file. + - Minimum value per axis is `0` and maximum value per axis is `1`. +* `tileHorizontalAlignment` - type: STRING + - If the images making up the tiled texture do not match precisely with the edges of the overall element, then this property can be used to define the alignment on the horizontal axis. + - Valid values are `left` or `right` + - Default is `left` +* `tileVerticalAlignment` - type: STRING + - If the images making up the tiled texture do not match precisely with the edges of the overall element, then this property can be used to define the alignment on the vertical axis. + - Valid values are `top` or `bottom` + - Default is `bottom` +* `interpolation` - type: STRING + - Interpolation method to use when scaling. Nearest neighbor (`nearest`) preserves sharp pixels and linear filtering (`linear`) makes the image smoother. This property has limited effect on scalable vector graphics (SVG) images unless rotation is applied. + - Valid values are `nearest` or `linear` + - Default is `nearest` +* `color` - type: COLOR + - Multiply each pixel's color by this color. For example, an all-white image with `FF0000` would become completely red. You can also control the transparency of an image with `FFFFFFAA` - keeping all the pixels their normal color and only affecting the alpha channel. +* `colorEnd` - type: COLOR + - Works exactly in the same way as `color` but can be set as the end color to apply a color shift gradient to the image. +* `gradientType` - type: STRING + - The direction to apply the color shift gradient if both `color` and `colorEnd` have been defined. + - Valid values are `horizontal` or `vertical` + - Default is `horizontal` +* `scrollFadeIn` - type: BOOLEAN + - If enabled, a short fade-in animation will be applied when scrolling through games in the gamelist view. This usually looks best if used for the main game image. + - Default is `false` +* `opacity` - type: FLOAT + - Controls the level of transparency. If set to `0` the element will be disabled. + - Minimum value is `0` and maximum value is `1` + - Default is `1` +* `saturation` - type: FLOAT + - Controls the level of color saturation. + - Minimum value is `0` (grayscale) and maximum value is `1` (original file saturation). + - Default is `1` +* `visible` - type: BOOLEAN + - If set to false, the element will be disabled. This is equivalent to setting `opacity` to `0` + - Default is `true` +* `zIndex` - type: FLOAT + - z-index value for element. Elements will be rendered in order of zIndex value from low to high. + - Default is `30` + +#### video + +Plays a video and provides support for displaying a static image for a defined time period before starting the video player. Although an unlimited number of videos could in theory be defined per view it's recommended to keep it at a single instance as playing videos takes a lot of CPU resources. But if still going for multiple videos, make sure to use the `audio` property to disable audio on all but one video as ES-DE currently has no audio mixing capabilities so the sound would not play correctly. To use videos in the `system` view, you either need to set a static video using the `path` property, or you need to create a `gameselector` element so game videos can be used. + +Supported views: +* `system ` +* `gamelist` + +Instances per view: +* `unlimited` (but recommended to keep at a single instance) + +Properties: +* `pos` - type: NORMALIZED_PAIR +* `size` - type: NORMALIZED_PAIR + - If only one axis is specified (and the other is zero), then the other will be automatically calculated in accordance with the static image's aspect ratio and the video's aspect ratio. Setting both axes to 0 is an error and the size will be clamped to `0.01 0.01` in this case. + - Minimum value per axis is `0.01` and maximum value per axis is `2`. If specifying a value outside the allowed range then no attempt will be made to preserve the aspect ratio. +* `maxSize` - type: NORMALIZED_PAIR + - The static image and video will be resized as large as possible so that they fit within this size while maintaining their aspect ratios. Use this instead of `size` when you don't know what kind of video you're using so it doesn't get grossly oversized on one axis (e.g. with a game's video metadata). + - Minimum value per axis is `0.01` and maximum value per axis is `2` +* `origin` - type: NORMALIZED_PAIR + - Where on the element `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the element exactly in the middle of the screen. If the position and size attributes are themeable, origin is implied. + - Minimum value per axis is `0` and maximum value per axis is `1` + - Default is `0 0` +* `path` - type: PATH + - Path to a video file. Setting a value for this property will make the video static, i.e. any `imageType`, `gameselector` and `default` properties will be ignored. +* `default` - type: PATH + - Path to a default video file. The default video will be played when the selected game does not have a video. This property is also applied to any custom collection that does not contain any games when browsing the grouped custom collections system. +* `defaultImage` - type: PATH + - Path to a default image file. The default image will be displayed when the selected game does not have an image of the type defined by the `imageType` property (i.e. this `default` property does nothing unless a `imageType` property has been set). It's also applied to any custom collection that does not contain any games when browsing the grouped custom collections system. +* `imageType` - type: STRING + - This displays a game image of a certain media type. Multiple types can be defined, in which case the entries should be delimited by commas or by whitespace characters (tabs, spaces or line breaks). The media will be searched for in the order that the entries have been defined. If no image is found, then the space will be left blank unless the `default` property has been set. To use this property from the `system` view, you will first need to add a `gameselector` element. If `delay` is set to zero, then this property is ignored. Defining duplicate values is considered an error and will also result in the property getting ignored. + - Valid values: + - `image` - This will look for a `miximage`, and if that is not found `screenshot` is tried next, then `titlescreen` and finally `cover`. This is just a convenient shortcut and it's equivalent to explicitly defining `miximage, screenshot, titlescreen, cover` + - `miximage` - This will look for a miximage. + - `marquee` - This will look for a marquee (wheel) image. + - `screenshot` - This will look for a screenshot image. + - `titlescreen` - This will look for a title screen image. + - `cover` - This will look for a box front cover image. + - `backcover` - This will look for a box back cover image. + - `3dbox` - This will look for a 3D box image. + - `physicalmedia` - This will look for a physical media image. + - `fanart` - This will look for a fan art image. +* `metadataElement` - type: BOOLEAN + - By default game metadata and media are faded out during gamelist fast-scrolling and text metadata fields, ratings and badges are hidden when enabling the _Hide metadata fields_ setting for a game entry. Using this property it's possible to explicitly define static video elements that should be treated as if they were game media files. This property is ignored if `path` is not set. + - Default is `false` +* `gameselector` - type: STRING + - If more than one gameselector element has been defined, this property makes it possible to state which one to use. If multiple gameselector elements have been defined and this property is missing then the first entry will be chosen and a warning message will be logged. If only a single gameselector has been defined, this property is ignored. The value of this property must match the `name` attribute value of the gameselector element. +* `audio` - type: BOOLEAN + - Whether to enable or disable audio playback for the video. For static videos in the gamelist view it's strongly recommended to set this to `false` if there is also a separate video element playing game videos. + - Default is `true` +* `interpolation` - type: STRING + - Interpolation method to use when scaling raster images. Nearest neighbor (`nearest`) preserves sharp pixels and linear filtering (`linear`) makes the image smoother. Note that this property only affects the static image, not the video scaling. This property also has no effect on scalable vector graphics (SVG) images. + - Valid values are `nearest` or `linear` + - Default is `nearest` +* `pillarboxes` - type: BOOLEAN + - Whether to render black pillarboxes (and to a lesses extent letterboxes) for videos with aspect ratios where this is applicable. This is for instance useful for arcade game videos in vertical orientation. + - Default is `true` +* `pillarboxThreshold` - type: NORMALIZED_PAIR + - Normally it doesn't look very good to add really narrow pillarboxes or letterboxes, so by default they are skipped if the actual video size is not reaching a threshold value as compared to the overall defined video area size. By modifying this property it's possible to control that threshold, as for some theme designs it will look better with the consistency of always rendering the pillarboxes/letterboxes even if they are narrow. To clarify, the default X axis value of 0.85 means that if the video width is 85% or less as compared to the X axis defined by the `size` property, then pillarboxes will be rendered. So setting the `pillarboxThreshold` value to `1 1` will always apply pillarboxes/letterboxes regardless of the video file dimension. + - Minimum value per axis is `0.2` and maximum value per axis is `1` + - Default is `0.85 0.90` +* `scanlines` - type: BOOLEAN + - Whether to use a shader to render scanlines. + - Default is `false` +* `delay` - type: FLOAT + - Delay in seconds before video will start playing. During the delay period the game image defined via the `imageType` property will be displayed. If that property is not set, then the `delay` property will be ignored. + - Minimum value is `0` and maximum value is `15` + - Default is `1.5` +* `fadeInTime` - type: FLOAT + - Time in seconds to fade in the video from pure black. This is completely unrelated to the `scrollFadeIn` property. Note that if this is set to zero it may seem as if the property doesn't work correctly as many ScreenScraper videos have a fade-in baked into the actual video stream. Setting this property to lower than 0.3 seconds or so is generally a bad idea for videos that don't have a fade-in baked in as transitions from the static image will then look like a bad jump cut. + - Minimum value is `0` and maximum value is `8` + - Default is `1` +* `scrollFadeIn` - type: BOOLEAN + - If enabled, a short fade-in animation will be applied when scrolling through games in the gamelist view. This animation is only applied to images and not to actual videos, so if no image metadata has been defined then this property has no effect. For this to work correctly the `delay` property also needs to be set. + - Default is `false` +* `opacity` - type: FLOAT + - Controls the level of transparency. If set to `0` the element will be disabled. + - Minimum value is `0` and maximum value is `1` + - Default is `1` +* `saturation` - type: FLOAT + - Controls the level of color saturation. This affects both the static image and the video stream. + - Minimum value is `0` (grayscale) and maximum value is `1` (original file saturation). + - Default is `1` +* `visible` - type: BOOLEAN + - If set to false, the element will be disabled. This is equivalent to setting `opacity` to `0` + - Default is `true` +* `zIndex` - type: FLOAT + - z-index value for element. Elements will be rendered in order of zIndex value from low to high. + - Default is `30` + +#### animation + +GIF and Lottie (vector graphics) animations. The type of animation is automatically selected based on the file extension with `.gif` for GIF animations and `.json` for Lottie animations. Note that Lottie animations take a lot of memory and CPU resources if scaled up to large sizes so it's adviced to not add too many of them to the same view and to not make them too large. GIF animations on the other hand are not as demanding except if they're really long and/or high-resolution. + +Supported views: +* `system ` +* `gamelist` + +Instances per view: +* `unlimited` + +Properties: +* `pos` - type: NORMALIZED_PAIR +* `size` - type: NORMALIZED_PAIR + - If only one axis is specified (and the other is zero), the other will be automatically calculated in accordance with the animation's aspect ratio. Note that this is sometimes not entirely accurate as some animations contain invalid size information. +* `origin` - type: NORMALIZED_PAIR + - Where on the element `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the element exactly in the middle of the screen. If the position and size attributes are themeable, origin is implied. + - Minimum value per axis is `0` and maximum value per axis is `1` + - Default is `0 0` +* `rotation` - type: FLOAT + - Angle in degrees that the animation should be rotated. Positive values will rotate clockwise, negative values will rotate counterclockwise. + - Default is `0` +* `rotationOrigin` - type: NORMALIZED_PAIR + - Point around which the animation will be rotated. + - Minimum value per axis is `0` and maximum value per axis is `1` + - Default is `0.5 0.5` +* `metadataElement` - type: BOOLEAN + - By default game metadata and media are faded out during gamelist fast-scrolling and text metadata fields, ratings and badges are hidden when enabling the _Hide metadata fields_ setting for a game entry. Using this property it's possible to explicitly define animation elements that should be treated as if they were game media files. This is for example useful for hiding and fading out animations that are used as indicators for the various metadata types like genre, publisher, players etc. + - Default is `false` +* `path` - type: PATH + - Path to the animation file. Only the .json extension is supported. +* `speed` - type: FLOAT. + - The relative speed at which to play the animation. + - Minimum value is `0.2` and maximum value is `3` + - Default is `1` +* `direction` - type: STRING + - The direction that the animation should be played. Valid values are `normal` (forwards), `reverse` (backwards), `alternate` (bouncing forwards/backwards) and `alternateReverse` (bouncing backwards/forwards, i.e. starting with playing backwards). + - Default is `normal` +* `keepAspectRatio` - type: BOOLEAN. + - If true, aspect ratio will be preserved. If false, animation will stretch to the defined size. Note that setting to `false` is incompatible with only defining one of the axes for the `size` element. + - Default is `true` +* `interpolation` - type: STRING + - Interpolation method to use when scaling GIF animations. Nearest neighbor (`nearest`) preserves sharp pixels and linear filtering (`linear`) makes the image smoother. This property has no effect on Lottie animations. + - Valid values are `nearest` or `linear` + - Default is `nearest` +* `opacity` - type: FLOAT + - Controls the level of transparency. If set to `0` the element will be disabled. + - Minimum value is `0` and maximum value is `1` + - Default is `1` +* `saturation` - type: FLOAT + - Controls the level of color saturation. + - Minimum value is `0` (grayscale) and maximum value is `1` (original file saturation). + - Default is `1` +* `visible` - type: BOOLEAN + - If set to false, the element will be disabled. This is equivalent to setting `opacity` to `0` + - Default is `true` +* `zIndex` - type: FLOAT + - z-index value for element. Elements will be rendered in order of zIndex value from low to high. + - Default is `35` + +#### badges + +Displays graphical symbols representing a number of metadata fields for the currently selected game. It's strongly recommended to use the same image dimensions for all badges as varying aspect ratios will lead to alignment issues. For the controller images it's recommended to keep to the square canvas size used by the default bundled graphics as otherwise sizing and placement will be inconsistent (unless all controller graphic files are customized of course). + +Supported views: +* `gamelist` + +Instances per view: +* `unlimited` + +Properties: +* `pos` - type: NORMALIZED_PAIR +* `size` - type: NORMALIZED_PAIR + - Possible combinations: + - `w h` - Dimensions of the badges container. The badges will be scaled to fit within these dimensions. + - Minimum value per axis is `0.03` and maximum value per axis is `1` + - Default is `0.15 0.20` +* `origin` - type: NORMALIZED_PAIR + - Where on the element `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the element exactly in the middle of the screen. If the position and size attributes are themeable, origin is implied. + - Minimum value per axis is `0` and maximum value per axis is `1` + - Default is `0 0` +* `rotation` - type: FLOAT + - Angle in degrees that the badges should be rotated. Positive values will rotate clockwise, negative values will rotate counterclockwise. + - Default is `0` +* `rotationOrigin` - type: NORMALIZED_PAIR + - Point around which the image will be rotated. + - Minimum value per axis is `0` and maximum value per axis is `1` + - Default is `0.5 0.5`. +* `horizontalAlignment` - type: STRING. + - Valid values are `left` or `right` +* `direction` - type: STRING + - Valid values are "row" or "column". Controls the primary layout direction (line axis) for the badges. Lines will fill up in the specified direction. + - Default is `row` +* `lines` - type: UNSIGNED_INTEGER + - The number of lines available. + - Default is `3` +* `itemsPerLine` - type: UNSIGNED_INTEGER + - Number of badges that fit on a line. When more badges are available a new line will be started. + - Default is `4` +* `itemMargin` - type: NORMALIZED_PAIR + - The horizontal and vertical margins between badges - `x y` + - If one of the axis is set to `-1` the margin of the other axis (in pixels) will be used, which makes it possible to get identical spacing between all items regardless of screen aspect ratio. + - Minimum value per axis is `0` and maximum value per axis is `0.2` + - Default is `0.01 0.01`. +* `slots` - type: STRING + - The badge types that should be displayed. Specified as a list of strings delimited by commas or by whitespace characters (tabs, spaces or line breaks). The order in which they are defined will be followed when placing badges on screen. Available badges are: + - `collection` - Will be shown when editing a custom collection and the current entry is part of that collection. + - `folder` - Will be shown when the current entry is a folder. If a folder link has been setup, then a configurable link icon will overlay this badge. + - `favorite` - Will be shown when the game is marked as favorite. + - `completed` - Will be shown when the game is marked as completed. + - `kidgame` - Will be shown when the game is marked as a kids game. + - `broken` - Will be shown when the game is marked as broken. + - `controller` - Will be shown and overlaid by the corresponding controller icon if a controller type has been selected for the game (using the metadata editor or via scraping). + - `altemulator` - Will be shown when an alternative emulator is setup for the game. + - `all` - Including this value will enable all badges. If some badges have been added already they will be shown in the order they were defined and the remaining ones will be added at the end, in the order listed above. Using the `all` value can be used as a way to future-proof the theme, because if additional badges are added in future ES-DE releases, no theme updates would be needed to accomodate them. Just make sure to include space for a few extra badges in the layout, and increase the `lines` and `itemsPerLine` accordingly. +* `controllerPos` - type: NORMALIZED_PAIR + - The position of the controller icon relative to the parent `controller` badge. + - Minimum value per axis is `-1` and maximum value per axis is `2` + - Default is `0.5 0.5` which centers the controller icon on the badge. +* `controllerSize` - type: FLOAT + - The size of the controller icon relative to the parent `controller` badge. + - Setting the value to `1` sizes the icon to the same width as the parent badge. The image aspect ratio is always maintained. + - Minimum value is `0.1` and maximum value is `2` + - Default is `0.5` +* `customBadgeIcon` - type: PATH + - A badge icon override. Specify the badge type in the attribute `badge`. The available badges are the ones listed above. +* `customControllerIcon` - type: PATH + - A controller icon override. Specify the controller type in the attribute `controller`. + - These are the available types: + - `gamepad_generic`, + `gamepad_nintendo_nes`, + `gamepad_nintendo_snes`, + `gamepad_nintendo_64`, + `gamepad_playstation`, + `gamepad_sega_md_3_buttons`, + `gamepad_sega_md_6_buttons`, + `gamepad_xbox`, + `joystick_generic`, + `joystick_arcade_no_buttons`, + `joystick_arcade_1_button`, + `joystick_arcade_2_buttons`, + `joystick_arcade_3_buttons`, + `joystick_arcade_4_buttons`, + `joystick_arcade_5_buttons`, + `joystick_arcade_6_buttons`, + `keyboard_generic`, + `keyboard_and_mouse_generic`, + `mouse_generic`, + `mouse_amiga`, + `lightgun_generic`, + `lightgun_nintendo`, + `steering_wheel_generic`, + `flight_stick_generic`, + `spinner_generic`, + `trackball_generic`, + `wii_remote_nintendo`, + `wii_remote_and_nunchuk_nintendo`, + `joycon_left_or_right_nintendo`, + `joycon_pair_nintendo`, + `xbox_kinect`, + `unknown` +* `folderLinkPos` - type: NORMALIZED_PAIR + - The position of the folder link icon relative to the parent `folder` badge. + - Minimum value per axis is `-1` and maximum value per axis is `2` + - Default is `0.5 0.5` which centers the folder link icon on the badge. +* `folderLinkSize` - type: FLOAT + - The size of the folder link icon relative to the parent `folder` badge. + - Setting the value to `1` sizes the icon to the same width as the parent badge. The image aspect ratio is always maintained. + - Minimum value is `0.1` and maximum value is `1` + - Default is `0.5` +* `customFolderLinkIcon` - type: PATH + - Folder link icon override. +* `opacity` - type: FLOAT + - Controls the level of transparency. If set to `0` the element will be disabled. + - Minimum value is `0` and maximum value is `1` + - Default is `1` +* `visible` - type: BOOLEAN + - If set to false, the element will be disabled. This is equivalent to setting `opacity` to `0` + - Default is `true` +* `zIndex` - type: FLOAT + - z-index value for element. Elements will be rendered in order of zIndex value from low to high. + - Default is `35` + +#### text + +Displays text. This can be literal strings or values based on game metadata or system variables, as described below. For the `gamelist` view it's also possible to place the text inside a scrollable container which is for example useful for longer texts like the game descriptions. + +Supported views: +* `system` +* `gamelist` + +Instances per view: +* `unlimited` + +Properties: +* `pos` - type: NORMALIZED_PAIR +* `size` - type: NORMALIZED_PAIR + - Possible combinations: + - `0 0` - automatically size so text fits on one line (expanding horizontally). + - `w 0` - automatically wrap text so it doesn't go beyond `w` (expanding vertically). + - `w h` - works like a "text box". If `h` is non-zero and `h` <= `fontSize` (implying it should be a single line of text), text that goes beyond `w` will be truncated with an elipses (...). +* `origin` - type: NORMALIZED_PAIR + - Where on the element `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the element exactly in the middle of the screen. If the position and size attributes are themeable, origin is implied. + - Minimum value per axis is `0` and maximum value per axis is `1` + - Default is `0 0` +* `rotation` - type: FLOAT + - Angle in degrees that the text should be rotated. Positive values will rotate clockwise, negative values will rotate counterclockwise. Rotation is not possible if the `container` property has been set to true. + - Default is `0` +* `rotationOrigin` - type: NORMALIZED_PAIR + - Point around which the text will be rotated. + - Minimum value per axis is `0` and maximum value per axis is `1` + - Default is `0.5 0.5` +* `text` - type: STRING + - A string literal to display. +* `systemdata` - type: STRING + - This translates to some system data including values defined in es_systems.xml as well as some statistics. This property can only be used in the `system` view and you can only define a single value per element. + - Valid values: + - `name` - Short system name as defined in es_systems.xml. + - `fullname` - Full system name as defined in es_systems.xml. + - `gamecount` - Number of games available for the system. Number of favorites are printed inside brackets if applicable. + - `gamecount_games` - Number of games available for the system. Does not print the favorites count. + - `gamecount_favorites` - Number of favorite games for the system, may be blank if favorites are not applicable. +* `metadata` - type: STRING + - This translates to the metadata values that are available for the game. To use this property from the `system` view, you will first need to add a `gameselector` element. You can only define a single metadata value per text element. + - Valid values: + - `name` - Game name. + - `description` - Game description. Should be combined with the `container` property in most cases. + - `rating` - The numerical representation of the game rating, for example `3` or `4.5`. + - `developer` - Developer. + - `publisher` - Publisher. + - `genre` - Genre. + - `players` - The number of players. + - `favorite` - Whether the game is a favorite. Will be printed as either `yes` or `no`. + - `completed` - Whether the game has been completed. Will be printed as either `yes` or `no`. + - `kidgame` - Whether the game is suitable for children. Will be printed as either `yes` or `no`. + - `broken` - Whether the game is broken/not working. Will be printed as either `yes` or `no`. + - `playcount` - How many times the game has been played. + - `controller` - The controller for the game. Will be blank if none has been selected. + - `altemulator` - The alternative emulator for the game. Will be blank if none has been selected. +* `metadataElement` - type: BOOLEAN + - By default game metadata and media are faded out during gamelist fast-scrolling and text metadata fields, ratings and badges are hidden when enabling the _Hide metadata fields_ setting for a game entry. Using this property it's possible to explicitly define additional text elements that should be treated as if they were game metadata entries. This is for example useful for hiding and fading out text labels for the various metadata types like genre, publisher, players etc. Note that it's not possible to disable the metadata hiding functionality for the default metadata fields as that would break basic application behavior. Also note that there is a slight exception to the hiding logic for text containers with the metadata value set to `description`. In this case the element is by default not hidden when enabling the _Hide metadata fields_ setting. To also hide such containers, set this property to true. + - Default is `false` +* `gameselector` - type: STRING + - If more than one gameselector element has been defined, this property makes it possible to state which one to use. If multiple gameselector elements have been defined and this property is missing then the first entry will be chosen and a warning message will be logged. If only a single gameselector has been defined, this property is ignored. The value of this property must match the `name` attribute value of the gameselector element. This property is only needed for the `system` view and only if the `metadata` property is utilized. +* `container` - type: BOOLEAN + - Whether the text should be placed inside a scrollable container. Only available for the `gamelist` view. + - Default is `false` +* `containerVerticalSnap` - type: BOOLEAN + - Whether the text should be vertically snapped to the font height. With this property enabled the container will have its height reduced as needed so that only complete rows of text are displayed at the start and end positions. This will not affect the "real" size of the container as set by the `size` property which means that the overall element placement will still be predictable if a vertical origin other than zero is used. + - Default is `true` +* `containerScrollSpeed` - type: FLOAT + - A base speed is automatically calculated based on the container and font sizes, so this property applies relative to the auto-calculated value. + - Minimum value is `0.1` and maximum value is `10` + - Default is `1` +* `containerStartDelay` - type: FLOAT + - Delay in seconds before scrolling starts. Note that the text fade-in animation that plays when resetting from the end position will cause a slight delay even if this property is set to zero. + - Minimum value is `0` and maximum value is `10` + - Default is `4.5` +* `containerResetDelay` - type: FLOAT + - Delay in seconds before resetting to the start position after reaching the scrolling end position. + - Minimum value is `0` and maximum value is `20` + - Default is `7` +* `fontPath` - type: PATH + - Path to a TrueType font (.ttf). +* `fontSize` - type: FLOAT + - Size of the font as a percentage of screen height (e.g. for a value of `0.1`, the text's height would be 10% of the screen height). This calculation is based on the reference 'S' character so other glyphs may not fill this area, or they may exceed this area. + - Minimum value is `0.001` and maximum value is `1.5`. Note that when running at a really low resolution, the minimum value may get clamped to a larger relative size. The font is allowed to overflow the height of the element by 100%, i.e. `fontSize` can be set to twice that of the y axis of the `size` property. Any value above that will be clamped. + - Default is `0.045` +* `horizontalAlignment` - type: STRING + - Controls alignment on the X axis. + - Valid values are `left`, `center` or `right` + - Default is `left` +* `verticalAlignment` - type: STRING + - Controls alignment on the Y axis. + - Valid values are `top`, `center` or `bottom` + - Default is `center` +* `color` - type: COLOR +* `backgroundColor` - type: COLOR +* `letterCase` - type: STRING + - Valid values are `none`, `uppercase`, `lowercase` or `capitalize` + - Default is `none` (original letter case is retained) +* `lineSpacing` - type: FLOAT + - Controls the space between lines (as a multiple of the font height). Due to the imprecise nature of typefaces where certain glyphs (characters) may exceed the requested font size, it's recommended to keep this value at around `1.1` or higher for multi-line text fields. This way overlapping glyphs or characters being cut off at the top or bottom will be prevented. + - Minimum value is `0.5` and maximum value is `3` + - Default is `1.5` +* `opacity` - type: FLOAT + - Controls the level of transparency. If set to `0` the element will be disabled. + - Minimum value is `0` and maximum value is `1` + - Default is `1` +* `visible` - type: BOOLEAN + - If set to false, the element will be disabled. This is equivalent to setting `opacity` to `0` + - Default is `true` +* `zIndex` - type: FLOAT + - z-index value for element. Elements will be rendered in order of zIndex value from low to high. + - Default is `40` + +#### datetime + +Displays a date and time as a text string. The format is ISO 8601 (YYYY-MM-DD) by default, but this can be changed using the `format` property. The text _unknown_ will be shown by default if there is no time stamp available. If the property `displayRelative` has been set, the text will be shown as _never_ in case of no time stamp. + +Supported views: +* `system` +* `gamelist` + +Instances per view: +* `unlimited` + +Properties: +* `pos` - type: NORMALIZED_PAIR +* `size` - type: NORMALIZED_PAIR + - Possible combinations: + - `0 0` - automatically size so text fits on one line (expanding horizontally). + - `w 0` - automatically wrap text so it doesn't go beyond `w` (expanding vertically). + - `w h` - works like a "text box". If `h` is non-zero and `h` <= `fontSize` (implying it should be a single line of text), text that goes beyond `w` will be truncated with an elipses (...). +* `origin` - type: NORMALIZED_PAIR + - Where on the element `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the element exactly in the middle of the screen. If the position and size attributes are themeable, origin is implied. + - Minimum value per axis is `0` and maximum value per axis is `1` + - Default is `0 0` +* `rotation` - type: FLOAT + - Angle in degrees that the text should be rotated. Positive values will rotate clockwise, negative values will rotate counterclockwise. + - Default is `0` +* `rotationOrigin` - type: NORMALIZED_PAIR + - Point around which the text will be rotated. + - Minimum value per axis is `0` and maximum value per axis is `1` + - Default is `0.5 0.5`. +* `metadata` - type: STRING + - This displays the metadata values that are available for the game. If an invalid metadata field is defined, the text "unknown" will be printed. To use this property from the `system` view, you will first need to add a `gameselector` element. You can only define a single metadata value per datetime element. + - Valid values: + - `releasedate` - The release date of the game. + - `lastplayed` - The time the game was last played. This will be displayed as a value relative to the current date and time by default, but can be overridden using the `displayRelative` property. +* `gameselector` - type: STRING + - If more than one gameselector element has been defined, this property makes it possible to state which one to use. If multiple gameselector elements have been defined and this property is missing then the first entry will be chosen and a warning message will be logged. If only a single gameselector has been defined, this property is ignored. The value of this property must match the `name` attribute value of the gameselector element. This property is only needed for the `system` view and only if the `metadata` property is utilized. +* `fontPath` - type: PATH + - Path to a TrueType font (.ttf). +* `fontSize` - type: FLOAT + - Size of the font as a percentage of screen height (e.g. for a value of `0.1`, the text's height would be 10% of the screen height). This calculation is based on the reference 'S' character so other glyphs may not fill this area, or they may exceed this area. + - Minimum value is `0.001` and maximum value is `1.5`. Note that when running at a really low resolution, the minimum value may get clamped to a larger relative size. The font is allowed to overflow the height of the element by 100%, i.e. `fontSize` can be set to twice that of the y axis of the `size` property. Any value above that will be clamped. + - Default is `0.045` +* `horizontalAlignment` - type: STRING + - Controls alignment on the X axis. + - Valid values are `left`, `center` or `right` + - Default is `left` +* `verticalAlignment` - type: STRING + - Controls alignment on the Y axis. + - Valid values are `top`, `center` or `bottom` + - Default is `center` +* `color` - type: COLOR +* `backgroundColor` - type: COLOR +* `letterCase` - type: STRING + - Valid values are `none`, `uppercase`, `lowercase` or `capitalize` + - Default is `none` (original letter case is retained) +* `lineSpacing` - type: FLOAT + - Controls the space between lines (as a multiple of font height). + - Minimum value is `0.5` and maximum value is `3` + - Default is `1.5` +* `format` - type: STRING + - Specifies the date and time format. Has no effect if `displayRelative` has been set to true. + - %Y: The year, including the century (1900) + - %m: The month number [01,12] + - %d: The day of the month [01,31] + - %H: The hour (24-hour clock) [00,23] + - %M: The minute [00,59] + - %S: The second [00,59] +* `displayRelative` - type: BOOLEAN. + - Renders the datetime as a relative string (e.g. 'x days ago'). + - Default is `false` +* `opacity` - type: FLOAT + - Controls the level of transparency. If set to `0` the element will be disabled. + - Minimum value is `0` and maximum value is `1` + - Default is `1` +* `visible` - type: BOOLEAN + - If set to false, the element will be disabled. This is equivalent to setting `opacity` to `0` + - Default is `true` +* `zIndex` - type: FLOAT + - z-index value for element. Elements will be rendered in order of zIndex value from low to high. + - Default is `40` + +#### gamelistinfo + +Displays the game count (all games as well as favorites), any applied filters, and a folder icon if a folder has been entered. If this text is left aligned or center aligned, the folder icon will be placed to the right of the other information, and if it's right aligned, the folder icon will be placed to the left. + +Supported views: +* `gamelist` + +Instances per view: +* `unlimited` + +Properties: +* `pos` - type: NORMALIZED_PAIR +* `size` - type: NORMALIZED_PAIR + - Possible combinations: + - `0 0` - automatically size so text fits on one line (expanding horizontally). + - `w 0` - automatically wrap text so it doesn't go beyond `w` (expanding vertically). + - `w h` - works like a "text box". If `h` is non-zero and `h` <= `fontSize` (implying it should be a single line of text), text that goes beyond `w` will be truncated with an elipses (...). +* `origin` - type: NORMALIZED_PAIR + - Where on the element `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the element exactly in the middle of the screen. If the position and size attributes are themeable, origin is implied. + - Minimum value per axis is `0` and maximum value per axis is `1` + - Default is `0 0` +* `rotation` - type: FLOAT + - Angle in degrees that the text should be rotated. Positive values will rotate clockwise, negative values will rotate counterclockwise. + - Default is `0` +* `rotationOrigin` - type: NORMALIZED_PAIR + - Point around which the element will be rotated. + - Minimum value per axis is `0` and maximum value per axis is `1` + - Default is `0.5 0.5` +* `fontPath` - type: PATH + - Path to a TrueType font (.ttf). +* `fontSize` - type: FLOAT + - Size of the font as a percentage of screen height (e.g. for a value of `0.1`, the text's height would be 10% of the screen height). This calculation is based on the reference 'S' character so other glyphs may not fill this area, or they may exceed this area. + - Minimum value is `0.001` and maximum value is `1.5`. Note that when running at a really low resolution, the minimum value may get clamped to a larger relative size. The font is allowed to overflow the height of the element by 100%, i.e. `fontSize` can be set to twice that of the y axis of the `size` property. Any value above that will be clamped. + - Default is `0.045` +* `horizontalAlignment` - type: STRING + - Controls alignment on the X axis. + - Valid values are `left`, `center` or `right` + - Default is `left` +* `verticalAlignment` - type: STRING + - Controls alignment on the Y axis. + - Valid values are `top`, `center` or `bottom` + - Default is `center` +* `color` - type: COLOR +* `backgroundColor` - type: COLOR +* `opacity` - type: FLOAT + - Controls the level of transparency. If set to `0` the element will be disabled. + - Minimum value is `0` and maximum value is `1` + - Default is `1` +* `visible` - type: BOOLEAN + - If set to false, the element will be disabled. This is equivalent to setting `opacity` to `0` + - Default is `true` +* `zIndex` - type: FLOAT + - z-index value for element. Elements will be rendered in order of zIndex value from low to high. + - Default is `45` + +#### rating + +Displays a graphical representation of the game rating, from 0 to 5. + +To display game ratings in the `system` view, you first need to create a `gameselector` element. + +Supported views: +* `system` +* `gamelist` + +Instances per view: +* `unlimited` + +Properties: +* `pos` - type: NORMALIZED_PAIR +* `size` - type: NORMALIZED_PAIR + - These values are mutually exclusive, if an X axis value is defined then the element will be sized based on this, and if an Y axis value is defined then the element will be sized based on that. If both the X and Y axis values are defined then the Y axis value will take precedence and the X axis value will be ignored. This makes sure that the image aspect ratio is always maintained. + - Minimum value per axis is `0.01` and maximum value for the X axis is `1` and maximum value for the Y axis is `0.5` + - Default is `0 0.06` +* `origin` - type: NORMALIZED_PAIR + - Where on the element `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the element exactly in the middle of the screen. If the position and size attributes are themeable, origin is implied. + - Minimum value per axis is `0` and maximum value per axis is `1` + - Default is `0 0` +* `rotation` - type: FLOAT + - Angle in degrees that the rating should be rotated. Positive values will rotate clockwise, negative values will rotate counterclockwise. + - Default is `0` +* `rotationOrigin` - type: NORMALIZED_PAIR + - Point around which the rating will be rotated. + - Minimum value per axis is `0` and maximum value per axis is `1` + - Default is `0.5 0.5` +* `gameselector` - type: STRING + - If more than one gameselector element has been defined, this property makes it possible to state which one to use. If multiple gameselector elements have been defined and this property is missing then the first entry will be chosen and a warning message will be logged. If only a single gameselector has been defined, this property is ignored. The value of this property must match the `name` attribute value of the gameselector element. This property is only needed for the `system` view. +* `interpolation` - type: STRING + - Interpolation method to use when scaling the images. Nearest neighbor (`nearest`) preserves sharp pixels and linear filtering (`linear`) makes the image smoother. The effect of this property is primarily visible for raster graphic images, but it has a limited effect also when using scalable vector graphics (SVG) images, and even more so if rotation is applied. + - Valid values are `nearest` or `linear` + - Default is `nearest` +* `color` - type: COLOR + - Multiply each pixel's color by this color. For example, an all-white image with `FF0000` would become completely red. You can also control the transparency of an image with `FFFFFFAA` - keeping all the pixels their normal color and only affecting the alpha channel. + - Default is `FFFFFFFF` +* `filledPath` - type: PATH + - Path to the "filled" rating image. Any aspect ratio is supported. Note that there is no explicit padding property, so to add spaces between each icon simply make the image content smaller on the canvas. The images should always be centered on the canvas or otherwise the filledPath and unfilledPath textures will not align properly for all rating values. Most common file extensions are supported (including .svg, .jpg, .png, and unanimated .gif). +* `unfilledPath` - type: PATH + - Path to the "unfilled" rating image. Any aspect ratio is supported. Note that there is no explicit padding property, so to add spaces between each icon simply make the image content smaller on the canvas. The images should always be centered on the canvas or otherwise the filledPath and unfilledPath textures will not align properly for all rating values. Most common file extensions are supported (including .svg, .jpg, .png, and unanimated .gif). +* `overlay` - type: BOOLEAN + - Whether to overlay the filledPath image on top of the unfilledPath image. If this property is set to false, then the unfilledPath image will only be rendered to the right of the rating value cut position. This property is useful for avoiding image aliasing artifacts that could otherwise occur when combining some rating images. It can also help with avoiding some inconsistent fade-out animations. + - Default is `true` +* `opacity` - type: FLOAT + - Controls the level of transparency. If set to `0` the element will be disabled. + - Minimum value is `0` and maximum value is `1` + - Default is `1` +* `visible` - type: BOOLEAN + - If set to false, the element will be disabled. This is equivalent to setting `opacity` to `0` + - Default is `true` +* `zIndex` - type: FLOAT + - z-index value for element. Elements will be rendered in order of zIndex value from low to high. + - Default is `45` + +### Special elements + +Elements from this group offer special functionality not covered by the primary and secondary elements. + +#### gameselector Selects games from the gamelists when navigating the `system` view. This makes it possible to display game media and game metadata directly from this view. It's possible to make separate gameselector configurations per game system, so that for instance a random game could be displayed for one system and the most recently played game could be displayed for another system. It's also possible to define multiple gameselector elements with different selection criterias per game system which makes it possible to for example set a random fan art background image and at the same time display a box cover image of the most recently played game. The gameselector logic can be used for the `image`, `video`, `text`, `datetime` and `rating` elements. @@ -1940,7 +1981,7 @@ Properties: - Minimum value is `1` and maximum value is `30` - Default is `1` -### helpsystem +#### helpsystem The helpsystem is a special element that displays a context-sensitive list of actions the user can take at any time. You should try and keep the position constant throughout every screen. Note that this element does not have a zIndex value, instead it's always rendered on top of all other elements. From 8062ebaf3a54d4f4bc12433637788cca6f2fa0ff Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sun, 13 Nov 2022 12:25:46 +0100 Subject: [PATCH 012/101] GridComponent now sets its default position and size if this is not defined by the theme configuration. --- es-core/src/components/primary/GridComponent.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index 972188f53..134b0f460 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -385,7 +385,13 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, const std::string& element, unsigned int properties) { + mSize.x = Renderer::getScreenWidth(); + mSize.y = Renderer::getScreenHeight() * 0.8; + GuiComponent::mPosition.x = 0.0f; + GuiComponent::mPosition.y = Renderer::getScreenHeight() * 0.1; + GuiComponent::applyTheme(theme, view, element, properties); + using namespace ThemeFlags; const ThemeData::ThemeElement* elem {theme->getElement(view, element, "grid")}; From 5ec0fda3a237ada60af6a7085a2fc86333ce3640 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sun, 13 Nov 2022 19:32:29 +0100 Subject: [PATCH 013/101] Implemented correct item rendering order in GridComponent. --- .../src/components/primary/GridComponent.h | 53 ++++++++++++++----- .../src/components/primary/PrimaryComponent.h | 2 +- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index 134b0f460..d1394cf15 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -114,6 +114,7 @@ private: bool mGamelistView; bool mLayoutValid; bool mRowJump; + bool mWasScrolling; }; template @@ -127,7 +128,7 @@ GridComponent::GridComponent() , mColumns {5} , mItemSize {glm::vec2 {Renderer::getScreenWidth() * 0.15f, Renderer::getScreenHeight() * 0.25f}} - , mItemScale {1.2f} + , mItemScale {1.05f} , mItemSpacing {glm::vec2 {Renderer::getScreenWidth() * 0.02f, Renderer::getScreenHeight() * 0.02f}} , mInstantItemTransitions {false} @@ -144,6 +145,7 @@ GridComponent::GridComponent() , mGamelistView {std::is_same_v ? true : false} , mLayoutValid {false} , mRowJump {false} + , mWasScrolling {false} { } @@ -356,24 +358,39 @@ template void GridComponent::render(const glm::mat4& parentTrans if (Settings::getInstance()->getBool("DebugImage")) mRenderer->drawRect(0.0f, 0.0f, mSize.x, mSize.y, 0x00FF0033, 0x00FF0033); - for (size_t i {0}; i < mEntries.size(); ++i) { - float opacity {mUnfocusedItemOpacity}; - float scale {1.0f}; + // We want to render the currently selected item last and before that the last selected + // item to avoid incorrect overlapping in case the element has been configured with for + // example large scaling or small or no margins between items. + std::vector renderEntries; - if (i == static_cast(mCursor)) { + for (size_t i {0}; i < mEntries.size(); ++i) { + if (i == static_cast(mCursor) || i == static_cast(mLastCursor)) + continue; + renderEntries.emplace_back(i); + } + + renderEntries.emplace_back(mLastCursor); + if (mLastCursor != mCursor) + renderEntries.emplace_back(mCursor); + + float opacity {mUnfocusedItemOpacity}; + float scale {1.0f}; + + for (auto it = renderEntries.cbegin(); it != renderEntries.cend(); ++it) { + if (*it == static_cast(mCursor)) { scale = glm::mix(1.0f, mItemScale, mTransitionFactor); - opacity = 1.0f - glm::mix(mUnfocusedItemOpacity, 0.0f, mTransitionFactor); + opacity = glm::mix(mUnfocusedItemOpacity, 1.0f, mTransitionFactor); } - else if (i == static_cast(mLastCursor)) { + else if (*it == static_cast(mLastCursor)) { scale = glm::mix(mItemScale, 1.0f, mTransitionFactor); opacity = glm::mix(1.0f, mUnfocusedItemOpacity, mTransitionFactor); } - mEntries.at(i).data.item->setScale(scale); - mEntries.at(i).data.item->setOpacity(opacity); - mEntries.at(i).data.item->render(trans); - mEntries.at(i).data.item->setScale(1.0f); - mEntries.at(i).data.item->setOpacity(1.0f); + mEntries.at(*it).data.item->setScale(scale); + mEntries.at(*it).data.item->setOpacity(opacity); + mEntries.at(*it).data.item->render(trans); + mEntries.at(*it).data.item->setScale(1.0f); + mEntries.at(*it).data.item->setOpacity(1.0f); } GuiComponent::renderChildren(trans); @@ -437,6 +454,16 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, template void GridComponent::onCursorChanged(const CursorState& state) { + if (mWasScrolling && state == CursorState::CURSOR_STOPPED) { + if (mCursorChangedCallback) + mCursorChangedCallback(state); + mWasScrolling = false; + return; + } + + if (mCursor == mLastCursor) + return; + float startPos {mEntryOffset}; float posMax {static_cast(mEntries.size())}; float target {static_cast(mCursor)}; @@ -517,6 +544,8 @@ template void GridComponent::onCursorChanged(const CursorState& if (mCursorChangedCallback) mCursorChangedCallback(state); + + mWasScrolling = (state == CursorState::CURSOR_SCROLLING); } template void GridComponent::calculateLayout() diff --git a/es-core/src/components/primary/PrimaryComponent.h b/es-core/src/components/primary/PrimaryComponent.h index 7d28a0c92..7136e019f 100644 --- a/es-core/src/components/primary/PrimaryComponent.h +++ b/es-core/src/components/primary/PrimaryComponent.h @@ -3,7 +3,7 @@ // EmulationStation Desktop Edition // PrimaryComponent.h // -// Base class for the primary components (carousel and textlist). +// Base class for the primary components (carousel, grid and textlist). // #ifndef ES_CORE_COMPONENTS_PRIMARY_PRIMARY_COMPONENT_H From 75ebd839b9ddc196d4cc07de8cb1d769170403b0 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sun, 13 Nov 2022 19:59:26 +0100 Subject: [PATCH 014/101] Placed the primary elements first in the ThemeData element map. --- es-core/src/ThemeData.cpp | 230 +++++++++++++++++++------------------- 1 file changed, 115 insertions(+), 115 deletions(-) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 06f54ca12..76ffb9e38 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -101,6 +101,119 @@ std::map> ThemeData::sPropertyAt std::map> ThemeData::sElementMap { + {"carousel", + {{"pos", NORMALIZED_PAIR}, + {"size", NORMALIZED_PAIR}, + {"origin", NORMALIZED_PAIR}, + {"type", STRING}, + {"staticItem", PATH}, + {"itemType", STRING}, + {"defaultItem", PATH}, + {"maxItemCount", FLOAT}, + {"maxLogoCount", FLOAT}, // For backward compatibility with legacy themes. + {"itemsBeforeCenter", UNSIGNED_INTEGER}, + {"itemsAfterCenter", UNSIGNED_INTEGER}, + {"itemSize", NORMALIZED_PAIR}, + {"itemScale", FLOAT}, + {"itemTransitions", STRING}, + {"itemInterpolation", STRING}, + {"itemRotation", FLOAT}, + {"itemRotationOrigin", NORMALIZED_PAIR}, + {"itemAxisHorizontal", BOOLEAN}, + {"itemHorizontalAlignment", STRING}, + {"itemVerticalAlignment", STRING}, + {"wheelHorizontalAlignment", STRING}, + {"horizontalOffset", FLOAT}, + {"verticalOffset", FLOAT}, + {"reflections", BOOLEAN}, + {"reflectionsOpacity", FLOAT}, + {"reflectionsFalloff", FLOAT}, + {"unfocusedItemOpacity", FLOAT}, + {"defaultLogo", PATH}, // For backward compatibility with legacy themes. + {"logoSize", NORMALIZED_PAIR}, // For backward compatibility with legacy themes. + {"logoScale", FLOAT}, // For backward compatibility with legacy themes. + {"logoRotation", FLOAT}, // For backward compatibility with legacy themes. + {"logoRotationOrigin", NORMALIZED_PAIR}, // For backward compatibility with legacy themes. + {"logoAlignment", STRING}, // For backward compatibility with legacy themes. + {"color", COLOR}, + {"colorEnd", COLOR}, + {"gradientType", STRING}, + {"text", STRING}, + {"textColor", COLOR}, + {"textBackgroundColor", COLOR}, + {"fontPath", PATH}, + {"fontSize", FLOAT}, + {"letterCase", STRING}, + {"letterCaseCollections", STRING}, + {"letterCaseGroupedCollections", STRING}, + {"lineSpacing", FLOAT}, + {"fadeAbovePrimary", BOOLEAN}, + {"zIndex", FLOAT}, + {"legacyZIndexMode", STRING}}}, // For backward compatibility with legacy themes. + {"grid", + {{"pos", NORMALIZED_PAIR}, + {"size", NORMALIZED_PAIR}, + {"origin", NORMALIZED_PAIR}, + {"columns", UNSIGNED_INTEGER}, + {"staticItem", PATH}, + {"itemType", STRING}, + {"defaultItem", PATH}, + {"itemSize", NORMALIZED_PAIR}, + {"itemScale", FLOAT}, + {"itemSpacing", NORMALIZED_PAIR}, + {"itemTransitions", STRING}, + {"itemHorizontalAlignment", STRING}, + {"itemVerticalAlignment", STRING}, + {"horizontalMargin", FLOAT}, + {"verticalMargin", FLOAT}, + {"horizontalOffset", FLOAT}, + {"verticalOffset", FLOAT}, + {"unfocusedItemOpacity", FLOAT}, + {"edgeScaleInwards", BOOLEAN}, + {"color", COLOR}, + {"colorEnd", COLOR}, + {"gradientType", STRING}, + {"text", STRING}, + {"textColor", COLOR}, + {"textBackgroundColor", COLOR}, + {"fontPath", PATH}, + {"fontSize", FLOAT}, + {"letterCase", STRING}, + {"letterCaseCollections", STRING}, + {"letterCaseGroupedCollections", STRING}, + {"lineSpacing", FLOAT}, + {"fadeAbovePrimary", BOOLEAN}, + {"zIndex", FLOAT}}}, + {"textlist", + {{"pos", NORMALIZED_PAIR}, + {"size", NORMALIZED_PAIR}, + {"origin", NORMALIZED_PAIR}, + {"selectorHeight", FLOAT}, + {"selectorOffsetY", FLOAT}, + {"selectorColor", COLOR}, + {"selectorColorEnd", COLOR}, + {"selectorGradientType", STRING}, + {"selectorImagePath", PATH}, + {"selectorImageTile", BOOLEAN}, + {"primaryColor", COLOR}, + {"secondaryColor", COLOR}, + {"selectedColor", COLOR}, + {"selectedSecondaryColor", COLOR}, + {"fontPath", PATH}, + {"fontSize", FLOAT}, + {"scrollSound", PATH}, // For backward compatibility with legacy themes. + {"horizontalAlignment", STRING}, + {"alignment", STRING}, // For backward compatibility with legacy themes. + {"horizontalMargin", FLOAT}, + {"letterCase", STRING}, + {"letterCaseCollections", STRING}, + {"letterCaseGroupedCollections", STRING}, + {"forceUppercase", BOOLEAN}, // For backward compatibility with legacy themes. + {"lineSpacing", FLOAT}, + {"indicators", STRING}, + {"collectionIndicators", STRING}, + {"fadeAbovePrimary", BOOLEAN}, + {"zIndex", FLOAT}}}, {"image", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, @@ -273,119 +386,6 @@ std::map> {"opacity", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, - {"carousel", - {{"pos", NORMALIZED_PAIR}, - {"size", NORMALIZED_PAIR}, - {"origin", NORMALIZED_PAIR}, - {"type", STRING}, - {"staticItem", PATH}, - {"itemType", STRING}, - {"defaultItem", PATH}, - {"maxItemCount", FLOAT}, - {"maxLogoCount", FLOAT}, // For backward compatibility with legacy themes. - {"itemsBeforeCenter", UNSIGNED_INTEGER}, - {"itemsAfterCenter", UNSIGNED_INTEGER}, - {"itemSize", NORMALIZED_PAIR}, - {"itemScale", FLOAT}, - {"itemTransitions", STRING}, - {"itemInterpolation", STRING}, - {"itemRotation", FLOAT}, - {"itemRotationOrigin", NORMALIZED_PAIR}, - {"itemAxisHorizontal", BOOLEAN}, - {"itemHorizontalAlignment", STRING}, - {"itemVerticalAlignment", STRING}, - {"wheelHorizontalAlignment", STRING}, - {"horizontalOffset", FLOAT}, - {"verticalOffset", FLOAT}, - {"reflections", BOOLEAN}, - {"reflectionsOpacity", FLOAT}, - {"reflectionsFalloff", FLOAT}, - {"unfocusedItemOpacity", FLOAT}, - {"defaultLogo", PATH}, // For backward compatibility with legacy themes. - {"logoSize", NORMALIZED_PAIR}, // For backward compatibility with legacy themes. - {"logoScale", FLOAT}, // For backward compatibility with legacy themes. - {"logoRotation", FLOAT}, // For backward compatibility with legacy themes. - {"logoRotationOrigin", NORMALIZED_PAIR}, // For backward compatibility with legacy themes. - {"logoAlignment", STRING}, // For backward compatibility with legacy themes. - {"color", COLOR}, - {"colorEnd", COLOR}, - {"gradientType", STRING}, - {"text", STRING}, - {"textColor", COLOR}, - {"textBackgroundColor", COLOR}, - {"fontPath", PATH}, - {"fontSize", FLOAT}, - {"letterCase", STRING}, - {"letterCaseCollections", STRING}, - {"letterCaseGroupedCollections", STRING}, - {"lineSpacing", FLOAT}, - {"fadeAbovePrimary", BOOLEAN}, - {"zIndex", FLOAT}, - {"legacyZIndexMode", STRING}}}, // For backward compatibility with legacy themes. - {"grid", - {{"pos", NORMALIZED_PAIR}, - {"size", NORMALIZED_PAIR}, - {"origin", NORMALIZED_PAIR}, - {"columns", UNSIGNED_INTEGER}, - {"staticItem", PATH}, - {"itemType", STRING}, - {"defaultItem", PATH}, - {"itemSize", NORMALIZED_PAIR}, - {"itemScale", FLOAT}, - {"itemSpacing", NORMALIZED_PAIR}, - {"itemTransitions", STRING}, - {"itemHorizontalAlignment", STRING}, - {"itemVerticalAlignment", STRING}, - {"horizontalMargins", NORMALIZED_PAIR}, - {"verticalMargins", NORMALIZED_PAIR}, - {"horizontalOffset", FLOAT}, - {"verticalOffset", FLOAT}, - {"unfocusedItemOpacity", FLOAT}, - {"edgeScaleInwards", BOOLEAN}, - {"color", COLOR}, - {"colorEnd", COLOR}, - {"gradientType", STRING}, - {"text", STRING}, - {"textColor", COLOR}, - {"textBackgroundColor", COLOR}, - {"fontPath", PATH}, - {"fontSize", FLOAT}, - {"letterCase", STRING}, - {"letterCaseCollections", STRING}, - {"letterCaseGroupedCollections", STRING}, - {"lineSpacing", FLOAT}, - {"fadeAbovePrimary", BOOLEAN}, - {"zIndex", FLOAT}}}, - {"textlist", - {{"pos", NORMALIZED_PAIR}, - {"size", NORMALIZED_PAIR}, - {"origin", NORMALIZED_PAIR}, - {"selectorHeight", FLOAT}, - {"selectorOffsetY", FLOAT}, - {"selectorColor", COLOR}, - {"selectorColorEnd", COLOR}, - {"selectorGradientType", STRING}, - {"selectorImagePath", PATH}, - {"selectorImageTile", BOOLEAN}, - {"primaryColor", COLOR}, - {"secondaryColor", COLOR}, - {"selectedColor", COLOR}, - {"selectedSecondaryColor", COLOR}, - {"fontPath", PATH}, - {"fontSize", FLOAT}, - {"scrollSound", PATH}, // For backward compatibility with legacy themes. - {"horizontalAlignment", STRING}, - {"alignment", STRING}, // For backward compatibility with legacy themes. - {"horizontalMargin", FLOAT}, - {"letterCase", STRING}, - {"letterCaseCollections", STRING}, - {"letterCaseGroupedCollections", STRING}, - {"forceUppercase", BOOLEAN}, // For backward compatibility with legacy themes. - {"lineSpacing", FLOAT}, - {"indicators", STRING}, - {"collectionIndicators", STRING}, - {"fadeAbovePrimary", BOOLEAN}, - {"zIndex", FLOAT}}}, {"gameselector", {{"selection", STRING}, {"gameCount", UNSIGNED_INTEGER}}}, @@ -404,8 +404,6 @@ std::map> {"textStyle", STRING}, // For backward compatibility with legacy themes. {"opacity", FLOAT}, {"customButtonIcon", PATH}}}, - {"sound", - {{"path", PATH}}}, {"navigationsounds", {{"systembrowseSound", PATH}, {"quicksysselectSound", PATH}, @@ -415,6 +413,8 @@ std::map> {"favoriteSound", PATH}, {"launchSound", PATH}}}, // Legacy components below, not in use any longer but needed for backward compatibility. + {"sound", + {{"path", PATH}}}, {"imagegrid", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, From 9013faf44534d77b915e20cc9b8a22fd832ab70c Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sun, 13 Nov 2022 22:31:41 +0100 Subject: [PATCH 015/101] Added proper item spacing, item offsets and element boundary clipping to GridComponent. --- es-core/src/ThemeData.cpp | 2 - .../src/components/primary/GridComponent.h | 86 ++++++++++++++++--- .../components/primary/TextListComponent.h | 4 +- 3 files changed, 75 insertions(+), 17 deletions(-) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 76ffb9e38..7ca30ca14 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -164,8 +164,6 @@ std::map> {"itemTransitions", STRING}, {"itemHorizontalAlignment", STRING}, {"itemVerticalAlignment", STRING}, - {"horizontalMargin", FLOAT}, - {"verticalMargin", FLOAT}, {"horizontalOffset", FLOAT}, {"verticalOffset", FLOAT}, {"unfocusedItemOpacity", FLOAT}, diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index d1394cf15..779b9f3df 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -101,6 +101,8 @@ private: float mItemScale; glm::vec2 mItemSpacing; bool mInstantItemTransitions; + float mHorizontalOffset; + float mVerticalOffset; float mUnfocusedItemOpacity; unsigned int mTextColor; unsigned int mTextBackgroundColor; @@ -126,12 +128,13 @@ GridComponent::GridComponent() , mTransitionFactor {1.0f} , mFont {Font::get(FONT_SIZE_LARGE)} , mColumns {5} - , mItemSize {glm::vec2 {Renderer::getScreenWidth() * 0.15f, - Renderer::getScreenHeight() * 0.25f}} + , mItemSize {glm::vec2 {mRenderer->getScreenWidth() * 0.15f, + mRenderer->getScreenHeight() * 0.25f}} , mItemScale {1.05f} - , mItemSpacing {glm::vec2 {Renderer::getScreenWidth() * 0.02f, - Renderer::getScreenHeight() * 0.02f}} + , mItemSpacing {0.0f, 0.0f} , mInstantItemTransitions {false} + , mHorizontalOffset {0.0f} + , mVerticalOffset {0.0f} , mUnfocusedItemOpacity {1.0f} , mTextColor {0x000000FF} , mTextBackgroundColor {0xFFFFFF00} @@ -358,6 +361,14 @@ template void GridComponent::render(const glm::mat4& parentTrans if (Settings::getInstance()->getBool("DebugImage")) mRenderer->drawRect(0.0f, 0.0f, mSize.x, mSize.y, 0x00FF0033, 0x00FF0033); + // Clip to element boundaries. + glm::vec3 dim {mSize.x, mSize.y, 0.0f}; + dim.x = (trans[0].x * dim.x + trans[3].x) - trans[3].x; + dim.y = (trans[1].y * dim.y + trans[3].y) - trans[3].y; + + mRenderer->pushClipRect(glm::ivec2 {static_cast(trans[3].x), static_cast(trans[3].y)}, + glm::ivec2 {static_cast(dim.x), static_cast(dim.y)}); + // We want to render the currently selected item last and before that the last selected // item to avoid incorrect overlapping in case the element has been configured with for // example large scaling or small or no margins between items. @@ -393,6 +404,7 @@ template void GridComponent::render(const glm::mat4& parentTrans mEntries.at(*it).data.item->setOpacity(1.0f); } + mRenderer->popClipRect(); GuiComponent::renderChildren(trans); } @@ -403,15 +415,21 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, unsigned int properties) { mSize.x = Renderer::getScreenWidth(); - mSize.y = Renderer::getScreenHeight() * 0.8; + mSize.y = Renderer::getScreenHeight() * 0.8f; GuiComponent::mPosition.x = 0.0f; - GuiComponent::mPosition.y = Renderer::getScreenHeight() * 0.1; + GuiComponent::mPosition.y = Renderer::getScreenHeight() * 0.1f; + mItemSpacing.x = ((mItemSize.x * mItemScale) - mItemSize.x) / 2.0f; + mItemSpacing.y = ((mItemSize.y * mItemScale) - mItemSize.y) / 2.0f; + mHorizontalOffset = ((mItemSize.x * mItemScale) - mItemSize.x) / 2.0f; + mVerticalOffset = ((mItemSize.y * mItemScale) - mItemSize.y) / 2.0f; GuiComponent::applyTheme(theme, view, element, properties); using namespace ThemeFlags; const ThemeData::ThemeElement* elem {theme->getElement(view, element, "grid")}; + mSize = glm::round(mSize); + if (!elem) return; @@ -419,7 +437,7 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, mColumns = glm::clamp(elem->get("columns"), 0u, 100u); if (elem->has("itemSize")) { - const glm::vec2 itemSize {glm::clamp(elem->get("itemSize"), 0.05f, 1.0f)}; + const glm::vec2& itemSize {glm::clamp(elem->get("itemSize"), 0.05f, 1.0f)}; mItemSize = itemSize * glm::vec2(Renderer::getScreenWidth(), Renderer::getScreenHeight()); } @@ -442,11 +460,50 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, } } + // If itemSpacing is not defined, then it's automatically calculated so that scaled items + // don't overlap. If the property is present but one axis is defined as -1 then set this + // axis to the same pixel value as the other axis. if (elem->has("itemSpacing")) { - const glm::vec2 itemSpacing {glm::clamp(elem->get("itemSpacing"), 0.0f, 0.1f)}; - mItemSpacing = - itemSpacing * glm::vec2(Renderer::getScreenWidth(), Renderer::getScreenHeight()); + const glm::vec2& itemSpacing {elem->get("itemSpacing")}; + if (itemSpacing.x == -1 && itemSpacing.y == -1) { + mItemSpacing = {0.0f, 0.0f}; + } + else if (itemSpacing.x == -1) { + mItemSpacing.y = glm::clamp(itemSpacing.y, 0.0f, 0.1f) * mRenderer->getScreenHeight(); + mItemSpacing.x = mItemSpacing.y; + } + else if (itemSpacing.y == -1) { + mItemSpacing.x = glm::clamp(itemSpacing.x, 0.0f, 0.1f) * mRenderer->getScreenWidth(); + mItemSpacing.y = mItemSpacing.x; + } + else { + mItemSpacing = glm::clamp(itemSpacing, 0.0f, 0.1f) * + glm::vec2(Renderer::getScreenWidth(), Renderer::getScreenHeight()); + } } + else if (mItemScale < 1.0f) { + mItemSpacing = glm::vec2 {0.0f, 0.0f}; + } + else { + mItemSpacing.x = ((mItemSize.x * mItemScale) - mItemSize.x) / 2.0f; + mItemSpacing.y = ((mItemSize.y * mItemScale) - mItemSize.y) / 2.0f; + } + + // If horizontalOffset or verticalOffset are not defined, then they are automatically + // calculated so that scaled items don't get clipped at grid boundaries. + if (elem->has("horizontalOffset")) + mHorizontalOffset = glm::clamp(elem->get("horizontalOffset"), -0.5f, 0.5f) * mSize.x; + else if (mItemScale < 1.0f) + mHorizontalOffset = 0.0f; + else + mHorizontalOffset = ((mItemSize.x * mItemScale) - mItemSize.x) / 2.0f; + + if (elem->has("verticalOffset")) + mVerticalOffset = glm::clamp(elem->get("verticalOffset"), -0.5f, 0.5f) * mSize.y; + else if (mItemScale < 1.0f) + mVerticalOffset = 0.0f; + else + mVerticalOffset = ((mItemSize.y * mItemScale) - mItemSize.y) / 2.0f; if (elem->has("unfocusedItemOpacity")) mUnfocusedItemOpacity = glm::clamp(elem->get("unfocusedItemOpacity"), 0.1f, 1.0f); @@ -556,9 +613,12 @@ template void GridComponent::calculateLayout() unsigned int rowCount {0}; for (auto& entry : mEntries) { - entry.data.item->setPosition(glm::vec3 { - (mItemSize.x * columnCount) + (mItemSize.x * 0.5f) + mItemSpacing.x * columnCount, - (mItemSize.y * rowCount) + (mItemSize.y * 0.5f) + mItemSpacing.y * rowCount, 0.0f}); + entry.data.item->setPosition( + glm::vec3 {mHorizontalOffset + (mItemSize.x * columnCount) + (mItemSize.x * 0.5f) + + mItemSpacing.x * columnCount, + mVerticalOffset + (mItemSize.y * rowCount) + (mItemSize.y * 0.5f) + + mItemSpacing.y * rowCount, + 0.0f}); if (columnCount == mColumns - 1) { ++rowCount; columnCount = 0; diff --git a/es-core/src/components/primary/TextListComponent.h b/es-core/src/components/primary/TextListComponent.h index 34b25f1ca..94694d71d 100644 --- a/es-core/src/components/primary/TextListComponent.h +++ b/es-core/src/components/primary/TextListComponent.h @@ -485,7 +485,7 @@ void TextListComponent::applyTheme(const std::shared_ptr& theme, mSize.x = Renderer::getScreenWidth(); mSize.y = Renderer::getScreenHeight() * 0.8f; GuiComponent::mPosition.x = 0.0f; - GuiComponent::mPosition.y = Renderer::getScreenHeight() * 0.1; + GuiComponent::mPosition.y = Renderer::getScreenHeight() * 0.1f; setAlignment(PrimaryAlignment::ALIGN_LEFT); GuiComponent::applyTheme(theme, view, element, properties); @@ -559,8 +559,8 @@ void TextListComponent::applyTheme(const std::shared_ptr& theme, << "\""; } } - // Legacy themes only. else if (elem->has("alignment")) { + // Legacy themes only. const std::string& alignment {elem->get("alignment")}; if (alignment == "left") { setAlignment(PrimaryAlignment::ALIGN_LEFT); From be903c0a86a68d3da9af39ab9547ccb5ac08be0d Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sun, 13 Nov 2022 23:04:48 +0100 Subject: [PATCH 016/101] Changed a value for the itemTransitions property for the carousel and grid components. Also changed the position of a grid property and set the minimum columns value to 1. --- es-core/src/ThemeData.cpp | 2 +- es-core/src/components/primary/CarouselComponent.h | 2 +- es-core/src/components/primary/GridComponent.h | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 7ca30ca14..80ba09d34 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -160,8 +160,8 @@ std::map> {"defaultItem", PATH}, {"itemSize", NORMALIZED_PAIR}, {"itemScale", FLOAT}, - {"itemSpacing", NORMALIZED_PAIR}, {"itemTransitions", STRING}, + {"itemSpacing", NORMALIZED_PAIR}, {"itemHorizontalAlignment", STRING}, {"itemVerticalAlignment", STRING}, {"horizontalOffset", FLOAT}, diff --git a/es-core/src/components/primary/CarouselComponent.h b/es-core/src/components/primary/CarouselComponent.h index 128f1cfb4..3515a99d5 100644 --- a/es-core/src/components/primary/CarouselComponent.h +++ b/es-core/src/components/primary/CarouselComponent.h @@ -1010,7 +1010,7 @@ void CarouselComponent::applyTheme(const std::shared_ptr& theme, if (elem->has("itemTransitions")) { const std::string& itemTransitions {elem->get("itemTransitions")}; - if (itemTransitions == "slide") { + if (itemTransitions == "animate") { mInstantItemTransitions = false; } else if (itemTransitions == "instant") { diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index 779b9f3df..254c1eff8 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -434,7 +434,7 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, return; if (elem->has("columns")) - mColumns = glm::clamp(elem->get("columns"), 0u, 100u); + mColumns = glm::clamp(elem->get("columns"), 1u, 100u); if (elem->has("itemSize")) { const glm::vec2& itemSize {glm::clamp(elem->get("itemSize"), 0.05f, 1.0f)}; @@ -446,7 +446,7 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, if (elem->has("itemTransitions")) { const std::string& itemTransitions {elem->get("itemTransitions")}; - if (itemTransitions == "scale") { + if (itemTransitions == "animate") { mInstantItemTransitions = false; } else if (itemTransitions == "instant") { From b7a17516ed158c0267a7232ecf267031d25d732d Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sun, 13 Nov 2022 23:07:17 +0100 Subject: [PATCH 017/101] Documentation update. --- THEMES-DEV.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/THEMES-DEV.md b/THEMES-DEV.md index 6d0cff341..e2c576c09 100644 --- a/THEMES-DEV.md +++ b/THEMES-DEV.md @@ -997,7 +997,7 @@ The order in which you define properties for a given element does not matter and ### Primary elements -Elements from this group can only occur once per view (for a certain variant) and they govern basic behavior and functionality like controller input and navigation. +Elements from this group can only occur once per view (for a certain variant) and they handle basic functionality like controller input and navigation. #### carousel @@ -1063,9 +1063,9 @@ Properties: - Minimum value is `0.2` and maximum value is `3` - Default is `1.2` * `itemTransitions` - type: STRING - - How to render item transitions when navigating the carousel. By default a slide animation will be played when moving between items but if this property is set to `instant` instead then the transitions will be immediate. - - Valid values are `slide` or `instant` - - Default is `slide` + - How to render item transitions when navigating the carousel. By default a slide and opacity fade animation will be played when moving between items but if this property is set to `instant` then the transitions will be immediate. + - Valid values are `animate` or `instant` + - Default is `animate` * `itemInterpolation` - type: STRING - Interpolation method to use when scaling items. Nearest neighbor (`nearest`) preserves sharp pixels and linear filtering (`linear`) makes the image smoother. The effect of this property is primarily visible for raster graphic images, but it has a limited effect also when using scalable vector graphics (SVG) images as these are rasterized at a set resolution and then scaled using the GPU. - Valid values are `nearest` or `linear` @@ -1181,6 +1181,53 @@ Properties: - Where on the element `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the textlist exactly in the middle of the screen. If the position and size attributes are themeable, origin is implied. - Minimum value per axis is `0` and maximum value per axis is `1` - Default is `0 0` +* `columns` - type: UNSIGNED_INTEGER + - The number of columns to add to the grid. The row count will be automatically calculated by dividing the overall number of items by the value of this property. + - Minimum value is `1` and maximum value is `100` + - Default is `5` +* `staticItem` - type: PATH + - Path to a static image file. Most common extensions are supported (including .svg, .jpg, .png, and unanimated .gif). This property can only be used in the `system` view. +* `itemType` - type: STRING + - This displays a game image of a certain media type, and can only be used in the `gamelist` view. + - Valid values: + - `marquee` - This will look for a marquee (wheel) image. + - `cover` - This will look for a box front cover image. + - `backcover` - This will look for a box back cover image. + - `3dbox` - This will look for a 3D box image. + - `physicalmedia` - This will look for a physical media image. + - `screenshot` - This will look for a screenshot image. + - `titlescreen` - This will look for a title screen image. + - `miximage` - This will look for a miximage. + - `fanart` - This will look for a fan art image. + - `none` - No image will be used, instead the game name will be displayed as text. + - Default is `marquee` +* `defaultItem` - type: PATH + - Path to the default image file which will be displayed if the image defined via the `staticItem` or `itemType` property is not found. Most common extensions are supported (including .svg, .jpg, .png, and unanimated .gif). +* `itemSize` - type: NORMALIZED_PAIR + - Size of the item prior to multiplication by the `itemScale` value, i.e. the size of all unselected items. Both axes need to be defined. + - Minimum value per axis is `0.05` and maximum value per axis is `1` + - Default is `0.15 0.25` +* `itemScale` - type: FLOAT. + - Selected item is increased in size by this scale. + - Minimum value is `0.5` and maximum value is `2` + - Default is `1.05` +* `itemTransitions` - type: STRING + - How to render item transitions when navigating the grid. By default a scaling and opacity fade animation will be played when moving between items (assuming `itemScale` and `unfocusedItemOpacity` have not been set to `1`) but if this property is set to `instant` then the transitions will be immediate. + - Valid values are `animate` or `instant` + - Default is `animate` +* `itemSpacing` - type: NORMALIZED_PAIR + - The horizontal and vertical space between items. This value is added to the unscaled item size, i.e. `itemSize` before it's been multiplied by `itemScale`. This means that if an axis is set to `0` then unscaled items will be perfectly adjacent to each other on that axis but if `itemScale` has been set to higher than `1` then the currently selected item may overlap adjacent items. If this property is omitted then spacing will be automatically calculated so that no overlaps occur during scaling. However you'd normally want to define and adjust this property for an optimal layout. If one of the axis is defined as `-1` then it will be set to the same pixel value as the other axis. Note that all spacing calculations are based on the value defined by `itemSize` which may or may not be the same as the actual image sizes, depending on their aspect ratios. + - Minimum value per axis is `0` and maximum value per axis is `0.1` +* `horizontalOffset` - type: FLOAT + - Offsets the grid horizontally inside its designated area, as defined by the `size` property. The value is relative to the size of the grid with `1` being equivalent to its entire width. This property is primarily used to make sure scaled items are not clipped at grid boundaries, but if omitted the offset is automatically calculated so you normally don't need to define it unless you have a very specific need to fine-tune the layout. + - Minimum value is `-0.5` and maximum value is `0.5` +* `verticalOffset` - type: FLOAT + - Offsets the grid vertically inside its designated area, as defined by the `size` property. The value is relative to the size of the grid with `1` being equivalent to its entire height. This property is primarily used to make sure scaled items are not clipped at grid boundaries, but if omitted the offset is automatically calculated so you normally don't need to define it unless you have a very specific need to fine-tune the layout. + - Minimum value is `-0.5` and maximum value is `0.5` +* `unfocusedItemOpacity` - type: FLOAT + - Sets the opacity for the items that are not currently focused. + - Minimum value is `0.1` and maximum value is `1` + - Default is `1` * `zIndex` - type: FLOAT - z-index value for element. Elements will be rendered in order of zIndex value from low to high. - Default is `50` @@ -1959,7 +2006,7 @@ Properties: ### Special elements -Elements from this group offer special functionality not covered by the primary and secondary elements. +Elements from this group provide special functionality not covered by the primary and secondary elements. #### gameselector From 218d64da95c87f9532aa6b64fbb33edb98246299 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Wed, 23 Nov 2022 18:49:53 +0100 Subject: [PATCH 018/101] (Windows) Added Rosalie's Mupen GUI as an alternative emulator for the n64 system. --- es-app/assets/Windows_Portable_README.txt | 1 + resources/systems/windows/es_find_rules.xml | 12 ++++++++++++ resources/systems/windows/es_find_rules_portable.xml | 8 ++++++++ resources/systems/windows/es_systems.xml | 1 + 4 files changed, 22 insertions(+) diff --git a/es-app/assets/Windows_Portable_README.txt b/es-app/assets/Windows_Portable_README.txt index b04af61e2..db8495226 100644 --- a/es-app/assets/Windows_Portable_README.txt +++ b/es-app/assets/Windows_Portable_README.txt @@ -64,6 +64,7 @@ Emulators\PPSSPP\PPSSPPWindows64.exe Emulators\PrimeHack\Dolphin.exe Emulators\Project64\Project64.exe Emulators\redream\redream.exe +Emulators\RMG\RMG.exe Emulators\RPCS3\rpcs3.exe Emulators\ruffle\ruffle.exe Emulators\ryujinx\Ryujinx.exe diff --git a/resources/systems/windows/es_find_rules.xml b/resources/systems/windows/es_find_rules.xml index 47feb85bf..5008f47b1 100644 --- a/resources/systems/windows/es_find_rules.xml +++ b/resources/systems/windows/es_find_rules.xml @@ -480,6 +480,18 @@ %ESPATH%\..\redream\redream.exe + + + + RMG.exe + + + ~\AppData\Local\Programs\Rosalie's Mupen GUI\RMG.exe + %ESPATH%\Emulators\RMG\RMG.exe + %ESPATH%\RMG\RMG.exe + %ESPATH%\..\RMG\RMG.exe + + diff --git a/resources/systems/windows/es_find_rules_portable.xml b/resources/systems/windows/es_find_rules_portable.xml index 644b778c9..005640cd4 100644 --- a/resources/systems/windows/es_find_rules_portable.xml +++ b/resources/systems/windows/es_find_rules_portable.xml @@ -323,6 +323,14 @@ %ESPATH%\..\redream\redream.exe + + + + %ESPATH%\Emulators\RMG\RMG.exe + %ESPATH%\RMG\RMG.exe + %ESPATH%\..\RMG\RMG.exe + + diff --git a/resources/systems/windows/es_systems.xml b/resources/systems/windows/es_systems.xml index 61d569808..40490195d 100644 --- a/resources/systems/windows/es_systems.xml +++ b/resources/systems/windows/es_systems.xml @@ -898,6 +898,7 @@ %EMULATOR_MUPEN64PLUS% --fullscreen %ROM% %EMULATOR_RETROARCH% -L %CORE_RETROARCH%\parallel_n64_libretro.dll %ROM% %EMULATOR_SIMPLE64% --nogui %ROM% + %EMULATOR_ROSALIES-MUPEN-GUI% %ROM% %EMULATOR_PROJECT64% %ROM% %EMULATOR_ARES% --fullscreen --system "Nintendo 64" %ROM% n64 From d31536af4b42ff71317050c34f38c7b65d31ef92 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Wed, 23 Nov 2022 20:26:27 +0100 Subject: [PATCH 019/101] (macOS) Added support for the Cemu standalone emulator for the wiiu system. --- resources/systems/macos/es_find_rules.xml | 6 ++++++ resources/systems/macos/es_systems.xml | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/resources/systems/macos/es_find_rules.xml b/resources/systems/macos/es_find_rules.xml index d40e8fe38..549b34778 100644 --- a/resources/systems/macos/es_find_rules.xml +++ b/resources/systems/macos/es_find_rules.xml @@ -46,6 +46,12 @@ /Applications/BasiliskII.app/Contents/MacOS/BasiliskII + + + + /Applications/Cemu.app/Contents/MacOS/Cemu + + diff --git a/resources/systems/macos/es_systems.xml b/resources/systems/macos/es_systems.xml index f8c899a87..86532499d 100644 --- a/resources/systems/macos/es_systems.xml +++ b/resources/systems/macos/es_systems.xml @@ -1579,8 +1579,8 @@ wiiu Nintendo Wii U %ROMPATH%/wiiu - .7z .7Z .zip .ZIP - PLACEHOLDER %ROM% + .rpx .RPX .wua .WUA .wud .WUD .wux .WUX + %EMULATOR_CEMU% -g %ROM% wiiu wiiu From 24fe15a84f72317392495ffdcba9ab13db51ca5c Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Fri, 25 Nov 2022 18:57:06 +0100 Subject: [PATCH 020/101] Fixed some erratic camera movement issues during extreme SystemView navigation input. --- es-app/src/views/SystemView.cpp | 39 +++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 850d45c0e..8973ac7a9 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -222,7 +222,9 @@ std::vector SystemView::getHelpPrompts() void SystemView::onCursorChanged(const CursorState& state) { - int cursor {mPrimary->getCursor()}; + const int cursor {mPrimary->getCursor()}; + const std::string& transitionStyle {Settings::getInstance()->getString("TransitionStyle")}; + mFadeTransitions = transitionStyle == "fade"; // Avoid double updates. if (cursor != mLastCursor) { @@ -237,6 +239,33 @@ void SystemView::onCursorChanged(const CursorState& state) video->stopVideoPlayer(); } + const int scrollVelocity {mPrimary->getScrollingVelocity()}; + + // This is needed to avoid erratic camera movements during extreme navigation input when using + // slide transitions. This should very rarely occur during normal application usage. + if (transitionStyle == "slide") { + bool resetCamOffset {false}; + + if (scrollVelocity == -1 && mPreviousScrollVelocity == 1) { + if (mLastCursor > cursor && mCamOffset > static_cast(mLastCursor)) + resetCamOffset = true; + else if (mLastCursor > cursor && mCamOffset < static_cast(cursor)) + resetCamOffset = true; + else if (mLastCursor < cursor && mCamOffset <= static_cast(cursor) && + mCamOffset != static_cast(mLastCursor)) + resetCamOffset = true; + } + else if (scrollVelocity == 1 && mPreviousScrollVelocity == -1) { + if (mLastCursor > cursor && mCamOffset < static_cast(mLastCursor)) + resetCamOffset = true; + else if (mLastCursor < cursor && mCamOffset > static_cast(cursor)) + resetCamOffset = true; + } + + if (resetCamOffset) + mCamOffset = static_cast(cursor); + } + mLastCursor = cursor; for (auto& video : mSystemElements[cursor].videoComponents) @@ -252,10 +281,9 @@ void SystemView::onCursorChanged(const CursorState& state) startViewVideos(); updateHelpPrompts(); - int scrollVelocity {mPrimary->getScrollingVelocity()}; + const float posMax {static_cast(mPrimary->getNumEntries())}; + const float target {static_cast(cursor)}; float startPos {mCamOffset}; - float posMax {static_cast(mPrimary->getNumEntries())}; - float target {static_cast(cursor)}; float endPos {target}; if (mPreviousScrollVelocity > 0 && scrollVelocity == 0 && mCamOffset > posMax - 1.0f) @@ -287,9 +315,6 @@ void SystemView::onCursorChanged(const CursorState& state) if (scrollVelocity != 0) mPreviousScrollVelocity = scrollVelocity; - std::string transitionStyle {Settings::getInstance()->getString("TransitionStyle")}; - mFadeTransitions = transitionStyle == "fade"; - Animation* anim; float animTime {380.0f}; From 99df9978331f41347db0ff93b227cd32d3019c64 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Fri, 25 Nov 2022 18:59:08 +0100 Subject: [PATCH 021/101] Added scrolling support to GridComponent. Also changed the names of two properties. --- es-core/src/ThemeData.cpp | 4 +- .../src/components/primary/GridComponent.h | 102 ++++++++++++------ 2 files changed, 70 insertions(+), 36 deletions(-) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 80ba09d34..fa07f07ad 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -164,8 +164,8 @@ std::map> {"itemSpacing", NORMALIZED_PAIR}, {"itemHorizontalAlignment", STRING}, {"itemVerticalAlignment", STRING}, - {"horizontalOffset", FLOAT}, - {"verticalOffset", FLOAT}, + {"horizontalMargin", FLOAT}, + {"verticalMargin", FLOAT}, {"unfocusedItemOpacity", FLOAT}, {"edgeScaleInwards", BOOLEAN}, {"color", COLOR}, diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index 254c1eff8..0901634b1 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -92,8 +92,10 @@ private: std::string mItemType; std::string mDefaultItem; float mEntryOffset; - float mEntryCamTarget; + float mScrollPos; float mTransitionFactor; + float mVisibleRows; + int mRowCount; std::shared_ptr mFont; unsigned int mColumns; @@ -101,8 +103,8 @@ private: float mItemScale; glm::vec2 mItemSpacing; bool mInstantItemTransitions; - float mHorizontalOffset; - float mVerticalOffset; + float mHorizontalMargin; + float mVerticalMargin; float mUnfocusedItemOpacity; unsigned int mTextColor; unsigned int mTextBackgroundColor; @@ -124,8 +126,10 @@ GridComponent::GridComponent() : IList {LIST_SCROLL_STYLE_SLOW, ListLoopType::LIST_PAUSE_AT_END} , mRenderer {Renderer::getInstance()} , mEntryOffset {0.0f} - , mEntryCamTarget {0.0f} + , mScrollPos {0.0f} , mTransitionFactor {1.0f} + , mVisibleRows {1.0f} + , mRowCount {1} , mFont {Font::get(FONT_SIZE_LARGE)} , mColumns {5} , mItemSize {glm::vec2 {mRenderer->getScreenWidth() * 0.15f, @@ -133,8 +137,8 @@ GridComponent::GridComponent() , mItemScale {1.05f} , mItemSpacing {0.0f, 0.0f} , mInstantItemTransitions {false} - , mHorizontalOffset {0.0f} - , mVerticalOffset {0.0f} + , mHorizontalMargin {0.0f} + , mVerticalMargin {0.0f} , mUnfocusedItemOpacity {1.0f} , mTextColor {0x000000FF} , mTextBackgroundColor {0xFFFFFF00} @@ -289,6 +293,8 @@ template bool GridComponent::input(InputConfig* config, Input in return true; } if (config->isMappedLike("up", input)) { + if (static_cast(mCursor) < mColumns) + return true; if (mCancelTransitionsCallback) mCancelTransitionsCallback(); mRowJump = true; @@ -296,6 +302,9 @@ template bool GridComponent::input(InputConfig* config, Input in return true; } if (config->isMappedLike("down", input)) { + if (static_cast(mCursor) >= (mColumns * mRowCount) - mColumns && + mEntries.size() - mCursor <= mColumns && mEntries.size() % mColumns == 0) + return true; if (mCancelTransitionsCallback) mCancelTransitionsCallback(); mRowJump = true; @@ -387,6 +396,9 @@ template void GridComponent::render(const glm::mat4& parentTrans float opacity {mUnfocusedItemOpacity}; float scale {1.0f}; + trans[3].y -= (mItemSize.y + mItemSpacing.y) * mScrollPos; + mRenderer->setMatrix(trans); + for (auto it = renderEntries.cbegin(); it != renderEntries.cend(); ++it) { if (*it == static_cast(mCursor)) { scale = glm::mix(1.0f, mItemScale, mTransitionFactor); @@ -420,16 +432,14 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, GuiComponent::mPosition.y = Renderer::getScreenHeight() * 0.1f; mItemSpacing.x = ((mItemSize.x * mItemScale) - mItemSize.x) / 2.0f; mItemSpacing.y = ((mItemSize.y * mItemScale) - mItemSize.y) / 2.0f; - mHorizontalOffset = ((mItemSize.x * mItemScale) - mItemSize.x) / 2.0f; - mVerticalOffset = ((mItemSize.y * mItemScale) - mItemSize.y) / 2.0f; + mHorizontalMargin = ((mItemSize.x * mItemScale) - mItemSize.x) / 2.0f; + mVerticalMargin = ((mItemSize.y * mItemScale) - mItemSize.y) / 2.0f; GuiComponent::applyTheme(theme, view, element, properties); using namespace ThemeFlags; const ThemeData::ThemeElement* elem {theme->getElement(view, element, "grid")}; - mSize = glm::round(mSize); - if (!elem) return; @@ -489,21 +499,21 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, mItemSpacing.y = ((mItemSize.y * mItemScale) - mItemSize.y) / 2.0f; } - // If horizontalOffset or verticalOffset are not defined, then they are automatically + // If horizontalMargin or verticalMargin are not defined, then they are automatically // calculated so that scaled items don't get clipped at grid boundaries. - if (elem->has("horizontalOffset")) - mHorizontalOffset = glm::clamp(elem->get("horizontalOffset"), -0.5f, 0.5f) * mSize.x; + if (elem->has("horizontalMargin")) + mHorizontalMargin = glm::clamp(elem->get("horizontalMargin"), -0.5f, 0.5f) * mSize.x; else if (mItemScale < 1.0f) - mHorizontalOffset = 0.0f; + mHorizontalMargin = 0.0f; else - mHorizontalOffset = ((mItemSize.x * mItemScale) - mItemSize.x) / 2.0f; + mHorizontalMargin = ((mItemSize.x * mItemScale) - mItemSize.x) / 2.0f; - if (elem->has("verticalOffset")) - mVerticalOffset = glm::clamp(elem->get("verticalOffset"), -0.5f, 0.5f) * mSize.y; + if (elem->has("verticalMargin")) + mVerticalMargin = glm::clamp(elem->get("verticalMargin"), -0.5f, 0.5f) * mSize.y; else if (mItemScale < 1.0f) - mVerticalOffset = 0.0f; + mVerticalMargin = 0.0f; else - mVerticalOffset = ((mItemSize.y * mItemScale) - mItemSize.y) / 2.0f; + mVerticalMargin = ((mItemSize.y * mItemScale) - mItemSize.y) / 2.0f; if (elem->has("unfocusedItemOpacity")) mUnfocusedItemOpacity = glm::clamp(elem->get("unfocusedItemOpacity"), 0.1f, 1.0f); @@ -558,25 +568,44 @@ template void GridComponent::onCursorChanged(const CursorState& else mPositiveDirection = false; - mEntryCamTarget = endPos; float animTime {250.0f}; - float timeDiff {1.0f}; // If startPos is inbetween two positions then reduce the time slightly as the distance will // be shorter meaning the animation would play for too long if not compensated for. - if (mScrollVelocity == 1) - timeDiff = endPos - startPos; - else if (mScrollVelocity == -1) - timeDiff = startPos - endPos; + // float timeDiff {1.0f}; + // if (mScrollVelocity == 1) + // timeDiff = endPos - startPos; + // else if (mScrollVelocity == -1) + // timeDiff = startPos - endPos; + // if (timeDiff != 1.0f) + // animTime = + // glm::clamp(std::fabs(glm::mix(0.0f, animTime, timeDiff * 1.5f)), 180.0f, + // animTime); - if (timeDiff != 1.0f) - animTime = - glm::clamp(std::fabs(glm::mix(0.0f, animTime, timeDiff * 1.5f)), 180.0f, animTime); + float visibleRows {mVisibleRows - 1.0f}; + float startRow {static_cast(mLastCursor / mColumns)}; + float endRow {static_cast(mCursor / mColumns)}; + + if (endRow <= visibleRows) { + if (startRow == endRow || startRow <= visibleRows) + startRow = 0.0f; + else if (startRow > visibleRows) + startRow -= visibleRows; + endRow = 0.0f; + } + else { + if (startRow <= visibleRows) + startRow = 0.0f; + else + startRow -= visibleRows; + endRow -= visibleRows; + } Animation* anim {new LambdaAnimation( - [this, startPos, endPos, posMax](float t) { + [this, startPos, endPos, posMax, startRow, endRow](float t) { // Non-linear interpolation. t = 1.0f - (1.0f - t) * (1.0f - t); + float f {(endPos * t) + (startPos * (1.0f - t))}; if (f < 0) f += posMax; @@ -584,6 +613,7 @@ template void GridComponent::onCursorChanged(const CursorState& f -= posMax; mEntryOffset = f; + mScrollPos = {(endRow * t) + (startRow * (1.0f - t))}; if (mInstantItemTransitions) { mTransitionFactor = 1.0f; @@ -610,17 +640,17 @@ template void GridComponent::calculateLayout() assert(!mEntries.empty()); unsigned int columnCount {0}; - unsigned int rowCount {0}; + mRowCount = 0; for (auto& entry : mEntries) { entry.data.item->setPosition( - glm::vec3 {mHorizontalOffset + (mItemSize.x * columnCount) + (mItemSize.x * 0.5f) + + glm::vec3 {mHorizontalMargin + (mItemSize.x * columnCount) + (mItemSize.x * 0.5f) + mItemSpacing.x * columnCount, - mVerticalOffset + (mItemSize.y * rowCount) + (mItemSize.y * 0.5f) + - mItemSpacing.y * rowCount, + mVerticalMargin + (mItemSize.y * mRowCount) + (mItemSize.y * 0.5f) + + mItemSpacing.y * mRowCount, 0.0f}); if (columnCount == mColumns - 1) { - ++rowCount; + ++mRowCount; columnCount = 0; continue; } @@ -628,6 +658,10 @@ template void GridComponent::calculateLayout() ++columnCount; } + mVisibleRows = mSize.y / (mItemSize.y + mItemSpacing.y); + mVisibleRows -= (mVerticalMargin / mSize.y) * mVisibleRows * 2.0f; + mVisibleRows += (mItemSpacing.y / mSize.y) * mVisibleRows; + mLayoutValid = true; } From 0cd814a4d1891aafa4e35c846d33c91aa8a16cd3 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Fri, 25 Nov 2022 22:07:36 +0100 Subject: [PATCH 022/101] Added on-demand texture loading to GridComponent. --- es-app/src/views/GamelistBase.cpp | 2 + es-app/src/views/SystemView.cpp | 3 + es-core/src/components/IList.h | 1 + .../components/primary/CarouselComponent.h | 2 +- .../src/components/primary/GridComponent.h | 102 ++++++++++-------- 5 files changed, 66 insertions(+), 44 deletions(-) diff --git a/es-app/src/views/GamelistBase.cpp b/es-app/src/views/GamelistBase.cpp index d03e4bf99..5fd0dfbb4 100644 --- a/es-app/src/views/GamelistBase.cpp +++ b/es-app/src/views/GamelistBase.cpp @@ -698,6 +698,8 @@ void GamelistBase::populateList(const std::vector& files, FileData* f mTextList->addEntry(textListEntry); } } + if (mGrid != nullptr) + mGrid->calculateLayout(); } else { addPlaceholder(firstEntry); diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 8973ac7a9..78797261d 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -727,6 +727,9 @@ void SystemView::populate() } } + if (mGrid != nullptr) + mGrid->calculateLayout(); + for (auto& elements : mSystemElements) { for (auto& text : elements.textComponents) { if (text->getThemeSystemdata() != "") { diff --git a/es-core/src/components/IList.h b/es-core/src/components/IList.h index 888a66042..d49213470 100644 --- a/es-core/src/components/IList.h +++ b/es-core/src/components/IList.h @@ -119,6 +119,7 @@ public: { mEntries.clear(); mCursor = 0; + mLastCursor = 0; listInput(0); onCursorChanged(CursorState::CURSOR_STOPPED); } diff --git a/es-core/src/components/primary/CarouselComponent.h b/es-core/src/components/primary/CarouselComponent.h index 3515a99d5..64238ce68 100644 --- a/es-core/src/components/primary/CarouselComponent.h +++ b/es-core/src/components/primary/CarouselComponent.h @@ -349,7 +349,7 @@ void CarouselComponent::updateEntry(Entry& entry, const std::shared_ptr void CarouselComponent::onDemandTextureLoad() { if constexpr (std::is_same_v) { - const int numEntries {static_cast(mEntries.size())}; + const int numEntries {size()}; const int center {getCursor()}; const bool isWheel {mType == CarouselType::VERTICAL_WHEEL || mType == CarouselType::HORIZONTAL_WHEEL}; diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index 0901634b1..95d67ae17 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -38,6 +38,7 @@ public: void addEntry(Entry& entry, const std::shared_ptr& theme); void updateEntry(Entry& entry, const std::shared_ptr& theme); void onDemandTextureLoad() override; + void calculateLayout(); void setCancelTransitionsCallback(const std::function& func) override { @@ -69,8 +70,6 @@ public: unsigned int properties) override; private: - void calculateLayout(); - void onCursorChanged(const CursorState& state) override; bool isScrolling() const override { return List::isScrolling(); } void stopScrolling() override { List::stopScrolling(); } @@ -98,7 +97,7 @@ private: int mRowCount; std::shared_ptr mFont; - unsigned int mColumns; + int mColumns; glm::vec2 mItemSize; float mItemScale; glm::vec2 mItemSpacing; @@ -119,6 +118,7 @@ private: bool mLayoutValid; bool mRowJump; bool mWasScrolling; + bool mJustCalculatedLayout; }; template @@ -153,6 +153,7 @@ GridComponent::GridComponent() , mLayoutValid {false} , mRowJump {false} , mWasScrolling {false} + , mJustCalculatedLayout {false} { } @@ -212,6 +213,7 @@ template void GridComponent::updateEntry(Entry& entry, const std::shared_ptr& theme) { if (entry.data.itemPath != "") { + const glm::vec3& calculatedItemPos {entry.data.item->getPosition()}; auto item = std::make_shared(false, true); item->setLinearInterpolation(true); item->setMipmapping(true); @@ -221,6 +223,7 @@ void GridComponent::updateEntry(Entry& entry, const std::shared_ptrsetOrigin(0.5f, 0.5f); item->setRotateByTargetSize(true); entry.data.item = item; + entry.data.item->setPosition(calculatedItemPos); } else { return; @@ -230,10 +233,22 @@ void GridComponent::updateEntry(Entry& entry, const std::shared_ptr void GridComponent::onDemandTextureLoad() { if constexpr (std::is_same_v) { - const int numEntries {static_cast(mEntries.size())}; + const int visibleRows {static_cast(std::ceil(mVisibleRows))}; + const int columnPos {mCursor % mColumns}; + const int loadItems {mColumns * visibleRows}; + const int numEntries {size()}; + int startPos {mCursor}; + int loadedItems {0}; - // TODO: Currently loads every item every time. - for (int i {0}; i < size(); ++i) { + if (mCursor / mColumns <= visibleRows - 1) + startPos = 0; + else + startPos = mCursor - (mColumns * (mVisibleRows - 1)) - 1 - columnPos; + + for (int i {startPos}; i < size(); ++i) { + if (loadedItems == loadItems) + break; + ++loadedItems; int cursor {i}; while (cursor < 0) @@ -274,6 +289,37 @@ template void GridComponent::onDemandTextureLoad() } } +template void GridComponent::calculateLayout() +{ + assert(!mEntries.empty()); + + int columnCount {0}; + mRowCount = 0; + + for (auto& entry : mEntries) { + entry.data.item->setPosition( + glm::vec3 {mHorizontalMargin + (mItemSize.x * columnCount) + (mItemSize.x * 0.5f) + + mItemSpacing.x * columnCount, + mVerticalMargin + (mItemSize.y * mRowCount) + (mItemSize.y * 0.5f) + + mItemSpacing.y * mRowCount, + 0.0f}); + if (columnCount == mColumns - 1) { + ++mRowCount; + columnCount = 0; + continue; + } + + ++columnCount; + } + + mVisibleRows = mSize.y / (mItemSize.y + mItemSpacing.y); + mVisibleRows -= (mVerticalMargin / mSize.y) * mVisibleRows * 2.0f; + mVisibleRows += (mItemSpacing.y / mSize.y) * mVisibleRows; + + mLayoutValid = true; + mJustCalculatedLayout = true; +} + template bool GridComponent::input(InputConfig* config, Input input) { if (size() > 0) { @@ -293,7 +339,7 @@ template bool GridComponent::input(InputConfig* config, Input in return true; } if (config->isMappedLike("up", input)) { - if (static_cast(mCursor) < mColumns) + if (mCursor < mColumns) return true; if (mCancelTransitionsCallback) mCancelTransitionsCallback(); @@ -302,8 +348,9 @@ template bool GridComponent::input(InputConfig* config, Input in return true; } if (config->isMappedLike("down", input)) { - if (static_cast(mCursor) >= (mColumns * mRowCount) - mColumns && - mEntries.size() - mCursor <= mColumns && mEntries.size() % mColumns == 0) + if (mCursor >= (mColumns * mRowCount) - mColumns && + static_cast(mEntries.size()) - mCursor <= mColumns && + mEntries.size() % mColumns == 0) return true; if (mCancelTransitionsCallback) mCancelTransitionsCallback(); @@ -350,9 +397,6 @@ template bool GridComponent::input(InputConfig* config, Input in template void GridComponent::update(int deltaTime) { - if (!mLayoutValid) - calculateLayout(); - List::listUpdate(deltaTime); GuiComponent::update(deltaTime); } @@ -528,8 +572,10 @@ template void GridComponent::onCursorChanged(const CursorState& return; } - if (mCursor == mLastCursor) + if (mCursor == mLastCursor && !mJustCalculatedLayout) return; + else + mJustCalculatedLayout = false; float startPos {mEntryOffset}; float posMax {static_cast(mEntries.size())}; @@ -635,34 +681,4 @@ template void GridComponent::onCursorChanged(const CursorState& mWasScrolling = (state == CursorState::CURSOR_SCROLLING); } -template void GridComponent::calculateLayout() -{ - assert(!mEntries.empty()); - - unsigned int columnCount {0}; - mRowCount = 0; - - for (auto& entry : mEntries) { - entry.data.item->setPosition( - glm::vec3 {mHorizontalMargin + (mItemSize.x * columnCount) + (mItemSize.x * 0.5f) + - mItemSpacing.x * columnCount, - mVerticalMargin + (mItemSize.y * mRowCount) + (mItemSize.y * 0.5f) + - mItemSpacing.y * mRowCount, - 0.0f}); - if (columnCount == mColumns - 1) { - ++mRowCount; - columnCount = 0; - continue; - } - - ++columnCount; - } - - mVisibleRows = mSize.y / (mItemSize.y + mItemSpacing.y); - mVisibleRows -= (mVerticalMargin / mSize.y) * mVisibleRows * 2.0f; - mVisibleRows += (mItemSpacing.y / mSize.y) * mVisibleRows; - - mLayoutValid = true; -} - #endif // ES_CORE_COMPONENTS_PRIMARY_GRID_COMPONENT_H From dfcf041efc5b432b60761e784d957851b03262fd Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Fri, 25 Nov 2022 22:35:35 +0100 Subject: [PATCH 023/101] Fixed a GridComponent slide transition issue when used in the system view. Also fixed a general navigation issue. --- es-app/src/views/SystemView.cpp | 26 ++++++++++++------- .../src/components/primary/GridComponent.h | 7 ++--- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 78797261d..e89b863ee 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -318,18 +318,24 @@ void SystemView::onCursorChanged(const CursorState& state) Animation* anim; float animTime {380.0f}; - float timeDiff {1.0f}; - // If startPos is inbetween two positions then reduce the time slightly as the distance will - // be shorter meaning the animation would play for too long if not compensated for. - if (scrollVelocity == 1) - timeDiff = endPos - startPos; - else if (scrollVelocity == -1) - timeDiff = startPos - endPos; + if (mGrid != nullptr) { + animTime = 250.0f; + } + else { + float timeDiff {1.0f}; - if (timeDiff != 1.0f) - animTime = - glm::clamp(std::fabs(glm::mix(0.0f, animTime, timeDiff * 1.5f)), 200.0f, animTime); + // If startPos is inbetween two positions then reduce the time slightly as the distance will + // be shorter meaning the animation would play for too long if not compensated for. + if (scrollVelocity == 1) + timeDiff = endPos - startPos; + else if (scrollVelocity == -1) + timeDiff = startPos - endPos; + + if (timeDiff != 1.0f) + animTime = + glm::clamp(std::fabs(glm::mix(0.0f, animTime, timeDiff * 1.5f)), 200.0f, animTime); + } if (transitionStyle == "fade") { float startFade {mFadeOpacity}; diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index 95d67ae17..e09ac83a7 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -348,9 +348,10 @@ template bool GridComponent::input(InputConfig* config, Input in return true; } if (config->isMappedLike("down", input)) { - if (mCursor >= (mColumns * mRowCount) - mColumns && - static_cast(mEntries.size()) - mCursor <= mColumns && - mEntries.size() % mColumns == 0) + if (mCursor >= (mColumns * mRowCount) - mColumns && size() - mCursor <= mColumns && + size() % mColumns == 0) + return true; + if (size() < mColumns) return true; if (mCancelTransitionsCallback) mCancelTransitionsCallback(); From 22d5886d467218cd9eb7577058ff97bd09f6c690 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sat, 26 Nov 2022 15:04:28 +0100 Subject: [PATCH 024/101] Added fullscreen-specific entries for the Supermodel emulator for the arcade, mame and model3 systems on Linux and Windows. --- resources/systems/unix/es_systems.xml | 3 +++ resources/systems/windows/es_systems.xml | 9 ++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/resources/systems/unix/es_systems.xml b/resources/systems/unix/es_systems.xml index d43befe45..185d217d4 100644 --- a/resources/systems/unix/es_systems.xml +++ b/resources/systems/unix/es_systems.xml @@ -115,6 +115,7 @@ %EMULATOR_FLYCAST% %ROM% %EMULATOR_RETROARCH% -L %CORE_RETROARCH%/kronos_libretro.so %ROM% %STARTDIR%=%GAMEDIR% %EMULATOR_SUPERMODEL% -log-output=%GAMEDIR%/Config/Supermodel.log -force-feedback %INJECT%=%BASENAME%.commands %ROM% + %STARTDIR%=%GAMEDIR% %EMULATOR_SUPERMODEL% -log-output=%GAMEDIR%/Config/Supermodel.log -fullscreen -force-feedback %INJECT%=%BASENAME%.commands %ROM% arcade arcade @@ -677,6 +678,7 @@ %EMULATOR_FLYCAST% %ROM% %EMULATOR_RETROARCH% -L %CORE_RETROARCH%/kronos_libretro.so %ROM% %STARTDIR%=%GAMEDIR% %EMULATOR_SUPERMODEL% -log-output=%GAMEDIR%/Config/Supermodel.log -force-feedback %INJECT%=%BASENAME%.commands %ROM% + %STARTDIR%=%GAMEDIR% %EMULATOR_SUPERMODEL% -log-output=%GAMEDIR%/Config/Supermodel.log -fullscreen -force-feedback %INJECT%=%BASENAME%.commands %ROM% arcade mame @@ -786,6 +788,7 @@ %ROMPATH%/model3 .7z .7Z .zip .ZIP %STARTDIR%=%GAMEDIR% %EMULATOR_SUPERMODEL% -log-output=%GAMEDIR%/Config/Supermodel.log -force-feedback %INJECT%=%BASENAME%.commands %ROM% + %STARTDIR%=%GAMEDIR% %EMULATOR_SUPERMODEL% -log-output=%GAMEDIR%/Config/Supermodel.log -fullscreen -force-feedback %INJECT%=%BASENAME%.commands %ROM% arcade model3 diff --git a/resources/systems/windows/es_systems.xml b/resources/systems/windows/es_systems.xml index 40490195d..946228722 100644 --- a/resources/systems/windows/es_systems.xml +++ b/resources/systems/windows/es_systems.xml @@ -116,7 +116,8 @@ %EMULATOR_RETROARCH% -L %CORE_RETROARCH%\kronos_libretro.dll %ROM% %RUNINBACKGROUND% %STARTDIR%=%EMUDIR% %EMULATOR_M2EMULATOR% %BASENAME% %STARTDIR%=%EMUDIR% %EMULATOR_M2EMULATOR% %BASENAME% - %STARTDIR%=%EMUDIR% %EMULATOR_SUPERMODEL% -fullscreen -force-feedback %INJECT%=%BASENAME%.commands %ROM% + %STARTDIR%=%EMUDIR% %EMULATOR_SUPERMODEL% -force-feedback %INJECT%=%BASENAME%.commands %ROM% + %STARTDIR%=%EMUDIR% %EMULATOR_SUPERMODEL% -fullscreen -force-feedback %INJECT%=%BASENAME%.commands %ROM% arcade arcade @@ -678,7 +679,8 @@ %EMULATOR_RETROARCH% -L %CORE_RETROARCH%\kronos_libretro.dll %ROM% %RUNINBACKGROUND% %STARTDIR%=%EMUDIR% %EMULATOR_M2EMULATOR% %BASENAME% %STARTDIR%=%EMUDIR% %EMULATOR_M2EMULATOR% %BASENAME% - %STARTDIR%=%EMUDIR% %EMULATOR_SUPERMODEL% -fullscreen -force-feedback %INJECT%=%BASENAME%.commands %ROM% + %STARTDIR%=%EMUDIR% %EMULATOR_SUPERMODEL% -force-feedback %INJECT%=%BASENAME%.commands %ROM% + %STARTDIR%=%EMUDIR% %EMULATOR_SUPERMODEL% -fullscreen -force-feedback %INJECT%=%BASENAME%.commands %ROM% arcade mame @@ -788,7 +790,8 @@ Sega Model 3 %ROMPATH%\model3 .7z .7Z .zip .ZIP - %STARTDIR%=%EMUDIR% %EMULATOR_SUPERMODEL% -fullscreen -force-feedback %INJECT%=%BASENAME%.commands %ROM% + %STARTDIR%=%EMUDIR% %EMULATOR_SUPERMODEL% -force-feedback %INJECT%=%BASENAME%.commands %ROM% + %STARTDIR%=%EMUDIR% %EMULATOR_SUPERMODEL% -fullscreen -force-feedback %INJECT%=%BASENAME%.commands %ROM% arcade model3 From 5d69fc7e6988c6820f3ecff17c26bd8f322f71a5 Mon Sep 17 00:00:00 2001 From: Griffin <19497824+WingofaGriffin@users.noreply.github.com> Date: Mon, 28 Nov 2022 22:42:34 -0800 Subject: [PATCH 025/101] add mgba-qt to system for unix install --- resources/systems/unix/es_find_rules.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/systems/unix/es_find_rules.xml b/resources/systems/unix/es_find_rules.xml index 122b0e508..16290b050 100644 --- a/resources/systems/unix/es_find_rules.xml +++ b/resources/systems/unix/es_find_rules.xml @@ -315,6 +315,7 @@ mgba + mgba-qt io.mgba.mGBA @@ -323,6 +324,7 @@ ~/Applications/mGBA*.AppImage ~/.local/bin/mGBA*.AppImage ~/bin/mGBA*.AppImage + ~/bin/mgba-qt From f5efae627839e90191794d5e316c4c75064c4198 Mon Sep 17 00:00:00 2001 From: Griffin <19497824+WingofaGriffin@users.noreply.github.com> Date: Mon, 28 Nov 2022 22:58:33 -0800 Subject: [PATCH 026/101] add snes9x-gtk --- resources/systems/unix/es_find_rules.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/systems/unix/es_find_rules.xml b/resources/systems/unix/es_find_rules.xml index 16290b050..3997e6998 100644 --- a/resources/systems/unix/es_find_rules.xml +++ b/resources/systems/unix/es_find_rules.xml @@ -324,7 +324,6 @@ ~/Applications/mGBA*.AppImage ~/.local/bin/mGBA*.AppImage ~/bin/mGBA*.AppImage - ~/bin/mgba-qt @@ -558,6 +557,7 @@ snes9x + snes9x-gtk /var/lib/flatpak/exports/bin/com.snes9x.Snes9x From 1631cbd30fea48e2f66d53ac9a2cbb155c7844c7 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Fri, 2 Dec 2022 18:08:48 +0100 Subject: [PATCH 027/101] Added automatic layout and dynamic rendering to GridComponent. Also fixed multiple navigation issues. --- es-core/src/ThemeData.cpp | 4 +- es-core/src/components/IList.h | 23 +++ .../src/components/primary/GridComponent.h | 183 +++++++++++++----- 3 files changed, 155 insertions(+), 55 deletions(-) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index fa07f07ad..e0ea74119 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -154,18 +154,16 @@ std::map> {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, - {"columns", UNSIGNED_INTEGER}, {"staticItem", PATH}, {"itemType", STRING}, {"defaultItem", PATH}, + {"fractionalRows", BOOLEAN}, {"itemSize", NORMALIZED_PAIR}, {"itemScale", FLOAT}, {"itemTransitions", STRING}, {"itemSpacing", NORMALIZED_PAIR}, {"itemHorizontalAlignment", STRING}, {"itemVerticalAlignment", STRING}, - {"horizontalMargin", FLOAT}, - {"verticalMargin", FLOAT}, {"unfocusedItemOpacity", FLOAT}, {"edgeScaleInwards", BOOLEAN}, {"color", COLOR}, diff --git a/es-core/src/components/IList.h b/es-core/src/components/IList.h index d49213470..2842ff84b 100644 --- a/es-core/src/components/IList.h +++ b/es-core/src/components/IList.h @@ -74,6 +74,8 @@ protected: const ListLoopType mLoopType; int mCursor; int mLastCursor; + int mColumns; + int mRows; int mScrollTier; int mScrollVelocity; int mScrollTierAccumulator; @@ -90,6 +92,8 @@ public: , mLoopType {loopType} , mCursor {0} , mLastCursor {0} + , mColumns {0} + , mRows {0} , mScrollTier {0} , mScrollVelocity {0} , mScrollTierAccumulator {0} @@ -328,8 +332,27 @@ protected: if (mScrollVelocity == 0 || size() < 2) return; + bool doScroll {true}; + + // This is only needed for GridComponent. + if (mColumns != 0 && mScrollVelocity == -mColumns && mCursor < mColumns) { + doScroll = false; + } + else if (mColumns != 0 && mScrollVelocity == mColumns) { + if (size() - mCursor <= size() % mColumns) + doScroll = false; + else if (mCursor >= (mColumns * mRows) - mColumns && size() - mCursor <= mColumns && + size() % mColumns == 0) + doScroll = false; + else if (size() < mColumns) + doScroll = false; + } + mLastCursor = mCursor; + if (!doScroll) + return; + int cursor {mCursor + amt}; int absAmt {amt < 0 ? -amt : amt}; diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index e09ac83a7..5ec58e201 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -24,9 +24,11 @@ class GridComponent : public PrimaryComponent, protected IList using List = IList; protected: + using List::mColumns; using List::mCursor; using List::mEntries; using List::mLastCursor; + using List::mRows; using List::mScrollVelocity; using List::mSize; @@ -94,10 +96,8 @@ private: float mScrollPos; float mTransitionFactor; float mVisibleRows; - int mRowCount; std::shared_ptr mFont; - int mColumns; glm::vec2 mItemSize; float mItemScale; glm::vec2 mItemSpacing; @@ -115,6 +115,7 @@ private: int mPreviousScrollVelocity; bool mPositiveDirection; bool mGamelistView; + bool mFractionalRows; bool mLayoutValid; bool mRowJump; bool mWasScrolling; @@ -129,9 +130,7 @@ GridComponent::GridComponent() , mScrollPos {0.0f} , mTransitionFactor {1.0f} , mVisibleRows {1.0f} - , mRowCount {1} , mFont {Font::get(FONT_SIZE_LARGE)} - , mColumns {5} , mItemSize {glm::vec2 {mRenderer->getScreenWidth() * 0.15f, mRenderer->getScreenHeight() * 0.25f}} , mItemScale {1.05f} @@ -150,6 +149,7 @@ GridComponent::GridComponent() , mPreviousScrollVelocity {0} , mPositiveDirection {false} , mGamelistView {std::is_same_v ? true : false} + , mFractionalRows {false} , mLayoutValid {false} , mRowJump {false} , mWasScrolling {false} @@ -235,7 +235,8 @@ template void GridComponent::onDemandTextureLoad() if constexpr (std::is_same_v) { const int visibleRows {static_cast(std::ceil(mVisibleRows))}; const int columnPos {mCursor % mColumns}; - const int loadItems {mColumns * visibleRows}; + int loadItems {mColumns * visibleRows}; + const int numEntries {size()}; int startPos {mCursor}; int loadedItems {0}; @@ -243,7 +244,17 @@ template void GridComponent::onDemandTextureLoad() if (mCursor / mColumns <= visibleRows - 1) startPos = 0; else - startPos = mCursor - (mColumns * (mVisibleRows - 1)) - 1 - columnPos; + startPos = mCursor - (mColumns * (visibleRows - 1)) - columnPos; + + if (mItemSpacing.y < mVerticalMargin) { + loadItems += mColumns; + if (!mFractionalRows) { + loadItems += mColumns; + startPos -= mColumns; + if (startPos < 0) + startPos = 0; + } + } for (int i {startPos}; i < size(); ++i) { if (loadedItems == loadItems) @@ -293,18 +304,41 @@ template void GridComponent::calculateLayout() { assert(!mEntries.empty()); + if (mItemScale < 1.0f) { + mHorizontalMargin = 0.0f; + mVerticalMargin = 0.0f; + } + else { + mHorizontalMargin = ((mItemSize.x * mItemScale) - mItemSize.x) / 2.0f; + mVerticalMargin = ((mItemSize.y * mItemScale) - mItemSize.y) / 2.0f; + } + int columnCount {0}; - mRowCount = 0; + mColumns = 0; + mRows = 0; + + float width {mHorizontalMargin * 2.0f}; + + while (1) { + width += mItemSize.x; + if (mColumns != 0) + width += mItemSpacing.x; + if (width > mSize.x) + break; + ++mColumns; + } + + if (mColumns == 0) + ++mColumns; for (auto& entry : mEntries) { - entry.data.item->setPosition( - glm::vec3 {mHorizontalMargin + (mItemSize.x * columnCount) + (mItemSize.x * 0.5f) + - mItemSpacing.x * columnCount, - mVerticalMargin + (mItemSize.y * mRowCount) + (mItemSize.y * 0.5f) + - mItemSpacing.y * mRowCount, - 0.0f}); + entry.data.item->setPosition(glm::vec3 { + mHorizontalMargin + (mItemSize.x * columnCount) + (mItemSize.x * 0.5f) + + mItemSpacing.x * columnCount, + mVerticalMargin + (mItemSize.y * mRows) + (mItemSize.y * 0.5f) + mItemSpacing.y * mRows, + 0.0f}); if (columnCount == mColumns - 1) { - ++mRowCount; + ++mRows; columnCount = 0; continue; } @@ -316,6 +350,12 @@ template void GridComponent::calculateLayout() mVisibleRows -= (mVerticalMargin / mSize.y) * mVisibleRows * 2.0f; mVisibleRows += (mItemSpacing.y / mSize.y) * mVisibleRows; + if (!mFractionalRows) + mVisibleRows = std::floor(mVisibleRows); + + if (mVisibleRows == 0.0f) + ++mVisibleRows; + mLayoutValid = true; mJustCalculatedLayout = true; } @@ -329,18 +369,20 @@ template bool GridComponent::input(InputConfig* config, Input in if (config->isMappedLike("left", input)) { if (mCancelTransitionsCallback) mCancelTransitionsCallback(); + if (mCursor % mColumns == 0) + mRowJump = true; List::listInput(-1); return true; } if (config->isMappedLike("right", input)) { if (mCancelTransitionsCallback) mCancelTransitionsCallback(); + if (mCursor % mColumns == mColumns - 1) + mRowJump = true; List::listInput(1); return true; } if (config->isMappedLike("up", input)) { - if (mCursor < mColumns) - return true; if (mCancelTransitionsCallback) mCancelTransitionsCallback(); mRowJump = true; @@ -348,11 +390,6 @@ template bool GridComponent::input(InputConfig* config, Input in return true; } if (config->isMappedLike("down", input)) { - if (mCursor >= (mColumns * mRowCount) - mColumns && size() - mCursor <= mColumns && - size() % mColumns == 0) - return true; - if (size() < mColumns) - return true; if (mCancelTransitionsCallback) mCancelTransitionsCallback(); mRowJump = true; @@ -404,8 +441,7 @@ template void GridComponent::update(int deltaTime) template void GridComponent::render(const glm::mat4& parentTrans) { - int numEntries {static_cast(mEntries.size())}; - if (numEntries == 0) + if (mEntries.empty()) return; glm::mat4 trans {parentTrans * List::getTransform()}; @@ -417,6 +453,11 @@ template void GridComponent::render(const glm::mat4& parentTrans // Clip to element boundaries. glm::vec3 dim {mSize.x, mSize.y, 0.0f}; + + if (!mFractionalRows && mSize.y > mItemSize.y) + dim.y = mVisibleRows * (mItemSize.y + mItemSpacing.y) + (mVerticalMargin * 2.0f) - + mItemSpacing.y; + dim.x = (trans[0].x * dim.x + trans[3].x) - trans[3].x; dim.y = (trans[1].y * dim.y + trans[3].y) - trans[3].y; @@ -428,13 +469,48 @@ template void GridComponent::render(const glm::mat4& parentTrans // example large scaling or small or no margins between items. std::vector renderEntries; - for (size_t i {0}; i < mEntries.size(); ++i) { - if (i == static_cast(mCursor) || i == static_cast(mLastCursor)) + const int currRow {static_cast(std::ceil(mScrollPos))}; + const int visibleRows {static_cast(std::ceil(mVisibleRows))}; + int startPos {0}; + int loadItems {mColumns * visibleRows}; + int loadedItems {0}; + + if (currRow > 0) { + if (GuiComponent::isAnimationPlaying(0) || mItemSpacing.y < mVerticalMargin) { + loadItems += mColumns; + startPos = (currRow - 1) * mColumns; + } + else { + if (mFractionalRows) + startPos = (currRow - 1) * mColumns; + else + startPos = currRow * mColumns; + } + + if (mItemSpacing.y < mVerticalMargin) { + if (GuiComponent::isAnimationPlaying(0)) { + loadItems += mColumns; + startPos -= mColumns; + if (startPos < 0) + startPos = 0; + } + } + } + + if (!mFractionalRows && mItemSpacing.y < mVerticalMargin) + loadItems += mColumns; + + for (int i {startPos}; i < size(); ++i) { + if (loadedItems == loadItems) + break; + ++loadedItems; + if (i == mCursor || i == mLastCursor) continue; renderEntries.emplace_back(i); } - renderEntries.emplace_back(mLastCursor); + if (mLastCursor >= startPos && mLastCursor < startPos + loadItems) + renderEntries.emplace_back(mLastCursor); if (mLastCursor != mCursor) renderEntries.emplace_back(mCursor); @@ -488,8 +564,7 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, if (!elem) return; - if (elem->has("columns")) - mColumns = glm::clamp(elem->get("columns"), 1u, 100u); + mFractionalRows = (elem->has("fractionalRows") && elem->get("fractionalRows")); if (elem->has("itemSize")) { const glm::vec2& itemSize {glm::clamp(elem->get("itemSize"), 0.05f, 1.0f)}; @@ -544,29 +619,23 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, mItemSpacing.y = ((mItemSize.y * mItemScale) - mItemSize.y) / 2.0f; } - // If horizontalMargin or verticalMargin are not defined, then they are automatically - // calculated so that scaled items don't get clipped at grid boundaries. - if (elem->has("horizontalMargin")) - mHorizontalMargin = glm::clamp(elem->get("horizontalMargin"), -0.5f, 0.5f) * mSize.x; - else if (mItemScale < 1.0f) - mHorizontalMargin = 0.0f; - else - mHorizontalMargin = ((mItemSize.x * mItemScale) - mItemSize.x) / 2.0f; - - if (elem->has("verticalMargin")) - mVerticalMargin = glm::clamp(elem->get("verticalMargin"), -0.5f, 0.5f) * mSize.y; - else if (mItemScale < 1.0f) - mVerticalMargin = 0.0f; - else - mVerticalMargin = ((mItemSize.y * mItemScale) - mItemSize.y) / 2.0f; - if (elem->has("unfocusedItemOpacity")) mUnfocusedItemOpacity = glm::clamp(elem->get("unfocusedItemOpacity"), 0.1f, 1.0f); + + mSize.x = glm::clamp(mSize.x, mRenderer->getScreenWidth() * 0.05f, + mRenderer->getScreenWidth() * 1.0f); + mSize.y = glm::clamp(mSize.y, mRenderer->getScreenHeight() * 0.05f, + mRenderer->getScreenHeight() * 1.0f); } template void GridComponent::onCursorChanged(const CursorState& state) { - if (mWasScrolling && state == CursorState::CURSOR_STOPPED) { + + if (mColumns != 0 && (mScrollVelocity == mColumns || mPreviousScrollVelocity == mColumns) && + size() - mCursor <= size() % mColumns) { + mWasScrolling = false; + } + else if (mWasScrolling && state == CursorState::CURSOR_STOPPED) { if (mCursorChangedCallback) mCursorChangedCallback(state); mWasScrolling = false; @@ -629,22 +698,32 @@ template void GridComponent::onCursorChanged(const CursorState& // glm::clamp(std::fabs(glm::mix(0.0f, animTime, timeDiff * 1.5f)), 180.0f, // animTime); - float visibleRows {mVisibleRows - 1.0f}; + const float visibleRows {mVisibleRows - 1.0f}; float startRow {static_cast(mLastCursor / mColumns)}; float endRow {static_cast(mCursor / mColumns)}; if (endRow <= visibleRows) { - if (startRow == endRow || startRow <= visibleRows) - startRow = 0.0f; - else if (startRow > visibleRows) + if (startRow == endRow || startRow <= visibleRows) { + startRow = mScrollPos; + } + else if (startRow > visibleRows) { + if (!mFractionalRows) + startRow = mScrollPos + 1.0f; startRow -= visibleRows; + } endRow = 0.0f; } else { - if (startRow <= visibleRows) - startRow = 0.0f; - else + if (startRow <= visibleRows) { + startRow = mScrollPos; + } + else { + if (mFractionalRows) + startRow = mScrollPos + visibleRows; + else + startRow = mScrollPos + 1.0f; startRow -= visibleRows; + } endRow -= visibleRows; } From 3ec8ec14cabad8be9afb8310265a9ee57b93c1eb Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sat, 3 Dec 2022 14:01:17 +0100 Subject: [PATCH 028/101] Fixed multiple navigation issues in GridComponent. --- es-core/src/components/IList.h | 4 +- .../src/components/primary/GridComponent.h | 112 +++++++----------- 2 files changed, 48 insertions(+), 68 deletions(-) diff --git a/es-core/src/components/IList.h b/es-core/src/components/IList.h index 2842ff84b..dfa122410 100644 --- a/es-core/src/components/IList.h +++ b/es-core/src/components/IList.h @@ -350,8 +350,10 @@ protected: mLastCursor = mCursor; - if (!doScroll) + if (!doScroll) { + onCursorChanged(CursorState::CURSOR_STOPPED); return; + } int cursor {mCursor + amt}; int absAmt {amt < 0 ? -amt : amt}; diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index 5ec58e201..94e6ed566 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -416,16 +416,9 @@ template bool GridComponent::input(InputConfig* config, Input in config->isMappedLike("up", input) || config->isMappedLike("down", input) || config->isMappedLike("lefttrigger", input) || config->isMappedLike("righttrigger", input)) { - if constexpr (std::is_same_v) { - if (isScrolling()) - onCursorChanged(CursorState::CURSOR_STOPPED); - List::listInput(0); - } - else { - if (isScrolling()) - onCursorChanged(CursorState::CURSOR_STOPPED); - List::listInput(0); - } + if (isScrolling()) + onCursorChanged(CursorState::CURSOR_STOPPED); + List::listInput(0); } } } @@ -630,22 +623,25 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, template void GridComponent::onCursorChanged(const CursorState& state) { + if (mColumns == 0) + return; - if (mColumns != 0 && (mScrollVelocity == mColumns || mPreviousScrollVelocity == mColumns) && - size() - mCursor <= size() % mColumns) { + if (mWasScrolling && state == CursorState::CURSOR_STOPPED && mScrollVelocity != 0) { mWasScrolling = false; - } - else if (mWasScrolling && state == CursorState::CURSOR_STOPPED) { if (mCursorChangedCallback) mCursorChangedCallback(state); - mWasScrolling = false; return; } - if (mCursor == mLastCursor && !mJustCalculatedLayout) + if (mCursor == mLastCursor && !mJustCalculatedLayout) { + mWasScrolling = false; + if (mCursorChangedCallback) + mCursorChangedCallback(state); return; - else + } + else { mJustCalculatedLayout = false; + } float startPos {mEntryOffset}; float posMax {static_cast(mEntries.size())}; @@ -699,62 +695,44 @@ template void GridComponent::onCursorChanged(const CursorState& // animTime); const float visibleRows {mVisibleRows - 1.0f}; - float startRow {static_cast(mLastCursor / mColumns)}; + const float startRow {static_cast(mScrollPos)}; float endRow {static_cast(mCursor / mColumns)}; - if (endRow <= visibleRows) { - if (startRow == endRow || startRow <= visibleRows) { - startRow = mScrollPos; - } - else if (startRow > visibleRows) { - if (!mFractionalRows) - startRow = mScrollPos + 1.0f; - startRow -= visibleRows; - } + if (endRow <= visibleRows) endRow = 0.0f; - } - else { - if (startRow <= visibleRows) { - startRow = mScrollPos; - } - else { - if (mFractionalRows) - startRow = mScrollPos + visibleRows; - else - startRow = mScrollPos + 1.0f; - startRow -= visibleRows; - } + else endRow -= visibleRows; + + if (startPos != endPos) { + Animation* anim {new LambdaAnimation( + [this, startPos, endPos, posMax, startRow, endRow](float t) { + // Non-linear interpolation. + t = 1.0f - (1.0f - t) * (1.0f - t); + + float f {(endPos * t) + (startPos * (1.0f - t))}; + if (f < 0) + f += posMax; + if (f >= posMax) + f -= posMax; + + mEntryOffset = f; + mScrollPos = {(endRow * t) + (startRow * (1.0f - t))}; + + if (mInstantItemTransitions) { + mTransitionFactor = 1.0f; + } + else { + // Linear interpolation. + mTransitionFactor = t; + // Non-linear interpolation doesn't seem to be a good match for this component. + // mTransitionFactor = {(1.0f * t) + (0.0f * (1.0f - t))}; + } + }, + static_cast(animTime))}; + + GuiComponent::setAnimation(anim, 0, nullptr, false, 0); } - Animation* anim {new LambdaAnimation( - [this, startPos, endPos, posMax, startRow, endRow](float t) { - // Non-linear interpolation. - t = 1.0f - (1.0f - t) * (1.0f - t); - - float f {(endPos * t) + (startPos * (1.0f - t))}; - if (f < 0) - f += posMax; - if (f >= posMax) - f -= posMax; - - mEntryOffset = f; - mScrollPos = {(endRow * t) + (startRow * (1.0f - t))}; - - if (mInstantItemTransitions) { - mTransitionFactor = 1.0f; - } - else { - // Linear interpolation. - mTransitionFactor = t; - // Non-linear interpolation doesn't seem to be a good match for this component. - // mTransitionFactor = {(1.0f * t) + (0.0f * (1.0f - t))}; - } - }, - static_cast(animTime))}; - - GuiComponent::setAnimation(anim, 0, nullptr, false, 0); - if (mCursorChangedCallback) mCursorChangedCallback(state); From 0a2a6d19072dfaa444bea54009072314b7d3ec34 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sat, 3 Dec 2022 14:12:14 +0100 Subject: [PATCH 029/101] Added a rowTransitions property to GridComponent. --- es-core/src/ThemeData.cpp | 1 + .../src/components/primary/GridComponent.h | 24 ++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index e0ea74119..c519d778f 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -161,6 +161,7 @@ std::map> {"itemSize", NORMALIZED_PAIR}, {"itemScale", FLOAT}, {"itemTransitions", STRING}, + {"rowTransitions", STRING}, {"itemSpacing", NORMALIZED_PAIR}, {"itemHorizontalAlignment", STRING}, {"itemVerticalAlignment", STRING}, diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index 94e6ed566..c7bb3cec7 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -102,6 +102,7 @@ private: float mItemScale; glm::vec2 mItemSpacing; bool mInstantItemTransitions; + bool mInstantRowTransitions; float mHorizontalMargin; float mVerticalMargin; float mUnfocusedItemOpacity; @@ -136,6 +137,7 @@ GridComponent::GridComponent() , mItemScale {1.05f} , mItemSpacing {0.0f, 0.0f} , mInstantItemTransitions {false} + , mInstantRowTransitions {false} , mHorizontalMargin {0.0f} , mVerticalMargin {0.0f} , mUnfocusedItemOpacity {1.0f} @@ -583,6 +585,22 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, } } + if (elem->has("rowTransitions")) { + const std::string& rowTransitions {elem->get("rowTransitions")}; + if (rowTransitions == "animate") { + mInstantRowTransitions = false; + } + else if (rowTransitions == "instant") { + mInstantRowTransitions = true; + } + else { + mInstantRowTransitions = false; + LOG(LogWarning) << "GridComponent: Invalid theme configuration, property " + "\"rowTransitions\" for element \"" + << element.substr(5) << "\" defined as \"" << rowTransitions << "\""; + } + } + // If itemSpacing is not defined, then it's automatically calculated so that scaled items // don't overlap. If the property is present but one axis is defined as -1 then set this // axis to the same pixel value as the other axis. @@ -716,7 +734,11 @@ template void GridComponent::onCursorChanged(const CursorState& f -= posMax; mEntryOffset = f; - mScrollPos = {(endRow * t) + (startRow * (1.0f - t))}; + + if (mInstantRowTransitions) + mScrollPos = endRow; + else + mScrollPos = {(endRow * t) + (startRow * (1.0f - t))}; if (mInstantItemTransitions) { mTransitionFactor = 1.0f; From 48d86ccea7805bb61a1ab1547604883eed7d85b7 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sat, 3 Dec 2022 14:18:30 +0100 Subject: [PATCH 030/101] Made two member functions in TextListComponent private instead of protected. --- es-core/src/components/primary/TextListComponent.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/es-core/src/components/primary/TextListComponent.h b/es-core/src/components/primary/TextListComponent.h index 94694d71d..bf986a299 100644 --- a/es-core/src/components/primary/TextListComponent.h +++ b/es-core/src/components/primary/TextListComponent.h @@ -82,7 +82,7 @@ public: return mLetterCaseGroupedCollections; } -protected: +private: void onShow() override { mLoopTime = 0; } void onScroll() override { @@ -90,8 +90,6 @@ protected: NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); } 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(); } From b67ce99a0e3988f717d5b866b819d831594597d9 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sat, 3 Dec 2022 14:19:22 +0100 Subject: [PATCH 031/101] Added navigation sound support to GridComponent. --- es-core/src/components/primary/GridComponent.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index c7bb3cec7..70b738e8a 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -72,6 +72,11 @@ public: unsigned int properties) override; private: + void onScroll() override + { + if (!NavigationSounds::getInstance().isPlayingThemeNavigationSound(SCROLLSOUND)) + NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); + } void onCursorChanged(const CursorState& state) override; bool isScrolling() const override { return List::isScrolling(); } void stopScrolling() override { List::stopScrolling(); } From 825d220be0243c6f645274ac799a97e74269890d Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sat, 3 Dec 2022 14:29:17 +0100 Subject: [PATCH 032/101] Animations in GridComponent are now finished on menu opening, game launch etc. --- es-core/src/components/primary/GridComponent.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index 70b738e8a..4adf92ec7 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -79,7 +79,13 @@ private: } void onCursorChanged(const CursorState& state) override; bool isScrolling() const override { return List::isScrolling(); } - void stopScrolling() override { List::stopScrolling(); } + void stopScrolling() override + { + List::stopScrolling(); + // Only finish the animation if we're in the gamelist view. + if (mGamelistView) + GuiComponent::finishAnimation(0); + } const int getScrollingVelocity() override { return List::getScrollingVelocity(); } void clear() override { List::clear(); } const T& getSelected() const override { return List::getSelected(); } From 4b03c90bf37072767b4c031bd15c9a0baf8effe9 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sat, 3 Dec 2022 15:15:53 +0100 Subject: [PATCH 033/101] Fixed an issue where using the trigger buttons did not reset any currently held buttons. --- es-core/src/components/IList.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/es-core/src/components/IList.h b/es-core/src/components/IList.h index dfa122410..158b24ef1 100644 --- a/es-core/src/components/IList.h +++ b/es-core/src/components/IList.h @@ -222,6 +222,8 @@ protected: { mLastCursor = mCursor; mCursor = 0; + mScrollVelocity = 0; + mScrollTier = 0; onCursorChanged(CursorState::CURSOR_STOPPED); onScroll(); return true; @@ -231,6 +233,8 @@ protected: { mLastCursor = mCursor; mCursor = static_cast(mEntries.size()) - 1; + mScrollVelocity = 0; + mScrollTier = 0; onCursorChanged(CursorState::CURSOR_STOPPED); onScroll(); return true; From da93533aedd0fb95a41567c36bf9c8020b5b0ce6 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sun, 4 Dec 2022 15:56:59 +0100 Subject: [PATCH 034/101] Fixed multiple navigation issues. --- es-app/src/views/SystemView.cpp | 39 +++++++- .../src/components/primary/GridComponent.h | 90 +++++++++---------- .../components/primary/TextListComponent.h | 29 +++--- 3 files changed, 95 insertions(+), 63 deletions(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index e89b863ee..8f86e9266 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -205,6 +205,9 @@ std::vector SystemView::getHelpPrompts() else prompts.push_back(HelpPrompt("left/right", "choose")); } + else if (mGrid != nullptr) { + prompts.push_back(HelpPrompt("up/down/left/right", "choose")); + } else if (mTextList != nullptr) { prompts.push_back(HelpPrompt("up/down", "choose")); } @@ -223,9 +226,43 @@ std::vector SystemView::getHelpPrompts() void SystemView::onCursorChanged(const CursorState& state) { const int cursor {mPrimary->getCursor()}; + const int scrollVelocity {mPrimary->getScrollingVelocity()}; const std::string& transitionStyle {Settings::getInstance()->getString("TransitionStyle")}; mFadeTransitions = transitionStyle == "fade"; + // Some logic needed to avoid various navigation glitches with GridComponent and + // TextListComponent. + if (state == CursorState::CURSOR_STOPPED && mCarousel == nullptr) { + const int numEntries {static_cast(mPrimary->getNumEntries())}; + bool doStop {false}; + + if (cursor == 0 && mLastCursor == numEntries - 1 && std::abs(scrollVelocity) == 1) + doStop = false; + else if (cursor == 0) + doStop = true; + else if (cursor == numEntries - 1 && mLastCursor == 0 && std::abs(scrollVelocity) == 1) + doStop = false; + else if (cursor == numEntries - 1) + doStop = true; + + if (!doStop && mGrid != nullptr && std::abs(scrollVelocity) == mGrid->getColumnCount()) { + const int columns {mGrid->getColumnCount()}; + const int columnModulus {numEntries % columns}; + + if (cursor < columns) + doStop = true; + else if (cursor >= numEntries - (columnModulus == 0 ? columns : columnModulus)) + doStop = true; + } + + if (doStop) { + if (mGrid != nullptr) + mGrid->setScrollVelocity(0); + mPrimary->stopScrolling(); + mNavigated = false; + } + } + // Avoid double updates. if (cursor != mLastCursor) { for (auto& selector : mSystemElements[cursor].gameSelectors) { @@ -239,8 +276,6 @@ void SystemView::onCursorChanged(const CursorState& state) video->stopVideoPlayer(); } - const int scrollVelocity {mPrimary->getScrollingVelocity()}; - // This is needed to avoid erratic camera movements during extreme navigation input when using // slide transitions. This should very rarely occur during normal application usage. if (transitionStyle == "slide") { diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index 4adf92ec7..c9fe5317e 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -41,6 +41,9 @@ public: void updateEntry(Entry& entry, const std::shared_ptr& theme); void onDemandTextureLoad() override; void calculateLayout(); + const int getColumnCount() const { return mColumns; } + const int getRowCount() const { return mRows; } + void setScrollVelocity(int velocity) { mScrollVelocity = velocity; } void setCancelTransitionsCallback(const std::function& func) override { @@ -93,7 +96,11 @@ private: 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 setCursor(const T& obj) override + { + mLastCursor = mCursor; + return List::setCursor(obj); + } bool remove(const T& obj) override { return List::remove(obj); } int size() const override { return List::size(); } @@ -129,7 +136,6 @@ private: bool mGamelistView; bool mFractionalRows; bool mLayoutValid; - bool mRowJump; bool mWasScrolling; bool mJustCalculatedLayout; }; @@ -164,7 +170,6 @@ GridComponent::GridComponent() , mGamelistView {std::is_same_v ? true : false} , mFractionalRows {false} , mLayoutValid {false} - , mRowJump {false} , mWasScrolling {false} , mJustCalculatedLayout {false} { @@ -377,36 +382,33 @@ template bool GridComponent::input(InputConfig* config, Input in { if (size() > 0) { if (input.value != 0) { - mRowJump = false; - if (config->isMappedLike("left", input)) { if (mCancelTransitionsCallback) mCancelTransitionsCallback(); - if (mCursor % mColumns == 0) - mRowJump = true; List::listInput(-1); return true; } if (config->isMappedLike("right", input)) { if (mCancelTransitionsCallback) mCancelTransitionsCallback(); - if (mCursor % mColumns == mColumns - 1) - mRowJump = true; List::listInput(1); return true; } if (config->isMappedLike("up", input)) { - if (mCancelTransitionsCallback) - mCancelTransitionsCallback(); - mRowJump = true; - List::listInput(-mColumns); + if (mCursor >= mColumns) { + if (mCancelTransitionsCallback) + mCancelTransitionsCallback(); + List::listInput(-mColumns); + } return true; } if (config->isMappedLike("down", input)) { - if (mCancelTransitionsCallback) - mCancelTransitionsCallback(); - mRowJump = true; - List::listInput(mColumns); + const int columnModulus {size() % mColumns}; + if (mCursor < size() - (columnModulus == 0 ? mColumns : columnModulus)) { + if (mCancelTransitionsCallback) + mCancelTransitionsCallback(); + List::listInput(mColumns); + } return true; } if (config->isMappedLike("lefttrigger", input)) { @@ -732,39 +734,37 @@ template void GridComponent::onCursorChanged(const CursorState& else endRow -= visibleRows; - if (startPos != endPos) { - Animation* anim {new LambdaAnimation( - [this, startPos, endPos, posMax, startRow, endRow](float t) { - // Non-linear interpolation. - t = 1.0f - (1.0f - t) * (1.0f - t); + Animation* anim {new LambdaAnimation( + [this, startPos, endPos, posMax, startRow, endRow](float t) { + // Non-linear interpolation. + t = 1.0f - (1.0f - t) * (1.0f - t); - float f {(endPos * t) + (startPos * (1.0f - t))}; - if (f < 0) - f += posMax; - if (f >= posMax) - f -= posMax; + float f {(endPos * t) + (startPos * (1.0f - t))}; + if (f < 0) + f += posMax; + if (f >= posMax) + f -= posMax; - mEntryOffset = f; + mEntryOffset = f; - if (mInstantRowTransitions) - mScrollPos = endRow; - else - mScrollPos = {(endRow * t) + (startRow * (1.0f - t))}; + if (mInstantRowTransitions) + mScrollPos = endRow; + else + mScrollPos = {(endRow * t) + (startRow * (1.0f - t))}; - if (mInstantItemTransitions) { - mTransitionFactor = 1.0f; - } - else { - // Linear interpolation. - mTransitionFactor = t; - // Non-linear interpolation doesn't seem to be a good match for this component. - // mTransitionFactor = {(1.0f * t) + (0.0f * (1.0f - t))}; - } - }, - static_cast(animTime))}; + if (mInstantItemTransitions) { + mTransitionFactor = 1.0f; + } + else { + // Linear interpolation. + mTransitionFactor = t; + // Non-linear interpolation doesn't seem to be a good match for this component. + // mTransitionFactor = {(1.0f * t) + (0.0f * (1.0f - t))}; + } + }, + static_cast(animTime))}; - GuiComponent::setAnimation(anim, 0, nullptr, false, 0); - } + GuiComponent::setAnimation(anim, 0, nullptr, false, 0); if (mCursorChangedCallback) mCursorChangedCallback(state); diff --git a/es-core/src/components/primary/TextListComponent.h b/es-core/src/components/primary/TextListComponent.h index bf986a299..4484e89a8 100644 --- a/es-core/src/components/primary/TextListComponent.h +++ b/es-core/src/components/primary/TextListComponent.h @@ -201,15 +201,19 @@ template bool TextListComponent::input(InputConfig* config, Inpu return true; } if (config->isMappedLike("leftshoulder", input)) { - if (mCancelTransitionsCallback) - mCancelTransitionsCallback(); - List::listInput(-10); + if (mCursor != 0) { + if (mCancelTransitionsCallback) + mCancelTransitionsCallback(); + List::listInput(-10); + } return true; } if (config->isMappedLike("rightshoulder", input)) { - if (mCancelTransitionsCallback) - mCancelTransitionsCallback(); - List::listInput(10); + if (mCursor != size() - 1) { + if (mCancelTransitionsCallback) + mCancelTransitionsCallback(); + List::listInput(10); + } return true; } if (config->isMappedLike("lefttrigger", input)) { @@ -233,16 +237,9 @@ template bool TextListComponent::input(InputConfig* config, Inpu config->isMappedLike("rightshoulder", input) || config->isMappedLike("lefttrigger", input) || config->isMappedLike("righttrigger", input)) { - if constexpr (std::is_same_v) { - if (isScrolling()) - onCursorChanged(CursorState::CURSOR_STOPPED); - List::listInput(0); - } - else { - if (isScrolling()) - onCursorChanged(CursorState::CURSOR_STOPPED); - List::listInput(0); - } + if (isScrolling()) + onCursorChanged(CursorState::CURSOR_STOPPED); + List::listInput(0); } } } From 8013e04d2c7cad8ab89dfd8245b36b1c8b3d838a Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Mon, 5 Dec 2022 21:08:20 +0100 Subject: [PATCH 035/101] Fixed an issue where folders in mixed gamelists were not always sorted correctly. --- es-app/src/FileData.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 7647a7001..80bcf5a08 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -538,7 +538,7 @@ void FileData::sort(ComparisonFunction& comparator, getSortTypeFromString("filename, ascending").comparisonFunction); } - if (foldersOnTop && mOnlyFolders) + if (foldersOnTop) std::stable_sort(mChildrenFolders.begin(), mChildrenFolders.end(), comparator); std::stable_sort(mChildrenOthers.begin(), mChildrenOthers.end(), comparator); @@ -685,7 +685,7 @@ void FileData::sortFavoritesOnTop(ComparisonFunction& comparator, } // Sort favorite games and the other games separately. - if (foldersOnTop && mOnlyFolders) { + if (foldersOnTop) { std::stable_sort(mChildrenFavoritesFolders.begin(), mChildrenFavoritesFolders.end(), comparator); std::stable_sort(mChildrenFolders.begin(), mChildrenFolders.end(), comparator); From fe1f408355e41017828f221db0e1dd4557b2010e Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Mon, 5 Dec 2022 21:10:26 +0100 Subject: [PATCH 036/101] Changed the menu header for the gamelist options menu from 'Options' to 'Gamelist options'. --- es-app/src/guis/GuiGamelistOptions.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 84f86da58..7a6897336 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -32,7 +32,7 @@ #include GuiGamelistOptions::GuiGamelistOptions(SystemData* system) - : mMenu {"OPTIONS"} + : mMenu {"GAMELIST OPTIONS"} , mSystem {system} , mFiltersChanged {false} , mCancelled {false} From b9e6cdd9da564eee16ac152b34518e07f8c1391b Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Mon, 5 Dec 2022 21:15:15 +0100 Subject: [PATCH 037/101] Fixed some GridComponent animation glitches when marking games as favorites. --- es-app/src/views/GamelistBase.cpp | 11 +++++++++++ es-app/src/views/GamelistView.cpp | 5 +++++ es-core/src/components/primary/GridComponent.h | 6 ++++++ 3 files changed, 22 insertions(+) diff --git a/es-app/src/views/GamelistBase.cpp b/es-app/src/views/GamelistBase.cpp index 5fd0dfbb4..ccf7f00f7 100644 --- a/es-app/src/views/GamelistBase.cpp +++ b/es-app/src/views/GamelistBase.cpp @@ -435,11 +435,18 @@ bool GamelistBase::input(InputConfig* config, Input input) } else if (CollectionSystemsManager::getInstance()->toggleGameInCollection( entryToUpdate)) { + // Needed to avoid some minor transition animation glitches. + auto grid = + ViewController::getInstance()->getGamelistView(system).get()->mGrid.get(); + if (grid != nullptr) + grid->setSuppressTransitions(true); + // 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()}; + // Jump to the first entry in the gamelist if the last favorite was unmarked. if (foldersOnTop && removedLastFavorite && !entryToUpdate->getSystem()->isCustomCollection()) { @@ -457,6 +464,10 @@ bool GamelistBase::input(InputConfig* config, Input input) else if (selectLastEntry && view->getPrimary()->size() > 0) { view->setCursor(view->getLastEntry()); } + + if (grid != nullptr) + grid->setSuppressTransitions(false); + // Display the indication icons which show what games are part of the // custom collection currently being edited. This is done cheaply using // onFileChanged() which will trigger populateList(). diff --git a/es-app/src/views/GamelistView.cpp b/es-app/src/views/GamelistView.cpp index 97cbf24b5..a21214f13 100644 --- a/es-app/src/views/GamelistView.cpp +++ b/es-app/src/views/GamelistView.cpp @@ -49,7 +49,12 @@ void GamelistView::onFileChanged(FileData* file, bool reloadGamelist) FileData* cursor {getCursor()}; if (!cursor->isPlaceHolder()) { populateList(cursor->getParent()->getChildrenListToDisplay(), cursor->getParent()); + // Needed to avoid some minor transition animation glitches. + if (mGrid != nullptr) + mGrid->setSuppressTransitions(true); setCursor(cursor); + if (mGrid != nullptr) + mGrid->setSuppressTransitions(false); } else { populateList(mRoot->getChildrenListToDisplay(), mRoot); diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index c9fe5317e..08ba06d39 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -44,6 +44,7 @@ public: const int getColumnCount() const { return mColumns; } const int getRowCount() const { return mRows; } void setScrollVelocity(int velocity) { mScrollVelocity = velocity; } + void setSuppressTransitions(bool state) { mSuppressTransitions = state; } void setCancelTransitionsCallback(const std::function& func) override { @@ -138,6 +139,7 @@ private: bool mLayoutValid; bool mWasScrolling; bool mJustCalculatedLayout; + bool mSuppressTransitions; }; template @@ -172,6 +174,7 @@ GridComponent::GridComponent() , mLayoutValid {false} , mWasScrolling {false} , mJustCalculatedLayout {false} + , mSuppressTransitions {false} { } @@ -725,6 +728,9 @@ template void GridComponent::onCursorChanged(const CursorState& // glm::clamp(std::fabs(glm::mix(0.0f, animTime, timeDiff * 1.5f)), 180.0f, // animTime); + if (mSuppressTransitions) + animTime = 0.0f; + const float visibleRows {mVisibleRows - 1.0f}; const float startRow {static_cast(mScrollPos)}; float endRow {static_cast(mCursor / mColumns)}; From fbfb84187a3da9020f071aa181bf8b66ece327c5 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Mon, 5 Dec 2022 21:21:48 +0100 Subject: [PATCH 038/101] Changed the button to start the screensaver from 'Back' to 'X'. --- es-app/src/views/SystemView.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 8f86e9266..c2216c349 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -128,7 +128,7 @@ bool SystemView::input(InputConfig* config, Input input) return true; } - if (config->isMappedTo("back", input) && + if (config->isMappedTo("x", input) && Settings::getInstance()->getBool("ScreensaverControls")) { if (!mWindow->isScreensaverActive()) { ViewController::getInstance()->stopScrolling(); @@ -218,7 +218,7 @@ std::vector SystemView::getHelpPrompts() prompts.push_back(HelpPrompt("thumbstickclick", "random")); if (Settings::getInstance()->getBool("ScreensaverControls")) - prompts.push_back(HelpPrompt("back", "screensaver")); + prompts.push_back(HelpPrompt("x", "screensaver")); return prompts; } From 56c8a43b360474898db39b7833abf5b431660cfe Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Mon, 5 Dec 2022 21:37:21 +0100 Subject: [PATCH 039/101] Changed the order of the A, B, X and Y help system buttons. Also changed the gamelist help text from 'enter' and 'launch' to 'select' for the gamelist view. --- es-app/src/views/GamelistView.cpp | 4 ++-- es-core/src/Window.cpp | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/es-app/src/views/GamelistView.cpp b/es-app/src/views/GamelistView.cpp index a21214f13..f950d5c61 100644 --- a/es-app/src/views/GamelistView.cpp +++ b/es-app/src/views/GamelistView.cpp @@ -438,9 +438,9 @@ std::vector GamelistView::getHelpPrompts() if (mRoot->getSystem()->getThemeFolder() == "custom-collections" && mCursorStack.empty() && ViewController::getInstance()->getState().viewing == ViewController::GAMELIST) - prompts.push_back(HelpPrompt("a", "enter")); + prompts.push_back(HelpPrompt("a", "select")); else - prompts.push_back(HelpPrompt("a", "launch")); + prompts.push_back(HelpPrompt("a", "select")); prompts.push_back(HelpPrompt("b", "back")); prompts.push_back(HelpPrompt("x", "view media")); diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 7d3e8bd48..6bee0c52b 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -680,14 +680,14 @@ void Window::setHelpPrompts(const std::vector& prompts, const HelpSt static const std::vector map = {"up/down/left/right", "up/down", "left/right", - "a", - "b", - "x", - "y", - "r", - "l", "rt", "lt", + "r", + "l", + "y", + "x", + "b", + "a", "start", "back"}; int i = 0; From 6bae602a8105b42e8d052364daca005a76cc2478 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Mon, 5 Dec 2022 21:40:33 +0100 Subject: [PATCH 040/101] Changed the help system text in the scraper menu from 'start' to 'start scraper'. --- es-app/src/guis/GuiScraperMenu.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/es-app/src/guis/GuiScraperMenu.cpp b/es-app/src/guis/GuiScraperMenu.cpp index 90da182d7..54df9b680 100644 --- a/es-app/src/guis/GuiScraperMenu.cpp +++ b/es-app/src/guis/GuiScraperMenu.cpp @@ -139,7 +139,7 @@ GuiScraperMenu::GuiScraperMenu(std::string title) addChild(&mMenu); - mMenu.addButton("START", "start", std::bind(&GuiScraperMenu::pressedStart, this)); + mMenu.addButton("START", "start scraper", std::bind(&GuiScraperMenu::pressedStart, this)); mMenu.addButton("BACK", "back", [&] { delete this; }); setSize(mMenu.getSize()); @@ -1192,6 +1192,6 @@ std::vector GuiScraperMenu::getHelpPrompts() { std::vector prompts {mMenu.getHelpPrompts()}; prompts.push_back(HelpPrompt("b", "back")); - prompts.push_back(HelpPrompt("y", "start")); + prompts.push_back(HelpPrompt("y", "start scraper")); return prompts; } From 7e6dfaf90866fb92f41552b3ff5bb5a9abc7eae9 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Mon, 5 Dec 2022 21:48:27 +0100 Subject: [PATCH 041/101] Documentation update. --- CHANGELOG.md | 11 ++++++++-- INSTALL-DEV.md | 2 +- THEMES-DEV.md | 20 ++++++++--------- USERGUIDE-DEV.md | 56 ++++++++++++++++++++++++------------------------ 4 files changed, 47 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 291363796..c400e2c5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * Made fundamental changes to the application logic by removing most view styles and replacing them with a new theme variants concept * Added theme support for defining and applying different layouts for various display aspect ratios such as 16:9 and 4:3 * Added theme support for defining and applying different color schemes +* Added a new grid component that is usable in both the system and gamelist views * Made gamelist theming much more flexible by allowing any number of elements of any types to be defined * Deprecated multiple older theming concepts like features, extras and hardcoded metadata attributes * Renamed the default theme set from rbsimple-DE to slate-DE @@ -21,10 +22,14 @@ * Added support for caching of SVG images * Added support for sizing SVG images arbitrarily (overriding the image aspect ratio by stretching and squashing) * (Windows) Made game launching more seamless by making the application window one pixel wider instead of one pixel less in height +* Changed the order of the help system entries Y, X, B and A to instead be listed as A, B, X and Y +* Changed the start button for the screensaver from "Back" to "X" +* Changed the help system description for the "A" button in the gamelist view from "Launch" to "Select" +* Changed the menu header for the gamelist options menu from "Options" to "Gamelist options" +* Added support for the Nintendo Wii U (wiiu) game system on Linux and macOS by adding the Cemu standalone emulator * Added ares standalone as an alternative emulator for many systems * Added MAME standalone as an alternative emulator for the gameandwatch system * Added openMSX standalone as an alternative emulator for the colecovision, msx, msx1, msx2 and msxturbor systems -* (Linux) Added support for the Nintendo Wii U (wiiu) game system by adding the Cemu standalone emulator * (Linux) Added support for the Sega Model 3 (model3) game system by adding the Supermodel standalone emulator * (Linux) Added Supermodel standalone as an alternative emulator for the arcade and mame systems * Added support for the Sega Model 2 (model2) game system on Linux on macOS by adding the MAME - Current RetroArch core @@ -34,7 +39,7 @@ * Added a %GAMEDIR% variable to the -rompath option for all MAME standalone entries to allow launching games from subdirectories * Added Triforce (Dolphin fork) standalone as an alternative emulator for the gc system on Linux and Windows * Added simple64 standalone as an alternative emulator for the n64 system on Linux and Windows -* (Linux) Added Rosalie's Mupen GUI standalone as an alternative emulator for the n64 system +* Added Rosalie's Mupen GUI standalone as an alternative emulator for the n64 system on Linux and Windows * Added VICE standalone as an alternative emulator for the c64 (x64sc only) and vic20 systems * (Windows) Added PrimeHack as an alternative emulator for the gc and wii systems * (Windows) Added Project64 as an alternative emulator for the n64 system @@ -212,6 +217,7 @@ * Text wrapping did not work correctly for text that typically does not contain spaces, like Japanese * Changing some values using the metadata editor could lead to an incorrect sort order if the changes were done from within a grouped custom collection * Changing the setting "Group unthemed custom collections" could lead to incorrect custom collections sorting under some circumstances +* For gamelists which mixed files and folders, the folder sorting was sometimes incorrect * Games located in subdirectories were not added back to custom collections when disabling the "Exclude from game counter" metadata option * Enabling and then disabling the "Exclude from game counter" metadata option would remove a game from all currently disabled custom collections * Navigation sounds for the trigger buttons would play when repeatedly pressed at the start or end of text lists @@ -227,6 +233,7 @@ * The video player output frame width was not set correctly which made some videos render as garbled when using FFmpeg 5.1 and later * If a gamelist scroll fade-in animation was playing when opening a menu, it would continue to play after closing the menu * The gamelist quick list scrolling overlay would not disappear as intended under some circumstances +* Using the trigger buttons to jump to the start or end of a gamelist did not reset any currently held navigation buttons * When a legacy theme set had a video view style but did not have a valid md_video entry then the video player would still start (and play the audio) * Clearing a game in the metadata editor would sometimes not remove all media files (if there were both a .jpg and a .png for a certain file type) * The tile property for the image element did not work correctly with SVG images diff --git a/INSTALL-DEV.md b/INSTALL-DEV.md index 0d9d28b06..7d98cd75a 100644 --- a/INSTALL-DEV.md +++ b/INSTALL-DEV.md @@ -1804,7 +1804,7 @@ In addition to this extra logging, a few key combinations are enabled when in de **Ctrl + i** -This will draw a semi-transparent red frame behind all image and animation components. It will also draw a green frame around the carousel. +This will draw a semi-transparent red frame behind all image and animation components. It will also draw a green frame around the carousel and grid. **Ctrl + t** diff --git a/THEMES-DEV.md b/THEMES-DEV.md index e2c576c09..87eda840a 100644 --- a/THEMES-DEV.md +++ b/THEMES-DEV.md @@ -1163,7 +1163,7 @@ Properties: **The grid component is currently in active development which means that properties and values may change without prior warning up until the final 2.0.0 release.** -An X*Y grid for navigating and selecting games or systems using the left/right and up/down buttons. +An X*Y grid for navigating and selecting games or systems using the left/right and up/down buttons. The layout including the amount of columns and rows is automatically calculated based on the relevant property values. Supported views: * `system` @@ -1176,15 +1176,12 @@ Properties: * `pos` - type: NORMALIZED_PAIR - Default is `0 0.1` * `size` - type: NORMALIZED_PAIR + - Minimum value per axis is `0.05` and maximum value per axis is `1` - Default is `1 0.8` * `origin` - type: NORMALIZED_PAIR - Where on the element `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the textlist exactly in the middle of the screen. If the position and size attributes are themeable, origin is implied. - Minimum value per axis is `0` and maximum value per axis is `1` - Default is `0 0` -* `columns` - type: UNSIGNED_INTEGER - - The number of columns to add to the grid. The row count will be automatically calculated by dividing the overall number of items by the value of this property. - - Minimum value is `1` and maximum value is `100` - - Default is `5` * `staticItem` - type: PATH - Path to a static image file. Most common extensions are supported (including .svg, .jpg, .png, and unanimated .gif). This property can only be used in the `system` view. * `itemType` - type: STRING @@ -1203,6 +1200,9 @@ Properties: - Default is `marquee` * `defaultItem` - type: PATH - Path to the default image file which will be displayed if the image defined via the `staticItem` or `itemType` property is not found. Most common extensions are supported (including .svg, .jpg, .png, and unanimated .gif). +* `fractionalRows` - type: BOOLEAN + - Whether to allow rendering of fractional rows of items. If set to false then the effective area of the overall element size will be snapped to the item height multiplied by `itemScale`. Note that if setting `itemScale` too high relative to the `itemSpacing` Y axis value then fractional rows may still be rendered even if the `fractionalRows` property is set to false. + - Default is `false` * `itemSize` - type: NORMALIZED_PAIR - Size of the item prior to multiplication by the `itemScale` value, i.e. the size of all unselected items. Both axes need to be defined. - Minimum value per axis is `0.05` and maximum value per axis is `1` @@ -1215,15 +1215,13 @@ Properties: - How to render item transitions when navigating the grid. By default a scaling and opacity fade animation will be played when moving between items (assuming `itemScale` and `unfocusedItemOpacity` have not been set to `1`) but if this property is set to `instant` then the transitions will be immediate. - Valid values are `animate` or `instant` - Default is `animate` +* `rowTransitions` - type: STRING + - How to render row transitions when navigating the grid. By default a sliding animation will be rendered when moving between rows but if this property is set to `instant` then the transitions will be immediate. + - Valid values are `animate` or `instant` + - Default is `animate` * `itemSpacing` - type: NORMALIZED_PAIR - The horizontal and vertical space between items. This value is added to the unscaled item size, i.e. `itemSize` before it's been multiplied by `itemScale`. This means that if an axis is set to `0` then unscaled items will be perfectly adjacent to each other on that axis but if `itemScale` has been set to higher than `1` then the currently selected item may overlap adjacent items. If this property is omitted then spacing will be automatically calculated so that no overlaps occur during scaling. However you'd normally want to define and adjust this property for an optimal layout. If one of the axis is defined as `-1` then it will be set to the same pixel value as the other axis. Note that all spacing calculations are based on the value defined by `itemSize` which may or may not be the same as the actual image sizes, depending on their aspect ratios. - Minimum value per axis is `0` and maximum value per axis is `0.1` -* `horizontalOffset` - type: FLOAT - - Offsets the grid horizontally inside its designated area, as defined by the `size` property. The value is relative to the size of the grid with `1` being equivalent to its entire width. This property is primarily used to make sure scaled items are not clipped at grid boundaries, but if omitted the offset is automatically calculated so you normally don't need to define it unless you have a very specific need to fine-tune the layout. - - Minimum value is `-0.5` and maximum value is `0.5` -* `verticalOffset` - type: FLOAT - - Offsets the grid vertically inside its designated area, as defined by the `size` property. The value is relative to the size of the grid with `1` being equivalent to its entire height. This property is primarily used to make sure scaled items are not clipped at grid boundaries, but if omitted the offset is automatically calculated so you normally don't need to define it unless you have a very specific need to fine-tune the layout. - - Minimum value is `-0.5` and maximum value is `0.5` * `unfocusedItemOpacity` - type: FLOAT - Sets the opacity for the items that are not currently focused. - Minimum value is `0.1` and maximum value is `1` diff --git a/USERGUIDE-DEV.md b/USERGUIDE-DEV.md index 45ccbfd49..198e7f737 100644 --- a/USERGUIDE-DEV.md +++ b/USERGUIDE-DEV.md @@ -481,8 +481,8 @@ ES-DE supports three separate modes, **Full**, **Kiosk** and **Kid**. These modes mandate the functionalty provided by the application in the following way: * Full - This is the default mode which enables all functionality. -* Kiosk - The main menu will be severely restricted, only displaying the entry to change the audio volume. The game options menu will be restricted as well, removing the metadata editor and the ability to modify custom game collections. And finally the ability to flag or unflag games as favorites will be removed. Apart from this all games will be playable. -* Kid - Only games marked as being suitable for children will be displayed (this flag is set manually per game using the metadata editor). Additionally, the game options menu is disabled as is the ability to flag and unflag games as favorites. There is also a separate option available to enable or disable the main menu when in Kid mode, see _Enable menu in kid mode_ for additional information. +* Kiosk - The main menu will be severely restricted, only displaying the entry to change the audio volume. The gamelist options menu will be restricted as well, removing the metadata editor and the ability to modify custom game collections. And finally the ability to flag or unflag games as favorites will be removed. Apart from this all games will be playable. +* Kid - Only games marked as being suitable for children will be displayed (this flag is set manually per game using the metadata editor). Additionally, the gamelist options menu is disabled as is the ability to flag and unflag games as favorites. There is also a separate option available to enable or disable the main menu when in Kid mode, see _Enable menu in kid mode_ for additional information. There is an unlock code available to revert to the Full mode from the Kiosk or Kid mode, as is described when changing this setting from the main menu. By default the button sequence is **Up, Up, Down, Down, Left, Right, Left, Right, B, A** (or equivalent buttons if an Xbox controller is not used). Either the keyboard or a controller can be used to input the passkey sequence, but it can't be entered when a menu is open. @@ -521,7 +521,7 @@ Opens and closes the main menu. **Back button**\ _(F1)_ -Opens and closes the game options menu in the gamelist view, or toggles the screensaver in the system view (if the _Enable screensaver controls_ setting is enabled). +Opens and closes the gamelist options menu in the gamelist view. **Left and right shoulder buttons**\ _(Page up / Page down)_ @@ -541,7 +541,7 @@ Jumps to a random game or system depending on whether pressed when in the system **A button**\ _(Enter)_ -Opens gamelists from the system view, launches games, enters folders, selects menu entries etc. +Select button which opens gamelists from the system view, launches games, enters folders, selects menu entries etc. **B button**\ _(Back key)_ @@ -551,7 +551,7 @@ Back button, self explanatory. **X button**\ _(Delete)_ -Starts the game media viewer (which is accessible from the gamelist views). Used by some other minor functions as explained by the help system and/or this guide. +Starts the media viewer in the gamelist view or the screensaver in the system view (if the _Enable screensaver controls_ setting is enabled). Used by some other minor functions as explained by the help system and/or this guide. **Y button**\ _(Insert on Unix and Windows, F13 on macOS)_ @@ -1000,11 +1000,9 @@ Apart from the potential difficulty in locating the emulator binary, there are s #### Nintendo Wii U -Cemu is available for Windows and Linux. +The .wua archive format is the preferred method to use for Wii U games, but the method of using unpacked games is also documented here for reference. -Some time ago Cemu added support for the .wua archive format which is much easier to use than the unpacked game format. Therefore this is now the recommended approach, but both this and the traditional method of adding unpacked games are covered here. - -.wud and .wux files are also supported, but these two formats are not discussed here as the .wua format is clearly the way to go in the future. +.wud and .wux files are supported as well, but these two formats are not discussed here as the .wua format is clearly the way to go in the future. **Method 1, using .wua files** @@ -1014,6 +1012,8 @@ Following this just start ES-DE and the game should be shown as a single entry t **Method 2, unpacked games** +Only the setup on Windows is covered here, but it's the same principle in Linux and macOS. + Using this unpacked approach, the content of each game is divided into the three directories _code, content_ and _meta_. The first step is to prepare the target directory in the `wiiu` ROMs directory, for this example we'll go for the game _Super Mario 3D World_. So simply create a directory with this name inside the wiiu folder. It should look something like the following: @@ -1942,11 +1942,11 @@ _Here's an example of the multi-scraper running in interactive mode, asking the ### Single-game scraper -The single-game scraper is launched from the metadata editor. You navigate to a game, open the game options menu, choose **Edit this game's metadata** and then select the **Scrape** button (alternatively the "Y" button shortcut can be used to start the scraper). +The single-game scraper is launched from the metadata editor. You navigate to a game, open the gamelist options menu, choose **Edit this game's metadata** and then select the **Scrape** button (alternatively the _Y_ button shortcut can be used to start the scraper). ### Multi-scraper -The multi-scraper is accessed from the main menu by entering the **Scraper** menu and then selecting the **Start** button (or the "Y" button shortcut can be used). +The multi-scraper is accessed from the main menu by entering the **Scraper** menu and then selecting the **Start** button (or the _Y_ button shortcut can be used). ### Scraping process @@ -2301,11 +2301,11 @@ If set to _None_, the system view will be displayed. Any other value will jump t **Default sort order** -The order in which to sort your gamelists. This can be overriden per game system using the game options menu, but that override will only be persistent during the application session. The _System_ sorting can not be selected here as it's only applicable to collection systems. +The order in which to sort your gamelists. This can be overriden per game system using the gamelist options menu, but that override will only be persistent during the application session. The _System_ sorting can not be selected here as it's only applicable to collection systems. **Menu opening effect** -Animation to play when opening the main menu or the game options menu. Also sets the animation for the game launch screen. Can be set to _Scale-up_ or _None_. +Animation to play when opening the main menu or the gamelist options menu. Also sets the animation for the game launch screen. Can be set to _Scale-up_ or _None_. **Launch screen duration** @@ -2663,14 +2663,14 @@ Self explanatory. Self explanatory. -## Game options menu +## Gamelist options menu This menu is opened from the gamelist view, and can't be accessed from the system view. The menu changes slightly depending on the context, for example if a game file or a folder is selected, or whether the current system is a collection or a normal game system. You open the menu using the **Back** button, and by pressing **B** or selecting the **Apply** button any settings such as letter jumping using the quick selector or sorting changes are applied. If you instead press the Back button again or select the **Cancel** button, the menu is closed without applying any changes. -![alt text](images/es-de_game_options_menu.png "ES-DE Game Options Menu") -_The game options menu as laid out when opening it from within a custom collection, which adds the menu entry to add or remove games from the collection._ +![alt text](images/es-de_game_options_menu.png "ES-DE Gamelist Options Menu") +_The gamelist options menu as laid out when opening it from within a custom collection, which adds the menu entry to add or remove games from the collection._ Here's a summary of the menu entries: @@ -2680,7 +2680,7 @@ This provides a quick selector for jumping to a certain letter. If the setting t ### Sort games by -This is the sort order for the gamelist. The default sorting shown here is taken from the setting _Default sort order_ under _UI settings_ in the main menu. Any sorting that is applied via the game options menu will be persistent throughout the program session, and it can be set individually per game system and per collection. +This is the sort order for the gamelist. The default sorting shown here is taken from the setting _Default sort order_ under _UI settings_ in the main menu. Any sorting that is applied via the gamelist options menu will be persistent throughout the program session, and it can be set individually per game system and per collection. Sorting can be applied using the following metadata, in either ascending or descending order: @@ -2702,7 +2702,7 @@ The secondary sorting is always in ascending filename order. Choosing this entry opens a separate screen where it's possible to apply a filter to the gamelist. The filter is persistent throughout the program session, or until it's manually reset. The option to reset all filters is shown on the same screen. ![alt text](images/es-de_gamelist_filters.png "ES-DE Gamelist Filters") -_The gamelist filter screen, accessed from the game options menu._ +_The gamelist filter screen, accessed from the gamelist options menu._ The following filters can be applied: @@ -2775,7 +2775,7 @@ This is the name that will be shown when browsing the gamelist. If no sortname h **Sortname** _(files only)_ -This entry makes it possible to change the sorting of a game without having to change its name. For instance it can be used to sort _Mille Miglia_ as _1000 Miglia_ or _The Punisher_ as _Punisher, The_. Note that the _Jump to..._ quick selector on the game options menu will base its index on the first character of the sortname if it exists for a game, which could be slightly confusing in some instances when quick jumping in the gamelist. The _sortname_ entry also affects custom collections, although for these it's possible to override the value as described below. This entry only applies if the sort order has been set to _Filename, Ascending_ or _Filename, Descending_. +This entry makes it possible to change the sorting of a game without having to change its name. For instance it can be used to sort _Mille Miglia_ as _1000 Miglia_ or _The Punisher_ as _Punisher, The_. Note that the _Jump to..._ quick selector on the gamelist options menu will base its index on the first character of the sortname if it exists for a game, which could be slightly confusing in some instances when quick jumping in the gamelist. The _sortname_ entry also affects custom collections, although for these it's possible to override the value as described below. This entry only applies if the sort order has been set to _Filename, Ascending_ or _Filename, Descending_. **Custom collections sortname** _(only visible when editing a game from within a custom collection)_ @@ -2855,7 +2855,7 @@ If the option _Enable alternative emulators per game_ has been enabled, there wi **Folder link** _(folders only)_ -Using this option it's possible to link a specific file inside the game's folder structure that will be launched directly instead of entering the folder when pressing the _A_ button. This is very useful for systems where there are multiple files per game, such as multi-disc games where an .m3u file is used to launch the game. As the name implies this is only a link, and as folders can't be part of collections (the automatic collections _All games, Favorites_ and _Last played_ as well as any custom collections) it's the linked file inside the folder that is included in such collections. So for instance, launching a game via a linked folder will have the linked file show up in the _Last played_ collection rather than the folder itself. This also means that you should scrape the linked file in addition to the folder to be able to see game media and metadata throughout the application. To override the folder link and enter the directory, there is an entry available in the game options menu. +Using this option it's possible to link a specific file inside the game's folder structure that will be launched directly instead of entering the folder when pressing the _A_ button. This is very useful for systems where there are multiple files per game, such as multi-disc games where an .m3u file is used to launch the game. As the name implies this is only a link, and as folders can't be part of collections (the automatic collections _All games, Favorites_ and _Last played_ as well as any custom collections) it's the linked file inside the folder that is included in such collections. So for instance, launching a game via a linked folder will have the linked file show up in the _Last played_ collection rather than the folder itself. This also means that you should scrape the linked file in addition to the folder to be able to see game media and metadata throughout the application. To override the folder link and enter the directory, there is an entry available in the gamelist options menu. ### Buttons @@ -2901,7 +2901,7 @@ Numerous options can be set for these screensavers, as detailed [here](USERGUIDE The Dim screensaver simply dims and desaturates the current view and Black will show a black screen. The Slideshow and Video screensavers are more interesting as they can display images and videos from your game collection. In addition to this, the Slideshow screensaver can be configured to only show images from a specified directory. -If the option _Enable screensaver controls_ has been activated, you can manually toggle the screensaver from the system view by pressing the _Back_ button. In addition to this, for the Slideshow and Video screensavers, the controls will allow you to jump to a new random image or video by using the _Left_ and _Right_ buttons on your keyboard or controller. It's also possible to launch the game currently displayed using the _A_ button, and the _Y_ button will jump to the game in its gamelist without starting it. +If the option _Enable screensaver controls_ has been activated, you can manually toggle the screensaver from the system view by pressing the _X_ button. In addition to this, for the Slideshow and Video screensavers, the controls will allow you to jump to a new random image or video by using the _Left_ and _Right_ buttons on your keyboard or controller. It's also possible to launch the game currently displayed using the _A_ button, and the _Y_ button will jump to the game in its gamelist without starting it. For the video and slideshow screensavers, an overlay can be enabled via the screensaver options that displays the game name and the game system as well as a star symbol if the game is marked as a favorite. @@ -2944,9 +2944,9 @@ During the time that the collection is being edited, any game that is part of th As well, when editing custom collections the _folder link_ configuration is disabled, making it possible to enter folders with such configuration just as if there were no folder links configured. -When you are done adding games, you can either open the main menu and go to **Game collection settings** and select the **Finish editing 'Platform' collection** or you can open the game options menu and select the same option there. The latter works from within any gamelist, so you don't need to first navigate back to the collection that you're editing. +When you are done adding games, you can either open the main menu and go to **Game collection settings** and select the **Finish editing 'Platform' collection** or you can open the gamelist options menu and select the same option there. The latter works from within any gamelist, so you don't need to first navigate back to the collection that you're editing. -You can later add additional games to the collection by navigating to it, bringing up the game options menu and choosing **Add/remove games to this game collection**. +You can later add additional games to the collection by navigating to it, bringing up the gamelist options menu and choosing **Add/remove games to this game collection**. ![alt text](images/es-de_custom_collections.png "ES-DE Custom Collections") _Example of custom collections, here configured as genres._ @@ -3071,7 +3071,7 @@ The **@** symbol indicates that the emulator is _deprecated_ and will be removed | android | Google Android | BlueStacks **(Standalone)** [W] | | No | Shortcut (.lnk) file in root folder | | apple2 | Apple II | LinApple **(Standalone)** [U],
Mednafen **(Standalone)** [M],
AppleWin **(Standalone)** [W*] | Mednafen **(Standalone)** [UW*],
MAME **(Standalone)** [UMW*] | Yes for Mednafen and MAME | See the specific _Apple II_ section elsewhere in this guide | | apple2gs | Apple IIGS | MAME **(Standalone)** [UMW*] | | Yes | See the specific _Apple IIGS_ section elsewhere in this guide | -| arcade | Arcade | MAME - Current | MAME 2010,
MAME 2003-Plus,
MAME 2000,
MAME **(Standalone)** [UMW*],
FinalBurn Neo,
FB Alpha 2012,
Flycast,
Flycast **(Standalone)** [UMW*],
Kronos [UW],
Model 2 Emulator **(Standalone)** [W*],
Model 2 Emulator [Suspend ES-DE] **(Standalone)** [W*],
Supermodel **(Standalone)** [UW*] | Depends | See the specific _Arcade and Neo Geo_ section elsewhere in this guide | +| arcade | Arcade | MAME - Current | MAME 2010,
MAME 2003-Plus,
MAME 2000,
MAME **(Standalone)** [UMW*],
FinalBurn Neo,
FB Alpha 2012,
Flycast,
Flycast **(Standalone)** [UMW*],
Kronos [UW],
Model 2 Emulator **(Standalone)** [W*],
Model 2 Emulator [Suspend ES-DE] **(Standalone)** [W*],
Supermodel **(Standalone)** [UW*],
Supermodel [Fullscreen] **(Standalone)** [UW*] | Depends | See the specific _Arcade and Neo Geo_ section elsewhere in this guide | | astrocde | Bally Astrocade | MAME - Current | MAME **(Standalone)** [UMW*] | | See the specific _Bally Astrocade_ section elsewhere in this guide | | atari2600 | Atari 2600 | Stella | Stella 2014,
ares **(Standalone)** [UMW*] | No | Single archive or ROM file in root folder | | atari5200 | Atari 5200 | a5200 | Atari800,
Atari800 **(Standalone)** [UMW*] | Yes | | @@ -3121,7 +3121,7 @@ The **@** symbol indicates that the emulator is _deprecated_ and will be removed | lutris | Lutris Open Gaming Platform | Lutris application **(Standalone)** [U] | | No | See the specific _Lutris_ section elsewhere in this guide | | lutro | Lutro Game Engine | Lutro | | | | | macintosh | Apple Macintosh | Basilisk II **(Standalone)** [UMW*] | SheepShaver **(Standalone)** [UMW*] | Yes | See the specific _Apple Macintosh_ section elsewhere in this guide | -| mame | Multiple Arcade Machine Emulator | MAME - Current | MAME 2010,
MAME 2003-Plus,
MAME 2000,
MAME **(Standalone)** [UMW*],
FinalBurn Neo,
FB Alpha 2012,
Flycast,
Flycast **(Standalone)** [UMW*],
Kronos [UW],
Model 2 Emulator **(Standalone)** [W*],
Model 2 Emulator [Suspend ES-DE] **(Standalone)** [W*],
Supermodel **(Standalone)** [UW*] | Depends | See the specific _Arcade and Neo Geo_ section elsewhere in this guide | +| mame | Multiple Arcade Machine Emulator | MAME - Current | MAME 2010,
MAME 2003-Plus,
MAME 2000,
MAME **(Standalone)** [UMW*],
FinalBurn Neo,
FB Alpha 2012,
Flycast,
Flycast **(Standalone)** [UMW*],
Kronos [UW],
Model 2 Emulator **(Standalone)** [W*],
Model 2 Emulator [Suspend ES-DE] **(Standalone)** [W*],
Supermodel **(Standalone)** [UW*],
Supermodel [Fullscreen] **(Standalone)** [UW*] | Depends | See the specific _Arcade and Neo Geo_ section elsewhere in this guide | | mame-advmame | AdvanceMAME | _Placeholder_ | | Depends | | | mame-mame4all | MAME4ALL | _Placeholder_ | | Depends | | | mastersystem | Sega Master System | Genesis Plus GX | Genesis Plus GX Wide,
SMS Plus GX,
Gearsystem,
PicoDrive,
Mednafen **(Standalone)** [UMW*],
ares **(Standalone)** [UMW*] | No | Single archive or ROM file in root folder | @@ -3131,7 +3131,7 @@ The **@** symbol indicates that the emulator is _deprecated_ and will be removed | megaduck | Creatronic Mega Duck | SameDuck | | No | Single archive or ROM file in root folder | | mess | Multi Emulator Super System | MESS 2015 | | | | | model2 | Sega Model 2 | Model 2 Emulator **(Standalone)** [W*],
MAME - Current [UM] | Model 2 Emulator [Suspend ES-DE] **(Standalone)** [W*],
MAME - Current [W],
MAME **(Standalone)** [UMW*] | Yes for MAME | See the specific _Arcade and Neo Geo_ section elsewhere in this guide | -| model3 | Sega Model 3 | Supermodel **(Standalone)** [UW*] | | No | See the specific _Arcade and Neo Geo_ section elsewhere in this guide | +| model3 | Sega Model 3 | Supermodel **(Standalone)** [UW*] | Supermodel [Fullscreen] **(Standalone)** [UW*] | No | See the specific _Arcade and Neo Geo_ section elsewhere in this guide | | moonlight | Moonlight Game Streaming | _Placeholder_ | | | | | moto | Thomson MO/TO Series | Theodore | | | | | msx | MSX | blueMSX | fMSX,
openMSX **(Standalone)** [UMW*],
openMSX No Machine **(Standalone)** [UMW*],
ares **(Standalone)** [UMW*] | Yes | | @@ -3143,7 +3143,7 @@ The **@** symbol indicates that the emulator is _deprecated_ and will be removed | naomi | Sega NAOMI | Flycast | Flycast **(Standalone)** [UMW*] | | | | naomigd | Sega NAOMI GD-ROM | Flycast | Flycast **(Standalone)** [UMW*] | | | | n3ds | Nintendo 3DS | Citra [UW],
Citra **(Standalone)** [M] | Citra 2018 [UW],
Citra **(Standalone)** [UW*] | No | Single ROM file in root folder | -| n64 | Nintendo 64 | Mupen64Plus-Next [UW],
ParaLLEl N64 [M] | Mupen64Plus **(Standalone)** [UMW*],
ParaLLEl N64 [UW],
simple64 **(Standalone)** [UW*],
Rosalie's Mupen GUI **(Standalone)** [U],
Project64 **(Standalone)** [W*],
ares **(Standalone)** [UMW*],
sixtyforce **(Standalone)** [M] | No | Single archive or ROM file in root folder | +| n64 | Nintendo 64 | Mupen64Plus-Next [UW],
ParaLLEl N64 [M] | Mupen64Plus **(Standalone)** [UMW*],
ParaLLEl N64 [UW],
simple64 **(Standalone)** [UW*],
Rosalie's Mupen GUI **(Standalone)** [UW],
Project64 **(Standalone)** [W*],
ares **(Standalone)** [UMW*],
sixtyforce **(Standalone)** [M] | No | Single archive or ROM file in root folder | | n64dd | Nintendo 64DD | ParaLLEl N64 | Mupen64Plus-Next [UW] | Yes | See the specific _Nintendo 64DD_ section elsewhere in this guide | | nds | Nintendo DS | DeSmuME | DeSmuME 2015,
DeSmuME **(Standalone)** [U],
melonDS,
melonDS **(Standalone)** [UMW*] | No | | | neogeo | SNK Neo Geo | FinalBurn Neo | FinalBurn Neo **(Standalone)** [UW*],
MAME **(Standalone)** [UMW*] | Yes | See the specific _Arcade and Neo Geo_ section elsewhere in this guide | @@ -3207,7 +3207,7 @@ The **@** symbol indicates that the emulator is _deprecated_ and will be removed | videopac | Philips Videopac G7000 | O2EM | | | | | virtualboy | Nintendo Virtual Boy | Beetle VB | Mednafen **(Standalone)** [UMW*] | No | | | wii | Nintendo Wii | Dolphin | Dolphin **(Standalone)** [UMW*],
PrimeHack **(Standalone)** [UW*] | No | | -| wiiu | Nintendo Wii U | Cemu **(Standalone)** [UW*] | | No | See the specific _Nintendo Wii U_ section elsewhere in this guide | +| wiiu | Nintendo Wii U | Cemu **(Standalone)** [UMW*] | | No | See the specific _Nintendo Wii U_ section elsewhere in this guide | | wonderswan | Bandai WonderSwan | Beetle Cygne | Mednafen **(Standalone)** [UMW*],
ares **(Standalone)** [UMW*] | No | | | wonderswancolor | Bandai WonderSwan Color | Beetle Cygne | Mednafen **(Standalone)** [UMW*],
ares **(Standalone)** [UMW*] | No | | | x1 | Sharp X1 | x1 | | | Single archive or ROM file in root folder | From 5a18b593f40f3333b66f9fd35db5a388b1b568fe Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Mon, 5 Dec 2022 23:12:41 +0100 Subject: [PATCH 042/101] Fixed a SystemView navigation rendering glitch when using GridComponent. --- es-app/src/views/SystemView.cpp | 29 ++++++++++--------- .../src/components/primary/GridComponent.h | 2 +- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index c2216c349..2e30a8fb0 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -353,24 +353,24 @@ void SystemView::onCursorChanged(const CursorState& state) Animation* anim; float animTime {380.0f}; + float timeMin {200.0f}; + float timeDiff {1.0f}; if (mGrid != nullptr) { animTime = 250.0f; + timeMin = 100.0f; } - else { - float timeDiff {1.0f}; - // If startPos is inbetween two positions then reduce the time slightly as the distance will - // be shorter meaning the animation would play for too long if not compensated for. - if (scrollVelocity == 1) - timeDiff = endPos - startPos; - else if (scrollVelocity == -1) - timeDiff = startPos - endPos; + // If startPos is inbetween two positions then reduce the time slightly as the distance will + // be shorter meaning the animation would play for too long if not compensated for. + if (scrollVelocity == 1) + timeDiff = endPos - startPos; + else if (scrollVelocity == -1) + timeDiff = startPos - endPos; - if (timeDiff != 1.0f) - animTime = - glm::clamp(std::fabs(glm::mix(0.0f, animTime, timeDiff * 1.5f)), 200.0f, animTime); - } + if (timeDiff != 1.0f) + animTime = + glm::clamp(std::fabs(glm::mix(0.0f, animTime, timeDiff * 1.5f)), timeMin, animTime); if (transitionStyle == "fade") { float startFade {mFadeOpacity}; @@ -405,6 +405,7 @@ void SystemView::onCursorChanged(const CursorState& state) // Non-linear interpolation. t = 1.0f - (1.0f - t) * (1.0f - t); float f {(endPos * t) + (startPos * (1.0f - t))}; + if (f < 0) f += posMax; if (f >= posMax) @@ -1327,7 +1328,7 @@ void SystemView::renderElements(const glm::mat4& parentTrans, bool abovePrimary) int renderAfter {static_cast(mCamOffset)}; // If we're transitioning then also render the previous and next systems. - if (mPrimary->isAnimationPlaying(0)) { + if (isAnimationPlaying(0)) { renderBefore -= 1; renderAfter += 1; } @@ -1339,7 +1340,7 @@ void SystemView::renderElements(const glm::mat4& parentTrans, bool abovePrimary) while (index >= static_cast(mPrimary->getNumEntries())) index -= static_cast(mPrimary->getNumEntries()); - if (mPrimary->isAnimationPlaying(0) || index == mPrimary->getCursor()) { + if (isAnimationPlaying(0) || index == mPrimary->getCursor()) { glm::mat4 elementTrans {trans}; if (mCarousel != nullptr) { if (mCarousel->getType() == diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index 08ba06d39..47ca1cba6 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -744,8 +744,8 @@ template void GridComponent::onCursorChanged(const CursorState& [this, startPos, endPos, posMax, startRow, endRow](float t) { // Non-linear interpolation. t = 1.0f - (1.0f - t) * (1.0f - t); - float f {(endPos * t) + (startPos * (1.0f - t))}; + if (f < 0) f += posMax; if (f >= posMax) From 74162f17408a58620e314cb905761cba2fb52609 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Mon, 5 Dec 2022 23:33:48 +0100 Subject: [PATCH 043/101] Added a left trigger + right trigger help system icon and removed the deprecated hotkey icon. --- resources/graphics/help/button_hotkey.svg | 5 ----- resources/graphics/help/button_ltrt.svg | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) delete mode 100644 resources/graphics/help/button_hotkey.svg create mode 100644 resources/graphics/help/button_ltrt.svg diff --git a/resources/graphics/help/button_hotkey.svg b/resources/graphics/help/button_hotkey.svg deleted file mode 100644 index f4e200c8b..000000000 --- a/resources/graphics/help/button_hotkey.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/resources/graphics/help/button_ltrt.svg b/resources/graphics/help/button_ltrt.svg new file mode 100644 index 000000000..4487ab02d --- /dev/null +++ b/resources/graphics/help/button_ltrt.svg @@ -0,0 +1,17 @@ + + + + + From f6c8921132c5fe8ed930968f49afe7af995af6c6 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Wed, 7 Dec 2022 17:26:35 +0100 Subject: [PATCH 044/101] GridComponent item opacity is now adjusted for hidden entries and entries marked as not being games. --- .../src/components/primary/GridComponent.h | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index 47ca1cba6..f07b1cfe9 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -525,20 +525,35 @@ template void GridComponent::render(const glm::mat4& parentTrans if (mLastCursor != mCursor) renderEntries.emplace_back(mCursor); - float opacity {mUnfocusedItemOpacity}; float scale {1.0f}; + float opacity {1.0f}; trans[3].y -= (mItemSize.y + mItemSpacing.y) * mScrollPos; mRenderer->setMatrix(trans); for (auto it = renderEntries.cbegin(); it != renderEntries.cend(); ++it) { + float metadataOpacity {1.0f}; + + if constexpr (std::is_same_v) { + // If a game is marked as hidden, lower the opacity a lot. + // If a game is marked to not be counted, lower the opacity a moderate amount. + if (mEntries.at(*it).object->getHidden()) + metadataOpacity = 0.4f; + else if (!mEntries.at(*it).object->getCountAsGame()) + metadataOpacity = 0.7f; + } + + opacity = mUnfocusedItemOpacity * metadataOpacity; + if (*it == static_cast(mCursor)) { scale = glm::mix(1.0f, mItemScale, mTransitionFactor); - opacity = glm::mix(mUnfocusedItemOpacity, 1.0f, mTransitionFactor); + opacity = glm::mix(mUnfocusedItemOpacity * metadataOpacity, 1.0f * metadataOpacity, + mTransitionFactor); } else if (*it == static_cast(mLastCursor)) { scale = glm::mix(mItemScale, 1.0f, mTransitionFactor); - opacity = glm::mix(1.0f, mUnfocusedItemOpacity, mTransitionFactor); + opacity = glm::mix(1.0f * metadataOpacity, mUnfocusedItemOpacity * metadataOpacity, + mTransitionFactor); } mEntries.at(*it).data.item->setScale(scale); From 9e0a6df760496b7fc99abff2dbe40937b17e0007 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Wed, 7 Dec 2022 17:34:07 +0100 Subject: [PATCH 045/101] Added support for a left/right trigger help system button. --- es-core/src/HelpStyle.cpp | 2 ++ es-core/src/HelpStyle.h | 1 + es-core/src/components/HelpComponent.cpp | 3 +++ 3 files changed, 6 insertions(+) diff --git a/es-core/src/HelpStyle.cpp b/es-core/src/HelpStyle.cpp index 137c9a797..95d8de433 100644 --- a/es-core/src/HelpStyle.cpp +++ b/es-core/src/HelpStyle.cpp @@ -100,6 +100,8 @@ void HelpStyle::applyTheme(const std::shared_ptr& theme, const std::s mCustomButtons.button_lt = elem->get(PREFIX "button_lt"); if (elem->has(PREFIX "button_rt")) mCustomButtons.button_rt = elem->get(PREFIX "button_rt"); + if (elem->has(PREFIX "button_ltrt")) + mCustomButtons.button_ltrt = elem->get(PREFIX "button_ltrt"); // SNES. if (elem->has(PREFIX "button_a_SNES")) diff --git a/es-core/src/HelpStyle.h b/es-core/src/HelpStyle.h index 86ada3fa8..6ebd49d32 100644 --- a/es-core/src/HelpStyle.h +++ b/es-core/src/HelpStyle.h @@ -42,6 +42,7 @@ struct HelpStyle { std::string button_lr; std::string button_lt; std::string button_rt; + std::string button_ltrt; // SNES std::string button_a_SNES; diff --git a/es-core/src/components/HelpComponent.cpp b/es-core/src/components/HelpComponent.cpp index 391be51c9..b33a3df79 100644 --- a/es-core/src/components/HelpComponent.cpp +++ b/es-core/src/components/HelpComponent.cpp @@ -56,6 +56,9 @@ void HelpComponent::assignIcons() mStyle.mCustomButtons.button_lt; sIconPathMap["rt"] = mStyle.mCustomButtons.button_rt.empty() ? ":/graphics/help/button_rt.svg" : mStyle.mCustomButtons.button_rt; + sIconPathMap["ltrt"] = mStyle.mCustomButtons.button_ltrt.empty() ? + ":/graphics/help/button_ltrt.svg" : + mStyle.mCustomButtons.button_ltrt; // These graphics files are custom per controller type. if (controllerType == "snes") { From 2799974938e429fe7f7d40fc29213cebda1f3e81 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Wed, 7 Dec 2022 18:09:05 +0100 Subject: [PATCH 046/101] Expanded the quick system select menu option from a simple on/off entry to a selection of different button combinations. Also did some minor code cleanup in GuiMenu. --- es-app/src/guis/GuiMenu.cpp | 67 ++++++++++++++----------- es-app/src/views/GamelistBase.cpp | 82 +++++++++++++++++++++++++++++-- es-app/src/views/GamelistBase.h | 4 +- es-app/src/views/GamelistView.cpp | 13 +++-- es-core/src/Settings.cpp | 2 +- 5 files changed, 129 insertions(+), 39 deletions(-) diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 4b892e6cd..44f7c4158 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -119,7 +119,7 @@ void GuiMenu::openUIOptions() for (auto it = themeSets.cbegin(); it != themeSets.cend(); ++it) { // If required, abbreviate the theme set name so it doesn't overlap the setting name. - float maxNameLength = mSize.x * 0.62f; + const float maxNameLength = mSize.x * 0.62f; themeSet->add(it->first, it->first, it == selectedSet, maxNameLength); } s->addWithLabel("THEME SET", themeSet); @@ -176,7 +176,7 @@ void GuiMenu::openUIOptions() if (variant.selectable) { // If required, abbreviate the variant name so it doesn't overlap the // setting name. - float maxNameLength {mSize.x * 0.62f}; + const float maxNameLength {mSize.x * 0.62f}; themeVariant->add(variant.label, variant.name, variant.name == selectedVariant, maxNameLength); } @@ -226,7 +226,7 @@ void GuiMenu::openUIOptions() for (auto& colorScheme : currentSet->second.capabilities.colorSchemes) { // If required, abbreviate the color scheme name so it doesn't overlap the // setting name. - float maxNameLength {mSize.x * 0.52f}; + const float maxNameLength {mSize.x * 0.52f}; themeColorScheme->add(colorScheme.label, colorScheme.name, colorScheme.name == selectedColorScheme, maxNameLength); } @@ -298,12 +298,12 @@ void GuiMenu::openUIOptions() auto gamelistViewStyle = std::make_shared>( getHelpStyle(), "LEGACY GAMELIST VIEW STYLE", false); std::string selectedViewStyle {Settings::getInstance()->getString("GamelistViewStyle")}; - gamelistViewStyle->add("automatic", "automatic", selectedViewStyle == "automatic"); - gamelistViewStyle->add("basic", "basic", selectedViewStyle == "basic"); - gamelistViewStyle->add("detailed", "detailed", selectedViewStyle == "detailed"); - gamelistViewStyle->add("video", "video", selectedViewStyle == "video"); + gamelistViewStyle->add("AUTOMATIC", "automatic", selectedViewStyle == "automatic"); + gamelistViewStyle->add("BASIC", "basic", selectedViewStyle == "basic"); + gamelistViewStyle->add("DETAILED", "detailed", selectedViewStyle == "detailed"); + gamelistViewStyle->add("VIDEO", "video", selectedViewStyle == "video"); // If there are no objects returned, then there must be a manually modified entry in the - // configuration file. Simply set the view style to Automatic in this case. + // configuration file. Simply set the view style to "automatic" in this case. if (gamelistViewStyle->getSelectedObjects().size() == 0) gamelistViewStyle->selectEntry(0); s->addWithLabel("LEGACY GAMELIST VIEW STYLE", gamelistViewStyle); @@ -337,20 +337,43 @@ void GuiMenu::openUIOptions() } }); + // Quick system select (navigate between systems in the gamelist view). + auto quickSystemSelect = std::make_shared>( + getHelpStyle(), "QUICK SYSTEM SELECT", false); + std::string selectedQuickSelect {Settings::getInstance()->getString("QuickSystemSelect")}; + quickSystemSelect->add("LEFT/RIGHT OR SHOULDERS", "leftrightshoulders", + selectedQuickSelect == "leftrightshoulders"); + quickSystemSelect->add("LEFT/RIGHT OR TRIGGERS", "leftrighttriggers", + selectedQuickSelect == "leftrighttriggers"); + quickSystemSelect->add("SHOULDERS", "shoulders", selectedQuickSelect == "shoulders"); + quickSystemSelect->add("TRIGGERS", "triggers", selectedQuickSelect == "triggers"); + quickSystemSelect->add("LEFT/RIGHT", "leftright", selectedQuickSelect == "leftright"); + quickSystemSelect->add("DISABLED", "disabled", selectedQuickSelect == "disabled"); + // If there are no objects returned, then there must be a manually modified entry in the + // configuration file. Simply set the quick system select to "leftrightshoulders" in this case. + if (quickSystemSelect->getSelectedObjects().size() == 0) + quickSystemSelect->selectEntry(0); + s->addWithLabel("QUICK SYSTEM SELECT", quickSystemSelect); + s->addSaveFunc([quickSystemSelect, s] { + if (quickSystemSelect->getSelected() != + Settings::getInstance()->getString("QuickSystemSelect")) { + Settings::getInstance()->setString("QuickSystemSelect", + quickSystemSelect->getSelected()); + s->setNeedsSaving(); + } + }); + // Optionally start in selected system/gamelist. auto startupSystem = std::make_shared>( getHelpStyle(), "GAMELIST ON STARTUP", false); startupSystem->add("NONE", "", Settings::getInstance()->getString("StartupSystem") == ""); for (auto it = SystemData::sSystemVector.cbegin(); // Line break. it != SystemData::sSystemVector.cend(); ++it) { - if ((*it)->getName() != "retropie") { - // If required, abbreviate the system name so it doesn't overlap the setting name. - float maxNameLength {mSize.x * 0.51f}; - startupSystem->add((*it)->getFullName(), (*it)->getName(), - Settings::getInstance()->getString("StartupSystem") == - (*it)->getName(), - maxNameLength); - } + // If required, abbreviate the system name so it doesn't overlap the setting name. + float maxNameLength {mSize.x * 0.51f}; + startupSystem->add((*it)->getFullName(), (*it)->getName(), + Settings::getInstance()->getString("StartupSystem") == (*it)->getName(), + maxNameLength); } // This can probably not happen but as an extra precaution select the "NONE" entry if no // entry is selected. @@ -686,18 +709,6 @@ void GuiMenu::openUIOptions() } }); - // Quick system select (navigate left/right in gamelist view). - auto quickSystemSelect = std::make_shared(); - quickSystemSelect->setState(Settings::getInstance()->getBool("QuickSystemSelect")); - s->addWithLabel("ENABLE QUICK SYSTEM SELECT", quickSystemSelect); - s->addSaveFunc([quickSystemSelect, s] { - if (Settings::getInstance()->getBool("QuickSystemSelect") != - quickSystemSelect->getState()) { - Settings::getInstance()->setBool("QuickSystemSelect", quickSystemSelect->getState()); - s->setNeedsSaving(); - } - }); - // On-screen help prompts. auto showHelpPrompts = std::make_shared(); showHelpPrompts->setState(Settings::getInstance()->getBool("ShowHelpPrompts")); diff --git a/es-app/src/views/GamelistBase.cpp b/es-app/src/views/GamelistBase.cpp index ccf7f00f7..c4f364e9b 100644 --- a/es-app/src/views/GamelistBase.cpp +++ b/es-app/src/views/GamelistBase.cpp @@ -196,8 +196,7 @@ bool GamelistBase::input(InputConfig* config, Input input) } } else if (config->isMappedLike(getQuickSystemSelectRightButton(), input)) { - if (mLeftRightAvailable && Settings::getInstance()->getBool("QuickSystemSelect") && - SystemData::sSystemVector.size() > 1) { + if (SystemData::sSystemVector.size() > 1) { muteViewVideos(); onFocusLost(); stopListScrolling(); @@ -207,8 +206,7 @@ bool GamelistBase::input(InputConfig* config, Input input) } } else if (config->isMappedLike(getQuickSystemSelectLeftButton(), input)) { - if (mLeftRightAvailable && Settings::getInstance()->getBool("QuickSystemSelect") && - SystemData::sSystemVector.size() > 1) { + if (SystemData::sSystemVector.size() > 1) { muteViewVideos(); onFocusLost(); stopListScrolling(); @@ -999,3 +997,79 @@ void GamelistBase::removeMedia(FileData* game) removeEmptyDirFunc(systemMediaDir, mediaType, path); } } + +std::string GamelistBase::getQuickSystemSelectLeftButton() +{ + if (Settings::getInstance()->getString("QuickSystemSelect") == "leftrightshoulders") { + if (mLeftRightAvailable) + return "left"; + else + return "leftshoulder"; + } + + if (Settings::getInstance()->getString("QuickSystemSelect") == "leftrighttriggers") { + if (mLeftRightAvailable) + return "left"; + else + return "lefttrigger"; + } + + if (Settings::getInstance()->getString("QuickSystemSelect") == "shoulders") + return "leftshoulder"; + + if (Settings::getInstance()->getString("QuickSystemSelect") == "triggers") + return "lefttrigger"; + + if (Settings::getInstance()->getString("QuickSystemSelect") == "leftright") { + if (mLeftRightAvailable) + return "left"; + else + return ""; + } + + if (Settings::getInstance()->getString("QuickSystemSelect") == "disabled") + return ""; + + // This should only happen if there is an invalid value in es_settings.xml. + if (mLeftRightAvailable) + return "left"; + else + return "leftshoulder"; +} +std::string GamelistBase::getQuickSystemSelectRightButton() +{ + if (Settings::getInstance()->getString("QuickSystemSelect") == "leftrightshoulders") { + if (mLeftRightAvailable) + return "right"; + else + return "rightshoulder"; + } + + if (Settings::getInstance()->getString("QuickSystemSelect") == "leftrighttriggers") { + if (mLeftRightAvailable) + return "right"; + else + return "righttrigger"; + } + + if (Settings::getInstance()->getString("QuickSystemSelect") == "shoulders") + return "rightshoulder"; + + if (Settings::getInstance()->getString("QuickSystemSelect") == "triggers") + return "righttrigger"; + + if (Settings::getInstance()->getString("QuickSystemSelect") == "leftright") { + if (mLeftRightAvailable) + return "right"; + else + return ""; + } + + if (Settings::getInstance()->getString("QuickSystemSelect") == "disabled") + return ""; + + if (mLeftRightAvailable) + return "right"; + else + return "rightshoulder"; +} diff --git a/es-app/src/views/GamelistBase.h b/es-app/src/views/GamelistBase.h index 59b188c29..839e1a9eb 100644 --- a/es-app/src/views/GamelistBase.h +++ b/es-app/src/views/GamelistBase.h @@ -86,8 +86,8 @@ protected: bool isListScrolling() override { return mPrimary->isScrolling(); } - std::string getQuickSystemSelectRightButton() { return "right"; } - std::string getQuickSystemSelectLeftButton() { return "left"; } + std::string getQuickSystemSelectLeftButton(); + std::string getQuickSystemSelectRightButton(); FileData* mRoot; std::unique_ptr> mCarousel; diff --git a/es-app/src/views/GamelistView.cpp b/es-app/src/views/GamelistView.cpp index f950d5c61..6f4dcb937 100644 --- a/es-app/src/views/GamelistView.cpp +++ b/es-app/src/views/GamelistView.cpp @@ -368,7 +368,7 @@ void GamelistView::onThemeChanged(const std::shared_ptr& theme) populateList(mRoot->getChildrenListToDisplay(), mRoot); - // Disable quick system select if the primary component uses the left and right buttons. + // Check whether the primary component uses the left and right buttons for its navigation. if (mCarousel != nullptr) { if (mCarousel->getType() == CarouselComponent::CarouselType::HORIZONTAL || mCarousel->getType() == CarouselComponent::CarouselType::HORIZONTAL_WHEEL) @@ -432,9 +432,14 @@ std::vector GamelistView::getHelpPrompts() { std::vector prompts; - if (Settings::getInstance()->getBool("QuickSystemSelect") && - SystemData::sSystemVector.size() > 1 && mLeftRightAvailable) - prompts.push_back(HelpPrompt("left/right", "system")); + if (Settings::getInstance()->getString("QuickSystemSelect") != "disabled") { + if (getQuickSystemSelectLeftButton() == "leftshoulder") + prompts.push_back(HelpPrompt("lr", "system")); + else if (getQuickSystemSelectLeftButton() == "lefttrigger") + prompts.push_back(HelpPrompt("ltrt", "system")); + else if (getQuickSystemSelectLeftButton() == "left") + prompts.push_back(HelpPrompt("left/right", "system")); + } if (mRoot->getSystem()->getThemeFolder() == "custom-collections" && mCursorStack.empty() && ViewController::getInstance()->getState().viewing == ViewController::GAMELIST) diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index ab49cfb64..e05797cde 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -136,6 +136,7 @@ void Settings::setDefaults() mStringMap["ThemeAspectRatio"] = {"", ""}; mStringMap["GamelistViewStyle"] = {"automatic", "automatic"}; mStringMap["TransitionStyle"] = {"slide", "slide"}; + mStringMap["QuickSystemSelect"] = {"leftrightshoulders", "leftrightshoulders"}; mStringMap["StartupSystem"] = {"", ""}; mStringMap["DefaultSortOrder"] = {"filename, ascending", "filename, ascending"}; mStringMap["MenuOpeningEffect"] = {"scale-up", "scale-up"}; @@ -190,7 +191,6 @@ void Settings::setDefaults() mBoolMap["FavoritesAddButton"] = {true, true}; mBoolMap["RandomAddButton"] = {false, false}; mBoolMap["GamelistFilters"] = {true, true}; - mBoolMap["QuickSystemSelect"] = {true, true}; mBoolMap["ShowHelpPrompts"] = {true, true}; // Sound settings. From 304d304727a601eedacb1d7a7ad1347c81258f28 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Wed, 7 Dec 2022 18:24:00 +0100 Subject: [PATCH 047/101] Added support for linear interpolation for font texture magnifications. --- es-core/src/HelpStyle.cpp | 2 +- es-core/src/components/DateTimeComponent.cpp | 2 +- es-core/src/components/TextComponent.cpp | 2 +- .../components/primary/CarouselComponent.h | 2 +- .../components/primary/TextListComponent.h | 2 +- es-core/src/resources/Font.cpp | 31 +++++++++++-------- es-core/src/resources/Font.h | 13 +++++--- 7 files changed, 32 insertions(+), 22 deletions(-) diff --git a/es-core/src/HelpStyle.cpp b/es-core/src/HelpStyle.cpp index 95d8de433..92291384d 100644 --- a/es-core/src/HelpStyle.cpp +++ b/es-core/src/HelpStyle.cpp @@ -63,7 +63,7 @@ void HelpStyle::applyTheme(const std::shared_ptr& theme, const std::s iconColorDimmed = iconColor; if (elem->has("fontPath") || elem->has("fontSize")) - font = Font::getFromTheme(elem, ThemeFlags::ALL, font, 0.0f, theme->isLegacyTheme()); + font = Font::getFromTheme(elem, ThemeFlags::ALL, font, 0.0f, false, theme->isLegacyTheme()); if (elem->has("entrySpacing")) entrySpacing = glm::clamp(elem->get("entrySpacing"), 0.0f, 0.04f); diff --git a/es-core/src/components/DateTimeComponent.cpp b/es-core/src/components/DateTimeComponent.cpp index 272cb2da8..0f6ef38a4 100644 --- a/es-core/src/components/DateTimeComponent.cpp +++ b/es-core/src/components/DateTimeComponent.cpp @@ -224,5 +224,5 @@ void DateTimeComponent::applyTheme(const std::shared_ptr& theme, if (properties & LINE_SPACING && elem->has("lineSpacing")) setLineSpacing(glm::clamp(elem->get("lineSpacing"), 0.5f, 3.0f)); - setFont(Font::getFromTheme(elem, properties, mFont, maxHeight, theme->isLegacyTheme())); + setFont(Font::getFromTheme(elem, properties, mFont, maxHeight, false, theme->isLegacyTheme())); } diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index 9499165b9..4bb5b7618 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -486,5 +486,5 @@ void TextComponent::applyTheme(const std::shared_ptr& theme, if (properties & LINE_SPACING && elem->has("lineSpacing")) setLineSpacing(glm::clamp(elem->get("lineSpacing"), 0.5f, 3.0f)); - setFont(Font::getFromTheme(elem, properties, mFont, maxHeight, theme->isLegacyTheme())); + setFont(Font::getFromTheme(elem, properties, mFont, maxHeight, false, theme->isLegacyTheme())); } diff --git a/es-core/src/components/primary/CarouselComponent.h b/es-core/src/components/primary/CarouselComponent.h index 64238ce68..2ee48eeeb 100644 --- a/es-core/src/components/primary/CarouselComponent.h +++ b/es-core/src/components/primary/CarouselComponent.h @@ -1204,7 +1204,7 @@ void CarouselComponent::applyTheme(const std::shared_ptr& theme, } // For non-legacy themes, scale the font size with the itemScale property value. - mFont = Font::getFromTheme(elem, properties, mFont, 0.0f, mLegacyMode, + mFont = Font::getFromTheme(elem, properties, mFont, 0.0f, false, mLegacyMode, (mLegacyMode ? 1.0f : (mItemScale >= 1.0f ? mItemScale : 1.0f))); if (elem->has("textColor")) diff --git a/es-core/src/components/primary/TextListComponent.h b/es-core/src/components/primary/TextListComponent.h index 4484e89a8..717c53125 100644 --- a/es-core/src/components/primary/TextListComponent.h +++ b/es-core/src/components/primary/TextListComponent.h @@ -529,7 +529,7 @@ void TextListComponent::applyTheme(const std::shared_ptr& theme, mSelectedSecondaryColor = mSelectedColor; } - setFont(Font::getFromTheme(elem, properties, mFont, 0.0f, mLegacyMode)); + setFont(Font::getFromTheme(elem, properties, mFont, 0.0f, false, mLegacyMode)); if (mLegacyMode) mFont->useLegacyMaxGlyphHeight(); const float selectorHeight {mFont->getHeight(mLineSpacing)}; diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index 0f05fce14..0cf5b8858 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -15,10 +15,11 @@ #include "utils/PlatformUtil.h" #include "utils/StringUtil.h" -Font::Font(float size, const std::string& path) +Font::Font(float size, const std::string& path, const bool linearMagnify) : mRenderer {Renderer::getInstance()} , mPath(path) , mFontSize {size} + , mLinearMagnify {linearMagnify} , mLetterHeight {0.0f} , mMaxGlyphHeight {static_cast(std::round(size))} , mLegacyMaxGlyphHeight {0} @@ -46,7 +47,8 @@ Font::~Font() { unload(ResourceManager::getInstance()); - auto fontEntry = sFontMap.find(std::pair(mPath, mFontSize)); + auto fontEntry = + sFontMap.find(std::tuple(mFontSize, mPath, mLinearMagnify)); if (fontEntry != sFontMap.cend()) sFontMap.erase(fontEntry); @@ -57,11 +59,11 @@ Font::~Font() } } -std::shared_ptr Font::get(float size, const std::string& path) +std::shared_ptr Font::get(float size, const std::string& path, const bool linearMagnify) { const std::string canonicalPath {Utils::FileSystem::getCanonicalPath(path)}; - const std::pair def { - canonicalPath.empty() ? getDefaultPath() : canonicalPath, size}; + const std::tuple def { + size, canonicalPath.empty() ? getDefaultPath() : canonicalPath, linearMagnify}; auto foundFont = sFontMap.find(def); if (foundFont != sFontMap.cend()) { @@ -69,7 +71,7 @@ std::shared_ptr Font::get(float size, const std::string& path) return foundFont->second.lock(); } - std::shared_ptr font {new Font(def.second, def.first)}; + std::shared_ptr font {new Font(std::get<0>(def), std::get<1>(def), std::get<2>(def))}; sFontMap[def] = std::weak_ptr(font); ResourceManager::getInstance().addReloadable(font); return font; @@ -423,6 +425,7 @@ std::shared_ptr Font::getFromTheme(const ThemeData::ThemeElement* elem, unsigned int properties, const std::shared_ptr& orig, const float maxHeight, + const bool linearMagnify, const bool legacyTheme, const float sizeMultiplier) { @@ -459,9 +462,9 @@ std::shared_ptr Font::getFromTheme(const ThemeData::ThemeElement* elem, } if (mLegacyTheme) - return get(std::floor(size), path); + return get(std::floor(size), path, false); else - return get(size, path); + return get(size, path, linearMagnify); } size_t Font::getMemUsage() const @@ -522,11 +525,12 @@ std::vector Font::getFallbackFontPaths() return fontPaths; } -Font::FontTexture::FontTexture(const int mFontSize) +Font::FontTexture::FontTexture(const int mFontSize, const bool linearMagnifyArg) { textureId = 0; rowHeight = 0; writePos = glm::ivec2 {0, 0}; + linearMagnify = linearMagnifyArg; // Set the texture to a reasonable size, if we run out of space for adding glyphs then // more textures will be created dynamically. @@ -573,9 +577,9 @@ void Font::FontTexture::initTexture() // glyphs will not be visible. That would otherwise lead to edge artifacts as these pixels // would get sampled during scaling. std::vector texture(textureSize.x * textureSize.y * 4, 0); - textureId = - Renderer::getInstance()->createTexture(Renderer::TextureType::RED, true, false, false, - false, textureSize.x, textureSize.y, &texture[0]); + textureId = Renderer::getInstance()->createTexture(Renderer::TextureType::RED, true, + linearMagnify, false, false, textureSize.x, + textureSize.y, &texture[0]); } void Font::FontTexture::deinitTexture() @@ -664,7 +668,8 @@ void Font::getTextureForNewGlyph(const glm::ivec2& glyphSize, return; // Yes. } - mTextures.emplace_back(std::make_unique(static_cast(std::round(mFontSize)))); + mTextures.emplace_back( + std::make_unique(static_cast(std::round(mFontSize)), mLinearMagnify)); tex_out = mTextures.back().get(); tex_out->initTexture(); diff --git a/es-core/src/resources/Font.h b/es-core/src/resources/Font.h index 02b387391..22ca1ff83 100644 --- a/es-core/src/resources/Font.h +++ b/es-core/src/resources/Font.h @@ -36,7 +36,9 @@ class Font : public IReloadable { public: virtual ~Font(); - static std::shared_ptr get(float size, const std::string& path = getDefaultPath()); + static std::shared_ptr get(float size, + const std::string& path = getDefaultPath(), + const bool linearMagnify = false); // Returns the expected size of a string when rendered. Extra spacing is applied to the Y axis. glm::vec2 sizeText(std::string text, float lineSpacing = 1.5f); @@ -93,6 +95,7 @@ public: unsigned int properties, const std::shared_ptr& orig, const float maxHeight = 0.0f, + const bool linearMagnify = false, const bool legacyTheme = false, const float sizeMultiplier = 1.0f); @@ -102,7 +105,7 @@ public: static size_t getTotalMemUsage(); private: - Font(float size, const std::string& path); + Font(float size, const std::string& path, const bool linearMagnify); static void initLibrary(); struct FontTexture { @@ -110,8 +113,9 @@ private: glm::ivec2 textureSize; glm::ivec2 writePos; int rowHeight; + bool linearMagnify; - FontTexture(const int mFontSize); + FontTexture(const int mFontSize, const bool linearMagnifyArg); ~FontTexture(); bool findEmpty(const glm::ivec2& size, glm::ivec2& cursor_out); @@ -161,7 +165,7 @@ private: void clearFaceCache() { mFaceCache.clear(); } static inline FT_Library sLibrary {nullptr}; - static inline std::map, std::weak_ptr> sFontMap; + static inline std::map, std::weak_ptr> sFontMap; static inline bool mLegacyTheme {false}; Renderer* mRenderer; @@ -171,6 +175,7 @@ private: const std::string mPath; float mFontSize; + const bool mLinearMagnify; float mLetterHeight; int mMaxGlyphHeight; int mLegacyMaxGlyphHeight; From 949f059b7e9fc9992c9e9f3efba27f6e1146955a Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Wed, 7 Dec 2022 19:43:03 +0100 Subject: [PATCH 048/101] Fixed an issue where the CarouselComponent text property value was ignored. --- es-app/src/views/SystemView.cpp | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 2e30a8fb0..823c72b3d 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -449,7 +449,7 @@ void SystemView::populate() if (SystemData::sSystemVector.size() == 0) return; - LOG(LogDebug) << "SystemView::populate(): Populating carousel"; + LOG(LogDebug) << "SystemView::populate(): Populating primary element..."; auto themeSets = ThemeData::getThemeSets(); std::map::const_iterator @@ -467,6 +467,7 @@ void SystemView::populate() const std::shared_ptr& theme {it->getTheme()}; std::string itemPath; std::string defaultItemPath; + std::string itemText; if (mLegacyMode && mViewNeedsReload) { if (mCarousel == nullptr) { @@ -577,6 +578,8 @@ void SystemView::populate() itemPath = element.second.get("staticItem"); if (element.second.has("defaultItem")) defaultItemPath = element.second.get("defaultItem"); + if (element.second.has("text")) + itemText = element.second.get("text"); } } else if (element.second.type == "image") { @@ -738,12 +741,17 @@ void SystemView::populate() if (mCarousel != nullptr) { CarouselComponent::Entry entry; - // Keep showing only the short name for legacy themes to maintain maximum - // backward compatibility. This also applies to unreadable theme sets. - if (mLegacyMode) + if (mLegacyMode) { + // Keep showing only the short name for legacy themes to maintain maximum + // backward compatibility. This also applies to unreadable theme sets. entry.name = it->getName(); - else - entry.name = it->getFullName(); + } + else { + if (itemText == "") + entry.name = it->getFullName(); + else + entry.name = itemText; + } letterCaseFunc(entry.name); entry.object = it; entry.data.itemPath = itemPath; @@ -752,7 +760,10 @@ void SystemView::populate() } else if (mGrid != nullptr) { GridComponent::Entry entry; - entry.name = it->getFullName(); + if (itemText == "") + entry.name = it->getFullName(); + else + entry.name = itemText; letterCaseFunc(entry.name); entry.object = it; entry.data.itemPath = itemPath; From 56b33c5371bdac776a441b237d0d2fcd3017774e Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Wed, 7 Dec 2022 19:46:41 +0100 Subject: [PATCH 049/101] Added text theming support to GridComponent. --- .../src/components/primary/GridComponent.h | 68 ++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index f07b1cfe9..6cc9d2409 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -150,7 +150,6 @@ GridComponent::GridComponent() , mScrollPos {0.0f} , mTransitionFactor {1.0f} , mVisibleRows {1.0f} - , mFont {Font::get(FONT_SIZE_LARGE)} , mItemSize {glm::vec2 {mRenderer->getScreenWidth() * 0.15f, mRenderer->getScreenHeight() * 0.25f}} , mItemScale {1.05f} @@ -664,6 +663,73 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, if (elem->has("unfocusedItemOpacity")) mUnfocusedItemOpacity = glm::clamp(elem->get("unfocusedItemOpacity"), 0.1f, 1.0f); + mFont = Font::getFromTheme(elem, properties, mFont, 0.0f, (mItemScale > 1.0f)); + + if (elem->has("textColor")) + mTextColor = elem->get("textColor"); + if (elem->has("textBackgroundColor")) + mTextBackgroundColor = elem->get("textBackgroundColor"); + + if (elem->has("lineSpacing")) + mLineSpacing = glm::clamp(elem->get("lineSpacing"), 0.5f, 3.0f); + + if (elem->has("letterCase")) { + const std::string& letterCase {elem->get("letterCase")}; + + if (letterCase == "uppercase") { + mLetterCase = LetterCase::UPPERCASE; + } + else if (letterCase == "lowercase") { + mLetterCase = LetterCase::LOWERCASE; + } + else if (letterCase == "capitalize") { + mLetterCase = LetterCase::CAPITALIZED; + } + else if (letterCase != "none") { + LOG(LogWarning) << "GridComponent: Invalid theme configuration, property " + "\"letterCase\" for element \"" + << element.substr(5) << "\" defined as \"" << letterCase << "\""; + } + } + + if (elem->has("letterCaseCollections")) { + const std::string& letterCase {elem->get("letterCaseCollections")}; + + if (letterCase == "uppercase") { + mLetterCaseCollections = LetterCase::UPPERCASE; + } + else if (letterCase == "lowercase") { + mLetterCaseCollections = LetterCase::LOWERCASE; + } + else if (letterCase == "capitalize") { + mLetterCaseCollections = LetterCase::CAPITALIZED; + } + else { + LOG(LogWarning) << "GridComponent: Invalid theme configuration, property " + "\"letterCaseCollections\" for element \"" + << element.substr(5) << "\" defined as \"" << letterCase << "\""; + } + } + + if (elem->has("letterCaseGroupedCollections")) { + const std::string& letterCase {elem->get("letterCaseGroupedCollections")}; + + if (letterCase == "uppercase") { + mLetterCaseGroupedCollections = LetterCase::UPPERCASE; + } + else if (letterCase == "lowercase") { + mLetterCaseGroupedCollections = LetterCase::LOWERCASE; + } + else if (letterCase == "capitalize") { + mLetterCaseGroupedCollections = LetterCase::CAPITALIZED; + } + else { + LOG(LogWarning) << "GridComponent: Invalid theme configuration, property " + "\"letterCaseGroupedCollections\" for element \"" + << element.substr(5) << "\" defined as \"" << letterCase << "\""; + } + } + mSize.x = glm::clamp(mSize.x, mRenderer->getScreenWidth() * 0.05f, mRenderer->getScreenWidth() * 1.0f); mSize.y = glm::clamp(mSize.y, mRenderer->getScreenHeight() * 0.05f, From 2d24230a48b38d5e8bf2b7da2998ca3901dc0926 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Wed, 7 Dec 2022 21:23:53 +0100 Subject: [PATCH 050/101] Fixed an issue where the GridComponent layout was not calculated if there was only a placeholder entry. --- es-app/src/views/GamelistBase.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/es-app/src/views/GamelistBase.cpp b/es-app/src/views/GamelistBase.cpp index c4f364e9b..1af535474 100644 --- a/es-app/src/views/GamelistBase.cpp +++ b/es-app/src/views/GamelistBase.cpp @@ -707,13 +707,14 @@ void GamelistBase::populateList(const std::vector& files, FileData* f mTextList->addEntry(textListEntry); } } - if (mGrid != nullptr) - mGrid->calculateLayout(); } else { addPlaceholder(firstEntry); } + if (mGrid != nullptr) + mGrid->calculateLayout(); + generateGamelistInfo(getCursor(), firstEntry); generateFirstLetterIndex(files); } From c12d57cb75ac228ba40d8285f130c71ae6297d47 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Wed, 7 Dec 2022 21:37:38 +0100 Subject: [PATCH 051/101] (Windows) Fixed an MSVC compiler warning. --- es-app/src/views/SystemView.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 823c72b3d..e3f17aac8 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -437,8 +437,9 @@ void SystemView::onCursorChanged(const CursorState& state) else { // Instant. updateGameCount(); - anim = new LambdaAnimation( - [this, startPos, endPos, posMax](float t) { mCamOffset = endPos; }, animTime); + anim = + new LambdaAnimation([this, startPos, endPos, posMax](float t) { mCamOffset = endPos; }, + static_cast(animTime)); } setAnimation(anim, 0, nullptr, false, 0); From aba6e040c2f1382793a18e50dbec5cf3df6fd707 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Wed, 7 Dec 2022 21:51:22 +0100 Subject: [PATCH 052/101] Documentation update. --- CHANGELOG.md | 3 +++ INSTALL-DEV.md | 2 +- THEMES-DEV.md | 31 ++++++++++++++++++++++++++++++- USERGUIDE-DEV.md | 18 +++++++++--------- 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c400e2c5a..aed41d8cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ * Added support for caching of SVG images * Added support for sizing SVG images arbitrarily (overriding the image aspect ratio by stretching and squashing) * (Windows) Made game launching more seamless by making the application window one pixel wider instead of one pixel less in height +* Expanded the quick system select menu option from an on/off entry to a selection of different button combinations * Changed the order of the help system entries Y, X, B and A to instead be listed as A, B, X and Y * Changed the start button for the screensaver from "Back" to "X" * Changed the help system description for the "A" button in the gamelist view from "Launch" to "Select" @@ -77,6 +78,7 @@ * Greatly improved application startup speed by avoiding a lot of unnecessary SVG rasterizations * Implemented dynamic texture allocation to the font code to reduce memory usage and avoid missing glyphs * Large optimizations to the text wrapping code (generallly 300-400% faster) +* Added support for linear interpolation for font texture magnifications * Added support for texture mipmapping with trilinear filtering * Added on-demand texture loading to the carousel * Improved the renderer scaling accuracy @@ -171,6 +173,7 @@ * Made the logging thread safe * (Windows) Changed many logging entries to use backslashes instead of forward slashes as directory separators * Added the build date to to main menu for alpha and dev builds +* Added a left trigger + right trigger help system icon and removed the deprecated hotkey icon * Moved all Platform functions to the utility namespace instead of using the global namespace * Implemented proper XML attribute support in ThemeData that eliminates the risk of name collisions * Added size restrictions to images and fonts so incorrect theme configuration would not lead to crashes or excessive memory utilization diff --git a/INSTALL-DEV.md b/INSTALL-DEV.md index 7d98cd75a..bfb869be3 100644 --- a/INSTALL-DEV.md +++ b/INSTALL-DEV.md @@ -8,7 +8,7 @@ Table of contents: [[_TOC_]] -## Development Environment +## Development environment ES-DE is developed and compiled using Clang/LLVM and GCC on Unix, Clang/LLVM on macOS and MSVC and GCC (MinGW) on Windows. diff --git a/THEMES-DEV.md b/THEMES-DEV.md index 87eda840a..732e74f8d 100644 --- a/THEMES-DEV.md +++ b/THEMES-DEV.md @@ -1127,7 +1127,8 @@ Properties: - Valid values are `horizontal` or `vertical` - Default is `horizontal` * `text` - type: STRING - - A string literal to display if there is no `staticItem` or `defaultItem` property defined and if no image is found. This property can only be used in the system view as for the gamelist view the text fallback will always be set to the game name. + - A string literal to display if there is no `staticItem` or `defaultItem` property defined and if no image is found. This property can only be used in the system view as for the gamelist view the game name is always used as fallback. + - Default is the full system name. * `textColor` - type: COLOR - Default is `000000FF` * `textBackgroundColor` - type: COLOR @@ -1226,6 +1227,33 @@ Properties: - Sets the opacity for the items that are not currently focused. - Minimum value is `0.1` and maximum value is `1` - Default is `1` +* `text` - type: STRING + - A string literal to display if there is no `staticItem` or `defaultItem` property defined and if no image is found. This property can only be used in the system view as for the gamelist view the game name is always used as fallback. + - Default is the full system name. +* `textColor` - type: COLOR + - Default is `000000FF` +* `textBackgroundColor` - type: COLOR + - Default is `FFFFFF00` +* `fontPath` - type: PATH + - Path to a TrueType font (.ttf) used as fallback if there is no `staticItem` / `itemType` image defined or found, and if `defaultItem` has not been defined. +* `fontSize` - type: FLOAT + - Size of the font as a percentage of screen height (e.g. for a value of `0.1`, the text's height would be 10% of the screen height). This calculation is based on the reference 'S' character so other glyphs may not fill this area, or they may exceed this area. + - Minimum value is `0.001` and maximum value is `1.5`. Note that when running at a really low resolution, the minimum value may get clamped to a larger relative size. + - Default is `0.045` +* `letterCase` - type: STRING + - Sets the letter case for all entries. + - Valid values are `none`, `uppercase`, `lowercase` or `capitalize` + - Default is `none` (original letter case is retained) +* `letterCaseCollections` - type: STRING + - For technical reasons both automatic collections and custom collections have their names spelled in lowercase characters. This property which can only be used in the `system` view will make it possible to change the letter case for all such collections. This is only needed when `letterCase` is omitted or has been set to `none` as that property will otherwise apply also to all collections. + - Valid values are `uppercase`, `lowercase` or `capitalize` +* `letterCaseGroupedCollections` - type: STRING + - For technical reasons custom collections have their names spelled in lowercase characters. This property which can only be used in the `gamelist` view will make it possible to change the letter case for all such grouped collections. This is only needed when `letterCase` is omitted or has been set to `none` as that property will otherwise apply also to all grouped collections. + - Valid values are `uppercase`, `lowercase` or `capitalize` +* `lineSpacing` - type: FLOAT + - Controls the space between lines (as a multiple of the font height). Due to the imprecise nature of typefaces where certain glyphs (characters) may exceed the requested font size, it's recommended to keep this value at around `1.1` or higher. This way overlapping glyphs or characters being cut off at the top or bottom will be prevented. + - Minimum value is `0.5` and maximum value is `3` + - Default is `1.5` * `zIndex` - type: FLOAT - z-index value for element. Elements will be rendered in order of zIndex value from low to high. - Default is `50` @@ -2088,6 +2116,7 @@ Properties: `button_lr`, `button_lt`, `button_rt`, + `button_ltrt`, `button_a_SNES`, `button_b_SNES`, `button_x_SNES`, diff --git a/USERGUIDE-DEV.md b/USERGUIDE-DEV.md index 198e7f737..3946733f4 100644 --- a/USERGUIDE-DEV.md +++ b/USERGUIDE-DEV.md @@ -506,12 +506,12 @@ Default keyboard mappings are shown in brackets. **Up and down**\ _(Arrow up / Arrow down)_ -Navigate up and down in gamelists, between systems in the system view (if the theme has a vertical carousel) and in menus. +Navigates between system and game entries where these buttons are applicable, such as for textlists and vertical carousels. Also used in menus for general navigation. **Left and right**\ _(Arrow left / Arrow right)_ -Navigate between gamelists (if the _Quick system select_ option has been enabled), between systems in the system view (if the theme has a horizontal carousel) and between media files in the media viewer. If the _Enable screensaver controls_ option has been enabled, either button also randomly selects a new game when using the Video or Slideshow screensavers. +Navigates between system and game entries where these buttons are applicable, such as for grids and horizontal carousels. Navigates between gamelists if the _Quick system select_ option has been set to use these buttons and the primary element supports it. Navigates between media files in the media viewer and selects a random game when using the _Video_ or _Slideshow_ screensavers if the _Enable screensaver controls_ option has been enabled. Also used in menus for general navigation. **Start button**\ _(Escape)_ @@ -526,12 +526,12 @@ Opens and closes the gamelist options menu in the gamelist view. **Left and right shoulder buttons**\ _(Page up / Page down)_ -Provides quick jumping in gamelists and menus, jumps 10 games in the gamelists and 6 entries in the menus. Also jumps forward in text edit dialogs. +Provides quick jumping in textlists and menus, jumps 10 games in the gamelists and 6 entries in the menus. Navigates between gamelists if the _Quick system select_ option has been set to use these buttons and the primary element supports it. Also used as back and blankspace keys in text edit dialogs. **Left and right trigger buttons**\ _(Home / End)_ -Jumps to the first and last entry of the gamelists, menus and text edit dialogs. +Jumps to the first or last entries in carousels, grids and textlists as well as in menus and text edit dialogs. Navigates between gamelists if the _Quick system select_ option has been set to use these buttons and the primary element supports it. **Left and right thumbstick click**\ _(F2 / F3)_ @@ -2295,6 +2295,10 @@ Sets the view style to _Automatic, Basic, Detailed or Video_ for legacy themes. Transition animation when navigating between gamelists, or between systems on the System view carousel. Can be set to _Slide, Fade_ or _Instant_. Only applicable for legacy themes as the newer type of theme sets lets the theme author define the transition animations in a more fine-grained manner. Therefore this option will be grayed out if a modern theme set has been selected. +**Quick system select** + +The buttons to use to jump between systems in the gamelist view. The options are _Left/right or shoulders_, _Left/right or triggers_, _Shoulders_, _Triggers_, _Left/right_ or _Disabled_. The first two options will apply either left/right or shoulder/trigger buttons depending on the type of primary element used for the gamelist. For example a textlist or a vertical carousel will allow the use of the left and right buttons, but for horizontal carousels and grids these buttons are reserved for navigating the entries so instead the secondary buttons will be used, i.e. the shoulder or trigger buttons. Using these two options therefore leads to a slight inconsistency as different buttons will be used depending on the theme configuration. If instead using any of the single button pair options, i.e. _Shoulders_, _Triggers_ or _Left/right_, the navigation will be consistent regardless of theme configuration but you'll sacrifice the ability to use the selected buttons if the gamelist supports it, such as the ability to jump rows in a textlist using the shoulder and trigger buttons. + **Gamelist on startup** If set to _None_, the system view will be displayed. Any other value will jump to that game system automatically on startup. @@ -2367,10 +2371,6 @@ This enables or disables the ability to jump to a random system or game. It's ma Activating or deactivating the ability to filter your gamelists. This can normally be left enabled. -**Enable quick system select** - -If enabled, it's possible to navigate between gamelists using the _Left_ and _Right_ buttons without having to first go back to the System view. - **Display on-screen help** Activates or deactivates the built-in help system that provides contextual information regarding button usage. @@ -2823,7 +2823,7 @@ A flag to mark whether the game is suitable for children. This will be applied a **Hidden** -A flag to indicate that the game is hidden. If the corresponding option has been set in the main menu, the game will not be shown. Useful for example for DOS games to hide batch scripts and unnecessary binaries or to hide the actual game files for multi-disc games. If a file or folder is flagged as hidden but the corresponding option to hide hidden games has not been enabled, then the opacity of the text will be lowered significantly to make it clear that it's a hidden entry. +A flag to indicate that the game is hidden. If the corresponding option has been set in the main menu, the game will not be shown. Useful for example for DOS games to hide batch files and unnecessary binaries or to hide the actual game files for multi-disc games. If a file or folder is flagged as hidden but the corresponding option to hide hidden games has not been enabled, then the opacity for the entry will be lowered significantly to make it clear that it's a hidden entry. **Broken/not working** From 3c2dc7b551a2ce5f92c429b9938241d7605a78ca Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Fri, 9 Dec 2022 18:49:32 +0100 Subject: [PATCH 053/101] Added a number of properties and corresponding functionality to GridComponent. --- es-core/src/ThemeData.cpp | 23 +- .../src/components/primary/GridComponent.h | 358 ++++++++++++++++-- 2 files changed, 340 insertions(+), 41 deletions(-) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index c519d778f..932a868bb 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -157,20 +157,31 @@ std::map> {"staticItem", PATH}, {"itemType", STRING}, {"defaultItem", PATH}, + {"backgroundImage", PATH}, + {"backgroundRelativeScale", FLOAT}, + {"backgroundColor", COLOR}, + {"backgroundColorEnd", COLOR}, + {"backgroundGradientType", STRING}, + {"selectorImage", PATH}, + {"selectorRelativeScale", FLOAT}, + {"selectorColor", COLOR}, + {"selectorColorEnd", COLOR}, + {"selectorGradientType", STRING}, + {"selectorLayer", STRING}, {"fractionalRows", BOOLEAN}, {"itemSize", NORMALIZED_PAIR}, {"itemScale", FLOAT}, + {"itemRelativeScale", FLOAT}, + {"itemSpacing", NORMALIZED_PAIR}, + {"itemColor", COLOR}, + {"itemColorEnd", COLOR}, + {"itemGradientType", STRING}, {"itemTransitions", STRING}, {"rowTransitions", STRING}, - {"itemSpacing", NORMALIZED_PAIR}, - {"itemHorizontalAlignment", STRING}, - {"itemVerticalAlignment", STRING}, {"unfocusedItemOpacity", FLOAT}, {"edgeScaleInwards", BOOLEAN}, - {"color", COLOR}, - {"colorEnd", COLOR}, - {"gradientType", STRING}, {"text", STRING}, + {"textRelativeScale", FLOAT}, {"textColor", COLOR}, {"textBackgroundColor", COLOR}, {"fontPath", PATH}, diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index 6cc9d2409..955965262 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -21,9 +21,8 @@ struct GridEntry { template class GridComponent : public PrimaryComponent, protected IList { - using List = IList; - protected: + using List = IList; using List::mColumns; using List::mCursor; using List::mEntries; @@ -105,41 +104,64 @@ private: bool remove(const T& obj) override { return List::remove(obj); } int size() const override { return List::size(); } + enum class SelectorLayer { + TOP, + MIDDLE, + BOTTOM + }; + Renderer* mRenderer; std::function mCancelTransitionsCallback; std::function mCursorChangedCallback; - - std::string mItemType; - std::string mDefaultItem; float mEntryOffset; float mScrollPos; float mTransitionFactor; float mVisibleRows; - std::shared_ptr mFont; + int mPreviousScrollVelocity; + bool mPositiveDirection; + bool mGamelistView; + bool mLayoutValid; + bool mWasScrolling; + bool mJustCalculatedLayout; + bool mSuppressTransitions; + std::string mItemType; + std::string mDefaultItem; + std::unique_ptr mBackgroundImage; + float mBackgroundRelativeScale; + unsigned int mBackgroundColor; + unsigned int mBackgroundColorEnd; + bool mBackgroundColorGradientHorizontal; + bool mHasBackgroundColor; + std::unique_ptr mSelectorImage; + float mSelectorRelativeScale; + unsigned int mSelectorColor; + unsigned int mSelectorColorEnd; + bool mSelectorColorGradientHorizontal; + bool mHasSelectorColor; + SelectorLayer mSelectorLayer; + bool mFractionalRows; glm::vec2 mItemSize; float mItemScale; + float mItemRelativeScale; glm::vec2 mItemSpacing; + unsigned int mItemColor; + unsigned int mItemColorEnd; + bool mItemColorGradientHorizontal; bool mInstantItemTransitions; bool mInstantRowTransitions; - float mHorizontalMargin; - float mVerticalMargin; float mUnfocusedItemOpacity; + float mTextRelativeScale; unsigned int mTextColor; unsigned int mTextBackgroundColor; + std::shared_ptr mFont; LetterCase mLetterCase; LetterCase mLetterCaseCollections; LetterCase mLetterCaseGroupedCollections; float mLineSpacing; bool mFadeAbovePrimary; - int mPreviousScrollVelocity; - bool mPositiveDirection; - bool mGamelistView; - bool mFractionalRows; - bool mLayoutValid; - bool mWasScrolling; - bool mJustCalculatedLayout; - bool mSuppressTransitions; + float mHorizontalMargin; + float mVerticalMargin; }; template @@ -150,15 +172,37 @@ GridComponent::GridComponent() , mScrollPos {0.0f} , mTransitionFactor {1.0f} , mVisibleRows {1.0f} + , mPreviousScrollVelocity {0} + , mPositiveDirection {false} + , mGamelistView {std::is_same_v ? true : false} + , mLayoutValid {false} + , mWasScrolling {false} + , mJustCalculatedLayout {false} + , mSuppressTransitions {false} + , mBackgroundRelativeScale {1.0f} + , mBackgroundColor {0xFFFFFFFF} + , mBackgroundColorEnd {0xFFFFFFFF} + , mBackgroundColorGradientHorizontal {true} + , mHasBackgroundColor {false} + , mSelectorRelativeScale {1.0f} + , mSelectorColor {0xFFFFFFFF} + , mSelectorColorEnd {0xFFFFFFFF} + , mSelectorColorGradientHorizontal {true} + , mHasSelectorColor {false} + , mSelectorLayer {SelectorLayer::TOP} + , mFractionalRows {false} , mItemSize {glm::vec2 {mRenderer->getScreenWidth() * 0.15f, mRenderer->getScreenHeight() * 0.25f}} , mItemScale {1.05f} + , mItemRelativeScale {1.0f} , mItemSpacing {0.0f, 0.0f} + , mItemColor {0xFFFFFFFF} + , mItemColorEnd {0xFFFFFFFF} + , mItemColorGradientHorizontal {true} , mInstantItemTransitions {false} , mInstantRowTransitions {false} - , mHorizontalMargin {0.0f} - , mVerticalMargin {0.0f} , mUnfocusedItemOpacity {1.0f} + , mTextRelativeScale {1.0f} , mTextColor {0x000000FF} , mTextBackgroundColor {0xFFFFFF00} , mLetterCase {LetterCase::NONE} @@ -166,33 +210,31 @@ GridComponent::GridComponent() , mLetterCaseGroupedCollections {LetterCase::NONE} , mLineSpacing {1.5f} , mFadeAbovePrimary {false} - , mPreviousScrollVelocity {0} - , mPositiveDirection {false} - , mGamelistView {std::is_same_v ? true : false} - , mFractionalRows {false} - , mLayoutValid {false} - , mWasScrolling {false} - , mJustCalculatedLayout {false} - , mSuppressTransitions {false} + , mHorizontalMargin {0.0f} + , mVerticalMargin {0.0f} { } template void GridComponent::addEntry(Entry& entry, const std::shared_ptr& theme) { - bool dynamic {true}; - - if (!mGamelistView) - dynamic = false; + const bool dynamic {mGamelistView}; if (entry.data.itemPath != "" && ResourceManager::getInstance().fileExists(entry.data.itemPath)) { auto item = std::make_shared(false, dynamic); item->setLinearInterpolation(true); item->setMipmapping(true); - item->setMaxSize(mItemSize); + item->setMaxSize(mItemSize * mItemRelativeScale); item->setImage(entry.data.itemPath); item->applyTheme(theme, "system", "", ThemeFlags::ALL); + if (mItemColor != 0xFFFFFFFF) + item->setColorShift(mItemColor); + if (mItemColorEnd != mItemColor) { + item->setColorShiftEnd(mItemColorEnd); + if (!mItemColorGradientHorizontal) + item->setColorGradientHorizontal(false); + } item->setOrigin(0.5f, 0.5f); item->setRotateByTargetSize(true); entry.data.item = item; @@ -202,9 +244,16 @@ void GridComponent::addEntry(Entry& entry, const std::shared_ptr& auto defaultItem = std::make_shared(false, dynamic); defaultItem->setLinearInterpolation(true); defaultItem->setMipmapping(true); - defaultItem->setMaxSize(mItemSize); + defaultItem->setMaxSize(mItemSize * mItemRelativeScale); defaultItem->setImage(entry.data.defaultItemPath); defaultItem->applyTheme(theme, "system", "", ThemeFlags::ALL); + if (mItemColor != 0xFFFFFFFF) + defaultItem->setColorShift(mItemColor); + if (mItemColorEnd != mItemColor) { + defaultItem->setColorShiftEnd(mItemColorEnd); + if (!mItemColorGradientHorizontal) + defaultItem->setColorGradientHorizontal(false); + } defaultItem->setOrigin(0.5f, 0.5f); defaultItem->setRotateByTargetSize(true); entry.data.item = defaultItem; @@ -214,7 +263,7 @@ void GridComponent::addEntry(Entry& entry, const std::shared_ptr& // If no item image is present, add item text as fallback. auto text = std::make_shared( entry.name, mFont, 0x000000FF, Alignment::ALIGN_CENTER, Alignment::ALIGN_CENTER, - glm::vec3 {0.0f, 0.0f, 0.0f}, mItemSize, 0x00000000); + glm::vec3 {0.0f, 0.0f, 0.0f}, mItemSize * mTextRelativeScale, 0x00000000); text->setOrigin(0.5f, 0.5f); text->setLineSpacing(mLineSpacing); if (!mGamelistView) @@ -237,9 +286,16 @@ void GridComponent::updateEntry(Entry& entry, const std::shared_ptr(false, true); item->setLinearInterpolation(true); item->setMipmapping(true); - item->setMaxSize(mItemSize); + item->setMaxSize(mItemSize * mItemRelativeScale); item->setImage(entry.data.itemPath); item->applyTheme(theme, "system", "", ThemeFlags::ALL); + if (mItemColor != 0xFFFFFFFF) + item->setColorShift(mItemColor); + if (mItemColorEnd != mItemColor) { + item->setColorShiftEnd(mItemColorEnd); + if (!mItemColorGradientHorizontal) + item->setColorGradientHorizontal(false); + } item->setOrigin(0.5f, 0.5f); item->setRotateByTargetSize(true); entry.data.item = item; @@ -528,10 +584,44 @@ template void GridComponent::render(const glm::mat4& parentTrans float opacity {1.0f}; trans[3].y -= (mItemSize.y + mItemSpacing.y) * mScrollPos; - mRenderer->setMatrix(trans); + + auto selectorRenderFunc = [this, &trans](std::vector::const_iterator it, + const float scale, const float opacity, + const bool cursorEntry, const bool lastCursorEntry) { + if (mSelectorImage != nullptr) { + mSelectorImage->setPosition(mEntries.at(*it).data.item->getPosition()); + mSelectorImage->setScale(scale); + mSelectorImage->setOpacity(opacity); + mSelectorImage->render(trans); + } + else if (mHasSelectorColor) { + // If a selector color is set but no selector image, then render a rectangle. + const float sizeX {mItemSize.x * (cursorEntry || lastCursorEntry ? scale : 1.0f) * + mSelectorRelativeScale}; + const float sizeY {mItemSize.y * (cursorEntry || lastCursorEntry ? scale : 1.0f) * + mSelectorRelativeScale}; + float posX {mEntries.at(*it).data.item->getPosition().x - mItemSize.x * 0.5f}; + float posY {mEntries.at(*it).data.item->getPosition().y - mItemSize.y * 0.5f}; + + if (cursorEntry || lastCursorEntry) { + posX -= ((mItemSize.x * scale * mSelectorRelativeScale) - mItemSize.x) / 2.0f; + posY -= ((mItemSize.y * scale * mSelectorRelativeScale) - mItemSize.y) / 2.0f; + } + else { + posX -= ((mItemSize.x * mSelectorRelativeScale) - mItemSize.x) / 2.0f; + posY -= ((mItemSize.y * mSelectorRelativeScale) - mItemSize.y) / 2.0f; + } + + mRenderer->setMatrix(trans); + mRenderer->drawRect(posX, posY, sizeX, sizeY, mSelectorColor, mSelectorColorEnd, + mSelectorColorGradientHorizontal, opacity); + } + }; for (auto it = renderEntries.cbegin(); it != renderEntries.cend(); ++it) { float metadataOpacity {1.0f}; + bool cursorEntry {false}; + bool lastCursorEntry {false}; if constexpr (std::is_same_v) { // If a game is marked as hidden, lower the opacity a lot. @@ -545,21 +635,61 @@ template void GridComponent::render(const glm::mat4& parentTrans opacity = mUnfocusedItemOpacity * metadataOpacity; if (*it == static_cast(mCursor)) { + cursorEntry = true; scale = glm::mix(1.0f, mItemScale, mTransitionFactor); opacity = glm::mix(mUnfocusedItemOpacity * metadataOpacity, 1.0f * metadataOpacity, mTransitionFactor); } else if (*it == static_cast(mLastCursor)) { + lastCursorEntry = true; scale = glm::mix(mItemScale, 1.0f, mTransitionFactor); opacity = glm::mix(1.0f * metadataOpacity, mUnfocusedItemOpacity * metadataOpacity, mTransitionFactor); } + if (cursorEntry && mSelectorLayer == SelectorLayer::BOTTOM) + selectorRenderFunc(it, scale, opacity, cursorEntry, lastCursorEntry); + + if (mBackgroundImage != nullptr) { + mBackgroundImage->setPosition(mEntries.at(*it).data.item->getPosition()); + mBackgroundImage->setScale(scale); + mBackgroundImage->setOpacity(opacity); + mBackgroundImage->render(trans); + } + else if (mHasBackgroundColor) { + // If a background color is set but no background image, then render a rectangle. + const float sizeX {mItemSize.x * (cursorEntry || lastCursorEntry ? scale : 1.0f) * + mBackgroundRelativeScale}; + const float sizeY {mItemSize.y * (cursorEntry || lastCursorEntry ? scale : 1.0f) * + mBackgroundRelativeScale}; + float posX {mEntries.at(*it).data.item->getPosition().x - mItemSize.x * 0.5f}; + float posY {mEntries.at(*it).data.item->getPosition().y - mItemSize.y * 0.5f}; + + if (cursorEntry || lastCursorEntry) { + posX -= ((mItemSize.x * scale * mBackgroundRelativeScale) - mItemSize.x) / 2.0f; + posY -= ((mItemSize.y * scale * mBackgroundRelativeScale) - mItemSize.y) / 2.0f; + } + else { + posX -= ((mItemSize.x * mBackgroundRelativeScale) - mItemSize.x) / 2.0f; + posY -= ((mItemSize.y * mBackgroundRelativeScale) - mItemSize.y) / 2.0f; + } + + mRenderer->setMatrix(trans); + mRenderer->drawRect(posX, posY, sizeX, sizeY, mBackgroundColor, mBackgroundColorEnd, + mBackgroundColorGradientHorizontal, opacity); + } + + if (cursorEntry && mSelectorLayer == SelectorLayer::MIDDLE) + selectorRenderFunc(it, scale, opacity, cursorEntry, lastCursorEntry); + mEntries.at(*it).data.item->setScale(scale); mEntries.at(*it).data.item->setOpacity(opacity); mEntries.at(*it).data.item->render(trans); mEntries.at(*it).data.item->setScale(1.0f); mEntries.at(*it).data.item->setOpacity(1.0f); + + if (cursorEntry && mSelectorLayer == SelectorLayer::TOP) + selectorRenderFunc(it, scale, opacity, cursorEntry, lastCursorEntry); } mRenderer->popClipRect(); @@ -599,6 +729,135 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, if (elem->has("itemScale")) mItemScale = glm::clamp(elem->get("itemScale"), 0.5f, 2.0f); + if (elem->has("itemRelativeScale")) + mItemRelativeScale = glm::clamp(elem->get("itemRelativeScale"), 0.2f, 1.0f); + + if (elem->has("backgroundRelativeScale")) + mBackgroundRelativeScale = + glm::clamp(elem->get("backgroundRelativeScale"), 0.2f, 1.0f); + + mHasBackgroundColor = false; + + if (elem->has("backgroundColor")) { + mHasBackgroundColor = true; + mBackgroundColor = elem->get("backgroundColor"); + mBackgroundColorEnd = mBackgroundColor; + } + if (elem->has("backgroundColorEnd")) + mBackgroundColorEnd = elem->get("backgroundColorEnd"); + + if (elem->has("backgroundGradientType")) { + const std::string& gradientType {elem->get("backgroundGradientType")}; + if (gradientType == "horizontal") { + mBackgroundColorGradientHorizontal = true; + } + else if (gradientType == "vertical") { + mBackgroundColorGradientHorizontal = false; + } + else { + mBackgroundColorGradientHorizontal = true; + LOG(LogWarning) << "GridComponent: Invalid theme configuration, property " + "\"backgroundGradientType\" for element \"" + << element.substr(5) << "\" defined as \"" << gradientType << "\""; + } + } + + if (elem->has("selectorRelativeScale")) + mSelectorRelativeScale = glm::clamp(elem->get("selectorRelativeScale"), 0.2f, 1.0f); + + mHasSelectorColor = false; + + if (elem->has("selectorColor")) { + mHasSelectorColor = true; + mSelectorColor = elem->get("selectorColor"); + mSelectorColorEnd = mSelectorColor; + } + if (elem->has("selectorColorEnd")) + mSelectorColorEnd = elem->get("selectorColorEnd"); + + if (elem->has("selectorGradientType")) { + const std::string& gradientType {elem->get("selectorGradientType")}; + if (gradientType == "horizontal") { + mSelectorColorGradientHorizontal = true; + } + else if (gradientType == "vertical") { + mSelectorColorGradientHorizontal = false; + } + else { + mSelectorColorGradientHorizontal = true; + LOG(LogWarning) << "GridComponent: Invalid theme configuration, property " + "\"selectorGradientType\" for element \"" + << element.substr(5) << "\" defined as \"" << gradientType << "\""; + } + } + + if (elem->has("backgroundImage")) { + const std::string& path {elem->get("backgroundImage")}; + if (Utils::FileSystem::exists(path) && !Utils::FileSystem::isDirectory(path)) { + mBackgroundImage = std::make_unique(false, false); + mBackgroundImage->setLinearInterpolation(true); + mBackgroundImage->setResize(mItemSize * mBackgroundRelativeScale); + mBackgroundImage->setOrigin(0.5f, 0.5f); + if (mHasBackgroundColor) { + mBackgroundImage->setColorShift(mBackgroundColor); + if (mBackgroundColor != mBackgroundColorEnd) { + mBackgroundImage->setColorShiftEnd(mBackgroundColorEnd); + if (!mBackgroundColorGradientHorizontal) + mBackgroundImage->setColorGradientHorizontal(false); + } + } + mBackgroundImage->setImage(elem->get("backgroundImage")); + } + else { + LOG(LogWarning) << "GridComponent: Invalid theme configuration, property " + "\"backgroundImage\" for element \"" + << element.substr(5) << "\", image does not exist: \"" << path << "\""; + } + } + + if (elem->has("selectorImage")) { + const std::string& path {elem->get("selectorImage")}; + if (Utils::FileSystem::exists(path) && !Utils::FileSystem::isDirectory(path)) { + mSelectorImage = std::make_unique(false, false); + mSelectorImage->setLinearInterpolation(true); + mSelectorImage->setResize(mItemSize * mSelectorRelativeScale); + mSelectorImage->setOrigin(0.5f, 0.5f); + if (mHasSelectorColor) { + mSelectorImage->setColorShift(mSelectorColor); + if (mBackgroundColor != mBackgroundColorEnd) { + mSelectorImage->setColorShiftEnd(mSelectorColorEnd); + if (!mSelectorColorGradientHorizontal) + mSelectorImage->setColorGradientHorizontal(false); + } + } + mSelectorImage->setImage(elem->get("selectorImage")); + } + else { + LOG(LogWarning) << "GridComponent: Invalid theme configuration, property " + "\"selectorImage\" for element \"" + << element.substr(5) << "\", image does not exist: \"" << path << "\""; + } + } + + if (elem->has("selectorLayer")) { + const std::string& selectorLayer {elem->get("selectorLayer")}; + if (selectorLayer == "top") { + mSelectorLayer = SelectorLayer::TOP; + } + else if (selectorLayer == "middle") { + mSelectorLayer = SelectorLayer::MIDDLE; + } + else if (selectorLayer == "bottom") { + mSelectorLayer = SelectorLayer::BOTTOM; + } + else { + mSelectorLayer = SelectorLayer::TOP; + LOG(LogWarning) << "GridComponent: Invalid theme configuration, property " + "\"selectorLayer\" for element \"" + << element.substr(5) << "\" defined as \"" << selectorLayer << "\""; + } + } + if (elem->has("itemTransitions")) { const std::string& itemTransitions {elem->get("itemTransitions")}; if (itemTransitions == "animate") { @@ -660,11 +919,37 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, mItemSpacing.y = ((mItemSize.y * mItemScale) - mItemSize.y) / 2.0f; } + if (elem->has("itemColor")) { + mItemColor = elem->get("itemColor"); + mItemColorEnd = mItemColor; + } + if (elem->has("itemColorEnd")) + mItemColorEnd = elem->get("itemColorEnd"); + + if (elem->has("itemGradientType")) { + const std::string& gradientType {elem->get("itemGradientType")}; + if (gradientType == "horizontal") { + mItemColorGradientHorizontal = true; + } + else if (gradientType == "vertical") { + mItemColorGradientHorizontal = false; + } + else { + mItemColorGradientHorizontal = true; + LOG(LogWarning) << "GridComponent: Invalid theme configuration, property " + "\"itemGradientType\" for element \"" + << element.substr(5) << "\" defined as \"" << gradientType << "\""; + } + } + if (elem->has("unfocusedItemOpacity")) mUnfocusedItemOpacity = glm::clamp(elem->get("unfocusedItemOpacity"), 0.1f, 1.0f); mFont = Font::getFromTheme(elem, properties, mFont, 0.0f, (mItemScale > 1.0f)); + if (elem->has("textRelativeScale")) + mTextRelativeScale = glm::clamp(elem->get("textRelativeScale"), 0.2f, 1.0f); + if (elem->has("textColor")) mTextColor = elem->get("textColor"); if (elem->has("textBackgroundColor")) @@ -730,6 +1015,9 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, } } + if (elem->has("fadeAbovePrimary")) + mFadeAbovePrimary = elem->get("fadeAbovePrimary"); + mSize.x = glm::clamp(mSize.x, mRenderer->getScreenWidth() * 0.05f, mRenderer->getScreenWidth() * 1.0f); mSize.y = glm::clamp(mSize.y, mRenderer->getScreenHeight() * 0.05f, From cb4f78b7630d0429ba183ed694aa3b222b1cc1f0 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Fri, 9 Dec 2022 19:27:48 +0100 Subject: [PATCH 054/101] Added color shift support to CarouselComponent items. --- es-core/src/ThemeData.cpp | 5 +- .../components/primary/CarouselComponent.h | 168 +++++++++++------- 2 files changed, 111 insertions(+), 62 deletions(-) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 932a868bb..3e680943b 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -115,11 +115,14 @@ std::map> {"itemsAfterCenter", UNSIGNED_INTEGER}, {"itemSize", NORMALIZED_PAIR}, {"itemScale", FLOAT}, - {"itemTransitions", STRING}, {"itemInterpolation", STRING}, {"itemRotation", FLOAT}, {"itemRotationOrigin", NORMALIZED_PAIR}, {"itemAxisHorizontal", BOOLEAN}, + {"itemColor", COLOR}, + {"itemColorEnd", COLOR}, + {"itemGradientType", STRING}, + {"itemTransitions", STRING}, {"itemHorizontalAlignment", STRING}, {"itemVerticalAlignment", STRING}, {"wheelHorizontalAlignment", STRING}, diff --git a/es-core/src/components/primary/CarouselComponent.h b/es-core/src/components/primary/CarouselComponent.h index 2ee48eeeb..28f8a8810 100644 --- a/es-core/src/components/primary/CarouselComponent.h +++ b/es-core/src/components/primary/CarouselComponent.h @@ -24,9 +24,8 @@ struct CarouselEntry { template class CarouselComponent : public PrimaryComponent, protected IList { - using List = IList; - protected: + using List = IList; using List::mCursor; using List::mEntries; using List::mScrollVelocity; @@ -122,41 +121,44 @@ private: bool mPositiveDirection; bool mTriggerJump; bool mGamelistView; + bool mLegacyMode; CarouselType mType; std::string mItemType; std::string mDefaultItem; - bool mLegacyMode; - std::shared_ptr mFont; - unsigned int mTextColor; - unsigned int mTextBackgroundColor; - float mLineSpacing; - Alignment mItemHorizontalAlignment; - Alignment mItemVerticalAlignment; - Alignment mWheelHorizontalAlignment; - float mUnfocusedItemOpacity; float mMaxItemCount; int mItemsBeforeCenter; int mItemsAfterCenter; glm::vec2 mItemSize; - bool mLinearInterpolation; - bool mInstantItemTransitions; - bool mItemAxisHorizontal; - bool mFadeAbovePrimary; - LetterCase mLetterCase; - LetterCase mLetterCaseCollections; - LetterCase mLetterCaseGroupedCollections; float mItemScale; + bool mLinearInterpolation; float mItemRotation; glm::vec2 mItemRotationOrigin; - unsigned int mCarouselColor; - unsigned int mCarouselColorEnd; - bool mColorGradientHorizontal; + bool mItemAxisHorizontal; + unsigned int mItemColorShift; + unsigned int mItemColorShiftEnd; + bool mItemColorGradientHorizontal; + bool mInstantItemTransitions; + Alignment mItemHorizontalAlignment; + Alignment mItemVerticalAlignment; + Alignment mWheelHorizontalAlignment; + float mHorizontalOffset; + float mVerticalOffset; bool mReflections; float mReflectionsOpacity; float mReflectionsFalloff; - float mHorizontalOffset; - float mVerticalOffset; + float mUnfocusedItemOpacity; + unsigned int mCarouselColor; + unsigned int mCarouselColorEnd; + bool mColorGradientHorizontal; + unsigned int mTextColor; + unsigned int mTextBackgroundColor; + std::shared_ptr mFont; + LetterCase mLetterCase; + LetterCase mLetterCaseCollections; + LetterCase mLetterCaseGroupedCollections; + float mLineSpacing; + bool mFadeAbovePrimary; }; template @@ -172,39 +174,42 @@ CarouselComponent::CarouselComponent() , mPositiveDirection {false} , mTriggerJump {false} , mGamelistView {std::is_same_v ? true : false} - , mType {CarouselType::HORIZONTAL} , mLegacyMode {false} - , mFont {Font::get(FONT_SIZE_LARGE)} - , mTextColor {0x000000FF} - , mTextBackgroundColor {0xFFFFFF00} - , mLineSpacing {1.5f} - , mItemHorizontalAlignment {ALIGN_CENTER} - , mItemVerticalAlignment {ALIGN_CENTER} - , mWheelHorizontalAlignment {ALIGN_CENTER} - , mUnfocusedItemOpacity {0.5f} + , mType {CarouselType::HORIZONTAL} , mMaxItemCount {3.0f} , mItemsBeforeCenter {8} , mItemsAfterCenter {8} , mItemSize {glm::vec2 {Renderer::getScreenWidth() * 0.25f, Renderer::getScreenHeight() * 0.155f}} - , mLinearInterpolation {false} - , mInstantItemTransitions {false} - , mItemAxisHorizontal {false} - , mFadeAbovePrimary {false} - , mLetterCase {LetterCase::NONE} - , mLetterCaseCollections {LetterCase::NONE} - , mLetterCaseGroupedCollections {LetterCase::NONE} , mItemScale {1.2f} + , mLinearInterpolation {false} , mItemRotation {7.5f} , mItemRotationOrigin {-3.0f, 0.5f} - , mCarouselColor {0} - , mCarouselColorEnd {0} - , mColorGradientHorizontal {true} + , mItemAxisHorizontal {false} + , mItemColorShift {0xFFFFFFFF} + , mItemColorShiftEnd {0xFFFFFFFF} + , mItemColorGradientHorizontal {true} + , mInstantItemTransitions {false} + , mItemHorizontalAlignment {ALIGN_CENTER} + , mItemVerticalAlignment {ALIGN_CENTER} + , mWheelHorizontalAlignment {ALIGN_CENTER} + , mHorizontalOffset {0.0f} + , mVerticalOffset {0.0f} , mReflections {false} , mReflectionsOpacity {0.5f} , mReflectionsFalloff {1.0f} - , mHorizontalOffset {0.0f} - , mVerticalOffset {0.0f} + , mUnfocusedItemOpacity {0.5f} + , mCarouselColor {0} + , mCarouselColorEnd {0} + , mColorGradientHorizontal {true} + , mTextColor {0x000000FF} + , mTextBackgroundColor {0xFFFFFF00} + , mFont {Font::get(FONT_SIZE_LARGE)} + , mLetterCase {LetterCase::NONE} + , mLetterCaseCollections {LetterCase::NONE} + , mLetterCaseGroupedCollections {LetterCase::NONE} + , mLineSpacing {1.5f} + , mFadeAbovePrimary {false} { } @@ -248,6 +253,12 @@ void CarouselComponent::addEntry(Entry& entry, const std::shared_ptrsetMaxSize(glm::round(mItemSize * (mItemScale >= 1.0f ? mItemScale : 1.0f))); item->setImage(entry.data.itemPath); item->applyTheme(theme, "system", "", ThemeFlags::ALL); + if (mItemColorShift != 0xFFFFFFFF) + item->setColorShift(mItemColorShift); + if (mItemColorShiftEnd != mItemColorShift) + item->setColorShiftEnd(mItemColorShiftEnd); + if (!mItemColorGradientHorizontal) + item->setColorGradientHorizontal(false); item->setRotateByTargetSize(true); entry.data.item = item; } @@ -260,6 +271,12 @@ void CarouselComponent::addEntry(Entry& entry, const std::shared_ptr= 1.0f ? mItemScale : 1.0f))); defaultItem->setImage(entry.data.defaultItemPath); defaultItem->applyTheme(theme, "system", "", ThemeFlags::ALL); + if (mItemColorShift != 0xFFFFFFFF) + defaultItem->setColorShift(mItemColorShift); + if (mItemColorShiftEnd != mItemColorShift) + defaultItem->setColorShiftEnd(mItemColorShiftEnd); + if (!mItemColorGradientHorizontal) + defaultItem->setColorGradientHorizontal(false); defaultItem->setRotateByTargetSize(true); entry.data.item = defaultItem; } @@ -320,6 +337,12 @@ void CarouselComponent::updateEntry(Entry& entry, const std::shared_ptrsetMaxSize(glm::round(mItemSize * (mItemScale >= 1.0f ? mItemScale : 1.0f))); item->setImage(entry.data.itemPath); item->applyTheme(theme, "system", "", ThemeFlags::ALL); + if (mItemColorShift != 0xFFFFFFFF) + item->setColorShift(mItemColorShift); + if (mItemColorShiftEnd != mItemColorShift) + item->setColorShiftEnd(mItemColorShiftEnd); + if (!mItemColorGradientHorizontal) + item->setColorGradientHorizontal(false); item->setRotateByTargetSize(true); entry.data.item = item; } @@ -1008,23 +1031,6 @@ void CarouselComponent::applyTheme(const std::shared_ptr& theme, if (elem->has("itemScale")) mItemScale = glm::clamp(elem->get("itemScale"), 0.2f, 3.0f); - if (elem->has("itemTransitions")) { - const std::string& itemTransitions {elem->get("itemTransitions")}; - if (itemTransitions == "animate") { - mInstantItemTransitions = false; - } - else if (itemTransitions == "instant") { - mInstantItemTransitions = true; - } - else { - mInstantItemTransitions = false; - LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " - "\"itemTransitions\" for element \"" - << element.substr(9) << "\" defined as \"" << itemTransitions - << "\""; - } - } - if (elem->has("itemInterpolation")) { const std::string& itemInterpolation {elem->get("itemInterpolation")}; if (itemInterpolation == "linear") { @@ -1042,6 +1048,23 @@ void CarouselComponent::applyTheme(const std::shared_ptr& theme, } } + if (elem->has("itemTransitions")) { + const std::string& itemTransitions {elem->get("itemTransitions")}; + if (itemTransitions == "animate") { + mInstantItemTransitions = false; + } + else if (itemTransitions == "instant") { + mInstantItemTransitions = true; + } + else { + mInstantItemTransitions = false; + LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " + "\"itemTransitions\" for element \"" + << element.substr(9) << "\" defined as \"" << itemTransitions + << "\""; + } + } + if (elem->has("itemRotation")) mItemRotation = elem->get("itemRotation"); @@ -1051,6 +1074,29 @@ void CarouselComponent::applyTheme(const std::shared_ptr& theme, mItemAxisHorizontal = (elem->has("itemAxisHorizontal") && elem->get("itemAxisHorizontal")); + if (elem->has("itemColor")) { + mItemColorShift = elem->get("itemColor"); + mItemColorShiftEnd = mItemColorShift; + } + if (elem->has("itemColorEnd")) + mItemColorShiftEnd = elem->get("itemColorEnd"); + + if (elem->has("itemGradientType")) { + const std::string& gradientType {elem->get("itemGradientType")}; + if (gradientType == "horizontal") { + mItemColorGradientHorizontal = true; + } + else if (gradientType == "vertical") { + mItemColorGradientHorizontal = false; + } + else { + mItemColorGradientHorizontal = true; + LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " + "\"gradientType\" for element \"" + << element.substr(9) << "\" defined as \"" << gradientType << "\""; + } + } + if (elem->has("itemHorizontalAlignment")) { const std::string& alignment {elem->get("itemHorizontalAlignment")}; if (alignment == "left" && mType != CarouselType::HORIZONTAL) { From 4e56dafc5954d464b13af1ac4946723fb1c4f509 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Fri, 9 Dec 2022 19:37:18 +0100 Subject: [PATCH 055/101] Made the SystemView grid animation slightly longer/slower. --- es-app/src/views/SystemView.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index e3f17aac8..3bf873d71 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -357,7 +357,7 @@ void SystemView::onCursorChanged(const CursorState& state) float timeDiff {1.0f}; if (mGrid != nullptr) { - animTime = 250.0f; + animTime = 300.0f; timeMin = 100.0f; } From f0e3addee6c6edae69a2a58612a309719a7ce61c Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sat, 10 Dec 2022 14:31:04 +0100 Subject: [PATCH 056/101] Added support for setting one of the itemSize property axes to -1 to get square GridComponent items. --- es-core/src/components/primary/GridComponent.h | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index 955965262..d2c4ff3a9 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -722,8 +722,21 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, mFractionalRows = (elem->has("fractionalRows") && elem->get("fractionalRows")); if (elem->has("itemSize")) { - const glm::vec2& itemSize {glm::clamp(elem->get("itemSize"), 0.05f, 1.0f)}; - mItemSize = itemSize * glm::vec2(Renderer::getScreenWidth(), Renderer::getScreenHeight()); + const glm::vec2& itemSize {elem->get("itemSize")}; + if (!(itemSize.x == -1 && itemSize.y == -1)) { + if (itemSize.x == -1) { + mItemSize.y = glm::clamp(itemSize.y, 0.05f, 1.0f) * mRenderer->getScreenHeight(); + mItemSize.x = mItemSize.y; + } + else if (itemSize.y == -1) { + mItemSize.x = glm::clamp(itemSize.x, 0.05f, 1.0f) * mRenderer->getScreenWidth(); + mItemSize.y = mItemSize.x; + } + else { + mItemSize = glm::clamp(itemSize, 0.05f, 1.0f) * + glm::vec2(mRenderer->getScreenWidth(), mRenderer->getScreenHeight()); + } + } } if (elem->has("itemScale")) From c6981084e0ac5a45cba1b03313ac1cc45a828e70 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sat, 10 Dec 2022 20:20:45 +0100 Subject: [PATCH 057/101] Added cover fit cropping support to ImageComponent. --- es-core/src/components/ImageComponent.cpp | 70 ++++++++++++++++++----- es-core/src/components/ImageComponent.h | 20 ++++--- 2 files changed, 68 insertions(+), 22 deletions(-) diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 9922485b8..489cb6092 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -22,6 +22,7 @@ ImageComponent::ImageComponent(bool forceLoad, bool dynamic) , mFlipX {false} , mFlipY {false} , mTargetIsMax {false} + , mTargetIsCrop {false} , mTileWidth {0.0f} , mTileHeight {0.0f} , mColorShift {0xFFFFFFFF} @@ -93,6 +94,8 @@ void ImageComponent::setImage(const std::string& path, bool tile) mMipmapping, static_cast(mSize.x), static_cast(mSize.y), mTileWidth, mTileHeight); mTexture->rasterizeAt(mSize.x, mSize.y); + if (mTargetIsCrop) + coverFitCrop(); onSizeChanged(); } } @@ -127,6 +130,7 @@ void ImageComponent::setResize(const float width, const float height) { mTargetSize = glm::vec2 {width, height}; mTargetIsMax = false; + mTargetIsCrop = false; resize(); } @@ -134,6 +138,7 @@ void ImageComponent::setResize(const glm::vec2& size, bool rasterize) { mTargetSize = size; mTargetIsMax = false; + mTargetIsCrop = false; resize(rasterize); } @@ -141,39 +146,48 @@ void ImageComponent::setMaxSize(const float width, const float height) { mTargetSize = glm::vec2 {width, height}; mTargetIsMax = true; + mTargetIsCrop = false; resize(); } -void ImageComponent::cropLeft(const float percent) +void ImageComponent::setCroppedSize(const glm::vec2& size) { - assert(percent >= 0.0f && percent <= 1.0f); - mTopLeftCrop.x = percent; + mTargetSize = size; + mTargetIsMax = false; + mTargetIsCrop = true; + resize(); } -void ImageComponent::cropTop(const float percent) +void ImageComponent::cropLeft(const float value) { - assert(percent >= 0.0f && percent <= 1.0f); - mTopLeftCrop.y = percent; + assert(value >= 0.0f && value <= 1.0f); + mTopLeftCrop.x = value; } -void ImageComponent::cropRight(const float percent) +void ImageComponent::cropTop(const float value) { - assert(percent >= 0.0f && percent <= 1.0f); - mBottomRightCrop.x = 1.0f - percent; + assert(value >= 0.0f && value <= 1.0f); + mTopLeftCrop.y = value; } -void ImageComponent::cropBot(const float percent) +void ImageComponent::cropRight(const float value) { - assert(percent >= 0.0f && percent <= 1.0f); - mBottomRightCrop.y = 1.0f - percent; + assert(value >= 0.0f && value <= 1.0f); + mBottomRightCrop.x = 1.0f - value; } -void ImageComponent::crop(const float left, const float top, const float right, const float bot) +void ImageComponent::cropBottom(const float value) +{ + assert(value >= 0.0f && value <= 1.0f); + mBottomRightCrop.y = 1.0f - value; +} + +void ImageComponent::crop(const float left, const float top, const float right, const float bottom) { cropLeft(left); cropTop(top); cropRight(right); - cropBot(bot); + cropBottom(bottom); } void ImageComponent::uncrop() @@ -182,6 +196,24 @@ void ImageComponent::uncrop() crop(0.0f, 0.0f, 0.0f, 0.0f); } +void ImageComponent::coverFitCrop() +{ + assert(mTargetIsCrop); + + if (std::round(mSize.y) > std::round(mTargetSize.y)) { + const float cropSize {1.0f - (mTargetSize.y / mSize.y)}; + cropTop(cropSize / 2.0f); + cropBottom(cropSize / 2.0f); + mSize.y = mSize.y - (mSize.y * cropSize); + } + else { + const float cropSize {1.0f - (mTargetSize.x / mSize.x)}; + cropLeft(cropSize / 2.0f); + cropRight(cropSize / 2.0f); + mSize.x = mSize.x - (mSize.x * cropSize); + } +} + void ImageComponent::cropTransparentPadding(const float maxSizeX, const float maxSizeY) { if (mSize == glm::vec2 {0.0f, 0.0f}) @@ -562,7 +594,7 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, if (elem->has("colorEnd")) setColorShiftEnd(elem->get("colorEnd")); if (elem->has("gradientType")) { - const std::string gradientType {elem->get("gradientType")}; + const std::string& gradientType {elem->get("gradientType")}; if (gradientType == "horizontal") { setColorGradientHorizontal(true); } @@ -617,6 +649,12 @@ void ImageComponent::resize(bool rasterize) mSize.x = std::min((mSize.y / textureSize.y) * textureSize.x, mTargetSize.x); } } + else if (mTargetIsCrop) { + // Size texture to allow for cropped image to fill the entire area. + const float cropFactor { + std::max(mTargetSize.x / textureSize.x, mTargetSize.y / textureSize.y)}; + mSize = textureSize * cropFactor; + } else { // If both axes are set we just stretch or squash, if no axes are set we do nothing. mSize = mTargetSize == glm::vec2 {0.0f, 0.0f} ? textureSize : mTargetSize; @@ -640,6 +678,8 @@ void ImageComponent::resize(bool rasterize) if (rasterize) { mTexture->rasterizeAt(mSize.x, mSize.y); + if (mTargetIsCrop) + coverFitCrop(); onSizeChanged(); } } diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index 54d0d6e63..4813724d4 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -47,6 +47,9 @@ public: void setMaxSize(const float width, const float height); void setMaxSize(const glm::vec2& size) { setMaxSize(size.x, size.y); } + // Resize and crop image so it fills the entire area defined by the size parameter. + void setCroppedSize(const glm::vec2& size); + void setTileSize(const float width, const float height) { mTileWidth = width; @@ -55,14 +58,16 @@ public: glm::vec2 getRotationSize() const override { return mRotateByTargetSize ? mTargetSize : mSize; } - // Applied AFTER image positioning and sizing. - // cropTop(0.2) will crop 20% of the top of the image. - void cropLeft(const float percent); - void cropTop(const float percent); - void cropRight(const float percent); - void cropBot(const float percent); - void crop(const float left, const float top, const float right, const float bot); + // Applied after image positioning and sizing. + void cropLeft(const float value); + void cropTop(const float value); + void cropRight(const float value); + void cropBottom(const float value); + void crop(const float left, const float top, const float right, const float bottom); void uncrop(); + // This essentially implements CSS "object-fit: cover" and has nothing to do with the + // cover image type (as the name may seem to imply). + void coverFitCrop(); // This crops any entirely transparent areas around the actual image. // The arguments restrict how much the end result is allowed to be scaled. @@ -115,6 +120,7 @@ private: bool mFlipX; bool mFlipY; bool mTargetIsMax; + bool mTargetIsCrop; float mTileWidth; float mTileHeight; From c394e4ae8d88cad82c006f3b6370318ad8111752 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sat, 10 Dec 2022 20:25:49 +0100 Subject: [PATCH 058/101] Added an itemFit property to GridComponent. --- .../src/components/primary/GridComponent.h | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index d2c4ff3a9..9c4fcac40 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -110,6 +110,12 @@ private: BOTTOM }; + enum class ItemFit { + CONTAIN, + FILL, + COVER + }; + Renderer* mRenderer; std::function mCancelTransitionsCallback; std::function mCursorChangedCallback; @@ -144,6 +150,7 @@ private: glm::vec2 mItemSize; float mItemScale; float mItemRelativeScale; + ItemFit mItemFit; glm::vec2 mItemSpacing; unsigned int mItemColor; unsigned int mItemColorEnd; @@ -195,6 +202,7 @@ GridComponent::GridComponent() mRenderer->getScreenHeight() * 0.25f}} , mItemScale {1.05f} , mItemRelativeScale {1.0f} + , mItemFit {ItemFit::CONTAIN} , mItemSpacing {0.0f, 0.0f} , mItemColor {0xFFFFFFFF} , mItemColorEnd {0xFFFFFFFF} @@ -225,7 +233,12 @@ void GridComponent::addEntry(Entry& entry, const std::shared_ptr& auto item = std::make_shared(false, dynamic); item->setLinearInterpolation(true); item->setMipmapping(true); - item->setMaxSize(mItemSize * mItemRelativeScale); + if (mItemFit == ItemFit::CONTAIN) + item->setMaxSize(mItemSize * mItemRelativeScale); + else if (mItemFit == ItemFit::FILL) + item->setResize(mItemSize * mItemRelativeScale); + else if (mItemFit == ItemFit::COVER) + item->setCroppedSize(mItemSize * mItemRelativeScale); item->setImage(entry.data.itemPath); item->applyTheme(theme, "system", "", ThemeFlags::ALL); if (mItemColor != 0xFFFFFFFF) @@ -244,7 +257,12 @@ void GridComponent::addEntry(Entry& entry, const std::shared_ptr& auto defaultItem = std::make_shared(false, dynamic); defaultItem->setLinearInterpolation(true); defaultItem->setMipmapping(true); - defaultItem->setMaxSize(mItemSize * mItemRelativeScale); + if (mItemFit == ItemFit::CONTAIN) + defaultItem->setMaxSize(mItemSize * mItemRelativeScale); + else if (mItemFit == ItemFit::FILL) + defaultItem->setResize(mItemSize * mItemRelativeScale); + else if (mItemFit == ItemFit::COVER) + defaultItem->setCroppedSize(mItemSize * mItemRelativeScale); defaultItem->setImage(entry.data.defaultItemPath); defaultItem->applyTheme(theme, "system", "", ThemeFlags::ALL); if (mItemColor != 0xFFFFFFFF) @@ -286,7 +304,12 @@ void GridComponent::updateEntry(Entry& entry, const std::shared_ptr(false, true); item->setLinearInterpolation(true); item->setMipmapping(true); - item->setMaxSize(mItemSize * mItemRelativeScale); + if (mItemFit == ItemFit::CONTAIN) + item->setMaxSize(mItemSize * mItemRelativeScale); + else if (mItemFit == ItemFit::FILL) + item->setResize(mItemSize * mItemRelativeScale); + else if (mItemFit == ItemFit::COVER) + item->setCroppedSize(mItemSize * mItemRelativeScale); item->setImage(entry.data.itemPath); item->applyTheme(theme, "system", "", ThemeFlags::ALL); if (mItemColor != 0xFFFFFFFF) @@ -745,6 +768,25 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, if (elem->has("itemRelativeScale")) mItemRelativeScale = glm::clamp(elem->get("itemRelativeScale"), 0.2f, 1.0f); + if (elem->has("itemFit")) { + const std::string& itemFit {elem->get("itemFit")}; + if (itemFit == "contain") { + mItemFit = ItemFit::CONTAIN; + } + else if (itemFit == "fill") { + mItemFit = ItemFit::FILL; + } + else if (itemFit == "cover") { + mItemFit = ItemFit::COVER; + } + else { + mItemFit = ItemFit::CONTAIN; + LOG(LogWarning) << "GridComponent: Invalid theme configuration, property " + "\"itemFit\" for element \"" + << element.substr(5) << "\" defined as \"" << itemFit << "\""; + } + } + if (elem->has("backgroundRelativeScale")) mBackgroundRelativeScale = glm::clamp(elem->get("backgroundRelativeScale"), 0.2f, 1.0f); From 23438cd101634fad7c72c6d75bf03e0c5824ab60 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sat, 10 Dec 2022 20:27:05 +0100 Subject: [PATCH 059/101] Updated ThemeData with the new itemFit grid property. --- es-core/src/ThemeData.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 3e680943b..4dc67761a 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -175,6 +175,7 @@ std::map> {"itemSize", NORMALIZED_PAIR}, {"itemScale", FLOAT}, {"itemRelativeScale", FLOAT}, + {"itemFit", STRING}, {"itemSpacing", NORMALIZED_PAIR}, {"itemColor", COLOR}, {"itemColorEnd", COLOR}, From 7ba596775da64f33598adc3bb21746535a50f960 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sat, 10 Dec 2022 20:36:52 +0100 Subject: [PATCH 060/101] Fixed a GridComponent navigation issue when there was only a single column. --- es-core/src/components/IList.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/es-core/src/components/IList.h b/es-core/src/components/IList.h index 158b24ef1..5f0e02f3b 100644 --- a/es-core/src/components/IList.h +++ b/es-core/src/components/IList.h @@ -339,14 +339,14 @@ protected: bool doScroll {true}; // This is only needed for GridComponent. - if (mColumns != 0 && mScrollVelocity == -mColumns && mCursor < mColumns) { + if (mColumns > 1 && mScrollVelocity == -mColumns && mCursor < mColumns) { doScroll = false; } else if (mColumns != 0 && mScrollVelocity == mColumns) { if (size() - mCursor <= size() % mColumns) doScroll = false; - else if (mCursor >= (mColumns * mRows) - mColumns && size() - mCursor <= mColumns && - size() % mColumns == 0) + else if (mColumns != 1 && mCursor >= (mColumns * mRows) - mColumns && + size() - mCursor <= mColumns && size() % mColumns == 0) doScroll = false; else if (size() < mColumns) doScroll = false; From 51e06641f5462c455ee2daa6cbcec820a368dda3 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sat, 10 Dec 2022 21:14:23 +0100 Subject: [PATCH 061/101] Documentation update. --- CHANGELOG.md | 2 + THEMES-DEV.md | 100 ++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 86 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aed41d8cf..548c56b9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -127,6 +127,7 @@ * Improved theme element placement by replacing the "alignment" and "logoAlignment" properties with specific horizontal and vertical properties * Made it possible to use almost all game metadata field when theming text elements * Made it possible to set the image interpolation method (nearest neighbor or linear filtering) per image from the theme configuration +* Added support for resizing and cropping images to fill the entire defined area (cover fitting) * Changed the helpsystem properties entrySpacing and iconTextSpacing from fixed pixel values to relative values * Added support for using unsigned integers for theme properties * Added a metadataElement theme property to the image, video, animation and text element types to control fading and hiding of arbitrary elements @@ -185,6 +186,7 @@ * Added reflections support to the carousel * Added a new itemAxisHorizontal property to the carousel to keep wheel items horizontal at all times * Added carousel theme support for setting the opacity for unfocused entries +* Added carousel theme support for applying item color shifts * Added carousel theme support for setting item transitions to "slide" or "instant" * Added a fadeAbovePrimary property to control whether elements above the system view carousel and textlist should be rendered during fade transitions * Removed support for the thumbnail game media type diff --git a/THEMES-DEV.md b/THEMES-DEV.md index 732e74f8d..704720ac1 100644 --- a/THEMES-DEV.md +++ b/THEMES-DEV.md @@ -1059,13 +1059,9 @@ Properties: - Minimum value per axis is `0.05` and maximum value per axis is `1` - Default is `0.25 0.155` * `itemScale` - type: FLOAT. - - Selected item is increased in size by this scale. + - Selected item is scaled by the value defined by this property. - Minimum value is `0.2` and maximum value is `3` - Default is `1.2` -* `itemTransitions` - type: STRING - - How to render item transitions when navigating the carousel. By default a slide and opacity fade animation will be played when moving between items but if this property is set to `instant` then the transitions will be immediate. - - Valid values are `animate` or `instant` - - Default is `animate` * `itemInterpolation` - type: STRING - Interpolation method to use when scaling items. Nearest neighbor (`nearest`) preserves sharp pixels and linear filtering (`linear`) makes the image smoother. The effect of this property is primarily visible for raster graphic images, but it has a limited effect also when using scalable vector graphics (SVG) images as these are rasterized at a set resolution and then scaled using the GPU. - Valid values are `nearest` or `linear` @@ -1081,6 +1077,20 @@ Properties: * `itemAxisHorizontal` - type: BOOLEAN - If `type` has been set to "horizontal_wheel" or "vertical_wheel" then the items are normally rotated towards the center of the wheel as defined by `itemRotation` and `itemRotationOrigin`. But if enabling this property the items will not get rotated along their own axis, meaning they will retain their original horizontal orientation regardless of their position along the wheel. Make sure that `itemVerticalAlignment` is set to `center` when using this attribute or you'll get some strange alignment issues. - Default is `false` +* `itemColor` - type: COLOR + - Applies a color shift to the images defined by `staticItem`, `itemType` and `defaultItem` by multiplying each pixel's color by this color value. For example, an all-white image with `FF0000` applied would become completely red. You can also control the transparency of the images by setting the value to for example `FFFFFFAA`. This keeps all pixels at their normal color and only affects the alpha channel. + - Default is `FFFFFFFF` (no color shift applied) +* `itemColorEnd` - type: COLOR + - Works in the exact same way as `itemColor` but can be set as the end color to apply a color shift gradient. + - Default is the same value as `itemColor` +* `itemGradientType` - type: STRING + - The direction to apply the color gradient if both `itemColor` and `itemColorEnd` have been defined. + - Valid values are `horizontal` or `vertical` + - Default is `horizontal` +* `itemTransitions` - type: STRING + - How to render item transitions when navigating the carousel. By default a slide, scale and opacity fade animation will be played when moving between items (the latter two assuming `itemScale` and `unfocusedItemOpacity` have not been set to `1`) but if this property is set to `instant` then transitions will be immediate. + - Valid values are `animate` or `instant` + - Default is `animate` * `itemHorizontalAlignment` - type: STRING - Sets `staticItem` / `itemType` and `text` alignment relative to the carousel on the X axis, which applies when `type` is "vertical", "horizontal_wheel" or "vertical_wheel". - Valid values are `left`, `center` or `right` @@ -1121,7 +1131,7 @@ Properties: - Default is `FFFFFFD8` * `colorEnd` - type: COLOR - Setting this to something other than what is defined for `color` creates a color gradient on the background panel. - - Default is `FFFFFFD8` + - Default is the same value as `color` * `gradientType` - type: STRING - The direction to apply the color gradient if both `color` and `colorEnd` have been defined. - Valid values are `horizontal` or `vertical` @@ -1166,6 +1176,8 @@ Properties: An X*Y grid for navigating and selecting games or systems using the left/right and up/down buttons. The layout including the amount of columns and rows is automatically calculated based on the relevant property values. +Property names for the grid element have been kept as similar as possible to those of the carousel. This does however introduce some potential confusion for a few properties due to the varying nature of these two elements. More specifically a grid `item` may refer to both the overall item in the same manner as it's used for the carousel but it may also refer to the primary image used for the item (e.g. a system logo, cover, marquee etc). Be aware of this when using the `itemRelativeScale`, `itemFit`, `itemColor`, `itemColorEnd` and `itemGradientType` properties as these will not apply to the overall item. + Supported views: * `system` * `gamelist` @@ -1180,7 +1192,7 @@ Properties: - Minimum value per axis is `0.05` and maximum value per axis is `1` - Default is `1 0.8` * `origin` - type: NORMALIZED_PAIR - - Where on the element `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the textlist exactly in the middle of the screen. If the position and size attributes are themeable, origin is implied. + - Where on the element `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the grid exactly in the middle of the screen. If the position and size attributes are themeable, origin is implied. - Minimum value per axis is `0` and maximum value per axis is `1` - Default is `0 0` * `staticItem` - type: PATH @@ -1201,28 +1213,79 @@ Properties: - Default is `marquee` * `defaultItem` - type: PATH - Path to the default image file which will be displayed if the image defined via the `staticItem` or `itemType` property is not found. Most common extensions are supported (including .svg, .jpg, .png, and unanimated .gif). +* `backgroundImage` - type: PATH + - Path to an optional background image file which will be displayed behind the image defined by `staticItem`, `itemType` or `defaultItem`. The aspect ratio for this image will not be preserved, it will be stretched or squashed to the aspect ratio set by `itemSize`. Most common extensions are supported (including .svg, .jpg, .png, and unanimated .gif). +* `backgroundRelativeScale` - type: FLOAT. + - This property makes it possible to size the background relative to the overall item size. This is mostly useful when combined with the `selectorImage` property. + - Minimum value is `0.2` and maximum value is `1` + - Default is `1` +* `backgroundColor` - type: COLOR + - Applies a color shift or draws a colored rectangle. If an image has been defined using the `backgroundImage` property then each pixel of that image is multiplied by this color value. For example, an all-white image with `FF0000` applied would become completely red. You can also control the transparency of the image by setting the value to for example `FFFFFFAA`. This keeps all pixels at their normal color and only affects the alpha channel. If no background image has been defined, then a colored rectangle will be drawn instead. +* `backgroundColorEnd` - type: COLOR + - Works in the exact same way as `backgroundColor` but can be set as the end color to apply a color gradient. + - Default is the same value as `backgroundColor` +* `backgroundGradientType` - type: STRING + - The direction to apply the color gradient if both `backgroundColor` and `backgroundColorEnd` have been defined. + - Valid values are `horizontal` or `vertical` + - Default is `horizontal` +* `selectorImage` - type: PATH + - Path to an optional selector image file which will be displayed for the currently selected item. The aspect ratio for this image will not be preserved, it will be stretched or squashed to the aspect ratio set by `itemSize`. Most common extensions are supported (including .svg, .jpg, .png, and unanimated .gif). +* `selectorRelativeScale` - type: FLOAT. + - This property makes it possible to size the selector relative to the overall item size. This is mostly useful when combined with the `backgroundImage` property. + - Minimum value is `0.2` and maximum value is `1` + - Default is `1` +* `selectorColor` - type: COLOR + - Applies a color shift or draws a colored rectangle. If an image has been defined using the `selectorImage` property then each pixel of that image is multiplied by this color value. For example, an all-white image with `FF0000` applied would become completely red. You can also control the transparency of the image by setting the value to for example `FFFFFFAA`. This keeps all pixels at their normal color and only affects the alpha channel. If no selector image has been defined, then a colored rectangle will be drawn instead. +* `selectorColorEnd` - type: COLOR + - Works in the exact same way as `selectorColor` but can be set as the end color to apply a color gradient. + - Default is the same value as `selectorColor` +* `selectorGradientType` - type: STRING + - The direction to apply the color gradient if both `selectorColor` and `selectorColorEnd` have been defined. + - Valid values are `horizontal` or `vertical` + - Default is `horizontal` +* `selectorLayer` - type: STRING + - Defines at what layer position to place the selector. It can either be placed at the bottom, in the middle between the background and image/text or on top. + - Valid values are `bottom`, `middle` or `top` + - Default is `top` * `fractionalRows` - type: BOOLEAN - Whether to allow rendering of fractional rows of items. If set to false then the effective area of the overall element size will be snapped to the item height multiplied by `itemScale`. Note that if setting `itemScale` too high relative to the `itemSpacing` Y axis value then fractional rows may still be rendered even if the `fractionalRows` property is set to false. - Default is `false` * `itemSize` - type: NORMALIZED_PAIR - - Size of the item prior to multiplication by the `itemScale` value, i.e. the size of all unselected items. Both axes need to be defined. + - Size of the overall item prior to multiplication by the `itemScale` value, i.e. the size of all unselected items. If one of the axis is defined as `-1` then it will be set to the same pixel value as the other axis, resulting in a perfectly square item. If not using this approach then both axes need to be defined. - Minimum value per axis is `0.05` and maximum value per axis is `1` - Default is `0.15 0.25` * `itemScale` - type: FLOAT. - - Selected item is increased in size by this scale. + - Selected overall item is scaled by the value defined by this property. - Minimum value is `0.5` and maximum value is `2` - Default is `1.05` +* `itemRelativeScale` - type: FLOAT. + - This property makes it possible to size the image defined by `staticItem`, `itemType` or `defaultItem` relative to the overall item size. This is mostly useful when combined with the `backgroundImage` and `selectorImage` properties. + - Minimum value is `0.2` and maximum value is `1` + - Default is `1` +* `itemFit` - type: STRING + - Controls how to fit the image within the aspect ratio defined by `itemSize`. To scale and preserve the original aspect ratio, set the value to `contain`, to stretch/squash the image to fill the entire area set it to `fill` and to crop the image to fill the entire area set it to `cover` + - Valid values are `contain`, `fill` or `cover` + - Default is `contain` +* `itemSpacing` - type: NORMALIZED_PAIR + - The horizontal and vertical space between items. This value is added to the unscaled item size, i.e. `itemSize` before it's been multiplied by `itemScale`. This means that if an axis is set to `0` then unscaled items will be perfectly adjacent to each other on that axis but if `itemScale` has been set to higher than `1` then the currently selected item will overlap adjacent items. If this property is omitted then spacing will be automatically calculated so that no overlaps occur during scaling. However you'd normally want to define and adjust this property for an optimal layout. If one of the axis is defined as `-1` then it will be set to the same pixel value as the other axis. Note that all spacing calculations are based on the value defined by `itemSize` which may or may not be the same as the actual image sizes, depending on their aspect ratios and if the `itemFit` property is used. + - Minimum value per axis is `0` and maximum value per axis is `0.1` +* `itemColor` - type: COLOR + - Applies a color shift to the images defined by `staticItem`, `itemType` and `defaultItem` by multiplying each pixel's color by this color value. For example, an all-white image with `FF0000` applied would become completely red. You can also control the transparency of the images by setting the value to for example `FFFFFFAA`. This keeps all pixels at their normal color and only affects the alpha channel. +* `itemColorEnd` - type: COLOR + - Works in the exact same way as `itemColor` but can be set as the end color to apply a color shift gradient. + - Default is the same value as `itemColor` +* `itemGradientType` - type: STRING + - The direction to apply the color gradient if both `itemColor` and `itemColorEnd` have been defined. + - Valid values are `horizontal` or `vertical` + - Default is `horizontal` * `itemTransitions` - type: STRING - - How to render item transitions when navigating the grid. By default a scaling and opacity fade animation will be played when moving between items (assuming `itemScale` and `unfocusedItemOpacity` have not been set to `1`) but if this property is set to `instant` then the transitions will be immediate. + - How to render item transitions when navigating the grid. By default a scaling and opacity fade animation will be played when moving between items (assuming `itemScale` and `unfocusedItemOpacity` have not been set to `1`) but if this property is set to `instant` then transitions will be immediate. - Valid values are `animate` or `instant` - Default is `animate` * `rowTransitions` - type: STRING - - How to render row transitions when navigating the grid. By default a sliding animation will be rendered when moving between rows but if this property is set to `instant` then the transitions will be immediate. + - How to render row transitions when navigating the grid. By default a sliding animation will be rendered when moving between rows but if this property is set to `instant` then transitions will be immediate. - Valid values are `animate` or `instant` - Default is `animate` -* `itemSpacing` - type: NORMALIZED_PAIR - - The horizontal and vertical space between items. This value is added to the unscaled item size, i.e. `itemSize` before it's been multiplied by `itemScale`. This means that if an axis is set to `0` then unscaled items will be perfectly adjacent to each other on that axis but if `itemScale` has been set to higher than `1` then the currently selected item may overlap adjacent items. If this property is omitted then spacing will be automatically calculated so that no overlaps occur during scaling. However you'd normally want to define and adjust this property for an optimal layout. If one of the axis is defined as `-1` then it will be set to the same pixel value as the other axis. Note that all spacing calculations are based on the value defined by `itemSize` which may or may not be the same as the actual image sizes, depending on their aspect ratios. - - Minimum value per axis is `0` and maximum value per axis is `0.1` * `unfocusedItemOpacity` - type: FLOAT - Sets the opacity for the items that are not currently focused. - Minimum value is `0.1` and maximum value is `1` @@ -1254,6 +1317,9 @@ Properties: - Controls the space between lines (as a multiple of the font height). Due to the imprecise nature of typefaces where certain glyphs (characters) may exceed the requested font size, it's recommended to keep this value at around `1.1` or higher. This way overlapping glyphs or characters being cut off at the top or bottom will be prevented. - Minimum value is `0.5` and maximum value is `3` - Default is `1.5` +* `fadeAbovePrimary` - type: BOOLEAN + - When using fade transitions, all elements in the `system` view with a zIndex value higher than the grid are by default still rendered during transitions. If this property is enabled then all such elements will instead be faded out. Note that elements below the grid will be dimmed to black and elements above the grid will be faded to transparent. + - Default is `false` * `zIndex` - type: FLOAT - z-index value for element. Elements will be rendered in order of zIndex value from low to high. - Default is `50` @@ -1426,9 +1492,11 @@ Properties: - Valid values are `nearest` or `linear` - Default is `nearest` * `color` - type: COLOR - - Multiply each pixel's color by this color. For example, an all-white image with `FF0000` would become completely red. You can also control the transparency of an image with `FFFFFFAA` - keeping all the pixels their normal color and only affecting the alpha channel. + - Applies a color shift to the image by multiplying each pixel's color by this color value. For example, an all-white image with `FF0000` applied would become completely red. You can also control the transparency of the image by setting the value to for example `FFFFFFAA`. This keeps all pixels at their normal color and only affects the alpha channel. + - Default is `FFFFFFFF` (no color shift applied) * `colorEnd` - type: COLOR - - Works exactly in the same way as `color` but can be set as the end color to apply a color shift gradient to the image. + - Works in the exact same way as `itemColor` but can be set as the end color to apply a color shift gradient. + - Default is the same value as `color` * `gradientType` - type: STRING - The direction to apply the color shift gradient if both `color` and `colorEnd` have been defined. - Valid values are `horizontal` or `vertical` From 0b5419316d5ab40b5e22dfe83d088a101408eaef Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sun, 11 Dec 2022 11:22:08 +0100 Subject: [PATCH 062/101] Changed a number of property names in CarouselComponent and GridComponent. --- es-app/src/views/GamelistBase.cpp | 22 +- es-app/src/views/GamelistView.cpp | 60 +++-- es-app/src/views/SystemView.cpp | 28 +- es-core/src/ThemeData.cpp | 76 ++++-- es-core/src/ThemeData.h | 3 +- .../components/primary/CarouselComponent.h | 179 +++++++------ .../src/components/primary/GridComponent.h | 246 +++++++++--------- 7 files changed, 342 insertions(+), 272 deletions(-) diff --git a/es-app/src/views/GamelistBase.cpp b/es-app/src/views/GamelistBase.cpp index 1af535474..512cf994f 100644 --- a/es-app/src/views/GamelistBase.cpp +++ b/es-app/src/views/GamelistBase.cpp @@ -577,17 +577,17 @@ void GamelistBase::populateList(const std::vector& files, FileData* f auto theme = mRoot->getSystem()->getTheme(); std::string name; - std::string defaultItem; + std::string defaultImage; if (mCarousel != nullptr) { - defaultItem = mCarousel->getDefaultItem(); - if (!ResourceManager::getInstance().fileExists(defaultItem)) - defaultItem = ""; + defaultImage = mCarousel->getDefaultImage(); + if (!ResourceManager::getInstance().fileExists(defaultImage)) + defaultImage = ""; } else if (mGrid != nullptr) { - defaultItem = mGrid->getDefaultItem(); - if (!ResourceManager::getInstance().fileExists(defaultItem)) - defaultItem = ""; + defaultImage = mGrid->getDefaultImage(); + if (!ResourceManager::getInstance().fileExists(defaultImage)) + defaultImage = ""; } if (files.size() > 0) { @@ -617,8 +617,8 @@ void GamelistBase::populateList(const std::vector& files, FileData* f else if (letterCase == LetterCase::CAPITALIZED) carouselEntry.name = Utils::String::toCapitalized(carouselEntry.name); - if (defaultItem != "") - carouselEntry.data.defaultItemPath = defaultItem; + if (defaultImage != "") + carouselEntry.data.defaultImagePath = defaultImage; mCarousel->addEntry(carouselEntry, theme); } @@ -634,8 +634,8 @@ void GamelistBase::populateList(const std::vector& files, FileData* f else if (letterCase == LetterCase::CAPITALIZED) gridEntry.name = Utils::String::toCapitalized(gridEntry.name); - if (defaultItem != "") - gridEntry.data.defaultItemPath = defaultItem; + if (defaultImage != "") + gridEntry.data.defaultImagePath = defaultImage; mGrid->addEntry(gridEntry, theme); } diff --git a/es-app/src/views/GamelistView.cpp b/es-app/src/views/GamelistView.cpp index 6f4dcb937..3d58985bf 100644 --- a/es-app/src/views/GamelistView.cpp +++ b/es-app/src/views/GamelistView.cpp @@ -155,14 +155,33 @@ void GamelistView::onThemeChanged(const std::shared_ptr& theme) if (element.second.type == "carousel") { if (mCarousel == nullptr) { mCarousel = std::make_unique>(); - if (element.second.has("itemType")) { + if (element.second.has("imageType")) { + const std::string imageType {element.second.get("imageType")}; + if (imageType == "marquee" || imageType == "cover" || + imageType == "backcover" || imageType == "3dbox" || + imageType == "physicalmedia" || imageType == "screenshot" || + imageType == "titlescreen" || imageType == "miximage" || + imageType == "fanart" || imageType == "none") { + mCarousel->setImageType(imageType); + } + else { + LOG(LogWarning) << "GamelistView::onThemeChanged(): Invalid theme " + "configuration, carousel property \"imageType\" " + "for element \"" + << element.first.substr(9) << "\" defined as \"" + << imageType << "\""; + mCarousel->setImageType("marquee"); + } + } + else if (element.second.has("itemType")) { + // TEMPORARY: Backward compatiblity due to property name changes. const std::string itemType {element.second.get("itemType")}; if (itemType == "marquee" || itemType == "cover" || itemType == "backcover" || itemType == "3dbox" || itemType == "physicalmedia" || itemType == "screenshot" || itemType == "titlescreen" || itemType == "miximage" || itemType == "fanart" || itemType == "none") { - mCarousel->setItemType(itemType); + mCarousel->setImageType(itemType); } else { LOG(LogWarning) << "GamelistView::onThemeChanged(): Invalid theme " @@ -170,14 +189,17 @@ void GamelistView::onThemeChanged(const std::shared_ptr& theme) "for element \"" << element.first.substr(9) << "\" defined as \"" << itemType << "\""; - mCarousel->setItemType("marquee"); + mCarousel->setImageType("marquee"); } } else { - mCarousel->setItemType("marquee"); + mCarousel->setImageType("marquee"); } + // TEMPORARY: Backward compatiblity due to property name changes. if (element.second.has("defaultItem")) - mCarousel->setDefaultItem(element.second.get("defaultItem")); + mCarousel->setDefaultImage(element.second.get("defaultItem")); + if (element.second.has("defaultImage")) + mCarousel->setDefaultImage(element.second.get("defaultImage")); mPrimary = mCarousel.get(); } mPrimary->setCursorChangedCallback( @@ -189,29 +211,29 @@ void GamelistView::onThemeChanged(const std::shared_ptr& theme) if (element.second.type == "grid") { if (mGrid == nullptr) { mGrid = std::make_unique>(); - if (element.second.has("itemType")) { - const std::string itemType {element.second.get("itemType")}; - if (itemType == "marquee" || itemType == "cover" || - itemType == "backcover" || itemType == "3dbox" || - itemType == "physicalmedia" || itemType == "screenshot" || - itemType == "titlescreen" || itemType == "miximage" || - itemType == "fanart" || itemType == "none") { - mGrid->setItemType(itemType); + if (element.second.has("imageType")) { + const std::string imageType {element.second.get("imageType")}; + if (imageType == "marquee" || imageType == "cover" || + imageType == "backcover" || imageType == "3dbox" || + imageType == "physicalmedia" || imageType == "screenshot" || + imageType == "titlescreen" || imageType == "miximage" || + imageType == "fanart" || imageType == "none") { + mGrid->setImageType(imageType); } else { LOG(LogWarning) << "GamelistView::onThemeChanged(): Invalid theme " - "configuration, grid property \"itemType\" " + "configuration, grid property \"imageType\" " "for element \"" << element.first.substr(5) << "\" defined as \"" - << itemType << "\""; - mGrid->setItemType("marquee"); + << imageType << "\""; + mGrid->setImageType("marquee"); } } else { - mGrid->setItemType("marquee"); + mGrid->setImageType("marquee"); } - if (element.second.has("defaultItem")) - mGrid->setDefaultItem(element.second.get("defaultItem")); + if (element.second.has("defaultImage")) + mGrid->setDefaultImage(element.second.get("defaultImage")); mPrimary = mGrid.get(); } mPrimary->setCursorChangedCallback( diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 3bf873d71..05890cf0f 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -466,8 +466,8 @@ void SystemView::populate() for (auto it : SystemData::sSystemVector) { const std::shared_ptr& theme {it->getTheme()}; - std::string itemPath; - std::string defaultItemPath; + std::string imagePath; + std::string defaultImagePath; std::string itemText; if (mLegacyMode && mViewNeedsReload) { @@ -575,10 +575,18 @@ void SystemView::populate() } }); if (mCarousel != nullptr || mGrid != nullptr) { - if (element.second.has("staticItem")) - itemPath = element.second.get("staticItem"); - if (element.second.has("defaultItem")) - defaultItemPath = element.second.get("defaultItem"); + if (mCarousel != nullptr) { + // TEMPORARY: Backward compatiblity due to property name changes. + if (element.second.has("staticItem")) + imagePath = element.second.get("staticItem"); + if (element.second.has("defaultItem")) + defaultImagePath = + element.second.get("defaultItem"); + } + if (element.second.has("staticImage")) + imagePath = element.second.get("staticImage"); + if (element.second.has("defaultImage")) + defaultImagePath = element.second.get("defaultImage"); if (element.second.has("text")) itemText = element.second.get("text"); } @@ -755,8 +763,8 @@ void SystemView::populate() } letterCaseFunc(entry.name); entry.object = it; - entry.data.itemPath = itemPath; - entry.data.defaultItemPath = defaultItemPath; + entry.data.imagePath = imagePath; + entry.data.defaultImagePath = defaultImagePath; mCarousel->addEntry(entry, theme); } else if (mGrid != nullptr) { @@ -767,8 +775,8 @@ void SystemView::populate() entry.name = itemText; letterCaseFunc(entry.name); entry.object = it; - entry.data.itemPath = itemPath; - entry.data.defaultItemPath = defaultItemPath; + entry.data.imagePath = imagePath; + entry.data.defaultImagePath = defaultImagePath; mGrid->addEntry(entry, theme); } else if (mTextList != nullptr) { diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 4dc67761a..91096e6a4 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -43,7 +43,7 @@ std::vector ThemeData::sLegacySupportedFeatures { {"z-index"}, {"visible"}}; -std::vector ThemeData::sLegacyElements { +std::vector ThemeData::sLegacyProperties { {"showSnapshotNoVideo"}, {"showSnapshotDelay"}, {"forceUppercase"}, @@ -56,6 +56,12 @@ std::vector ThemeData::sLegacyElements { {"logoAlignment"}, {"maxLogoCount"}}; +std::vector ThemeData::sDeprecatedProperties { + {"staticItem"}, + {"itemType"}, + {"defaultItem"}, + {"itemInterpolation"}}; + std::vector> ThemeData::sSupportedAspectRatios { {"automatic", "automatic"}, {"16:9", "16:9"}, @@ -106,22 +112,26 @@ std::map> {"size", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"type", STRING}, - {"staticItem", PATH}, - {"itemType", STRING}, - {"defaultItem", PATH}, + {"staticImage", PATH}, + {"imageType", STRING}, + {"defaultImage", PATH}, + {"staticItem", PATH}, // TEMPORARY: For backward compatibility. + {"itemType", STRING}, // TEMPORARY: For backward compatibility. + {"defaultItem", PATH}, // TEMPORARY: For backward compatibility. {"maxItemCount", FLOAT}, {"maxLogoCount", FLOAT}, // For backward compatibility with legacy themes. {"itemsBeforeCenter", UNSIGNED_INTEGER}, {"itemsAfterCenter", UNSIGNED_INTEGER}, {"itemSize", NORMALIZED_PAIR}, {"itemScale", FLOAT}, - {"itemInterpolation", STRING}, {"itemRotation", FLOAT}, {"itemRotationOrigin", NORMALIZED_PAIR}, {"itemAxisHorizontal", BOOLEAN}, - {"itemColor", COLOR}, - {"itemColorEnd", COLOR}, - {"itemGradientType", STRING}, + {"itemInterpolation", STRING}, // TEMPORARY: For backward compatibility. + {"imageInterpolation", STRING}, + {"imageColor", COLOR}, + {"imageColorEnd", COLOR}, + {"imageGradientType", STRING}, {"itemTransitions", STRING}, {"itemHorizontalAlignment", STRING}, {"itemVerticalAlignment", STRING}, @@ -157,9 +167,22 @@ std::map> {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, - {"staticItem", PATH}, - {"itemType", STRING}, - {"defaultItem", PATH}, + {"staticImage", PATH}, + {"imageType", STRING}, + {"defaultImage", PATH}, + {"itemSize", NORMALIZED_PAIR}, + {"itemScale", FLOAT}, + {"itemSpacing", NORMALIZED_PAIR}, + {"fractionalRows", BOOLEAN}, + {"itemTransitions", STRING}, + {"rowTransitions", STRING}, + {"unfocusedItemOpacity", FLOAT}, + {"edgeScaleInwards", BOOLEAN}, // TODO + {"imageRelativeScale", FLOAT}, + {"imageFit", STRING}, + {"imageColor", COLOR}, + {"imageColorEnd", COLOR}, + {"imageGradientType", STRING}, {"backgroundImage", PATH}, {"backgroundRelativeScale", FLOAT}, {"backgroundColor", COLOR}, @@ -171,19 +194,6 @@ std::map> {"selectorColorEnd", COLOR}, {"selectorGradientType", STRING}, {"selectorLayer", STRING}, - {"fractionalRows", BOOLEAN}, - {"itemSize", NORMALIZED_PAIR}, - {"itemScale", FLOAT}, - {"itemRelativeScale", FLOAT}, - {"itemFit", STRING}, - {"itemSpacing", NORMALIZED_PAIR}, - {"itemColor", COLOR}, - {"itemColorEnd", COLOR}, - {"itemGradientType", STRING}, - {"itemTransitions", STRING}, - {"rowTransitions", STRING}, - {"unfocusedItemOpacity", FLOAT}, - {"edgeScaleInwards", BOOLEAN}, {"text", STRING}, {"textRelativeScale", FLOAT}, {"textColor", COLOR}, @@ -1461,17 +1471,29 @@ void ThemeData::parseElement(const pugi::xml_node& root, std::string nodeName = node.name(); - // Strictly enforce removal of legacy elements for non-legacy theme sets by creating + // Strictly enforce removal of legacy properties for non-legacy theme sets by creating // an unthemed system if they're present in the configuration. if (!mLegacyTheme) { - for (auto& legacyElement : sLegacyElements) { - if (nodeName == legacyElement) { + for (auto& legacyProperty : sLegacyProperties) { + if (nodeName == legacyProperty) { throw error << ": Legacy <" << nodeName << "> property found for non-legacy theme set"; } } } + // Print a warning if a deprecated property is used for a non-legacy theme set. + if (!mLegacyTheme) { + for (auto& deprecatedProperty : sDeprecatedProperties) { + if (nodeName == deprecatedProperty) { + LOG(LogWarning) + << "ThemeData::parseElement(): Property \"" << deprecatedProperty + << "\" is deprecated and support for it will be removed in a future " + "version"; + } + } + } + // If an attribute exists, then replace nodeName with its name. auto attributeEntry = sPropertyAttributeMap.find(element.type); if (attributeEntry != sPropertyAttributeMap.end()) { diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 368008289..b912593f9 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -281,7 +281,8 @@ private: static std::vector sSupportedViews; static std::vector sLegacySupportedViews; static std::vector sLegacySupportedFeatures; - static std::vector sLegacyElements; + static std::vector sLegacyProperties; + static std::vector sDeprecatedProperties; static std::vector> sSupportedAspectRatios; static std::map sAspectRatioMap; diff --git a/es-core/src/components/primary/CarouselComponent.h b/es-core/src/components/primary/CarouselComponent.h index 28f8a8810..f2e16c035 100644 --- a/es-core/src/components/primary/CarouselComponent.h +++ b/es-core/src/components/primary/CarouselComponent.h @@ -17,8 +17,8 @@ struct CarouselEntry { std::shared_ptr item; - std::string itemPath; - std::string defaultItemPath; + std::string imagePath; + std::string defaultImagePath; }; template @@ -53,10 +53,10 @@ public: Entry& getEntry(int index) { return mEntries.at(index); } void onDemandTextureLoad() override; const CarouselType getType() { return mType; } - const std::string& getItemType() { return mItemType; } - void setItemType(std::string itemType) { mItemType = itemType; } - const std::string& getDefaultItem() { return mDefaultItem; } - void setDefaultItem(std::string defaultItem) { mDefaultItem = defaultItem; } + const std::string& getImageType() { return mImageType; } + void setImageType(std::string imageType) { mImageType = imageType; } + const std::string& getDefaultImage() { return mDefaultImage; } + void setDefaultImage(std::string defaultImage) { mDefaultImage = defaultImage; } bool isScrolling() const override { return List::isScrolling(); } const LetterCase getLetterCase() const override { return mLetterCase; } const LetterCase getLetterCaseCollections() const override { return mLetterCaseCollections; } @@ -124,20 +124,20 @@ private: bool mLegacyMode; CarouselType mType; - std::string mItemType; - std::string mDefaultItem; + std::string mImageType; + std::string mDefaultImage; float mMaxItemCount; int mItemsBeforeCenter; int mItemsAfterCenter; glm::vec2 mItemSize; float mItemScale; - bool mLinearInterpolation; float mItemRotation; glm::vec2 mItemRotationOrigin; bool mItemAxisHorizontal; - unsigned int mItemColorShift; - unsigned int mItemColorShiftEnd; - bool mItemColorGradientHorizontal; + bool mLinearInterpolation; + unsigned int mImageColorShift; + unsigned int mImageColorShiftEnd; + bool mImageColorGradientHorizontal; bool mInstantItemTransitions; Alignment mItemHorizontalAlignment; Alignment mItemVerticalAlignment; @@ -182,13 +182,13 @@ CarouselComponent::CarouselComponent() , mItemSize {glm::vec2 {Renderer::getScreenWidth() * 0.25f, Renderer::getScreenHeight() * 0.155f}} , mItemScale {1.2f} - , mLinearInterpolation {false} , mItemRotation {7.5f} , mItemRotationOrigin {-3.0f, 0.5f} , mItemAxisHorizontal {false} - , mItemColorShift {0xFFFFFFFF} - , mItemColorShiftEnd {0xFFFFFFFF} - , mItemColorGradientHorizontal {true} + , mLinearInterpolation {false} + , mImageColorShift {0xFFFFFFFF} + , mImageColorShiftEnd {0xFFFFFFFF} + , mImageColorGradientHorizontal {true} , mInstantItemTransitions {false} , mItemHorizontalAlignment {ALIGN_CENTER} , mItemVerticalAlignment {ALIGN_CENTER} @@ -245,46 +245,45 @@ void CarouselComponent::addEntry(Entry& entry, const std::shared_ptr(false, dynamic); item->setLinearInterpolation(mLinearInterpolation); item->setMipmapping(true); item->setMaxSize(glm::round(mItemSize * (mItemScale >= 1.0f ? mItemScale : 1.0f))); - item->setImage(entry.data.itemPath); + item->setImage(entry.data.imagePath); item->applyTheme(theme, "system", "", ThemeFlags::ALL); - if (mItemColorShift != 0xFFFFFFFF) - item->setColorShift(mItemColorShift); - if (mItemColorShiftEnd != mItemColorShift) - item->setColorShiftEnd(mItemColorShiftEnd); - if (!mItemColorGradientHorizontal) + if (mImageColorShift != 0xFFFFFFFF) + item->setColorShift(mImageColorShift); + if (mImageColorShiftEnd != mImageColorShift) + item->setColorShiftEnd(mImageColorShiftEnd); + if (!mImageColorGradientHorizontal) item->setColorGradientHorizontal(false); item->setRotateByTargetSize(true); entry.data.item = item; } - else if (entry.data.defaultItemPath != "" && - ResourceManager::getInstance().fileExists(entry.data.defaultItemPath)) { - auto defaultItem = std::make_shared(false, dynamic); - defaultItem->setLinearInterpolation(mLinearInterpolation); - defaultItem->setMipmapping(true); - defaultItem->setMaxSize( + else if (entry.data.defaultImagePath != "" && + ResourceManager::getInstance().fileExists(entry.data.defaultImagePath)) { + auto defaultImage = std::make_shared(false, dynamic); + defaultImage->setLinearInterpolation(mLinearInterpolation); + defaultImage->setMipmapping(true); + defaultImage->setMaxSize( glm::round(mItemSize * (mItemScale >= 1.0f ? mItemScale : 1.0f))); - defaultItem->setImage(entry.data.defaultItemPath); - defaultItem->applyTheme(theme, "system", "", ThemeFlags::ALL); - if (mItemColorShift != 0xFFFFFFFF) - defaultItem->setColorShift(mItemColorShift); - if (mItemColorShiftEnd != mItemColorShift) - defaultItem->setColorShiftEnd(mItemColorShiftEnd); - if (!mItemColorGradientHorizontal) - defaultItem->setColorGradientHorizontal(false); - defaultItem->setRotateByTargetSize(true); - entry.data.item = defaultItem; + defaultImage->setImage(entry.data.defaultImagePath); + defaultImage->applyTheme(theme, "system", "", ThemeFlags::ALL); + if (mImageColorShift != 0xFFFFFFFF) + defaultImage->setColorShift(mImageColorShift); + if (mImageColorShiftEnd != mImageColorShift) + defaultImage->setColorShiftEnd(mImageColorShiftEnd); + if (!mImageColorGradientHorizontal) + defaultImage->setColorGradientHorizontal(false); + defaultImage->setRotateByTargetSize(true); + entry.data.item = defaultImage; } } if (!entry.data.item) { // If no item image is present, add item text as fallback. - auto text = std::make_shared( entry.name, mFont, 0x000000FF, mItemHorizontalAlignment, mItemVerticalAlignment, glm::vec3 {0.0f, 0.0f, 0.0f}, @@ -330,18 +329,18 @@ void CarouselComponent::addEntry(Entry& entry, const std::shared_ptr void CarouselComponent::updateEntry(Entry& entry, const std::shared_ptr& theme) { - if (entry.data.itemPath != "") { + if (entry.data.imagePath != "") { auto item = std::make_shared(false, true); item->setLinearInterpolation(mLinearInterpolation); item->setMipmapping(true); item->setMaxSize(glm::round(mItemSize * (mItemScale >= 1.0f ? mItemScale : 1.0f))); - item->setImage(entry.data.itemPath); + item->setImage(entry.data.imagePath); item->applyTheme(theme, "system", "", ThemeFlags::ALL); - if (mItemColorShift != 0xFFFFFFFF) - item->setColorShift(mItemColorShift); - if (mItemColorShiftEnd != mItemColorShift) - item->setColorShiftEnd(mItemColorShiftEnd); - if (!mItemColorGradientHorizontal) + if (mImageColorShift != 0xFFFFFFFF) + item->setColorShift(mImageColorShift); + if (mImageColorShiftEnd != mImageColorShift) + item->setColorShiftEnd(mImageColorShiftEnd); + if (!mImageColorGradientHorizontal) item->setColorGradientHorizontal(false); item->setRotateByTargetSize(true); entry.data.item = item; @@ -432,28 +431,28 @@ template void CarouselComponent::onDemandTextureLoad() auto& entry = mEntries.at(cursor); - if (entry.data.itemPath == "") { + if (entry.data.imagePath == "") { FileData* game {entry.object}; - if (mItemType == "" || mItemType == "marquee") - entry.data.itemPath = game->getMarqueePath(); - else if (mItemType == "cover") - entry.data.itemPath = game->getCoverPath(); - else if (mItemType == "backcover") - entry.data.itemPath = game->getBackCoverPath(); - else if (mItemType == "3dbox") - entry.data.itemPath = game->get3DBoxPath(); - else if (mItemType == "physicalmedia") - entry.data.itemPath = game->getPhysicalMediaPath(); - else if (mItemType == "screenshot") - entry.data.itemPath = game->getScreenshotPath(); - else if (mItemType == "titlescreen") - entry.data.itemPath = game->getTitleScreenPath(); - else if (mItemType == "miximage") - entry.data.itemPath = game->getMiximagePath(); - else if (mItemType == "fanart") - entry.data.itemPath = game->getFanArtPath(); - else if (mItemType == "none") // Display the game name as text. + if (mImageType == "" || mImageType == "marquee") + entry.data.imagePath = game->getMarqueePath(); + else if (mImageType == "cover") + entry.data.imagePath = game->getCoverPath(); + else if (mImageType == "backcover") + entry.data.imagePath = game->getBackCoverPath(); + else if (mImageType == "3dbox") + entry.data.imagePath = game->get3DBoxPath(); + else if (mImageType == "physicalmedia") + entry.data.imagePath = game->getPhysicalMediaPath(); + else if (mImageType == "screenshot") + entry.data.imagePath = game->getScreenshotPath(); + else if (mImageType == "titlescreen") + entry.data.imagePath = game->getTitleScreenPath(); + else if (mImageType == "miximage") + entry.data.imagePath = game->getMiximagePath(); + else if (mImageType == "fanart") + entry.data.imagePath = game->getFanArtPath(); + else if (mImageType == "none") // Display the game name as text. return; auto theme = game->getSystem()->getTheme(); @@ -910,8 +909,8 @@ template void CarouselComponent::render(const glm::mat4& parentT // TODO: Rewrite to use "real" reflections instead of this hack. // Don't attempt to add reflections for text entries. - if (mReflections && (mEntries.at(renderItem.index).data.itemPath != "" || - mEntries.at(renderItem.index).data.defaultItemPath != "")) { + if (mReflections && (mEntries.at(renderItem.index).data.imagePath != "" || + mEntries.at(renderItem.index).data.defaultImagePath != "")) { glm::mat4 reflectionTrans {glm::translate( renderItem.trans, glm::vec3 {0.0f, comp->getSize().y * renderItem.scale, 0.0f})}; float falloff {glm::clamp(mReflectionsFalloff, 0.0f, 1.0f)}; @@ -1032,6 +1031,7 @@ void CarouselComponent::applyTheme(const std::shared_ptr& theme, mItemScale = glm::clamp(elem->get("itemScale"), 0.2f, 3.0f); if (elem->has("itemInterpolation")) { + // TEMPORARY: Backward compatiblity due to property name changes. const std::string& itemInterpolation {elem->get("itemInterpolation")}; if (itemInterpolation == "linear") { mLinearInterpolation = true; @@ -1048,6 +1048,23 @@ void CarouselComponent::applyTheme(const std::shared_ptr& theme, } } + if (elem->has("imageInterpolation")) { + const std::string& imageInterpolation {elem->get("imageInterpolation")}; + if (imageInterpolation == "linear") { + mLinearInterpolation = true; + } + else if (imageInterpolation == "nearest") { + mLinearInterpolation = false; + } + else { + mLinearInterpolation = true; + LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " + "\"imageInterpolation\" for element \"" + << element.substr(9) << "\" defined as \"" << imageInterpolation + << "\""; + } + } + if (elem->has("itemTransitions")) { const std::string& itemTransitions {elem->get("itemTransitions")}; if (itemTransitions == "animate") { @@ -1074,25 +1091,25 @@ void CarouselComponent::applyTheme(const std::shared_ptr& theme, mItemAxisHorizontal = (elem->has("itemAxisHorizontal") && elem->get("itemAxisHorizontal")); - if (elem->has("itemColor")) { - mItemColorShift = elem->get("itemColor"); - mItemColorShiftEnd = mItemColorShift; + if (elem->has("imageColor")) { + mImageColorShift = elem->get("imageColor"); + mImageColorShiftEnd = mImageColorShift; } - if (elem->has("itemColorEnd")) - mItemColorShiftEnd = elem->get("itemColorEnd"); + if (elem->has("imageColorEnd")) + mImageColorShiftEnd = elem->get("imageColorEnd"); - if (elem->has("itemGradientType")) { - const std::string& gradientType {elem->get("itemGradientType")}; + if (elem->has("imageGradientType")) { + const std::string& gradientType {elem->get("imageGradientType")}; if (gradientType == "horizontal") { - mItemColorGradientHorizontal = true; + mImageColorGradientHorizontal = true; } else if (gradientType == "vertical") { - mItemColorGradientHorizontal = false; + mImageColorGradientHorizontal = false; } else { - mItemColorGradientHorizontal = true; + mImageColorGradientHorizontal = true; LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " - "\"gradientType\" for element \"" + "\"imageGradientType\" for element \"" << element.substr(9) << "\" defined as \"" << gradientType << "\""; } } diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index 9c4fcac40..7d4c82342 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -14,8 +14,8 @@ struct GridEntry { std::shared_ptr item; - std::string itemPath; - std::string defaultItemPath; + std::string imagePath; + std::string defaultImagePath; }; template @@ -62,10 +62,10 @@ public: { return mLetterCaseGroupedCollections; } - const std::string& getItemType() { return mItemType; } - void setItemType(std::string itemType) { mItemType = itemType; } - const std::string& getDefaultItem() { return mDefaultItem; } - void setDefaultItem(std::string defaultItem) { mDefaultItem = defaultItem; } + const std::string& getImageType() { return mImageType; } + void setImageType(std::string imageType) { mImageType = imageType; } + const std::string& getDefaultImage() { return mDefaultImage; } + void setDefaultImage(std::string defaultItem) { mDefaultImage = defaultItem; } bool input(InputConfig* config, Input input) override; void update(int deltaTime) override; void render(const glm::mat4& parentTrans) override; @@ -110,7 +110,7 @@ private: BOTTOM }; - enum class ItemFit { + enum class ImageFit { CONTAIN, FILL, COVER @@ -130,9 +130,23 @@ private: bool mWasScrolling; bool mJustCalculatedLayout; bool mSuppressTransitions; + float mHorizontalMargin; + float mVerticalMargin; - std::string mItemType; - std::string mDefaultItem; + std::string mImageType; + std::string mDefaultImage; + glm::vec2 mItemSize; + float mItemScale; + glm::vec2 mItemSpacing; + bool mFractionalRows; + bool mInstantItemTransitions; + bool mInstantRowTransitions; + float mUnfocusedItemOpacity; + float mImageRelativeScale; + ImageFit mImagefit; + unsigned int mImageColor; + unsigned int mImageColorEnd; + bool mImageColorGradientHorizontal; std::unique_ptr mBackgroundImage; float mBackgroundRelativeScale; unsigned int mBackgroundColor; @@ -146,18 +160,6 @@ private: bool mSelectorColorGradientHorizontal; bool mHasSelectorColor; SelectorLayer mSelectorLayer; - bool mFractionalRows; - glm::vec2 mItemSize; - float mItemScale; - float mItemRelativeScale; - ItemFit mItemFit; - glm::vec2 mItemSpacing; - unsigned int mItemColor; - unsigned int mItemColorEnd; - bool mItemColorGradientHorizontal; - bool mInstantItemTransitions; - bool mInstantRowTransitions; - float mUnfocusedItemOpacity; float mTextRelativeScale; unsigned int mTextColor; unsigned int mTextBackgroundColor; @@ -167,8 +169,6 @@ private: LetterCase mLetterCaseGroupedCollections; float mLineSpacing; bool mFadeAbovePrimary; - float mHorizontalMargin; - float mVerticalMargin; }; template @@ -186,6 +186,21 @@ GridComponent::GridComponent() , mWasScrolling {false} , mJustCalculatedLayout {false} , mSuppressTransitions {false} + , mHorizontalMargin {0.0f} + , mVerticalMargin {0.0f} + , mItemSize {glm::vec2 {mRenderer->getScreenWidth() * 0.15f, + mRenderer->getScreenHeight() * 0.25f}} + , mItemScale {1.05f} + , mItemSpacing {0.0f, 0.0f} + , mFractionalRows {false} + , mInstantItemTransitions {false} + , mInstantRowTransitions {false} + , mUnfocusedItemOpacity {1.0f} + , mImageRelativeScale {1.0f} + , mImagefit {ImageFit::CONTAIN} + , mImageColor {0xFFFFFFFF} + , mImageColorEnd {0xFFFFFFFF} + , mImageColorGradientHorizontal {true} , mBackgroundRelativeScale {1.0f} , mBackgroundColor {0xFFFFFFFF} , mBackgroundColorEnd {0xFFFFFFFF} @@ -197,19 +212,6 @@ GridComponent::GridComponent() , mSelectorColorGradientHorizontal {true} , mHasSelectorColor {false} , mSelectorLayer {SelectorLayer::TOP} - , mFractionalRows {false} - , mItemSize {glm::vec2 {mRenderer->getScreenWidth() * 0.15f, - mRenderer->getScreenHeight() * 0.25f}} - , mItemScale {1.05f} - , mItemRelativeScale {1.0f} - , mItemFit {ItemFit::CONTAIN} - , mItemSpacing {0.0f, 0.0f} - , mItemColor {0xFFFFFFFF} - , mItemColorEnd {0xFFFFFFFF} - , mItemColorGradientHorizontal {true} - , mInstantItemTransitions {false} - , mInstantRowTransitions {false} - , mUnfocusedItemOpacity {1.0f} , mTextRelativeScale {1.0f} , mTextColor {0x000000FF} , mTextBackgroundColor {0xFFFFFF00} @@ -218,8 +220,6 @@ GridComponent::GridComponent() , mLetterCaseGroupedCollections {LetterCase::NONE} , mLineSpacing {1.5f} , mFadeAbovePrimary {false} - , mHorizontalMargin {0.0f} - , mVerticalMargin {0.0f} { } @@ -228,48 +228,48 @@ void GridComponent::addEntry(Entry& entry, const std::shared_ptr& { const bool dynamic {mGamelistView}; - if (entry.data.itemPath != "" && - ResourceManager::getInstance().fileExists(entry.data.itemPath)) { + if (entry.data.imagePath != "" && + ResourceManager::getInstance().fileExists(entry.data.imagePath)) { auto item = std::make_shared(false, dynamic); item->setLinearInterpolation(true); item->setMipmapping(true); - if (mItemFit == ItemFit::CONTAIN) - item->setMaxSize(mItemSize * mItemRelativeScale); - else if (mItemFit == ItemFit::FILL) - item->setResize(mItemSize * mItemRelativeScale); - else if (mItemFit == ItemFit::COVER) - item->setCroppedSize(mItemSize * mItemRelativeScale); - item->setImage(entry.data.itemPath); + if (mImagefit == ImageFit::CONTAIN) + item->setMaxSize(mItemSize * mImageRelativeScale); + else if (mImagefit == ImageFit::FILL) + item->setResize(mItemSize * mImageRelativeScale); + else if (mImagefit == ImageFit::COVER) + item->setCroppedSize(mItemSize * mImageRelativeScale); + item->setImage(entry.data.imagePath); item->applyTheme(theme, "system", "", ThemeFlags::ALL); - if (mItemColor != 0xFFFFFFFF) - item->setColorShift(mItemColor); - if (mItemColorEnd != mItemColor) { - item->setColorShiftEnd(mItemColorEnd); - if (!mItemColorGradientHorizontal) + if (mImageColor != 0xFFFFFFFF) + item->setColorShift(mImageColor); + if (mImageColorEnd != mImageColor) { + item->setColorShiftEnd(mImageColorEnd); + if (!mImageColorGradientHorizontal) item->setColorGradientHorizontal(false); } item->setOrigin(0.5f, 0.5f); item->setRotateByTargetSize(true); entry.data.item = item; } - else if (entry.data.defaultItemPath != "" && - ResourceManager::getInstance().fileExists(entry.data.defaultItemPath)) { + else if (entry.data.defaultImagePath != "" && + ResourceManager::getInstance().fileExists(entry.data.defaultImagePath)) { auto defaultItem = std::make_shared(false, dynamic); defaultItem->setLinearInterpolation(true); defaultItem->setMipmapping(true); - if (mItemFit == ItemFit::CONTAIN) - defaultItem->setMaxSize(mItemSize * mItemRelativeScale); - else if (mItemFit == ItemFit::FILL) - defaultItem->setResize(mItemSize * mItemRelativeScale); - else if (mItemFit == ItemFit::COVER) - defaultItem->setCroppedSize(mItemSize * mItemRelativeScale); - defaultItem->setImage(entry.data.defaultItemPath); + if (mImagefit == ImageFit::CONTAIN) + defaultItem->setMaxSize(mItemSize * mImageRelativeScale); + else if (mImagefit == ImageFit::FILL) + defaultItem->setResize(mItemSize * mImageRelativeScale); + else if (mImagefit == ImageFit::COVER) + defaultItem->setCroppedSize(mItemSize * mImageRelativeScale); + defaultItem->setImage(entry.data.defaultImagePath); defaultItem->applyTheme(theme, "system", "", ThemeFlags::ALL); - if (mItemColor != 0xFFFFFFFF) - defaultItem->setColorShift(mItemColor); - if (mItemColorEnd != mItemColor) { - defaultItem->setColorShiftEnd(mItemColorEnd); - if (!mItemColorGradientHorizontal) + if (mImageColor != 0xFFFFFFFF) + defaultItem->setColorShift(mImageColor); + if (mImageColorEnd != mImageColor) { + defaultItem->setColorShiftEnd(mImageColorEnd); + if (!mImageColorGradientHorizontal) defaultItem->setColorGradientHorizontal(false); } defaultItem->setOrigin(0.5f, 0.5f); @@ -299,24 +299,24 @@ void GridComponent::addEntry(Entry& entry, const std::shared_ptr& template void GridComponent::updateEntry(Entry& entry, const std::shared_ptr& theme) { - if (entry.data.itemPath != "") { + if (entry.data.imagePath != "") { const glm::vec3& calculatedItemPos {entry.data.item->getPosition()}; auto item = std::make_shared(false, true); item->setLinearInterpolation(true); item->setMipmapping(true); - if (mItemFit == ItemFit::CONTAIN) - item->setMaxSize(mItemSize * mItemRelativeScale); - else if (mItemFit == ItemFit::FILL) - item->setResize(mItemSize * mItemRelativeScale); - else if (mItemFit == ItemFit::COVER) - item->setCroppedSize(mItemSize * mItemRelativeScale); - item->setImage(entry.data.itemPath); + if (mImagefit == ImageFit::CONTAIN) + item->setMaxSize(mItemSize * mImageRelativeScale); + else if (mImagefit == ImageFit::FILL) + item->setResize(mItemSize * mImageRelativeScale); + else if (mImagefit == ImageFit::COVER) + item->setCroppedSize(mItemSize * mImageRelativeScale); + item->setImage(entry.data.imagePath); item->applyTheme(theme, "system", "", ThemeFlags::ALL); - if (mItemColor != 0xFFFFFFFF) - item->setColorShift(mItemColor); - if (mItemColorEnd != mItemColor) { - item->setColorShiftEnd(mItemColorEnd); - if (!mItemColorGradientHorizontal) + if (mImageColor != 0xFFFFFFFF) + item->setColorShift(mImageColor); + if (mImageColorEnd != mImageColor) { + item->setColorShiftEnd(mImageColorEnd); + if (!mImageColorGradientHorizontal) item->setColorGradientHorizontal(false); } item->setOrigin(0.5f, 0.5f); @@ -368,28 +368,28 @@ template void GridComponent::onDemandTextureLoad() auto& entry = mEntries.at(cursor); - if (entry.data.itemPath == "") { + if (entry.data.imagePath == "") { FileData* game {entry.object}; - if (mItemType == "" || mItemType == "marquee") - entry.data.itemPath = game->getMarqueePath(); - else if (mItemType == "cover") - entry.data.itemPath = game->getCoverPath(); - else if (mItemType == "backcover") - entry.data.itemPath = game->getBackCoverPath(); - else if (mItemType == "3dbox") - entry.data.itemPath = game->get3DBoxPath(); - else if (mItemType == "physicalmedia") - entry.data.itemPath = game->getPhysicalMediaPath(); - else if (mItemType == "screenshot") - entry.data.itemPath = game->getScreenshotPath(); - else if (mItemType == "titlescreen") - entry.data.itemPath = game->getTitleScreenPath(); - else if (mItemType == "miximage") - entry.data.itemPath = game->getMiximagePath(); - else if (mItemType == "fanart") - entry.data.itemPath = game->getFanArtPath(); - else if (mItemType == "none") // Display the game name as text. + if (mImageType == "" || mImageType == "marquee") + entry.data.imagePath = game->getMarqueePath(); + else if (mImageType == "cover") + entry.data.imagePath = game->getCoverPath(); + else if (mImageType == "backcover") + entry.data.imagePath = game->getBackCoverPath(); + else if (mImageType == "3dbox") + entry.data.imagePath = game->get3DBoxPath(); + else if (mImageType == "physicalmedia") + entry.data.imagePath = game->getPhysicalMediaPath(); + else if (mImageType == "screenshot") + entry.data.imagePath = game->getScreenshotPath(); + else if (mImageType == "titlescreen") + entry.data.imagePath = game->getTitleScreenPath(); + else if (mImageType == "miximage") + entry.data.imagePath = game->getMiximagePath(); + else if (mImageType == "fanart") + entry.data.imagePath = game->getFanArtPath(); + else if (mImageType == "none") // Display the game name as text. return; auto theme = game->getSystem()->getTheme(); @@ -765,25 +765,25 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, if (elem->has("itemScale")) mItemScale = glm::clamp(elem->get("itemScale"), 0.5f, 2.0f); - if (elem->has("itemRelativeScale")) - mItemRelativeScale = glm::clamp(elem->get("itemRelativeScale"), 0.2f, 1.0f); + if (elem->has("imageRelativeScale")) + mImageRelativeScale = glm::clamp(elem->get("imageRelativeScale"), 0.2f, 1.0f); - if (elem->has("itemFit")) { - const std::string& itemFit {elem->get("itemFit")}; - if (itemFit == "contain") { - mItemFit = ItemFit::CONTAIN; + if (elem->has("imageFit")) { + const std::string& imageFit {elem->get("imageFit")}; + if (imageFit == "contain") { + mImagefit = ImageFit::CONTAIN; } - else if (itemFit == "fill") { - mItemFit = ItemFit::FILL; + else if (imageFit == "fill") { + mImagefit = ImageFit::FILL; } - else if (itemFit == "cover") { - mItemFit = ItemFit::COVER; + else if (imageFit == "cover") { + mImagefit = ImageFit::COVER; } else { - mItemFit = ItemFit::CONTAIN; + mImagefit = ImageFit::CONTAIN; LOG(LogWarning) << "GridComponent: Invalid theme configuration, property " - "\"itemFit\" for element \"" - << element.substr(5) << "\" defined as \"" << itemFit << "\""; + "\"imageFit\" for element \"" + << element.substr(5) << "\" defined as \"" << imageFit << "\""; } } @@ -974,25 +974,25 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, mItemSpacing.y = ((mItemSize.y * mItemScale) - mItemSize.y) / 2.0f; } - if (elem->has("itemColor")) { - mItemColor = elem->get("itemColor"); - mItemColorEnd = mItemColor; + if (elem->has("imageColor")) { + mImageColor = elem->get("imageColor"); + mImageColorEnd = mImageColor; } - if (elem->has("itemColorEnd")) - mItemColorEnd = elem->get("itemColorEnd"); + if (elem->has("imageColorEnd")) + mImageColorEnd = elem->get("imageColorEnd"); - if (elem->has("itemGradientType")) { - const std::string& gradientType {elem->get("itemGradientType")}; + if (elem->has("imageGradientType")) { + const std::string& gradientType {elem->get("imageGradientType")}; if (gradientType == "horizontal") { - mItemColorGradientHorizontal = true; + mImageColorGradientHorizontal = true; } else if (gradientType == "vertical") { - mItemColorGradientHorizontal = false; + mImageColorGradientHorizontal = false; } else { - mItemColorGradientHorizontal = true; + mImageColorGradientHorizontal = true; LOG(LogWarning) << "GridComponent: Invalid theme configuration, property " - "\"itemGradientType\" for element \"" + "\"imageGradientType\" for element \"" << element.substr(5) << "\" defined as \"" << gradientType << "\""; } } From fca865ca9f910f62988902fdb4bd311fa98171b3 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sun, 11 Dec 2022 11:24:39 +0100 Subject: [PATCH 063/101] (slate-DE) Updated for the latest theme engine changes. --- themes/slate-DE/theme.xml | 4 ++-- themes/slate-DE/theme_engine_test_1.xml | 2 +- themes/slate-DE/theme_engine_test_2.xml | 4 ++-- themes/slate-DE/theme_engine_test_3.xml | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/themes/slate-DE/theme.xml b/themes/slate-DE/theme.xml index bf94eb049..462834e05 100644 --- a/themes/slate-DE/theme.xml +++ b/themes/slate-DE/theme.xml @@ -27,11 +27,11 @@ 0 0.383796 1 0.232407 horizontal - ./${system.theme}/images/logo.svg + ./${system.theme}/images/logo.svg 3 0.25 0.125 1.23 - linear + linear 0.5 ${systemCarouselColor} ${system.fullName} diff --git a/themes/slate-DE/theme_engine_test_1.xml b/themes/slate-DE/theme_engine_test_1.xml index 841b00745..2ca6ac26d 100644 --- a/themes/slate-DE/theme_engine_test_1.xml +++ b/themes/slate-DE/theme_engine_test_1.xml @@ -6,7 +6,7 @@ 0 0 0.28 1.0 vertical_wheel - ./${system.theme}/images/logo.svg + ./${system.theme}/images/logo.svg 4 4 0.15 0.125 diff --git a/themes/slate-DE/theme_engine_test_2.xml b/themes/slate-DE/theme_engine_test_2.xml index ed35aa110..a56f43ac7 100644 --- a/themes/slate-DE/theme_engine_test_2.xml +++ b/themes/slate-DE/theme_engine_test_2.xml @@ -110,11 +110,11 @@ 0.025 0.201 0.39 0.711 vertical - marquee + marquee 5 0.26 0.105 1.23 - linear + linear center 0 0 diff --git a/themes/slate-DE/theme_engine_test_3.xml b/themes/slate-DE/theme_engine_test_3.xml index 8bd5c1b9f..fc7722457 100644 --- a/themes/slate-DE/theme_engine_test_3.xml +++ b/themes/slate-DE/theme_engine_test_3.xml @@ -110,11 +110,11 @@ 0.025 0.231 0.95 0.692 horizontal - cover + cover 5 0.105 0.235 1.53 - linear + linear center 0.1 true From 02b71248744fa20b83d04ee889c5c87300cd15e0 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sun, 11 Dec 2022 11:25:34 +0100 Subject: [PATCH 064/101] (modern-DE) Updated for the latest theme engine changes. --- themes/modern-DE/theme.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/themes/modern-DE/theme.xml b/themes/modern-DE/theme.xml index bfd86797b..27988003b 100644 --- a/themes/modern-DE/theme.xml +++ b/themes/modern-DE/theme.xml @@ -60,7 +60,7 @@ 0 0.3 1 0.4 horizontal - ./art/${system.theme}.jpg + ./art/${system.theme}.jpg 1 center 1 From c5b42379c0a14c5851f3928f59646bdcc14fd10a Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sun, 11 Dec 2022 11:27:01 +0100 Subject: [PATCH 065/101] Documentation update. --- CHANGELOG.md | 2 +- THEMES-DEV.md | 144 +++++++++++++++++++++++++------------------------- 2 files changed, 72 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 548c56b9f..9b56e16e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -186,7 +186,7 @@ * Added reflections support to the carousel * Added a new itemAxisHorizontal property to the carousel to keep wheel items horizontal at all times * Added carousel theme support for setting the opacity for unfocused entries -* Added carousel theme support for applying item color shifts +* Added carousel theme support for applying image color shifts * Added carousel theme support for setting item transitions to "slide" or "instant" * Added a fadeAbovePrimary property to control whether elements above the system view carousel and textlist should be rendered during fade transitions * Removed support for the thumbnail game media type diff --git a/THEMES-DEV.md b/THEMES-DEV.md index 704720ac1..cb0c72c54 100644 --- a/THEMES-DEV.md +++ b/THEMES-DEV.md @@ -88,7 +88,7 @@ As for more specific changes, the following are the most important ones compared * The rating elements were previously not sized and overlaid consistently, this has now been fixed and rating images should now be centered on the image canvas in order for this element to render correctly rather than being left-adjusted as has previously been done by some theme authors (likely as a workaround for the previous buggy implementation). Images of any aspect ratios are now also supported where previously only square images could be used * The carousel text element hacks `systemInfo` and `logoText` have been removed and replaced with proper carousel properties * The carousel property `maxItemCount` (formerly named maxLogoCount) is now in float format for more granular control of logo placement compared to integer format for legacy themes. However some legacy theme authors thought this property supported floats (as the theme documentation incorrectly stated this) and have therefore set it to fractional values such as 3.5. This was actually rounded up when loading the theme configuration, and this logic is retained for legacy themes for backward compatibility. But for current themes the float value is correctly interpreted which means a manual rounding of the value is required in order to retain an identical layout when porting theme sets to the new theme engine. As well carousels of the wheel type now have the amount of entries controlled by the two new properties `itemsBeforeCenter` and `itemsAfterCenter`. This provides more exact control, including the ability to setup asymmetric wheels. -* The full names of unthemed systems (or systems where the defined itemType file is missing) will now be displayed in the system carousel instead of the short names shown for legacy themes. So for instance, instead of "cps" the full name "Capcom Play System" (as defined in es_systems.xml) will be displayed. +* The full names of unthemed systems (or systems where the defined staticImage file is missing) will now be displayed in the system carousel instead of the short names shown for legacy themes. So for instance, instead of "cps" the full name "Capcom Play System" (as defined in es_systems.xml) will be displayed. * The carousel now has a zIndex value of 50 instead of 40. This means it's aligned with the textlist element which already had a zIndex value of 50. * The helpsystem `textColorDimmed` and `iconColorDimmed` properties (which apply when opening a menu) were always defined under the system view configuration which meant these properties could not be separately set for the gamelist views. Now these properties work as expected with the possibility to configure separate values for the system and gamelist views * When right-aligning the helpsystem using an X origin value of 1, the element is now aligned correctly to the defined position instead of being offset by the entrySpacing width (in RetroPie ES the offset was instead the hardcoded element entry padding) @@ -1024,9 +1024,9 @@ Properties: - Sets the carousel type and scroll direction. - Valid values are `horizontal`, `vertical`, `horizontal_wheel` or `vertical_wheel`. - Default is `horizontal` -* `staticItem` - type: PATH +* `staticImage` - type: PATH - Path to a static image file. Most common extensions are supported (including .svg, .jpg, .png, and unanimated .gif). This property can only be used in the `system` view. -* `itemType` - type: STRING +* `imageType` - type: STRING - This displays a game image of a certain media type, and can only be used in the `gamelist` view. - Valid values: - `marquee` - This will look for a marquee (wheel) image. @@ -1040,8 +1040,8 @@ Properties: - `fanart` - This will look for a fan art image. - `none` - No image will be used, instead the game name will be displayed as text. - Default is `marquee` -* `defaultItem` - type: PATH - - Path to the default image file which will be displayed if the image defined via the `staticItem` or `itemType` property is not found. Most common extensions are supported (including .svg, .jpg, .png, and unanimated .gif). +* `defaultImage` - type: PATH + - Path to the default image file which will be displayed if the image defined via the `staticImage` or `imageType` property is not found. Most common extensions are supported (including .svg, .jpg, .png, and unanimated .gif). * `maxItemCount` - type: FLOAT - Sets the number of items to display in the carousel when the `type` property has been set to "horizontal" or "vertical". Has no effect if the `type` has been set to "horizontal_wheel" or "vertical_wheel". - Minimum value is `0.5` and maximum value is `30` @@ -1062,10 +1062,6 @@ Properties: - Selected item is scaled by the value defined by this property. - Minimum value is `0.2` and maximum value is `3` - Default is `1.2` -* `itemInterpolation` - type: STRING - - Interpolation method to use when scaling items. Nearest neighbor (`nearest`) preserves sharp pixels and linear filtering (`linear`) makes the image smoother. The effect of this property is primarily visible for raster graphic images, but it has a limited effect also when using scalable vector graphics (SVG) images as these are rasterized at a set resolution and then scaled using the GPU. - - Valid values are `nearest` or `linear` - - Default is `linear` * `itemRotation` - type: FLOAT - Angle in degrees that the item should be rotated. This value should be positive if the `itemRotationOrigin` X axis has a negative value, and it should be negative if the `itemRotationOrigin` X axis has a positive value, otherwise the wheel will rotate in the wrong direction. - This property only applies when `type` is "horizontal_wheel" or "vertical_wheel". @@ -1077,14 +1073,18 @@ Properties: * `itemAxisHorizontal` - type: BOOLEAN - If `type` has been set to "horizontal_wheel" or "vertical_wheel" then the items are normally rotated towards the center of the wheel as defined by `itemRotation` and `itemRotationOrigin`. But if enabling this property the items will not get rotated along their own axis, meaning they will retain their original horizontal orientation regardless of their position along the wheel. Make sure that `itemVerticalAlignment` is set to `center` when using this attribute or you'll get some strange alignment issues. - Default is `false` -* `itemColor` - type: COLOR - - Applies a color shift to the images defined by `staticItem`, `itemType` and `defaultItem` by multiplying each pixel's color by this color value. For example, an all-white image with `FF0000` applied would become completely red. You can also control the transparency of the images by setting the value to for example `FFFFFFAA`. This keeps all pixels at their normal color and only affects the alpha channel. +* `imageInterpolation` - type: STRING + - Interpolation method to use when scaling images. Nearest neighbor (`nearest`) preserves sharp pixels and linear filtering (`linear`) makes the image smoother. The effect of this property is primarily visible for raster graphic images, but it has a limited effect also when using scalable vector graphics (SVG) images as these are rasterized at a set resolution and then scaled using the GPU. + - Valid values are `nearest` or `linear` + - Default is `linear` +* `imageColor` - type: COLOR + - Applies a color shift to the images defined by `staticImage`, `imageType` and `defaultImage` by multiplying each pixel's color by this color value. For example, an all-white image with `FF0000` applied would become completely red. You can also control the transparency of the images by setting the value to for example `FFFFFFAA`. This keeps all pixels at their normal color and only affects the alpha channel. - Default is `FFFFFFFF` (no color shift applied) -* `itemColorEnd` - type: COLOR - - Works in the exact same way as `itemColor` but can be set as the end color to apply a color shift gradient. - - Default is the same value as `itemColor` -* `itemGradientType` - type: STRING - - The direction to apply the color gradient if both `itemColor` and `itemColorEnd` have been defined. +* `imageColorEnd` - type: COLOR + - Works in the exact same way as `imageColor` but can be set as the end color to apply a color shift gradient. + - Default is the same value as `imageColor` +* `imageGradientType` - type: STRING + - The direction to apply the color gradient if both `imageColor` and `imageColorEnd` have been defined. - Valid values are `horizontal` or `vertical` - Default is `horizontal` * `itemTransitions` - type: STRING @@ -1092,11 +1092,11 @@ Properties: - Valid values are `animate` or `instant` - Default is `animate` * `itemHorizontalAlignment` - type: STRING - - Sets `staticItem` / `itemType` and `text` alignment relative to the carousel on the X axis, which applies when `type` is "vertical", "horizontal_wheel" or "vertical_wheel". + - Sets `staticImage` / `imageType` and `text` alignment relative to the carousel on the X axis, which applies when `type` is "vertical", "horizontal_wheel" or "vertical_wheel". - Valid values are `left`, `center` or `right` - Default is `center` * `itemVerticalAlignment` - type: STRING - - Sets `staticItem` / `itemType` and `text` alignment relative to the carousel on the Y axis, which applies when `type` is "horizontal", "horizontal_wheel" or "vertical_wheel". + - Sets `staticImage` / `imageType` and `text` alignment relative to the carousel on the Y axis, which applies when `type` is "horizontal", "horizontal_wheel" or "vertical_wheel". - Valid values are `top`, `center` or `bottom` - Default is `center` * `wheelHorizontalAlignment` - type: STRING @@ -1137,14 +1137,14 @@ Properties: - Valid values are `horizontal` or `vertical` - Default is `horizontal` * `text` - type: STRING - - A string literal to display if there is no `staticItem` or `defaultItem` property defined and if no image is found. This property can only be used in the system view as for the gamelist view the game name is always used as fallback. + - A string literal to display if there is no `staticImage` or `defaultImage` property defined and if no image is found. This property can only be used in the `system` view as for the gamelist view the game name is always used as fallback. - Default is the full system name. * `textColor` - type: COLOR - Default is `000000FF` * `textBackgroundColor` - type: COLOR - Default is `FFFFFF00` * `fontPath` - type: PATH - - Path to a TrueType font (.ttf) used as fallback if there is no `staticItem` / `itemType` image defined or found, and if `defaultItem` has not been defined. + - Path to a TrueType font (.ttf) used as fallback if there is no `staticImage` / `imageType` image defined or found, and if `defaultImage` has not been defined. * `fontSize` - type: FLOAT - Size of the font as a percentage of screen height (e.g. for a value of `0.1`, the text's height would be 10% of the screen height). This calculation is based on the reference 'S' character so other glyphs may not fill this area, or they may exceed this area. This property value is effectively multiplied by the `itemScale` value for the currently selected item (but if this property is omitted then the default value will not get multiplied by `itemScale`). - Minimum value is `0.001` and maximum value is `1.5`. Note that when running at a really low resolution, the minimum value may get clamped to a larger relative size. @@ -1176,8 +1176,6 @@ Properties: An X*Y grid for navigating and selecting games or systems using the left/right and up/down buttons. The layout including the amount of columns and rows is automatically calculated based on the relevant property values. -Property names for the grid element have been kept as similar as possible to those of the carousel. This does however introduce some potential confusion for a few properties due to the varying nature of these two elements. More specifically a grid `item` may refer to both the overall item in the same manner as it's used for the carousel but it may also refer to the primary image used for the item (e.g. a system logo, cover, marquee etc). Be aware of this when using the `itemRelativeScale`, `itemFit`, `itemColor`, `itemColorEnd` and `itemGradientType` properties as these will not apply to the overall item. - Supported views: * `system` * `gamelist` @@ -1195,9 +1193,9 @@ Properties: - Where on the element `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the grid exactly in the middle of the screen. If the position and size attributes are themeable, origin is implied. - Minimum value per axis is `0` and maximum value per axis is `1` - Default is `0 0` -* `staticItem` - type: PATH +* `staticImage` - type: PATH - Path to a static image file. Most common extensions are supported (including .svg, .jpg, .png, and unanimated .gif). This property can only be used in the `system` view. -* `itemType` - type: STRING +* `imageType` - type: STRING - This displays a game image of a certain media type, and can only be used in the `gamelist` view. - Valid values: - `marquee` - This will look for a marquee (wheel) image. @@ -1211,10 +1209,53 @@ Properties: - `fanart` - This will look for a fan art image. - `none` - No image will be used, instead the game name will be displayed as text. - Default is `marquee` -* `defaultItem` - type: PATH - - Path to the default image file which will be displayed if the image defined via the `staticItem` or `itemType` property is not found. Most common extensions are supported (including .svg, .jpg, .png, and unanimated .gif). +* `defaultImage` - type: PATH + - Path to the default image file which will be displayed if the image defined via the `staticImage` or `imageType` property is not found. Most common extensions are supported (including .svg, .jpg, .png, and unanimated .gif). +* `itemSize` - type: NORMALIZED_PAIR + - Size of the overall item prior to multiplication by the `itemScale` value, i.e. the size of all unselected items. If one of the axis is defined as `-1` then it will be set to the same pixel value as the other axis, resulting in a perfectly square item. If not using this approach then both axes need to be defined. + - Minimum value per axis is `0.05` and maximum value per axis is `1` + - Default is `0.15 0.25` +* `itemScale` - type: FLOAT. + - Selected overall item is scaled by the value defined by this property. + - Minimum value is `0.5` and maximum value is `2` + - Default is `1.05` +* `itemSpacing` - type: NORMALIZED_PAIR + - The horizontal and vertical space between items. This value is added to the unscaled item size, i.e. `itemSize` before it's been multiplied by `itemScale`. This means that if an axis is set to `0` then unscaled items will be perfectly adjacent to each other on that axis but if `itemScale` has been set to higher than `1` then the currently selected item will overlap adjacent items. If this property is omitted then spacing will be automatically calculated so that no overlaps occur during scaling. However you'd normally want to define and adjust this property for an optimal layout. If one of the axis is defined as `-1` then it will be set to the same pixel value as the other axis. Note that all spacing calculations are based on the value defined by `itemSize` which may or may not be the same as the actual image sizes, depending on their aspect ratios and if the `imageFit` property is used. + - Minimum value per axis is `0` and maximum value per axis is `0.1` +* `fractionalRows` - type: BOOLEAN + - Whether to allow rendering of fractional rows of items. If set to false then the effective area of the overall element size will be snapped to the item height multiplied by `itemScale`. Note that if setting `itemScale` too high relative to the `itemSpacing` Y axis value then fractional rows may still be rendered even if the `fractionalRows` property is set to false. + - Default is `false` +* `itemTransitions` - type: STRING + - How to render item transitions when navigating the grid. By default a scaling and opacity fade animation will be played when moving between items (assuming `itemScale` and `unfocusedItemOpacity` have not been set to `1`) but if this property is set to `instant` then transitions will be immediate. + - Valid values are `animate` or `instant` + - Default is `animate` +* `rowTransitions` - type: STRING + - How to render row transitions when navigating the grid. By default a sliding animation will be rendered when moving between rows but if this property is set to `instant` then transitions will be immediate. + - Valid values are `animate` or `instant` + - Default is `animate` +* `unfocusedItemOpacity` - type: FLOAT + - Sets the opacity for the items that are not currently focused. + - Minimum value is `0.1` and maximum value is `1` + - Default is `1` +* `imageRelativeScale` - type: FLOAT. + - This property makes it possible to size the image defined by `staticImage`, `imageType` or `defaultImage` relative to the overall item size. This is mostly useful when combined with the `backgroundImage` and `selectorImage` properties. + - Minimum value is `0.2` and maximum value is `1` + - Default is `1` +* `imageFit` - type: STRING + - Controls how to fit the image within the aspect ratio defined by `itemSize`. To scale and preserve the original aspect ratio, set the value to `contain`, to stretch/squash the image to fill the entire area set it to `fill` and to crop the image to fill the entire area set it to `cover` + - Valid values are `contain`, `fill` or `cover` + - Default is `contain` +* `imageColor` - type: COLOR + - Applies a color shift to the images defined by `staticImage`, `imageType` and `defaultImage` by multiplying each pixel's color by this color value. For example, an all-white image with `FF0000` applied would become completely red. You can also control the transparency of the images by setting the value to for example `FFFFFFAA`. This keeps all pixels at their normal color and only affects the alpha channel. +* `imageColorEnd` - type: COLOR + - Works in the exact same way as `imageColor` but can be set as the end color to apply a color shift gradient. + - Default is the same value as `imageColor` +* `imageGradientType` - type: STRING + - The direction to apply the color gradient if both `imageColor` and `imageColorEnd` have been defined. + - Valid values are `horizontal` or `vertical` + - Default is `horizontal` * `backgroundImage` - type: PATH - - Path to an optional background image file which will be displayed behind the image defined by `staticItem`, `itemType` or `defaultItem`. The aspect ratio for this image will not be preserved, it will be stretched or squashed to the aspect ratio set by `itemSize`. Most common extensions are supported (including .svg, .jpg, .png, and unanimated .gif). + - Path to an optional background image file which will be displayed behind the image defined by `staticImage`, `imageType` or `defaultImage`. The aspect ratio for this image will not be preserved, it will be stretched or squashed to the aspect ratio set by `itemSize`. Most common extensions are supported (including .svg, .jpg, .png, and unanimated .gif). * `backgroundRelativeScale` - type: FLOAT. - This property makes it possible to size the background relative to the overall item size. This is mostly useful when combined with the `selectorImage` property. - Minimum value is `0.2` and maximum value is `1` @@ -1247,58 +1288,15 @@ Properties: - Defines at what layer position to place the selector. It can either be placed at the bottom, in the middle between the background and image/text or on top. - Valid values are `bottom`, `middle` or `top` - Default is `top` -* `fractionalRows` - type: BOOLEAN - - Whether to allow rendering of fractional rows of items. If set to false then the effective area of the overall element size will be snapped to the item height multiplied by `itemScale`. Note that if setting `itemScale` too high relative to the `itemSpacing` Y axis value then fractional rows may still be rendered even if the `fractionalRows` property is set to false. - - Default is `false` -* `itemSize` - type: NORMALIZED_PAIR - - Size of the overall item prior to multiplication by the `itemScale` value, i.e. the size of all unselected items. If one of the axis is defined as `-1` then it will be set to the same pixel value as the other axis, resulting in a perfectly square item. If not using this approach then both axes need to be defined. - - Minimum value per axis is `0.05` and maximum value per axis is `1` - - Default is `0.15 0.25` -* `itemScale` - type: FLOAT. - - Selected overall item is scaled by the value defined by this property. - - Minimum value is `0.5` and maximum value is `2` - - Default is `1.05` -* `itemRelativeScale` - type: FLOAT. - - This property makes it possible to size the image defined by `staticItem`, `itemType` or `defaultItem` relative to the overall item size. This is mostly useful when combined with the `backgroundImage` and `selectorImage` properties. - - Minimum value is `0.2` and maximum value is `1` - - Default is `1` -* `itemFit` - type: STRING - - Controls how to fit the image within the aspect ratio defined by `itemSize`. To scale and preserve the original aspect ratio, set the value to `contain`, to stretch/squash the image to fill the entire area set it to `fill` and to crop the image to fill the entire area set it to `cover` - - Valid values are `contain`, `fill` or `cover` - - Default is `contain` -* `itemSpacing` - type: NORMALIZED_PAIR - - The horizontal and vertical space between items. This value is added to the unscaled item size, i.e. `itemSize` before it's been multiplied by `itemScale`. This means that if an axis is set to `0` then unscaled items will be perfectly adjacent to each other on that axis but if `itemScale` has been set to higher than `1` then the currently selected item will overlap adjacent items. If this property is omitted then spacing will be automatically calculated so that no overlaps occur during scaling. However you'd normally want to define and adjust this property for an optimal layout. If one of the axis is defined as `-1` then it will be set to the same pixel value as the other axis. Note that all spacing calculations are based on the value defined by `itemSize` which may or may not be the same as the actual image sizes, depending on their aspect ratios and if the `itemFit` property is used. - - Minimum value per axis is `0` and maximum value per axis is `0.1` -* `itemColor` - type: COLOR - - Applies a color shift to the images defined by `staticItem`, `itemType` and `defaultItem` by multiplying each pixel's color by this color value. For example, an all-white image with `FF0000` applied would become completely red. You can also control the transparency of the images by setting the value to for example `FFFFFFAA`. This keeps all pixels at their normal color and only affects the alpha channel. -* `itemColorEnd` - type: COLOR - - Works in the exact same way as `itemColor` but can be set as the end color to apply a color shift gradient. - - Default is the same value as `itemColor` -* `itemGradientType` - type: STRING - - The direction to apply the color gradient if both `itemColor` and `itemColorEnd` have been defined. - - Valid values are `horizontal` or `vertical` - - Default is `horizontal` -* `itemTransitions` - type: STRING - - How to render item transitions when navigating the grid. By default a scaling and opacity fade animation will be played when moving between items (assuming `itemScale` and `unfocusedItemOpacity` have not been set to `1`) but if this property is set to `instant` then transitions will be immediate. - - Valid values are `animate` or `instant` - - Default is `animate` -* `rowTransitions` - type: STRING - - How to render row transitions when navigating the grid. By default a sliding animation will be rendered when moving between rows but if this property is set to `instant` then transitions will be immediate. - - Valid values are `animate` or `instant` - - Default is `animate` -* `unfocusedItemOpacity` - type: FLOAT - - Sets the opacity for the items that are not currently focused. - - Minimum value is `0.1` and maximum value is `1` - - Default is `1` * `text` - type: STRING - - A string literal to display if there is no `staticItem` or `defaultItem` property defined and if no image is found. This property can only be used in the system view as for the gamelist view the game name is always used as fallback. + - A string literal to display if there is no `staticImage` or `defaultImage` property defined or if no image is found. This property can only be used in the `system` view as for the gamelist view the game name is always used as fallback. - Default is the full system name. * `textColor` - type: COLOR - Default is `000000FF` * `textBackgroundColor` - type: COLOR - Default is `FFFFFF00` * `fontPath` - type: PATH - - Path to a TrueType font (.ttf) used as fallback if there is no `staticItem` / `itemType` image defined or found, and if `defaultItem` has not been defined. + - Path to a TrueType font (.ttf) used as fallback if there is no `staticImage` / `imageType` image defined or found, and if `defaultImage` has not been defined. * `fontSize` - type: FLOAT - Size of the font as a percentage of screen height (e.g. for a value of `0.1`, the text's height would be 10% of the screen height). This calculation is based on the reference 'S' character so other glyphs may not fill this area, or they may exceed this area. - Minimum value is `0.001` and maximum value is `1.5`. Note that when running at a really low resolution, the minimum value may get clamped to a larger relative size. @@ -1495,7 +1493,7 @@ Properties: - Applies a color shift to the image by multiplying each pixel's color by this color value. For example, an all-white image with `FF0000` applied would become completely red. You can also control the transparency of the image by setting the value to for example `FFFFFFAA`. This keeps all pixels at their normal color and only affects the alpha channel. - Default is `FFFFFFFF` (no color shift applied) * `colorEnd` - type: COLOR - - Works in the exact same way as `itemColor` but can be set as the end color to apply a color shift gradient. + - Works in the exact same way as `color` but can be set as the end color to apply a color shift gradient. - Default is the same value as `color` * `gradientType` - type: STRING - The direction to apply the color shift gradient if both `color` and `colorEnd` have been defined. From abcd3e5bacb255b0ddd079412d002dfc760e0e23 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sun, 11 Dec 2022 12:10:08 +0100 Subject: [PATCH 066/101] Fixed an issue in CarouselComponent and GridComponent where the default image was rendered when it shouldn't. Also moved a property position in GridComponent. --- es-core/src/ThemeData.cpp | 2 +- .../components/primary/CarouselComponent.h | 7 ++- .../src/components/primary/GridComponent.h | 51 ++++++++++--------- 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 91096e6a4..8477e1b9f 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -190,10 +190,10 @@ std::map> {"backgroundGradientType", STRING}, {"selectorImage", PATH}, {"selectorRelativeScale", FLOAT}, + {"selectorLayer", STRING}, {"selectorColor", COLOR}, {"selectorColorEnd", COLOR}, {"selectorGradientType", STRING}, - {"selectorLayer", STRING}, {"text", STRING}, {"textRelativeScale", FLOAT}, {"textColor", COLOR}, diff --git a/es-core/src/components/primary/CarouselComponent.h b/es-core/src/components/primary/CarouselComponent.h index f2e16c035..3006f7265 100644 --- a/es-core/src/components/primary/CarouselComponent.h +++ b/es-core/src/components/primary/CarouselComponent.h @@ -278,7 +278,9 @@ void CarouselComponent::addEntry(Entry& entry, const std::shared_ptrsetColorGradientHorizontal(false); defaultImage->setRotateByTargetSize(true); - entry.data.item = defaultImage; + // For the gamelist view the default image is applied in onDemandTextureLoad(). + if (!mGamelistView) + entry.data.item = defaultImage; } } @@ -455,6 +457,9 @@ template void CarouselComponent::onDemandTextureLoad() else if (mImageType == "none") // Display the game name as text. return; + if (entry.data.imagePath == "") + entry.data.imagePath = entry.data.defaultImagePath; + auto theme = game->getSystem()->getTheme(); updateEntry(entry, theme); } diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index 7d4c82342..f70e1e862 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -65,7 +65,7 @@ public: const std::string& getImageType() { return mImageType; } void setImageType(std::string imageType) { mImageType = imageType; } const std::string& getDefaultImage() { return mDefaultImage; } - void setDefaultImage(std::string defaultItem) { mDefaultImage = defaultItem; } + void setDefaultImage(std::string defaultImage) { mDefaultImage = defaultImage; } bool input(InputConfig* config, Input input) override; void update(int deltaTime) override; void render(const glm::mat4& parentTrans) override; @@ -104,18 +104,18 @@ private: bool remove(const T& obj) override { return List::remove(obj); } int size() const override { return List::size(); } - enum class SelectorLayer { - TOP, - MIDDLE, - BOTTOM - }; - enum class ImageFit { CONTAIN, FILL, COVER }; + enum class SelectorLayer { + TOP, + MIDDLE, + BOTTOM + }; + Renderer* mRenderer; std::function mCancelTransitionsCallback; std::function mCursorChangedCallback; @@ -155,11 +155,11 @@ private: bool mHasBackgroundColor; std::unique_ptr mSelectorImage; float mSelectorRelativeScale; + SelectorLayer mSelectorLayer; unsigned int mSelectorColor; unsigned int mSelectorColorEnd; bool mSelectorColorGradientHorizontal; bool mHasSelectorColor; - SelectorLayer mSelectorLayer; float mTextRelativeScale; unsigned int mTextColor; unsigned int mTextBackgroundColor; @@ -207,11 +207,11 @@ GridComponent::GridComponent() , mBackgroundColorGradientHorizontal {true} , mHasBackgroundColor {false} , mSelectorRelativeScale {1.0f} + , mSelectorLayer {SelectorLayer::TOP} , mSelectorColor {0xFFFFFFFF} , mSelectorColorEnd {0xFFFFFFFF} , mSelectorColorGradientHorizontal {true} , mHasSelectorColor {false} - , mSelectorLayer {SelectorLayer::TOP} , mTextRelativeScale {1.0f} , mTextColor {0x000000FF} , mTextBackgroundColor {0xFFFFFF00} @@ -254,27 +254,29 @@ void GridComponent::addEntry(Entry& entry, const std::shared_ptr& } else if (entry.data.defaultImagePath != "" && ResourceManager::getInstance().fileExists(entry.data.defaultImagePath)) { - auto defaultItem = std::make_shared(false, dynamic); - defaultItem->setLinearInterpolation(true); - defaultItem->setMipmapping(true); + auto defaultImage = std::make_shared(false, dynamic); + defaultImage->setLinearInterpolation(true); + defaultImage->setMipmapping(true); if (mImagefit == ImageFit::CONTAIN) - defaultItem->setMaxSize(mItemSize * mImageRelativeScale); + defaultImage->setMaxSize(mItemSize * mImageRelativeScale); else if (mImagefit == ImageFit::FILL) - defaultItem->setResize(mItemSize * mImageRelativeScale); + defaultImage->setResize(mItemSize * mImageRelativeScale); else if (mImagefit == ImageFit::COVER) - defaultItem->setCroppedSize(mItemSize * mImageRelativeScale); - defaultItem->setImage(entry.data.defaultImagePath); - defaultItem->applyTheme(theme, "system", "", ThemeFlags::ALL); + defaultImage->setCroppedSize(mItemSize * mImageRelativeScale); + defaultImage->setImage(entry.data.defaultImagePath); + defaultImage->applyTheme(theme, "system", "", ThemeFlags::ALL); if (mImageColor != 0xFFFFFFFF) - defaultItem->setColorShift(mImageColor); + defaultImage->setColorShift(mImageColor); if (mImageColorEnd != mImageColor) { - defaultItem->setColorShiftEnd(mImageColorEnd); + defaultImage->setColorShiftEnd(mImageColorEnd); if (!mImageColorGradientHorizontal) - defaultItem->setColorGradientHorizontal(false); + defaultImage->setColorGradientHorizontal(false); } - defaultItem->setOrigin(0.5f, 0.5f); - defaultItem->setRotateByTargetSize(true); - entry.data.item = defaultItem; + defaultImage->setOrigin(0.5f, 0.5f); + defaultImage->setRotateByTargetSize(true); + // For the gamelist view the default image is applied in onDemandTextureLoad(). + if (!mGamelistView) + entry.data.item = defaultImage; } if (!entry.data.item) { @@ -392,6 +394,9 @@ template void GridComponent::onDemandTextureLoad() else if (mImageType == "none") // Display the game name as text. return; + if (entry.data.imagePath == "") + entry.data.imagePath = entry.data.defaultImagePath; + auto theme = game->getSystem()->getTheme(); updateEntry(entry, theme); } From 8b33c43dba5b8b443265d018d8410640f46033b2 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sun, 11 Dec 2022 16:40:47 +0100 Subject: [PATCH 067/101] Added support to GridComponent for flushing cached background and selector images on reload when in debug mode. --- es-core/src/components/primary/GridComponent.h | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index f70e1e862..78dd10e92 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -35,6 +35,7 @@ public: using Entry = typename IList::Entry; GridComponent(); + ~GridComponent(); void addEntry(Entry& entry, const std::shared_ptr& theme); void updateEntry(Entry& entry, const std::shared_ptr& theme); @@ -148,12 +149,14 @@ private: unsigned int mImageColorEnd; bool mImageColorGradientHorizontal; std::unique_ptr mBackgroundImage; + std::string mBackgroundImagePath; float mBackgroundRelativeScale; unsigned int mBackgroundColor; unsigned int mBackgroundColorEnd; bool mBackgroundColorGradientHorizontal; bool mHasBackgroundColor; std::unique_ptr mSelectorImage; + std::string mSelectorImagePath; float mSelectorRelativeScale; SelectorLayer mSelectorLayer; unsigned int mSelectorColor; @@ -223,6 +226,18 @@ GridComponent::GridComponent() { } +template GridComponent::~GridComponent() +{ + // Manually flush the background and selector images from the texture cache on destruction + // when running in debug mode, otherwise a complete system view reload would be needed to + // get these images updated. This is useful during theme development when using the Ctrl-r + // keyboard combination to reload the theme configuration. + if (Settings::getInstance()->getBool("Debug")) { + TextureResource::manualUnload(mBackgroundImagePath, false); + TextureResource::manualUnload(mSelectorImagePath, false); + } +} + template void GridComponent::addEntry(Entry& entry, const std::shared_ptr& theme) { @@ -867,6 +882,7 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, } } mBackgroundImage->setImage(elem->get("backgroundImage")); + mBackgroundImagePath = path; } else { LOG(LogWarning) << "GridComponent: Invalid theme configuration, property " @@ -891,6 +907,7 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, } } mSelectorImage->setImage(elem->get("selectorImage")); + mSelectorImagePath = path; } else { LOG(LogWarning) << "GridComponent: Invalid theme configuration, property " From 030604ff9867b263e64f331b540e0ec8c255d1d0 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sun, 11 Dec 2022 16:53:11 +0100 Subject: [PATCH 068/101] (slate-DE) Added a theme engine test variant for the grid element. --- themes/slate-DE/capabilities.xml | 5 + themes/slate-DE/core/images/grid_frame.svg | 26 +++ themes/slate-DE/theme.xml | 4 + themes/slate-DE/theme_engine_test_4.xml | 188 +++++++++++++++++++++ 4 files changed, 223 insertions(+) create mode 100644 themes/slate-DE/core/images/grid_frame.svg create mode 100644 themes/slate-DE/theme_engine_test_4.xml diff --git a/themes/slate-DE/capabilities.xml b/themes/slate-DE/capabilities.xml index 8efdc6e72..ba3329bd0 100644 --- a/themes/slate-DE/capabilities.xml +++ b/themes/slate-DE/capabilities.xml @@ -47,6 +47,11 @@ true + + + true + + false diff --git a/themes/slate-DE/core/images/grid_frame.svg b/themes/slate-DE/core/images/grid_frame.svg new file mode 100644 index 000000000..68d8815c9 --- /dev/null +++ b/themes/slate-DE/core/images/grid_frame.svg @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/themes/slate-DE/theme.xml b/themes/slate-DE/theme.xml index 462834e05..9697d4750 100644 --- a/themes/slate-DE/theme.xml +++ b/themes/slate-DE/theme.xml @@ -431,4 +431,8 @@ ./theme_engine_test_3.xml + + + ./theme_engine_test_4.xml + \ No newline at end of file diff --git a/themes/slate-DE/theme_engine_test_4.xml b/themes/slate-DE/theme_engine_test_4.xml new file mode 100644 index 000000000..601ffc2f0 --- /dev/null +++ b/themes/slate-DE/theme_engine_test_4.xml @@ -0,0 +1,188 @@ + + + + + + 0.5 0.2 + 0.86 0.715 + 0.5 0 + ./${system.theme}/images/logo.svg + 0.151 -1 + 1.1 + 0.022 -1 + true + animate + animate + 1.0 + 0.9 + contain + ./core/images/grid_frame.svg + 1.0 + 424242 + ./core/images/grid_frame.svg + 1.0 + middle + FF3333 + 0.8 + F0F0F0 + ./core/fonts/Exo2-RegularCondensed.otf + 0.032 + uppercase + 1.2 + true + + + 0.02 0.17 + 0.4 0.15 + 0 1 + + + 0.78 0.17 + 0.1 0.15 + 1 1 + + + 0.5 0.19 + 0.96 0.735 + 0.5 0 + ./core/images/frame.png + 181818 + 10 + + + 0.02 0.17 + 0.32 0.13 + 0 1 + + + 0.8 0.145 + 0.117 0.056 + 0 0.5 + gamecount_games + ./core/fonts/Exo2-RegularCondensed.otf + 0.035 + center + DDDDDD + 262626DD + uppercase + 50 + + + 0.012 0.955 + ${gamelistHelpColor} + ${gamelistHelpColor} + ${gamelistHelpColorDimmed} + ${gamelistHelpColorDimmed} + + + + false + + + + + + 0.5 0.3 + 0.86 0.623 + 0.5 0 + cover + 0.151 -1 + 1.1 + 0.022 -1 + false + animate + animate + 1.0 + 0.9 + contain + ./core/images/grid_frame.svg + 1.0 + 424242 + ./core/images/grid_frame.svg + 1.0 + middle + FF3333 + 0.8 + F0F0F0 + ./core/fonts/Exo2-RegularCondensed.otf + 0.032 + uppercase + 1.2 + + + 0.5 0.19 + 0.96 0.735 + 0.5 0 + ./core/images/frame.png + 181818 + 10 + + + 0.92 0.225 + 0.55 0.08 + 1 0 + name + selector_recent + ./core/fonts/Exo2-RegularCondensed.otf + 0.045 + right + center + D6D6D6 + none + 80 + + + 0.025 0.205 + 0.446 0.185 + 0.02 + 1.2 + + + 0.815 0.203 + 0.04 0.03 + + + 888888 + + + 0.03 0.205 + 0.38 0.08 + 1 + 10 + + + 0.012 0.955 + ${gamelistHelpColor} + ${gamelistHelpColor} + ${gamelistHelpColorDimmed} + ${gamelistHelpColorDimmed} + + + + false + + + false + + + false + + + false + + + + + + + 0.192 -1 + + + 0.034 + + + + + \ No newline at end of file From e3be493d0dd9c6b45775cb7e6f6907df417a1c8c Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sun, 11 Dec 2022 17:02:21 +0100 Subject: [PATCH 069/101] Removed a few unused lambda captures from SystemView. --- es-app/src/views/SystemView.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 05890cf0f..6f4e5fbe6 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -375,7 +375,7 @@ void SystemView::onCursorChanged(const CursorState& state) if (transitionStyle == "fade") { float startFade {mFadeOpacity}; anim = new LambdaAnimation( - [this, startFade, startPos, endPos, posMax](float t) { + [this, startFade, endPos](float t) { if (t < 0.3f) mFadeOpacity = glm::mix(0.0f, 1.0f, glm::clamp(t / 0.2f + startFade, 0.0f, 1.0f)); @@ -437,9 +437,8 @@ void SystemView::onCursorChanged(const CursorState& state) else { // Instant. updateGameCount(); - anim = - new LambdaAnimation([this, startPos, endPos, posMax](float t) { mCamOffset = endPos; }, - static_cast(animTime)); + anim = new LambdaAnimation([this, endPos](float t) { mCamOffset = endPos; }, + static_cast(animTime)); } setAnimation(anim, 0, nullptr, false, 0); From 7b7a33de1f6a46de3e33b43a846b6ff5814b8b54 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sun, 11 Dec 2022 17:08:05 +0100 Subject: [PATCH 070/101] Documentation update. --- THEMES-DEV.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/THEMES-DEV.md b/THEMES-DEV.md index cb0c72c54..f0f3f625c 100644 --- a/THEMES-DEV.md +++ b/THEMES-DEV.md @@ -1230,7 +1230,7 @@ Properties: - Valid values are `animate` or `instant` - Default is `animate` * `rowTransitions` - type: STRING - - How to render row transitions when navigating the grid. By default a sliding animation will be rendered when moving between rows but if this property is set to `instant` then transitions will be immediate. + - How to render row transitions when navigating the grid. By default a sliding animation will be rendered when moving between rows but if this property is set to `instant` then transitions will be immediate. If setting this to `instant` it's recommended to do the same for `itemTransitions` or otherwise the animations will look a bit ugly. - Valid values are `animate` or `instant` - Default is `animate` * `unfocusedItemOpacity` - type: FLOAT @@ -1275,6 +1275,10 @@ Properties: - This property makes it possible to size the selector relative to the overall item size. This is mostly useful when combined with the `backgroundImage` property. - Minimum value is `0.2` and maximum value is `1` - Default is `1` +* `selectorLayer` - type: STRING + - Defines at what layer position to place the selector. It can either be placed at the bottom, in the middle between the background and image/text or on top. + - Valid values are `bottom`, `middle` or `top` + - Default is `top` * `selectorColor` - type: COLOR - Applies a color shift or draws a colored rectangle. If an image has been defined using the `selectorImage` property then each pixel of that image is multiplied by this color value. For example, an all-white image with `FF0000` applied would become completely red. You can also control the transparency of the image by setting the value to for example `FFFFFFAA`. This keeps all pixels at their normal color and only affects the alpha channel. If no selector image has been defined, then a colored rectangle will be drawn instead. * `selectorColorEnd` - type: COLOR @@ -1284,10 +1288,6 @@ Properties: - The direction to apply the color gradient if both `selectorColor` and `selectorColorEnd` have been defined. - Valid values are `horizontal` or `vertical` - Default is `horizontal` -* `selectorLayer` - type: STRING - - Defines at what layer position to place the selector. It can either be placed at the bottom, in the middle between the background and image/text or on top. - - Valid values are `bottom`, `middle` or `top` - - Default is `top` * `text` - type: STRING - A string literal to display if there is no `staticImage` or `defaultImage` property defined or if no image is found. This property can only be used in the `system` view as for the gamelist view the game name is always used as fallback. - Default is the full system name. From a5b08e3da258bbb3aa18e1517409f081b55db0aa Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sun, 11 Dec 2022 17:48:56 +0100 Subject: [PATCH 071/101] Moved the position of one GridComponent property. --- es-core/src/ThemeData.cpp | 2 +- es-core/src/components/primary/GridComponent.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 8477e1b9f..76366472a 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -178,8 +178,8 @@ std::map> {"rowTransitions", STRING}, {"unfocusedItemOpacity", FLOAT}, {"edgeScaleInwards", BOOLEAN}, // TODO - {"imageRelativeScale", FLOAT}, {"imageFit", STRING}, + {"imageRelativeScale", FLOAT}, {"imageColor", COLOR}, {"imageColorEnd", COLOR}, {"imageGradientType", STRING}, diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index 78dd10e92..2a09cbce9 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -143,8 +143,8 @@ private: bool mInstantItemTransitions; bool mInstantRowTransitions; float mUnfocusedItemOpacity; - float mImageRelativeScale; ImageFit mImagefit; + float mImageRelativeScale; unsigned int mImageColor; unsigned int mImageColorEnd; bool mImageColorGradientHorizontal; @@ -199,8 +199,8 @@ GridComponent::GridComponent() , mInstantItemTransitions {false} , mInstantRowTransitions {false} , mUnfocusedItemOpacity {1.0f} - , mImageRelativeScale {1.0f} , mImagefit {ImageFit::CONTAIN} + , mImageRelativeScale {1.0f} , mImageColor {0xFFFFFFFF} , mImageColorEnd {0xFFFFFFFF} , mImageColorGradientHorizontal {true} From 19111a9c2487b156fd37891a4cc6949efc986cb8 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Mon, 12 Dec 2022 17:45:07 +0100 Subject: [PATCH 072/101] Added an itemStacking property to CarouselComponent. --- es-core/src/ThemeData.cpp | 1 + .../components/primary/CarouselComponent.h | 63 +++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 76366472a..b4a9704b5 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -122,6 +122,7 @@ std::map> {"maxLogoCount", FLOAT}, // For backward compatibility with legacy themes. {"itemsBeforeCenter", UNSIGNED_INTEGER}, {"itemsAfterCenter", UNSIGNED_INTEGER}, + {"itemStacking", STRING}, {"itemSize", NORMALIZED_PAIR}, {"itemScale", FLOAT}, {"itemRotation", FLOAT}, diff --git a/es-core/src/components/primary/CarouselComponent.h b/es-core/src/components/primary/CarouselComponent.h index 3006f7265..f8056084c 100644 --- a/es-core/src/components/primary/CarouselComponent.h +++ b/es-core/src/components/primary/CarouselComponent.h @@ -46,6 +46,14 @@ public: NO_CAROUSEL }; + enum class ItemStacking { + CENTERED, + ASCENDING, + ASCENDING_RAISED, + DESCENDING, + DESCENDING_RAISED + }; + CarouselComponent(); void addEntry(Entry& entry, const std::shared_ptr& theme); @@ -129,6 +137,7 @@ private: float mMaxItemCount; int mItemsBeforeCenter; int mItemsAfterCenter; + ItemStacking mItemStacking; glm::vec2 mItemSize; float mItemScale; float mItemRotation; @@ -179,6 +188,7 @@ CarouselComponent::CarouselComponent() , mMaxItemCount {3.0f} , mItemsBeforeCenter {8} , mItemsAfterCenter {8} + , mItemStacking {ItemStacking::CENTERED} , mItemSize {glm::vec2 {Renderer::getScreenWidth() * 0.25f, Renderer::getScreenHeight() * 0.155f}} , mItemScale {1.2f} @@ -420,6 +430,7 @@ template void CarouselComponent::onDemandTextureLoad() if (mVerticalOffset > 0.0f) centerOffset = -centerOffset; } + itemInclusion += 1; } for (int i = center - itemInclusion - itemInclusionBefore; @@ -840,6 +851,33 @@ template void CarouselComponent::render(const glm::mat4& parentT if (renderItems.size() == 1) { renderItemsSorted.emplace_back(renderItems.front()); } + else if (!isWheel && mItemStacking != ItemStacking::CENTERED) { + if (mItemStacking == ItemStacking::ASCENDING) { + renderItemsSorted.insert(renderItemsSorted.begin(), + std::make_move_iterator(renderItems.begin()), + std::make_move_iterator(renderItems.end())); + } + else if (mItemStacking == ItemStacking::ASCENDING_RAISED) { + for (size_t i {0}; i < renderItems.size(); ++i) { + if (i == static_cast(belowCenter)) + continue; + renderItemsSorted.emplace_back(std::move(renderItems[i])); + } + renderItemsSorted.emplace_back(std::move(renderItems[belowCenter])); + } + else if (mItemStacking == ItemStacking::DESCENDING) { + for (size_t i {renderItems.size()}; i > 0; --i) + renderItemsSorted.emplace_back(std::move(renderItems[i - 1])); + } + else if (mItemStacking == ItemStacking::DESCENDING_RAISED) { + for (size_t i {renderItems.size()}; i > 0; --i) { + if (i - 1 == static_cast(belowCenter)) + continue; + renderItemsSorted.emplace_back(std::move(renderItems[i - 1])); + } + renderItemsSorted.emplace_back(std::move(renderItems[belowCenter])); + } + } else { // Make sure that overlapping items are rendered in the correct order. size_t zeroDistanceEntry {0}; @@ -1032,6 +1070,31 @@ void CarouselComponent::applyTheme(const std::shared_ptr& theme, itemSize * glm::vec2(Renderer::getScreenWidth(), Renderer::getScreenHeight()); } + if (elem->has("itemStacking")) { + const std::string& itemStacking {elem->get("itemStacking")}; + if (itemStacking == "ascending") { + mItemStacking = ItemStacking::ASCENDING; + } + else if (itemStacking == "ascendingRaised") { + mItemStacking = ItemStacking::ASCENDING_RAISED; + } + else if (itemStacking == "descending") { + mItemStacking = ItemStacking::DESCENDING; + } + else if (itemStacking == "descendingRaised") { + mItemStacking = ItemStacking::DESCENDING_RAISED; + } + else if (itemStacking == "centered") { + mItemStacking = ItemStacking::CENTERED; + } + else { + mItemStacking = ItemStacking::CENTERED; + LOG(LogWarning) << "CarouselComponent: Invalid theme configuration, property " + "\"itemStacking\" for element \"" + << element.substr(9) << "\" defined as \"" << itemStacking << "\""; + } + } + if (elem->has("itemScale")) mItemScale = glm::clamp(elem->get("itemScale"), 0.2f, 3.0f); From 0ad339d4c4d5e50971bd9a614cd774dc2b8a81f0 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Mon, 12 Dec 2022 20:21:22 +0100 Subject: [PATCH 073/101] Fixed an issue where lowered saturation could not be combined with color shifts. --- resources/shaders/glsl/core.glsl | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/resources/shaders/glsl/core.glsl b/resources/shaders/glsl/core.glsl index 22fb53cfc..2bcd22374 100644 --- a/resources/shaders/glsl/core.glsl +++ b/resources/shaders/glsl/core.glsl @@ -4,7 +4,7 @@ // core.glsl // // Core shader functionality: -// Clipping, opacity, saturation, dimming and reflections falloff. +// Clipping, saturation, opacity, dimming and reflections falloff. // // Vertex section of code: @@ -39,8 +39,8 @@ in vec2 texCoord; in vec4 color; uniform vec4 clipRegion; -uniform float opacity; uniform float saturation; +uniform float opacity; uniform float dimming; uniform float reflectionsFalloff; uniform uint shaderFlags; @@ -70,6 +70,18 @@ void main() vec4 sampledColor = texture(textureSampler, texCoord); + // Saturation. + if (saturation != 1.0) { + vec3 grayscale; + // Premultiplied textures are all in BGRA format. + if (0x0u != (shaderFlags & 0x01u)) + grayscale = vec3(dot(sampledColor.bgr, vec3(0.0721, 0.7154, 0.2125))); + else + grayscale = vec3(dot(sampledColor.rgb, vec3(0.2125, 0.7154, 0.0721))); + vec3 blendedColor = mix(grayscale, sampledColor.rgb, saturation); + sampledColor = vec4(blendedColor, sampledColor.a); + } + // For fonts the alpha information is stored in the red channel. if (0x0u != (shaderFlags & 0x2u)) sampledColor = vec4(1.0, 1.0, 1.0, sampledColor.r); @@ -97,18 +109,6 @@ void main() sampledColor *= opacity; } - // Saturation. - if (saturation != 1.0) { - vec3 grayscale; - // Premultiplied textures are all in BGRA format. - if (0x0u != (shaderFlags & 0x01u)) - grayscale = vec3(dot(sampledColor.bgr, vec3(0.34, 0.55, 0.11))); - else - grayscale = vec3(dot(sampledColor.rgb, vec3(0.34, 0.55, 0.11))); - vec3 blendedColor = mix(grayscale, sampledColor.rgb, saturation); - sampledColor = vec4(blendedColor, sampledColor.a); - } - // Dimming. if (dimming != 1.0) { vec4 dimColor = vec4(dimming, dimming, dimming, 1.0); From 1517826b2342a48e79394d74ee7f740362b438c9 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Mon, 12 Dec 2022 20:24:16 +0100 Subject: [PATCH 074/101] Fixed an issue where lowered saturation would not work correctly when combined with scanline rendering. --- resources/shaders/glsl/scanlines.glsl | 30 ++++++++------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/resources/shaders/glsl/scanlines.glsl b/resources/shaders/glsl/scanlines.glsl index 960a0065e..9e9bc62f0 100644 --- a/resources/shaders/glsl/scanlines.glsl +++ b/resources/shaders/glsl/scanlines.glsl @@ -101,13 +101,6 @@ void main() float h_weight_00 = dx / SPOT_WIDTH; WEIGHT(h_weight_00); - // Saturation. - if (saturation != 1.0) { - vec3 grayscale = vec3(dot(color.rgb, vec3(0.34, 0.55, 0.11))); - vec3 blendedColor = mix(grayscale, color.rgb, saturation); - color = vec4(blendedColor, color.a); - } - color *= vec4(h_weight_00, h_weight_00, h_weight_00, h_weight_00); // Get closest horizontal neighbour to blend. @@ -122,13 +115,6 @@ void main() } vec4 colorNB = TEX2D(texture_coords + coords01); - // Saturation. - if (saturation != 1.0) { - vec3 grayscale = vec3(dot(colorNB.rgb, vec3(0.34, 0.55, 0.11))); - vec3 blendedColor = mix(grayscale, colorNB.rgb, saturation); - colorNB = vec4(blendedColor, colorNB.a); - } - float h_weight_01 = dx / SPOT_WIDTH; WEIGHT(h_weight_01); @@ -152,13 +138,6 @@ void main() } colorNB = TEX2D(texture_coords + coords10); - // Saturation. - if (saturation != 1.0) { - vec3 grayscale = vec3(dot(colorNB.rgb, vec3(0.34, 0.55, 0.11))); - vec3 blendedColor = mix(grayscale, colorNB.rgb, saturation); - colorNB = vec4(blendedColor, colorNB.a); - } - float v_weight_10 = dy / SPOT_HEIGHT; WEIGHT(v_weight_10); @@ -170,6 +149,15 @@ void main() color *= vec4(COLOR_BOOST); vec4 colorTemp = clamp(GAMMA_OUT(color), 0.0, 1.0); + + // Saturation. + if (saturation != 1.0) { + vec3 grayscale; + grayscale = vec3(dot(colorTemp.bgr, vec3(0.0721, 0.7154, 0.2125))); + vec3 blendedColor = mix(grayscale, colorTemp.rgb, saturation); + colorTemp = vec4(blendedColor, colorTemp.a); + } + FragColor = vec4(colorTemp.rgb, colorTemp.a * opacity); } #endif From 46a1e28aa2b3004265fddb65ae1598a953406820 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Mon, 12 Dec 2022 20:42:54 +0100 Subject: [PATCH 075/101] Fixed an issue where the shader postprocessing function did not set the shader premultiplied flag. --- es-core/src/renderers/RendererOpenGL.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/es-core/src/renderers/RendererOpenGL.cpp b/es-core/src/renderers/RendererOpenGL.cpp index 8907cc1f5..cb7d611d7 100644 --- a/es-core/src/renderers/RendererOpenGL.cpp +++ b/es-core/src/renderers/RendererOpenGL.cpp @@ -548,7 +548,7 @@ void RendererOpenGL::shaderPostprocessing(unsigned int shaders, vertices->opacity = parameters.opacity; vertices->saturation = parameters.saturation; vertices->dimming = parameters.dimming; - vertices->shaderFlags = ShaderFlags::POST_PROCESSING; + vertices->shaderFlags = ShaderFlags::POST_PROCESSING | ShaderFlags::PREMULTIPLIED; if (shaders & Shader::CORE) shaderList.push_back(Shader::CORE); From 6037e80bf169d2c30152e3f0d5c419c2470e9060 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Mon, 12 Dec 2022 21:51:27 +0100 Subject: [PATCH 076/101] Added color shift support to VideoComponent. --- es-core/src/ThemeData.cpp | 3 ++ es-core/src/components/VideoComponent.cpp | 32 +++++++++++++++++++ es-core/src/components/VideoComponent.h | 3 ++ .../src/components/VideoFFmpegComponent.cpp | 5 +++ resources/shaders/glsl/scanlines.glsl | 9 ++++++ 5 files changed, 52 insertions(+) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index b4a9704b5..568c5c01e 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -275,6 +275,9 @@ std::map> {"gameselector", STRING}, {"audio", BOOLEAN}, {"interpolation", STRING}, + {"color", COLOR}, + {"colorEnd", COLOR}, + {"gradientType", STRING}, {"pillarboxes", BOOLEAN}, {"pillarboxThreshold", NORMALIZED_PAIR}, {"scanlines", BOOLEAN}, diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index 5bde60c27..fc08db3ec 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -22,6 +22,9 @@ VideoComponent::VideoComponent() : mVideoWidth {0} , mVideoHeight {0} + , mColorShift {0xFFFFFFFF} + , mColorShiftEnd {0xFFFFFFFF} + , mColorGradientHorizontal {true} , mTargetSize {0.0f, 0.0f} , mVideoAreaPos {0.0f, 0.0f} , mVideoAreaSize {0.0f, 0.0f} @@ -254,6 +257,29 @@ void VideoComponent::applyTheme(const std::shared_ptr& theme, } } + if (elem->has("color")) { + mColorShift = elem->get("color"); + mColorShiftEnd = mColorShift; + } + if (elem->has("colorEnd")) + mColorShiftEnd = elem->get("colorEnd"); + + if (elem->has("gradientType")) { + const std::string& gradientType {elem->get("gradientType")}; + if (gradientType == "horizontal") { + mColorGradientHorizontal = true; + } + else if (gradientType == "vertical") { + mColorGradientHorizontal = false; + } + else { + mColorGradientHorizontal = true; + LOG(LogWarning) << "VideoComponent: Invalid theme configuration, property " + "\"gradientType\" for element \"" + << element.substr(6) << "\" defined as \"" << gradientType << "\""; + } + } + if (elem->has("pillarboxes")) mDrawPillarboxes = elem->get("pillarboxes"); @@ -355,6 +381,12 @@ void VideoComponent::renderSnapshot(const glm::mat4& parentTrans) if (mStaticImagePath != "") { mStaticImage.setOpacity(mOpacity * mThemeOpacity); mStaticImage.setSaturation(mSaturation * mThemeSaturation); + if (mColorShift != 0xFFFFFFFF) + mStaticImage.setColorShift(mColorShift); + if (mColorShift != mColorShiftEnd) + mStaticImage.setColorShiftEnd(mColorShiftEnd); + if (!mColorGradientHorizontal) + mStaticImage.setColorGradientHorizontal(mColorGradientHorizontal); mStaticImage.setDimming(mDimming); mStaticImage.render(parentTrans); } diff --git a/es-core/src/components/VideoComponent.h b/es-core/src/components/VideoComponent.h index d214dead5..ef590cee9 100644 --- a/es-core/src/components/VideoComponent.h +++ b/es-core/src/components/VideoComponent.h @@ -101,6 +101,9 @@ protected: unsigned mVideoWidth; unsigned mVideoHeight; + unsigned int mColorShift; + unsigned int mColorShiftEnd; + bool mColorGradientHorizontal; glm::vec2 mTargetSize; glm::vec2 mVideoAreaPos; glm::vec2 mVideoAreaSize; diff --git a/es-core/src/components/VideoFFmpegComponent.cpp b/es-core/src/components/VideoFFmpegComponent.cpp index b78994535..08279e73d 100644 --- a/es-core/src/components/VideoFFmpegComponent.cpp +++ b/es-core/src/components/VideoFFmpegComponent.cpp @@ -164,6 +164,11 @@ void VideoFFmpegComponent::render(const glm::mat4& parentTrans) vertices[3] = {{mSize.x + mRectangleOffset.x, mSize.y + + mRectangleOffset.y}, {1.0f, 1.0f}, 0xFFFFFFFF}; // clang-format on + vertices[0].color = mColorShift; + vertices[1].color = mColorGradientHorizontal ? mColorShift : mColorShiftEnd; + vertices[2].color = mColorGradientHorizontal ? mColorShiftEnd : mColorShift; + vertices[3].color = mColorShiftEnd; + // Round vertices. for (int i = 0; i < 4; ++i) vertices[i].position = glm::round(vertices[i].position); diff --git a/resources/shaders/glsl/scanlines.glsl b/resources/shaders/glsl/scanlines.glsl index 9e9bc62f0..cbc2cb60b 100644 --- a/resources/shaders/glsl/scanlines.glsl +++ b/resources/shaders/glsl/scanlines.glsl @@ -30,10 +30,13 @@ precision mediump float; uniform mat4 MVPMatrix; in vec2 positionVertex; in vec2 texCoordVertex; +in vec4 colorVertex; uniform vec2 textureSize; + out vec2 texCoord; out vec2 onex; out vec2 oney; +out vec4 colorShift; #define SourceSize vec4(textureSize, 1.0 / textureSize) @@ -43,6 +46,7 @@ void main() texCoord = texCoordVertex; onex = vec2(SourceSize.z, 0.0); oney = vec2(0.0, SourceSize.w); + colorShift.abgr = colorVertex.rgba; } // Fragment section of code: @@ -59,6 +63,7 @@ uniform sampler2D textureSampler; in vec2 texCoord; in vec2 onex; in vec2 oney; +in vec4 colorShift; out vec4 FragColor; #define SourceSize vec4(textureSize, 1.0 / textureSize) @@ -158,6 +163,10 @@ void main() colorTemp = vec4(blendedColor, colorTemp.a); } + // Color shift. + colorTemp.rgb *= colorShift.rgb; + colorTemp.a *= colorShift.a; + FragColor = vec4(colorTemp.rgb, colorTemp.a * opacity); } #endif From 16aacd5953abba1534e15fdb781119bd0b9bbb5a Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Mon, 12 Dec 2022 21:56:35 +0100 Subject: [PATCH 077/101] Documentation update. --- CHANGELOG.md | 2 ++ THEMES-DEV.md | 24 +++++++++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b56e16e5..ef9abbd4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -114,6 +114,7 @@ * Added theme support for enabling and disabling video pillarboxes and scanline rendering * Added theme support for defining the threshold for when pillarboxes should be applied to a video * Added theme support for enabling or disabling audio playback for videos +* Added theme support for color shifting videos (and the static image) * Added theme support for setting separate textColorDimmed and iconColorDimmed properties for the system and gamelist views * Added support for nesting of theme variables * Added support for defining multiple theme "variables" tags in the same XML file @@ -188,6 +189,7 @@ * Added carousel theme support for setting the opacity for unfocused entries * Added carousel theme support for applying image color shifts * Added carousel theme support for setting item transitions to "slide" or "instant" +* Added carousel theme support for controlling item stacking for overlapping items * Added a fadeAbovePrimary property to control whether elements above the system view carousel and textlist should be rendered during fade transitions * Removed support for the thumbnail game media type * Changed all occurances of "GameList" to "Gamelist" throughout the codebase diff --git a/THEMES-DEV.md b/THEMES-DEV.md index f0f3f625c..1c2bfaa4c 100644 --- a/THEMES-DEV.md +++ b/THEMES-DEV.md @@ -1054,6 +1054,10 @@ Properties: - Sets the number of items below the center position (the currently selected item) when the `type` property has been set to "horizontal_wheel" or "vertical_wheel". By setting this property and `itemsBeforeCenter` to different values an asymmetric wheel can be configured. Combine with `itemRotation` to control how many entries to display in the carousel. - Minimum value is `0` and maximum value is `20` - Default is `8` +* `itemStacking` - type: STRING + - Controls how to stack overlapping items when `type` has been set to "horizontal" or "vertical". It has no effect on wheel carousels or when items do not overlap. When set to `centered` the selected item will be raised and items further from the selected item (to the left/right or above/below depending on the carousel orientation) will be progressively rendered lower than the items closer to the center. If set to `ascending` then items will be rendered progressively higher from left to right or from top to bottom depending on the carousel orientation. If set to `descending` the opposite takes place with items being progressively rendered lower from left to right or top to bottom depending on the carousel orientation. Finally `ascendingRaised` and `descendingRaised` work identically to `ascending` and `descending` with the only difference that the currently selected item will be raised above the other items. + - Valid values are `centered`, `ascending`, `ascendingRaised`, `descending` or `descendingRaised` + - Default is `centered` * `itemSize` - type: NORMALIZED_PAIR - Size of the item prior to multiplication by the `itemScale` value, i.e. the size of all unselected items. Both axes need to be defined. - Minimum value per axis is `0.05` and maximum value per axis is `1` @@ -1237,14 +1241,14 @@ Properties: - Sets the opacity for the items that are not currently focused. - Minimum value is `0.1` and maximum value is `1` - Default is `1` -* `imageRelativeScale` - type: FLOAT. - - This property makes it possible to size the image defined by `staticImage`, `imageType` or `defaultImage` relative to the overall item size. This is mostly useful when combined with the `backgroundImage` and `selectorImage` properties. - - Minimum value is `0.2` and maximum value is `1` - - Default is `1` * `imageFit` - type: STRING - Controls how to fit the image within the aspect ratio defined by `itemSize`. To scale and preserve the original aspect ratio, set the value to `contain`, to stretch/squash the image to fill the entire area set it to `fill` and to crop the image to fill the entire area set it to `cover` - Valid values are `contain`, `fill` or `cover` - Default is `contain` +* `imageRelativeScale` - type: FLOAT. + - This property makes it possible to size the image defined by `staticImage`, `imageType` or `defaultImage` relative to the overall item size. This is mostly useful when combined with the `backgroundImage` and `selectorImage` properties. + - Minimum value is `0.2` and maximum value is `1` + - Default is `1` * `imageColor` - type: COLOR - Applies a color shift to the images defined by `staticImage`, `imageType` and `defaultImage` by multiplying each pixel's color by this color value. For example, an all-white image with `FF0000` applied would become completely red. You can also control the transparency of the images by setting the value to for example `FFFFFFAA`. This keeps all pixels at their normal color and only affects the alpha channel. * `imageColorEnd` - type: COLOR @@ -1490,7 +1494,7 @@ Properties: - Valid values are `nearest` or `linear` - Default is `nearest` * `color` - type: COLOR - - Applies a color shift to the image by multiplying each pixel's color by this color value. For example, an all-white image with `FF0000` applied would become completely red. You can also control the transparency of the image by setting the value to for example `FFFFFFAA`. This keeps all pixels at their normal color and only affects the alpha channel. + - Applies a color shift to the image by multiplying each pixel's color by this color value. For example, an all-white image with `FF0000` applied would become completely red. You can also control the transparency of the image by setting the value to for example `FFFFFFAA`. This keeps all pixels at their normal color and only affects the alpha channel. This property is applied after `saturation` so by setting that property to `0` it's possible to colorize rather than color shift. - Default is `FFFFFFFF` (no color shift applied) * `colorEnd` - type: COLOR - Works in the exact same way as `color` but can be set as the end color to apply a color shift gradient. @@ -1571,6 +1575,16 @@ Properties: - Interpolation method to use when scaling raster images. Nearest neighbor (`nearest`) preserves sharp pixels and linear filtering (`linear`) makes the image smoother. Note that this property only affects the static image, not the video scaling. This property also has no effect on scalable vector graphics (SVG) images. - Valid values are `nearest` or `linear` - Default is `nearest` +* `color` - type: COLOR + - Applies a color shift to both the static image and video by multiplying each pixel's color by this color value. For example, an all-white image or video with `FF0000` applied would become completely red. You can also control the transparency of the image and video by setting the value to for example `FFFFFFAA`. This keeps all pixels at their normal color and only affects the alpha channel. This property is applied after `saturation` so by setting that property to `0` it's possible to colorize rather than color shift. + - Default is `FFFFFFFF` (no color shift applied) +* `colorEnd` - type: COLOR + - Works in the exact same way as `color` but can be set as the end color to apply a color shift gradient. + - Default is the same value as `color` +* `gradientType` - type: STRING + - The direction to apply the color shift gradient if both `color` and `colorEnd` have been defined. + - Valid values are `horizontal` or `vertical` + - Default is `horizontal` * `pillarboxes` - type: BOOLEAN - Whether to render black pillarboxes (and to a lesses extent letterboxes) for videos with aspect ratios where this is applicable. This is for instance useful for arcade game videos in vertical orientation. - Default is `true` From c260c929b56f0efe0e168dea9e4a13adbe9a0b72 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Tue, 13 Dec 2022 21:35:21 +0100 Subject: [PATCH 078/101] Added itemSaturation properties to CarouselComponent and GridComponent. --- es-core/src/ThemeData.cpp | 2 ++ es-core/src/components/primary/CarouselComponent.h | 11 +++++++++++ es-core/src/components/primary/GridComponent.h | 11 +++++++++++ 3 files changed, 24 insertions(+) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 568c5c01e..6b1f40af1 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -133,6 +133,7 @@ std::map> {"imageColor", COLOR}, {"imageColorEnd", COLOR}, {"imageGradientType", STRING}, + {"imageSaturation", FLOAT}, {"itemTransitions", STRING}, {"itemHorizontalAlignment", STRING}, {"itemVerticalAlignment", STRING}, @@ -184,6 +185,7 @@ std::map> {"imageColor", COLOR}, {"imageColorEnd", COLOR}, {"imageGradientType", STRING}, + {"imageSaturation", FLOAT}, {"backgroundImage", PATH}, {"backgroundRelativeScale", FLOAT}, {"backgroundColor", COLOR}, diff --git a/es-core/src/components/primary/CarouselComponent.h b/es-core/src/components/primary/CarouselComponent.h index f8056084c..6d2467fe9 100644 --- a/es-core/src/components/primary/CarouselComponent.h +++ b/es-core/src/components/primary/CarouselComponent.h @@ -147,6 +147,7 @@ private: unsigned int mImageColorShift; unsigned int mImageColorShiftEnd; bool mImageColorGradientHorizontal; + float mImageSaturation; bool mInstantItemTransitions; Alignment mItemHorizontalAlignment; Alignment mItemVerticalAlignment; @@ -199,6 +200,7 @@ CarouselComponent::CarouselComponent() , mImageColorShift {0xFFFFFFFF} , mImageColorShiftEnd {0xFFFFFFFF} , mImageColorGradientHorizontal {true} + , mImageSaturation {1.0f} , mInstantItemTransitions {false} , mItemHorizontalAlignment {ALIGN_CENTER} , mItemVerticalAlignment {ALIGN_CENTER} @@ -263,6 +265,8 @@ void CarouselComponent::addEntry(Entry& entry, const std::shared_ptrsetMaxSize(glm::round(mItemSize * (mItemScale >= 1.0f ? mItemScale : 1.0f))); item->setImage(entry.data.imagePath); item->applyTheme(theme, "system", "", ThemeFlags::ALL); + if (mImageSaturation != 1.0) + item->setSaturation(mImageSaturation); if (mImageColorShift != 0xFFFFFFFF) item->setColorShift(mImageColorShift); if (mImageColorShiftEnd != mImageColorShift) @@ -281,6 +285,8 @@ void CarouselComponent::addEntry(Entry& entry, const std::shared_ptr= 1.0f ? mItemScale : 1.0f))); defaultImage->setImage(entry.data.defaultImagePath); defaultImage->applyTheme(theme, "system", "", ThemeFlags::ALL); + if (mImageSaturation != 1.0) + defaultImage->setSaturation(mImageSaturation); if (mImageColorShift != 0xFFFFFFFF) defaultImage->setColorShift(mImageColorShift); if (mImageColorShiftEnd != mImageColorShift) @@ -348,6 +354,8 @@ void CarouselComponent::updateEntry(Entry& entry, const std::shared_ptrsetMaxSize(glm::round(mItemSize * (mItemScale >= 1.0f ? mItemScale : 1.0f))); item->setImage(entry.data.imagePath); item->applyTheme(theme, "system", "", ThemeFlags::ALL); + if (mImageSaturation != 1.0) + item->setSaturation(mImageSaturation); if (mImageColorShift != 0xFFFFFFFF) item->setColorShift(mImageColorShift); if (mImageColorShiftEnd != mImageColorShift) @@ -1116,6 +1124,9 @@ void CarouselComponent::applyTheme(const std::shared_ptr& theme, } } + if (elem->has("imageSaturation")) + mImageSaturation = glm::clamp(elem->get("imageSaturation"), 0.0f, 1.0f); + if (elem->has("imageInterpolation")) { const std::string& imageInterpolation {elem->get("imageInterpolation")}; if (imageInterpolation == "linear") { diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index 2a09cbce9..a9b4c48ca 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -148,6 +148,7 @@ private: unsigned int mImageColor; unsigned int mImageColorEnd; bool mImageColorGradientHorizontal; + float mImageSaturation; std::unique_ptr mBackgroundImage; std::string mBackgroundImagePath; float mBackgroundRelativeScale; @@ -204,6 +205,7 @@ GridComponent::GridComponent() , mImageColor {0xFFFFFFFF} , mImageColorEnd {0xFFFFFFFF} , mImageColorGradientHorizontal {true} + , mImageSaturation {1.0f} , mBackgroundRelativeScale {1.0f} , mBackgroundColor {0xFFFFFFFF} , mBackgroundColorEnd {0xFFFFFFFF} @@ -256,6 +258,8 @@ void GridComponent::addEntry(Entry& entry, const std::shared_ptr& item->setCroppedSize(mItemSize * mImageRelativeScale); item->setImage(entry.data.imagePath); item->applyTheme(theme, "system", "", ThemeFlags::ALL); + if (mImageSaturation != 1.0) + item->setSaturation(mImageSaturation); if (mImageColor != 0xFFFFFFFF) item->setColorShift(mImageColor); if (mImageColorEnd != mImageColor) { @@ -280,6 +284,8 @@ void GridComponent::addEntry(Entry& entry, const std::shared_ptr& defaultImage->setCroppedSize(mItemSize * mImageRelativeScale); defaultImage->setImage(entry.data.defaultImagePath); defaultImage->applyTheme(theme, "system", "", ThemeFlags::ALL); + if (mImageSaturation != 1.0) + defaultImage->setSaturation(mImageSaturation); if (mImageColor != 0xFFFFFFFF) defaultImage->setColorShift(mImageColor); if (mImageColorEnd != mImageColor) { @@ -329,6 +335,8 @@ void GridComponent::updateEntry(Entry& entry, const std::shared_ptrsetCroppedSize(mItemSize * mImageRelativeScale); item->setImage(entry.data.imagePath); item->applyTheme(theme, "system", "", ThemeFlags::ALL); + if (mImageSaturation != 1.0) + item->setSaturation(mImageSaturation); if (mImageColor != 0xFFFFFFFF) item->setColorShift(mImageColor); if (mImageColorEnd != mImageColor) { @@ -1019,6 +1027,9 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, } } + if (elem->has("imageSaturation")) + mImageSaturation = glm::clamp(elem->get("imageSaturation"), 0.0f, 1.0f); + if (elem->has("unfocusedItemOpacity")) mUnfocusedItemOpacity = glm::clamp(elem->get("unfocusedItemOpacity"), 0.1f, 1.0f); From 468d7aa09d9b65a0cfad86a31c8e3623869d412a Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Tue, 13 Dec 2022 21:39:04 +0100 Subject: [PATCH 079/101] Documentation update. --- CHANGELOG.md | 1 + THEMES-DEV.md | 14 +++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef9abbd4a..f3de9013e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -188,6 +188,7 @@ * Added a new itemAxisHorizontal property to the carousel to keep wheel items horizontal at all times * Added carousel theme support for setting the opacity for unfocused entries * Added carousel theme support for applying image color shifts +* Added carousel theme support for defining image saturation * Added carousel theme support for setting item transitions to "slide" or "instant" * Added carousel theme support for controlling item stacking for overlapping items * Added a fadeAbovePrimary property to control whether elements above the system view carousel and textlist should be rendered during fade transitions diff --git a/THEMES-DEV.md b/THEMES-DEV.md index 1c2bfaa4c..860912439 100644 --- a/THEMES-DEV.md +++ b/THEMES-DEV.md @@ -1082,7 +1082,7 @@ Properties: - Valid values are `nearest` or `linear` - Default is `linear` * `imageColor` - type: COLOR - - Applies a color shift to the images defined by `staticImage`, `imageType` and `defaultImage` by multiplying each pixel's color by this color value. For example, an all-white image with `FF0000` applied would become completely red. You can also control the transparency of the images by setting the value to for example `FFFFFFAA`. This keeps all pixels at their normal color and only affects the alpha channel. + - Applies a color shift to the images defined by `staticImage`, `imageType` and `defaultImage` by multiplying each pixel's color by this color value. For example, an all-white image with `FF0000` applied would become completely red. You can also control the transparency of the images by setting the value to for example `FFFFFFAA`. This keeps all pixels at their normal color and only affects the alpha channel. This property is applied after `imageSaturation` so by setting that property to `0` it's possible to colorize rather than color shift. - Default is `FFFFFFFF` (no color shift applied) * `imageColorEnd` - type: COLOR - Works in the exact same way as `imageColor` but can be set as the end color to apply a color shift gradient. @@ -1091,6 +1091,10 @@ Properties: - The direction to apply the color gradient if both `imageColor` and `imageColorEnd` have been defined. - Valid values are `horizontal` or `vertical` - Default is `horizontal` +* `imageSaturation` - type: FLOAT + - Controls the level of color saturation. + - Minimum value is `0` (grayscale) and maximum value is `1` (original file saturation). + - Default is `1` * `itemTransitions` - type: STRING - How to render item transitions when navigating the carousel. By default a slide, scale and opacity fade animation will be played when moving between items (the latter two assuming `itemScale` and `unfocusedItemOpacity` have not been set to `1`) but if this property is set to `instant` then transitions will be immediate. - Valid values are `animate` or `instant` @@ -1250,7 +1254,7 @@ Properties: - Minimum value is `0.2` and maximum value is `1` - Default is `1` * `imageColor` - type: COLOR - - Applies a color shift to the images defined by `staticImage`, `imageType` and `defaultImage` by multiplying each pixel's color by this color value. For example, an all-white image with `FF0000` applied would become completely red. You can also control the transparency of the images by setting the value to for example `FFFFFFAA`. This keeps all pixels at their normal color and only affects the alpha channel. + - Applies a color shift to the images defined by `staticImage`, `imageType` and `defaultImage` by multiplying each pixel's color by this color value. For example, an all-white image with `FF0000` applied would become completely red. You can also control the transparency of the images by setting the value to for example `FFFFFFAA`. This keeps all pixels at their normal color and only affects the alpha channel. This property is applied after `imageSaturation` so by setting that property to `0` it's possible to colorize rather than color shift. * `imageColorEnd` - type: COLOR - Works in the exact same way as `imageColor` but can be set as the end color to apply a color shift gradient. - Default is the same value as `imageColor` @@ -1258,6 +1262,10 @@ Properties: - The direction to apply the color gradient if both `imageColor` and `imageColorEnd` have been defined. - Valid values are `horizontal` or `vertical` - Default is `horizontal` +* `imageSaturation` - type: FLOAT + - Controls the level of color saturation. + - Minimum value is `0` (grayscale) and maximum value is `1` (original file saturation). + - Default is `1` * `backgroundImage` - type: PATH - Path to an optional background image file which will be displayed behind the image defined by `staticImage`, `imageType` or `defaultImage`. The aspect ratio for this image will not be preserved, it will be stretched or squashed to the aspect ratio set by `itemSize`. Most common extensions are supported (including .svg, .jpg, .png, and unanimated .gif). * `backgroundRelativeScale` - type: FLOAT. @@ -1576,7 +1584,7 @@ Properties: - Valid values are `nearest` or `linear` - Default is `nearest` * `color` - type: COLOR - - Applies a color shift to both the static image and video by multiplying each pixel's color by this color value. For example, an all-white image or video with `FF0000` applied would become completely red. You can also control the transparency of the image and video by setting the value to for example `FFFFFFAA`. This keeps all pixels at their normal color and only affects the alpha channel. This property is applied after `saturation` so by setting that property to `0` it's possible to colorize rather than color shift. + - Applies a color shift to both the static image and video by multiplying each pixel's color by this color value. For example, an all-white image or video with `FF0000` applied would become completely red. It's however not recommended to use this property to control opacity as this will not look right for actual videos, instead use the `opacity` property if you want to render this element as semi-transparent. The `color` property is applied after `saturation` so by setting that property to `0` it's possible to colorize rather than color shift. - Default is `FFFFFFFF` (no color shift applied) * `colorEnd` - type: COLOR - Works in the exact same way as `color` but can be set as the end color to apply a color shift gradient. From 59d5e1b5b8d55fa88b9b8c3912fc9b2a7fc8257a Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Tue, 13 Dec 2022 22:45:05 +0100 Subject: [PATCH 080/101] Changed the color channel mix for grayscale conversions. --- resources/shaders/glsl/core.glsl | 4 ++-- resources/shaders/glsl/scanlines.glsl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/shaders/glsl/core.glsl b/resources/shaders/glsl/core.glsl index 2bcd22374..b918fe969 100644 --- a/resources/shaders/glsl/core.glsl +++ b/resources/shaders/glsl/core.glsl @@ -75,9 +75,9 @@ void main() vec3 grayscale; // Premultiplied textures are all in BGRA format. if (0x0u != (shaderFlags & 0x01u)) - grayscale = vec3(dot(sampledColor.bgr, vec3(0.0721, 0.7154, 0.2125))); + grayscale = vec3(dot(sampledColor.bgr, vec3(0.114, 0.587, 0.299))); else - grayscale = vec3(dot(sampledColor.rgb, vec3(0.2125, 0.7154, 0.0721))); + grayscale = vec3(dot(sampledColor.rgb, vec3(0.299, 0.587, 0.114))); vec3 blendedColor = mix(grayscale, sampledColor.rgb, saturation); sampledColor = vec4(blendedColor, sampledColor.a); } diff --git a/resources/shaders/glsl/scanlines.glsl b/resources/shaders/glsl/scanlines.glsl index cbc2cb60b..2be2e9b71 100644 --- a/resources/shaders/glsl/scanlines.glsl +++ b/resources/shaders/glsl/scanlines.glsl @@ -158,7 +158,7 @@ void main() // Saturation. if (saturation != 1.0) { vec3 grayscale; - grayscale = vec3(dot(colorTemp.bgr, vec3(0.0721, 0.7154, 0.2125))); + grayscale = vec3(dot(colorTemp.bgr, vec3(0.114, 0.587, 0.299))); vec3 blendedColor = mix(grayscale, colorTemp.rgb, saturation); colorTemp = vec4(blendedColor, colorTemp.a); } From ab31eafaca6a7b96b0ae0ef559173d886edbf208 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Wed, 14 Dec 2022 17:30:34 +0100 Subject: [PATCH 081/101] Some code cleanup. --- es-app/src/FileData.cpp | 86 +++++------ es-app/src/GamelistFileParser.cpp | 108 +++++++------- es-app/src/SystemData.cpp | 216 ++++++++++++++------------- es-core/src/utils/FileSystemUtil.cpp | 182 +++++++++++----------- 4 files changed, 302 insertions(+), 290 deletions(-) diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 80bcf5a08..dea180154 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -40,6 +40,7 @@ FileData::FileData(FileType type, , mEnvData {envData} , mSystem {system} , mOnlyFolders {false} + , mHasFolders {false} , mUpdateChildrenLastPlayed {false} , mUpdateChildrenMostPlayed {false} , mDeletionFlag {false} @@ -76,7 +77,7 @@ FileData::~FileData() std::string FileData::getDisplayName() const { - std::string stem {Utils::FileSystem::getStem(mPath)}; + const std::string& stem {Utils::FileSystem::getStem(mPath)}; return stem; } @@ -167,7 +168,7 @@ const std::vector FileData::getChildrenRecursive() const const std::string FileData::getROMDirectory() { - std::string romDirSetting {Settings::getInstance()->getString("ROMDirectory")}; + const std::string& romDirSetting {Settings::getInstance()->getString("ROMDirectory")}; std::string romDirPath; if (romDirSetting == "") { @@ -196,7 +197,7 @@ const std::string FileData::getROMDirectory() const std::string FileData::getMediaDirectory() { - std::string mediaDirSetting {Settings::getInstance()->getString("MediaDirectory")}; + const std::string& mediaDirSetting {Settings::getInstance()->getString("MediaDirectory")}; std::string mediaDirPath; if (mediaDirSetting == "") { @@ -233,7 +234,7 @@ const std::string FileData::getMediafilePath(const std::string& subdirectory) co subFolders + "/" + getDisplayName()}; // Look for an image file in the media directory. - for (size_t i = 0; i < extList.size(); ++i) { + for (size_t i {0}; i < extList.size(); ++i) { std::string mediaPath {tempPath + extList[i]}; if (Utils::FileSystem::exists(mediaPath)) return mediaPath; @@ -332,7 +333,7 @@ const std::string FileData::getVideoPath() const getDisplayName()}; // Look for media in the media directory. - for (size_t i = 0; i < extList.size(); ++i) { + for (size_t i {0}; i < extList.size(); ++i) { std::string mediaPath {tempPath + extList[i]}; if (Utils::FileSystem::exists(mediaPath)) return mediaPath; @@ -424,7 +425,7 @@ std::vector FileData::getScrapeFilesRecursive(bool includeFolders, const bool FileData::isArcadeAsset() const { - const std::string stem {Utils::FileSystem::getStem(mPath)}; + const std::string& stem {Utils::FileSystem::getStem(mPath)}; return ((mSystem && (mSystem->hasPlatformId(PlatformIds::ARCADE) || mSystem->hasPlatformId(PlatformIds::SNK_NEO_GEO))) && (MameNames::getInstance().isBios(stem) || MameNames::getInstance().isDevice(stem))); @@ -432,7 +433,7 @@ const bool FileData::isArcadeAsset() const const bool FileData::isArcadeGame() const { - const std::string stem {Utils::FileSystem::getStem(mPath)}; + const std::string& stem {Utils::FileSystem::getStem(mPath)}; return ((mSystem && (mSystem->hasPlatformId(PlatformIds::ARCADE) || mSystem->hasPlatformId(PlatformIds::SNK_NEO_GEO))) && (!MameNames::getInstance().isBios(stem) && !MameNames::getInstance().isDevice(stem))); @@ -444,7 +445,7 @@ void FileData::addChild(FileData* file) if (!mSystem->getFlattenFolders()) assert(file->getParent() == nullptr); - const std::string key = file->getKey(); + const std::string& key {file->getKey()}; if (mChildrenByFilename.find(key) == mChildrenByFilename.cend()) { mChildrenByFilename[key] = file; mChildren.emplace_back(file); @@ -481,7 +482,7 @@ void FileData::sort(ComparisonFunction& comparator, std::vector mChildrenOthers; if (mSystem->isGroupedCustomCollection()) - gameCount = {}; + gameCount = {0, 0}; if (!showHiddenGames) { for (auto it = mChildren.begin(); it != mChildren.end();) { @@ -505,20 +506,20 @@ void FileData::sort(ComparisonFunction& comparator, // The main custom collections view is sorted during startup in CollectionSystemsManager. // The individual collections are however sorted as any normal systems/folders. if (mSystem->isCollection() && mSystem->getFullName() == "collections") { - std::pair tempGameCount {}; + std::pair tempGameCount {0, 0}; for (auto it = mChildren.cbegin(); it != mChildren.cend(); ++it) { if ((*it)->getChildren().size() > 0) (*it)->sort(comparator, gameCount); tempGameCount.first += gameCount.first; tempGameCount.second += gameCount.second; - gameCount = {}; + gameCount = {0, 0}; } gameCount = tempGameCount; return; } if (foldersOnTop) { - for (unsigned int i = 0; i < mChildren.size(); ++i) { + for (unsigned int i {0}; i < mChildren.size(); ++i) { if (mChildren[i]->getType() == FOLDER) { mChildrenFolders.emplace_back(mChildren[i]); } @@ -597,24 +598,24 @@ void FileData::sortFavoritesOnTop(ComparisonFunction& comparator, std::vector mChildrenOthers; if (mSystem->isGroupedCustomCollection()) - gameCount = {}; + gameCount = {0, 0}; // The main custom collections view is sorted during startup in CollectionSystemsManager. // The individual collections are however sorted as any normal systems/folders. if (mSystem->isCollection() && mSystem->getFullName() == "collections") { - std::pair tempGameCount = {}; + std::pair tempGameCount = {0, 0}; for (auto it = mChildren.cbegin(); it != mChildren.cend(); ++it) { if ((*it)->getChildren().size() > 0) (*it)->sortFavoritesOnTop(comparator, gameCount); tempGameCount.first += gameCount.first; tempGameCount.second += gameCount.second; - gameCount = {}; + gameCount = {0, 0}; } gameCount = tempGameCount; return; } - for (unsigned int i = 0; i < mChildren.size(); ++i) { + for (unsigned int i {0}; i < mChildren.size(); ++i) { // If the option to hide hidden games has been set and the game is hidden, // then skip it. Normally games are hidden during loading of the gamelists in // Gamelist::parseGamelist() and this code should only run when a user has marked @@ -745,7 +746,7 @@ void FileData::countGames(std::pair& gameCount) bool isKidMode {(Settings::getInstance()->getString("UIMode") == "kid" || Settings::getInstance()->getBool("ForceKid"))}; - for (unsigned int i = 0; i < mChildren.size(); ++i) { + for (unsigned int i {0}; i < mChildren.size(); ++i) { if (mChildren[i]->getType() == GAME && mChildren[i]->getCountAsGame()) { if (!isKidMode || (isKidMode && mChildren[i]->getKidgame())) { ++gameCount.first; @@ -798,9 +799,9 @@ void FileData::updateMostPlayedList() const FileData::SortType& FileData::getSortTypeFromString(const std::string& desc) const { - std::vector SortTypes = FileSorts::SortTypes; + std::vector SortTypes {FileSorts::SortTypes}; - for (unsigned int i = 0; i < FileSorts::SortTypes.size(); ++i) { + for (unsigned int i {0}; i < FileSorts::SortTypes.size(); ++i) { const FileData::SortType& sort {FileSorts::SortTypes.at(i)}; if (sort.description == desc) return sort; @@ -983,11 +984,11 @@ void FileData::launchGame() command = Utils::FileSystem::expandHomePath(command); // Check that the emulator binary actually exists, and if so, get its path. - std::string binaryPath {findEmulatorPath(command)}; + const std::string& binaryPath {findEmulatorPath(command)}; // Hack to show an error message if there was no emulator entry in es_find_rules.xml. if (binaryPath.substr(0, 18) == "NO EMULATOR RULE: ") { - std::string emulatorEntry {binaryPath.substr(18, binaryPath.size() - 18)}; + const std::string& emulatorEntry {binaryPath.substr(18, binaryPath.size() - 18)}; LOG(LogError) << "Couldn't launch game, either there is no emulator entry for \"" << emulatorEntry << "\" in es_find_rules.xml or there are no rules defined"; LOG(LogError) << "Raw emulator launch command:"; @@ -1051,7 +1052,7 @@ void FileData::launchGame() quotationMarkPos = static_cast(command.find("\"", emuPathPos + 9) - emuPathPos); } - size_t spacePos {command.find(" ", emuPathPos + quotationMarkPos)}; + const size_t spacePos {command.find(" ", emuPathPos + quotationMarkPos)}; std::string coreRaw; std::string coreFile; if (spacePos != std::string::npos) { @@ -1120,7 +1121,7 @@ void FileData::launchGame() // If a %CORE_ find rule entry is used in es_systems.xml for this system, then try to find // the emulator core using the rules defined in es_find_rules.xml. - for (std::string path : emulatorCorePaths) { + for (std::string& path : emulatorCorePaths) { // The position of the %CORE_ variable could have changed as there may have been an // %EMULATOR_ variable that was substituted for the actual emulator binary. coreEntryPos = command.find("%CORE_"); @@ -1210,7 +1211,7 @@ void FileData::launchGame() } std::string startDirectory; - size_t startDirPos {command.find("%STARTDIR%")}; + const size_t startDirPos {command.find("%STARTDIR%")}; if (startDirPos != std::string::npos) { bool invalidEntry {false}; @@ -1233,7 +1234,7 @@ void FileData::launchGame() } } else if (!invalidEntry) { - size_t spacePos {command.find(" ", startDirPos)}; + const size_t spacePos {command.find(" ", startDirPos)}; if (spacePos != std::string::npos) { startDirectory = command.substr(startDirPos + 11, spacePos - startDirPos - 11); command = command.replace(startDirPos, spacePos - startDirPos + 1, ""); @@ -1331,7 +1332,7 @@ void FileData::launchGame() } } else if (!invalidEntry) { - size_t spacePos {command.find(" ", injectPos)}; + const size_t spacePos {command.find(" ", injectPos)}; if (spacePos != std::string::npos) { injectFile = command.substr(injectPos + 9, spacePos - injectPos - 9); command = command.replace(injectPos, spacePos - injectPos + 1, ""); @@ -1407,8 +1408,8 @@ void FileData::launchGame() // The special characters need to be procesed in this order. std::string specialCharacters {"^&()=;,"}; - for (size_t i = 0; i < specialCharacters.size(); ++i) { - std::string special(1, specialCharacters[i]); + for (size_t i {0}; i < specialCharacters.size(); ++i) { + const std::string& special {1, specialCharacters[i]}; if (romPath.find(special) != std::string::npos) { romPath = Utils::String::replace(romPath, special, "^" + special); foundSpecial = true; @@ -1701,15 +1702,15 @@ const std::string FileData::findEmulatorPath(std::string& command) return "NO EMULATOR RULE: " + emulatorEntry; #if defined(_WIN64) - for (std::string path : emulatorWinRegistryPaths) { + for (std::string& path : emulatorWinRegistryPaths) { // Search for the emulator using the App Paths keys in the Windows Registry. - std::string registryKeyPath {"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\" + - path}; + const std::string& registryKeyPath { + "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\" + path}; HKEY registryKey; LSTATUS keyStatus {-1}; LSTATUS pathStatus {-1}; - char registryPath[1024] {}; + char registryPath[1024] {0}; DWORD pathSize {1024}; // First look in HKEY_CURRENT_USER. @@ -1746,11 +1747,11 @@ const std::string FileData::findEmulatorPath(std::string& command) RegCloseKey(registryKey); } - for (std::string value : emulatorWinRegistryValues) { + for (std::string& value : emulatorWinRegistryValues) { // If the pipe character is found, then the string following this should be appended // to the key value, assuming the key is found. std::string appendString; - size_t pipePos {value.find('|')}; + const size_t pipePos {value.find('|')}; if (pipePos != std::string::npos) { appendString = value.substr(pipePos + 1, std::string::npos); @@ -1758,14 +1759,14 @@ const std::string FileData::findEmulatorPath(std::string& command) } // Search for the defined value in the Windows Registry. - std::string registryValueKey { + const std::string& registryValueKey { Utils::String::replace(Utils::FileSystem::getParent(value), "/", "\\")}; - std::string registryValue {Utils::FileSystem::getFileName(value)}; + const std::string& registryValue {Utils::FileSystem::getFileName(value)}; HKEY registryKey; LSTATUS keyStatus {-1}; LSTATUS pathStatus {-1}; - char path[1024] {}; + char path[1024] {0}; DWORD pathSize {1024}; // First look in HKEY_CURRENT_USER. @@ -1811,7 +1812,7 @@ const std::string FileData::findEmulatorPath(std::string& command) } #endif - for (std::string path : emulatorSystemPaths) { + for (std::string& path : emulatorSystemPaths) { #if defined(_WIN64) std::wstring pathWide {Utils::String::stringToWideString(path)}; // Search for the emulator using the PATH environment variable. @@ -1846,11 +1847,11 @@ const std::string FileData::findEmulatorPath(std::string& command) #endif } - for (std::string path : emulatorStaticPaths) { + for (std::string& path : emulatorStaticPaths) { // If a pipe character is present in the staticpath entry it means we should substitute // the emulator binary with whatever is defined after the pipe character. std::string replaceCommand; - size_t pipePos {path.find('|')}; + const size_t pipePos {path.find('|')}; if (pipePos != std::string::npos) { replaceCommand = path.substr(pipePos + 1); @@ -1899,7 +1900,7 @@ const std::string FileData::findEmulatorPath(std::string& command) // If the first character is a quotation mark, then we need to extract up to the // next quotation mark, otherwise we'll only extract up to the first space character. if (command.front() == '\"') { - std::string emuTemp {command.substr(1, std::string::npos)}; + const std::string& emuTemp {command.substr(1, std::string::npos)}; emuExecutable = emuTemp.substr(0, emuTemp.find('"')); } else { @@ -1917,7 +1918,8 @@ const std::string FileData::findEmulatorPath(std::string& command) #if defined(_WIN64) std::wstring emuExecutableWide {Utils::String::stringToWideString(emuExecutable)}; // Search for the emulator using the PATH environment variable. - DWORD size {SearchPathW(nullptr, emuExecutableWide.c_str(), L".exe", 0, nullptr, nullptr)}; + const DWORD size { + SearchPathW(nullptr, emuExecutableWide.c_str(), L".exe", 0, nullptr, nullptr)}; if (size) { std::vector pathBuffer(static_cast(size) + 1); diff --git a/es-app/src/GamelistFileParser.cpp b/es-app/src/GamelistFileParser.cpp index a6ca3659f..412014095 100644 --- a/es-app/src/GamelistFileParser.cpp +++ b/es-app/src/GamelistFileParser.cpp @@ -32,19 +32,19 @@ namespace GamelistFileParser return nullptr; } - Utils::FileSystem::StringList pathList = Utils::FileSystem::getPathList(relative); + const Utils::FileSystem::StringList& pathList {Utils::FileSystem::getPathList(relative)}; auto path_it = pathList.begin(); - FileData* treeNode = root; - bool found = false; - while (path_it != pathList.end()) { - const std::unordered_map& children = - treeNode->getChildrenByFilename(); + FileData* treeNode {root}; + bool found {false}; - std::string key = *path_it; + while (path_it != pathList.end()) { + const std::unordered_map& children { + treeNode->getChildrenByFilename()}; + + const std::string key {*path_it}; found = children.find(key) != children.cend(); - if (found) { + if (found) treeNode = children.at(key); - } // This is the end. if (path_it == --pathList.end()) { @@ -58,8 +58,8 @@ namespace GamelistFileParser // Handle the special situation where a file exists and has an entry in the // gamelist.xml file but the file extension is not configured in es_systems.xml. - const std::vector extensions = - system->getSystemEnvData()->mSearchExtensions; + const std::vector& extensions { + system->getSystemEnvData()->mSearchExtensions}; if (std::find(extensions.cbegin(), extensions.cend(), Utils::FileSystem::getExtension(path)) == extensions.cend()) { @@ -69,7 +69,7 @@ namespace GamelistFileParser return nullptr; } - FileData* file = new FileData(type, path, system->getSystemEnvData(), system); + FileData* file {new FileData(type, path, system->getSystemEnvData(), system)}; // Skipping arcade assets from gamelist. if (!file->isArcadeAsset()) @@ -103,8 +103,8 @@ namespace GamelistFileParser void parseGamelist(SystemData* system) { - bool trustGamelist = Settings::getInstance()->getBool("ParseGamelistOnly"); - std::string xmlpath = system->getGamelistPath(false); + const bool trustGamelist {Settings::getInstance()->getBool("ParseGamelistOnly")}; + const std::string& xmlpath {system->getGamelistPath(false)}; if (!Utils::FileSystem::exists(xmlpath)) { LOG(LogDebug) << "GamelistFileParser::parseGamelist(): System \"" << system->getName() @@ -121,10 +121,10 @@ namespace GamelistFileParser pugi::xml_document doc; #if defined(_WIN64) - pugi::xml_parse_result result = - doc.load_file(Utils::String::stringToWideString(xmlpath).c_str()); + const pugi::xml_parse_result& result { + doc.load_file(Utils::String::stringToWideString(xmlpath).c_str())}; #else - pugi::xml_parse_result result = doc.load_file(xmlpath.c_str()); + const pugi::xml_parse_result& result {doc.load_file(xmlpath.c_str())}; #endif if (!result) { @@ -133,18 +133,18 @@ namespace GamelistFileParser return; } - pugi::xml_node root = doc.child("gameList"); + const pugi::xml_node& root {doc.child("gameList")}; if (!root) { LOG(LogError) << "Couldn't find node in gamelist \"" << xmlpath << "\""; return; } - pugi::xml_node alternativeEmulator = doc.child("alternativeEmulator"); + const pugi::xml_node& alternativeEmulator {doc.child("alternativeEmulator")}; if (alternativeEmulator) { - std::string label = alternativeEmulator.child("label").text().get(); + const std::string& label {alternativeEmulator.child("label").text().get()}; if (label != "") { - bool validLabel = false; - for (auto command : system->getSystemEnvData()->mLaunchCommands) { + bool validLabel {false}; + for (auto& command : system->getSystemEnvData()->mLaunchCommands) { if (command.second == label) validLabel = true; } @@ -165,18 +165,19 @@ namespace GamelistFileParser } } - std::string relativeTo = system->getStartPath(); - bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); + const std::string& relativeTo {system->getStartPath()}; + const bool showHiddenFiles {Settings::getInstance()->getBool("ShowHiddenFiles")}; - std::vector tagList = {"game", "folder"}; - FileType typeList[2] = {GAME, FOLDER}; - for (int i = 0; i < 2; ++i) { - std::string tag = tagList[i]; - FileType type = typeList[i]; - for (pugi::xml_node fileNode = root.child(tag.c_str()); fileNode; + const std::vector tagList {"game", "folder"}; + const FileType typeList[2] = {GAME, FOLDER}; + + for (int i {0}; i < 2; ++i) { + std::string tag {tagList[i]}; + FileType type {typeList[i]}; + for (pugi::xml_node fileNode {root.child(tag.c_str())}; fileNode; fileNode = fileNode.next_sibling(tag.c_str())) { - const std::string path = Utils::FileSystem::resolveRelativePath( - fileNode.child("path").text().get(), relativeTo, false); + const std::string& path {Utils::FileSystem::resolveRelativePath( + fileNode.child("path").text().get(), relativeTo, false)}; if (!trustGamelist && !Utils::FileSystem::exists(path)) { #if defined(_WIN64) @@ -199,7 +200,7 @@ namespace GamelistFileParser continue; } - FileData* file = findOrCreateFile(system, path, type); + FileData* file {findOrCreateFile(system, path, type)}; // Don't load entries with the wrong type. This should very rarely (if ever) happen. if (file != nullptr && ((tag == "game" && file->getType() == FOLDER) || @@ -214,7 +215,7 @@ namespace GamelistFileParser continue; } else if (!file->isArcadeAsset()) { - std::string defaultName = file->metadata.get("name"); + const std::string& defaultName {file->metadata.get("name")}; if (file->getType() == FOLDER) { file->metadata = MetaDataList::createFromXML(FOLDER_METADATA, fileNode, relativeTo); @@ -265,7 +266,7 @@ namespace GamelistFileParser SystemData* system) { // Create game and add to parent node. - pugi::xml_node newNode = parent.append_child(tag.c_str()); + pugi::xml_node newNode {parent.append_child(tag.c_str())}; // Write metadata. file->metadata.appendToXML(newNode, true, system->getStartPath()); @@ -303,17 +304,17 @@ namespace GamelistFileParser pugi::xml_document doc; pugi::xml_node root; - std::string xmlReadPath = system->getGamelistPath(false); - bool hasAlternativeEmulatorTag = false; + const std::string& xmlReadPath {system->getGamelistPath(false)}; + bool hasAlternativeEmulatorTag {false}; if (Utils::FileSystem::exists(xmlReadPath)) { // Parse an existing file first. #if defined(_WIN64) - pugi::xml_parse_result result = - doc.load_file(Utils::String::stringToWideString(xmlReadPath).c_str()); + const pugi::xml_parse_result& result { + doc.load_file(Utils::String::stringToWideString(xmlReadPath).c_str())}; #else - pugi::xml_parse_result result = doc.load_file(xmlReadPath.c_str()); + const pugi::xml_parse_result& result {doc.load_file(xmlReadPath.c_str())}; #endif if (!result) { @@ -329,7 +330,7 @@ namespace GamelistFileParser return; } if (updateAlternativeEmulator) { - pugi::xml_node alternativeEmulator = doc.child("alternativeEmulator"); + pugi::xml_node alternativeEmulator {doc.child("alternativeEmulator")}; if (alternativeEmulator) hasAlternativeEmulatorTag = true; @@ -340,7 +341,7 @@ namespace GamelistFileParser alternativeEmulator = doc.child("alternativeEmulator"); } - pugi::xml_node label = alternativeEmulator.child("label"); + const pugi::xml_node& label {alternativeEmulator.child("label")}; if (label && system->getAlternativeEmulator() != alternativeEmulator.child("label").text().get()) { @@ -360,7 +361,7 @@ namespace GamelistFileParser } else { if (updateAlternativeEmulator && system->getAlternativeEmulator() != "") { - pugi::xml_node alternativeEmulator = doc.prepend_child("alternativeEmulator"); + pugi::xml_node alternativeEmulator {doc.prepend_child("alternativeEmulator")}; alternativeEmulator.prepend_child("label").text().set( system->getAlternativeEmulator().c_str()); } @@ -372,14 +373,14 @@ namespace GamelistFileParser // through all our games and add the information from there. FileData* rootFolder {system->getRootFolder()}; if (rootFolder != nullptr) { - int numUpdated = 0; + int numUpdated {0}; // Get only files, no folders. - std::vector files = rootFolder->getFilesRecursive(GAME | FOLDER); + std::vector files {rootFolder->getFilesRecursive(GAME | FOLDER)}; // Iterate through all files, checking if they're already in the XML file. - for (std::vector::const_iterator fit = files.cbegin(); // Line break. + for (std::vector::const_iterator fit {files.cbegin()}; // Line break. fit != files.cend(); ++fit) { - const std::string tag = ((*fit)->getType() == GAME) ? "game" : "folder"; + const std::string& tag {((*fit)->getType() == GAME) ? "game" : "folder"}; // Do not touch if it wasn't changed and is not flagged for deletion. if (!(*fit)->metadata.wasChanged() && !(*fit)->getDeletionFlag()) @@ -387,18 +388,19 @@ namespace GamelistFileParser // Check if the file already exists in the XML file. // If it does, remove the entry before adding it back. - for (pugi::xml_node fileNode = root.child(tag.c_str()); fileNode; + for (pugi::xml_node fileNode {root.child(tag.c_str())}; fileNode; fileNode = fileNode.next_sibling(tag.c_str())) { - pugi::xml_node pathNode = fileNode.child("path"); + const pugi::xml_node& pathNode {fileNode.child("path")}; if (!pathNode) { LOG(LogError) << "<" << tag << "> node contains no child"; continue; } - std::string nodePath = + const std::string& nodePath { Utils::FileSystem::getCanonicalPath(Utils::FileSystem::resolveRelativePath( - pathNode.text().get(), system->getStartPath(), true)); - std::string gamePath = Utils::FileSystem::getCanonicalPath((*fit)->getPath()); + pathNode.text().get(), system->getStartPath(), true))}; + const std::string& gamePath { + Utils::FileSystem::getCanonicalPath((*fit)->getPath())}; if (nodePath == gamePath) { // Found it @@ -420,7 +422,7 @@ namespace GamelistFileParser // Now write the file. if (numUpdated > 0 || updateAlternativeEmulator) { // Make sure the folders leading up to this path exist (or the write will fail). - std::string xmlWritePath(system->getGamelistPath(true)); + const std::string& xmlWritePath {system->getGamelistPath(true)}; Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(xmlWritePath)); if (updateAlternativeEmulator) { diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index dee6fc7bb..001f644f4 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -40,8 +40,8 @@ FindRules::FindRules() void FindRules::loadFindRules() { - std::string customSystemsDirectory {Utils::FileSystem::getHomePath() + - "/.emulationstation/custom_systems"}; + const std::string& customSystemsDirectory {Utils::FileSystem::getHomePath() + + "/.emulationstation/custom_systems"}; std::string path {customSystemsDirectory + "/es_find_rules.xml"}; @@ -75,9 +75,10 @@ void FindRules::loadFindRules() pugi::xml_document doc; #if defined(_WIN64) - pugi::xml_parse_result res = doc.load_file(Utils::String::stringToWideString(path).c_str()); + const pugi::xml_parse_result& res { + doc.load_file(Utils::String::stringToWideString(path).c_str())}; #else - pugi::xml_parse_result res = doc.load_file(path.c_str()); + const pugi::xml_parse_result& res {doc.load_file(path.c_str())}; #endif if (!res) { @@ -86,7 +87,7 @@ void FindRules::loadFindRules() } // Actually read the file. - pugi::xml_node ruleList = doc.child("ruleList"); + const pugi::xml_node& ruleList {doc.child("ruleList")}; if (!ruleList) { LOG(LogError) << "es_find_rules.xml is missing the tag"; @@ -96,9 +97,9 @@ void FindRules::loadFindRules() EmulatorRules emulatorRules; CoreRules coreRules; - for (pugi::xml_node emulator = ruleList.child("emulator"); emulator; + for (pugi::xml_node emulator {ruleList.child("emulator")}; emulator; emulator = emulator.next_sibling("emulator")) { - std::string emulatorName = emulator.attribute("name").as_string(); + const std::string& emulatorName {emulator.attribute("name").as_string()}; if (emulatorName.empty()) { LOG(LogWarning) << "Found emulator tag without name attribute, skipping entry"; continue; @@ -109,7 +110,7 @@ void FindRules::loadFindRules() continue; } for (pugi::xml_node rule = emulator.child("rule"); rule; rule = rule.next_sibling("rule")) { - std::string ruleType = rule.attribute("type").as_string(); + const std::string& ruleType {rule.attribute("type").as_string()}; if (ruleType.empty()) { LOG(LogWarning) << "Found rule tag without type attribute for emulator \"" << emulatorName << "\", skipping entry"; @@ -125,18 +126,18 @@ void FindRules::loadFindRules() << emulatorName << "\", skipping entry"; continue; } - for (pugi::xml_node entry = rule.child("entry"); entry; + for (pugi::xml_node entry {rule.child("entry")}; entry; entry = entry.next_sibling("entry")) { - std::string entryValue = entry.text().get(); + const std::string& entryValue {entry.text().get()}; if (ruleType == "systempath") - emulatorRules.systemPaths.push_back(entryValue); + emulatorRules.systemPaths.emplace_back(entryValue); else if (ruleType == "staticpath") - emulatorRules.staticPaths.push_back(entryValue); + emulatorRules.staticPaths.emplace_back(entryValue); #if defined(_WIN64) else if (ruleType == "winregistrypath") - emulatorRules.winRegistryPaths.push_back(entryValue); + emulatorRules.winRegistryPaths.emplace_back(entryValue); else if (ruleType == "winregistryvalue") - emulatorRules.winRegistryValues.push_back(entryValue); + emulatorRules.winRegistryValues.emplace_back(entryValue); #endif } } @@ -149,8 +150,8 @@ void FindRules::loadFindRules() #endif } - for (pugi::xml_node core = ruleList.child("core"); core; core = core.next_sibling("core")) { - std::string coreName = core.attribute("name").as_string(); + for (pugi::xml_node core {ruleList.child("core")}; core; core = core.next_sibling("core")) { + const std::string& coreName {core.attribute("name").as_string()}; if (coreName.empty()) { LOG(LogWarning) << "Found core tag without name attribute, skipping entry"; continue; @@ -159,8 +160,8 @@ void FindRules::loadFindRules() LOG(LogWarning) << "Found repeating core tag \"" << coreName << "\", skipping entry"; continue; } - for (pugi::xml_node rule = core.child("rule"); rule; rule = rule.next_sibling("rule")) { - std::string ruleType = rule.attribute("type").as_string(); + for (pugi::xml_node rule {core.child("rule")}; rule; rule = rule.next_sibling("rule")) { + const std::string& ruleType {rule.attribute("type").as_string()}; if (ruleType.empty()) { LOG(LogWarning) << "Found rule tag without type attribute for core \"" << coreName << "\", skipping entry"; @@ -171,11 +172,11 @@ void FindRules::loadFindRules() << coreName << "\", skipping entry"; continue; } - for (pugi::xml_node entry = rule.child("entry"); entry; + for (pugi::xml_node entry {rule.child("entry")}; entry; entry = entry.next_sibling("entry")) { - std::string entryValue = entry.text().get(); + const std::string& entryValue {entry.text().get()}; if (ruleType == "corepath") - coreRules.corePaths.push_back(entryValue); + coreRules.corePaths.emplace_back(entryValue); } } mCores[coreName] = coreRules; @@ -265,13 +266,12 @@ void SystemData::setIsGameSystemStatus() bool SystemData::populateFolder(FileData* folder) { - const std::string& folderPath = folder->getPath(); - std::string filePath; std::string extension; - bool isGame; - bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); - Utils::FileSystem::StringList dirContent = Utils::FileSystem::getDirContent(folderPath); + const std::string& folderPath {folder->getPath()}; + const bool showHiddenFiles {Settings::getInstance()->getBool("ShowHiddenFiles")}; + const Utils::FileSystem::StringList& dirContent {Utils::FileSystem::getDirContent(folderPath)}; + bool isGame {false}; // If system directory exists but contains no games, return as error. if (dirContent.size() == 0) @@ -290,9 +290,10 @@ bool SystemData::populateFolder(FileData* folder) mFlattenFolders = true; } - for (Utils::FileSystem::StringList::const_iterator it = dirContent.cbegin(); + for (Utils::FileSystem::StringList::const_iterator it {dirContent.cbegin()}; it != dirContent.cend(); ++it) { filePath = *it; + const bool isDirectory {Utils::FileSystem::isDirectory(filePath)}; // Skip any recursive symlinks as those would hang the application at various places. if (Utils::FileSystem::isSymlink(filePath)) { @@ -306,8 +307,7 @@ bool SystemData::populateFolder(FileData* folder) // Skip hidden files and folders. if (!showHiddenFiles && Utils::FileSystem::isHidden(filePath)) { LOG(LogDebug) << "SystemData::populateFolder(): Skipping hidden " - << (Utils::FileSystem::isDirectory(filePath) ? "directory \"" : "file \"") - << filePath << "\""; + << (isDirectory ? "directory \"" : "file \"") << filePath << "\""; continue; } @@ -317,17 +317,18 @@ bool SystemData::populateFolder(FileData* folder) extension = Utils::FileSystem::getExtension(filePath); isGame = false; + if (std::find(mEnvData->mSearchExtensions.cbegin(), mEnvData->mSearchExtensions.cend(), extension) != mEnvData->mSearchExtensions.cend()) { - FileData* newGame = new FileData(GAME, filePath, mEnvData, this); + FileData* newGame {new FileData(GAME, filePath, mEnvData, this)}; // If adding a configured file extension to a directory it will get interpreted as // a regular file. This is useful for displaying multi-file/multi-disc games as single // entries or for emulators that can get directories passed to them as command line // parameters instead of regular files. In these instances we remove the extension // from the metadata name so it does not show up in the gamelists and similar. - if (Utils::FileSystem::isDirectory(filePath)) { - const std::string folderName = newGame->metadata.get("name"); + if (isDirectory) { + const std::string& folderName {newGame->metadata.get("name")}; newGame->metadata.set( "name", folderName.substr(0, folderName.length() - extension.length())); } @@ -343,15 +344,15 @@ bool SystemData::populateFolder(FileData* folder) } // Add directories that also do not match an extension as folders. - if (!isGame && Utils::FileSystem::isDirectory(filePath)) { + if (!isGame && isDirectory) { // Make sure that it's not a recursive symlink pointing to a location higher in the // hierarchy as the application would run forever trying to resolve the link. if (Utils::FileSystem::isSymlink(filePath)) { - const std::string canonicalPath {Utils::FileSystem::getCanonicalPath(filePath)}; - const std::string canonicalStartPath { + const std::string& canonicalPath {Utils::FileSystem::getCanonicalPath(filePath)}; + const std::string& canonicalStartPath { Utils::FileSystem::getCanonicalPath(mEnvData->mStartPath)}; if (canonicalPath.size() >= canonicalStartPath.size()) { - const std::string combinedPath { + const std::string& combinedPath { mEnvData->mStartPath + canonicalPath.substr(canonicalStartPath.size(), canonicalStartPath.size() - canonicalPath.size())}; @@ -384,9 +385,9 @@ bool SystemData::populateFolder(FileData* folder) void SystemData::indexAllGameFilters(const FileData* folder) { - const std::vector& children = folder->getChildren(); + const std::vector& children {folder->getChildren()}; - for (std::vector::const_iterator it = children.cbegin(); // Line break. + for (std::vector::const_iterator it {children.cbegin()}; // Line break. it != children.cend(); ++it) { switch ((*it)->getType()) { case GAME: @@ -405,10 +406,10 @@ std::vector readList(const std::string& str, const std::string& del { std::vector ret; - size_t prevOff = str.find_first_not_of(delims, 0); - size_t off = str.find_first_of(delims, prevOff); + size_t prevOff {str.find_first_not_of(delims, 0)}; + size_t off {str.find_first_of(delims, prevOff)}; while (off != std::string::npos || prevOff != std::string::npos) { - ret.push_back(str.substr(prevOff, off - prevOff)); + ret.emplace_back(str.substr(prevOff, off - prevOff)); prevOff = str.find_first_not_of(delims, off); off = str.find_first_of(delims, prevOff); @@ -430,12 +431,12 @@ bool SystemData::loadConfig() LOG(LogInfo) << "Only parsing the gamelist.xml files, not scanning system directories"; } - std::vector configPaths = getConfigPath(true); - const std::string rompath = FileData::getROMDirectory(); + const std::vector& configPaths {getConfigPath(true)}; + const std::string& rompath {FileData::getROMDirectory()}; - bool onlyProcessCustomFile = false; + bool onlyProcessCustomFile {false}; - for (auto configPath : configPaths) { + for (auto& configPath : configPaths) { // If the loadExclusive tag is present in the custom es_systems.xml file, then skip // processing of the bundled configuration file. if (onlyProcessCustomFile) @@ -450,10 +451,10 @@ bool SystemData::loadConfig() pugi::xml_document doc; #if defined(_WIN64) - pugi::xml_parse_result res = - doc.load_file(Utils::String::stringToWideString(configPath).c_str()); + const pugi::xml_parse_result& res { + doc.load_file(Utils::String::stringToWideString(configPath).c_str())}; #else - pugi::xml_parse_result res = doc.load_file(configPath.c_str()); + const pugi::xml_parse_result& res {doc.load_file(configPath.c_str())}; #endif if (!res) { @@ -461,7 +462,7 @@ bool SystemData::loadConfig() return true; } - pugi::xml_node loadExclusive = doc.child("loadExclusive"); + const pugi::xml_node& loadExclusive {doc.child("loadExclusive")}; if (loadExclusive) { if (configPath == configPaths.front() && configPaths.size() > 1) { LOG(LogInfo) << "Only loading custom file as the tag is present"; @@ -475,14 +476,14 @@ bool SystemData::loadConfig() } // Actually read the file. - pugi::xml_node systemList = doc.child("systemList"); + const pugi::xml_node& systemList {doc.child("systemList")}; if (!systemList) { LOG(LogError) << "es_systems.xml is missing the tag"; return true; } - for (pugi::xml_node system = systemList.child("system"); system; + for (pugi::xml_node system {systemList.child("system")}; system; system = system.next_sibling("system")) { std::string name; std::string fullname; @@ -545,7 +546,7 @@ bool SystemData::loadConfig() if (Utils::FileSystem::isSymlink(path)) { // Make sure that the symlink is not pointing to somewhere higher in the hierarchy // as that would lead to an infite loop, meaning the application would never start. - std::string resolvedRompath = Utils::FileSystem::getCanonicalPath(rompath); + const std::string& resolvedRompath {Utils::FileSystem::getCanonicalPath(rompath)}; if (resolvedRompath.find(Utils::FileSystem::getCanonicalPath(path)) == 0) { LOG(LogWarning) << "Skipping system \"" << name << "\" as the defined ROM directory \"" @@ -561,7 +562,7 @@ bool SystemData::loadConfig() // the label attribute needs to be set on all entries as it's a requirement for the // alternative emulator logic. std::vector> commands; - for (pugi::xml_node entry = system.child("command"); entry; + for (pugi::xml_node entry {system.child("command")}; entry; entry = entry.next_sibling("command")) { if (!entry.attribute("label")) { if (commands.size() == 1) { @@ -589,29 +590,29 @@ bool SystemData::loadConfig() << name << "\""; break; } - commands.push_back( + commands.emplace_back( std::make_pair(entry.text().get(), entry.attribute("label").as_string())); } // Platform ID list - const std::string platformList = - Utils::String::toLower(system.child("platform").text().get()); + const std::string& platformList { + Utils::String::toLower(system.child("platform").text().get())}; if (platformList == "") { LOG(LogWarning) << "No platform defined for system \"" << name << "\", scraper searches will be inaccurate"; } - std::vector platformStrs = readList(platformList); + const std::vector& platformStrs {readList(platformList)}; std::vector platformIds; for (auto it = platformStrs.cbegin(); it != platformStrs.cend(); ++it) { - std::string str = *it; - PlatformIds::PlatformId platformId = PlatformIds::getPlatformId(str); + std::string str {*it}; + const PlatformIds::PlatformId platformId {PlatformIds::getPlatformId(str)}; if (platformId == PlatformIds::PLATFORM_IGNORE) { // When platform is PLATFORM_IGNORE, do not allow other platforms. platformIds.clear(); - platformIds.push_back(platformId); + platformIds.emplace_back(platformId); break; } @@ -621,7 +622,7 @@ bool SystemData::loadConfig() LOG(LogWarning) << "Unknown platform \"" << str << "\" defined for system \"" << name << "\", scraper searches will be inaccurate"; else if (platformId != PlatformIds::PLATFORM_UNKNOWN) - platformIds.push_back(platformId); + platformIds.emplace_back(platformId); } // Theme folder. @@ -662,20 +663,20 @@ bool SystemData::loadConfig() #endif // Create the system runtime environment data. - SystemEnvironmentData* envData = new SystemEnvironmentData; + SystemEnvironmentData* envData {new SystemEnvironmentData}; envData->mStartPath = path; envData->mSearchExtensions = extensions; envData->mLaunchCommands = commands; envData->mPlatformIds = platformIds; - SystemData* newSys = new SystemData(name, fullname, sortName, envData, themeFolder); - bool onlyHidden = false; + SystemData* newSys {new SystemData(name, fullname, sortName, envData, themeFolder)}; + bool onlyHidden {false}; // If the option to show hidden games has been disabled, then check whether all // games for the system are hidden. That will flag the system as empty. if (!Settings::getInstance()->getBool("ShowHiddenGames")) { - std::vector recursiveGames = - newSys->getRootFolder()->getChildrenRecursive(); + std::vector recursiveGames { + newSys->getRootFolder()->getChildrenRecursive()}; onlyHidden = true; for (auto it = recursiveGames.cbegin(); it != recursiveGames.cend(); ++it) { if ((*it)->getType() != FOLDER) { @@ -692,7 +693,7 @@ bool SystemData::loadConfig() delete newSys; } else { - sSystemVector.push_back(newSys); + sSystemVector.emplace_back(newSys); } } } @@ -723,7 +724,7 @@ std::string SystemData::getLaunchCommandFromLabel(const std::string& label) void SystemData::deleteSystems() { - for (unsigned int i = 0; i < sSystemVector.size(); ++i) + for (unsigned int i {0}; i < sSystemVector.size(); ++i) delete sSystemVector.at(i); sSystemVector.clear(); @@ -734,8 +735,8 @@ std::vector SystemData::getConfigPath(bool legacyWarning) std::vector paths; if (legacyWarning) { - std::string legacyConfigFile = - Utils::FileSystem::getHomePath() + "/.emulationstation/es_systems.cfg"; + const std::string& legacyConfigFile {Utils::FileSystem::getHomePath() + + "/.emulationstation/es_systems.cfg"}; if (Utils::FileSystem::exists(legacyConfigFile)) { #if defined(_WIN64) @@ -751,8 +752,8 @@ std::vector SystemData::getConfigPath(bool legacyWarning) } } - std::string customSystemsDirectory = - Utils::FileSystem::getHomePath() + "/.emulationstation/custom_systems"; + const std::string& customSystemsDirectory {Utils::FileSystem::getHomePath() + + "/.emulationstation/custom_systems"}; if (!Utils::FileSystem::exists(customSystemsDirectory)) { LOG(LogInfo) << "Creating custom systems directory \"" << customSystemsDirectory << "\"..."; @@ -766,7 +767,7 @@ std::vector SystemData::getConfigPath(bool legacyWarning) if (Utils::FileSystem::exists(path)) { LOG(LogInfo) << "Found custom systems configuration file"; - paths.push_back(path); + paths.emplace_back(path); } #if defined(_WIN64) @@ -777,14 +778,14 @@ std::vector SystemData::getConfigPath(bool legacyWarning) path = ResourceManager::getInstance().getResourcePath(":/systems/unix/es_systems.xml", true); #endif - paths.push_back(path); + paths.emplace_back(path); return paths; } bool SystemData::createSystemDirectories() { - std::vector configPaths = getConfigPath(true); - const std::string rompath = FileData::getROMDirectory(); + std::vector configPaths {getConfigPath(true)}; + const std::string& rompath {FileData::getROMDirectory()}; bool onlyProcessCustomFile = false; @@ -831,13 +832,13 @@ bool SystemData::createSystemDirectories() // processing of the bundled configuration file. pugi::xml_document doc; #if defined(_WIN64) - pugi::xml_parse_result res = - doc.load_file(Utils::String::stringToWideString(configPaths.front()).c_str()); + const pugi::xml_parse_result& res { + doc.load_file(Utils::String::stringToWideString(configPaths.front()).c_str())}; #else - pugi::xml_parse_result res = doc.load_file(configPaths.front().c_str()); + const pugi::xml_parse_result& res {doc.load_file(configPaths.front().c_str())}; #endif if (res) { - pugi::xml_node loadExclusive = doc.child("loadExclusive"); + const pugi::xml_node& loadExclusive {doc.child("loadExclusive")}; if (loadExclusive) onlyProcessCustomFile = true; } @@ -849,7 +850,7 @@ bool SystemData::createSystemDirectories() std::vector> systemsVector; - for (auto configPath : configPaths) { + for (auto& configPath : configPaths) { // If the loadExclusive tag is present. if (onlyProcessCustomFile && configPath == configPaths.front()) continue; @@ -863,10 +864,10 @@ bool SystemData::createSystemDirectories() pugi::xml_document doc; #if defined(_WIN64) - pugi::xml_parse_result res = - doc.load_file(Utils::String::stringToWideString(configPath).c_str()); + const pugi::xml_parse_result& res { + doc.load_file(Utils::String::stringToWideString(configPath).c_str())}; #else - pugi::xml_parse_result res = doc.load_file(configPath.c_str()); + const pugi::xml_parse_result& res {doc.load_file(configPath.c_str())}; #endif if (!res) { @@ -876,14 +877,14 @@ bool SystemData::createSystemDirectories() } // Actually read the file. - pugi::xml_node systemList = doc.child("systemList"); + const pugi::xml_node& systemList {doc.child("systemList")}; if (!systemList) { LOG(LogError) << "es_systems.xml is missing the tag"; return true; } - for (pugi::xml_node system = systemList.child("system"); system; + for (pugi::xml_node system {systemList.child("system")}; system; system = system.next_sibling("system")) { std::string systemDir; std::string name; @@ -893,7 +894,7 @@ bool SystemData::createSystemDirectories() std::vector commands; std::string platform; std::string themeFolder; - const std::string systemInfoFileName = "/systeminfo.txt"; + const std::string systemInfoFileName {"/systeminfo.txt"}; bool replaceInfoFile = false; std::ofstream systemInfoFile; @@ -901,9 +902,9 @@ bool SystemData::createSystemDirectories() fullname = system.child("fullname").text().get(); path = system.child("path").text().get(); extensions = system.child("extension").text().get(); - for (pugi::xml_node entry = system.child("command"); entry; + for (pugi::xml_node entry {system.child("command")}; entry; entry = entry.next_sibling("command")) { - commands.push_back(entry.text().get()); + commands.emplace_back(entry.text().get()); } platform = Utils::String::toLower(system.child("platform").text().get()); themeFolder = system.child("theme").text().as_string(name.c_str()); @@ -1000,9 +1001,10 @@ bool SystemData::createSystemDirectories() systemsVector.erase(systemIter); if (configPaths.size() != 1 && configPath == configPaths.back()) - systemsVector.push_back(std::make_pair(systemDir + " (custom system)", fullname)); + systemsVector.emplace_back( + std::make_pair(systemDir + " (custom system)", fullname)); else - systemsVector.push_back(std::make_pair(systemDir, fullname)); + systemsVector.emplace_back(std::make_pair(systemDir, fullname)); if (replaceInfoFile) { LOG(LogInfo) << "Replaced existing system information file \"" @@ -1019,8 +1021,8 @@ bool SystemData::createSystemDirectories() // mappings between the system directory names and the full system names. This makes it // easier for the users to identify the correct directories for their games. if (!systemsVector.empty()) { - const std::string systemsFileName = "/systems.txt"; - bool systemsFileSuccess = true; + const std::string& systemsFileName {"/systems.txt"}; + bool systemsFileSuccess {true}; if (Utils::FileSystem::exists(rompath + systemsFileName)) { if (Utils::FileSystem::removeFile(rompath + systemsFileName)) @@ -1133,7 +1135,7 @@ std::string SystemData::getThemePath() const SystemData* SystemData::getRandomSystem(const SystemData* currentSystem) { - unsigned int total = 0; + int total {0}; for (auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); ++it) { if ((*it)->isGameSystem()) ++total; @@ -1142,15 +1144,15 @@ SystemData* SystemData::getRandomSystem(const SystemData* currentSystem) if (total < 2) return nullptr; - SystemData* randomSystem = nullptr; + SystemData* randomSystem {nullptr}; do { // Get a random number in range. std::random_device randDev; // Mersenne Twister pseudorandom number generator. std::mt19937 engine {randDev()}; - std::uniform_int_distribution uniform_dist(0, total - 1); - int target = uniform_dist(engine); + std::uniform_int_distribution uniform_dist {0, total - 1}; + int target {uniform_dist(engine)}; for (auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); ++it) { if ((*it)->isGameSystem()) { @@ -1171,8 +1173,8 @@ SystemData* SystemData::getRandomSystem(const SystemData* currentSystem) FileData* SystemData::getRandomGame(const FileData* currentGame, bool gameSelectorMode) { std::vector gameList; - bool onlyFolders = false; - bool hasFolders = false; + bool onlyFolders {false}; + bool hasFolders {false}; // If we're in the custom collection group list, then get the list of collections, // otherwise get a list of all the folder and file entries in the view. @@ -1203,7 +1205,7 @@ FileData* SystemData::getRandomGame(const FileData* currentGame, bool gameSelect // If this is a mixed view of folders and files, then remove all the folder entries // as we want to exclude them from the random selection. if (!onlyFolders && hasFolders) { - unsigned int i = 0; + unsigned int i {0}; do { if (gameList[i]->getType() == FOLDER) gameList.erase(gameList.begin() + i); @@ -1222,8 +1224,8 @@ FileData* SystemData::getRandomGame(const FileData* currentGame, bool gameSelect if (currentGame && currentGame->getType() == PLACEHOLDER) return nullptr; - unsigned int total = static_cast(gameList.size()); - int target = 0; + int total {static_cast(gameList.size())}; + int target {0}; if (total < 2) return nullptr; @@ -1233,7 +1235,7 @@ FileData* SystemData::getRandomGame(const FileData* currentGame, bool gameSelect std::random_device randDev; // Mersenne Twister pseudorandom number generator. std::mt19937 engine {randDev()}; - std::uniform_int_distribution uniform_dist(0, total - 1); + std::uniform_int_distribution uniform_dist {0, total - 1}; target = uniform_dist(engine); } while (currentGame && gameList.at(target) == currentGame); @@ -1245,7 +1247,7 @@ void SystemData::sortSystem(bool reloadGamelist, bool jumpToFirstRow) if (getName() == "recent") return; - bool favoritesSorting; + bool favoritesSorting {false}; if (this->isCustomCollection() || (this->isCollection() && this->getFullName() == "collections")) { @@ -1255,7 +1257,7 @@ void SystemData::sortSystem(bool reloadGamelist, bool jumpToFirstRow) favoritesSorting = Settings::getInstance()->getBool("FavoritesFirst"); } - FileData* rootFolder = getRootFolder(); + FileData* rootFolder {getRootFolder()}; // Assign the sort type to all grouped custom collections. if (mIsCollectionSystem && mFullName == "collections") { for (auto it = rootFolder->getChildren().begin(); // Line break. @@ -1289,7 +1291,7 @@ void SystemData::loadTheme() { mTheme = std::make_shared(); - std::string path {getThemePath()}; + const std::string& path {getThemePath()}; if (!Utils::FileSystem::exists(path)) { // No theme available for this platform. @@ -1364,7 +1366,7 @@ void SystemData::setupSystemSortType(FileData* rootFolder) { // If DefaultSortOrder is set to something, check that it is actually a valid value. if (Settings::getInstance()->getString("DefaultSortOrder") != "") { - for (unsigned int i = 0; i < FileSorts::SortTypes.size(); ++i) { + for (unsigned int i {0}; i < FileSorts::SortTypes.size(); ++i) { if (FileSorts::SortTypes.at(i).description == Settings::getInstance()->getString("DefaultSortOrder")) { rootFolder->setSortTypeString( diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 4ac86cd9b..de2341377 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -47,14 +47,14 @@ // build environment is broken. #if defined(__unix__) #if defined(ES_INSTALL_PREFIX) -std::string installPrefix = ES_INSTALL_PREFIX; +const std::string installPrefix {ES_INSTALL_PREFIX}; #else #if defined(__linux__) -std::string installPrefix = "/usr"; +const std::string installPrefix {"/usr"}; #elif defined(__NetBSD__) -std::string installPrefix = "/usr/pkg"; +const std::string installPrefix {"/usr/pkg"}; #else -std::string installPrefix = "/usr/local"; +const std::string installPrefix {"/usr/local"}; #endif #endif #endif @@ -63,12 +63,12 @@ namespace Utils { namespace FileSystem { - static std::string homePath = ""; - static std::string exePath = ""; + static std::string homePath; + static std::string exePath; StringList getDirContent(const std::string& path, const bool recursive) { - std::string genericPath = getGenericPath(path); + const std::string& genericPath {getGenericPath(path)}; StringList contentList; // Only parse the directory, if it's a directory. @@ -76,17 +76,19 @@ namespace Utils #if defined(_WIN64) WIN32_FIND_DATAW findData; - std::wstring wildcard = Utils::String::stringToWideString(genericPath) + L"/*"; - HANDLE hFind = FindFirstFileW(wildcard.c_str(), &findData); + const std::wstring& wildcard {Utils::String::stringToWideString(genericPath) + + L"/*"}; + const HANDLE hFind {FindFirstFileW(wildcard.c_str(), &findData)}; if (hFind != INVALID_HANDLE_VALUE) { // Loop over all files in the directory. do { - std::string name = Utils::String::wideStringToString(findData.cFileName); + const std::string& name { + Utils::String::wideStringToString(findData.cFileName)}; // Ignore "." and ".." if ((name != ".") && (name != "..")) { - std::string fullName(getGenericPath(genericPath + "/" + name)); - contentList.push_back(fullName); + const std::string& fullName {getGenericPath(genericPath + "/" + name)}; + contentList.emplace_back(fullName); if (recursive && isDirectory(fullName)) { contentList.sort(); @@ -97,18 +99,18 @@ namespace Utils FindClose(hFind); } #else - DIR* dir = opendir(genericPath.c_str()); + DIR* dir {opendir(genericPath.c_str())}; if (dir != nullptr) { struct dirent* entry; // Loop over all files in the directory. while ((entry = readdir(dir)) != nullptr) { - std::string name(entry->d_name); + const std::string& name(entry->d_name); // Ignore "." and ".." if ((name != ".") && (name != "..")) { - std::string fullName(getGenericPath(genericPath + "/" + name)); - contentList.push_back(fullName); + const std::string& fullName {getGenericPath(genericPath + "/" + name)}; + contentList.emplace_back(fullName); if (recursive && isDirectory(fullName)) { contentList.sort(); @@ -132,13 +134,13 @@ namespace Utils if (entry == std::string::npos) return files; - std::string parent {getParent(pattern)}; + const std::string& parent {getParent(pattern)}; // Don't allow wildcard matching for the parent directory. if (entry <= parent.size()) return files; - StringList dirContent {getDirContent(parent)}; + const StringList& dirContent {getDirContent(parent)}; if (dirContent.size() == 0) return files; @@ -172,19 +174,19 @@ namespace Utils StringList getPathList(const std::string& path) { StringList pathList; - std::string genericPath = getGenericPath(path); - size_t start = 0; - size_t end = 0; + const std::string& genericPath {getGenericPath(path)}; + size_t start {0}; + size_t end {0}; // Split at '/' while ((end = genericPath.find("/", start)) != std::string::npos) { if (end != start) - pathList.push_back(std::string(genericPath, start, end - start)); + pathList.emplace_back(std::string {genericPath, start, end - start}); start = end + 1; } // Add last folder / file to pathList. if (start != genericPath.size()) - pathList.push_back(std::string(genericPath, start, genericPath.size() - start)); + pathList.emplace_back(std::string {genericPath, start, genericPath.size() - start}); return pathList; } @@ -223,7 +225,7 @@ namespace Utils #else if (!homePath.length()) { - std::string envHome = getenv("HOME"); + const std::string& envHome {getenv("HOME")}; if (envHome.length()) homePath = getGenericPath(envHome); } @@ -260,17 +262,18 @@ namespace Utils // Ugly hack to compensate for the Flatpak sandbox restrictions. We traverse // this hardcoded list of paths and use the "which" command to check outside the // sandbox if the emulator binary exists. - std::string pathVariable {"/var/lib/flatpak/exports/bin:/usr/bin:/usr/local/" - "bin:/usr/local/sbin:/usr/sbin:/sbin:/bin:/usr/games:/usr/" - "local/games:/snap/bin:/var/lib/snapd/snap/bin"}; + const std::string& pathVariable { + "/var/lib/flatpak/exports/bin:/usr/bin:/usr/local/" + "bin:/usr/local/sbin:/usr/sbin:/sbin:/bin:/usr/games:/usr/" + "local/games:/snap/bin:/var/lib/snapd/snap/bin"}; - std::vector pathList { + const std::vector& pathList { Utils::String::delimitedStringToVector(pathVariable, ":")}; // Using a temporary file is the only viable solution I've found to communicate // between the sandbox and the outside world. - std::string tempFile {Utils::FileSystem::getHomePath() + "/.emulationstation/" + - ".flatpak_emulator_binary_path.tmp"}; + const std::string& tempFile {Utils::FileSystem::getHomePath() + "/.emulationstation/" + + ".flatpak_emulator_binary_path.tmp"}; std::string emulatorPath; @@ -293,9 +296,9 @@ namespace Utils return emulatorPath; #else - std::string pathVariable {std::string(getenv("PATH"))}; + const std::string& pathVariable {std::string {getenv("PATH")}}; - std::vector pathList { + const std::vector& pathList { Utils::String::delimitedStringToVector(pathVariable, ":")}; std::string pathTest; @@ -313,7 +316,7 @@ namespace Utils void setExePath(const std::string& path) { - constexpr int pathMax = 32767; + constexpr int pathMax {32767}; #if defined(_WIN64) std::wstring result(pathMax, 0); if (GetModuleFileNameW(nullptr, &result[0], pathMax) != 0) @@ -349,20 +352,22 @@ namespace Utils std::string getPreferredPath(const std::string& path) { - std::string preferredPath = path; #if defined(_WIN64) - size_t offset = std::string::npos; + std::string preferredPath {path}; + size_t offset {std::string::npos}; // Convert '/' to '\\' while ((offset = preferredPath.find('/')) != std::string::npos) preferredPath.replace(offset, 1, "\\"); +#else + const std::string& preferredPath {path}; #endif return preferredPath; } std::string getGenericPath(const std::string& path) { - std::string genericPath = path; - size_t offset = std::string::npos; + std::string genericPath {path}; + size_t offset {std::string::npos}; // Remove "\\\\?\\" if ((genericPath.find("\\\\?\\")) == 0) @@ -386,7 +391,7 @@ namespace Utils std::string getEscapedPath(const std::string& path) { - std::string escapedPath = getGenericPath(path); + std::string escapedPath {getGenericPath(path)}; #if defined(_WIN64) // Windows escapes stuff by just putting everything in quotes. @@ -396,12 +401,12 @@ namespace Utils return getPreferredPath(escapedPath); #else // Insert a backslash before most characters that would mess up a bash path. - const char* invalidChars = "\\ '\"!$^&*(){}[]?;<>"; - const char* invalidChar = invalidChars; + const char* invalidChars {"\\ '\"!$^&*(){}[]?;<>"}; + const char* invalidChar {invalidChars}; while (*invalidChar) { - size_t start = 0; - size_t offset = 0; + size_t start {0}; + size_t offset {0}; while ((offset = escapedPath.find(*invalidChar, start)) != std::string::npos) { start = offset + 1; @@ -423,17 +428,17 @@ namespace Utils if ((path[0] == ':') && (path[1] == '/')) return path; - std::string canonicalPath = exists(path) ? getAbsolutePath(path) : getGenericPath(path); + std::string canonicalPath {exists(path) ? getAbsolutePath(path) : getGenericPath(path)}; // Cleanup path. - bool scan = true; + bool scan {true}; while (scan) { - StringList pathList = getPathList(canonicalPath); + const StringList& pathList {getPathList(canonicalPath)}; canonicalPath.clear(); scan = false; - for (StringList::const_iterator it = pathList.cbegin(); it != pathList.cend(); + for (StringList::const_iterator it {pathList.cbegin()}; it != pathList.cend(); ++it) { // Ignore empty. if ((*it).empty()) @@ -458,7 +463,7 @@ namespace Utils #endif if (isSymlink(canonicalPath)) { - std::string resolved = resolveSymlink(canonicalPath); + const std::string& resolved {resolveSymlink(canonicalPath)}; if (resolved.empty()) return ""; @@ -481,8 +486,9 @@ namespace Utils std::string getAbsolutePath(const std::string& path, const std::string& base) { - std::string absolutePath = getGenericPath(path); - std::string baseVar = isAbsolute(base) ? getGenericPath(base) : getAbsolutePath(base); + const std::string& absolutePath {getGenericPath(path)}; + const std::string& baseVar {isAbsolute(base) ? getGenericPath(base) : + getAbsolutePath(base)}; return isAbsolute(absolutePath) ? absolutePath : getGenericPath(baseVar + "/" + absolutePath); @@ -490,8 +496,8 @@ namespace Utils std::string getParent(const std::string& path) { - std::string genericPath = getGenericPath(path); - size_t offset = std::string::npos; + std::string genericPath {getGenericPath(path)}; + size_t offset {std::string::npos}; // Find last '/' and erase it. if ((offset = genericPath.find_last_of('/')) != std::string::npos) @@ -503,13 +509,13 @@ namespace Utils std::string getFileName(const std::string& path) { - std::string genericPath = getGenericPath(path); - size_t offset = std::string::npos; + const std::string& genericPath {getGenericPath(path)}; + size_t offset {std::string::npos}; // Find last '/' and return the filename. if ((offset = genericPath.find_last_of('/')) != std::string::npos) return ((genericPath[offset + 1] == 0) ? "." : - std::string(genericPath, offset + 1)); + std::string {genericPath, offset + 1}); // No '/' found, entire path is a filename. return genericPath; @@ -517,8 +523,8 @@ namespace Utils std::string getStem(const std::string& path) { - std::string fileName = getFileName(path); - size_t offset = std::string::npos; + std::string fileName {getFileName(path)}; + size_t offset {std::string::npos}; // Empty fileName. if (fileName == ".") @@ -536,8 +542,8 @@ namespace Utils std::string getExtension(const std::string& path) { - std::string fileName = getFileName(path); - size_t offset = std::string::npos; + const std::string& fileName {getFileName(path)}; + size_t offset {std::string::npos}; // Empty fileName. if (fileName == ".") @@ -545,7 +551,7 @@ namespace Utils // Find last '.' and return the extension. if ((offset = fileName.find_last_of('.')) != std::string::npos) - return std::string(fileName, offset); + return std::string {fileName, offset}; // No '.' found, filename has no extension. return "."; @@ -561,9 +567,9 @@ namespace Utils const std::string& relativeTo, const bool allowHome) { - std::string genericPath = getGenericPath(path); - std::string relativeToVar = - isDirectory(relativeTo) ? getGenericPath(relativeTo) : getParent(relativeTo); + const std::string& genericPath {getGenericPath(path)}; + const std::string& relativeToVar {isDirectory(relativeTo) ? getGenericPath(relativeTo) : + getParent(relativeTo)}; // Nothing to resolve. if (!genericPath.length()) @@ -585,8 +591,8 @@ namespace Utils const std::string& relativeTo, const bool allowHome) { - bool contains = false; - std::string relativePath = removeCommonPath(path, relativeTo, contains); + bool contains {false}; + std::string relativePath {removeCommonPath(path, relativeTo, contains)}; if (contains) return ("./" + relativePath); @@ -604,9 +610,9 @@ namespace Utils const std::string& commonArg, bool& contains) { - std::string genericPath = getGenericPath(path); - std::string common = - isDirectory(commonArg) ? getGenericPath(commonArg) : getParent(commonArg); + const std::string& genericPath {getGenericPath(path)}; + const std::string& common {isDirectory(commonArg) ? getGenericPath(commonArg) : + getParent(commonArg)}; if (genericPath.find(common) == 0) { contains = true; @@ -619,7 +625,7 @@ namespace Utils std::string resolveSymlink(const std::string& path) { - std::string genericPath = getGenericPath(path); + const std::string& genericPath {getGenericPath(path)}; std::string resolved; #if defined(_WIN64) @@ -675,10 +681,10 @@ namespace Utils } #if defined(_WIN64) - std::ifstream sourceFile(Utils::String::stringToWideString(sourcePath).c_str(), - std::ios::binary); + std::ifstream sourceFile {Utils::String::stringToWideString(sourcePath).c_str(), + std::ios::binary}; #else - std::ifstream sourceFile(sourcePath, std::ios::binary); + std::ifstream sourceFile {sourcePath, std::ios::binary}; #endif if (sourceFile.fail()) { @@ -689,10 +695,10 @@ namespace Utils } #if defined(_WIN64) - std::ofstream targetFile(Utils::String::stringToWideString(destinationPath).c_str(), - std::ios::binary); + std::ofstream targetFile {Utils::String::stringToWideString(destinationPath).c_str(), + std::ios::binary}; #else - std::ofstream targetFile(destinationPath, std::ios::binary); + std::ofstream targetFile {destinationPath, std::ios::binary}; #endif if (targetFile.fail()) { @@ -744,7 +750,7 @@ namespace Utils bool removeFile(const std::string& path) { - std::string genericPath = getGenericPath(path); + const std::string& genericPath {getGenericPath(path)}; // Don't remove if it doesn't exists. if (!exists(genericPath)) @@ -798,7 +804,7 @@ namespace Utils bool createDirectory(const std::string& path) { - std::string genericPath = getGenericPath(path); + const std::string& genericPath {getGenericPath(path)}; if (exists(genericPath)) return true; @@ -812,7 +818,7 @@ namespace Utils #endif // Failed to create directory, try to create the parent. - std::string parent = getParent(genericPath); + const std::string& parent {getParent(genericPath)}; // Only try to create parent if it's not identical to genericPath. if (parent != genericPath) @@ -829,7 +835,7 @@ namespace Utils bool exists(const std::string& path) { - std::string genericPath = getGenericPath(path); + const std::string& genericPath {getGenericPath(path)}; #if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) struct stat info; @@ -846,7 +852,7 @@ namespace Utils bool driveExists(const std::string& path) { #if defined(_WIN64) - std::string genericPath = getGenericPath(path); + std::string genericPath {getGenericPath(path)}; // Try to add a dot or a backslash and a dot depending on how the drive // letter was defined by the user. if (genericPath.length() == 2 && genericPath.at(1) == ':') @@ -864,7 +870,7 @@ namespace Utils bool isAbsolute(const std::string& path) { - std::string genericPath = getGenericPath(path); + const std::string& genericPath {getGenericPath(path)}; #if defined(_WIN64) return ((genericPath.size() > 1) && (genericPath[1] == ':')); @@ -875,7 +881,7 @@ namespace Utils bool isRegularFile(const std::string& path) { - std::string genericPath = getGenericPath(path); + const std::string& genericPath {getGenericPath(path)}; #if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) struct stat info; @@ -897,7 +903,7 @@ namespace Utils bool isDirectory(const std::string& path) { - std::string genericPath = getGenericPath(path); + const std::string& genericPath {getGenericPath(path)}; #if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) struct stat info; @@ -919,12 +925,12 @@ namespace Utils bool isSymlink(const std::string& path) { - std::string genericPath = getGenericPath(path); + const std::string& genericPath {getGenericPath(path)}; #if defined(_WIN64) // Check for symlink attribute. - const DWORD Attributes = - GetFileAttributesW(Utils::String::stringToWideString(genericPath).c_str()); + const DWORD Attributes { + GetFileAttributesW(Utils::String::stringToWideString(genericPath).c_str())}; if ((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_REPARSE_POINT)) return true; @@ -952,12 +958,12 @@ namespace Utils bool isHidden(const std::string& path) { - std::string genericPath = getGenericPath(path); + const std::string& genericPath {getGenericPath(path)}; #if defined(_WIN64) // Check for hidden attribute. - const DWORD Attributes = - GetFileAttributesW(Utils::String::stringToWideString(genericPath).c_str()); + const DWORD Attributes { + GetFileAttributesW(Utils::String::stringToWideString(genericPath).c_str())}; if ((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_HIDDEN)) return true; #endif From fafb7c257f202f35f850cddb4ba7211beb14f269 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Wed, 14 Dec 2022 17:35:38 +0100 Subject: [PATCH 082/101] Fixed two issues with the 'Only show ROMs from gamelist.xml files' setting where an incorrect folder path would be shown in the metadata editor and folder links wouldn't work. --- es-app/src/GamelistFileParser.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/es-app/src/GamelistFileParser.cpp b/es-app/src/GamelistFileParser.cpp index 412014095..089a65876 100644 --- a/es-app/src/GamelistFileParser.cpp +++ b/es-app/src/GamelistFileParser.cpp @@ -87,9 +87,8 @@ namespace GamelistFileParser if (!system->getFlattenFolders()) { // Create missing folder. - FileData* folder {new FileData( - FOLDER, Utils::FileSystem::getStem(treeNode->getPath()) + "/" + *path_it, - system->getSystemEnvData(), system)}; + FileData* folder {new FileData(FOLDER, treeNode->getPath() + "/" + *path_it, + system->getSystemEnvData(), system)}; treeNode->addChild(folder); treeNode = folder; } From 715c1433241d79d6753db017a70b37331656a839 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Wed, 14 Dec 2022 17:44:44 +0100 Subject: [PATCH 083/101] Updated some log output text when deleting files from the metadata editor. --- es-app/src/guis/GuiGamelistOptions.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 7a6897336..3ec3a7390 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -403,29 +403,29 @@ void GuiGamelistOptions::openMetaDataEd() clearGameBtnFunc = [this, file] { #if defined(_WIN64) if (file->getType() == FOLDER) { - LOG(LogInfo) << "Deleting the media files and gamelist.xml entry for the folder \"" + LOG(LogInfo) << "Deleting media files and gamelist.xml entry for the folder \"" << Utils::String::replace(file->getFullPath(), "/", "\\") << "\""; } else if (file->getType() == GAME && Utils::FileSystem::isDirectory(file->getFullPath())) { - LOG(LogInfo) << "Deleting the media files and gamelist.xml entry for the " + LOG(LogInfo) << "Deleting media files and gamelist.xml entry for the " "file-interpreted folder \"" << Utils::String::replace(file->getFullPath(), "/", "\\") << "\""; } else { - LOG(LogInfo) << "Deleting the media files and gamelist.xml entry for the file \"" + LOG(LogInfo) << "Deleting media files and gamelist.xml entry for the file \"" << Utils::String::replace(file->getFullPath(), "/", "\\") << "\""; #else if (file->getType() == FOLDER) { - LOG(LogInfo) << "Deleting the media files and gamelist.xml entry for the folder \"" + LOG(LogInfo) << "Deleting media files and gamelist.xml entry for the folder \"" << file->getFullPath() << "\""; } else if (file->getType() == GAME && Utils::FileSystem::isDirectory(file->getFullPath())) { - LOG(LogInfo) << "Deleting the media files and gamelist.xml entry for the " + LOG(LogInfo) << "Deleting media files and gamelist.xml entry for the " "file-interpreted folder \"" << file->getFullPath() << "\""; } else { - LOG(LogInfo) << "Deleting the media files and gamelist.xml entry for the file \"" + LOG(LogInfo) << "Deleting media files and gamelist.xml entry for the file \"" << file->getFullPath() << "\""; #endif } @@ -473,8 +473,8 @@ void GuiGamelistOptions::openMetaDataEd() }; deleteGameBtnFunc = [this, file] { - LOG(LogInfo) << "Deleting the game file \"" << file->getFullPath() - << "\", all its media files and its gamelist.xml entry."; + LOG(LogInfo) << "Deleting game file \"" << file->getFullPath() + << "\", all its media files and its gamelist.xml entry"; CollectionSystemsManager::getInstance()->deleteCollectionFiles(file); ViewController::getInstance()->getGamelistView(file->getSystem()).get()->removeMedia(file); ViewController::getInstance()->getGamelistView(file->getSystem()).get()->remove(file, true); From d692e8f9cd8df2998dd9b2f966d4dd0b2329cc34 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Wed, 14 Dec 2022 17:56:50 +0100 Subject: [PATCH 084/101] Fixed a crash when attempting to use extension-less files in gamelists containing folders. --- es-app/src/GamelistFileParser.cpp | 13 +++++++++++++ es-app/src/SystemData.cpp | 5 +++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/es-app/src/GamelistFileParser.cpp b/es-app/src/GamelistFileParser.cpp index 089a65876..f3c316860 100644 --- a/es-app/src/GamelistFileParser.cpp +++ b/es-app/src/GamelistFileParser.cpp @@ -38,6 +38,19 @@ namespace GamelistFileParser bool found {false}; while (path_it != pathList.end()) { + // Workaround for an extremely rare issue that can basically only happen if a dot (.) + // has been defined as a valid extension for the system (meaning extension-less files + // are loaded), in combination with the "Only show ROMs from gamelist.xml files" option + // being enabled and a stale entry being present in the gamelist.xml file that perfectly + // matches a folder which is actually in use. The workaround is not a perfect solution + // but it at least prevents the application from crashing. + if (treeNode->getType() != FOLDER) { + LOG(LogWarning) + << "Invalid gamelist entry caused by folder having the same name as a stale " + << "extension-less game file (this may cause undefined behavior):"; + return nullptr; + } + const std::unordered_map& children { treeNode->getChildrenByFilename()}; diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 001f644f4..1102f2dcf 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -319,7 +319,8 @@ bool SystemData::populateFolder(FileData* folder) isGame = false; if (std::find(mEnvData->mSearchExtensions.cbegin(), mEnvData->mSearchExtensions.cend(), - extension) != mEnvData->mSearchExtensions.cend()) { + extension) != mEnvData->mSearchExtensions.cend() && + !(isDirectory && extension == ".")) { FileData* newGame {new FileData(GAME, filePath, mEnvData, this)}; // If adding a configured file extension to a directory it will get interpreted as @@ -327,7 +328,7 @@ bool SystemData::populateFolder(FileData* folder) // entries or for emulators that can get directories passed to them as command line // parameters instead of regular files. In these instances we remove the extension // from the metadata name so it does not show up in the gamelists and similar. - if (isDirectory) { + if (isDirectory && extension != ".") { const std::string& folderName {newGame->metadata.get("name")}; newGame->metadata.set( "name", folderName.substr(0, folderName.length() - extension.length())); From d9e2e41117311fe64fedbcca23f0e24576c2797d Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Wed, 14 Dec 2022 18:08:49 +0100 Subject: [PATCH 085/101] Added the . (dot) file extension to the xbox360 system on Windows to support extension-less XBLA games. --- resources/systems/unix/es_systems.xml | 2 +- resources/systems/windows/es_systems.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/systems/unix/es_systems.xml b/resources/systems/unix/es_systems.xml index 185d217d4..1f9c2fd19 100644 --- a/resources/systems/unix/es_systems.xml +++ b/resources/systems/unix/es_systems.xml @@ -1681,7 +1681,7 @@ xbox360 Microsoft Xbox 360 %ROMPATH%/xbox360 - .iso .ISO .xex .XEX + . .iso .ISO .xex .XEX PLACEHOLDER %ROM% xbox360 xbox360 diff --git a/resources/systems/windows/es_systems.xml b/resources/systems/windows/es_systems.xml index 946228722..b6c3bea2a 100644 --- a/resources/systems/windows/es_systems.xml +++ b/resources/systems/windows/es_systems.xml @@ -1684,7 +1684,7 @@ xbox360 Microsoft Xbox 360 %ROMPATH%\xbox360 - .iso .ISO .xex .XEX + . .iso .ISO .xex .XEX %STARTDIR%=%EMUDIR% %EMULATOR_XENIA% %ROM% xbox360 xbox360 From cfdfe3e6e049d5bc87c540536ac750d005279acd Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Wed, 14 Dec 2022 18:35:39 +0100 Subject: [PATCH 086/101] Updated a log warning about extensionless files. --- es-app/src/GamelistFileParser.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/es-app/src/GamelistFileParser.cpp b/es-app/src/GamelistFileParser.cpp index f3c316860..eebf13da1 100644 --- a/es-app/src/GamelistFileParser.cpp +++ b/es-app/src/GamelistFileParser.cpp @@ -39,7 +39,7 @@ namespace GamelistFileParser while (path_it != pathList.end()) { // Workaround for an extremely rare issue that can basically only happen if a dot (.) - // has been defined as a valid extension for the system (meaning extension-less files + // has been defined as a valid extension for the system (meaning extensionless files // are loaded), in combination with the "Only show ROMs from gamelist.xml files" option // being enabled and a stale entry being present in the gamelist.xml file that perfectly // matches a folder which is actually in use. The workaround is not a perfect solution @@ -47,7 +47,7 @@ namespace GamelistFileParser if (treeNode->getType() != FOLDER) { LOG(LogWarning) << "Invalid gamelist entry caused by folder having the same name as a stale " - << "extension-less game file (this may cause undefined behavior):"; + << "extensionless game file (this may cause undefined behavior):"; return nullptr; } From f4d4abb33be7fb6df1e1a907eb6f5a92878769f9 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Wed, 14 Dec 2022 20:17:41 +0100 Subject: [PATCH 087/101] Added theme support for defining relative brightness for images, videos and animations. --- es-core/src/GuiComponent.cpp | 14 ++++++++++++++ es-core/src/GuiComponent.h | 3 +++ es-core/src/ThemeData.cpp | 5 +++++ es-core/src/ThemeData.h | 7 ++++--- es-core/src/components/GIFAnimComponent.cpp | 1 + es-core/src/components/ImageComponent.cpp | 3 ++- es-core/src/components/LottieAnimComponent.cpp | 1 + es-core/src/components/VideoComponent.cpp | 2 ++ es-core/src/components/VideoFFmpegComponent.cpp | 1 + es-core/src/components/primary/CarouselComponent.h | 11 +++++++++++ es-core/src/components/primary/GridComponent.h | 11 +++++++++++ es-core/src/renderers/Renderer.h | 5 ++++- es-core/src/renderers/RendererOpenGL.cpp | 2 ++ es-core/src/renderers/ShaderOpenGL.cpp | 8 ++++++++ es-core/src/renderers/ShaderOpenGL.h | 2 ++ resources/shaders/glsl/core.glsl | 7 ++++++- resources/shaders/glsl/scanlines.glsl | 5 +++++ 17 files changed, 82 insertions(+), 6 deletions(-) diff --git a/es-core/src/GuiComponent.cpp b/es-core/src/GuiComponent.cpp index d66849627..4edf89f6b 100644 --- a/es-core/src/GuiComponent.cpp +++ b/es-core/src/GuiComponent.cpp @@ -29,6 +29,7 @@ GuiComponent::GuiComponent() , mOrigin {0.0f, 0.0f} , mRotationOrigin {0.5f, 0.5f} , mSize {0.0f, 0.0f} + , mBrightness {0.0f} , mOpacity {1.0f} , mSaturation {1.0f} , mDimming {1.0f} @@ -183,6 +184,16 @@ const int GuiComponent::getChildIndex() const return -1; } +void GuiComponent::setBrightness(float brightness) +{ + if (mBrightness == brightness) + return; + + mBrightness = brightness; + for (auto it = mChildren.cbegin(); it != mChildren.cend(); ++it) + (*it)->setBrightness(brightness); +} + void GuiComponent::setOpacity(float opacity) { if (mOpacity == opacity) @@ -371,6 +382,9 @@ void GuiComponent::applyTheme(const std::shared_ptr& theme, else setZIndex(getDefaultZIndex()); + if (properties & ThemeFlags::BRIGHTNESS && elem->has("brightness")) + mBrightness = glm::clamp(elem->get("brightness"), -2.0f, 2.0f); + if (properties & ThemeFlags::OPACITY && elem->has("opacity")) mThemeOpacity = glm::clamp(elem->get("opacity"), 0.0f, 1.0f); diff --git a/es-core/src/GuiComponent.h b/es-core/src/GuiComponent.h index 8727af152..968a5e9c3 100644 --- a/es-core/src/GuiComponent.h +++ b/es-core/src/GuiComponent.h @@ -222,8 +222,10 @@ public: virtual void stopGamelistFadeAnimations() {} virtual bool isListScrolling() { return false; } virtual void stopListScrolling() {} + virtual const float getBrightness() const { return mBrightness; } virtual const float getOpacity() const { return mOpacity; } virtual const float getColorOpacity() const { return 1.0f; } + virtual void setBrightness(float brightness); virtual void setOpacity(float opacity); virtual float getSaturation() const { return static_cast(mColor); } virtual void setSaturation(float saturation) { mSaturation = saturation; } @@ -336,6 +338,7 @@ protected: glm::vec2 mRotationOrigin; glm::vec2 mSize; + float mBrightness; float mOpacity; float mSaturation; float mDimming; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 6b1f40af1..3d25696c6 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -133,6 +133,7 @@ std::map> {"imageColor", COLOR}, {"imageColorEnd", COLOR}, {"imageGradientType", STRING}, + {"imageBrightness", FLOAT}, {"imageSaturation", FLOAT}, {"itemTransitions", STRING}, {"itemHorizontalAlignment", STRING}, @@ -185,6 +186,7 @@ std::map> {"imageColor", COLOR}, {"imageColorEnd", COLOR}, {"imageGradientType", STRING}, + {"imageBrightness", FLOAT}, {"imageSaturation", FLOAT}, {"backgroundImage", PATH}, {"backgroundRelativeScale", FLOAT}, @@ -260,6 +262,7 @@ std::map> {"colorEnd", COLOR}, {"gradientType", STRING}, {"scrollFadeIn", BOOLEAN}, + {"brightness", FLOAT}, {"opacity", FLOAT}, {"saturation", FLOAT}, {"visible", BOOLEAN}, @@ -286,6 +289,7 @@ std::map> {"delay", FLOAT}, {"fadeInTime", FLOAT}, {"scrollFadeIn", BOOLEAN}, + {"brightness", FLOAT}, {"opacity", FLOAT}, {"saturation", FLOAT}, {"visible", BOOLEAN}, @@ -304,6 +308,7 @@ std::map> {"direction", STRING}, {"keepAspectRatio", BOOLEAN}, {"interpolation", STRING}, + {"brightness", FLOAT}, {"opacity", FLOAT}, {"saturation", FLOAT}, {"visible", BOOLEAN}, diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index b912593f9..42af83e25 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -57,9 +57,10 @@ namespace ThemeFlags DELAY = 0x00002000, Z_INDEX = 0x00004000, ROTATION = 0x00008000, - OPACITY = 0x00010000, - SATURATION = 0x00020000, - VISIBLE = 0x00040000, + BRIGHTNESS = 0x00010000, + OPACITY = 0x00020000, + SATURATION = 0x00040000, + VISIBLE = 0x00080000, ALL = 0xFFFFFFFF }; // clang-format on diff --git a/es-core/src/components/GIFAnimComponent.cpp b/es-core/src/components/GIFAnimComponent.cpp index ccadfcb03..e6f39f9a6 100644 --- a/es-core/src/components/GIFAnimComponent.cpp +++ b/es-core/src/components/GIFAnimComponent.cpp @@ -495,6 +495,7 @@ void GIFAnimComponent::render(const glm::mat4& parentTrans) for (int i = 0; i < 4; ++i) vertices[i].position = glm::round(vertices[i].position); + vertices->brightness = mBrightness; vertices->saturation = mSaturation * mThemeSaturation; vertices->opacity = mOpacity * mThemeOpacity; vertices->dimming = mDimming; diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 489cb6092..9fec9d6e2 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -394,8 +394,9 @@ void ImageComponent::render(const glm::mat4& parentTrans) else fadeIn(mTexture->bind()); - mVertices->saturation = mSaturation * mThemeSaturation; + mVertices->brightness = mBrightness; mVertices->opacity = mThemeOpacity; + mVertices->saturation = mSaturation * mThemeSaturation; mVertices->dimming = mDimming; mVertices->reflectionsFalloff = mReflectionsFalloff; diff --git a/es-core/src/components/LottieAnimComponent.cpp b/es-core/src/components/LottieAnimComponent.cpp index fe32254c8..2c673478c 100644 --- a/es-core/src/components/LottieAnimComponent.cpp +++ b/es-core/src/components/LottieAnimComponent.cpp @@ -484,6 +484,7 @@ void LottieAnimComponent::render(const glm::mat4& parentTrans) for (int i = 0; i < 4; ++i) vertices[i].position = glm::round(vertices[i].position); + vertices->brightness = mBrightness; vertices->saturation = mSaturation * mThemeSaturation; vertices->opacity = mOpacity * mThemeOpacity; vertices->dimming = mDimming; diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index fc08db3ec..b996481d8 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -381,6 +381,8 @@ void VideoComponent::renderSnapshot(const glm::mat4& parentTrans) if (mStaticImagePath != "") { mStaticImage.setOpacity(mOpacity * mThemeOpacity); mStaticImage.setSaturation(mSaturation * mThemeSaturation); + if (mBrightness != 0.0f) + mStaticImage.setBrightness(mBrightness); if (mColorShift != 0xFFFFFFFF) mStaticImage.setColorShift(mColorShift); if (mColorShift != mColorShiftEnd) diff --git a/es-core/src/components/VideoFFmpegComponent.cpp b/es-core/src/components/VideoFFmpegComponent.cpp index 08279e73d..ab4c0ad4f 100644 --- a/es-core/src/components/VideoFFmpegComponent.cpp +++ b/es-core/src/components/VideoFFmpegComponent.cpp @@ -176,6 +176,7 @@ void VideoFFmpegComponent::render(const glm::mat4& parentTrans) if (mFadeIn < 1.0f || mThemeOpacity < 1.0f) vertices->opacity = mFadeIn * mThemeOpacity; + vertices->brightness = mBrightness; vertices->saturation = mSaturation * mThemeSaturation; vertices->dimming = mDimming; diff --git a/es-core/src/components/primary/CarouselComponent.h b/es-core/src/components/primary/CarouselComponent.h index 6d2467fe9..2ddfc63ab 100644 --- a/es-core/src/components/primary/CarouselComponent.h +++ b/es-core/src/components/primary/CarouselComponent.h @@ -147,6 +147,7 @@ private: unsigned int mImageColorShift; unsigned int mImageColorShiftEnd; bool mImageColorGradientHorizontal; + float mImageBrightness; float mImageSaturation; bool mInstantItemTransitions; Alignment mItemHorizontalAlignment; @@ -200,6 +201,7 @@ CarouselComponent::CarouselComponent() , mImageColorShift {0xFFFFFFFF} , mImageColorShiftEnd {0xFFFFFFFF} , mImageColorGradientHorizontal {true} + , mImageBrightness {0.0f} , mImageSaturation {1.0f} , mInstantItemTransitions {false} , mItemHorizontalAlignment {ALIGN_CENTER} @@ -265,6 +267,8 @@ void CarouselComponent::addEntry(Entry& entry, const std::shared_ptrsetMaxSize(glm::round(mItemSize * (mItemScale >= 1.0f ? mItemScale : 1.0f))); item->setImage(entry.data.imagePath); item->applyTheme(theme, "system", "", ThemeFlags::ALL); + if (mImageBrightness != 0.0) + item->setBrightness(mImageBrightness); if (mImageSaturation != 1.0) item->setSaturation(mImageSaturation); if (mImageColorShift != 0xFFFFFFFF) @@ -285,6 +289,8 @@ void CarouselComponent::addEntry(Entry& entry, const std::shared_ptr= 1.0f ? mItemScale : 1.0f))); defaultImage->setImage(entry.data.defaultImagePath); defaultImage->applyTheme(theme, "system", "", ThemeFlags::ALL); + if (mImageBrightness != 0.0) + defaultImage->setBrightness(mImageBrightness); if (mImageSaturation != 1.0) defaultImage->setSaturation(mImageSaturation); if (mImageColorShift != 0xFFFFFFFF) @@ -354,6 +360,8 @@ void CarouselComponent::updateEntry(Entry& entry, const std::shared_ptrsetMaxSize(glm::round(mItemSize * (mItemScale >= 1.0f ? mItemScale : 1.0f))); item->setImage(entry.data.imagePath); item->applyTheme(theme, "system", "", ThemeFlags::ALL); + if (mImageBrightness != 0.0) + item->setBrightness(mImageBrightness); if (mImageSaturation != 1.0) item->setSaturation(mImageSaturation); if (mImageColorShift != 0xFFFFFFFF) @@ -1124,6 +1132,9 @@ void CarouselComponent::applyTheme(const std::shared_ptr& theme, } } + if (elem->has("imageBrightness")) + mImageBrightness = glm::clamp(elem->get("imageBrightness"), -2.0f, 2.0f); + if (elem->has("imageSaturation")) mImageSaturation = glm::clamp(elem->get("imageSaturation"), 0.0f, 1.0f); diff --git a/es-core/src/components/primary/GridComponent.h b/es-core/src/components/primary/GridComponent.h index a9b4c48ca..0ee7b24df 100644 --- a/es-core/src/components/primary/GridComponent.h +++ b/es-core/src/components/primary/GridComponent.h @@ -148,6 +148,7 @@ private: unsigned int mImageColor; unsigned int mImageColorEnd; bool mImageColorGradientHorizontal; + float mImageBrightness; float mImageSaturation; std::unique_ptr mBackgroundImage; std::string mBackgroundImagePath; @@ -205,6 +206,7 @@ GridComponent::GridComponent() , mImageColor {0xFFFFFFFF} , mImageColorEnd {0xFFFFFFFF} , mImageColorGradientHorizontal {true} + , mImageBrightness {0.0f} , mImageSaturation {1.0f} , mBackgroundRelativeScale {1.0f} , mBackgroundColor {0xFFFFFFFF} @@ -258,6 +260,8 @@ void GridComponent::addEntry(Entry& entry, const std::shared_ptr& item->setCroppedSize(mItemSize * mImageRelativeScale); item->setImage(entry.data.imagePath); item->applyTheme(theme, "system", "", ThemeFlags::ALL); + if (mImageBrightness != 0.0) + item->setBrightness(mImageBrightness); if (mImageSaturation != 1.0) item->setSaturation(mImageSaturation); if (mImageColor != 0xFFFFFFFF) @@ -284,6 +288,8 @@ void GridComponent::addEntry(Entry& entry, const std::shared_ptr& defaultImage->setCroppedSize(mItemSize * mImageRelativeScale); defaultImage->setImage(entry.data.defaultImagePath); defaultImage->applyTheme(theme, "system", "", ThemeFlags::ALL); + if (mImageBrightness != 0.0) + defaultImage->setBrightness(mImageBrightness); if (mImageSaturation != 1.0) defaultImage->setSaturation(mImageSaturation); if (mImageColor != 0xFFFFFFFF) @@ -335,6 +341,8 @@ void GridComponent::updateEntry(Entry& entry, const std::shared_ptrsetCroppedSize(mItemSize * mImageRelativeScale); item->setImage(entry.data.imagePath); item->applyTheme(theme, "system", "", ThemeFlags::ALL); + if (mImageBrightness != 0.0) + item->setBrightness(mImageBrightness); if (mImageSaturation != 1.0) item->setSaturation(mImageSaturation); if (mImageColor != 0xFFFFFFFF) @@ -1027,6 +1035,9 @@ void GridComponent::applyTheme(const std::shared_ptr& theme, } } + if (elem->has("imageBrightness")) + mImageBrightness = glm::clamp(elem->get("imageBrightness"), -2.0f, 2.0f); + if (elem->has("imageSaturation")) mImageSaturation = glm::clamp(elem->get("imageSaturation"), 0.0f, 1.0f); diff --git a/es-core/src/renderers/Renderer.h b/es-core/src/renderers/Renderer.h index 8295b9f0f..4e51f2cd6 100644 --- a/es-core/src/renderers/Renderer.h +++ b/es-core/src/renderers/Renderer.h @@ -61,6 +61,7 @@ public: glm::vec2 texcoord; unsigned int color; glm::vec4 clipregion; + float brightness; float opacity; float saturation; float dimming; @@ -69,7 +70,8 @@ public: unsigned int shaderFlags; Vertex() - : opacity {1.0f} + : brightness {0.0f} + , opacity {1.0f} , saturation {1.0f} , dimming {1.0f} , reflectionsFalloff {0.0f} @@ -86,6 +88,7 @@ public: , texcoord(textureCoord) , color(color) , clipregion(clipRegion) + , brightness {0.0f} , opacity {1.0f} , saturation {1.0f} , dimming {1.0f} diff --git a/es-core/src/renderers/RendererOpenGL.cpp b/es-core/src/renderers/RendererOpenGL.cpp index cb7d611d7..adaa1dbff 100644 --- a/es-core/src/renderers/RendererOpenGL.cpp +++ b/es-core/src/renderers/RendererOpenGL.cpp @@ -443,6 +443,7 @@ void RendererOpenGL::drawTriangleStrips(const Vertex* vertices, GL_CHECK_ERROR(glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex) * numVertices, vertices, GL_DYNAMIC_DRAW)); mCoreShader->setClipRegion(vertices->clipregion); + mCoreShader->setBrightness(vertices->brightness); mCoreShader->setOpacity(vertices->opacity); mCoreShader->setSaturation(vertices->saturation); mCoreShader->setDimming(vertices->dimming); @@ -517,6 +518,7 @@ void RendererOpenGL::drawTriangleStrips(const Vertex* vertices, GL_CHECK_ERROR(glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex) * numVertices, vertices, GL_DYNAMIC_DRAW)); mScanlinelShader->setOpacity(vertices->opacity); + mScanlinelShader->setBrightness(vertices->brightness); mScanlinelShader->setSaturation(vertices->saturation); mScanlinelShader->setTextureSize({shaderWidth, shaderHeight}); GL_CHECK_ERROR(glDrawArrays(GL_TRIANGLE_STRIP, 0, numVertices)); diff --git a/es-core/src/renderers/ShaderOpenGL.cpp b/es-core/src/renderers/ShaderOpenGL.cpp index ec119407f..7d04d8c71 100644 --- a/es-core/src/renderers/ShaderOpenGL.cpp +++ b/es-core/src/renderers/ShaderOpenGL.cpp @@ -19,6 +19,7 @@ ShaderOpenGL::ShaderOpenGL() , mShaderTextureCoord {0} , mShaderColor {0} , mShaderTextureSize {0} + , mShaderBrightness {0} , mShaderOpacity {0} , mShaderSaturation {0} , mShaderDimming {0} @@ -122,6 +123,7 @@ void ShaderOpenGL::getVariableLocations(GLuint programID) mShaderColor = glGetAttribLocation(mProgramID, "colorVertex"); mShaderTextureSize = glGetUniformLocation(mProgramID, "textureSize"); mShaderClipRegion = glGetUniformLocation(mProgramID, "clipRegion"); + mShaderBrightness = glGetUniformLocation(mProgramID, "brightness"); mShaderOpacity = glGetUniformLocation(mProgramID, "opacity"); mShaderSaturation = glGetUniformLocation(mProgramID, "saturation"); mShaderDimming = glGetUniformLocation(mProgramID, "dimming"); @@ -166,6 +168,12 @@ void ShaderOpenGL::setClipRegion(glm::vec4 clipRegion) clipRegion[3])); } +void ShaderOpenGL::setBrightness(GLfloat brightness) +{ + if (mShaderBrightness != -1) + GL_CHECK_ERROR(glUniform1f(mShaderBrightness, brightness)); +} + void ShaderOpenGL::setOpacity(GLfloat opacity) { if (mShaderOpacity != -1) diff --git a/es-core/src/renderers/ShaderOpenGL.h b/es-core/src/renderers/ShaderOpenGL.h index 48a13daae..f8f594d3e 100644 --- a/es-core/src/renderers/ShaderOpenGL.h +++ b/es-core/src/renderers/ShaderOpenGL.h @@ -69,6 +69,7 @@ public: void setAttribPointers(); void setTextureSize(std::array shaderVec2); void setClipRegion(glm::vec4 clipRegion); + void setBrightness(GLfloat brightness); void setOpacity(GLfloat opacity); void setSaturation(GLfloat saturation); void setDimming(GLfloat dimming); @@ -95,6 +96,7 @@ private: GLint mShaderColor; GLint mShaderClipRegion; GLint mShaderTextureSize; + GLint mShaderBrightness; GLint mShaderOpacity; GLint mShaderSaturation; GLint mShaderDimming; diff --git a/resources/shaders/glsl/core.glsl b/resources/shaders/glsl/core.glsl index b918fe969..32de8cd6f 100644 --- a/resources/shaders/glsl/core.glsl +++ b/resources/shaders/glsl/core.glsl @@ -4,7 +4,7 @@ // core.glsl // // Core shader functionality: -// Clipping, saturation, opacity, dimming and reflections falloff. +// Clipping, brightness, saturation, opacity, dimming and reflections falloff. // // Vertex section of code: @@ -39,6 +39,7 @@ in vec2 texCoord; in vec4 color; uniform vec4 clipRegion; +uniform float brightness; uniform float saturation; uniform float opacity; uniform float dimming; @@ -70,6 +71,10 @@ void main() vec4 sampledColor = texture(textureSampler, texCoord); + // Brightness. + if (brightness != 0.0) + sampledColor.rgb = sampledColor.rgb * pow(2.0, brightness); + // Saturation. if (saturation != 1.0) { vec3 grayscale; diff --git a/resources/shaders/glsl/scanlines.glsl b/resources/shaders/glsl/scanlines.glsl index 2be2e9b71..aa24780e1 100644 --- a/resources/shaders/glsl/scanlines.glsl +++ b/resources/shaders/glsl/scanlines.glsl @@ -58,6 +58,7 @@ precision mediump float; uniform vec2 textureSize; uniform float opacity; +uniform float brightness; uniform float saturation; uniform sampler2D textureSampler; in vec2 texCoord; @@ -155,6 +156,10 @@ void main() vec4 colorTemp = clamp(GAMMA_OUT(color), 0.0, 1.0); + // Brightness. + if (brightness != 0.0) + colorTemp.rgb = colorTemp.rgb * pow(2.0, brightness); + // Saturation. if (saturation != 1.0) { vec3 grayscale; From 2e425b62af11276a59174a21fcb0b0802d638517 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Wed, 14 Dec 2022 20:26:00 +0100 Subject: [PATCH 088/101] Documentation update. --- CHANGELOG.md | 5 +++++ INSTALL-DEV.md | 6 ++++-- THEMES-DEV.md | 22 +++++++++++++++++++++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3de9013e..bd133cd51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ * Added support for the mugen system on Linux and macOS using the Ikemen GO game engine * Added CPCemu standalone as an alternative emulator for the amstradcpc system * Added MAME standalone as an alternative emulator for the gx4000 system +* Added the . (dot) file extension to the xbox360 system on Windows to support extensionless XBLA games * Added the .car and .rom file extensions to the a5200 system * Added the .car file extension to the atari800 system * Added the .bin file extension to the gx4000 system @@ -109,6 +110,7 @@ * Added support for defining the scrollable container speed, start delay and reset delay from the theme configuration * Added support for arbitrary aspect ratios for RatingComponent images and also added an overlay property * Added theme support for defining the opacity for most element types +* Added theme support for defining relative brightness for images, videos and animations * Added theme support for defining color saturation for images, videos and animations * Added theme support for defining the video fade-in time * Added theme support for enabling and disabling video pillarboxes and scanline rendering @@ -188,6 +190,7 @@ * Added a new itemAxisHorizontal property to the carousel to keep wheel items horizontal at all times * Added carousel theme support for setting the opacity for unfocused entries * Added carousel theme support for applying image color shifts +* Added carousel theme support for defining image brightness * Added carousel theme support for defining image saturation * Added carousel theme support for setting item transitions to "slide" or "instant" * Added carousel theme support for controlling item stacking for overlapping items @@ -220,12 +223,14 @@ ### Bug fixes * Multiple levels of symlinking in the ROMs directory tree could crash the application on startup +* Adding a dot (.) to the es_systems.xml extension tag (to setup extensionless entries) lead to a crash if the system contained folders * For the cps system, MAME standalone was configured with the wrong system directory for the -rompath option, pointing to "arcade" instead of "cps" * During some menu operations that reloaded the gamelist view, the cached background could miss some components as they were not rendered in time * Text wrapping did not work correctly for text that typically does not contain spaces, like Japanese * Changing some values using the metadata editor could lead to an incorrect sort order if the changes were done from within a grouped custom collection * Changing the setting "Group unthemed custom collections" could lead to incorrect custom collections sorting under some circumstances * For gamelists which mixed files and folders, the folder sorting was sometimes incorrect +* Incorrect folder paths were displayed in the metadata editor if the setting "Only show ROMs from gamelist.xml files" was enabled * Games located in subdirectories were not added back to custom collections when disabling the "Exclude from game counter" metadata option * Enabling and then disabling the "Exclude from game counter" metadata option would remove a game from all currently disabled custom collections * Navigation sounds for the trigger buttons would play when repeatedly pressed at the start or end of text lists diff --git a/INSTALL-DEV.md b/INSTALL-DEV.md index bfb869be3..2d5d81c6a 100644 --- a/INSTALL-DEV.md +++ b/INSTALL-DEV.md @@ -1165,6 +1165,8 @@ Wildcards are supported for emulator binaries, but not for directories: ~/App*/yuzu*.AppImage %ROM% ``` +There is a special case when it comes to file extensions where it's possible to use extensionless files if required. To accomplish this simply add a dot (.) to the list of extensions in the `` tag. Obviously this makes it impossible to use the _directories interpreted as files_ functionality as there is no file extension, but apart from that everything should work the same as for regular files. + Keep in mind that you have to set up your emulators separately from ES-DE as the es_systems.xml file assumes that your emulator environment is properly configured. Below is an overview of the file layout with various examples. For the command tag, the newer es_find_rules.xml logic described later in this document removes the need for most of the legacy options, but they are still supported for special configurations and for backward compatibility with old configuration files. @@ -1195,8 +1197,8 @@ Below is an overview of the file layout with various examples. For the command t All subdirectories (and non-recursive links) will be included. --> %ROMPATH%/snes - + .smc .SMC .sfc .SFC .swc .SWC .fig .FIG .bs .BS .bin .BIN .mgd .MGD .7z .7Z .zip .ZIP + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 4bbc7adb97849fa23136a6dfb84156cf80b235bd Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Thu, 15 Dec 2022 18:34:48 +0100 Subject: [PATCH 094/101] Documentation update. --- CHANGELOG.md | 2 ++ THEMES-DEV.md | 23 ++++++++++++++++++++++- USERGUIDE-DEV.md | 7 ------- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd133cd51..6abd06f34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -140,6 +140,7 @@ * Added scraping of fan art and updated the media viewer to display these images * Added scraping of box back covers when using TheGamesDB * If a wheel (marquee) image on ScreenScraper falls back to another region, then the wheel-hd image is now used instead if it matches the set region +* Removed scraping of arcade controller information using ScreenScraper as they have ruined this functionality * Added a ScreenScraper-specific option to remove dots from game name searches when using the multi-scraper in automatic mode * Moved the option "Scrape actual folders" higher up in the scraper options menu * Added the ability to set a manual sortname specifically for custom collections using the metadata editor @@ -178,6 +179,7 @@ * (Windows) Changed many logging entries to use backslashes instead of forward slashes as directory separators * Added the build date to to main menu for alpha and dev builds * Added a left trigger + right trigger help system icon and removed the deprecated hotkey icon +* Added an arcade twin stick controller badge icon * Moved all Platform functions to the utility namespace instead of using the global namespace * Implemented proper XML attribute support in ThemeData that eliminates the risk of name collisions * Added size restrictions to images and fonts so incorrect theme configuration would not lead to crashes or excessive memory utilization diff --git a/THEMES-DEV.md b/THEMES-DEV.md index 3df84c34e..b903cdd06 100644 --- a/THEMES-DEV.md +++ b/THEMES-DEV.md @@ -1491,6 +1491,10 @@ Properties: - Default is `false` * `gameselector` - type: STRING - If more than one gameselector element has been defined, this property makes it possible to state which one to use. If multiple gameselector elements have been defined and this property is missing then the first entry will be chosen and a warning message will be logged. If only a single gameselector has been defined, this property is ignored. The value of this property must match the `name` attribute value of the gameselector element. This property is only needed for the `system` view and only if the `imageType` property is utilized. +* `gameselectorEntry` - type: UNSIGNED_INTEGER + - This property which is only available in the `system` view makes it possible to select which `gameselector` entry to use to populate the `imageType` property. This assumes that a `gameCount` property for the gameselector element has been defined with a value higher than `1`. By defining multiple `image` elements with different values for the `gameselectorEntry` property it's possible to display multiple game entries at the same time, for example listing a couple of games that were last played, or a selection of random games. If the requested entry does not exist (for instance if `gameCount` has been set to 5 and `gameselectorEntry` has been set to `4` but the system only contains 3 games), then the overall element will not get rendered. Note that the first entry is defined as `0`, the second entry as `1` etc. + - Minimum value is `0` and maximum value is the value of the `gameselector` element property `gameCount` minus 1. If a value outside this range is defined, then it will be automatically clamped to a valid value. + - Default is `0` * `tile` - type: BOOLEAN - If true, the image will be tiled instead of stretched to fit its size. Useful for backgrounds. - Default is `false` @@ -1588,6 +1592,10 @@ Properties: - Default is `false` * `gameselector` - type: STRING - If more than one gameselector element has been defined, this property makes it possible to state which one to use. If multiple gameselector elements have been defined and this property is missing then the first entry will be chosen and a warning message will be logged. If only a single gameselector has been defined, this property is ignored. The value of this property must match the `name` attribute value of the gameselector element. +* `gameselectorEntry` - type: UNSIGNED_INTEGER + - This property which is only available in the `system` view makes it possible to select which `gameselector` entry to use to populate the `imageType` property and to use for playing the video stream. This assumes that a `gameCount` property for the gameselector element has been defined with a value higher than `1`. By defining multiple `video` elements with different values for the `gameselectorEntry` property it's possible to display multiple game entries at the same time, for example listing a couple of games that were last played, or a selection of random games. If the requested entry does not exist (for instance if `gameCount` has been set to 5 and `gameselectorEntry` has been set to `4` but the system only contains 3 games), then the overall element will not get rendered. Note that the first entry is defined as `0`, the second entry as `1` etc. + - Minimum value is `0` and maximum value is the value of the `gameselector` element property `gameCount` minus 1. If a value outside this range is defined, then it will be automatically clamped to a valid value. + - Default is `0` * `audio` - type: BOOLEAN - Whether to enable or disable audio playback for the video. For static videos in the gamelist view it's strongly recommended to set this to `false` if there is also a separate video element playing game videos. - Default is `true` @@ -1627,7 +1635,7 @@ Properties: - If enabled, a short fade-in animation will be applied when scrolling through games in the gamelist view. This animation is only applied to images and not to actual videos, so if no image metadata has been defined then this property has no effect. For this to work correctly the `delay` property also needs to be set. - Default is `false` * `brightness` - type: FLOAT - - Controls the relative level of brightness. This affects both the static image and the video stream. This is intended primarily for fine adjustments, for example if a color shift has been applied which may have lowered the overall brightness of the image. + - Controls the relative level of brightness. This affects both the static image and the video stream. This is intended primarily for fine adjustments, for example if a color shift has been applied which may have lowered the overall brightness of the image/video. - Minimum value is `-2` and maximum value is `2` - Default is `0` (no brightness adjustments applied) * `opacity` - type: FLOAT @@ -1788,6 +1796,7 @@ Properties: `gamepad_xbox`, `joystick_generic`, `joystick_arcade_no_buttons`, + `joystick_arcade_no_buttons_twin`, `joystick_arcade_1_button`, `joystick_arcade_2_buttons`, `joystick_arcade_3_buttons`, @@ -1893,6 +1902,10 @@ Properties: - Default is `false` * `gameselector` - type: STRING - If more than one gameselector element has been defined, this property makes it possible to state which one to use. If multiple gameselector elements have been defined and this property is missing then the first entry will be chosen and a warning message will be logged. If only a single gameselector has been defined, this property is ignored. The value of this property must match the `name` attribute value of the gameselector element. This property is only needed for the `system` view and only if the `metadata` property is utilized. +* `gameselectorEntry` - type: UNSIGNED_INTEGER + - This property which is only available in the `system` view makes it possible to select which `gameselector` entry to use to populate the `metadata` property. This assumes that a `gameCount` property for the gameselector element has been defined with a value higher than `1`. By defining multiple `text` elements with different values for the `gameselectorEntry` property it's possible to display multiple game entries at the same time, for example listing a couple of games that were last played, or a selection of random games. If the requested entry does not exist (for instance if `gameCount` has been set to 5 and `gameselectorEntry` has been set to `4` but the system only contains 3 games), then the overall element will not get rendered. Note that the first entry is defined as `0`, the second entry as `1` etc. + - Minimum value is `0` and maximum value is the value of the `gameselector` element property `gameCount` minus 1. If a value outside this range is defined, then it will be automatically clamped to a valid value. + - Default is `0` * `container` - type: BOOLEAN - Whether the text should be placed inside a scrollable container. Only available for the `gamelist` view. - Default is `false` @@ -1981,6 +1994,10 @@ Properties: - `lastplayed` - The time the game was last played. This will be displayed as a value relative to the current date and time by default, but can be overridden using the `displayRelative` property. * `gameselector` - type: STRING - If more than one gameselector element has been defined, this property makes it possible to state which one to use. If multiple gameselector elements have been defined and this property is missing then the first entry will be chosen and a warning message will be logged. If only a single gameselector has been defined, this property is ignored. The value of this property must match the `name` attribute value of the gameselector element. This property is only needed for the `system` view and only if the `metadata` property is utilized. +* `gameselectorEntry` - type: UNSIGNED_INTEGER + - This property which is only available in the `system` view makes it possible to select which `gameselector` entry to use to populate the `metadata` property. This assumes that a `gameCount` property for the gameselector element has been defined with a value higher than `1`. By defining multiple `datetime` elements with different values for the `gameselectorEntry` property it's possible to display multiple game entries at the same time, for example listing a couple of games that were last played, or a selection of random games. If the requested entry does not exist (for instance if `gameCount` has been set to 5 and `gameselectorEntry` has been set to `4` but the system only contains 3 games), then the overall element will not get rendered. Note that the first entry is defined as `0`, the second entry as `1` etc. + - Minimum value is `0` and maximum value is the value of the `gameselector` element property `gameCount` minus 1. If a value outside this range is defined, then it will be automatically clamped to a valid value. + - Default is `0` * `fontPath` - type: PATH - Path to a TrueType font (.ttf). * `fontSize` - type: FLOAT @@ -2113,6 +2130,10 @@ Properties: - Default is `0.5 0.5` * `gameselector` - type: STRING - If more than one gameselector element has been defined, this property makes it possible to state which one to use. If multiple gameselector elements have been defined and this property is missing then the first entry will be chosen and a warning message will be logged. If only a single gameselector has been defined, this property is ignored. The value of this property must match the `name` attribute value of the gameselector element. This property is only needed for the `system` view. +* `gameselectorEntry` - type: UNSIGNED_INTEGER + - This property which is only available in the `system` view makes it possible to select which `gameselector` entry to use to populate the rating value. This assumes that a `gameCount` property for the gameselector element has been defined with a value higher than `1`. By defining multiple `rating` elements with different values for the `gameselectorEntry` property it's possible to display multiple game entries at the same time, for example listing a couple of games that were last played, or a selection of random games. If the requested entry does not exist (for instance if `gameCount` has been set to 5 and `gameselectorEntry` has been set to `4` but the system only contains 3 games), then the overall element will not get rendered. Note that the first entry is defined as `0`, the second entry as `1` etc. + - Minimum value is `0` and maximum value is the value of the `gameselector` element property `gameCount` minus 1. If a value outside this range is defined, then it will be automatically clamped to a valid value. + - Default is `0` * `interpolation` - type: STRING - Interpolation method to use when scaling the images. Nearest neighbor (`nearest`) preserves sharp pixels and linear filtering (`linear`) makes the image smoother. The effect of this property is primarily visible for raster graphic images, but it has a limited effect also when using scalable vector graphics (SVG) images, and even more so if rotation is applied. - Valid values are `nearest` or `linear` diff --git a/USERGUIDE-DEV.md b/USERGUIDE-DEV.md index 3946733f4..826584ef5 100644 --- a/USERGUIDE-DEV.md +++ b/USERGUIDE-DEV.md @@ -1915,7 +1915,6 @@ Here's an overview of what's supported when using these scrapers: | Multi-language | Yes | No | | Game names | Yes | Yes | | Ratings | Yes | No | -| Controllers (arcade systems only) | Yes | No | | Other game metadata | Yes | Yes | | Videos | Yes | No | | Screenshots | Yes | Yes | @@ -1931,8 +1930,6 @@ The category **Other game metadata** includes Description, Release date, Develop The **Multi-language** support includes translated game genres and game descriptions for a number of languages. -**Controllers** is metadata indicating the controller type (it's not images of controllers). - **Physical media** means images of cartridges, diskettes, tapes, CD-ROMs etc. that were used to distribute the games. There are two approaches to scraping, either for a single game from the metadata editor, or for multiple games and systems using the multi-scraper. @@ -2103,10 +2100,6 @@ Whether to scrape the names of the games. This does not affect the actual files Downloads game ratings. -**Controllers (arcade systems only)** _(ScreenScraper only)_ - -Scrapes controller metadata which is used to set the correct controller badge. This is only available for MAME arcade games, for systems such as _arcade_, _mame_, _neogeo_, _fba_ etc. This is so because ScreenScraper does not seem to provide controller information for other platforms. The type of controllers that are scraped are _joystick_ (separated into entries from no buttons up to 6 buttons), _steering wheel_, _flight stick_, _spinner_, _trackball_ and _lightgun_. - **Other metadata** This includes the game description, release date, developer, publisher, genre and the number of players. From dcc99cf352e97ecdf3c8c312691946dedf8f1585 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Thu, 15 Dec 2022 18:54:28 +0100 Subject: [PATCH 095/101] (slate-DE) Fixed some minor issues in the theme engine test 1 variant. --- themes/slate-DE/theme_engine_test_1.xml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/themes/slate-DE/theme_engine_test_1.xml b/themes/slate-DE/theme_engine_test_1.xml index 2ca6ac26d..5609c013a 100644 --- a/themes/slate-DE/theme_engine_test_1.xml +++ b/themes/slate-DE/theme_engine_test_1.xml @@ -28,7 +28,7 @@
lastplayed - 3 + 1 random @@ -40,7 +40,6 @@ fanart, titlescreen selector_random nearest - true 0.8 0.8 3 @@ -52,7 +51,6 @@ cover selector_recent nearest - true 3