diff --git a/es-app/src/guis/GuiMenu.cpp.orig b/es-app/src/guis/GuiMenu.cpp.orig new file mode 100644 index 000000000..8eff03a15 --- /dev/null +++ b/es-app/src/guis/GuiMenu.cpp.orig @@ -0,0 +1,1949 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// GuiMenu.cpp +// +// Main menu. +// Some submenus are covered in separate source files. +// + +#include "guis/GuiMenu.h" + +#if defined(_WIN64) +// Why this is needed here is anyone's guess but without it the compilation fails. +#include +#endif + +#include "CollectionSystemsManager.h" +#include "EmulationStation.h" +#include "FileFilterIndex.h" +#include "FileSorts.h" +#include "Scripting.h" +#include "SystemData.h" +#include "UIModeController.h" +#include "VolumeControl.h" +#include "components/OptionListComponent.h" +#include "components/SliderComponent.h" +#include "components/SwitchComponent.h" +#include "guis/GuiAlternativeEmulators.h" +#include "guis/GuiCollectionSystemsOptions.h" +#include "guis/GuiDetectDevice.h" +#include "guis/GuiMediaViewerOptions.h" +#include "guis/GuiMsgBox.h" +#include "guis/GuiOrphanedDataCleanup.h" +#include "guis/GuiScraperMenu.h" +#include "guis/GuiScreensaverOptions.h" +#include "guis/GuiTextEditKeyboardPopup.h" +#include "guis/GuiTextEditPopup.h" +#include "guis/GuiThemeDownloader.h" +#include "utils/PlatformUtil.h" + +#include +#include + +GuiMenu::GuiMenu() + : mRenderer {Renderer::getInstance()} + , mMenu {"MAIN MENU"} + , mThemeDownloaderReloadCounter {0} +{ + const bool isFullUI {UIModeController::getInstance()->isUIModeFull()}; + + if (isFullUI) + addEntry("SCRAPER", mMenuColorPrimary, true, [this] { openScraperOptions(); }); + + if (isFullUI) + addEntry("UI SETTINGS", mMenuColorPrimary, true, [this] { openUIOptions(); }); + + addEntry("SOUND SETTINGS", mMenuColorPrimary, true, [this] { openSoundOptions(); }); + + if (isFullUI) + addEntry("INPUT DEVICE SETTINGS", mMenuColorPrimary, true, + [this] { openInputDeviceOptions(); }); + + if (isFullUI) + addEntry("GAME COLLECTION SETTINGS", mMenuColorPrimary, true, + [this] { openCollectionSystemOptions(); }); + + if (isFullUI) + addEntry("OTHER SETTINGS", mMenuColorPrimary, true, [this] { openOtherOptions(); }); + + if (isFullUI) + addEntry("UTILITIES", mMenuColorPrimary, true, [this] { openUtilities(); }); + + if (!Settings::getInstance()->getBool("ForceKiosk") && + Settings::getInstance()->getString("UIMode") != "kiosk") { +#if defined(__APPLE__) + addEntry("QUIT EMULATIONSTATION", mMenuColorPrimary, false, [this] { openQuitMenu(); }); +#else + if (Settings::getInstance()->getBool("ShowQuitMenu")) + addEntry("QUIT", mMenuColorPrimary, true, [this] { openQuitMenu(); }); + else + addEntry("QUIT EMULATIONSTATION", mMenuColorPrimary, false, [this] { openQuitMenu(); }); +#endif + } + + addChild(&mMenu); + addVersionInfo(); + setSize(mMenu.getSize()); + setPosition((mRenderer->getScreenWidth() - mSize.x) / 2.0f, + std::round(mRenderer->getScreenHeight() * 0.13f)); +} + +GuiMenu::~GuiMenu() +{ + if (ViewController::getInstance()->getState().viewing != ViewController::ViewMode::NOTHING) { + // This is required for the situation where scrolling started just before the menu + // was openened. Without this, the scrolling would run until manually stopped after + // the menu has been closed. + ViewController::getInstance()->stopScrolling(); + + ViewController::getInstance()->startViewVideos(); + } +} + +void GuiMenu::openScraperOptions() +{ + // Open the scraper menu. + mWindow->pushGui(new GuiScraperMenu("SCRAPER")); +} + +void GuiMenu::openUIOptions() +{ + auto s = new GuiSettings("UI SETTINGS"); + + // Theme options section. + + std::map themes { + ThemeData::getThemes()}; + std::map::const_iterator + selectedTheme; + + auto theme = std::make_shared>(getHelpStyle(), "THEME", false); + + ComponentListRow themeDownloaderInputRow; + themeDownloaderInputRow.elements.clear(); + themeDownloaderInputRow.addElement(std::make_shared("THEME DOWNLOADER", + Font::get(FONT_SIZE_MEDIUM), + mMenuColorPrimary), + true); + themeDownloaderInputRow.addElement(mMenu.makeArrow(), false); + + themeDownloaderInputRow.makeAcceptInputHandler( + std::bind(&GuiMenu::openThemeDownloader, this, s)); + s->addRow(themeDownloaderInputRow); + + // Theme. + if (!themes.empty()) { + selectedTheme = themes.find(Settings::getInstance()->getString("Theme")); + if (selectedTheme == themes.cend()) + selectedTheme = themes.cbegin(); + std::vector>> themesSorted; + std::string sortName; + for (auto& theme : themes) { + if (theme.second.capabilities.themeName != "") + sortName = theme.second.capabilities.themeName; + else + sortName = theme.first; + themesSorted.emplace_back(std::make_pair(Utils::String::toUpper(sortName), + std::make_pair(theme.first, theme.second))); + } + std::sort(themesSorted.begin(), themesSorted.end(), + [](const auto& a, const auto& b) { return a.first < b.first; }); + for (auto it = themesSorted.cbegin(); it != themesSorted.cend(); ++it) { + // If required, abbreviate the theme name so it doesn't overlap the setting name. + const float maxNameLength {mSize.x * 0.62f}; + std::string themeName {(*it).first}; + theme->add(themeName, it->second.first, (*it).second.first == selectedTheme->first, + maxNameLength); + } + s->addWithLabel("THEME", theme); + s->addSaveFunc([this, theme, s] { + if (theme->getSelected() != Settings::getInstance()->getString("Theme")) { + Scripting::fireEvent("theme-changed", theme->getSelected(), + Settings::getInstance()->getString("Theme")); + // Handle the situation where the previously selected theme has been deleted + // using the theme downloader. In this case attempt to fall back to slate-es-de + // and if this theme doesn't exist then select the first available one. + auto themes = ThemeData::getThemes(); + if (themes.find(theme->getSelected()) == themes.end()) { + if (themes.find("slate-es-de") != themes.end()) + Settings::getInstance()->setString("Theme", "slate-es-de"); + else + Settings::getInstance()->setString("Theme", themes.begin()->first); + } + else { + Settings::getInstance()->setString("Theme", theme->getSelected()); + } + mWindow->setChangedTheme(); + // This is required so that the custom collection system does not disappear + // if the user is editing a custom collection when switching themes. + if (CollectionSystemsManager::getInstance()->isEditing()) + CollectionSystemsManager::getInstance()->exitEditMode(); + s->setNeedsSaving(); + s->setNeedsReloading(); + s->setNeedsGoToStart(); + s->setNeedsCollectionsUpdate(); + s->setInvalidateCachedBackground(); + } + }); + } + + // Theme variants. + auto themeVariant = + std::make_shared>(getHelpStyle(), "THEME VARIANT", false); + s->addWithLabel("THEME VARIANT", themeVariant); + s->addSaveFunc([themeVariant, s] { + if (themeVariant->getSelected() != Settings::getInstance()->getString("ThemeVariant")) { + Settings::getInstance()->setString("ThemeVariant", themeVariant->getSelected()); + s->setNeedsSaving(); + s->setNeedsReloading(); + s->setInvalidateCachedBackground(); + } + }); + + auto themeVariantsFunc = [=](const std::string& selectedTheme, + const std::string& selectedVariant) { + std::map::const_iterator + currentSet {themes.find(selectedTheme)}; + if (currentSet == themes.cend()) + return; + // We need to recreate the OptionListComponent entries. + themeVariant->clearEntries(); + int selectableVariants {0}; + for (auto& variant : currentSet->second.capabilities.variants) { + if (variant.selectable) + ++selectableVariants; + } + if (selectableVariants > 0) { + for (auto& variant : currentSet->second.capabilities.variants) { + if (variant.selectable) { + // If required, abbreviate the variant name so it doesn't overlap the + // setting name. + const float maxNameLength {mSize.x * 0.62f}; + themeVariant->add(variant.label, variant.name, variant.name == selectedVariant, + maxNameLength); + } + } + if (themeVariant->getSelectedObjects().size() == 0) + themeVariant->selectEntry(0); + } + else { + themeVariant->add("None defined", "none", true); + themeVariant->setEnabled(false); + themeVariant->setOpacity(DISABLED_OPACITY); + themeVariant->getParent() + ->getChild(themeVariant->getChildIndex() - 1) + ->setOpacity(DISABLED_OPACITY); + } + }; + + themeVariantsFunc(Settings::getInstance()->getString("Theme"), + Settings::getInstance()->getString("ThemeVariant")); + + // Theme color schemes. + auto themeColorScheme = std::make_shared>( + getHelpStyle(), "THEME COLOR SCHEME", false); + s->addWithLabel("THEME COLOR SCHEME", themeColorScheme); + s->addSaveFunc([themeColorScheme, s] { + if (themeColorScheme->getSelected() != + Settings::getInstance()->getString("ThemeColorScheme")) { + Settings::getInstance()->setString("ThemeColorScheme", themeColorScheme->getSelected()); + s->setNeedsSaving(); + s->setNeedsReloading(); + s->setInvalidateCachedBackground(); + } + }); + + auto themeColorSchemesFunc = [=](const std::string& selectedTheme, + const std::string& selectedColorScheme) { + std::map::const_iterator + currentSet {themes.find(selectedTheme)}; + if (currentSet == themes.cend()) + return; + // We need to recreate the OptionListComponent entries. + themeColorScheme->clearEntries(); + if (currentSet->second.capabilities.colorSchemes.size() > 0) { + for (auto& colorScheme : currentSet->second.capabilities.colorSchemes) { + // If required, abbreviate the color scheme name so it doesn't overlap the + // setting name. + const float maxNameLength {mSize.x * 0.52f}; + themeColorScheme->add(colorScheme.label, colorScheme.name, + colorScheme.name == selectedColorScheme, maxNameLength); + } + if (themeColorScheme->getSelectedObjects().size() == 0) + themeColorScheme->selectEntry(0); + } + else { + themeColorScheme->add("None defined", "none", true); + themeColorScheme->setEnabled(false); + themeColorScheme->setOpacity(DISABLED_OPACITY); + themeColorScheme->getParent() + ->getChild(themeColorScheme->getChildIndex() - 1) + ->setOpacity(DISABLED_OPACITY); + } + }; + + themeColorSchemesFunc(Settings::getInstance()->getString("Theme"), + Settings::getInstance()->getString("ThemeColorScheme")); + + // Theme aspect ratios. + auto themeAspectRatio = std::make_shared>( + getHelpStyle(), "THEME ASPECT RATIO", false); + s->addWithLabel("THEME ASPECT RATIO", themeAspectRatio); + s->addSaveFunc([themeAspectRatio, s] { + if (themeAspectRatio->getSelected() != + Settings::getInstance()->getString("ThemeAspectRatio")) { + Settings::getInstance()->setString("ThemeAspectRatio", themeAspectRatio->getSelected()); + s->setNeedsSaving(); + s->setNeedsReloading(); + s->setInvalidateCachedBackground(); + } + }); + + auto themeAspectRatiosFunc = [=](const std::string& selectedTheme, + const std::string& selectedAspectRatio) { + std::map::const_iterator + currentSet {themes.find(selectedTheme)}; + if (currentSet == themes.cend()) + return; + // We need to recreate the OptionListComponent entries. + themeAspectRatio->clearEntries(); + if (currentSet->second.capabilities.aspectRatios.size() > 0) { + for (auto& aspectRatio : currentSet->second.capabilities.aspectRatios) + themeAspectRatio->add(ThemeData::getAspectRatioLabel(aspectRatio), aspectRatio, + aspectRatio == selectedAspectRatio); + if (themeAspectRatio->getSelectedObjects().size() == 0) + themeAspectRatio->selectEntry(0); + } + else { + themeAspectRatio->add("None defined", "none", true); + themeAspectRatio->setEnabled(false); + themeAspectRatio->setOpacity(DISABLED_OPACITY); + themeAspectRatio->getParent() + ->getChild(themeAspectRatio->getChildIndex() - 1) + ->setOpacity(DISABLED_OPACITY); + } + }; + + themeAspectRatiosFunc(Settings::getInstance()->getString("Theme"), + Settings::getInstance()->getString("ThemeAspectRatio")); + + // Theme transitions. + auto themeTransitions = std::make_shared>( + getHelpStyle(), "THEME TRANSITIONS", false); + std::string selectedThemeTransitions {Settings::getInstance()->getString("ThemeTransitions")}; + themeTransitions->add("AUTOMATIC", "automatic", selectedThemeTransitions == "automatic"); + // If there are no objects returned, then there must be a manually modified entry in the + // configuration file. Simply set theme transitions to "automatic" in this case. + if (themeTransitions->getSelectedObjects().size() == 0) + themeTransitions->selectEntry(0); + s->addWithLabel("THEME TRANSITIONS", themeTransitions); + s->addSaveFunc([themeTransitions, s] { + if (themeTransitions->getSelected() != + Settings::getInstance()->getString("ThemeTransitions")) { + Settings::getInstance()->setString("ThemeTransitions", themeTransitions->getSelected()); + ThemeData::setThemeTransitions(); + s->setNeedsSaving(); + } + }); + + auto themeTransitionsFunc = [=](const std::string& selectedTheme, + const std::string& selectedThemeTransitions) { + std::map::const_iterator + currentSet {themes.find(selectedTheme)}; + if (currentSet == themes.cend()) + return; + // We need to recreate the OptionListComponent entries. + themeTransitions->clearEntries(); + themeTransitions->add("AUTOMATIC", "automatic", "automatic" == selectedThemeTransitions); + if (currentSet->second.capabilities.transitions.size() == 1 && + currentSet->second.capabilities.transitions.front().selectable) { + std::string label; + if (currentSet->second.capabilities.transitions.front().label == "") + label = "THEME PROFILE"; + else + label = currentSet->second.capabilities.transitions.front().label; + const std::string transitions { + currentSet->second.capabilities.transitions.front().name}; + themeTransitions->add(label, transitions, transitions == selectedThemeTransitions); + } + else { + for (size_t i {0}; i < currentSet->second.capabilities.transitions.size(); ++i) { + if (!currentSet->second.capabilities.transitions[i].selectable) + continue; + std::string label; + if (currentSet->second.capabilities.transitions[i].label == "") + label = "THEME PROFILE " + std::to_string(i + 1); + else + label = currentSet->second.capabilities.transitions[i].label; + const std::string transitions {currentSet->second.capabilities.transitions[i].name}; + themeTransitions->add(label, transitions, transitions == selectedThemeTransitions); + } + } + if (std::find(currentSet->second.capabilities.suppressedTransitionProfiles.cbegin(), + currentSet->second.capabilities.suppressedTransitionProfiles.cend(), + "builtin-instant") == + currentSet->second.capabilities.suppressedTransitionProfiles.cend()) { + themeTransitions->add("INSTANT (BUILT-IN)", "builtin-instant", + "builtin-instant" == selectedThemeTransitions); + } + if (std::find(currentSet->second.capabilities.suppressedTransitionProfiles.cbegin(), + currentSet->second.capabilities.suppressedTransitionProfiles.cend(), + "builtin-slide") == + currentSet->second.capabilities.suppressedTransitionProfiles.cend()) { + themeTransitions->add("SLIDE (BUILT-IN)", "builtin-slide", + "builtin-slide" == selectedThemeTransitions); + } + if (std::find(currentSet->second.capabilities.suppressedTransitionProfiles.cbegin(), + currentSet->second.capabilities.suppressedTransitionProfiles.cend(), + "builtin-fade") == + currentSet->second.capabilities.suppressedTransitionProfiles.cend()) { + themeTransitions->add("FADE (BUILT-IN)", "builtin-fade", + "builtin-fade" == selectedThemeTransitions); + } + if (themeTransitions->getSelectedObjects().size() == 0) + themeTransitions->selectEntry(0); + + if (themeTransitions->getNumEntries() == 1) { + themeTransitions->setEnabled(false); + themeTransitions->setOpacity(DISABLED_OPACITY); + themeTransitions->getParent() + ->getChild(themeTransitions->getChildIndex() - 1) + ->setOpacity(DISABLED_OPACITY); + } + else { + themeTransitions->setEnabled(true); + themeTransitions->setOpacity(1.0f); + themeTransitions->getParent() + ->getChild(themeTransitions->getChildIndex() - 1) + ->setOpacity(1.0f); + } + }; + + themeTransitionsFunc(Settings::getInstance()->getString("Theme"), + Settings::getInstance()->getString("ThemeTransitions")); + + // Quick system select (navigate between systems in the gamelist view). + auto quickSystemSelect = std::make_shared>( + getHelpStyle(), "QUICK SYSTEM SELECT", false); + std::string selectedQuickSelect {Settings::getInstance()->getString("QuickSystemSelect")}; + quickSystemSelect->add("LEFT/RIGHT OR SHOULDERS", "leftrightshoulders", + selectedQuickSelect == "leftrightshoulders"); + quickSystemSelect->add("LEFT/RIGHT OR TRIGGERS", "leftrighttriggers", + selectedQuickSelect == "leftrighttriggers"); + quickSystemSelect->add("SHOULDERS", "shoulders", selectedQuickSelect == "shoulders"); + quickSystemSelect->add("TRIGGERS", "triggers", selectedQuickSelect == "triggers"); + quickSystemSelect->add("LEFT/RIGHT", "leftright", selectedQuickSelect == "leftright"); + quickSystemSelect->add("DISABLED", "disabled", selectedQuickSelect == "disabled"); + // If there are no objects returned, then there must be a manually modified entry in the + // configuration file. Simply set the quick system select to "leftrightshoulders" in this case. + if (quickSystemSelect->getSelectedObjects().size() == 0) + quickSystemSelect->selectEntry(0); + s->addWithLabel("QUICK SYSTEM SELECT", quickSystemSelect); + s->addSaveFunc([quickSystemSelect, s] { + if (quickSystemSelect->getSelected() != + Settings::getInstance()->getString("QuickSystemSelect")) { + Settings::getInstance()->setString("QuickSystemSelect", + quickSystemSelect->getSelected()); + s->setNeedsSaving(); + } + }); + + // Optionally start in selected system/gamelist. + auto startupSystem = std::make_shared>( + getHelpStyle(), "GAMELIST ON STARTUP", false); + startupSystem->add("NONE", "", Settings::getInstance()->getString("StartupSystem") == ""); + for (auto it = SystemData::sSystemVector.cbegin(); // Line break. + it != SystemData::sSystemVector.cend(); ++it) { + // If required, abbreviate the system name so it doesn't overlap the setting name. + float maxNameLength {mSize.x * 0.51f}; + startupSystem->add((*it)->getFullName(), (*it)->getName(), + Settings::getInstance()->getString("StartupSystem") == (*it)->getName(), + maxNameLength); + } + // This can probably not happen but as an extra precaution select the "NONE" entry if no + // entry is selected. + if (startupSystem->getSelectedObjects().size() == 0) + startupSystem->selectEntry(0); + s->addWithLabel("GAMELIST ON STARTUP", startupSystem); + s->addSaveFunc([startupSystem, s] { + if (startupSystem->getSelected() != Settings::getInstance()->getString("StartupSystem")) { + Settings::getInstance()->setString("StartupSystem", startupSystem->getSelected()); + s->setNeedsSaving(); + } + }); + + // Systems sorting. + auto systemsSorting = std::make_shared>( + getHelpStyle(), "SYSTEMS SORTING", false); + std::string selectedSystemsSorting {Settings::getInstance()->getString("SystemsSorting")}; + systemsSorting->add("FULL NAMES OR CUSTOM", "default", selectedSystemsSorting == "default"); + systemsSorting->add("RELEASE YEAR", "year", selectedSystemsSorting == "year"); + systemsSorting->add("MANUFACTURER, RELEASE YEAR", "manufacturer_year", + selectedSystemsSorting == "manufacturer_year"); + systemsSorting->add("HW TYPE, RELEASE YEAR", "hwtype_year", + selectedSystemsSorting == "hwtype_year"); + systemsSorting->add("MANUFACTURER, HW TYPE, REL. YEAR", "manufacturer_hwtype_year", + selectedSystemsSorting == "manufacturer_hwtype_year"); + // If there are no objects returned, then there must be a manually modified entry in the + // configuration file. Simply set the systems sorting to "default" in this case. + if (systemsSorting->getSelectedObjects().size() == 0) + systemsSorting->selectEntry(0); + s->addWithLabel("SYSTEMS SORTING", systemsSorting); + s->addSaveFunc([this, systemsSorting, s] { + if (systemsSorting->getSelected() != Settings::getInstance()->getString("SystemsSorting")) { + Settings::getInstance()->setString("SystemsSorting", systemsSorting->getSelected()); + s->setNeedsSaving(); + if (mThemeDownloaderReloadCounter == 0) + s->setNeedsCloseMenu([this] { delete this; }); + else + ++mThemeDownloaderReloadCounter; + s->setNeedsRescanROMDirectory(); + } + }); + + // Default gamelist sort order. + std::string sortOrder; + auto defaultSortOrder = std::make_shared>( + getHelpStyle(), "DEFAULT SORT ORDER", false); + // Exclude the System sort options. + unsigned int numSortTypes {static_cast(FileSorts::SortTypes.size() - 2)}; + for (unsigned int i {0}; i < numSortTypes; ++i) { + if (FileSorts::SortTypes[i].description == + Settings::getInstance()->getString("DefaultSortOrder")) { + sortOrder = FileSorts::SortTypes[i].description; + break; + } + } + // If an invalid sort order was defined in es_settings.xml, then apply the default + // sort order "name, ascending". + if (sortOrder == "") { + sortOrder = Settings::getInstance()->getDefaultString("DefaultSortOrder"); + Settings::getInstance()->setString("DefaultSortOrder", sortOrder); + s->setNeedsSaving(); + } + for (unsigned int i {0}; i < numSortTypes; ++i) { + const FileData::SortType& sort {FileSorts::SortTypes[i]}; + if (sort.description == sortOrder) + defaultSortOrder->add(sort.description, &sort, true); + else + defaultSortOrder->add(sort.description, &sort, false); + } + s->addWithLabel("GAMES DEFAULT SORT ORDER", defaultSortOrder); + s->addSaveFunc([defaultSortOrder, sortOrder, s] { + std::string selectedSortOrder {defaultSortOrder.get()->getSelected()->description}; + if (selectedSortOrder != sortOrder) { + Settings::getInstance()->setString("DefaultSortOrder", selectedSortOrder); + s->setNeedsSaving(); + s->setNeedsSorting(); + s->setNeedsSortingCollections(); + s->setInvalidateCachedBackground(); + } + }); + + // Menu color scheme. + auto menuColorScheme = std::make_shared>( + getHelpStyle(), "MENU COLOR SCHEME", false); + const std::string selectedMenuColor {Settings::getInstance()->getString("MenuColorScheme")}; + menuColorScheme->add("DARK", "dark", selectedMenuColor == "dark"); + menuColorScheme->add("LIGHT", "light", selectedMenuColor == "light"); + // If there are no objects returned, then there must be a manually modified entry in the + // configuration file. Simply set the menu color scheme to "dark" in this case. + if (menuColorScheme->getSelectedObjects().size() == 0) + menuColorScheme->selectEntry(0); + s->addWithLabel("MENU COLOR SCHEME", menuColorScheme); + s->addSaveFunc([this, menuColorScheme, s] { + if (menuColorScheme->getSelected() != + Settings::getInstance()->getString("MenuColorScheme")) { + Settings::getInstance()->setString("MenuColorScheme", menuColorScheme->getSelected()); + ViewController::getInstance()->setMenuColors(); + s->setNeedsSaving(); + if (mThemeDownloaderReloadCounter == 0) + s->setNeedsCloseMenu([this] { delete this; }); + else + ++mThemeDownloaderReloadCounter; + } + }); + + // Open menu effect. + auto menuOpeningEffect = std::make_shared>( + getHelpStyle(), "MENU OPENING EFFECT", false); + std::string selectedMenuEffect {Settings::getInstance()->getString("MenuOpeningEffect")}; + menuOpeningEffect->add("SCALE-UP", "scale-up", selectedMenuEffect == "scale-up"); + menuOpeningEffect->add("NONE", "none", selectedMenuEffect == "none"); + // If there are no objects returned, then there must be a manually modified entry in the + // configuration file. Simply set the opening effect to "scale-up" in this case. + if (menuOpeningEffect->getSelectedObjects().size() == 0) + menuOpeningEffect->selectEntry(0); + s->addWithLabel("MENU OPENING EFFECT", menuOpeningEffect); + s->addSaveFunc([menuOpeningEffect, s] { + if (menuOpeningEffect->getSelected() != + Settings::getInstance()->getString("MenuOpeningEffect")) { + Settings::getInstance()->setString("MenuOpeningEffect", + menuOpeningEffect->getSelected()); + s->setNeedsSaving(); + } + }); + + // Launch screen duration. + auto launchScreenDuration = std::make_shared>( + getHelpStyle(), "LAUNCH SCREEN DURATION", false); + std::string selectedDuration {Settings::getInstance()->getString("LaunchScreenDuration")}; + launchScreenDuration->add("NORMAL", "normal", selectedDuration == "normal"); + launchScreenDuration->add("BRIEF", "brief", selectedDuration == "brief"); + launchScreenDuration->add("LONG", "long", selectedDuration == "long"); + launchScreenDuration->add("DISABLED", "disabled", selectedDuration == "disabled"); + // If there are no objects returned, then there must be a manually modified entry in the + // configuration file. Simply set the duration to "normal" in this case. + if (launchScreenDuration->getSelectedObjects().size() == 0) + launchScreenDuration->selectEntry(0); + s->addWithLabel("LAUNCH SCREEN DURATION", launchScreenDuration); + s->addSaveFunc([launchScreenDuration, s] { + if (launchScreenDuration->getSelected() != + Settings::getInstance()->getString("LaunchScreenDuration")) { + Settings::getInstance()->setString("LaunchScreenDuration", + launchScreenDuration->getSelected()); + s->setNeedsSaving(); + } + }); + + // UI mode. + auto uiMode = + std::make_shared>(getHelpStyle(), "UI MODE", false); + std::vector uiModes; + uiModes.push_back("full"); + uiModes.push_back("kiosk"); + uiModes.push_back("kid"); + std::string setMode; + if (Settings::getInstance()->getBool("ForceKiosk")) + setMode = "kiosk"; + else if (Settings::getInstance()->getBool("ForceKid")) + setMode = "kid"; + else + setMode = Settings::getInstance()->getString("UIMode"); + for (auto it = uiModes.cbegin(); it != uiModes.cend(); ++it) + uiMode->add(*it, *it, setMode == *it); + s->addWithLabel("UI MODE", uiMode); + s->addSaveFunc([uiMode, this, s] { + std::string selectedMode {uiMode->getSelected()}; + // If any of the force flags are set, then always apply and save the setting. + if (selectedMode == Settings::getInstance()->getString("UIMode") && + !Settings::getInstance()->getBool("ForceFull") && + !Settings::getInstance()->getBool("ForceKiosk") && + !Settings::getInstance()->getBool("ForceKid")) { + return; + } + else if (selectedMode != "full") { + std::string msg {"YOU ARE CHANGING THE UI TO THE RESTRICTED MODE\n'" + + Utils::String::toUpper(selectedMode) + "'\n"}; + if (selectedMode == "kiosk") { + msg.append("THIS WILL HIDE MOST MENU OPTIONS TO PREVENT\n"); + msg.append("CHANGES TO THE SYSTEM\n"); + } + else { + msg.append("THIS WILL LIMIT THE AVAILABLE GAMES TO THE ONES\n"); + msg.append("FLAGGED SUITABLE FOR CHILDREN\n"); + } + msg.append("TO UNLOCK AND RETURN TO THE FULL UI, ENTER THIS CODE: \n") + .append(UIModeController::getInstance()->getFormattedPassKeyStr()) + .append("\n\n") + .append("DO YOU WANT TO PROCEED?"); + mWindow->pushGui(new GuiMsgBox( + this->getHelpStyle(), msg, "YES", + [this, selectedMode] { + LOG(LogDebug) << "GuiMenu::openUISettings(): Setting UI mode to '" + << selectedMode << "'."; + Settings::getInstance()->setString("UIMode", selectedMode); + Settings::getInstance()->setBool("ForceFull", false); + Settings::getInstance()->setBool("ForceKiosk", false); + Settings::getInstance()->setBool("ForceKid", false); + Settings::getInstance()->saveFile(); + if (CollectionSystemsManager::getInstance()->isEditing()) + CollectionSystemsManager::getInstance()->exitEditMode(); + UIModeController::getInstance()->setCurrentUIMode(selectedMode); + for (auto it = SystemData::sSystemVector.cbegin(); + it != SystemData::sSystemVector.cend(); ++it) { + if ((*it)->getThemeFolder() == "custom-collections") { + for (FileData* customSystem : + (*it)->getRootFolder()->getChildrenListToDisplay()) + customSystem->getSystem()->getIndex()->resetFilters(); + } + (*it)->sortSystem(); + (*it)->getIndex()->resetFilters(); + } + ViewController::getInstance()->reloadAll(); + ViewController::getInstance()->goToSystem(SystemData::sSystemVector.front(), + false); + mWindow->invalidateCachedBackground(); + }, + "NO", nullptr, "", nullptr, nullptr, true)); + } + else { + LOG(LogDebug) << "GuiMenu::openUISettings(): Setting UI mode to '" << selectedMode + << "'."; + Settings::getInstance()->setString("UIMode", uiMode->getSelected()); + Settings::getInstance()->setBool("ForceFull", false); + Settings::getInstance()->setBool("ForceKiosk", false); + Settings::getInstance()->setBool("ForceKid", false); + UIModeController::getInstance()->setCurrentUIMode("full"); + s->setNeedsSaving(); + s->setNeedsSorting(); + s->setNeedsSortingCollections(); + s->setNeedsResetFilters(); + s->setNeedsReloading(); + s->setNeedsGoToSystem(SystemData::sSystemVector.front()); + s->setInvalidateCachedBackground(); + } + }); + + // Random entry button. + auto randomEntryButton = std::make_shared>( + getHelpStyle(), "RANDOM ENTRY BUTTON", false); + const std::string selectedRandomEntryButton { + Settings::getInstance()->getString("RandomEntryButton")}; + randomEntryButton->add("GAMES ONLY", "games", selectedRandomEntryButton == "games"); + randomEntryButton->add("GAMES AND SYSTEMS", "gamessystems", + selectedRandomEntryButton == "gamessystems"); + randomEntryButton->add("DISABLED", "disabled", selectedRandomEntryButton == "disabled"); + // If there are no objects returned, then there must be a manually modified entry in the + // configuration file. Simply set the random entry button to "games" in this case. + if (randomEntryButton->getSelectedObjects().size() == 0) + randomEntryButton->selectEntry(0); + s->addWithLabel("RANDOM ENTRY BUTTON", randomEntryButton); + s->addSaveFunc([randomEntryButton, s] { + if (randomEntryButton->getSelected() != + Settings::getInstance()->getString("RandomEntryButton")) { + Settings::getInstance()->setString("RandomEntryButton", + randomEntryButton->getSelected()); + s->setNeedsSaving(); + } + }); + + // Media viewer. + ComponentListRow mediaViewerRow; + mediaViewerRow.elements.clear(); + mediaViewerRow.addElement(std::make_shared("MEDIA VIEWER SETTINGS", + Font::get(FONT_SIZE_MEDIUM), + mMenuColorPrimary), + true); + mediaViewerRow.addElement(mMenu.makeArrow(), false); + mediaViewerRow.makeAcceptInputHandler(std::bind(&GuiMenu::openMediaViewerOptions, this)); + s->addRow(mediaViewerRow); + + // Screensaver. + ComponentListRow screensaverRow; + screensaverRow.elements.clear(); + screensaverRow.addElement(std::make_shared("SCREENSAVER SETTINGS", + Font::get(FONT_SIZE_MEDIUM), + mMenuColorPrimary), + true); + screensaverRow.addElement(mMenu.makeArrow(), false); + screensaverRow.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); + s->addRow(screensaverRow); + + // Enable theme variant triggers. + auto themeVariantTriggers = std::make_shared(); + themeVariantTriggers->setState(Settings::getInstance()->getBool("ThemeVariantTriggers")); + s->addWithLabel("ENABLE THEME VARIANT TRIGGERS", themeVariantTriggers); + s->addSaveFunc([themeVariantTriggers, s] { + if (themeVariantTriggers->getState() != + Settings::getInstance()->getBool("ThemeVariantTriggers")) { + Settings::getInstance()->setBool("ThemeVariantTriggers", + themeVariantTriggers->getState()); + s->setNeedsSaving(); + s->setNeedsReloading(); + s->setInvalidateCachedBackground(); + } + }); + + // Blur background when the menu is open. + auto menuBlurBackground = std::make_shared(); + if (mRenderer->getScreenRotation() == 90 || mRenderer->getScreenRotation() == 270) { + // TODO: Add support for non-blurred background when rotating screen 90 or 270 degrees. + menuBlurBackground->setState(true); + s->addWithLabel("BLUR BACKGROUND WHEN MENU IS OPEN", menuBlurBackground); + menuBlurBackground->setEnabled(false); + menuBlurBackground->setOpacity(DISABLED_OPACITY); + menuBlurBackground->getParent() + ->getChild(menuBlurBackground->getChildIndex() - 1) + ->setOpacity(DISABLED_OPACITY); + } + else { + menuBlurBackground->setState(Settings::getInstance()->getBool("MenuBlurBackground")); + s->addWithLabel("BLUR BACKGROUND WHEN MENU IS OPEN", menuBlurBackground); + s->addSaveFunc([menuBlurBackground, s] { + if (menuBlurBackground->getState() != + Settings::getInstance()->getBool("MenuBlurBackground")) { + Settings::getInstance()->setBool("MenuBlurBackground", + menuBlurBackground->getState()); + s->setNeedsSaving(); + s->setInvalidateCachedBackground(); + } + }); + } + + // Sort folders on top of the gamelists. + auto foldersOnTop = std::make_shared(); + foldersOnTop->setState(Settings::getInstance()->getBool("FoldersOnTop")); + s->addWithLabel("SORT FOLDERS ON TOP OF GAMELISTS", foldersOnTop); + s->addSaveFunc([foldersOnTop, s] { + if (foldersOnTop->getState() != Settings::getInstance()->getBool("FoldersOnTop")) { + Settings::getInstance()->setBool("FoldersOnTop", foldersOnTop->getState()); + s->setNeedsSaving(); + s->setNeedsSorting(); + s->setInvalidateCachedBackground(); + } + }); + + // Sort favorites on top of non-favorites in the gamelists. + auto favoritesFirst = std::make_shared(); + favoritesFirst->setState(Settings::getInstance()->getBool("FavoritesFirst")); + s->addWithLabel("SORT FAVORITE GAMES ABOVE NON-FAVORITES", favoritesFirst); + s->addSaveFunc([favoritesFirst, s] { + if (favoritesFirst->getState() != Settings::getInstance()->getBool("FavoritesFirst")) { + Settings::getInstance()->setBool("FavoritesFirst", favoritesFirst->getState()); + s->setNeedsSaving(); + s->setNeedsSorting(); + s->setNeedsSortingCollections(); + s->setInvalidateCachedBackground(); + } + }); + + // Enable gamelist star markings for favorite games. + auto favoritesStar = std::make_shared(); + favoritesStar->setState(Settings::getInstance()->getBool("FavoritesStar")); + s->addWithLabel("ADD STAR MARKINGS TO FAVORITE GAMES", favoritesStar); + s->addSaveFunc([favoritesStar, s] { + if (favoritesStar->getState() != Settings::getInstance()->getBool("FavoritesStar")) { + Settings::getInstance()->setBool("FavoritesStar", favoritesStar->getState()); + s->setNeedsSaving(); + s->setNeedsReloading(); + s->setInvalidateCachedBackground(); + } + }); + + // Enable quick list scrolling overlay. + auto listScrollOverlay = std::make_shared(); + listScrollOverlay->setState(Settings::getInstance()->getBool("ListScrollOverlay")); + s->addWithLabel("ENABLE TEXTLIST QUICK SCROLLING OVERLAY", listScrollOverlay); + s->addSaveFunc([listScrollOverlay, s] { + if (listScrollOverlay->getState() != + Settings::getInstance()->getBool("ListScrollOverlay")) { + Settings::getInstance()->setBool("ListScrollOverlay", listScrollOverlay->getState()); + s->setNeedsSaving(); + } + }); + + // Enable virtual (on-screen) keyboard. + auto virtualKeyboard = std::make_shared(); + virtualKeyboard->setState(Settings::getInstance()->getBool("VirtualKeyboard")); + s->addWithLabel("ENABLE VIRTUAL KEYBOARD", virtualKeyboard); + s->addSaveFunc([virtualKeyboard, s] { + if (virtualKeyboard->getState() != Settings::getInstance()->getBool("VirtualKeyboard")) { + Settings::getInstance()->setBool("VirtualKeyboard", virtualKeyboard->getState()); + s->setNeedsSaving(); + s->setInvalidateCachedBackground(); + } + }); + + // Enable the 'Y' button for tagging games as favorites. + auto favoritesAddButton = std::make_shared(); + favoritesAddButton->setState(Settings::getInstance()->getBool("FavoritesAddButton")); + s->addWithLabel("ENABLE TOGGLE FAVORITES BUTTON", favoritesAddButton); + s->addSaveFunc([favoritesAddButton, s] { + if (Settings::getInstance()->getBool("FavoritesAddButton") != + favoritesAddButton->getState()) { + Settings::getInstance()->setBool("FavoritesAddButton", favoritesAddButton->getState()); + s->setNeedsSaving(); + } + }); + + // Gamelist filters. + auto gamelistFilters = std::make_shared(); + gamelistFilters->setState(Settings::getInstance()->getBool("GamelistFilters")); + s->addWithLabel("ENABLE GAMELIST FILTERS", gamelistFilters); + s->addSaveFunc([gamelistFilters, s] { + if (Settings::getInstance()->getBool("GamelistFilters") != gamelistFilters->getState()) { + Settings::getInstance()->setBool("GamelistFilters", gamelistFilters->getState()); + s->setNeedsSaving(); + s->setNeedsReloading(); + } + }); + + // On-screen help prompts. + auto showHelpPrompts = std::make_shared(); + showHelpPrompts->setState(Settings::getInstance()->getBool("ShowHelpPrompts")); + s->addWithLabel("DISPLAY ON-SCREEN HELP", showHelpPrompts); + s->addSaveFunc([showHelpPrompts, s] { + if (Settings::getInstance()->getBool("ShowHelpPrompts") != showHelpPrompts->getState()) { + Settings::getInstance()->setBool("ShowHelpPrompts", showHelpPrompts->getState()); + s->setNeedsSaving(); + } + }); + + // When the theme entries are scrolled or selected, update the relevant rows. + auto scrollThemeFunc = [=](const std::string& themeName, bool firstRun = false) { + auto selectedTheme = themes.find(themeName); + if (selectedTheme == themes.cend()) + return; + if (!firstRun) { + themeVariantsFunc(themeName, themeVariant->getSelected()); + themeColorSchemesFunc(themeName, themeColorScheme->getSelected()); + themeAspectRatiosFunc(themeName, themeAspectRatio->getSelected()); + themeTransitionsFunc(themeName, themeTransitions->getSelected()); + } + int selectableVariants {0}; + for (auto& variant : selectedTheme->second.capabilities.variants) { + if (variant.selectable) + ++selectableVariants; + } + if (selectableVariants > 0) { + themeVariant->setEnabled(true); + themeVariant->setOpacity(1.0f); + themeVariant->getParent() + ->getChild(themeVariant->getChildIndex() - 1) + ->setOpacity(1.0f); + } + else { + themeVariant->setEnabled(false); + themeVariant->setOpacity(DISABLED_OPACITY); + themeVariant->getParent() + ->getChild(themeVariant->getChildIndex() - 1) + ->setOpacity(DISABLED_OPACITY); + } + if (selectedTheme->second.capabilities.colorSchemes.size() > 0) { + themeColorScheme->setEnabled(true); + themeColorScheme->setOpacity(1.0f); + themeColorScheme->getParent() + ->getChild(themeColorScheme->getChildIndex() - 1) + ->setOpacity(1.0f); + } + else { + themeColorScheme->setEnabled(false); + themeColorScheme->setOpacity(DISABLED_OPACITY); + themeColorScheme->getParent() + ->getChild(themeColorScheme->getChildIndex() - 1) + ->setOpacity(DISABLED_OPACITY); + } + if (selectedTheme->second.capabilities.aspectRatios.size() > 0) { + themeAspectRatio->setEnabled(true); + themeAspectRatio->setOpacity(1.0f); + themeAspectRatio->getParent() + ->getChild(themeAspectRatio->getChildIndex() - 1) + ->setOpacity(1.0f); + } + else { + themeAspectRatio->setEnabled(false); + themeAspectRatio->setOpacity(DISABLED_OPACITY); + themeAspectRatio->getParent() + ->getChild(themeAspectRatio->getChildIndex() - 1) + ->setOpacity(DISABLED_OPACITY); + } + }; + + scrollThemeFunc(selectedTheme->first, true); + theme->setCallback(scrollThemeFunc); + + s->setSize(mSize); + mWindow->pushGui(s); +} + +void GuiMenu::openSoundOptions() +{ + auto s = new GuiSettings("SOUND SETTINGS"); + +// TODO: Hide the volume slider on macOS and BSD Unix until the volume control logic has been +// implemented for these operating systems. +#if !defined(__APPLE__) && !defined(__FreeBSD__) && !defined(__OpenBSD__) && !defined(__NetBSD__) + // System volume. + // The reason to create the VolumeControl object every time instead of making it a singleton + // is that this is the easiest way to detect new default audio devices or changes to the + // audio volume done by the operating system. And we don't really need this object laying + // around anyway as it's only used here. + VolumeControl volumeControl; + int currentVolume {volumeControl.getVolume()}; + + auto systemVolume = std::make_shared(0.0f, 100.0f, 1.0f, "%"); + systemVolume->setValue(static_cast(currentVolume)); + s->addWithLabel("SYSTEM VOLUME", systemVolume); + s->addSaveFunc([systemVolume, currentVolume] { + // No need to create the VolumeControl object unless the volume has actually been changed. + if (static_cast(systemVolume->getValue()) != currentVolume) { + VolumeControl volumeControl; + volumeControl.setVolume(static_cast(std::round(systemVolume->getValue()))); + } + }); +#endif + + // Volume for navigation sounds. + auto soundVolumeNavigation = std::make_shared(0.0f, 100.0f, 1.0f, "%"); + soundVolumeNavigation->setValue( + static_cast(Settings::getInstance()->getInt("SoundVolumeNavigation"))); + s->addWithLabel("NAVIGATION SOUNDS VOLUME", soundVolumeNavigation); + s->addSaveFunc([soundVolumeNavigation, s] { + if (soundVolumeNavigation->getValue() != + static_cast(Settings::getInstance()->getInt("SoundVolumeNavigation"))) { + Settings::getInstance()->setInt("SoundVolumeNavigation", + static_cast(soundVolumeNavigation->getValue())); + s->setNeedsSaving(); + } + }); + + // Volume for videos. + auto soundVolumeVideos = std::make_shared(0.0f, 100.0f, 1.0f, "%"); + soundVolumeVideos->setValue( + static_cast(Settings::getInstance()->getInt("SoundVolumeVideos"))); + s->addWithLabel("VIDEO PLAYER VOLUME", soundVolumeVideos); + s->addSaveFunc([soundVolumeVideos, s] { + if (soundVolumeVideos->getValue() != + static_cast(Settings::getInstance()->getInt("SoundVolumeVideos"))) { + Settings::getInstance()->setInt("SoundVolumeVideos", + static_cast(soundVolumeVideos->getValue())); + s->setNeedsSaving(); + } + }); + + if (UIModeController::getInstance()->isUIModeFull()) { + // Play audio for gamelist videos. + auto viewsVideoAudio = std::make_shared(); + viewsVideoAudio->setState(Settings::getInstance()->getBool("ViewsVideoAudio")); + s->addWithLabel("PLAY AUDIO FOR GAMELIST AND SYSTEM VIEW VIDEOS", viewsVideoAudio); + s->addSaveFunc([viewsVideoAudio, s] { + if (viewsVideoAudio->getState() != + Settings::getInstance()->getBool("ViewsVideoAudio")) { + Settings::getInstance()->setBool("ViewsVideoAudio", viewsVideoAudio->getState()); + s->setNeedsSaving(); + } + }); + + // Play audio for media viewer videos. + auto mediaViewerVideoAudio = std::make_shared(); + mediaViewerVideoAudio->setState(Settings::getInstance()->getBool("MediaViewerVideoAudio")); + s->addWithLabel("PLAY AUDIO FOR MEDIA VIEWER VIDEOS", mediaViewerVideoAudio); + s->addSaveFunc([mediaViewerVideoAudio, s] { + if (mediaViewerVideoAudio->getState() != + Settings::getInstance()->getBool("MediaViewerVideoAudio")) { + Settings::getInstance()->setBool("MediaViewerVideoAudio", + mediaViewerVideoAudio->getState()); + s->setNeedsSaving(); + } + }); + + // Play audio for screensaver videos. + auto screensaverVideoAudio = std::make_shared(); + screensaverVideoAudio->setState(Settings::getInstance()->getBool("ScreensaverVideoAudio")); + s->addWithLabel("PLAY AUDIO FOR SCREENSAVER VIDEOS", screensaverVideoAudio); + s->addSaveFunc([screensaverVideoAudio, s] { + if (screensaverVideoAudio->getState() != + Settings::getInstance()->getBool("ScreensaverVideoAudio")) { + Settings::getInstance()->setBool("ScreensaverVideoAudio", + screensaverVideoAudio->getState()); + s->setNeedsSaving(); + } + }); + + // Navigation sounds. + auto navigationSounds = std::make_shared(); + navigationSounds->setState(Settings::getInstance()->getBool("NavigationSounds")); + s->addWithLabel("ENABLE NAVIGATION SOUNDS", navigationSounds); + s->addSaveFunc([navigationSounds, s] { + if (navigationSounds->getState() != + Settings::getInstance()->getBool("NavigationSounds")) { + Settings::getInstance()->setBool("NavigationSounds", navigationSounds->getState()); + s->setNeedsSaving(); + } + }); + } + + s->setSize(mSize); + mWindow->pushGui(s); +} + +void GuiMenu::openInputDeviceOptions() +{ + auto s = new GuiSettings("INPUT DEVICE SETTINGS"); + + // Controller type. + auto inputControllerType = std::make_shared>( + getHelpStyle(), "CONTROLLER TYPE", false); + std::string selectedPlayer {Settings::getInstance()->getString("InputControllerType")}; + inputControllerType->add("XBOX", "xbox", selectedPlayer == "xbox"); + inputControllerType->add("XBOX 360", "xbox360", selectedPlayer == "xbox360"); + inputControllerType->add("PLAYSTATION 1/2/3", "ps123", selectedPlayer == "ps123"); + inputControllerType->add("PLAYSTATION 4", "ps4", selectedPlayer == "ps4"); + inputControllerType->add("PLAYSTATION 5", "ps5", selectedPlayer == "ps5"); + inputControllerType->add("SWITCH PRO", "switchpro", selectedPlayer == "switchpro"); + inputControllerType->add("SNES", "snes", selectedPlayer == "snes"); + // If there are no objects returned, then there must be a manually modified entry in the + // configuration file. Simply set the controller type to "xbox" in this case. + if (inputControllerType->getSelectedObjects().size() == 0) + inputControllerType->selectEntry(0); + s->addWithLabel("CONTROLLER TYPE", inputControllerType); + s->addSaveFunc([inputControllerType, s] { + if (inputControllerType->getSelected() != + Settings::getInstance()->getString("InputControllerType")) { + Settings::getInstance()->setString("InputControllerType", + inputControllerType->getSelected()); + s->setNeedsSaving(); + } + }); + + // Whether to only accept input from the first controller. + auto inputOnlyFirstController = std::make_shared(); + inputOnlyFirstController->setState( + Settings::getInstance()->getBool("InputOnlyFirstController")); + s->addWithLabel("ONLY ACCEPT INPUT FROM FIRST CONTROLLER", inputOnlyFirstController); + s->addSaveFunc([inputOnlyFirstController, s] { + if (Settings::getInstance()->getBool("InputOnlyFirstController") != + inputOnlyFirstController->getState()) { + Settings::getInstance()->setBool("InputOnlyFirstController", + inputOnlyFirstController->getState()); + s->setNeedsSaving(); + } + }); + + // Whether to ignore keyboard input (except the quit shortcut). + auto inputIgnoreKeyboard = std::make_shared(); + inputIgnoreKeyboard->setState(Settings::getInstance()->getBool("InputIgnoreKeyboard")); + s->addWithLabel("IGNORE KEYBOARD INPUT", inputIgnoreKeyboard); + s->addSaveFunc([inputIgnoreKeyboard, s] { + if (Settings::getInstance()->getBool("InputIgnoreKeyboard") != + inputIgnoreKeyboard->getState()) { + Settings::getInstance()->setBool("InputIgnoreKeyboard", + inputIgnoreKeyboard->getState()); + s->setNeedsSaving(); + } + }); + + // Configure keyboard and controllers. + ComponentListRow configureInputRow; + configureInputRow.elements.clear(); + configureInputRow.addElement( + std::make_shared("CONFIGURE KEYBOARD AND CONTROLLERS", + Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary), + true); + configureInputRow.addElement(mMenu.makeArrow(), false); + configureInputRow.makeAcceptInputHandler(std::bind(&GuiMenu::openConfigInput, this, s)); + s->addRow(configureInputRow); + + s->setSize(mSize); + mWindow->pushGui(s); +} + +void GuiMenu::openConfigInput(GuiSettings* settings) +{ + // Always save the settings before starting the input configuration, in case the + // controller type was changed. + settings->save(); + // Also unset the save flag so that a double saving does not take place when closing + // the input device settings menu later on. + settings->setNeedsSaving(false); + + std::string message { + "THE KEYBOARD AND CONTROLLERS ARE AUTOMATICALLY CONFIGURED, BUT USING THIS " + "CONFIGURATION TOOL YOU CAN OVERRIDE THE DEFAULT BUTTON MAPPINGS (THIS WILL NOT " + "AFFECT THE HELP PROMPTS)"}; + + Window* window {mWindow}; + window->pushGui(new GuiMsgBox( + getHelpStyle(), message, "PROCEED", + [window] { window->pushGui(new GuiDetectDevice(false, false, nullptr)); }, "CANCEL", + nullptr, "", nullptr, nullptr, false, true, + (mRenderer->getIsVerticalOrientation() ? + 0.84f : + 0.54f * (1.778f / mRenderer->getScreenAspectRatio())))); +} + +void GuiMenu::openOtherOptions() +{ + auto s = new GuiSettings("OTHER SETTINGS"); + + // Alternative emulators GUI. + ComponentListRow alternativeEmulatorsRow; + alternativeEmulatorsRow.elements.clear(); + alternativeEmulatorsRow.addElement(std::make_shared("ALTERNATIVE EMULATORS", + Font::get(FONT_SIZE_MEDIUM), + mMenuColorPrimary), + true); + alternativeEmulatorsRow.addElement(mMenu.makeArrow(), false); + alternativeEmulatorsRow.makeAcceptInputHandler( + std::bind([this] { mWindow->pushGui(new GuiAlternativeEmulators); })); + s->addRow(alternativeEmulatorsRow); + + // Game media directory. + ComponentListRow rowMediaDir; + auto mediaDirectory = std::make_shared( + "GAME MEDIA DIRECTORY", Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary); + auto bracketMediaDirectory = std::make_shared(); + bracketMediaDirectory->setResize( + glm::vec2 {0.0f, Font::get(FONT_SIZE_MEDIUM)->getLetterHeight()}); + bracketMediaDirectory->setImage(":/graphics/arrow.svg"); + bracketMediaDirectory->setColorShift(mMenuColorPrimary); + rowMediaDir.addElement(mediaDirectory, true); + rowMediaDir.addElement(bracketMediaDirectory, false); + std::string titleMediaDir {"ENTER GAME MEDIA DIRECTORY"}; + std::string mediaDirectoryStaticText {"Default directory:"}; + std::string defaultDirectoryText {"~/.emulationstation/downloaded_media/"}; + std::string initValueMediaDir {Settings::getInstance()->getString("MediaDirectory")}; + bool multiLineMediaDir {false}; + auto updateValMediaDir = [this](const std::string& newVal) { + Settings::getInstance()->setString("MediaDirectory", newVal); + Settings::getInstance()->saveFile(); + ViewController::getInstance()->reloadAll(); + mWindow->invalidateCachedBackground(); + }; + rowMediaDir.makeAcceptInputHandler([this, s, titleMediaDir, mediaDirectoryStaticText, + defaultDirectoryText, initValueMediaDir, updateValMediaDir, + multiLineMediaDir] { + if (Settings::getInstance()->getBool("VirtualKeyboard")) { + mWindow->pushGui(new GuiTextEditKeyboardPopup( + getHelpStyle(), s->getMenu().getPosition().y, titleMediaDir, + Settings::getInstance()->getString("MediaDirectory"), updateValMediaDir, + multiLineMediaDir, "SAVE", "SAVE CHANGES?", mediaDirectoryStaticText, + defaultDirectoryText, "load default directory")); + } + else { + mWindow->pushGui(new GuiTextEditPopup( + getHelpStyle(), titleMediaDir, Settings::getInstance()->getString("MediaDirectory"), + updateValMediaDir, multiLineMediaDir, "SAVE", "SAVE CHANGES?", + mediaDirectoryStaticText, defaultDirectoryText, "load default directory")); + } + }); + s->addRow(rowMediaDir); + + // Maximum VRAM. + auto maxVram = std::make_shared(128.0f, 2048.0f, 16.0f, "MiB"); + maxVram->setValue(static_cast(Settings::getInstance()->getInt("MaxVRAM"))); + s->addWithLabel("VRAM LIMIT", maxVram); + s->addSaveFunc([maxVram, s] { + if (maxVram->getValue() != Settings::getInstance()->getInt("MaxVRAM")) { + Settings::getInstance()->setInt("MaxVRAM", + static_cast(std::round(maxVram->getValue()))); + s->setNeedsSaving(); + } + }); + +#if !defined(USE_OPENGLES) + // Anti-aliasing (MSAA). + auto antiAliasing = std::make_shared>( + getHelpStyle(), "ANTI-ALIASING (MSAA)", false); + const std::string& selectedAntiAliasing { + std::to_string(Settings::getInstance()->getInt("AntiAliasing"))}; + antiAliasing->add("DISABLED", "0", selectedAntiAliasing == "0"); + antiAliasing->add("2X", "2", selectedAntiAliasing == "2"); + antiAliasing->add("4X", "4", selectedAntiAliasing == "4"); + // If there are no objects returned, then there must be a manually modified entry in the + // configuration file. Simply set anti-aliasing to "0" in this case. + if (antiAliasing->getSelectedObjects().size() == 0) + antiAliasing->selectEntry(0); + s->addWithLabel("ANTI-ALIASING (MSAA) (REQUIRES RESTART)", antiAliasing); + s->addSaveFunc([antiAliasing, s] { + if (antiAliasing->getSelected() != + std::to_string(Settings::getInstance()->getInt("AntiAliasing"))) { + Settings::getInstance()->setInt("AntiAliasing", + atoi(antiAliasing->getSelected().c_str())); + s->setNeedsSaving(); + } + }); +#endif + + // Display/monitor. + auto displayIndex = std::make_shared>( + getHelpStyle(), "DISPLAY/MONITOR INDEX", false); + std::vector displayIndexEntry; + displayIndexEntry.push_back("1"); + displayIndexEntry.push_back("2"); + displayIndexEntry.push_back("3"); + displayIndexEntry.push_back("4"); + for (auto it = displayIndexEntry.cbegin(); it != displayIndexEntry.cend(); ++it) + displayIndex->add(*it, *it, + Settings::getInstance()->getInt("DisplayIndex") == atoi((*it).c_str())); + s->addWithLabel("DISPLAY/MONITOR INDEX (REQUIRES RESTART)", displayIndex); + s->addSaveFunc([displayIndex, s] { + if (atoi(displayIndex->getSelected().c_str()) != + Settings::getInstance()->getInt("DisplayIndex")) { + Settings::getInstance()->setInt("DisplayIndex", + atoi(displayIndex->getSelected().c_str())); + s->setNeedsSaving(); + } + }); + + // Screen contents rotation. + auto screenRotate = + std::make_shared>(getHelpStyle(), "ROTATE SCREEN", false); + const std::string& selectedScreenRotate { + std::to_string(Settings::getInstance()->getInt("ScreenRotate"))}; + screenRotate->add("DISABLED", "0", selectedScreenRotate == "0"); + screenRotate->add("90 DEGREES", "90", selectedScreenRotate == "90"); + screenRotate->add("180 DEGREES", "180", selectedScreenRotate == "180"); + screenRotate->add("270 DEGREES", "270", selectedScreenRotate == "270"); + // If there are no objects returned, then there must be a manually modified entry in the + // configuration file. Simply set screen rotation to "0" in this case. + if (screenRotate->getSelectedObjects().size() == 0) + screenRotate->selectEntry(0); + s->addWithLabel("ROTATE SCREEN (REQUIRES RESTART)", screenRotate); + s->addSaveFunc([screenRotate, s] { + if (screenRotate->getSelected() != + std::to_string(Settings::getInstance()->getInt("ScreenRotate"))) { + Settings::getInstance()->setInt("ScreenRotate", + atoi(screenRotate->getSelected().c_str())); + s->setNeedsSaving(); + } + }); + + // Keyboard quit shortcut. + auto keyboardQuitShortcut = std::make_shared>( + getHelpStyle(), "KEYBOARD QUIT SHORTCUT", false); + std::string selectedShortcut {Settings::getInstance()->getString("KeyboardQuitShortcut")}; +#if defined(_WIN64) || defined(__unix__) + keyboardQuitShortcut->add("Alt + F4", "AltF4", selectedShortcut == "AltF4"); + keyboardQuitShortcut->add("Ctrl + Q", "CtrlQ", selectedShortcut == "CtrlQ"); + keyboardQuitShortcut->add("Alt + Q", "AltQ", selectedShortcut == "AltQ"); +#endif +#if defined(__APPLE__) + keyboardQuitShortcut->add("\u2318 + Q", "CmdQ", selectedShortcut == "CmdQ"); + keyboardQuitShortcut->add("Ctrl + Q", "CtrlQ", selectedShortcut == "CtrlQ"); + keyboardQuitShortcut->add("Alt + Q", "AltQ", selectedShortcut == "AltQ"); +#endif + keyboardQuitShortcut->add("F4", "F4", selectedShortcut == "F4"); + // If there are no objects returned, then there must be a manually modified entry in the + // configuration file. Simply set the keyboard quit shortcut to the first entry in this case. + if (keyboardQuitShortcut->getSelectedObjects().size() == 0) + keyboardQuitShortcut->selectEntry(0); + s->addWithLabel("KEYBOARD QUIT SHORTCUT", keyboardQuitShortcut); + s->addSaveFunc([keyboardQuitShortcut, s] { + if (keyboardQuitShortcut->getSelected() != + Settings::getInstance()->getString("KeyboardQuitShortcut")) { + Settings::getInstance()->setString("KeyboardQuitShortcut", + keyboardQuitShortcut->getSelected()); + s->setNeedsSaving(); + } + }); + + // When to save game metadata. + auto saveGamelistsMode = std::make_shared>( + getHelpStyle(), "WHEN TO SAVE METADATA", false); + std::vector saveModes; + saveModes.push_back("on exit"); + saveModes.push_back("always"); + saveModes.push_back("never"); + for (auto it = saveModes.cbegin(); it != saveModes.cend(); ++it) { + saveGamelistsMode->add(*it, *it, + Settings::getInstance()->getString("SaveGamelistsMode") == *it); + } + s->addWithLabel("WHEN TO SAVE GAME METADATA", saveGamelistsMode); + s->addSaveFunc([saveGamelistsMode, s] { + if (saveGamelistsMode->getSelected() != + Settings::getInstance()->getString("SaveGamelistsMode")) { + Settings::getInstance()->setString("SaveGamelistsMode", + saveGamelistsMode->getSelected()); + // Always save the gamelist.xml files if switching to "always" as there may + // be changes that will otherwise be lost. + if (Settings::getInstance()->getString("SaveGamelistsMode") == "always") { + for (auto it = SystemData::sSystemVector.cbegin(); + it != SystemData::sSystemVector.cend(); ++it) + (*it)->writeMetaData(); + } + s->setNeedsSaving(); + } + }); + +#if defined(APPLICATION_UPDATER) + // Application updater frequency. + auto applicationUpdaterFrequency = std::make_shared>( + getHelpStyle(), "APPLICATION UPDATES", false); + const std::string& selectedUpdaterFrequency { + Settings::getInstance()->getString("ApplicationUpdaterFrequency")}; + applicationUpdaterFrequency->add("ALWAYS", "always", selectedUpdaterFrequency == "always"); + applicationUpdaterFrequency->add("DAILY", "daily", selectedUpdaterFrequency == "daily"); + applicationUpdaterFrequency->add("WEEKLY", "weekly", selectedUpdaterFrequency == "weekly"); + applicationUpdaterFrequency->add("MONTHLY", "monthly", selectedUpdaterFrequency == "monthly"); + applicationUpdaterFrequency->add("NEVER", "never", selectedUpdaterFrequency == "never"); + // If there are no objects returned, then there must be a manually modified entry in the + // configuration file. Simply set updater frequency to "always" in this case. + if (applicationUpdaterFrequency->getSelectedObjects().size() == 0) + applicationUpdaterFrequency->selectEntry(0); + s->addWithLabel("CHECK FOR APPLICATION UPDATES", applicationUpdaterFrequency); + s->addSaveFunc([applicationUpdaterFrequency, s] { + if (applicationUpdaterFrequency->getSelected() != + Settings::getInstance()->getString("ApplicationUpdaterFrequency")) { + Settings::getInstance()->setString("ApplicationUpdaterFrequency", + applicationUpdaterFrequency->getSelected()); + s->setNeedsSaving(); + } + }); +#endif + +#if defined(APPLICATION_UPDATER) +#if defined(IS_PRERELEASE) + // Add a dummy entry to indicate that this setting is always enabled when running a prerelease. + auto applicationUpdaterPrereleases = std::make_shared(); + applicationUpdaterPrereleases->setState(true); + s->addWithLabel("INCLUDE PRERELEASES IN UPDATE CHECKS", applicationUpdaterPrereleases); + applicationUpdaterPrereleases->setEnabled(false); + applicationUpdaterPrereleases->setOpacity(DISABLED_OPACITY); + applicationUpdaterPrereleases->getParent() + ->getChild(applicationUpdaterPrereleases->getChildIndex() - 1) + ->setOpacity(DISABLED_OPACITY); +#else + // Whether to include prereleases when checking for application updates. + auto applicationUpdaterPrereleases = std::make_shared(); + applicationUpdaterPrereleases->setState( + Settings::getInstance()->getBool("ApplicationUpdaterPrereleases")); + s->addWithLabel("INCLUDE PRERELEASES IN UPDATE CHECKS", applicationUpdaterPrereleases); + s->addSaveFunc([applicationUpdaterPrereleases, s] { + if (applicationUpdaterPrereleases->getState() != + Settings::getInstance()->getBool("ApplicationUpdaterPrereleases")) { + Settings::getInstance()->setBool("ApplicationUpdaterPrereleases", + applicationUpdaterPrereleases->getState()); + s->setNeedsSaving(); + } + }); +#endif +#endif + +#if defined(_WIN64) + // Hide taskbar during the program session. + auto hide_taskbar = std::make_shared(); + hide_taskbar->setState(Settings::getInstance()->getBool("HideTaskbar")); + s->addWithLabel("HIDE TASKBAR (REQUIRES RESTART)", hide_taskbar); + s->addSaveFunc([hide_taskbar, s] { + if (hide_taskbar->getState() != Settings::getInstance()->getBool("HideTaskbar")) { + Settings::getInstance()->setBool("HideTaskbar", hide_taskbar->getState()); + s->setNeedsSaving(); + } + }); +#endif + + // Run ES in the background when a game has been launched. + auto runInBackground = std::make_shared(); + runInBackground->setState(Settings::getInstance()->getBool("RunInBackground")); + s->addWithLabel("RUN IN BACKGROUND (WHILE GAME IS LAUNCHED)", runInBackground); + s->addSaveFunc([runInBackground, s] { + if (runInBackground->getState() != Settings::getInstance()->getBool("RunInBackground")) { + Settings::getInstance()->setBool("RunInBackground", runInBackground->getState()); + s->setNeedsSaving(); + } + }); + +#if defined(VIDEO_HW_DECODING) + // Whether to enable hardware decoding for the FFmpeg video player. + auto videoHardwareDecoding = std::make_shared(); + videoHardwareDecoding->setState(Settings::getInstance()->getBool("VideoHardwareDecoding")); + s->addWithLabel("VIDEO HARDWARE DECODING (EXPERIMENTAL)", videoHardwareDecoding); + s->addSaveFunc([videoHardwareDecoding, s] { + if (videoHardwareDecoding->getState() != + Settings::getInstance()->getBool("VideoHardwareDecoding")) { + Settings::getInstance()->setBool("VideoHardwareDecoding", + videoHardwareDecoding->getState()); + s->setNeedsSaving(); + } + }); +#endif + + // Whether to upscale the video frame rate to 60 FPS. + auto videoUpscaleFrameRate = std::make_shared(); + videoUpscaleFrameRate->setState(Settings::getInstance()->getBool("VideoUpscaleFrameRate")); + s->addWithLabel("UPSCALE VIDEO FRAME RATE TO 60 FPS", videoUpscaleFrameRate); + s->addSaveFunc([videoUpscaleFrameRate, s] { + if (videoUpscaleFrameRate->getState() != + Settings::getInstance()->getBool("VideoUpscaleFrameRate")) { + Settings::getInstance()->setBool("VideoUpscaleFrameRate", + videoUpscaleFrameRate->getState()); + s->setNeedsSaving(); + } + }); + + // Whether to enable alternative emulators per game (the option to disable this is intended + // primarily for testing purposes). + auto alternativeEmulatorPerGame = std::make_shared(); + alternativeEmulatorPerGame->setState( + Settings::getInstance()->getBool("AlternativeEmulatorPerGame")); + s->addWithLabel("ENABLE ALTERNATIVE EMULATORS PER GAME", alternativeEmulatorPerGame); + s->addSaveFunc([alternativeEmulatorPerGame, s] { + if (alternativeEmulatorPerGame->getState() != + Settings::getInstance()->getBool("AlternativeEmulatorPerGame")) { + Settings::getInstance()->setBool("AlternativeEmulatorPerGame", + alternativeEmulatorPerGame->getState()); + s->setNeedsSaving(); + s->setNeedsReloading(); + s->setInvalidateCachedBackground(); + } + }); + + // Show hidden files. + auto showHiddenFiles = std::make_shared(); + showHiddenFiles->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); + s->addWithLabel("SHOW HIDDEN FILES AND FOLDERS", showHiddenFiles); + s->addSaveFunc([this, showHiddenFiles, s] { + if (showHiddenFiles->getState() != Settings::getInstance()->getBool("ShowHiddenFiles")) { + Settings::getInstance()->setBool("ShowHiddenFiles", showHiddenFiles->getState()); + s->setNeedsSaving(); + s->setNeedsCloseMenu([this] { delete this; }); + s->setNeedsRescanROMDirectory(); + } + }); + + // Show hidden games. + auto showHiddenGames = std::make_shared(); + showHiddenGames->setState(Settings::getInstance()->getBool("ShowHiddenGames")); + s->addWithLabel("SHOW HIDDEN GAMES", showHiddenGames); + s->addSaveFunc([this, showHiddenGames, s] { + if (showHiddenGames->getState() != Settings::getInstance()->getBool("ShowHiddenGames")) { + Settings::getInstance()->setBool("ShowHiddenGames", showHiddenGames->getState()); + s->setNeedsSaving(); + s->setNeedsCloseMenu([this] { delete this; }); + s->setNeedsRescanROMDirectory(); + } + }); + + // Custom event scripts, fired using Scripting::fireEvent(). + auto customEventScripts = std::make_shared(); + customEventScripts->setState(Settings::getInstance()->getBool("CustomEventScripts")); + s->addWithLabel("ENABLE CUSTOM EVENT SCRIPTS", customEventScripts); + s->addSaveFunc([customEventScripts, s] { + if (customEventScripts->getState() != + Settings::getInstance()->getBool("CustomEventScripts")) { + Settings::getInstance()->setBool("CustomEventScripts", customEventScripts->getState()); + s->setNeedsSaving(); + } + }); + + // Only show games included in the gamelist.xml files. + auto parseGamelistOnly = std::make_shared(); + parseGamelistOnly->setState(Settings::getInstance()->getBool("ParseGamelistOnly")); + s->addWithLabel("ONLY SHOW GAMES FROM GAMELIST.XML FILES", parseGamelistOnly); + s->addSaveFunc([this, parseGamelistOnly, s] { + if (parseGamelistOnly->getState() != + Settings::getInstance()->getBool("ParseGamelistOnly")) { + Settings::getInstance()->setBool("ParseGamelistOnly", parseGamelistOnly->getState()); + s->setNeedsSaving(); + s->setNeedsCloseMenu([this] { delete this; }); + s->setNeedsRescanROMDirectory(); + } + }); + + // Strip extra MAME name info. + auto mameNameStripExtraInfo = std::make_shared(); + mameNameStripExtraInfo->setState(Settings::getInstance()->getBool("MAMENameStripExtraInfo")); + s->addWithLabel("STRIP EXTRA MAME NAME INFO (REQUIRES RESTART)", mameNameStripExtraInfo); + s->addSaveFunc([mameNameStripExtraInfo, s] { + if (Settings::getInstance()->getBool("MAMENameStripExtraInfo") != + mameNameStripExtraInfo->getState()) { + Settings::getInstance()->setBool("MAMENameStripExtraInfo", + mameNameStripExtraInfo->getState()); + s->setNeedsSaving(); + } + }); + +#if defined(__unix__) + // Whether to disable desktop composition. + auto disableComposition = std::make_shared(); + disableComposition->setState(Settings::getInstance()->getBool("DisableComposition")); + s->addWithLabel("DISABLE DESKTOP COMPOSITION (REQUIRES RESTART)", disableComposition); + s->addSaveFunc([disableComposition, s] { + if (disableComposition->getState() != + Settings::getInstance()->getBool("DisableComposition")) { + Settings::getInstance()->setBool("DisableComposition", disableComposition->getState()); + s->setNeedsSaving(); + } + }); +#endif + + if (Settings::getInstance()->getBool("DebugFlag")) { + // If the --debug command line option was passed then create a dummy entry. + auto debugMode = std::make_shared(); + debugMode->setState(true); + s->addWithLabel("DEBUG MODE", debugMode); + debugMode->setEnabled(false); + debugMode->setOpacity(DISABLED_OPACITY); + debugMode->getParent() + ->getChild(debugMode->getChildIndex() - 1) + ->setOpacity(DISABLED_OPACITY); + } + else { + // Debug mode. + auto debugMode = std::make_shared(); + debugMode->setState(Settings::getInstance()->getBool("DebugMode")); + s->addWithLabel("DEBUG MODE", debugMode); + s->addSaveFunc([debugMode, s] { + if (debugMode->getState() != Settings::getInstance()->getBool("DebugMode")) { + if (!Settings::getInstance()->getBool("DebugMode")) { + Settings::getInstance()->setBool("DebugMode", true); + Settings::getInstance()->setBool("Debug", true); + Log::setReportingLevel(LogDebug); + } + else { + Settings::getInstance()->setBool("DebugMode", false); + Settings::getInstance()->setBool("Debug", false); + Log::setReportingLevel(LogInfo); + } + s->setNeedsSaving(); + } + }); + } + + // GPU statistics overlay. + auto displayGpuStatistics = std::make_shared(); + displayGpuStatistics->setState(Settings::getInstance()->getBool("DisplayGPUStatistics")); + s->addWithLabel("DISPLAY GPU STATISTICS OVERLAY", displayGpuStatistics); + s->addSaveFunc([displayGpuStatistics, s] { + if (displayGpuStatistics->getState() != + Settings::getInstance()->getBool("DisplayGPUStatistics")) { + Settings::getInstance()->setBool("DisplayGPUStatistics", + displayGpuStatistics->getState()); + s->setNeedsSaving(); + } + }); + + // Whether to enable the menu in Kid mode. + auto enableMenuKidMode = std::make_shared(); + enableMenuKidMode->setState(Settings::getInstance()->getBool("EnableMenuKidMode")); + s->addWithLabel("ENABLE MENU IN KID MODE", enableMenuKidMode); + s->addSaveFunc([enableMenuKidMode, s] { + if (Settings::getInstance()->getBool("EnableMenuKidMode") != + enableMenuKidMode->getState()) { + Settings::getInstance()->setBool("EnableMenuKidMode", enableMenuKidMode->getState()); + s->setNeedsSaving(); + } + }); + +// macOS requires root privileges to reboot and power off so it doesn't make much +// sense to enable this setting and menu entry for that operating system. +#if !defined(__APPLE__) + // Whether to show the quit menu with the options to reboot and shutdown the computer. + auto showQuitMenu = std::make_shared(); + showQuitMenu->setState(Settings::getInstance()->getBool("ShowQuitMenu")); + s->addWithLabel("SHOW QUIT MENU (REBOOT AND POWER OFF ENTRIES)", showQuitMenu); + s->addSaveFunc([this, showQuitMenu, s] { + if (showQuitMenu->getState() != Settings::getInstance()->getBool("ShowQuitMenu")) { + Settings::getInstance()->setBool("ShowQuitMenu", showQuitMenu->getState()); + s->setNeedsSaving(); + s->setNeedsCloseMenu([this] { delete this; }); + } + }); +#endif + +#if defined(APPLICATION_UPDATER) && !defined(IS_PRERELEASE) + auto applicationUpdaterFrequencyFunc = + [applicationUpdaterPrereleases](const std::string& frequency) { + if (frequency == "never") { + applicationUpdaterPrereleases->setEnabled(false); + applicationUpdaterPrereleases->setOpacity(DISABLED_OPACITY); + applicationUpdaterPrereleases->getParent() + ->getChild(applicationUpdaterPrereleases->getChildIndex() - 1) + ->setOpacity(DISABLED_OPACITY); + } + else { + applicationUpdaterPrereleases->setEnabled(true); + applicationUpdaterPrereleases->setOpacity(1.0f); + applicationUpdaterPrereleases->getParent() + ->getChild(applicationUpdaterPrereleases->getChildIndex() - 1) + ->setOpacity(1.0f); + } + }; + + applicationUpdaterFrequencyFunc(applicationUpdaterFrequency->getSelected()); + applicationUpdaterFrequency->setCallback(applicationUpdaterFrequencyFunc); +#endif + + s->setSize(mSize); + mWindow->pushGui(s); +} + +void GuiMenu::openUtilities() +{ + auto s = new GuiSettings("UTILITIES"); + + HelpStyle style {getHelpStyle()}; + + ComponentListRow row; + + row.addElement(std::make_shared("ORPHANED DATA CLEANUP", + Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary), + true); + row.addElement(mMenu.makeArrow(), false); + row.makeAcceptInputHandler(std::bind( + [this] { mWindow->pushGui(new GuiOrphanedDataCleanup([&]() { close(true); })); })); + s->addRow(row); + + row.elements.clear(); + row.addElement(std::make_shared("CREATE/UPDATE SYSTEM DIRECTORIES", + Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary), + true); + + // This transparent dummy arrow is only here to enable the "select" help prompt. + auto dummyArrow = mMenu.makeArrow(); + dummyArrow->setOpacity(0.0f); + row.addElement(dummyArrow, false); + + row.makeAcceptInputHandler([this] { + mWindow->pushGui(new GuiMsgBox( + getHelpStyle(), + "THIS WILL CREATE ALL GAME SYSTEM DIRECTORIES INSIDE YOUR ROM FOLDER AND IT WILL ALSO " + "UPDATE ALL SYSTEMINFO.TXT FILES. THIS IS A SAFE OPERATION THAT WILL NOT DELETE OR " + "MODIFY YOUR GAME FILES. TO DECREASE APPLICATION STARTUP TIMES IT'S RECOMMENDED TO " + "DELETE THE SYSTEM DIRECTORIES YOU DON'T NEED AFTER RUNNING THIS UTILITY", + "PROCEED", + [this] { + if (!SystemData::createSystemDirectories()) { + mWindow->pushGui(new GuiMsgBox( + getHelpStyle(), "THE SYSTEM DIRECTORIES WERE SUCCESSFULLY CREATED", "OK", + [this] { + if (CollectionSystemsManager::getInstance()->isEditing()) + CollectionSystemsManager::getInstance()->exitEditMode(); + mWindow->stopInfoPopup(); + GuiMenu::close(true); + // Write any gamelist.xml changes before proceeding with the rescan. + if (Settings::getInstance()->getString("SaveGamelistsMode") == + "on exit") { + for (auto system : SystemData::sSystemVector) + system->writeMetaData(); + } + ViewController::getInstance()->rescanROMDirectory(); + }, + "", nullptr, "", nullptr, nullptr, true)); + } + else { + mWindow->pushGui( + new GuiMsgBox(getHelpStyle(), + "ERROR CREATING SYSTEM DIRECTORIES, PERMISSION PROBLEMS OR " + "DISK FULL?\nSEE THE LOG FILE FOR MORE DETAILS", + "OK", nullptr, "", nullptr, "", nullptr, nullptr, true, true, + (mRenderer->getIsVerticalOrientation() ? + 0.70f : + 0.44f * (1.778f / mRenderer->getScreenAspectRatio())))); + } + }, + "CANCEL", nullptr, "", nullptr, nullptr, false, true, + (mRenderer->getIsVerticalOrientation() ? + 0.80f : + 0.52f * (1.778f / mRenderer->getScreenAspectRatio())))); + }); + + s->addRow(row); + + row.elements.clear(); + row.addElement(std::make_shared("RESCAN ROM DIRECTORY", + Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary), + true); + + // This transparent dummy arrow is only here to enable the "select" help prompt. + row.addElement(dummyArrow, false); + + row.makeAcceptInputHandler([this] { + mWindow->pushGui(new GuiMsgBox( + getHelpStyle(), + "THIS WILL RESCAN YOUR ROM DIRECTORY FOR CHANGES SUCH AS ADDED OR REMOVED GAMES AND " + "SYSTEMS", + "PROCEED", + [this] { + if (CollectionSystemsManager::getInstance()->isEditing()) + CollectionSystemsManager::getInstance()->exitEditMode(); + mWindow->stopInfoPopup(); + GuiMenu::close(true); + // Write any gamelist.xml changes before proceeding with the rescan. + if (Settings::getInstance()->getString("SaveGamelistsMode") == "on exit") { + for (auto system : SystemData::sSystemVector) + system->writeMetaData(); + } + ViewController::getInstance()->rescanROMDirectory(); + }, + "CANCEL", nullptr, "", nullptr, nullptr, false, true, + (mRenderer->getIsVerticalOrientation() ? + 0.76f : + 0.52f * (1.778f / mRenderer->getScreenAspectRatio())))); + }); + s->addRow(row); + + s->setSize(mSize); + mWindow->pushGui(s); +} + +void GuiMenu::openQuitMenu() +{ + if (!Settings::getInstance()->getBool("ShowQuitMenu")) { + mWindow->pushGui(new GuiMsgBox( + this->getHelpStyle(), "REALLY QUIT?", "YES", + [this] { + close(true); + Utils::Platform::quitES(); + }, + "NO", nullptr)); + } + else { + auto s = new GuiSettings("QUIT"); + + Window* window {mWindow}; + HelpStyle style {getHelpStyle()}; + + ComponentListRow row; + + row.makeAcceptInputHandler([window, this] { + window->pushGui(new GuiMsgBox( + this->getHelpStyle(), "REALLY QUIT?", "YES", + [this] { + close(true); + Utils::Platform::quitES(); + }, + "NO", nullptr)); + }); + auto quitText = std::make_shared( + "QUIT EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary); + quitText->setSelectable(true); + row.addElement(quitText, true); + s->addRow(row); + + row.elements.clear(); + row.makeAcceptInputHandler([window, this] { + window->pushGui(new GuiMsgBox( + this->getHelpStyle(), "REALLY REBOOT?", "YES", + [] { + if (Utils::Platform::quitES(Utils::Platform::QuitMode::REBOOT) != 0) { + LOG(LogWarning) << "Reboot terminated with non-zero result!"; + } + }, + "NO", nullptr)); + }); + auto rebootText = std::make_shared( + "REBOOT SYSTEM", Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary); + rebootText->setSelectable(true); + row.addElement(rebootText, true); + s->addRow(row); + + row.elements.clear(); + row.makeAcceptInputHandler([window, this] { + window->pushGui(new GuiMsgBox( + this->getHelpStyle(), "REALLY POWER OFF?", "YES", + [] { + if (Utils::Platform::quitES(Utils::Platform::QuitMode::POWEROFF) != 0) { + LOG(LogWarning) << "Power off terminated with non-zero result!"; + } + }, + "NO", nullptr)); + }); + auto powerOffText = std::make_shared( + "POWER OFF SYSTEM", Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary); + powerOffText->setSelectable(true); + row.addElement(powerOffText, true); + s->addRow(row); + + s->setSize(mSize); + mWindow->pushGui(s); + } +} + +void GuiMenu::addVersionInfo() +{ + mVersion.setFont(Font::get(FONT_SIZE_SMALL)); + mVersion.setColor(mMenuColorTertiary); + +#if defined(IS_PRERELEASE) + mVersion.setText("EMULATIONSTATION-DE V" + Utils::String::toUpper(PROGRAM_VERSION_STRING) + + " (Built " + __DATE__ + ")"); +#else + mVersion.setText("EMULATIONSTATION-DE V" + Utils::String::toUpper(PROGRAM_VERSION_STRING)); +#endif + + mVersion.setHorizontalAlignment(ALIGN_CENTER); + addChild(&mVersion); +} + +void GuiMenu::openThemeDownloader(GuiSettings* settings) +{ + auto updateFunc = [&, settings]() { + LOG(LogDebug) << "GuiMenu::openThemeDownloader(): Themes were updated, reloading menu"; + mThemeDownloaderReloadCounter = 1; + delete settings; + if (mThemeDownloaderReloadCounter != 1) { + delete this; + } + else { + openUIOptions(); + mWindow->invalidateCachedBackground(); + } + }; + + mWindow->pushGui(new GuiThemeDownloader(updateFunc)); +} + +void GuiMenu::openMediaViewerOptions() +{ + mWindow->pushGui(new GuiMediaViewerOptions("MEDIA VIEWER SETTINGS")); +} + +void GuiMenu::openScreensaverOptions() +{ + mWindow->pushGui(new GuiScreensaverOptions("SCREENSAVER SETTINGS")); +} + +void GuiMenu::openCollectionSystemOptions() +{ + mWindow->pushGui(new GuiCollectionSystemsOptions("GAME COLLECTION SETTINGS")); +} + +void GuiMenu::onSizeChanged() +{ + mVersion.setSize(mSize.x, 0.0f); + mVersion.setPosition(0.0f, mSize.y - mVersion.getSize().y); +} + +void GuiMenu::addEntry(const std::string& name, + unsigned int color, + bool add_arrow, + const std::function& func) +{ + std::shared_ptr font {Font::get(FONT_SIZE_MEDIUM)}; + + // Populate the list. + ComponentListRow row; + row.addElement(std::make_shared(name, font, color), true); + + if (add_arrow) { + std::shared_ptr bracket {mMenu.makeArrow()}; + row.addElement(bracket, false); + } + + row.makeAcceptInputHandler(func); + mMenu.addRow(row); +} + +void GuiMenu::close(bool closeAllWindows) +{ + std::function closeFunc; + if (!closeAllWindows) { + closeFunc = [this] { delete this; }; + } + else { + Window* window {mWindow}; + closeFunc = [window] { + while (window->peekGui() != ViewController::getInstance()) + delete window->peekGui(); + }; + } + closeFunc(); +} + +bool GuiMenu::input(InputConfig* config, Input input) +{ + if (GuiComponent::input(config, input)) + return true; + + const bool isStart {config->isMappedTo("start", input)}; + if (input.value != 0 && (config->isMappedTo("b", input) || isStart)) { + close(isStart); + return true; + } + + return false; +} + +std::vector GuiMenu::getHelpPrompts() +{ + std::vector prompts; + prompts.push_back(HelpPrompt("up/down", "choose")); + prompts.push_back(HelpPrompt("a", "select")); + prompts.push_back(HelpPrompt("b", "close menu")); + prompts.push_back(HelpPrompt("start", "close menu")); + return prompts; +} diff --git a/es-app/src/guis/GuiMenu.h.orig b/es-app/src/guis/GuiMenu.h.orig new file mode 100644 index 000000000..924f92137 --- /dev/null +++ b/es-app/src/guis/GuiMenu.h.orig @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// GuiMenu.h +// +// Main menu. +// Some submenus are covered in separate source files. +// + +#ifndef ES_APP_GUIS_GUI_MENU_H +#define ES_APP_GUIS_GUI_MENU_H + +#include "GuiComponent.h" +#include "components/MenuComponent.h" +#include "guis/GuiSettings.h" +#include "views/ViewController.h" + +class GuiMenu : public GuiComponent +{ +public: + GuiMenu(); + ~GuiMenu(); + + bool input(InputConfig* config, Input input) override; + void onSizeChanged() override; + std::vector getHelpPrompts() override; + HelpStyle getHelpStyle() override { return ViewController::getInstance()->getViewHelpStyle(); } + +private: + void close(bool closeAllWindows); + void addEntry(const std::string& name, + unsigned int color, + bool add_arrow, + const std::function& func); + void addVersionInfo(); + + void openScraperOptions(); + void openUIOptions(); + void openThemeDownloader(GuiSettings* settings); + void openMediaViewerOptions(); + void openScreensaverOptions(); + void openSoundOptions(); + void openInputDeviceOptions(); + void openConfigInput(GuiSettings* settings); + void openCollectionSystemOptions(); + void openOtherOptions(); + void openUtilities(); + void openQuitMenu(); + + Renderer* mRenderer; + MenuComponent mMenu; + TextComponent mVersion; + int mThemeDownloaderReloadCounter; +}; + +#endif // ES_APP_GUIS_GUI_MENU_H diff --git a/es-core/src/Window.cpp.orig b/es-core/src/Window.cpp.orig new file mode 100644 index 000000000..4ef169f3c --- /dev/null +++ b/es-core/src/Window.cpp.orig @@ -0,0 +1,919 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// Window.cpp +// +// Window management, screensaver management, help prompts and splash screen. +// The input stack starts here as well, as this is the first instance called by InputManager. +// + +#include "Window.h" + +#include "InputManager.h" +#include "Log.h" +#include "Scripting.h" +#include "Sound.h" +#include "components/HelpComponent.h" +#include "components/ImageComponent.h" +#include "guis/GuiInfoPopup.h" +#include "resources/Font.h" + +#include +#include + +#define CLOCK_BACKGROUND_CREATION false + +Window::Window() noexcept + : mRenderer {Renderer::getInstance()} + , mSplashTextPositions {0.0f, 0.0f, 0.0f, 0.0f} + , mBackgroundOverlayOpacity {1.0f} + , mScreensaver {nullptr} + , mMediaViewer {nullptr} + , mPDFViewer {nullptr} + , mLaunchScreen {nullptr} + , mInfoPopup {nullptr} + , mListScrollOpacity {0.0f} + , mFrameTimeElapsed {0} + , mFrameCountElapsed {0} + , mAverageDeltaTime {10} + , mTimeSinceLastInput {0} + , mBlockInput {false} + , mNormalizeNextUpdate {false} + , mRenderScreensaver {false} + , mRenderMediaViewer {false} + , mRenderLaunchScreen {false} + , mRenderPDFViewer {false} + , mGameLaunchedState {false} + , mAllowTextScrolling {true} + , mAllowFileAnimation {true} + , mCachedBackground {false} + , mInvalidatedCachedBackground {false} + , mInitiateCacheTimer {false} + , mInvalidateCacheTimer {0} + , mVideoPlayerCount {0} + , mTopScale {0.5f} + , mRenderedHelpPrompts {false} + , mChangedTheme {false} +{ +} + +Window::~Window() +{ + // Delete all our GUIs. + while (peekGui()) + delete peekGui(); + + if (mInfoPopup) + delete mInfoPopup; +} + +Window* Window::getInstance() +{ + static Window instance; + return &instance; +} + +void Window::pushGui(GuiComponent* gui) +{ + mGuiStack.push_back(gui); + gui->updateHelpPrompts(); +} + +void Window::removeGui(GuiComponent* gui) +{ + for (auto it = mGuiStack.cbegin(); it != mGuiStack.cend(); ++it) { + if (*it == gui) { + it = mGuiStack.erase(it); + + // We just popped the stack and the stack is not empty. + if (it == mGuiStack.cend() && mGuiStack.size()) + mGuiStack.back()->updateHelpPrompts(); + + return; + } + } +} + +GuiComponent* Window::peekGui() +{ + if (mGuiStack.size() == 0) + return nullptr; + + return mGuiStack.back(); +} + +bool Window::init() +{ + if (!mRenderer->init()) { + LOG(LogError) << "Renderer failed to initialize."; + return false; + } + + InputManager::getInstance().init(); + + ResourceManager::getInstance().reloadAll(); + + mHelp = std::make_unique(); + mSplash = std::make_unique(false, false); + + mBackgroundOverlay = std::make_unique(false, false); + mBackgroundOverlayOpacity = 0.0f; + + // Keep a reference to the default fonts, so they don't keep getting destroyed/recreated. + if (mDefaultFonts.empty()) { + mDefaultFonts.push_back(Font::get(FONT_SIZE_SMALL)); + mDefaultFonts.push_back(Font::get(FONT_SIZE_MEDIUM)); + mDefaultFonts.push_back(Font::get(FONT_SIZE_MEDIUM_FIXED)); + mDefaultFonts.push_back(Font::get(FONT_SIZE_LARGE)); + mDefaultFonts.push_back(Font::get(FONT_SIZE_LARGE_FIXED)); + } + + if (mRenderer->getIsVerticalOrientation()) + mSplash->setResize(mRenderer->getScreenWidth() * 0.8f, 0.0f); + else + mSplash->setResize(0.0f, glm::clamp(mRenderer->getScreenHeight() * 0.62f, 0.0f, + mRenderer->getScreenWidth() * 0.42f)); + + mSplash->setImage(":/graphics/splash.svg"); + mSplash->setPosition((mRenderer->getScreenWidth() - mSplash->getSize().x) / 2.0f, + (mRenderer->getScreenHeight() - mSplash->getSize().y) / 2.0f * 0.6f); + + mSplashTextScanning = std::unique_ptr( + mDefaultFonts.at(1)->buildTextCache("Searching for games...", 0.0f, 0.0f, 0x777777FF)); + mSplashTextPopulating = std::unique_ptr( + mDefaultFonts.at(1)->buildTextCache("Loading systems...", 0.0f, 0.0f, 0x777777FF)); + mSplashTextReloading = std::unique_ptr( + mDefaultFonts.at(1)->buildTextCache("Reloading...", 0.0f, 0.0f, 0x777777FF)); + + mSplashTextPositions.x = + (mRenderer->getScreenWidth() - mSplashTextScanning->metrics.size.x) / 2.0f; + mSplashTextPositions.z = + (mRenderer->getScreenWidth() - mSplashTextPopulating->metrics.size.x) / 2.0f; + mSplashTextPositions.w = + (mRenderer->getScreenWidth() - mSplashTextReloading->metrics.size.x) / 2.0f; + mSplashTextPositions.y = + mRenderer->getScreenHeight() * (mRenderer->getIsVerticalOrientation() ? 0.620f : 0.745f); + + ProgressBarRectangle progressBarRect; + if (mRenderer->getIsVerticalOrientation()) + progressBarRect.barWidth = mRenderer->getScreenWidth() * 0.53f; + else + progressBarRect.barWidth = mRenderer->getScreenHeight() * 0.53f; + + progressBarRect.barHeight = mDefaultFonts.at(1)->getLetterHeight() * 1.1f; + progressBarRect.barPosX = + (mRenderer->getScreenWidth() / 2.0f) - (progressBarRect.barWidth / 2.0f); + progressBarRect.barPosY = mSplashTextPositions.y + (progressBarRect.barHeight * 2.0f); + progressBarRect.color = 0x777777FF; + mProgressBarRectangles.emplace_back(progressBarRect); + + const float borderThickness {std::ceil(2.0f * mRenderer->getScreenResolutionModifier())}; + + progressBarRect.barWidth -= borderThickness * 2.0f; + progressBarRect.barHeight -= borderThickness * 2.0f; + progressBarRect.barPosX += borderThickness; + progressBarRect.barPosY += borderThickness; + progressBarRect.color = 0x000000FF; + mProgressBarRectangles.emplace_back(progressBarRect); + + progressBarRect.barWidth -= borderThickness * 2.0f; + progressBarRect.barHeight -= borderThickness * 2.0f; + progressBarRect.barPosX += borderThickness; + progressBarRect.barPosY += borderThickness; + progressBarRect.color = 0x79010FFF; + mProgressBarRectangles.emplace_back(progressBarRect); + + mBackgroundOverlay->setImage(":/graphics/frame.png"); + mBackgroundOverlay->setResize(mRenderer->getScreenWidth(), mRenderer->getScreenHeight()); + + mPostprocessedBackground = TextureResource::get("", false, false, false, false, false); + + mListScrollFont = Font::get(FONT_SIZE_LARGE); + + // Update our help because font sizes probably changed. + if (peekGui()) + peekGui()->updateHelpPrompts(); + + return true; +} + +void Window::deinit() +{ + // Hide all GUI elements on uninitialisation - this disable. + for (auto it = mGuiStack.cbegin(); it != mGuiStack.cend(); ++it) + (*it)->onHide(); + + mPostprocessedBackground.reset(); + + InputManager::getInstance().deinit(); + ResourceManager::getInstance().unloadAll(); + mRenderer->deinit(); +} + +void Window::input(InputConfig* config, Input input) +{ + if (mBlockInput) + return; + + mTimeSinceLastInput = 0; + + // The DebugSkipInputLogging option has to be set manually in es_settings.xml as + // it does not have any settings menu entry. + if (Settings::getInstance()->getBool("Debug") && + !Settings::getInstance()->getBool("DebugSkipInputLogging")) { + logInput(config, input); + } + + if (mMediaViewer && mRenderMediaViewer) { + mMediaViewer->input(config, input); + return; + } + + if (mPDFViewer && mRenderPDFViewer) { + mPDFViewer->input(config, input); + return; + } + + if (mGameLaunchedState && mLaunchScreen && mRenderLaunchScreen) { + if (input.value != 0) { + mLaunchScreen->closeLaunchScreen(); + mRenderLaunchScreen = false; + } + } + + if (mScreensaver) { + if (mScreensaver->isScreensaverActive() && + Settings::getInstance()->getBool("ScreensaverControls") && + ((Settings::getInstance()->getString("ScreensaverType") == "video") || + (Settings::getInstance()->getString("ScreensaverType") == "slideshow"))) { + bool customImageSlideshow = false; + if (Settings::getInstance()->getString("ScreensaverType") == "slideshow" && + Settings::getInstance()->getBool("ScreensaverSlideshowCustomImages")) + customImageSlideshow = true; + + if ((customImageSlideshow || mScreensaver->getCurrentGame() != nullptr) && + (config->isMappedTo("a", input) || config->isMappedTo("y", input) || + config->isMappedLike("left", input) || config->isMappedLike("right", input))) { + // Left or right browses to the next video or image. + if (config->isMappedLike("left", input) || config->isMappedLike("right", input)) { + if (input.value != 0) { + // Handle screensaver control. + mScreensaver->nextGame(); + } + return; + } + else if (config->isMappedTo("a", input) && input.value != 0) { + // Launch game. + Scripting::fireEvent("screensaver-end", "game-start"); + stopScreensaver(); + mScreensaver->launchGame(); + return; + } + else if (config->isMappedTo("y", input) && input.value != 0) { + // Jump to the game in its gamelist, but do not launch it. + Scripting::fireEvent("screensaver-end", "game-jump"); + stopScreensaver(); + NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); + mScreensaver->goToGame(); + return; + } + } + } + } + + // Any keypress cancels the screensaver. + if (input.value != 0 && isScreensaverActive()) { + Scripting::fireEvent("screensaver-end", "cancel"); + stopScreensaver(); + return; + } + + if (config->isMappedTo("a", input) && input.value != 0 && + Settings::getInstance()->getString("MenuOpeningEffect") == "scale-up" && mTopScale < 1.0f && + mGuiStack.size() == 2) { + // The user has entered a submenu when the initial menu screen has not finished scaling + // up. So scale it to full size so it won't be stuck at a smaller size when returning + // from the submenu. + mTopScale = 1.0f; + GuiComponent* menu {mGuiStack.back()}; + glm::vec2 menuCenter {menu->getCenter()}; + menu->setOrigin(0.5f, 0.5f); + menu->setPosition(menuCenter.x, menuCenter.y, 0.0f); + menu->setScale(1.0f); + } + + if (config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_g && + SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) { + // Toggle debug grid with Ctrl-G. + Settings::getInstance()->setBool("DebugGrid", + !Settings::getInstance()->getBool("DebugGrid")); + } + else if (config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_t && + SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) { + // Toggle TextComponent debug view with Ctrl-T. + Settings::getInstance()->setBool("DebugText", + !Settings::getInstance()->getBool("DebugText")); + } + else if (config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_i && + SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) { + // Toggle ImageComponent debug view with Ctrl-I. + Settings::getInstance()->setBool("DebugImage", + !Settings::getInstance()->getBool("DebugImage")); + } + else { + if (peekGui()) + // This is where the majority of inputs will be consumed: the GuiComponent Stack. + this->peekGui()->input(config, input); + } +} + +void Window::textInput(const std::string& text, const bool pasting) +{ + if (peekGui()) + peekGui()->textInput(text, pasting); +} + +void Window::logInput(InputConfig* config, Input input) +{ + std::string mapname; + std::vector maps {config->getMappedTo(input)}; + + for (auto mn : maps) { + mapname += mn; + mapname += ", "; + } + + LOG(LogDebug) << "Window::logInput(" << config->getDeviceName() << "): " << input.string() + << ", isMappedTo=" << mapname << "value=" << input.value; +} + +void Window::update(int deltaTime) +{ + if (mInvalidateCacheTimer > 0) + mInvalidateCacheTimer = glm::clamp(mInvalidateCacheTimer - deltaTime, 0, 500); + + if (mNormalizeNextUpdate) { + mNormalizeNextUpdate = false; + mTimeSinceLastInput = 0; + if (deltaTime > mAverageDeltaTime) + deltaTime = mAverageDeltaTime; + } + + mFrameTimeElapsed += deltaTime; + ++mFrameCountElapsed; + if (mFrameTimeElapsed > 500) { + mAverageDeltaTime = mFrameTimeElapsed / mFrameCountElapsed; + + if (Settings::getInstance()->getBool("DisplayGPUStatistics")) { + std::stringstream ss; + + // FPS. + ss << std::fixed << std::setprecision(1) + << (1000.0f * static_cast(mFrameCountElapsed) / + static_cast(mFrameTimeElapsed)) + << " FPS ("; + ss << std::fixed << std::setprecision(2) + << (static_cast(mFrameTimeElapsed) / static_cast(mFrameCountElapsed)) + << " ms)"; + + // The following calculations are not accurate, and the font calculation is completely + // broken. For now, still report the figures as it's somehow useful to locate memory + // leaks and similar. But this needs to be completely overhauled later on. + // VRAM. + float textureVramUsageMiB {TextureResource::getTotalMemUsage() / 1024.0f / 1024.0f}; + float textureTotalUsageMiB {TextureResource::getTotalTextureSize() / 1024.0f / 1024.0f}; + float fontVramUsageMiB {Font::getTotalMemUsage() / 1024.0f / 1024.0f}; + + ss << "\nFont VRAM: " << fontVramUsageMiB + << " MiB\nTexture VRAM: " << textureVramUsageMiB + << " MiB\nMax Texture VRAM: " << textureTotalUsageMiB << " MiB"; + mFrameDataText = std::unique_ptr(mDefaultFonts.at(0)->buildTextCache( + ss.str(), mRenderer->getScreenWidth() * 0.02f, mRenderer->getScreenHeight() * 0.02f, + 0xFF00FFFF, 1.3f)); + } + + mFrameTimeElapsed = 0; + mFrameCountElapsed = 0; + } + + mTimeSinceLastInput += deltaTime; + + // If there is a popup notification queued, then display it. + if (mInfoPopupQueue.size() > 0) { + bool popupIsRunning = false; + + // If uncommenting the following, new popups will not be displayed until the one + // currently shown has reached its display duration. This will be used later when + // support for multiple GuiInfoPopup notifications is implemented. + // if (mInfoPopup != nullptr && mInfoPopup->isRunning()) + // popupIsRunning = true; + + if (!popupIsRunning) { + delete mInfoPopup; + mInfoPopup = + new GuiInfoPopup(mInfoPopupQueue.front().first, mInfoPopupQueue.front().second); + mInfoPopupQueue.pop(); + } + } + + if (peekGui()) + peekGui()->update(deltaTime); + + // If the theme changed, we need to update the background once so that the camera + // will be moved. This is required as theme changes always make a transition to + // the system view. If we wouldn't make this update, the camera movement would take + // place once the menu has been closed. + if (mChangedTheme) { + mGuiStack.front()->update(deltaTime); + mChangedTheme = false; + } + + if (mMediaViewer && mRenderMediaViewer) + mMediaViewer->update(deltaTime); + + if (mPDFViewer && mRenderPDFViewer) + mPDFViewer->update(deltaTime); + + if (mLaunchScreen && mRenderLaunchScreen) + mLaunchScreen->update(deltaTime); + + if (mScreensaver && mRenderScreensaver) + mScreensaver->update(deltaTime); +} + +bool Window::isBackgroundDimmed() +{ + return !mGuiStack.empty() && (mGuiStack.front() != mGuiStack.back() || mRenderLaunchScreen); +} + +void Window::render() +{ + // Short 25 ms delay before invalidating the cached background which will give the various + // components a chance to render so they don't get exclued from the new cached image. + if (mInitiateCacheTimer) { + mInvalidateCacheTimer = 25; + mInitiateCacheTimer = false; + } + + glm::mat4 trans {mRenderer->getIdentity()}; + + mRenderedHelpPrompts = false; + + // Draw only bottom and top of GuiStack (if they are different). + if (!mGuiStack.empty()) { + auto& bottom = mGuiStack.front(); + auto& top = mGuiStack.back(); + + if (mRenderMediaViewer || mRenderPDFViewer || mRenderScreensaver) { + bottom->cancelAllAnimations(); + bottom->stopAllAnimations(); + } + + // Don't render the system view or gamelist view if the media viewer is active or if the + // video or slideshow screensaver is running. The exception is if the fallback screensaver + // is active due to a lack of videos or images. + bool renderBottom {true}; + if (mRenderMediaViewer || mRenderPDFViewer) + renderBottom = false; + else if (mRenderScreensaver && mScreensaver->isFallbackScreensaver()) + renderBottom = true; + else if (mRenderScreensaver && + Settings::getInstance()->getString("ScreensaverType") == "video") + renderBottom = false; + else if (mRenderScreensaver && + Settings::getInstance()->getString("ScreensaverType") == "slideshow") + renderBottom = false; + + // Don't render the bottom if the menu is open and the opening animation has finished + // playing. If the background is invalidated rendering will be enabled briefly until + // a new cached background has been generated. + if (mGuiStack.size() > 1 && mCachedBackground) { + if ((Settings::getInstance()->getString("MenuOpeningEffect") == "scale-up" && + mBackgroundOverlayOpacity == 1.0f) || + Settings::getInstance()->getString("MenuOpeningEffect") != "scale-up") + renderBottom = false; + } + + if (renderBottom) + bottom->render(trans); + + if (bottom != top || mRenderLaunchScreen) { + if (!mCachedBackground && mInvalidateCacheTimer == 0) { + // Generate a cache texture of the shaded background when opening the menu, which + // will remain valid until the menu is closed. This is way faster than having to + // render the shaders for every frame. +#if (CLOCK_BACKGROUND_CREATION) + const auto backgroundStartTime = std::chrono::system_clock::now(); +#endif + std::vector processedTexture( + static_cast(mRenderer->getScreenWidth()) * + static_cast(mRenderer->getScreenHeight()) * 4); + + // De-focus the background using multiple passes of gaussian blur, with the number + // of iterations relative to the screen resolution. + Renderer::postProcessingParams backgroundParameters; + + // TODO: Add support for non-blurred background when rotating screen 90 or 270 + // degrees. + if (Settings::getInstance()->getBool("MenuBlurBackground") || + mRenderer->getScreenRotation() == 90 || mRenderer->getScreenRotation() == 270) { + + // We run two passes to make the blur smoother. + backgroundParameters.blurPasses = 2; + backgroundParameters.blurStrength = 1.35f; + + // Also dim the background slightly. + if (Settings::getInstance()->getString("MenuColorScheme") == "light") + backgroundParameters.dimming = 0.60f; + else + backgroundParameters.dimming = 0.80f; + + mRenderer->shaderPostprocessing(Renderer::Shader::CORE | + Renderer::Shader::BLUR_HORIZONTAL | + Renderer::Shader::BLUR_VERTICAL, + backgroundParameters, &processedTexture[0]); + } + else { + // Dim the background slightly. + if (Settings::getInstance()->getString("MenuColorScheme") == "light") + backgroundParameters.dimming = 0.60f; + else + backgroundParameters.dimming = 0.80f; + + mRenderer->shaderPostprocessing(Renderer::Shader::CORE, backgroundParameters, + &processedTexture[0]); + } + + if (mRenderer->getScreenRotation() == 0 || mRenderer->getScreenRotation() == 180) { + mPostprocessedBackground->initFromPixels( + &processedTexture[0], static_cast(mRenderer->getScreenWidth()), + static_cast(mRenderer->getScreenHeight())); + } + else { + mPostprocessedBackground->initFromPixels( + &processedTexture[0], static_cast(mRenderer->getScreenHeight()), + static_cast(mRenderer->getScreenWidth())); + } + + mBackgroundOverlay->setImage(mPostprocessedBackground); + + // The following is done to avoid fading in if the cached image was + // invalidated (rather than the menu being opened). + if (mInvalidatedCachedBackground) { + mBackgroundOverlayOpacity = 1.0f; + mInvalidatedCachedBackground = false; + } + else { + mBackgroundOverlayOpacity = 0.1f; + } + + mCachedBackground = true; + +#if (CLOCK_BACKGROUND_CREATION) + LOG(LogDebug) << "Window::render(): Time to create cached background: " + << std::chrono::duration_cast( + std::chrono::system_clock::now() - backgroundStartTime) + .count() + << " ms"; +#endif + } + // Fade in the cached background if the menu opening effect has been set to scale-up. + if (Settings::getInstance()->getString("MenuOpeningEffect") == "scale-up") { + mBackgroundOverlay->setOpacity(mBackgroundOverlayOpacity); + if (mBackgroundOverlayOpacity < 1.0f) + mBackgroundOverlayOpacity = + glm::clamp(mBackgroundOverlayOpacity + 0.118f, 0.0f, 1.0f); + } + + mBackgroundOverlay->render(trans); + + // Scale-up menu opening effect. + if (Settings::getInstance()->getString("MenuOpeningEffect") == "scale-up") { + if (mTopScale < 1.0f) { + mTopScale = glm::clamp(mTopScale + 0.07f, 0.0f, 1.0f); + glm::vec2 topCenter {top->getCenter()}; + top->setOrigin(0.5f, 0.5f); + top->setPosition(topCenter.x, topCenter.y, 0.0f); + top->setScale(mTopScale); + } + } + + if (!mRenderedHelpPrompts) + mHelp->render(trans); + + if (!mRenderLaunchScreen) + top->render(trans); + } + else { + mCachedBackground = false; + mTopScale = 0.5f; + } + } + + // Render the quick list scrolling overlay, which is triggered in IList. + if (mListScrollOpacity != 0.0f) { + mRenderer->setMatrix(mRenderer->getIdentity()); + mRenderer->drawRect(0.0f, 0.0f, mRenderer->getScreenWidth(), mRenderer->getScreenHeight(), + 0x00000000 | static_cast(mListScrollOpacity * 255.0f), + 0x00000000 | static_cast(mListScrollOpacity * 255.0f)); + + glm::vec2 offset {mListScrollFont->sizeText(mListScrollText)}; + offset.x = (mRenderer->getScreenWidth() - offset.x) * 0.5f; + offset.y = (mRenderer->getScreenHeight() - offset.y) * 0.5f; + + TextCache* cache {mListScrollFont->buildTextCache( + mListScrollText, offset.x, offset.y, + 0xFFFFFF00 | static_cast(mListScrollOpacity * 255.0f))}; + mListScrollFont->renderTextCache(cache); + delete cache; + } + + unsigned int screensaverTimer { + static_cast(Settings::getInstance()->getInt("ScreensaverTimer"))}; + if (mTimeSinceLastInput >= screensaverTimer && screensaverTimer != 0) { + // If the media viewer or PDF viewer is running, or if a menu is open, then reset the + // screensaver timer so that the screensaver won't start. + if (mRenderMediaViewer || mRenderPDFViewer || mGuiStack.front() != mGuiStack.back()) + mTimeSinceLastInput = 0; + // If a game has been launched, reset the screensaver timer as we don't want to start + // the screensaver in the background when running a game. + else if (mGameLaunchedState) + mTimeSinceLastInput = 0; + else if (!isProcessing() && !mScreensaver->isScreensaverActive()) + startScreensaver(true); + } + + if (mInfoPopup) + mInfoPopup->render(trans); + + if (mRenderMediaViewer) + mMediaViewer->render(trans); + + if (mRenderPDFViewer) + mPDFViewer->render(trans); + + if (mRenderLaunchScreen) + mLaunchScreen->render(trans); + + if (mRenderScreensaver) + mScreensaver->renderScreensaver(); + + if (Settings::getInstance()->getBool("DisplayGPUStatistics") && mFrameDataText) { + mRenderer->setMatrix(mRenderer->getIdentity()); + mDefaultFonts.at(1)->renderTextCache(mFrameDataText.get()); + } +} + +void Window::renderSplashScreen(SplashScreenState state, float progress) +{ + glm::mat4 trans {mRenderer->getIdentity()}; + mRenderer->setMatrix(trans); + mRenderer->drawRect(0.0f, 0.0f, mRenderer->getScreenWidth(), mRenderer->getScreenHeight(), + 0x000000FF, 0x000000FF); + mSplash->render(trans); + mRenderer->setMatrix(trans); + + if (state != SplashScreenState::RELOADING) { + // We need to render three rectangles: border, black center and actual progress bar. + for (size_t i {0}; i < mProgressBarRectangles.size(); ++i) { + const float rectWidth {i == mProgressBarRectangles.size() - 1 ? progress : 1.0f}; + mRenderer->drawRect( + mProgressBarRectangles.at(i).barPosX, mProgressBarRectangles.at(i).barPosY, + mProgressBarRectangles.at(i).barWidth * rectWidth, + mProgressBarRectangles.at(i).barHeight, mProgressBarRectangles.at(i).color, + mProgressBarRectangles.at(i).color); + } + } + + float textPosX {0.0f}; + float textPosY {mSplashTextPositions.y}; + + if (state == SplashScreenState::SCANNING) { + textPosX = mSplashTextPositions.x; + } + else if (state == SplashScreenState::POPULATING) { + textPosX = mSplashTextPositions.z; + } + else if (state == SplashScreenState::RELOADING) { + textPosX = mSplashTextPositions.w; + textPosY += mDefaultFonts.at(1)->getLetterHeight(); + } + + trans = glm::translate(trans, glm::round(glm::vec3 {textPosX, textPosY, 0.0f})); + mRenderer->setMatrix(trans); + + if (state == SplashScreenState::SCANNING) + mDefaultFonts.at(1)->renderTextCache(mSplashTextScanning.get()); + else if (state == SplashScreenState::POPULATING) + mDefaultFonts.at(1)->renderTextCache(mSplashTextPopulating.get()); + else if (state == SplashScreenState::RELOADING) + mDefaultFonts.at(1)->renderTextCache(mSplashTextReloading.get()); + + mRenderer->swapBuffers(); +} + +void Window::renderListScrollOverlay(const float opacity, const std::string& text) +{ + mListScrollOpacity = opacity * 0.6f; + mListScrollText = text; +} + +void Window::renderHelpPromptsEarly() +{ + mHelp->render(mRenderer->getIdentity()); + mRenderedHelpPrompts = true; +} + +void Window::setHelpPrompts(const std::vector& prompts, const HelpStyle& style) +{ + mHelp->clearPrompts(); + mHelp->setStyle(style); + + std::vector addPrompts; + + std::map inputSeenMap; + std::map mappedToSeenMap; + for (auto it = prompts.cbegin(); it != prompts.cend(); ++it) { + // Only add it if the same icon hasn't already been added. + if (inputSeenMap.emplace(it->first, true).second) { + // This symbol hasn't been seen yet, what about the action name? + auto mappedTo = mappedToSeenMap.find(it->second); + if (mappedTo != mappedToSeenMap.cend()) { + // Yes, it has! + + // Can we combine? (dpad only). + if ((it->first == "up/down" && + addPrompts.at(mappedTo->second).first != "left/right") || + (it->first == "left/right" && + addPrompts.at(mappedTo->second).first != "up/down")) { + // Yes. + addPrompts.at(mappedTo->second).first = "up/down/left/right"; + } + else { + addPrompts.push_back(*it); + } + } + else { + mappedToSeenMap.emplace(it->second, static_cast(addPrompts.size())); + addPrompts.push_back(*it); + } + } + } + + // Sort prompts so it goes [dpad_all] [dpad_u/d] [dpad_l/r] [a/b/x/y/l/r] [start/back]. + std::sort(addPrompts.begin(), addPrompts.end(), + [](const HelpPrompt& a, const HelpPrompt& b) -> bool { + static const std::vector map {"up/down/left/right", + "up/down", + "up", + "down", + "left/right", + "rt", + "lt", + "r", + "l", + "y", + "x", + "b", + "a", + "start", + "back"}; + int i {0}; + int aVal {0}; + int bVal {0}; + while (i < static_cast(map.size())) { + if (a.first == map[i]) + aVal = i; + if (b.first == map[i]) + bVal = i; + ++i; + } + + return aVal > bVal; + }); + + mHelp->setPrompts(addPrompts); +} + +void Window::stopInfoPopup() +{ + if (mInfoPopup) + mInfoPopup->stop(); + + if (mInfoPopupQueue.size() > 0) + std::queue>().swap(mInfoPopupQueue); +} + +void Window::startScreensaver(bool onTimer) +{ + if (mScreensaver && !mRenderScreensaver) { + if (onTimer) + Scripting::fireEvent("screensaver-start", "timer"); + else + Scripting::fireEvent("screensaver-start", "manual"); + setAllowTextScrolling(false); + setAllowFileAnimation(false); + mScreensaver->startScreensaver(true); + mScreensaver->renderScreensaver(); + mRenderScreensaver = true; + } +} + +bool Window::stopScreensaver() +{ + if (mScreensaver && mRenderScreensaver) { + mScreensaver->stopScreensaver(); + mRenderScreensaver = false; + setAllowTextScrolling(true); + setAllowFileAnimation(true); + + return true; + } + + return false; +} + +void Window::startMediaViewer(FileData* game) +{ + if (mMediaViewer) { + if (mMediaViewer->startMediaViewer(game)) { + setAllowTextScrolling(false); + setAllowFileAnimation(false); + + mRenderMediaViewer = true; + } + } +} + +void Window::stopMediaViewer() +{ + if (mMediaViewer) { + mMediaViewer->stopMediaViewer(); + setAllowTextScrolling(true); + setAllowFileAnimation(true); + } + + mRenderMediaViewer = false; +} + +void Window::startPDFViewer(FileData* game) +{ + if (mPDFViewer) { + if (mPDFViewer->startPDFViewer(game)) { + setAllowTextScrolling(false); + setAllowFileAnimation(false); + + mRenderPDFViewer = true; + } + else { + queueInfoPopup("ERROR: COULDN'T RENDER PDF FILE", 4000); + } + } +} + +void Window::stopPDFViewer() +{ + if (mPDFViewer) { + mPDFViewer->stopPDFViewer(); + setAllowTextScrolling(true); + setAllowFileAnimation(true); + } + + mRenderPDFViewer = false; +} + +void Window::displayLaunchScreen(FileData* game) +{ + if (mLaunchScreen) { + mLaunchScreen->displayLaunchScreen(game); + mRenderLaunchScreen = true; + } +} + +void Window::closeLaunchScreen() +{ + if (mLaunchScreen) + mLaunchScreen->closeLaunchScreen(); + + mRenderLaunchScreen = false; +} + +int Window::getVideoPlayerCount() +{ + int videoPlayerCount; + videoPlayerCount = mVideoPlayerCount; + return videoPlayerCount; +} + +void Window::invalidateCachedBackground() +{ + mCachedBackground = false; + mInvalidatedCachedBackground = true; + mInitiateCacheTimer = true; +} + +bool Window::isProcessing() +{ + return count_if(mGuiStack.cbegin(), mGuiStack.cend(), + [](GuiComponent* c) { return c->isProcessing(); }) > 0; +}