// SPDX-License-Identifier: MIT // // EmulationStation Desktop Edition // SystemView.cpp // // Main system view. // #include "views/SystemView.h" #include "Log.h" #include "Settings.h" #include "Sound.h" #include "UIModeController.h" #include "Window.h" #include "animations/LambdaAnimation.h" #include "guis/GuiMsgBox.h" #include "views/ViewController.h" #if defined(_WIN64) #include #endif namespace { // Buffer values for scrolling velocity (left, stopped, right). const int logoBuffersLeft[] = {-5, -2, -1}; const int logoBuffersRight[] = {1, 2, 5}; } // namespace SystemView::SystemView() : mCamOffset {0.0f} , mFadeOpacity {0.0f} , mUpdatedGameCount {false} , mViewNeedsReload {true} , mLegacyMode {false} { mCarousel = std::make_unique(); mSystemInfo = std::make_unique("SYSTEM INFO", Font::get(FONT_SIZE_SMALL), 0x33333300, ALIGN_CENTER); setSize(static_cast(Renderer::getScreenWidth()), static_cast(Renderer::getScreenHeight())); populate(); mCarousel->setCursorChangedCallback([&](const CursorState& state) { onCursorChanged(state); }); mCarousel->setCancelTransitionsCallback( [&] { ViewController::getInstance()->cancelViewTransitions(); }); } SystemView::~SystemView() { if (mLegacyMode) { // Delete any existing extras. for (auto& entry : mElements) { for (auto extra : entry.legacyExtras) delete extra; entry.legacyExtras.clear(); } } } void SystemView::populate() { auto themeSets = ThemeData::getThemeSets(); std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); assert(selectedSet != themeSets.cend()); mLegacyMode = selectedSet->second.capabilities.legacyTheme; for (auto it : SystemData::sSystemVector) { const std::shared_ptr& theme {it->getTheme()}; if (mViewNeedsReload) getViewElements(theme); if (mLegacyMode) { SystemViewElements elements; elements.name = it->getName(); elements.legacyExtras = ThemeData::makeExtras(theme, "system"); // Sort the extras by z-index. std::stable_sort( elements.legacyExtras.begin(), elements.legacyExtras.end(), [](GuiComponent* a, GuiComponent* b) { return b->getZIndex() > a->getZIndex(); }); mElements.emplace_back(std::move(elements)); } if (!mLegacyMode) { SystemViewElements elements; if (theme->hasView("system")) { elements.name = it->getName(); for (auto& element : theme->getViewElements("system").elements) { if (element.second.type == "image") { elements.imageComponents.emplace_back(std::make_unique()); elements.imageComponents.back()->setDefaultZIndex(30.0f); elements.imageComponents.back()->applyTheme(theme, "system", element.first, ThemeFlags::ALL); if (elements.imageComponents.back()->getMetadataField() != "") elements.imageComponents.back()->setScrollHide(true); elements.children.emplace_back(elements.imageComponents.back().get()); } else if (element.second.type == "text") { elements.textComponents.push_back(std::make_unique()); elements.textComponents.back()->setDefaultZIndex(40.0f); elements.textComponents.back()->applyTheme(theme, "system", element.first, ThemeFlags::ALL); if (elements.textComponents.back()->getMetadataField() != "") elements.textComponents.back()->setScrollHide(true); elements.children.emplace_back(elements.textComponents.back().get()); } } } elements.children.emplace_back(mCarousel.get()); elements.children.emplace_back(mSystemInfo.get()); std::stable_sort( elements.children.begin(), elements.children.end(), [](GuiComponent* a, GuiComponent* b) { return b->getZIndex() > a->getZIndex(); }); std::stable_sort(elements.imageComponents.begin(), elements.imageComponents.end(), [](const std::unique_ptr& a, const std::unique_ptr& b) { return b->getZIndex() > a->getZIndex(); }); std::stable_sort(elements.textComponents.begin(), elements.textComponents.end(), [](const std::unique_ptr& a, const std::unique_ptr& b) { return b->getZIndex() > a->getZIndex(); }); mElements.emplace_back(std::move(elements)); } CarouselComponent::Entry entry; entry.name = it->getName(); entry.object = it; mCarousel->addEntry(theme, entry); } if (mCarousel->getNumEntries() == 0) { // Something is wrong, there is not a single system to show, check if UI mode is not full. if (!UIModeController::getInstance()->isUIModeFull()) { Settings::getInstance()->setString("UIMode", "full"); mWindow->pushGui(new GuiMsgBox( getHelpStyle(), "The selected UI mode has nothing to show,\n returning to UI mode \"Full\"", "OK", nullptr)); } } } void SystemView::updateGameCount() { std::pair gameCount = mCarousel->getSelected()->getDisplayedGameCount(); std::stringstream ss; if (!mCarousel->getSelected()->isGameSystem()) ss << "CONFIGURATION"; else if (mCarousel->getSelected()->isCollection() && (mCarousel->getSelected()->getName() == "favorites")) ss << gameCount.first << " GAME" << (gameCount.first == 1 ? " " : "S"); // The "recent" gamelist has probably been trimmed after sorting, so we'll cap it at // its maximum limit of 50 games. else if (mCarousel->getSelected()->isCollection() && (mCarousel->getSelected()->getName() == "recent")) ss << (gameCount.first > 50 ? 50 : gameCount.first) << " GAME" << (gameCount.first == 1 ? " " : "S"); else ss << gameCount.first << " GAME" << (gameCount.first == 1 ? " " : "S ") << "(" << gameCount.second << " FAVORITE" << (gameCount.second == 1 ? ")" : "S)"); mSystemInfo->setText(ss.str()); } void SystemView::goToSystem(SystemData* system, bool animate) { mCarousel->setCursor(system); updateGameCount(); if (!animate) finishSystemAnimation(0); } bool SystemView::input(InputConfig* config, Input input) { if (input.value != 0) { if (config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_r && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) { LOG(LogDebug) << "SystemView::input(): Reloading all"; ViewController::getInstance()->reloadAll(); return true; } if (config->isMappedTo("a", input)) { mCarousel->stopScrolling(); ViewController::getInstance()->goToGamelist(mCarousel->getSelected()); NavigationSounds::getInstance().playThemeNavigationSound(SELECTSOUND); return true; } if (Settings::getInstance()->getBool("RandomAddButton") && (config->isMappedTo("leftthumbstickclick", input) || config->isMappedTo("rightthumbstickclick", input))) { // Get a random system and jump to it. NavigationSounds::getInstance().playThemeNavigationSound(SYSTEMBROWSESOUND); mCarousel->setCursor(SystemData::getRandomSystem(mCarousel->getSelected())); return true; } if (!UIModeController::getInstance()->isUIModeKid() && config->isMappedTo("back", input) && Settings::getInstance()->getBool("ScreensaverControls")) { if (!mWindow->isScreensaverActive()) { ViewController::getInstance()->stopScrolling(); ViewController::getInstance()->cancelViewTransitions(); mWindow->startScreensaver(); mWindow->renderScreensaver(); } return true; } } return mCarousel->input(config, input); } void SystemView::update(int deltaTime) { mCarousel->update(deltaTime); GuiComponent::update(deltaTime); } void SystemView::onCursorChanged(const CursorState& /*state*/) { // Update help style. updateHelpPrompts(); int scrollVelocity {mCarousel->getScrollingVelocity()}; float startPos {mCamOffset}; float posMax {static_cast(mCarousel->getNumEntries())}; float target {static_cast(mCarousel->getCursor())}; // Find the shortest path to the target. float endPos {target}; // Directly. float dist {fabs(endPos - startPos)}; if (fabs(target + posMax - startPos - scrollVelocity) < dist) endPos = target + posMax; // Loop around the end (0 -> max). if (fabs(target - posMax - startPos - scrollVelocity) < dist) endPos = target - posMax; // Loop around the start (max - 1 -> -1). std::string transition_style {Settings::getInstance()->getString("TransitionStyle")}; Animation* anim; if (transition_style == "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); if (f < 0) f += posMax; if (f >= posMax) f -= posMax; t += 1; if (t < 0.3f) this->mFadeOpacity = glm::mix(0.0f, 1.0f, glm::clamp(t / 0.2f + startFade, 0.0f, 1.0f)); else if (t < 0.7f) this->mFadeOpacity = 1.0f; else this->mFadeOpacity = glm::mix(1.0f, 0.0f, glm::clamp((t - 0.6f) / 0.3f, 0.0f, 1.0f)); if (t > 0.5f) this->mCamOffset = endPos; // Update the game count when the entire animation has been completed. if (mFadeOpacity == 1.0f) updateGameCount(); }, 500); } else if (transition_style == "slide") { mUpdatedGameCount = false; anim = new LambdaAnimation( [this, startPos, endPos, posMax](float t) { t -= 1; float f = glm::mix(startPos, endPos, t * t * t + 1); if (f < 0) f += posMax; if (f >= posMax) f -= posMax; this->mCamOffset = f; // Hack to make the game count being updated in the middle of the animation. bool update {false}; if (endPos == -1.0f && fabs(fabs(posMax) - fabs(mCamOffset)) > 0.5f && !mUpdatedGameCount) { update = true; } else if (endPos > posMax && fabs(endPos - posMax - fabs(mCamOffset)) < 0.5f && !mUpdatedGameCount) { update = true; } else if (fabs(fabs(endPos) - fabs(mCamOffset)) < 0.5f && !mUpdatedGameCount) { update = true; } if (update) { mUpdatedGameCount = true; updateGameCount(); } }, 500); } else { // Instant. updateGameCount(); anim = new LambdaAnimation( [this, startPos, endPos, posMax](float t) { t -= 1; float f = glm::mix(startPos, endPos, t * t * t + 1); if (f < 0) f += posMax; if (f >= posMax) f -= posMax; this->mCamOffset = endPos; }, 500); } setAnimation(anim, 0, nullptr, false, 0); } void SystemView::render(const glm::mat4& parentTrans) { if (mCarousel->getNumEntries() == 0) return; // Nothing to render. glm::mat4 trans {getTransform() * parentTrans}; // Adding texture loading buffers depending on scrolling speed and status. int bufferIndex {mCarousel->getScrollingVelocity() + 1}; Renderer::pushClipRect(glm::ivec2 {}, glm::ivec2 {static_cast(mSize.x), static_cast(mSize.y)}); for (int i = static_cast(mCamOffset) + logoBuffersLeft[bufferIndex]; i <= static_cast(mCamOffset) + logoBuffersRight[bufferIndex]; ++i) { int index {i}; while (index < 0) index += static_cast(mCarousel->getNumEntries()); while (index >= static_cast(mCarousel->getNumEntries())) index -= static_cast(mCarousel->getNumEntries()); if (mCarousel->isAnimationPlaying(0) || index == mCarousel->getCursor()) { glm::mat4 elementTrans {trans}; if (mCarousel->getType() == CarouselComponent::HORIZONTAL || mCarousel->getType() == CarouselComponent::HORIZONTAL_WHEEL) elementTrans = glm::translate(elementTrans, glm::vec3 {(i - mCamOffset) * mSize.x, 0.0f, 0.0f}); else elementTrans = glm::translate(elementTrans, glm::vec3 {0.0f, (i - mCamOffset) * mSize.y, 0.0f}); Renderer::pushClipRect( glm::ivec2 {static_cast(elementTrans[3].x), static_cast(elementTrans[3].y)}, glm::ivec2 {static_cast(mSize.x), static_cast(mSize.y)}); if (mLegacyMode && mElements.size() > static_cast(index)) { for (auto element : mElements[index].legacyExtras) element->render(elementTrans); } else if (mElements.size() > static_cast(index)) { for (auto child : mElements[index].children) { if (child == mCarousel.get()) { // Render black above anything lower than the zIndex of the carousel // if fade transitions are in use and we're transitioning. if (mFadeOpacity) renderFade(trans); child->render(trans); } else { child->render(elementTrans); } } } if (mLegacyMode) mSystemInfo->render(elementTrans); Renderer::popClipRect(); } } if (mLegacyMode) { if (mFadeOpacity) renderFade(trans); mCarousel->render(trans); } Renderer::popClipRect(); } std::vector SystemView::getHelpPrompts() { std::vector prompts; if (mCarousel->getType() == CarouselComponent::VERTICAL || mCarousel->getType() == CarouselComponent::VERTICAL_WHEEL) prompts.push_back(HelpPrompt("up/down", "choose")); else prompts.push_back(HelpPrompt("left/right", "choose")); prompts.push_back(HelpPrompt("a", "select")); if (Settings::getInstance()->getBool("RandomAddButton")) prompts.push_back(HelpPrompt("thumbstickclick", "random")); if (!UIModeController::getInstance()->isUIModeKid() && Settings::getInstance()->getBool("ScreensaverControls")) prompts.push_back(HelpPrompt("back", "screensaver")); return prompts; } HelpStyle SystemView::getHelpStyle() { HelpStyle style; style.applyTheme(mCarousel->getEntry(mCarousel->getCursor()).object->getTheme(), "system"); return style; } void SystemView::onThemeChanged(const std::shared_ptr& /*theme*/) { LOG(LogDebug) << "SystemView::onThemeChanged()"; mViewNeedsReload = true; populate(); } void SystemView::getViewElements(const std::shared_ptr& theme) { LOG(LogDebug) << "SystemView::getViewElements()"; if (theme->hasView("system")) mViewNeedsReload = false; else mViewNeedsReload = true; mCarousel->applyTheme(theme, "system", "carousel_systemcarousel", ThemeFlags::ALL); // System info bar. mSystemInfo->setSize(mSize.x, mSystemInfo->getFont()->getLetterHeight() * 2.2f); mSystemInfo->setPosition(0.0f, mCarousel->getPosition().y + mCarousel->getSize().y); mSystemInfo->setBackgroundColor(0xDDDDDDD8); mSystemInfo->setRenderBackground(true); mSystemInfo->setFont(Font::get(static_cast(0.035f * mSize.y), Font::getDefaultPath())); mSystemInfo->setColor(0x000000FF); mSystemInfo->setZIndex(49.0f); mSystemInfo->setDefaultZIndex(49.0f); const ThemeData::ThemeElement* sysInfoElem { theme->getElement("system", "text_systemInfo", "text")}; if (sysInfoElem) mSystemInfo->applyTheme(theme, "system", "text_systemInfo", ThemeFlags::ALL); } void SystemView::renderFade(const glm::mat4& trans) { unsigned int fadeColor {0x00000000 | static_cast(mFadeOpacity * 255.0f)}; Renderer::setMatrix(trans); Renderer::drawRect(0.0f, 0.0f, mSize.x, mSize.y, fadeColor, fadeColor); }