// SPDX-License-Identifier: MIT // // ES-DE Frontend // ViewController.cpp // // Handles overall system navigation including animations and transitions. // Creates the gamelist views and handles refresh and reloads of these when needed // (for example when metadata has been changed or when a list sorting has taken place). // Initiates the launching of games, calling FileData to do the actual launch. // Displays a dialog when there are no games found on startup. // #include "views/ViewController.h" #include "ApplicationUpdater.h" #include "AudioManager.h" #include "CollectionSystemsManager.h" #include "FileFilterIndex.h" #include "InputManager.h" #include "Log.h" #include "Scripting.h" #include "Settings.h" #include "Sound.h" #include "SystemData.h" #include "SystemView.h" #include "UIModeController.h" #include "Window.h" #include "animations/Animation.h" #include "animations/LambdaAnimation.h" #include "animations/MoveCameraAnimation.h" #include "guis/GuiApplicationUpdater.h" #include "guis/GuiMenu.h" #include "guis/GuiTextEditKeyboardPopup.h" #include "guis/GuiTextEditPopup.h" #include "utils/LocalizationUtil.h" #include "views/GamelistView.h" #include "views/SystemView.h" #if defined(__ANDROID__) #include "utils/PlatformUtilAndroid.h" #endif ViewController::ViewController() noexcept : mRenderer {Renderer::getInstance()} , mNoGamesMessageBox {nullptr} , mCurrentView {nullptr} , mPreviousView {nullptr} , mSkipView {nullptr} , mLastTransitionAnim {ViewTransitionAnimation::INSTANT} , mGameToLaunch {nullptr} , mCamera {Renderer::getIdentity()} , mSystemViewTransition {false} , mWrappedViews {false} , mFadeOpacity {0} , mCancelledTransition {false} , mNextSystem {false} , mWindowChangedWidth {0} , mWindowChangedHeight {0} { mState.viewing = ViewMode::NOTHING; mState.previouslyViewed = ViewMode::NOTHING; } ViewController* ViewController::getInstance() { static ViewController instance; return &instance; } void ViewController::setMenuColors() { if (Settings::getInstance()->getString("MenuColorScheme") == "light") { mMenuColorFrame = 0xEFEFEFFF; mMenuColorFrameLaunchScreen = 0xDFDFDFFF; mMenuColorFrameBusyComponent = 0xFFFFFFFF; mMenuColorPanelDimmed = 0x00000009; mMenuColorTitle = 0x555555FF; mMenuColorPrimary = 0x777777FF; mMenuColorSecondary = 0x888888FF; mMenuColorTertiary = 0x666666FF; mMenuColorRed = 0x992222FF; mMenuColorGreen = 0x449944FF; mMenuColorBlue = 0x222299FF; mMenuColorSelector = 0xFFFFFFFF; mMenuColorSeparators = 0xC6C7C6FF; mMenuColorBusyComponent = 0xB8B8B8FF; mMenuColorScrollIndicators = 0x888888FF; mMenuColorPopupText = 0x444444FF; mMenuColorButtonFocused = 0x777777FF; mMenuColorButtonTextFocused = 0xFFFFFFFF; mMenuColorButtonTextUnfocused = 0x777777FF; mMenuColorButtonFlatFocused = 0x878787FF; mMenuColorButtonFlatUnfocused = 0xDADADAFF; mMenuColorKeyboardModifier = 0xF26767FF; mMenuColorKeyboardCursorFocused = 0x777777FF; mMenuColorKeyboardCursorUnfocused = 0xC7C7C7FF; mMenuColorKeyboardText = 0x77777700; mMenuColorTextInputFrameFocused = 0xFFFFFFFF; mMenuColorTextInputFrameUnfocused = 0xFFFFFFFF; mMenuColorSliderKnobDisabled = 0xC9C9C9FF; mMenuColorDateTimeEditMarker = 0x00000022; mMenuColorDetectDeviceHeld = 0x44444400; } else if (Settings::getInstance()->getString("MenuColorScheme") == "darkred") { mMenuColorFrame = 0x191919FF; mMenuColorFrameLaunchScreen = 0x121212FF; mMenuColorFrameBusyComponent = 0x090909FF; mMenuColorPanelDimmed = 0x00000024; mMenuColorTitle = 0x909090FF; mMenuColorPrimary = 0x808080FF; mMenuColorSecondary = 0x939393FF; mMenuColorTertiary = 0x909090FF; mMenuColorRed = 0xCA3E3EFF; mMenuColorGreen = 0x449944FF; mMenuColorBlue = 0x4757ddff; mMenuColorSelector = 0x461816FF; mMenuColorSeparators = 0x303030FF; mMenuColorBusyComponent = 0x888888FF; mMenuColorScrollIndicators = 0x707070FF; mMenuColorPopupText = 0xBBBBBBFF; mMenuColorButtonFocused = 0x050505FF; mMenuColorButtonTextFocused = 0xAFAFAFFF; mMenuColorButtonTextUnfocused = 0x808080FF; mMenuColorButtonFlatFocused = 0x090909FF; mMenuColorButtonFlatUnfocused = 0x242424FF; mMenuColorKeyboardModifier = 0xC62F2FFF; mMenuColorKeyboardCursorFocused = 0xAAAAAAFF; mMenuColorKeyboardCursorUnfocused = 0x666666FF; mMenuColorKeyboardText = 0x92929200; mMenuColorTextInputFrameFocused = 0x090909FF; mMenuColorTextInputFrameUnfocused = 0x242424FF; mMenuColorSliderKnobDisabled = 0x393939FF; mMenuColorDateTimeEditMarker = 0xFFFFFF22; mMenuColorDetectDeviceHeld = 0x99999900; } else { mMenuColorFrame = 0x191919FF; mMenuColorFrameLaunchScreen = 0x121212FF; mMenuColorFrameBusyComponent = 0x090909FF; mMenuColorPanelDimmed = 0x00000024; mMenuColorTitle = 0x909090FF; mMenuColorPrimary = 0x808080FF; mMenuColorSecondary = 0x939393FF; mMenuColorTertiary = 0x909090FF; mMenuColorRed = 0xCA3E3EFF; mMenuColorGreen = 0x449944FF; mMenuColorBlue = 0x4757ddff; mMenuColorSelector = 0x000000FF; mMenuColorSeparators = 0x303030FF; mMenuColorBusyComponent = 0x888888FF; mMenuColorScrollIndicators = 0x707070FF; mMenuColorPopupText = 0xBBBBBBFF; mMenuColorButtonFocused = 0x050505FF; mMenuColorButtonTextFocused = 0xAFAFAFFF; mMenuColorButtonTextUnfocused = 0x808080FF; mMenuColorButtonFlatFocused = 0x090909FF; mMenuColorButtonFlatUnfocused = 0x242424FF; mMenuColorKeyboardModifier = 0xC62F2FFF; mMenuColorKeyboardCursorFocused = 0xAAAAAAFF; mMenuColorKeyboardCursorUnfocused = 0x666666FF; mMenuColorKeyboardText = 0x92929200; mMenuColorTextInputFrameFocused = 0x090909FF; mMenuColorTextInputFrameUnfocused = 0x242424FF; mMenuColorSliderKnobDisabled = 0x393939FF; mMenuColorDateTimeEditMarker = 0xFFFFFF22; mMenuColorDetectDeviceHeld = 0x99999900; } } void ViewController::legacyAppDataDialog() { const std::string upgradeMessage { "AS OF ES-DE 3.0 THE APPLICATION DATA DIRECTORY HAS CHANGED FROM \".emulationstation\" " "to \"ES-DE\"\nPLEASE RENAME YOUR CURRENT DATA DIRECTORY:\n" + #if defined(_WIN64) Utils::String::replace(Utils::FileSystem::getAppDataDirectory(), "/", "\\") + "\nTO THE FOLLOWING:\n" + Utils::String::replace( Utils::FileSystem::getParent(Utils::FileSystem::getAppDataDirectory()), "/", "\\") + "\\ES-DE"}; #else Utils::FileSystem::getAppDataDirectory() + "\nTO THE FOLLOWING:\n" + Utils::FileSystem::getParent(Utils::FileSystem::getAppDataDirectory()) + "/ES-DE"}; #endif mWindow->pushGui(new GuiMsgBox( HelpStyle(), upgradeMessage.c_str(), _("OK"), [] {}, "", nullptr, "", nullptr, nullptr, true, true, (mRenderer->getIsVerticalOrientation() ? 0.85f : 0.55f * (1.778f / mRenderer->getScreenAspectRatio())))); } void ViewController::migratedAppDataFilesDialog() { const std::string message {"SETTINGS HAVE BEEN MIGRATED FROM A LEGACY APPLICATION DATA " "DIRECTORY STRUCTURE, YOU NEED TO RESTART ES-DE TO APPLY " "THE CONFIGURATION"}; mWindow->pushGui(new GuiMsgBox( HelpStyle(), message.c_str(), "QUIT", [] { SDL_Event quit {}; quit.type = SDL_QUIT; SDL_PushEvent(&quit); }, "", nullptr, "", nullptr, nullptr, true, true, (mRenderer->getIsVerticalOrientation() ? 0.65f : 0.55f * (1.778f / mRenderer->getScreenAspectRatio())))); } void ViewController::unsafeUpgradeDialog() { const std::string upgradeMessage { _("IT SEEMS AS IF AN UNSAFE UPGRADE HAS BEEN MADE, POSSIBLY BY " "UNPACKING THE NEW RELEASE ON TOP OF THE OLD ONE? THIS MAY CAUSE " "VARIOUS PROBLEMS, SOME OF WHICH MAY NOT BE APPARENT IMMEDIATELY. " "MAKE SURE TO ALWAYS FOLLOW THE UPGRADE INSTRUCTIONS IN THE " "README.TXT FILE THAT CAN BE FOUND IN THE ES-DE DIRECTORY.")}; mWindow->pushGui(new GuiMsgBox( HelpStyle(), upgradeMessage.c_str(), _("OK"), [] {}, "", nullptr, "", nullptr, nullptr, true, true, (mRenderer->getIsVerticalOrientation() ? 0.85f : 0.55f * (1.778f / mRenderer->getScreenAspectRatio())))); } void ViewController::invalidSystemsFileDialog() { const std::string errorMessage {_("COULDN'T PARSE THE SYSTEMS CONFIGURATION FILE. " "IF YOU HAVE A CUSTOMIZED es_systems.xml FILE, THEN " "SOMETHING IS LIKELY WRONG WITH YOUR XML SYNTAX. " "IF YOU DON'T HAVE A CUSTOM SYSTEMS FILE, THEN THE " "ES-DE INSTALLATION IS BROKEN. SEE THE APPLICATION " "LOG FILE es_log.txt FOR ADDITIONAL INFO")}; mWindow->pushGui(new GuiMsgBox( HelpStyle(), errorMessage.c_str(), _("QUIT"), [] { SDL_Event quit {}; quit.type = SDL_QUIT; SDL_PushEvent(&quit); }, "", nullptr, "", nullptr, nullptr, true, true, (mRenderer->getIsVerticalOrientation() ? 0.85f : 0.55f * (1.778f / mRenderer->getScreenAspectRatio())))); } void ViewController::noGamesDialog() { #if defined(RETRODECK) mNoGamesErrorMessage = _("NO GAME WERE FOUND. PLEASE PLACE YOUR GAMES IN " "THE RETRODECK ROM DIRECTORY LOCATED IN:\n"); #elif defined(__ANDROID__) mNoGamesErrorMessage = _("NO GAME FILES WERE FOUND, PLEASE PLACE YOUR GAMES IN " "THE CONFIGURED ROM DIRECTORY. OPTIONALLY THE ROM " "DIRECTORY STRUCTURE CAN BE GENERATED WHICH WILL " "CREATE A TEXT FILE FOR EACH SYSTEM PROVIDING SOME " "INFORMATION SUCH AS THE SUPPORTED FILE EXTENSIONS.\n" "THIS IS THE CURRENTLY CONFIGURED ROM DIRECTORY:\n"); #else mNoGamesErrorMessage = _("NO GAME FILES WERE FOUND. EITHER PLACE YOUR GAMES IN " "THE CURRENTLY CONFIGURED ROM DIRECTORY OR CHANGE " "ITS PATH USING THE BUTTON BELOW. OPTIONALLY THE ROM " "DIRECTORY STRUCTURE CAN BE GENERATED WHICH WILL " "CREATE A TEXT FILE FOR EACH SYSTEM PROVIDING SOME " "INFORMATION SUCH AS THE SUPPORTED FILE EXTENSIONS.\n" "THIS IS THE CURRENTLY CONFIGURED ROM DIRECTORY:\n"); #endif #if defined(_WIN64) mRomDirectory = Utils::String::replace(FileData::getROMDirectory(), "/", "\\"); #else mRomDirectory = FileData::getROMDirectory(); #endif #if defined(RETRODECK) // Show a simple message with a "QUIT" option if RETRODECK is defined mNoGamesMessageBox = new GuiMsgBox( HelpStyle(), mNoGamesErrorMessage + mRomDirectory, _("QUIT"), [] { SDL_Event quit {}; quit.type = SDL_QUIT; SDL_PushEvent(&quit); } ); #else #if defined(__ANDROID__) mNoGamesMessageBox = new GuiMsgBox( HelpStyle(), mNoGamesErrorMessage + mRomDirectory, #else mNoGamesMessageBox = new GuiMsgBox( HelpStyle(), mNoGamesErrorMessage + mRomDirectory, _("CHANGE ROM DIRECTORY"), [this] { std::string currentROMDirectory; #if defined(_WIN64) currentROMDirectory = Utils::String::replace(FileData::getROMDirectory(), "/", "\\"); #else currentROMDirectory = FileData::getROMDirectory(); #endif if (Settings::getInstance()->getBool("VirtualKeyboard")) { mWindow->pushGui(new GuiTextEditKeyboardPopup( HelpStyle(), 0.0f, _("ENTER ROM DIRECTORY PATH"), currentROMDirectory, [this, currentROMDirectory](const std::string& newROMDirectory) { if (currentROMDirectory != newROMDirectory) { Settings::getInstance()->setString( "ROMDirectory", Utils::String::trim(newROMDirectory)); Settings::getInstance()->saveFile(); #if defined(_WIN64) mRomDirectory = Utils::String::replace(FileData::getROMDirectory(), "/", "\\"); #else mRomDirectory = FileData::getROMDirectory(); #endif mNoGamesMessageBox->changeText(mNoGamesErrorMessage + mRomDirectory); mWindow->pushGui(new GuiMsgBox( HelpStyle(), _("ROM DIRECTORY SETTING SAVED, RESTART " "THE APPLICATION TO RESCAN THE SYSTEMS"), _("OK"), nullptr, "", nullptr, "", nullptr, nullptr, true, true, (mRenderer->getIsVerticalOrientation() ? 0.66f : 0.42f * (1.778f / mRenderer->getScreenAspectRatio())))); } }, false, _("SAVE"), _("SAVE CHANGES?"), _("Currently configured path:"), currentROMDirectory, _("LOAD CURRENTLY CONFIGURED PATH"), _("CLEAR (LEAVE BLANK TO RESET TO DEFAULT PATH)"))); } else { mWindow->pushGui(new GuiTextEditPopup( HelpStyle(), _("ENTER ROM DIRECTORY PATH"), currentROMDirectory, [this](const std::string& newROMDirectory) { Settings::getInstance()->setString("ROMDirectory", Utils::String::trim(newROMDirectory)); Settings::getInstance()->saveFile(); #if defined(_WIN64) mRomDirectory = Utils::String::replace(FileData::getROMDirectory(), "/", "\\"); #else mRomDirectory = FileData::getROMDirectory(); #endif mNoGamesMessageBox->changeText(mNoGamesErrorMessage + mRomDirectory); mWindow->pushGui(new GuiMsgBox( HelpStyle(), _("ROM DIRECTORY SETTING SAVED, RESTART " "THE APPLICATION TO RESCAN THE SYSTEMS"), _("OK"), nullptr, "", nullptr, "", nullptr, nullptr, true, true, (mRenderer->getIsVerticalOrientation() ? 0.66f : 0.42f * (1.778f / mRenderer->getScreenAspectRatio())))); }, false, _("SAVE"), _("SAVE CHANGES?"), _("Currently configured path:"), currentROMDirectory, _("LOAD CURRENTLY CONFIGURED PATH"), _("CLEAR (LEAVE BLANK TO RESET TO DEFAULT PATH)"))); } }, #endif // __ANDROID__ _("CREATE DIRECTORIES"), [this] { mWindow->pushGui(new GuiMsgBox( HelpStyle(), _("THIS WILL CREATE DIRECTORIES FOR ALL THE " "GAME SYSTEMS DEFINED IN es_systems.xml\n\n" "THIS MAY CREATE A LOT OF FOLDERS SO IT'S " "ADVICED TO REMOVE THE ONES YOU DON'T NEED"), _("PROCEED"), [this] { if (!SystemData::createSystemDirectories()) { mWindow->pushGui(new GuiMsgBox( HelpStyle(), _("THE SYSTEM DIRECTORIES WERE SUCCESSFULLY " "GENERATED, EXIT THE APPLICATION AND PLACE " "YOUR GAMES IN THE NEW FOLDERS"), _("OK"), nullptr, "", nullptr, "", nullptr, nullptr, true, true, (mRenderer->getIsVerticalOrientation() ? 0.74f : 0.46f * (1.778f / mRenderer->getScreenAspectRatio())))); } else { mWindow->pushGui(new GuiMsgBox( HelpStyle(), _("ERROR CREATING THE SYSTEM DIRECTORIES, " "PERMISSION PROBLEMS OR DISK FULL?\n\n" "SEE THE LOG FILE FOR MORE DETAILS"), _("OK"), nullptr, "", nullptr, "", nullptr, nullptr, true, true, (mRenderer->getIsVerticalOrientation() ? 0.75f : 0.47f * (1.778f / mRenderer->getScreenAspectRatio())))); } }, _("CANCEL"), nullptr, "", nullptr, nullptr, false, true, (mRenderer->getIsVerticalOrientation() ? 0.78f : 0.50f * (1.778f / mRenderer->getScreenAspectRatio())))); }, _("QUIT"), [] { SDL_Event quit {}; quit.type = SDL_QUIT; SDL_PushEvent(&quit); }, #if defined(__ANDROID__) "", nullptr, nullptr, true, false, (mRenderer->getIsVerticalOrientation() ? 0.90f : 0.58f * (1.778f / mRenderer->getScreenAspectRatio()))); #else nullptr, true, false, (mRenderer->getIsVerticalOrientation() ? 0.90f : 0.62f * (1.778f / mRenderer->getScreenAspectRatio()))); #endif #endif // RetroDECK mWindow->pushGui(mNoGamesMessageBox); } void ViewController::invalidAlternativeEmulatorDialog() { cancelViewTransitions(); mWindow->pushGui(new GuiMsgBox(getHelpStyle(), _("AT LEAST ONE OF YOUR SYSTEMS HAS AN " "INVALID ALTERNATIVE EMULATOR CONFIGURED " "WITH NO MATCHING ENTRY IN THE SYSTEMS " "CONFIGURATION FILE, PLEASE REVIEW YOUR " "SETUP USING THE 'ALTERNATIVE EMULATORS' " "INTERFACE IN THE 'OTHER SETTINGS' MENU"), _("OK"), nullptr, "", nullptr, "", nullptr, nullptr, true, true, (mRenderer->getIsVerticalOrientation() ? 0.70f : 0.45f * (1.778f / mRenderer->getScreenAspectRatio())))); } void ViewController::updateAvailableDialog() { cancelViewTransitions(); std::string results {ApplicationUpdater::getInstance().getResultsString()}; ApplicationUpdater::Package package {ApplicationUpdater::getInstance().getPackageInfo()}; if (package.name != "") { LOG(LogDebug) << "ViewController::updateAvailableDialog(): Package filename \"" << package.filename << "\""; LOG(LogDebug) << "ViewController::updateAvailableDialog(): Package url \"" << package.url << "\""; LOG(LogDebug) << "ViewController::updateAvailableDialog(): Package md5 \"" << package.md5 << "\""; mWindow->pushGui(new GuiMsgBox( getHelpStyle(), results, _("UPDATE"), [this, package] { mWindow->pushGui(new GuiApplicationUpdater()); if (package.name != "LinuxAppImage" && package.name != "LinuxSteamDeckAppImage") { std::string upgradeMessage; if (package.name == "WindowsPortable") { upgradeMessage = _("THE APPLICATION UPDATER WILL DOWNLOAD THE LATEST PORTABLE WINDOWS " "RELEASE FOR YOU, BUT YOU WILL NEED TO MANUALLY PERFORM THE UPGRADE. " "SEE THE README.TXT FILE INSIDE THE DOWNLOADED ZIP FILE FOR " "INSTRUCTIONS ON HOW THIS IS ACCOMPLISHED. AS IS ALSO DESCRIBED IN " "THAT DOCUMENT, NEVER UNPACK A NEW RELEASE ON TOP OF AN OLD " "INSTALLATION AS THAT MAY BREAK THE APPLICATION."); } else if (package.name == "WindowsInstaller") { upgradeMessage = _("THE APPLICATION UPDATER WILL DOWNLOAD THE LATEST WINDOWS INSTALLER " "RELEASE FOR YOU, BUT YOU WILL NEED TO MANUALLY RUN IT TO PERFORM " "THE UPGRADE. WHEN DOING THIS, MAKE SURE THAT YOU ANSWER YES TO THE " "QUESTION OF WHETHER TO UNINSTALL THE OLD VERSION, OR YOU MAY " "END UP WITH A BROKEN SETUP."); } else if (package.name == "macOSApple" || package.name == "macOSIntel") { upgradeMessage = _("THE APPLICATION UPDATER WILL DOWNLOAD THE LATEST RELEASE FOR " "YOU, BUT YOU WILL NEED TO MANUALLY INSTALL THE DMG FILE TO PERFORM " "THE UPGRADE."); } mWindow->pushGui(new GuiMsgBox( getHelpStyle(), upgradeMessage.c_str(), _("OK"), [] {}, "", nullptr, "", nullptr, nullptr, true, true, (mRenderer->getIsVerticalOrientation() ? 0.85f : 0.535f * (1.778f / mRenderer->getScreenAspectRatio())))); } }, _("CANCEL"), [] { HttpReq::cleanupCurlMulti(); return; }, "", nullptr, nullptr, true, true, (mRenderer->getIsVerticalOrientation() ? 0.70f : 0.45f * (1.778f / mRenderer->getScreenAspectRatio())))); } else { mWindow->pushGui(new GuiMsgBox(getHelpStyle(), results, _("OK"), nullptr, "", nullptr, "", nullptr, nullptr, true, true, (mRenderer->getIsVerticalOrientation() ? 0.70f : 0.45f * (1.778f / mRenderer->getScreenAspectRatio())))); } } void ViewController::goToStart(bool playTransition) { // Needed to avoid segfaults during emergency shutdown. if (mRenderer->getSDLWindow() == nullptr) return; // If the system view does not exist, then create it. We do this here as it would // otherwise not be done if jumping directly into a specific game system on startup. if (!mSystemListView) getSystemListView(); // If a specific system is requested, go directly to its game list. auto requestedSystem = Settings::getInstance()->getString("StartupSystem"); if (requestedSystem != "") { for (auto it = SystemData::sSystemVector.cbegin(); // Line break. it != SystemData::sSystemVector.cend(); ++it) { if ((*it)->getName() == requestedSystem) { goToGamelist(*it); if (!playTransition) cancelViewTransitions(); return; } } // Requested system doesn't exist. Settings::getInstance()->setString("StartupSystem", ""); } // Get the first system entry. goToSystemView(getSystemListView()->getFirstSystem(), false); } void ViewController::ReloadAndGoToStart() { mWindow->renderSplashScreen(Window::SplashScreenState::RELOADING, 0.0f); reloadAll(); if (mState.viewing == ViewMode::GAMELIST) { goToSystemView(SystemData::sSystemVector.front(), false); goToSystem(SystemData::sSystemVector.front(), false); } else { goToSystem(SystemData::sSystemVector.front(), false); } } bool ViewController::isCameraMoving() { if (mCurrentView) { if (mCamera[3].x - -mCurrentView->getPosition().x != 0.0f || mCamera[3].y - -mCurrentView->getPosition().y != 0.0f) return true; } return false; } void ViewController::cancelViewTransitions() { if (mLastTransitionAnim == ViewTransitionAnimation::SLIDE) { if (isCameraMoving()) { mCamera[3].x = -mCurrentView->getPosition().x; mCamera[3].y = -mCurrentView->getPosition().y; stopAllAnimations(); } // mSkipView is used when skipping through the gamelists in quick succession. // Without this, the game video (or static image) would not get rendered during // the slide transition animation. else if (mSkipView) { mSkipView.reset(); mSkipView = nullptr; } } else if (mLastTransitionAnim == ViewTransitionAnimation::FADE) { if (isAnimationPlaying(0)) { finishAnimation(0); mCancelledTransition = true; mFadeOpacity = 0; mWindow->invalidateCachedBackground(); } } } void ViewController::stopScrolling() { if (mRenderer->getSDLWindow() == nullptr) return; mSystemListView->stopScrolling(); mCurrentView->stopListScrolling(); if (mSystemListView->isSystemAnimationPlaying(0)) mSystemListView->finishSystemAnimation(0); } int ViewController::getSystemId(SystemData* system) { std::vector& sysVec {SystemData::sSystemVector}; return static_cast(std::find(sysVec.cbegin(), sysVec.cend(), system) - sysVec.cbegin()); } void ViewController::restoreViewPosition() { if (mPreviousView) { glm::vec3 restorePosition {mPreviousView->getPosition()}; restorePosition.x = mWrapPreviousPositionX; mPreviousView->setPosition(restorePosition); mWrapPreviousPositionX = 0; mWrappedViews = false; } } void ViewController::goToSystemView(SystemData* system, bool playTransition) { bool applicationStartup {false}; if (mState.viewing == ViewMode::NOTHING) applicationStartup = true; // Restore the X position for the view, if it was previously moved. if (mWrappedViews) restoreViewPosition(); if (mPreviousView) { mPreviousView.reset(); mPreviousView = nullptr; } if (mCurrentView != nullptr) mCurrentView->onTransition(); mPreviousView = mCurrentView; if (system->isGroupedCustomCollection()) system = system->getRootFolder()->getParent()->getSystem(); mState.previouslyViewed = mState.viewing; mState.viewing = ViewMode::SYSTEM_SELECT; mState.system = system; mSystemViewTransition = true; auto systemList = getSystemListView(); systemList->setPosition(getSystemId(system) * Renderer::getScreenWidth(), systemList->getPosition().y); systemList->goToSystem(system, false); mCurrentView = systemList; mCurrentView->onShow(); // Application startup animation. if (applicationStartup) { const ViewTransitionAnimation transitionAnim {static_cast( Settings::getInstance()->getInt("TransitionsStartupToSystem"))}; mCamera = glm::translate(mCamera, glm::round(-mCurrentView->getPosition())); if (transitionAnim == ViewTransitionAnimation::SLIDE) { if (getSystemListView()->getPrimaryType() == SystemView::PrimaryType::CAROUSEL) { if (getSystemListView()->getCarouselType() == CarouselComponent::CarouselType::HORIZONTAL || getSystemListView()->getCarouselType() == CarouselComponent::CarouselType::HORIZONTAL_WHEEL) mCamera[3].y += Renderer::getScreenHeight(); else mCamera[3].x -= Renderer::getScreenWidth(); } else if (getSystemListView()->getPrimaryType() == SystemView::PrimaryType::TEXTLIST || getSystemListView()->getPrimaryType() == SystemView::PrimaryType::GRID) { mCamera[3].y += Renderer::getScreenHeight(); } updateHelpPrompts(); } else if (transitionAnim == ViewTransitionAnimation::FADE) { if (getSystemListView()->getPrimaryType() == SystemView::PrimaryType::CAROUSEL) { if (getSystemListView()->getCarouselType() == CarouselComponent::CarouselType::HORIZONTAL || getSystemListView()->getCarouselType() == CarouselComponent::CarouselType::HORIZONTAL_WHEEL) mCamera[3].y += Renderer::getScreenHeight(); else mCamera[3].x += Renderer::getScreenWidth(); } else if (getSystemListView()->getPrimaryType() == SystemView::PrimaryType::TEXTLIST || getSystemListView()->getPrimaryType() == SystemView::PrimaryType::GRID) { mCamera[3].y += Renderer::getScreenHeight(); } } else { updateHelpPrompts(); } } if (applicationStartup) playViewTransition(ViewTransition::STARTUP_TO_SYSTEM); else if (playTransition) playViewTransition(ViewTransition::GAMELIST_TO_SYSTEM); else playViewTransition(ViewTransition::GAMELIST_TO_SYSTEM, true); } void ViewController::goToSystem(SystemData* system, bool animate) { mSystemListView->goToSystem(system, animate); } void ViewController::goToNextGamelist() { assert(mState.viewing == ViewMode::GAMELIST); SystemData* system {getState().getSystem()}; assert(system); NavigationSounds::getInstance().playThemeNavigationSound(QUICKSYSSELECTSOUND); mNextSystem = true; goToGamelist(system->getNext()); } void ViewController::goToPrevGamelist() { assert(mState.viewing == ViewMode::GAMELIST); SystemData* system {getState().getSystem()}; assert(system); NavigationSounds::getInstance().playThemeNavigationSound(QUICKSYSSELECTSOUND); mNextSystem = false; goToGamelist(system->getPrev()); } void ViewController::goToGamelist(SystemData* system) { bool wrapFirstToLast {false}; bool wrapLastToFirst {false}; bool slideTransitions {false}; bool fadeTransitions {false}; // Special case where we moved to another gamelist while the system to gamelist animation // was still playing, in this case we need to explictly call onHide() so that all system view // videos are stopped. if (mState.previouslyViewed == ViewMode::SYSTEM_SELECT && mState.viewing == ViewMode::GAMELIST && isAnimationPlaying(0)) { getSystemListView()->onHide(); } if (mCurrentView != nullptr) mCurrentView->onTransition(); ViewTransition transitionType; ViewTransitionAnimation transitionAnim; if (mState.viewing == ViewMode::SYSTEM_SELECT) { transitionType = ViewTransition::SYSTEM_TO_GAMELIST; transitionAnim = static_cast( Settings::getInstance()->getInt("TransitionsSystemToGamelist")); } else if (mState.viewing == ViewMode::NOTHING) { transitionType = ViewTransition::STARTUP_TO_GAMELIST; transitionAnim = static_cast( Settings::getInstance()->getInt("TransitionsStartupToGamelist")); } else { transitionType = ViewTransition::GAMELIST_TO_GAMELIST; transitionAnim = static_cast( Settings::getInstance()->getInt("TransitionsGamelistToGamelist")); } if (transitionAnim == ViewTransitionAnimation::SLIDE) slideTransitions = true; if (transitionAnim == ViewTransitionAnimation::FADE) fadeTransitions = true; // Restore the X position for the view, if it was previously moved. if (mWrappedViews) restoreViewPosition(); if (mPreviousView && fadeTransitions && isAnimationPlaying(0)) mPreviousView->onHide(); if (mPreviousView) { mSkipView = mPreviousView; mPreviousView.reset(); mPreviousView = nullptr; } else if (!mPreviousView && mState.viewing == ViewMode::GAMELIST) { // This is needed as otherwise the static image would not get rendered during the // first Slide transition when coming from the System view. mSkipView = getGamelistView(system); } if (mState.viewing != ViewMode::SYSTEM_SELECT) { mPreviousView = mCurrentView; mSystemViewTransition = false; } else { mSystemViewTransition = true; } // Find if we're wrapping around the first and last systems, which requires the gamelist // to be moved in order to avoid weird camera movements. This is only needed for the // slide transition style. if (mState.viewing == ViewMode::GAMELIST && SystemData::sSystemVector.size() > 1 && slideTransitions) { if (SystemData::sSystemVector.front() == mState.getSystem()) { if (SystemData::sSystemVector.back() == system) wrapFirstToLast = true; } else if (SystemData::sSystemVector.back() == mState.getSystem()) { if (SystemData::sSystemVector.front() == system) wrapLastToFirst = true; } } // Stop any scrolling, animations and camera movements. if (mState.viewing == ViewMode::SYSTEM_SELECT) { mSystemListView->stopScrolling(); if (mSystemListView->isSystemAnimationPlaying(0)) mSystemListView->finishSystemAnimation(0); } if (slideTransitions || (!fadeTransitions && mLastTransitionAnim == ViewTransitionAnimation::FADE)) cancelViewTransitions(); if (mState.viewing == ViewMode::SYSTEM_SELECT) { // Move the system list. auto sysList = getSystemListView(); float offsetX {sysList->getPosition().x}; int sysId {getSystemId(system)}; sysList->setPosition(sysId * Renderer::getScreenWidth(), sysList->getPosition().y); offsetX = sysList->getPosition().x - offsetX; mCamera[3].x -= offsetX; } // If we are wrapping around, either from the first to last system, or the other way // around, we need to temporarily move the gamelist view location so that the camera // movements will be correct. This is accomplished by simply offsetting the X position // with the position of the first or last system plus the screen width. if (wrapFirstToLast) { glm::vec3 currentPosition {mCurrentView->getPosition()}; mWrapPreviousPositionX = currentPosition.x; float offsetX {getGamelistView(system)->getPosition().x}; // This is needed to move the camera in the correct direction if there are only two systems. if (SystemData::sSystemVector.size() == 2 && mNextSystem) offsetX -= Renderer::getScreenWidth(); else offsetX += Renderer::getScreenWidth(); currentPosition.x = offsetX; mCurrentView->setPosition(currentPosition); mCamera[3].x -= offsetX; mWrappedViews = true; } else if (wrapLastToFirst) { glm::vec3 currentPosition {mCurrentView->getPosition()}; mWrapPreviousPositionX = currentPosition.x; float offsetX {getGamelistView(system)->getPosition().x}; if (SystemData::sSystemVector.size() == 2 && !mNextSystem) offsetX += Renderer::getScreenWidth(); else offsetX -= Renderer::getScreenWidth(); currentPosition.x = offsetX; mCurrentView->setPosition(currentPosition); mCamera[3].x = -offsetX; mWrappedViews = true; } mCurrentView = getGamelistView(system); mCurrentView->finishAnimation(0); // Application startup animation, if starting in a gamelist rather than in the system view. if (mState.viewing == ViewMode::NOTHING) { if (mLastTransitionAnim == ViewTransitionAnimation::FADE) cancelViewTransitions(); mCamera = glm::translate(mCamera, glm::round(-mCurrentView->getPosition())); if (transitionAnim == ViewTransitionAnimation::SLIDE) { mCamera[3].y -= Renderer::getScreenHeight(); updateHelpPrompts(); } else if (transitionAnim == ViewTransitionAnimation::FADE) { mCamera[3].y += Renderer::getScreenHeight() * 2.0f; } else { updateHelpPrompts(); } } mState.previouslyViewed = mState.viewing; mState.viewing = ViewMode::GAMELIST; mState.system = system; if (mCurrentView) mCurrentView->onShow(); playViewTransition(transitionType); } void ViewController::playViewTransition(ViewTransition transitionType, bool instant) { mCancelledTransition = false; glm::vec3 target {0.0f, 0.0f, 0.0f}; if (mCurrentView) target = mCurrentView->getPosition(); // No need to animate, we're not going anywhere (probably due to goToNextGamelist() // or goToPrevGamelist() being called when there's only 1 system). if (target == static_cast(-mCamera[3]) && !isAnimationPlaying(0)) return; ViewTransitionAnimation transitionAnim {ViewTransitionAnimation::INSTANT}; if (transitionType == ViewTransition::SYSTEM_TO_SYSTEM) transitionAnim = static_cast( Settings::getInstance()->getInt("TransitionsSystemToSystem")); else if (transitionType == ViewTransition::SYSTEM_TO_GAMELIST) transitionAnim = static_cast( Settings::getInstance()->getInt("TransitionsSystemToGamelist")); else if (transitionType == ViewTransition::GAMELIST_TO_GAMELIST) transitionAnim = static_cast( Settings::getInstance()->getInt("TransitionsGamelistToGamelist")); else if (transitionType == ViewTransition::GAMELIST_TO_SYSTEM) transitionAnim = static_cast( Settings::getInstance()->getInt("TransitionsGamelistToSystem")); else if (transitionType == ViewTransition::STARTUP_TO_SYSTEM) transitionAnim = static_cast( Settings::getInstance()->getInt("TransitionsStartupToSystem")); else transitionAnim = static_cast( Settings::getInstance()->getInt("TransitionsStartupToGamelist")); mLastTransitionAnim = transitionAnim; if (instant || transitionAnim == ViewTransitionAnimation::INSTANT) { setAnimation(new LambdaAnimation( [this, target](float /*t*/) { this->mCamera[3].x = -target.x; this->mCamera[3].y = -target.y; this->mCamera[3].z = -target.z; if (mState.previouslyViewed == ViewMode::SYSTEM_SELECT) getSystemListView()->onHide(); if (mPreviousView) mPreviousView->onHide(); }, 1)); updateHelpPrompts(); } else if (transitionAnim == ViewTransitionAnimation::FADE) { // Stop whatever's currently playing, leaving mFadeOpacity wherever it is. cancelAnimation(0); auto fadeFunc = [this](float t) { // The flag mCancelledTransition is required only when cancelViewTransitions() // cancels the animation, and it's only needed for the Fade transitions. // Without this, a (much shorter) fade transition would still play as // finishedCallback is calling this function. if (!mCancelledTransition) mFadeOpacity = glm::mix(0.0f, 1.0f, t); }; auto fadeCallback = [this]() { if (mState.previouslyViewed == ViewMode::SYSTEM_SELECT || mSystemViewTransition) getSystemListView()->onHide(); if (mPreviousView) mPreviousView->onHide(); }; const static int FADE_DURATION {120}; // Fade in/out time. const static int FADE_WAIT {200}; // Time to wait between in/out. setAnimation(new LambdaAnimation(fadeFunc, FADE_DURATION), 0, [this, fadeFunc, fadeCallback, target] { this->mCamera[3].x = -target.x; this->mCamera[3].y = -target.y; this->mCamera[3].z = -target.z; updateHelpPrompts(); setAnimation(new LambdaAnimation(fadeFunc, FADE_DURATION), FADE_WAIT, fadeCallback, true); }); // Fast-forward animation if we're partially faded. if (target == static_cast(-mCamera[3])) { // Not changing screens, so cancel the first half entirely. advanceAnimation(0, FADE_DURATION); advanceAnimation(0, FADE_WAIT); advanceAnimation(0, FADE_DURATION - static_cast(mFadeOpacity * FADE_DURATION)); } else { advanceAnimation(0, static_cast(mFadeOpacity * FADE_DURATION)); } } else if (transitionAnim == ViewTransitionAnimation::SLIDE) { auto slideCallback = [this]() { if (mState.previouslyViewed == ViewMode::SYSTEM_SELECT || mSystemViewTransition) getSystemListView()->onHide(); if (mSkipView) { mSkipView->onHide(); mSkipView.reset(); mSkipView = nullptr; } else if (mPreviousView) { mPreviousView->onHide(); } }; setAnimation(new MoveCameraAnimation(mCamera, target), 0, slideCallback); updateHelpPrompts(); // Update help prompts immediately. } } void ViewController::onFileChanged(FileData* file, bool reloadGamelist) { auto it = mGamelistViews.find(file->getSystem()); if (it != mGamelistViews.cend()) it->second->onFileChanged(file, reloadGamelist); } void ViewController::launch(FileData* game) { if (game->getType() != GAME) { LOG(LogError) << "Tried to launch something that isn't a game"; return; } // Disable text scrolling and stop any Lottie animations. These will be enabled again in // FileData upon returning from the game. mWindow->setAllowTextScrolling(false); mWindow->setAllowFileAnimation(false); stopAnimation(1); // Make sure the fade in isn't still playing. mWindow->stopInfoPopup(); // Make sure we disable any existing info popup. int duration {0}; std::string durationString {Settings::getInstance()->getString("LaunchScreenDuration")}; if (durationString == "disabled") { // If the game launch screen has been set as disabled, show a simple info popup // notification instead. mWindow->queueInfoPopup( Utils::String::format(_("LAUNCHING GAME '%s'"), Utils::String::toUpper(game->metadata.get("name")).c_str()), 10000); duration = 1700; } else if (durationString == "brief") { duration = 1700; } else if (durationString == "long") { duration = 4500; } else { // Normal duration. duration = 3000; } if (durationString != "disabled") mWindow->displayLaunchScreen(game->getSourceFileData()); NavigationSounds::getInstance().playThemeNavigationSound(LAUNCHSOUND); // This is just a dummy animation in order for the launch screen or notification popup // to be displayed briefly, and for the navigation sound playing to be able to complete. // During this time period, all user input is blocked. setAnimation(new LambdaAnimation([](float t) {}, duration), 0, [this, game] { game->launchGame(); // If the launch screen is disabled then this will do nothing. mWindow->closeLaunchScreen(); onFileChanged(game, true); // This is a workaround so that any keys or button presses used for exiting the emulator // are not captured upon returning. setAnimation(new LambdaAnimation([](float t) {}, 1), 0, [this] { mWindow->setBlockInput(false); }); }); } void ViewController::removeGamelistView(SystemData* system) { auto exists = mGamelistViews.find(system); if (exists != mGamelistViews.cend()) { exists->second.reset(); mGamelistViews.erase(system); } } std::shared_ptr ViewController::getGamelistView(SystemData* system) { // If we have already created an entry for this system, then return that one. auto exists = mGamelistViews.find(system); if (exists != mGamelistViews.cend()) return exists->second; system->getIndex()->setKidModeFilters(); // If there's no entry, then create it and return it. std::shared_ptr view; if (Settings::getInstance()->getBool("ThemeVariantTriggers")) { const auto overrides = system->getTheme()->getCurrentThemeSelectedVariantOverrides(); if (!overrides.empty()) { ThemeTriggers::TriggerType noVideosTriggerType {ThemeTriggers::TriggerType::NONE}; ThemeTriggers::TriggerType noMediaTriggerType {ThemeTriggers::TriggerType::NONE}; const std::vector files { system->getRootFolder()->getFilesRecursive(GAME | FOLDER)}; if (overrides.find(ThemeTriggers::TriggerType::NO_VIDEOS) != overrides.end()) { noVideosTriggerType = ThemeTriggers::TriggerType::NO_VIDEOS; for (auto it = files.cbegin(); it != files.cend(); ++it) { if (!(*it)->getVideoPath().empty()) { noVideosTriggerType = ThemeTriggers::TriggerType::NONE; break; } } } if (overrides.find(ThemeTriggers::TriggerType::NO_MEDIA) != overrides.end()) { noMediaTriggerType = ThemeTriggers::TriggerType::NO_MEDIA; for (auto imageType : overrides.at(ThemeTriggers::TriggerType::NO_MEDIA).second) { for (auto it = files.cbegin(); it != files.cend(); ++it) { if (imageType == "miximage") { if (!(*it)->getMiximagePath().empty()) { noMediaTriggerType = ThemeTriggers::TriggerType::NONE; goto BREAK; } } else if (imageType == "marquee") { if (!(*it)->getMarqueePath().empty()) { noMediaTriggerType = ThemeTriggers::TriggerType::NONE; goto BREAK; } } else if (imageType == "screenshot") { if (!(*it)->getScreenshotPath().empty()) { noMediaTriggerType = ThemeTriggers::TriggerType::NONE; goto BREAK; } } else if (imageType == "titlescreen") { if (!(*it)->getTitleScreenPath().empty()) { noMediaTriggerType = ThemeTriggers::TriggerType::NONE; goto BREAK; } } else if (imageType == "cover") { if (!(*it)->getCoverPath().empty()) { noMediaTriggerType = ThemeTriggers::TriggerType::NONE; goto BREAK; } } else if (imageType == "backcover") { if (!(*it)->getBackCoverPath().empty()) { noMediaTriggerType = ThemeTriggers::TriggerType::NONE; goto BREAK; } } else if (imageType == "3dbox") { if (!(*it)->get3DBoxPath().empty()) { noMediaTriggerType = ThemeTriggers::TriggerType::NONE; goto BREAK; } } else if (imageType == "physicalmedia") { if (!(*it)->getPhysicalMediaPath().empty()) { noMediaTriggerType = ThemeTriggers::TriggerType::NONE; goto BREAK; } } else if (imageType == "fanart") { if (!(*it)->getFanArtPath().empty()) { noMediaTriggerType = ThemeTriggers::TriggerType::NONE; goto BREAK; } } else if (imageType == "video") { if (!(*it)->getVideoPath().empty()) { noMediaTriggerType = ThemeTriggers::TriggerType::NONE; goto BREAK; } } } } } BREAK: // noMedia takes precedence over the noVideos trigger. if (noMediaTriggerType == ThemeTriggers::TriggerType::NO_MEDIA) system->loadTheme(noMediaTriggerType); else system->loadTheme(noVideosTriggerType); } } view = std::shared_ptr(new GamelistView(system->getRootFolder())); view->setTheme(system->getTheme()); std::vector& sysVec {SystemData::sSystemVector}; int id {static_cast(std::find(sysVec.cbegin(), sysVec.cend(), system) - sysVec.cbegin())}; view->setPosition(id * Renderer::getScreenWidth(), Renderer::getScreenHeight() * 2.0f); addChild(view.get()); mGamelistViews[system] = view; return view; } std::shared_ptr ViewController::getSystemListView() { // If we have already created a system view entry, then return it. if (mSystemListView) return mSystemListView; mSystemListView = std::shared_ptr(new SystemView); addChild(mSystemListView.get()); mSystemListView->setPosition(0, Renderer::getScreenHeight()); return mSystemListView; } bool ViewController::input(InputConfig* config, Input input) { // If using the %RUNINBACKGROUND% variable in a launch command or if enabling the // RunInBackground setting, ES-DE will run in the background while a game is launched. // If we're in this state and then register some input, it means that the user is back in ES-DE. // Therefore unset the game launch flag and update all the GUI components. This will re-enable // the video player and scrolling of game names and game descriptions as well as letting the // screensaver start on schedule. On Android the onResume() method will call the native onResume // function which will perform the same steps as shown below (on Android we always keep running // when launching games). #if !defined(__ANDROID__) if (mWindow->getGameLaunchedState()) { mWindow->setAllowTextScrolling(true); mWindow->setAllowFileAnimation(true); mWindow->setLaunchedGame(false); // Filter out the "a" button so the game is not restarted if there was such a button press // queued when leaving the game. if (config->isMappedTo("a", input) && input.value != 0) return true; // Trigger the game-end event. auto& eventParams = mWindow->getGameEndEventParams(); if (eventParams.size() == 5) { Scripting::fireEvent(eventParams[0], eventParams[1], eventParams[2], eventParams[3], eventParams[4]); eventParams.clear(); } } #endif // Open the main menu. if (!(UIModeController::getInstance()->isUIModeKid() && !Settings::getInstance()->getBool("EnableMenuKidMode")) && config->isMappedTo("start", input) && input.value != 0 && mCurrentView != nullptr) { // If we don't stop the scrolling here, it will continue to // run after closing the menu. if (mSystemListView->isScrolling()) mSystemListView->stopScrolling(); // Finish the animation too, so that it doesn't continue // to play when we've closed the menu. if (mSystemListView->isSystemAnimationPlaying(0)) mSystemListView->finishSystemAnimation(0); // Stop the gamelist scrolling as well as it would otherwise continue to run after // closing the menu. mCurrentView->stopListScrolling(); // Pause all videos as they would otherwise continue to play beneath the menu. mCurrentView->pauseViewVideos(); mCurrentView->stopGamelistFadeAnimations(); mWindow->setAllowTextScrolling(false); mWindow->setAllowFileAnimation(false); // Finally, if the camera is currently moving, reset its position. cancelViewTransitions(); mWindow->pushGui(new GuiMenu); return true; } if (!mWindow->isScreensaverActive()) { mWindow->setAllowTextScrolling(true); mWindow->setAllowFileAnimation(true); } // Check if UI mode has changed due to passphrase completion. if (UIModeController::getInstance()->listen(config, input)) return true; if (mCurrentView) return mCurrentView->input(config, input); return false; } void ViewController::update(int deltaTime) { if (mWindow->getChangedTheme()) cancelViewTransitions(); if (mCurrentView) mCurrentView->update(deltaTime); updateSelf(deltaTime); if (mGameToLaunch) { launch(mGameToLaunch); mGameToLaunch = nullptr; } } void ViewController::render(const glm::mat4& parentTrans) { glm::mat4 trans {mCamera * parentTrans}; glm::mat4 transInverse {glm::inverse(trans)}; // Camera position, position + size. const glm::vec3 viewStart {transInverse[3]}; const glm::vec3 viewEnd {std::fabs(trans[3].x) + Renderer::getScreenWidth(), std::fabs(trans[3].y) + Renderer::getScreenHeight(), 0.0f}; // Keep track of UI mode changes. UIModeController::getInstance()->monitorUIMode(); // Render the system view if it's the currently displayed view, or if we're in the progress // of transitioning to or from this view. if (mSystemListView == mCurrentView || (mSystemViewTransition && isCameraMoving())) getSystemListView()->render(trans); auto gamelistRenderFunc = [trans, viewStart, viewEnd](auto it) { const glm::vec3 guiStart {it->second->getPosition()}; const glm::vec3 guiEnd {it->second->getPosition() + glm::vec3 {it->second->getSize().x, it->second->getSize().y, 0.0f}}; if (guiEnd.x >= viewStart.x && guiEnd.y >= viewStart.y && guiStart.x <= viewEnd.x && guiStart.y <= viewEnd.y) it->second->render(trans); }; // Draw the gamelists. In the same manner as for the system view, limit the rendering only // to what needs to be drawn. for (auto it = mGamelistViews.cbegin(); it != mGamelistViews.cend(); ++it) { if (it->second == mPreviousView && isCameraMoving()) gamelistRenderFunc(it); } // Always render the currently selected system last so that any stationary elements will get // correctly rendered on top. for (auto it = mGamelistViews.cbegin(); it != mGamelistViews.cend(); ++it) { if (it->second == mCurrentView) gamelistRenderFunc(it); } if (mWindow->peekGui() == this) mWindow->renderHelpPromptsEarly(); // Fade out. if (mFadeOpacity) { unsigned int fadeColor {0x00000000 | static_cast(mFadeOpacity * 255.0f)}; mRenderer->setMatrix(parentTrans); mRenderer->drawRect(0.0f, 0.0f, Renderer::getScreenWidth(), Renderer::getScreenHeight(), fadeColor, fadeColor); } } void ViewController::preload() { const unsigned int systemCount {static_cast(SystemData::sSystemVector.size())}; // This reduces the amount of texture pop-in when loading theme extras. if (!SystemData::sSystemVector.empty()) getSystemListView(); const bool splashScreen {Settings::getInstance()->getBool("SplashScreen")}; float loadedSystems {0.0f}; unsigned int lastTime {0}; unsigned int accumulator {0}; SDL_Event event {}; for (auto it = SystemData::sSystemVector.cbegin(); // Line break. it != SystemData::sSystemVector.cend(); ++it) { // Poll events so that the OS doesn't think the application is hanging on startup, // this is required as the main application loop hasn't started yet. while (SDL_PollEvent(&event)) { InputManager::getInstance().parseEvent(event); if (event.type == SDL_QUIT) { SystemData::sStartupExitSignal = true; return; } #if defined(__ANDROID__) if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { setWindowSizeChanged(static_cast(event.window.data1), static_cast(event.window.data2)); } #endif }; const std::string entryType {(*it)->isCustomCollection() ? "custom collection" : "system"}; LOG(LogDebug) << "ViewController::preload(): Populating gamelist for " << entryType << " \"" << (*it)->getName() << "\""; if (splashScreen) { const unsigned int curTime {SDL_GetTicks()}; accumulator += curTime - lastTime; lastTime = curTime; ++loadedSystems; // This prevents Renderer::swapBuffers() from being called excessively which // could lead to significantly longer application startup times. if (accumulator > 20) { accumulator = 0; const float progress { glm::mix(0.5f, 1.0f, loadedSystems / static_cast(systemCount))}; mWindow->renderSplashScreen(Window::SplashScreenState::POPULATING, progress); lastTime += SDL_GetTicks() - curTime; } } (*it)->getIndex()->resetFilters(); getGamelistView(*it)->preloadGamelist(); } if (splashScreen && SystemData::sSystemVector.size() > 0) Window::getInstance()->renderSplashScreen(Window::SplashScreenState::POPULATING, 1.0f); // Short delay so that the full progress bar is always visible before proceeding. SDL_Delay(100); if (SystemData::sSystemVector.size() > 0) ThemeData::setThemeTransitions(); // Load navigation sounds, either from the theme if it supports it, or otherwise from // the bundled fallback sound files. bool themeSoundSupport {false}; for (auto system : SystemData::sSystemVector) { if (!themeSoundSupport && system->getTheme()->hasView("all")) { NavigationSounds::getInstance().loadThemeNavigationSounds(system->getTheme().get()); themeSoundSupport = true; } if (system->getRootFolder()->getName() == "recent") { CollectionSystemsManager::getInstance()->trimCollectionCount(system->getRootFolder(), LAST_PLAYED_MAX); } } if (!SystemData::sSystemVector.empty() && !themeSoundSupport) NavigationSounds::getInstance().loadThemeNavigationSounds(nullptr); } void ViewController::reloadGamelistView(GamelistView* view, bool reloadTheme) { for (auto it = mGamelistViews.cbegin(); it != mGamelistViews.cend(); ++it) { if (it->second.get() == view) { bool isCurrent {mCurrentView == it->second}; SystemData* system {it->first}; FileData* cursor {view->getCursor()}; // Retain the cursor history for the view. std::vector cursorHistoryTemp; it->second->copyCursorHistory(cursorHistoryTemp); mGamelistViews.erase(it); if (isCurrent) mCurrentView = nullptr; if (reloadTheme) system->loadTheme(ThemeTriggers::TriggerType::NONE); system->getIndex()->setKidModeFilters(); std::shared_ptr newView {getGamelistView(system)}; // Make sure we don't attempt to set the cursor to a nonexistent entry. auto children = system->getRootFolder()->getChildrenRecursive(); if (std::find(children.cbegin(), children.cend(), cursor) != children.cend()) newView->setCursor(cursor); if (isCurrent) mCurrentView = newView; newView->populateCursorHistory(cursorHistoryTemp); // This is required to get the game count updated if the favorite metadata value has // been changed for any game that is part of a custom collection. if (system->isCollection() && system->getName() == "collections") { std::pair gameCount {0, 0}; system->getRootFolder()->countGames(gameCount); } updateHelpPrompts(); break; } } // If using the %RUNINBACKGROUND% variable in a launch command or if enabling the // RunInBackground setting, ES-DE will run in the background while a game is launched. // If this flag has been set, then update all the GUI components. This will block the // video player, prevent scrolling of game names and game descriptions and prevent the // screensaver from starting on schedule. if (mWindow->getGameLaunchedState()) mWindow->setLaunchedGame(true); // Redisplay the current view. if (mCurrentView) mCurrentView->onShow(); } void ViewController::reloadAll() { if (mRenderer->getSDLWindow() == nullptr) return; cancelViewTransitions(); // Clear all GamelistViews. std::map cursorMap; for (auto it = mGamelistViews.cbegin(); it != mGamelistViews.cend(); ++it) { if (std::find(SystemData::sSystemVector.cbegin(), SystemData::sSystemVector.cend(), (*it).first) != SystemData::sSystemVector.cend()) cursorMap[it->first] = it->second->getCursor(); } mGamelistViews.clear(); mCurrentView = nullptr; // Load themes, create GamelistViews and reset filters. for (auto it = cursorMap.cbegin(); it != cursorMap.cend(); ++it) { it->first->loadTheme(ThemeTriggers::TriggerType::NONE); it->first->getIndex()->resetFilters(); } ThemeData::setThemeTransitions(); // Rebuild SystemListView. mSystemListView.reset(); getSystemListView(); // Restore cursor positions for all systems. for (auto it = cursorMap.cbegin(); it != cursorMap.cend(); ++it) { const std::string entryType {(*it).first->isCustomCollection() ? "custom collection" : "system"}; LOG(LogDebug) << "ViewController::reloadAll(): Populating gamelist for " << entryType << " \"" << (*it).first->getName() << "\""; getGamelistView(it->first)->setCursor(it->second); } // Update mCurrentView since the pointers changed. if (mState.viewing == ViewMode::GAMELIST) { mCurrentView = getGamelistView(mState.getSystem()); } else if (mState.viewing == ViewMode::SYSTEM_SELECT) { SystemData* system {mState.getSystem()}; mSystemListView->goToSystem(system, false); mCurrentView = mSystemListView; mCamera[3].x = 0.0f; } else { goToSystemView(SystemData::sSystemVector.front(), false); } // Load navigation sounds, either from the theme if it supports it, or otherwise from // the bundled fallback sound files. NavigationSounds::getInstance().deinit(); bool themeSoundSupport {false}; for (SystemData* system : SystemData::sSystemVector) { if (system->getTheme()->hasView("all")) { NavigationSounds::getInstance().loadThemeNavigationSounds(system->getTheme().get()); themeSoundSupport = true; break; } } if (!SystemData::sSystemVector.empty() && !themeSoundSupport) NavigationSounds::getInstance().loadThemeNavigationSounds(nullptr); ThemeData::themeLoadedLogOutput(); mCurrentView->onShow(); updateHelpPrompts(); } void ViewController::setWindowSizeChanged(const int width, const int height) { #if defined(__ANDROID__) const std::pair windowSize {Utils::Platform::Android::getWindowSize()}; if (windowSize.first == static_cast(mRenderer->getScreenWidth()) && windowSize.second == static_cast(mRenderer->getScreenHeight())) { mWindowChangedWidth = 0; mWindowChangedHeight = 0; } else { mWindowChangedWidth = windowSize.first; mWindowChangedHeight = windowSize.second; } #endif } void ViewController::checkWindowSizeChanged() { if (mWindowChangedWidth == 0 || mWindowChangedHeight == 0) return; LOG(LogInfo) << "Window size has changed from " << mRenderer->getScreenWidth() << "x" << mRenderer->getScreenHeight() << " to " << mWindowChangedWidth << "x" << mWindowChangedHeight << ", reloading..."; mWindowChangedWidth = 0; mWindowChangedHeight = 0; mWindow->stopInfoPopup(); if (mState.viewing != ViewController::ViewMode::NOTHING) { mWindow->stopScreensaver(); mWindow->stopMediaViewer(); mWindow->stopPDFViewer(); } // This is done quite ungracefully, essentially forcekilling all open windows. while (mWindow->getGuiStackSize() > 1) mWindow->removeGui(mWindow->peekGui()); AudioManager::getInstance().deinit(); mWindow->deinit(); SDL_Delay(20); AudioManager::getInstance().init(); mWindow->init(true); mWindow->setLaunchedGame(false); mWindow->invalidateCachedBackground(); mWindow->renderSplashScreen(Window::SplashScreenState::RELOADING, 0.0f); if (mState.viewing != ViewController::ViewMode::NOTHING) { reloadAll(); goToStart(false); resetCamera(); } else { noGamesDialog(); } #if defined(__ANDROID__) InputOverlay::getInstance().init(); #endif } void ViewController::rescanROMDirectory() { mWindow->setBlockInput(true); resetCamera(); mState.viewing = ViewMode::NOTHING; mGamelistViews.clear(); mSystemListView.reset(); mCurrentView.reset(); mPreviousView.reset(); mSkipView.reset(); mWindow->renderSplashScreen(Window::SplashScreenState::SCANNING, 0.0f); CollectionSystemsManager::getInstance()->deinit(false); SystemData::loadConfig(); if (SystemData::sStartupExitSignal) { SDL_Event quit {}; quit.type = SDL_QUIT; SDL_PushEvent(&quit); return; } if (SystemData::sSystemVector.empty()) { // It's possible that there are no longer any games. mWindow->setBlockInput(false); mWindow->invalidateCachedBackground(); noGamesDialog(); } else { preload(); if (SystemData::sStartupExitSignal) { SDL_Event quit {}; quit.type = SDL_QUIT; SDL_PushEvent(&quit); return; } mWindow->setBlockInput(false); goToStart(false); } } std::vector ViewController::getHelpPrompts() { std::vector prompts; if (!mCurrentView) return prompts; prompts = mCurrentView->getHelpPrompts(); if (!(UIModeController::getInstance()->isUIModeKid() && !Settings::getInstance()->getBool("EnableMenuKidMode"))) prompts.push_back(HelpPrompt("start", _("menu"))); return prompts; } HelpStyle ViewController::getHelpStyle() { if (!mCurrentView) return GuiComponent::getHelpStyle(); return mCurrentView->getHelpStyle(); } HelpStyle ViewController::getViewHelpStyle() { if (mState.viewing == ViewMode::GAMELIST) return getGamelistView(mState.getSystem())->getHelpStyle(); else return getSystemListView()->getHelpStyle(); }