ES-DE/es-app/src/views/ViewController.cpp

1692 lines
67 KiB
C++

// 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 = 0xF5F5F5FF;
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 = 0x000000FF;
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 = 0x000000FF;
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(
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(
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(
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(
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(__ANDROID__) || defined(__IOS__)
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(__ANDROID__) || defined(__IOS__)
mNoGamesMessageBox = new GuiMsgBox(
mNoGamesErrorMessage + mRomDirectory,
#else
mNoGamesMessageBox = new GuiMsgBox(
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(
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(
_("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(
_("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(
_("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(
_("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(
_("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(
_("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__) || defined(__IOS__)
"", 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
mWindow->pushGui(mNoGamesMessageBox);
}
void ViewController::invalidAlternativeEmulatorDialog()
{
cancelViewTransitions();
mWindow->pushGui(new GuiMsgBox(_("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(
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(
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(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<SystemData*>& sysVec {SystemData::sSystemVector};
return static_cast<int>(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<ViewTransitionAnimation>(
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<SystemData*>::CarouselType::HORIZONTAL ||
getSystemListView()->getCarouselType() ==
CarouselComponent<SystemData*>::CarouselType::HORIZONTAL_WHEEL)
mCamera[3].y += Renderer::getScreenHeight();
else
mCamera[3].x -= Renderer::getScreenWidth();
}
else if (getSystemListView()->getPrimaryType() == SystemView::PrimaryType::TEXTLIST ||
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<SystemData*>::CarouselType::HORIZONTAL ||
getSystemListView()->getCarouselType() ==
CarouselComponent<SystemData*>::CarouselType::HORIZONTAL_WHEEL)
mCamera[3].y += Renderer::getScreenHeight();
else
mCamera[3].x += Renderer::getScreenWidth();
}
else if (getSystemListView()->getPrimaryType() == SystemView::PrimaryType::TEXTLIST ||
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<ViewTransitionAnimation>(
Settings::getInstance()->getInt("TransitionsSystemToGamelist"));
}
else if (mState.viewing == ViewMode::NOTHING) {
transitionType = ViewTransition::STARTUP_TO_GAMELIST;
transitionAnim = static_cast<ViewTransitionAnimation>(
Settings::getInstance()->getInt("TransitionsStartupToGamelist"));
}
else {
transitionType = ViewTransition::GAMELIST_TO_GAMELIST;
transitionAnim = static_cast<ViewTransitionAnimation>(
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<glm::vec3>(-mCamera[3]) && !isAnimationPlaying(0))
return;
ViewTransitionAnimation transitionAnim {ViewTransitionAnimation::INSTANT};
if (transitionType == ViewTransition::SYSTEM_TO_SYSTEM)
transitionAnim = static_cast<ViewTransitionAnimation>(
Settings::getInstance()->getInt("TransitionsSystemToSystem"));
else if (transitionType == ViewTransition::SYSTEM_TO_GAMELIST)
transitionAnim = static_cast<ViewTransitionAnimation>(
Settings::getInstance()->getInt("TransitionsSystemToGamelist"));
else if (transitionType == ViewTransition::GAMELIST_TO_GAMELIST)
transitionAnim = static_cast<ViewTransitionAnimation>(
Settings::getInstance()->getInt("TransitionsGamelistToGamelist"));
else if (transitionType == ViewTransition::GAMELIST_TO_SYSTEM)
transitionAnim = static_cast<ViewTransitionAnimation>(
Settings::getInstance()->getInt("TransitionsGamelistToSystem"));
else if (transitionType == ViewTransition::STARTUP_TO_SYSTEM)
transitionAnim = static_cast<ViewTransitionAnimation>(
Settings::getInstance()->getInt("TransitionsStartupToSystem"));
else
transitionAnim = static_cast<ViewTransitionAnimation>(
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<glm::vec3>(-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<int>(mFadeOpacity * FADE_DURATION));
}
else {
advanceAnimation(0, static_cast<int>(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};
const std::string durationString {Settings::getInstance()->getString("LaunchScreenDuration")};
if (durationString == "popup") {
// Show a simple info popup notification instead of the launch screen.
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 if (durationString == "disabled") {
duration = 0;
}
else {
// Normal duration.
duration = 3000;
}
if (durationString != "disabled" && durationString != "popup")
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<GamelistView> 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<GamelistView> 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<FileData*> 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<GamelistView>(new GamelistView(system->getRootFolder()));
view->setTheme(system->getTheme());
std::vector<SystemData*>& sysVec {SystemData::sSystemVector};
int id {static_cast<int>(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<SystemView> ViewController::getSystemListView()
{
// If we have already created a system view entry, then return it.
if (mSystemListView)
return mSystemListView;
mSystemListView = std::shared_ptr<SystemView>(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);
resetViewVideosTimer();
// 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) {
setHelpComponentsVisibility(false);
// 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<unsigned int>(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<unsigned int>(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<int>(event.window.data1),
static_cast<int>(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<float>(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<FileData*> 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<GamelistView> 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<unsigned int, unsigned int> 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<SystemData*, FileData*> 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__)
std::pair<int, int> windowSize {Utils::Platform::Android::getWindowSize()};
const int screenRotation {Settings::getInstance()->getInt("ScreenRotate")};
if (screenRotation == 90 || screenRotation == 270)
windowSize = std::make_pair(windowSize.second, windowSize.first);
if (windowSize.first == static_cast<int>(mRenderer->getScreenWidth()) &&
windowSize.second == static_cast<int>(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();
mWindow->passHelpComponents(nullptr);
mWindow->passClockComponents(nullptr);
mWindow->passSystemStatusComponents(nullptr);
noGamesDialog();
}
else {
preload();
if (SystemData::sStartupExitSignal) {
SDL_Event quit {};
quit.type = SDL_QUIT;
SDL_PushEvent(&quit);
return;
}
mWindow->setBlockInput(false);
goToStart(false);
}
}
std::vector<HelpPrompt> ViewController::getHelpPrompts()
{
std::vector<HelpPrompt> prompts;
if (!mCurrentView)
return prompts;
prompts = mCurrentView->getHelpPrompts();
if (!(UIModeController::getInstance()->isUIModeKid() &&
!Settings::getInstance()->getBool("EnableMenuKidMode")))
prompts.push_back(HelpPrompt("start", _("menu")));
return prompts;
}