From 960152683efb5ebe031ebceeef39dcc8fea62f0d Mon Sep 17 00:00:00 2001 From: XargonWan Date: Mon, 13 Nov 2023 09:21:26 +0100 Subject: [PATCH] SFTP: various fixes, missing libwrap --- functions/sftp_server.sh | 13 +- net.retrodeck.retrodeck.yml | 2 + rd-submodules/es-de/patches-tmp/GuiMenu.cpp | 1949 +++++++++++++++++ rd-submodules/es-de/patches-tmp/GuiMenu.h | 56 + .../es-de/patches-tmp/GuiThemeDownloader.cpp | 1372 ++++++++++++ .../es-de/patches-tmp/ViewController.cpp | 1390 ++++++++++++ rd-submodules/es-de/patches-tmp/Window.cpp | 919 ++++++++ rd-submodules/retroarch | 2 +- rd-submodules/shared-modules | 2 +- 9 files changed, 5696 insertions(+), 9 deletions(-) create mode 100644 rd-submodules/es-de/patches-tmp/GuiMenu.cpp create mode 100644 rd-submodules/es-de/patches-tmp/GuiMenu.h create mode 100644 rd-submodules/es-de/patches-tmp/GuiThemeDownloader.cpp create mode 100644 rd-submodules/es-de/patches-tmp/ViewController.cpp create mode 100644 rd-submodules/es-de/patches-tmp/Window.cpp diff --git a/functions/sftp_server.sh b/functions/sftp_server.sh index f50a5b67..6e3b9a35 100644 --- a/functions/sftp_server.sh +++ b/functions/sftp_server.sh @@ -6,6 +6,7 @@ ip=$(hostname -I | awk '{print $1}') # Set the log file path log_file="$rdhome/.logs/sftp_server.log" +# TODO: add nc or find an alternative command # Check if the port is in use if nc -z localhost $port; then zenity --error --no-wrap \ @@ -16,20 +17,18 @@ if nc -z localhost $port; then fi # Create a temporary directory for SFTP chroot -mkdir -p /tmp/sftp_home/retrodeck +mkdir -p /tmp/sftp_home/retrodeck/etc echo "retrodeck:$(openssl passwd -1 retrodeck)" >> /tmp/sftp_home/retrodeck/etc/passwd # Set rdhome as the home directory for retrodeck user -echo "Match User retrodeck\n ChrootDirectory $rdhome" >> /etc/ssh/sshd_config +mkdir -p /var/config/retrodeck/ssh +echo "Match User retrodeck\n ChrootDirectory $rdhome" >> /var/config/retrodeck/ssh/sshd_config # Redirect SSHD logs to the specified log file -echo "Match User retrodeck\n ChrootDirectory $rdhome\n ForceCommand internal-sftp -l INFO -f $log_file" >> /etc/ssh/sshd_config - -# Restart SSHD to apply the new configuration -service ssh restart +echo "Match User retrodeck\n ChrootDirectory $rdhome\n ForceCommand internal-sftp -l INFO -f $log_file" >> /var/config/retrodeck/ssh/sshd_config # Start SSHD with SFTP support and specific user and password -nohup /usr/sbin/sshd -p $port -o PasswordAuthentication=yes -o PubkeyAuthentication=no -o AuthorizedKeysFile=/dev/null -o UsePAM=no -o AllowTcpForwarding=no -o PermitRootLogin=no -o ChrootDirectory=/tmp/sftp_home/retrodeck & +nohup sshd -p $port -o PasswordAuthentication=yes -o PubkeyAuthentication=no -o AuthorizedKeysFile=/dev/null -o UsePAM=no -o AllowTcpForwarding=no -o PermitRootLogin=no -o ChrootDirectory=/tmp/sftp_home/retrodeck & # Get the PID of the SSH/SFTP server process ssh_pid=$! diff --git a/net.retrodeck.retrodeck.yml b/net.retrodeck.retrodeck.yml index da7e4494..7cf7923f 100644 --- a/net.retrodeck.retrodeck.yml +++ b/net.retrodeck.retrodeck.yml @@ -297,6 +297,8 @@ modules: url: http://de.archive.ubuntu.com/ubuntu/pool/main/h/hostname/hostname_3.23ubuntu2_amd64.deb sha256: cec0448fef88a43a3c232fd9df555ca4c468cf5e16ddf3a1b0a5f7b7f076e413 + # TODO: we need libwrap: https://ubuntu.pkgs.org/20.04/ubuntu-main-amd64/libwrap0_7.6.q-30_amd64.deb.html + # NOTE: we're only copying usr, there are other dirs but we don't need them atm - name: openssh-server buildsystem: simple diff --git a/rd-submodules/es-de/patches-tmp/GuiMenu.cpp b/rd-submodules/es-de/patches-tmp/GuiMenu.cpp new file mode 100644 index 00000000..8eff03a1 --- /dev/null +++ b/rd-submodules/es-de/patches-tmp/GuiMenu.cpp @@ -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/rd-submodules/es-de/patches-tmp/GuiMenu.h b/rd-submodules/es-de/patches-tmp/GuiMenu.h new file mode 100644 index 00000000..924f9213 --- /dev/null +++ b/rd-submodules/es-de/patches-tmp/GuiMenu.h @@ -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/rd-submodules/es-de/patches-tmp/GuiThemeDownloader.cpp b/rd-submodules/es-de/patches-tmp/GuiThemeDownloader.cpp new file mode 100644 index 00000000..efb05884 --- /dev/null +++ b/rd-submodules/es-de/patches-tmp/GuiThemeDownloader.cpp @@ -0,0 +1,1372 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// GuiThemeDownloader.cpp +// +// Theme downloader. +// + +#include "guis/GuiThemeDownloader.h" + +#include "EmulationStation.h" +#include "ThemeData.h" +#include "components/MenuComponent.h" +#include "resources/ResourceManager.h" + +#include "rapidjson/document.h" +#include "rapidjson/error/en.h" + +#define LOCAL_TESTING_FILE false +#define DEBUG_CLONING false + +GuiThemeDownloader::GuiThemeDownloader(std::function updateCallback) + : mRenderer {Renderer::getInstance()} + , mBackground {":/graphics/frame.svg"} + , mGrid {glm::ivec2 {2, 4}} + , mUpdateCallback(updateCallback) + , mRepositoryError {RepositoryError::NO_REPO_ERROR} + , mFetching {false} + , mLatestThemesList {false} + , mAttemptedFetch {false} + , mHasThemeUpdates {false} + , mFullscreenViewing {false} + , mFullscreenViewerIndex {0} +{ + addChild(&mBackground); + addChild(&mGrid); + +#if defined(_WIN64) + // Required due to the idiotic file locking that exists on this operating system. + ViewController::getInstance()->stopViewVideos(); +#endif + + const float fontSizeSmall {mRenderer->getIsVerticalOrientation() ? FONT_SIZE_MINI : + FONT_SIZE_SMALL}; + + // Set up main grid. + mTitle = std::make_shared("THEME DOWNLOADER", Font::get(FONT_SIZE_LARGE), + mMenuColorTitle, ALIGN_CENTER); + mGrid.setEntry(mTitle, glm::ivec2 {0, 0}, false, true, glm::ivec2 {2, 2}, + GridFlags::BORDER_BOTTOM); + + // We need a center grid embedded within the main grid in order for navigation and helpsystem + // entries to work and display correctly. + mCenterGrid = std::make_shared(glm::ivec2 {8, 5}); + mCenterGrid->setEntry(std::make_shared(), glm::ivec2 {0, 0}, false, false, + glm::ivec2 {1, 5}); + + mVariantsLabel = + std::make_shared("", Font::get(fontSizeSmall), mMenuColorTitle, ALIGN_LEFT); + mCenterGrid->setEntry(mVariantsLabel, glm::ivec2 {1, 0}, false, true, glm::ivec2 {1, 1}); + + mColorSchemesLabel = + std::make_shared("", Font::get(fontSizeSmall), mMenuColorTitle, ALIGN_LEFT); + mCenterGrid->setEntry(mColorSchemesLabel, glm::ivec2 {1, 1}, false, true, glm::ivec2 {1, 1}); + + mAspectRatiosLabel = + std::make_shared("", Font::get(fontSizeSmall), mMenuColorTitle, ALIGN_LEFT); + mCenterGrid->setEntry(mAspectRatiosLabel, glm::ivec2 {3, 0}, false, true, glm::ivec2 {1, 1}); + + mFutureUseLabel = + std::make_shared("", Font::get(fontSizeSmall), mMenuColorTitle, ALIGN_LEFT); + mCenterGrid->setEntry(mFutureUseLabel, glm::ivec2 {3, 1}, false, true, glm::ivec2 {1, 1}); + + mCenterGrid->setEntry(std::make_shared(), glm::ivec2 {5, 0}, false, false, + glm::ivec2 {1, 5}); + + mVariantCount = std::make_shared("", Font::get(fontSizeSmall, FONT_PATH_LIGHT), + mMenuColorTitle, ALIGN_LEFT); + mCenterGrid->setEntry(mVariantCount, glm::ivec2 {2, 0}, false, true, glm::ivec2 {1, 1}); + + mColorSchemesCount = std::make_shared( + "", Font::get(fontSizeSmall, FONT_PATH_LIGHT), mMenuColorTitle, ALIGN_LEFT); + mCenterGrid->setEntry(mColorSchemesCount, glm::ivec2 {2, 1}, false, true, glm::ivec2 {1, 1}); + + mAspectRatiosCount = std::make_shared( + "", Font::get(fontSizeSmall, FONT_PATH_LIGHT), mMenuColorTitle, ALIGN_LEFT); + mCenterGrid->setEntry(mAspectRatiosCount, glm::ivec2 {4, 0}, false, true, glm::ivec2 {1, 1}); + + mFutureUseCount = std::make_shared("", Font::get(fontSizeSmall, FONT_PATH_LIGHT), + mMenuColorTitle, ALIGN_LEFT); + mCenterGrid->setEntry(mFutureUseCount, glm::ivec2 {4, 1}, false, true, glm::ivec2 {1, 1}); + + mDownloadStatus = std::make_shared("", Font::get(fontSizeSmall, FONT_PATH_BOLD), + mMenuColorTitle, ALIGN_LEFT); + mCenterGrid->setEntry(mDownloadStatus, glm::ivec2 {1, 2}, false, true, glm::ivec2 {2, 1}); + + mLocalChanges = std::make_shared("", Font::get(fontSizeSmall, FONT_PATH_BOLD), + mMenuColorTitle, ALIGN_LEFT); + mCenterGrid->setEntry(mLocalChanges, glm::ivec2 {3, 2}, false, true, glm::ivec2 {2, 1}); + + mScreenshot = std::make_shared(); + mScreenshot->setLinearInterpolation(true); + mCenterGrid->setEntry(mScreenshot, glm::ivec2 {1, 3}, false, true, glm::ivec2 {4, 1}); + + mAuthor = std::make_shared("", Font::get(FONT_SIZE_MINI * 0.9f, FONT_PATH_LIGHT), + mMenuColorTitle, ALIGN_LEFT); + mCenterGrid->setEntry(mAuthor, glm::ivec2 {1, 4}, false, true, glm::ivec2 {4, 1}); + + mList = std::make_shared(); + mCenterGrid->setEntry(mList, glm::ivec2 {6, 0}, true, true, glm::ivec2 {2, 5}, + GridFlags::BORDER_LEFT); + + mGrid.setEntry(mCenterGrid, glm::ivec2 {0, 2}, true, false, glm::ivec2 {2, 1}); + + // Set up scroll indicators. + mScrollUp = std::make_shared(); + mScrollDown = std::make_shared(); + + mScrollUp->setResize(0.0f, mTitle->getFont()->getLetterHeight() / 2.0f); + mScrollUp->setOrigin(0.0f, -0.35f); + + mScrollDown->setResize(0.0f, mTitle->getFont()->getLetterHeight() / 2.0f); + mScrollDown->setOrigin(0.0f, 0.35f); + + mScrollIndicator = std::make_shared(mList, mScrollUp, mScrollDown); + + mGrid.setEntry(mScrollUp, glm::ivec2 {1, 0}, false, false, glm::ivec2 {1, 1}); + mGrid.setEntry(mScrollDown, glm::ivec2 {1, 1}, false, false, glm::ivec2 {1, 1}); + + std::vector> buttons; + buttons.push_back(std::make_shared("CLOSE", "CLOSE", [&] { delete this; })); + mButtons = MenuComponent::makeButtonGrid(buttons); + mGrid.setEntry(mButtons, glm::ivec2 {0, 3}, true, false, glm::ivec2 {2, 1}, + GridFlags::BORDER_TOP); + + // Limit the width of the GUI on ultrawide monitors. The 1.778 aspect ratio value is + // the 16:9 reference. + const float aspectValue {1.778f / Renderer::getScreenAspectRatio()}; + const float width {glm::clamp(0.95f * aspectValue, 0.45f, 0.98f) * mRenderer->getScreenWidth()}; + setSize(width, + mTitle->getSize().y + (mList->getRowHeight() * 9.0f) + mButtons->getSize().y * 1.1f); + + setPosition((mRenderer->getScreenWidth() - mSize.x) / 2.0f, + (mRenderer->getScreenHeight() - mSize.y) / 2.0f); + + mBusyAnim.setSize(mSize); + mBusyAnim.setText("DOWNLOADING THEMES LIST 100%"); + mBusyAnim.onSizeChanged(); + + mList->setCursorChangedCallback([this](CursorState state) { + if (state == CursorState::CURSOR_SCROLLING || state == CursorState::CURSOR_STOPPED) + updateInfoPane(); + }); + + mViewerIndicatorLeft = std::make_shared( + ViewController::ARROW_LEFT_CHAR, Font::get(FONT_SIZE_LARGE * 1.2f, FONT_PATH_BOLD), + 0xCCCCCCFF, ALIGN_CENTER); + + mViewerIndicatorRight = std::make_shared( + ViewController::ARROW_RIGHT_CHAR, Font::get(FONT_SIZE_LARGE * 1.2f, FONT_PATH_BOLD), + 0xCCCCCCFF, ALIGN_CENTER); + + git_libgit2_init(); + + // The promise/future mechanism is used as signaling for the thread to indicate that + // repository fetching has been completed. + std::promise().swap(mPromise); + mFuture = mPromise.get_future(); + + const std::string defaultUserThemeDir {Utils::FileSystem::getHomePath() + + "/.emulationstation/themes"}; + std::string userThemeDirSetting {Utils::FileSystem::expandHomePath( + Settings::getInstance()->getString("UserThemeDirectory"))}; +#if defined(_WIN64) + mThemeDirectory = Utils::String::replace(mThemeDirectory, "\\", "/"); +#endif + + if (userThemeDirSetting == "") { + mThemeDirectory = defaultUserThemeDir; + } + else if (Utils::FileSystem::isDirectory(userThemeDirSetting) || + Utils::FileSystem::isSymlink(userThemeDirSetting)) { + mThemeDirectory = userThemeDirSetting; + } + else { + LOG(LogWarning) << "GuiThemeDownloader: Requested user theme directory \"" + << userThemeDirSetting + << "\" does not exist or is not a directory, reverting to \"" + << defaultUserThemeDir << "\""; + mThemeDirectory = defaultUserThemeDir; + } + + if (mThemeDirectory.back() != '/') + mThemeDirectory.append("/"); +} + +GuiThemeDownloader::~GuiThemeDownloader() +{ + if (mFetchThread.joinable()) + mFetchThread.join(); + + git_libgit2_shutdown(); + + if (mHasThemeUpdates) { + LOG(LogInfo) << "GuiThemeDownloader: There are updates, repopulating the themes"; + ThemeData::populateThemes(); + ViewController::getInstance()->reloadAll(); + if (mUpdateCallback) + mUpdateCallback(); + } + + mWindow->stopInfoPopup(); +} + +bool GuiThemeDownloader::fetchRepository(const std::string& repositoryName, bool allowReset) +{ + int errorCode {0}; + const std::string path {mThemeDirectory + repositoryName}; + mRepositoryError = RepositoryError::NO_REPO_ERROR; + mMessage = ""; + + const bool isThemesList {repositoryName == "themes-list"}; + git_repository* repository {nullptr}; + git_remote* gitRemote {nullptr}; + + try { + mFetching = true; + errorCode = git_repository_open(&repository, &path[0]); + + if (errorCode != 0) { + mRepositoryError = RepositoryError::NOT_A_REPOSITORY; + throw std::runtime_error("Couldn't open local repository, "); + } + errorCode = git_remote_lookup(&gitRemote, repository, "origin"); + if (errorCode != 0) { + mRepositoryError = RepositoryError::INVALID_ORIGIN; + throw std::runtime_error("Couldn't get information about origin, "); + } + +#if LIBGIT2_VER_MAJOR >= 1 + git_fetch_options fetchOptions; + git_fetch_options_init(&fetchOptions, GIT_FETCH_OPTIONS_VERSION); +#else + git_fetch_options fetchOptions = GIT_FETCH_OPTIONS_INIT; +#endif + // Prune branches that are no longer present on remote. + fetchOptions.prune = GIT_FETCH_PRUNE; + + errorCode = git_remote_fetch(gitRemote, nullptr, &fetchOptions, nullptr); + + if (errorCode != 0) + throw std::runtime_error("Couldn't fetch latest commits for \"" + repositoryName + + "\", "); + + git_annotated_commit* annotated {nullptr}; + git_object* object {nullptr}; + + if (git_repository_head_detached(repository)) { + LOG(LogWarning) << "GuiThemeDownloader: Repository \"" << repositoryName + << "\" has HEAD detached, resetting it"; + git_buf buffer {}; + errorCode = git_remote_default_branch(&buffer, gitRemote); + if (errorCode == 0) { + git_reference* oldTargetRef; + git_repository_head(&oldTargetRef, repository); + + const std::string branchName {buffer.ptr, buffer.size}; + errorCode = git_revparse_single(&object, repository, branchName.c_str()); +#if LIBGIT2_VER_MAJOR >= 1 + git_checkout_options checkoutOptions; + git_checkout_options_init(&checkoutOptions, GIT_CHECKOUT_OPTIONS_VERSION); +#else + git_checkout_options checkoutOptions = GIT_CHECKOUT_OPTIONS_INIT; +#endif + checkoutOptions.checkout_strategy = GIT_CHECKOUT_FORCE; + errorCode = git_checkout_tree(repository, object, &checkoutOptions); + errorCode = git_repository_set_head(repository, branchName.c_str()); + + git_reference_free(oldTargetRef); + } + git_buf_dispose(&buffer); + if (repositoryName != "themes-list") + mHasThemeUpdates = true; + } + + errorCode = git_revparse_single(&object, repository, "FETCH_HEAD"); + errorCode = git_annotated_commit_lookup(&annotated, repository, git_object_id(object)); + + git_merge_analysis_t mergeAnalysis {}; + git_merge_preference_t mergePreference {}; + + errorCode = git_merge_analysis(&mergeAnalysis, &mergePreference, repository, + (const git_annotated_commit**)(&annotated), 1); + + if (errorCode != 0) { + git_object_free(object); + git_annotated_commit_free(annotated); + throw std::runtime_error("GuiThemeDownloader: Couldn't run Git merge analysis, "); + } + + if (!(mergeAnalysis & GIT_MERGE_ANALYSIS_UP_TO_DATE) && + !(mergeAnalysis & GIT_MERGE_ANALYSIS_FASTFORWARD)) { + if (allowReset) { + LOG(LogWarning) << "GuiThemeDownloader: Repository \"" << repositoryName + << "\" has diverged from origin, performing hard reset"; + git_object* objectHead {nullptr}; + errorCode = git_revparse_single(&objectHead, repository, "HEAD"); + errorCode = git_reset(repository, objectHead, GIT_RESET_HARD, nullptr); + git_object_free(objectHead); + if (repositoryName != "themes-list") + mHasThemeUpdates = true; + } + else { + LOG(LogWarning) << "GuiThemeDownloader: Repository \"" << repositoryName + << "\" has diverged from origin, can't fast-forward"; + git_annotated_commit_free(annotated); + git_object_free(object); + mPromise.set_value(true); + mRepositoryError = RepositoryError::HAS_DIVERGED; + return true; + } + } + + if (allowReset && checkLocalChanges(repository)) { + LOG(LogWarning) << "GuiThemeDownloader: Repository \"" << repositoryName + << "\" contains local changes, performing hard reset"; + resetRepository(repository); + if (repositoryName != "themes-list") + mHasThemeUpdates = true; + } + + if (mergeAnalysis & GIT_MERGE_ANALYSIS_UP_TO_DATE) { + LOG(LogInfo) << "GuiThemeDownloader: Repository \"" << repositoryName + << "\" already up to date"; + if (repositoryName != "themes-list") + mMessage = "THEME ALREADY UP TO DATE"; + git_annotated_commit_free(annotated); + git_object_free(object); + git_remote_free(gitRemote); + git_repository_free(repository); + mPromise.set_value(true); + if (isThemesList) + mLatestThemesList = true; + return false; + } + + LOG(LogInfo) << "GuiThemeDownloader: Performing fast-forward of repository \"" + << repositoryName << "\""; + + git_reference* oldTargetRef {nullptr}; + git_repository_head(&oldTargetRef, repository); + + const git_oid* objectID {nullptr}; + objectID = git_annotated_commit_id(annotated); + + git_object_lookup(&object, repository, objectID, GIT_OBJECT_COMMIT); + git_reference* newTargetRef {nullptr}; + +#if LIBGIT2_VER_MAJOR >= 1 + git_checkout_options checkoutOptions; + git_checkout_options_init(&checkoutOptions, GIT_CHECKOUT_OPTIONS_VERSION); +#else + git_checkout_options checkoutOptions = GIT_CHECKOUT_OPTIONS_INIT; +#endif + checkoutOptions.checkout_strategy = GIT_CHECKOUT_FORCE; + + git_checkout_tree(repository, object, &checkoutOptions); + errorCode = git_reference_set_target(&newTargetRef, oldTargetRef, objectID, nullptr); + + git_reference_free(oldTargetRef); + git_reference_free(newTargetRef); + git_annotated_commit_free(annotated); + // Not sure why you need to run this twice, but if you don't there will be a memory leak. + git_object_free(object); + git_object_free(object); + + if (errorCode != 0) + throw std::runtime_error("Couldn't fast-forward repository, "); + + if (isThemesList) + mLatestThemesList = true; + } + catch (std::runtime_error& runtimeError) { + const git_error* gitError {git_error_last()}; + LOG(LogError) << "GuiThemeDownloader: " << runtimeError.what() << gitError->message; + mRepositoryError = RepositoryError::FETCH_ERROR; + mMessage = gitError->message; + git_error_clear(); + git_remote_free(gitRemote); + git_repository_free(repository); + mPromise.set_value(true); + return true; + } + + if (repositoryName != "themes-list") { + mMessage = "THEME HAS BEEN UPDATED"; + mHasThemeUpdates = true; + } + + git_remote_free(gitRemote); + git_repository_free(repository); + mPromise.set_value(true); + return false; +} + +bool GuiThemeDownloader::checkLocalChanges(git_repository* repository) +{ + git_status_list* status {nullptr}; + size_t statusEntryCount {0}; + int errorCode {0}; + +#if LIBGIT2_VER_MAJOR >= 1 + git_status_options statusOptions; + git_status_options_init(&statusOptions, GIT_STATUS_OPTIONS_VERSION); +#else + git_status_options statusOptions = GIT_STATUS_OPTIONS_INIT; +#endif + // We don't include untracked files (GIT_STATUS_OPT_INCLUDE_UNTRACKED) as this makes + // it possible to add custom files to the repository without overwriting these when + // pulling theme updates. + statusOptions.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; + statusOptions.flags = + GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX | GIT_STATUS_OPT_SORT_CASE_SENSITIVELY; + + errorCode = git_status_list_new(&status, repository, &statusOptions); + if (errorCode == 0) + statusEntryCount = git_status_list_entrycount(status); + + git_status_list_free(status); + // TODO: Also check if there are any local commits not on origin. + + return (statusEntryCount != 0); +} + +bool GuiThemeDownloader::checkCorruptRepository(git_repository* repository) +{ + // For the time being we only check if there are no tracked files in the repository. If there + // are none then it would indicate that it has not been properly cloned (for example if the + // ES-DE process was killed during the clone operation). + git_status_list* status {nullptr}; + size_t statusEntryCount {0}; + int errorCode {0}; + +#if LIBGIT2_VER_MAJOR >= 1 + git_status_options statusOptions; + git_status_options_init(&statusOptions, GIT_STATUS_OPTIONS_VERSION); +#else + git_status_options statusOptions = GIT_STATUS_OPTIONS_INIT; +#endif + statusOptions.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; + statusOptions.flags = GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX | + GIT_STATUS_OPT_SORT_CASE_SENSITIVELY | GIT_STATUS_OPT_INCLUDE_UNMODIFIED; + + errorCode = git_status_list_new(&status, repository, &statusOptions); + if (errorCode == 0) + statusEntryCount = git_status_list_entrycount(status); + + git_status_list_free(status); + + return (statusEntryCount == 0); +} + +void GuiThemeDownloader::resetRepository(git_repository* repository) +{ + git_object* objectHead {nullptr}; + if (git_revparse_single(&objectHead, repository, "HEAD") == 0) + git_reset(repository, objectHead, GIT_RESET_HARD, nullptr); + git_object_free(objectHead); +} + +void GuiThemeDownloader::makeInventory() +{ + for (auto& theme : mThemes) { + const std::string path {mThemeDirectory + theme.reponame}; + theme.invalidRepository = false; + theme.corruptRepository = false; + theme.shallowRepository = false; + theme.manuallyDownloaded = false; + theme.hasLocalChanges = false; + theme.isCloned = false; + + if (Utils::FileSystem::exists(path + "-main")) { + theme.manuallyDownloaded = true; + theme.manualExtension = "-main"; + } + else if (Utils::FileSystem::exists(path + "-master")) { + theme.manuallyDownloaded = true; + theme.manualExtension = "-master"; + } + + if (Utils::FileSystem::exists(path)) { + git_repository* repository {nullptr}; + int errorCode {0}; + + errorCode = git_repository_open(&repository, &path[0]); + if (errorCode != 0) { + theme.invalidRepository = true; + git_repository_free(repository); + continue; + } + + if (git_repository_is_shallow(repository)) { + theme.shallowRepository = true; + git_repository_free(repository); + continue; + } + + if (checkCorruptRepository(repository)) { + theme.corruptRepository = true; + git_repository_free(repository); + continue; + } + + theme.isCloned = true; + + if (checkLocalChanges(repository)) + theme.hasLocalChanges = true; + else if (git_repository_head_detached(repository)) + theme.hasLocalChanges = true; + + git_repository_free(repository); + } + } +} + +bool GuiThemeDownloader::renameDirectory(const std::string& path, const std::string& extension) +{ + LOG(LogInfo) << "Renaming directory " << path; + int index {1}; + bool renameStatus {false}; + + if (!Utils::FileSystem::exists(path + extension)) { + renameStatus = Utils::FileSystem::renameFile(path, path + extension, false); + } + else { + // This will hopefully never be needed as it should only occur if a theme has been + // downloaded manually multiple times and the theme downloader has been ran multiple times + // as well. + for (; index < 10; ++index) { + if (!Utils::FileSystem::exists(path + "_" + std::to_string(index) + extension)) { + renameStatus = Utils::FileSystem::renameFile( + path, path + "_" + std::to_string(index) + extension, false); + break; + } + } + } + + if (renameStatus) { + mWindow->pushGui(new GuiMsgBox( + getHelpStyle(), "COULDN'T RENAME DIRECTORY \"" + path + "\", PERMISSION PROBLEMS?", + "OK", [] { return; }, "", nullptr, "", nullptr, nullptr, true)); + return true; + } + else { + return false; + } +} + +void GuiThemeDownloader::parseThemesList() +{ +#if (LOCAL_TESTING_FILE) + LOG(LogWarning) << "GuiThemeDownloader: Using local \"themes.json\" testing file"; + + const std::string themesFile {Utils::FileSystem::getHomePath() + + "/.emulationstation/themes.json"}; +#else + const std::string themesFile {mThemeDirectory + "themes-list/themes.json"}; +#endif + + if (!Utils::FileSystem::exists(themesFile)) { + LOG(LogError) << "GuiThemeDownloader: No themes.json file found"; + mWindow->pushGui(new GuiMsgBox( + getHelpStyle(), "COULDN'T FIND THE THEMES LIST CONFIGURATION FILE", "OK", + [] { return; }, "", nullptr, "", nullptr, nullptr, true)); + mGrid.removeEntry(mCenterGrid); + mGrid.setCursorTo(mButtons); + return; + } + + const ResourceData& themesFileData {ResourceManager::getInstance().getFileData(themesFile)}; + rapidjson::Document doc; + doc.Parse(reinterpret_cast(themesFileData.ptr.get()), themesFileData.length); + + if (doc.HasParseError()) { + LOG(LogError) << "GuiThemeDownloader: Couldn't parse the themes.json file"; + mWindow->pushGui(new GuiMsgBox( + getHelpStyle(), + "COULDN'T PARSE THE THEMES LIST CONFIGURATION FILE, MAYBE THE LOCAL REPOSITORY IS " + "CORRUPT?", + "OK", [] { return; }, "", nullptr, "", nullptr, nullptr, true)); + mGrid.removeEntry(mCenterGrid); + mGrid.setCursorTo(mButtons); + return; + } + + if (doc.HasMember("latestStableRelease") && doc["latestStableRelease"].IsString()) { + const int latestStableRelease {std::stoi(doc["latestStableRelease"].GetString())}; + if (latestStableRelease > PROGRAM_RELEASE_NUMBER) { + LOG(LogWarning) << "Not running the most current application release, theme " + "downloading is not recommended"; + mWindow->pushGui(new GuiMsgBox( + getHelpStyle(), + "IT SEEMS AS IF YOU'RE NOT RUNNING THE LATEST ES-DE RELEASE, PLEASE UPGRADE BEFORE " + "PROCEEDING AS THESE THEMES MAY NOT BE COMPATIBLE WITH YOUR VERSION", + "OK", [] { return; }, "", nullptr, "", nullptr, nullptr, true)); + } + } + + if (doc.HasMember("themes") && doc["themes"].IsArray()) { + const rapidjson::Value& themes {doc["themes"]}; + for (int i {0}; i < static_cast(themes.Size()); ++i) { + ThemeEntry themeEntry; + const rapidjson::Value& theme {themes[i]}; + + if (theme.HasMember("name") && theme["name"].IsString()) + themeEntry.name = theme["name"].GetString(); + + if (theme.HasMember("reponame") && theme["reponame"].IsString()) + themeEntry.reponame = theme["reponame"].GetString(); + + if (theme.HasMember("url") && theme["url"].IsString()) + themeEntry.url = theme["url"].GetString(); + + if (theme.HasMember("author") && theme["author"].IsString()) + themeEntry.author = theme["author"].GetString(); + + if (theme.HasMember("newEntry") && theme["newEntry"].IsBool()) + themeEntry.newEntry = theme["newEntry"].GetBool(); + + if (theme.HasMember("variants") && theme["variants"].IsArray()) { + const rapidjson::Value& variants {theme["variants"]}; + for (int i {0}; i < static_cast(variants.Size()); ++i) + themeEntry.variants.emplace_back(variants[i].GetString()); + } + + if (theme.HasMember("colorSchemes") && theme["colorSchemes"].IsArray()) { + const rapidjson::Value& colorSchemes {theme["colorSchemes"]}; + for (int i {0}; i < static_cast(colorSchemes.Size()); ++i) + themeEntry.colorSchemes.emplace_back(colorSchemes[i].GetString()); + } + + if (theme.HasMember("aspectRatios") && theme["aspectRatios"].IsArray()) { + const rapidjson::Value& aspectRatios {theme["aspectRatios"]}; + for (int i {0}; i < static_cast(aspectRatios.Size()); ++i) + themeEntry.aspectRatios.emplace_back(aspectRatios[i].GetString()); + } + + if (theme.HasMember("transitions") && theme["transitions"].IsArray()) { + const rapidjson::Value& transitions {theme["transitions"]}; + for (int i {0}; i < static_cast(transitions.Size()); ++i) + themeEntry.transitions.emplace_back(transitions[i].GetString()); + } + + if (theme.HasMember("screenshots") && theme["screenshots"].IsArray()) { + const rapidjson::Value& screenshots {theme["screenshots"]}; + for (int i {0}; i < static_cast(screenshots.Size()); ++i) { + Screenshot screenshotEntry; + if (screenshots[i].HasMember("image") && screenshots[i]["image"].IsString()) + screenshotEntry.image = screenshots[i]["image"].GetString(); + + if (screenshots[i].HasMember("caption") && screenshots[i]["caption"].IsString()) + screenshotEntry.caption = screenshots[i]["caption"].GetString(); + + if (screenshotEntry.image != "" && screenshotEntry.caption != "") + themeEntry.screenshots.emplace_back(screenshotEntry); + } + } + + mThemes.emplace_back(themeEntry); + } + } + + LOG(LogDebug) << "GuiThemeDownloader::parseThemesList(): Parsed " << mThemes.size() + << " themes"; +} + +void GuiThemeDownloader::populateGUI() +{ + if (mThemes.empty()) + return; + + for (auto& theme : mThemes) { + std::string themeName {Utils::String::toUpper(theme.name)}; + if (theme.newEntry && !theme.isCloned) + themeName.append(" ").append(ViewController::BRANCH_CHAR); + if (theme.isCloned) + themeName.append(" ").append(ViewController::TICKMARK_CHAR); + if (theme.manuallyDownloaded || theme.invalidRepository || theme.corruptRepository || + theme.shallowRepository) + themeName.append(" ").append(ViewController::CROSSEDCIRCLE_CHAR); + if (theme.hasLocalChanges) + themeName.append(" ").append(ViewController::EXCLAMATION_CHAR); + + ComponentListRow row; + std::shared_ptr themeNameElement {std::make_shared( + themeName, Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary)}; + + ThemeGUIEntry guiEntry; + guiEntry.themeName = themeNameElement; + mThemeGUIEntries.emplace_back(guiEntry); + row.addElement(themeNameElement, false); + + row.makeAcceptInputHandler([this, &theme] { + std::promise().swap(mPromise); + if (theme.manuallyDownloaded || theme.invalidRepository) { + mWindow->pushGui(new GuiMsgBox( + getHelpStyle(), + "IT SEEMS AS IF THIS THEME HAS BEEN MANUALLY DOWNLOADED INSTEAD OF VIA " + "THIS THEME DOWNLOADER. A FRESH DOWNLOAD IS REQUIRED AND THE OLD THEME " + "DIRECTORY \"" + + theme.reponame + theme.manualExtension + "\" WILL BE RENAMED TO \"" + + theme.reponame + theme.manualExtension + "_DISABLED\"", + "PROCEED", + [this, theme] { + if (renameDirectory(mThemeDirectory + theme.reponame + + theme.manualExtension, + "_DISABLED")) { + return; + } + std::promise().swap(mPromise); + mFuture = mPromise.get_future(); + mFetchThread = std::thread(&GuiThemeDownloader::cloneRepository, this, + theme.reponame, theme.url); + mStatusType = StatusType::STATUS_DOWNLOADING; + mStatusText = "DOWNLOADING THEME"; + }, + "CANCEL", [] { return; }, "", nullptr, nullptr, false, true, + (mRenderer->getIsVerticalOrientation() ? + 0.75f : + 0.46f * (1.778f / mRenderer->getScreenAspectRatio())))); + } + else if (theme.corruptRepository) { + mWindow->pushGui(new GuiMsgBox( + getHelpStyle(), + "IT SEEMS AS IF THIS THEME REPOSITORY IS CORRUPT, WHICH COULD HAVE BEEN CAUSED " + "BY AN INTERRUPTION OF A PREVIOUS DOWNLOAD OR UPDATE, FOR EXAMPLE IF THE ES-DE " + "PROCESS WAS KILLED. A FRESH DOWNLOAD IS REQUIRED AND THE OLD THEME DIRECTORY " + "\"" + + theme.reponame + theme.manualExtension + "\" WILL BE RENAMED TO \"" + + theme.reponame + theme.manualExtension + "_CORRUPT_DISABLED\"", + "PROCEED", + [this, theme] { + if (renameDirectory(mThemeDirectory + theme.reponame + + theme.manualExtension, + "_CORRUPT_DISABLED")) { + return; + } + std::promise().swap(mPromise); + mFuture = mPromise.get_future(); + mFetchThread = std::thread(&GuiThemeDownloader::cloneRepository, this, + theme.reponame, theme.url); + mStatusType = StatusType::STATUS_DOWNLOADING; + mStatusText = "DOWNLOADING THEME"; + }, + "CANCEL", [] { return; }, "", nullptr, nullptr, false, true, + (mRenderer->getIsVerticalOrientation() ? + 0.75f : + 0.46f * (1.778f / mRenderer->getScreenAspectRatio())))); + } + else if (theme.shallowRepository) { + mWindow->pushGui(new GuiMsgBox( + getHelpStyle(), + "IT SEEMS AS IF THIS IS A SHALLOW REPOSITORY WHICH MEANS THAT IT HAS BEEN " + "DOWNLOADED USING SOME OTHER TOOL THAN THIS THEME DOWNLOADER. A FRESH DOWNLOAD " + "IS REQUIRED AND THE OLD THEME DIRECTORY \"" + + theme.reponame + theme.manualExtension + "\" WILL BE RENAMED TO \"" + + theme.reponame + theme.manualExtension + "_DISABLED\"", + "PROCEED", + [this, theme] { + if (renameDirectory(mThemeDirectory + theme.reponame + + theme.manualExtension, + "_DISABLED")) { + return; + } + std::promise().swap(mPromise); + mFuture = mPromise.get_future(); + mFetchThread = std::thread(&GuiThemeDownloader::cloneRepository, this, + theme.reponame, theme.url); + mStatusType = StatusType::STATUS_DOWNLOADING; + mStatusText = "DOWNLOADING THEME"; + }, + "CANCEL", [] { return; }, "", nullptr, nullptr, false, true, + (mRenderer->getIsVerticalOrientation() ? + 0.75f : + 0.46f * (1.778f / mRenderer->getScreenAspectRatio())))); + } + else if (theme.hasLocalChanges) { + mWindow->pushGui(new GuiMsgBox( + getHelpStyle(), + "THEME REPOSITORY \"" + theme.reponame + + "\" CONTAINS LOCAL CHANGES. PROCEED TO OVERWRITE YOUR CHANGES " + "OR CANCEL TO SKIP ALL UPDATES FOR THIS THEME", + "PROCEED", + [this, theme] { + std::promise().swap(mPromise); + mFuture = mPromise.get_future(); + mFetchThread = std::thread(&GuiThemeDownloader::fetchRepository, this, + theme.reponame, true); + mStatusType = StatusType::STATUS_UPDATING; + mStatusText = "UPDATING THEME"; + }, + "CANCEL", [] { return; }, "", nullptr, nullptr, false, true, + (mRenderer->getIsVerticalOrientation() ? + 0.75f : + 0.45f * (1.778f / mRenderer->getScreenAspectRatio())))); + } + else if (theme.isCloned) { + mFuture = mPromise.get_future(); + mFetchThread = + std::thread(&GuiThemeDownloader::fetchRepository, this, theme.reponame, false); + mStatusType = StatusType::STATUS_UPDATING; + mStatusText = "UPDATING THEME"; + } + else { + mFuture = mPromise.get_future(); + mFetchThread = std::thread(&GuiThemeDownloader::cloneRepository, this, + theme.reponame, theme.url); + mStatusType = StatusType::STATUS_DOWNLOADING; + mStatusText = "DOWNLOADING THEME"; + } + mWindow->stopInfoPopup(); + }); + mList->addRow(row); + } + + mVariantsLabel->setText("VARIANTS:"); + mColorSchemesLabel->setText("COLOR SCHEMES:"); + mAspectRatiosLabel->setText("ASPECT RATIOS:"); + + updateInfoPane(); + updateHelpPrompts(); +} + +void GuiThemeDownloader::updateGUI() +{ + updateInfoPane(); + updateHelpPrompts(); + + for (size_t i {0}; i < mThemes.size(); ++i) { + std::string themeName {Utils::String::toUpper(mThemes[i].name)}; + if (mThemes[i].newEntry && !mThemes[i].isCloned) + themeName.append(" ").append(ViewController::BRANCH_CHAR); + if (mThemes[i].isCloned) + themeName.append(" ").append(ViewController::TICKMARK_CHAR); + if (mThemes[i].manuallyDownloaded || mThemes[i].invalidRepository || + mThemes[i].corruptRepository || mThemes[i].shallowRepository) + themeName.append(" ").append(ViewController::CROSSEDCIRCLE_CHAR); + if (mThemes[i].hasLocalChanges) + themeName.append(" ").append(ViewController::EXCLAMATION_CHAR); + + mThemeGUIEntries[i].themeName->setText(themeName); + } +} + +void GuiThemeDownloader::updateInfoPane() +{ + assert(static_cast(mList->size()) == mThemes.size()); + if (!mThemes[mList->getCursorId()].screenshots.empty()) + mScreenshot->setImage(mThemeDirectory + "themes-list/" + + mThemes[mList->getCursorId()].screenshots.front().image); + else + mScreenshot->setImage(""); + + if (mThemes[mList->getCursorId()].isCloned) { + mDownloadStatus->setText(ViewController::TICKMARK_CHAR + " INSTALLED"); + mDownloadStatus->setColor(mMenuColorGreen); + mDownloadStatus->setOpacity(1.0f); + } + else if (mThemes[mList->getCursorId()].invalidRepository || + mThemes[mList->getCursorId()].manuallyDownloaded) { + mDownloadStatus->setText(ViewController::CROSSEDCIRCLE_CHAR + " MANUAL DOWNLOAD"); + mDownloadStatus->setColor(mMenuColorRed); + mDownloadStatus->setOpacity(1.0f); + } + else if (mThemes[mList->getCursorId()].corruptRepository) { + mDownloadStatus->setText(ViewController::CROSSEDCIRCLE_CHAR + " CORRUPT"); + mDownloadStatus->setColor(mMenuColorRed); + mDownloadStatus->setOpacity(1.0f); + } + else if (mThemes[mList->getCursorId()].shallowRepository) { + mDownloadStatus->setText(ViewController::CROSSEDCIRCLE_CHAR + " SHALLOW"); + mDownloadStatus->setColor(mMenuColorRed); + mDownloadStatus->setOpacity(1.0f); + } + else { + if (mThemes[mList->getCursorId()].newEntry) + mDownloadStatus->setText("NOT INSTALLED (NEW)"); + else + mDownloadStatus->setText("NOT INSTALLED"); + mDownloadStatus->setColor(mMenuColorPrimary); + mDownloadStatus->setOpacity(0.7f); + } + if (mThemes[mList->getCursorId()].hasLocalChanges) { + mLocalChanges->setText(ViewController::EXCLAMATION_CHAR + " LOCAL CHANGES"); + mLocalChanges->setColor(mMenuColorRed); + } + else { + mLocalChanges->setText(""); + } + + mVariantCount->setText(std::to_string(mThemes[mList->getCursorId()].variants.size())); + mColorSchemesCount->setText(std::to_string(mThemes[mList->getCursorId()].colorSchemes.size())); + mAspectRatiosCount->setText(std::to_string(mThemes[mList->getCursorId()].aspectRatios.size())); + mAuthor->setText("CREATED BY " + Utils::String::toUpper(mThemes[mList->getCursorId()].author)); +} + +void GuiThemeDownloader::setupFullscreenViewer() +{ + if (mThemes.empty()) + return; + + mViewerScreenshots.clear(); + mViewerCaptions.clear(); + mFullscreenViewerIndex = 0; + mFullscreenViewing = true; + + for (auto& screenshot : mThemes[mList->getCursorId()].screenshots) { + auto image = std::make_shared(false, false); + image->setLinearInterpolation(true); + image->setMaxSize(mRenderer->getScreenWidth() * 0.86f, + mRenderer->getScreenHeight() * 0.86f); + if (!Utils::FileSystem::exists(mThemeDirectory + "themes-list/" + screenshot.image)) + continue; + image->setImage(mThemeDirectory + "themes-list/" + screenshot.image); + // Center image on screen. + glm::vec3 imagePos {image->getPosition()}; + imagePos.x = (mRenderer->getScreenWidth() - image->getSize().x) / 2.0f; + imagePos.y = (mRenderer->getScreenHeight() - image->getSize().y) / 2.0f; + image->setPosition(imagePos); + mViewerScreenshots.emplace_back(image); + auto caption = std::make_shared(screenshot.caption, + Font::get(FONT_SIZE_MINI, FONT_PATH_REGULAR), + 0xCCCCCCFF, ALIGN_LEFT); + glm::vec3 textPos {image->getPosition()}; + textPos.y += image->getSize().y; + caption->setPosition(textPos); + mViewerCaptions.emplace_back(caption); + } + + if (mViewerScreenshots.size() > 0) { + // Navigation indicators to the left and right of the screenshot. + glm::vec3 indicatorPos {mViewerScreenshots.front()->getPosition()}; + indicatorPos.x -= mViewerIndicatorLeft->getSize().x * 2.0f; + indicatorPos.y += (mViewerScreenshots.front()->getSize().y / 2.0f) - + (mViewerIndicatorLeft->getSize().y / 2.0f); + mViewerIndicatorLeft->setPosition(indicatorPos); + indicatorPos.x += + mViewerScreenshots.front()->getSize().x + (mViewerIndicatorRight->getSize().x * 3.0f); + mViewerIndicatorRight->setPosition(indicatorPos); + } + else { + mFullscreenViewing = false; + } +} + +void GuiThemeDownloader::update(int deltaTime) +{ + if (!mAttemptedFetch) { + // We need to run this here instead of from the constructor so that GuiMsgBox will be + // on top of the GUI stack if it needs to be displayed. + mAttemptedFetch = true; + fetchThemesList(); + } + + if (mFuture.valid()) { + // Only wait one millisecond as this update() function runs very frequently. + if (mFuture.wait_for(std::chrono::milliseconds(1)) == std::future_status::ready) { + if (mFetchThread.joinable()) { + mFetchThread.join(); + mFetching = false; + if (mRepositoryError != RepositoryError::NO_REPO_ERROR) { + std::string errorMessage {"ERROR: "}; + if (mThemes.empty()) { + errorMessage.append("COULDN'T DOWNLOAD THEMES LIST, "); + mGrid.removeEntry(mCenterGrid); + mGrid.setCursorTo(mButtons); + } + errorMessage.append(Utils::String::toUpper(mMessage)); + mWindow->pushGui(new GuiMsgBox( + getHelpStyle(), errorMessage, "OK", [] { return; }, "", nullptr, "", + nullptr, nullptr, true)); + mMessage = ""; + getHelpPrompts(); + } + if (mThemes.empty() && mLatestThemesList) { + parseThemesList(); + makeInventory(); + populateGUI(); + } + else if (!mThemes.empty()) { + makeInventory(); + updateGUI(); + } + } + } + } + + if (mFetching) { + int progress {mReceivedObjectsProgress != 1.0f ? 0 : 100}; + if (mStatusType != StatusType::STATUS_NO_CHANGE) { + if (mStatusType == StatusType::STATUS_DOWNLOADING) + mBusyAnim.setText(mStatusText + " 100%"); + else if (mStatusType == StatusType::STATUS_UPDATING) + mBusyAnim.setText(mStatusText); + mBusyAnim.onSizeChanged(); + mStatusType = StatusType::STATUS_NO_CHANGE; + } + if (mReceivedObjectsProgress != 1.0f) { + progress = static_cast( + std::round(glm::mix(0.0f, 100.0f, static_cast(mReceivedObjectsProgress)))); + if (mStatusText.substr(0, 11) == "DOWNLOADING") + mBusyAnim.setText(mStatusText + " " + std::to_string(progress) + "%"); + else + mBusyAnim.setText(mStatusText); + } + else if (mReceivedObjectsProgress != 0.0f) { + progress = static_cast( + std::round(glm::mix(0.0f, 100.0f, static_cast(mResolveDeltaProgress)))); + if (mStatusText.substr(0, 11) == "DOWNLOADING") + mBusyAnim.setText(mStatusText + " " + std::to_string(progress) + "%"); + else + mBusyAnim.setText(mStatusText); + } + mBusyAnim.update(deltaTime); + } + + if (mRepositoryError == RepositoryError::NO_REPO_ERROR && mMessage != "") { + mWindow->queueInfoPopup(mMessage, 6000); + mMessage = ""; + } + + GuiComponent::update(deltaTime); +} + +void GuiThemeDownloader::render(const glm::mat4& parentTrans) +{ + glm::mat4 trans {parentTrans * getTransform()}; + renderChildren(trans); + + if (mGrayRectangleCoords.size() == 4) { + mRenderer->setMatrix(parentTrans * getTransform()); + mRenderer->drawRect(mGrayRectangleCoords[0], mGrayRectangleCoords[1], + mGrayRectangleCoords[2], mGrayRectangleCoords[3], mMenuColorPanelDimmed, + mMenuColorPanelDimmed); + } + + if (mFetching) + mBusyAnim.render(trans); + + if (mFullscreenViewing && mViewerScreenshots.size() > 0) { + mRenderer->setMatrix(parentTrans); + mRenderer->drawRect(0.0f, 0.0f, mRenderer->getScreenWidth(), mRenderer->getScreenHeight(), + 0x222222FF, 0x222222FF); + mViewerScreenshots[mFullscreenViewerIndex]->render(parentTrans); + mViewerCaptions[mFullscreenViewerIndex]->render(parentTrans); + if (mFullscreenViewerIndex != 0) + mViewerIndicatorLeft->render(parentTrans); + if (mFullscreenViewerIndex != mViewerCaptions.size() - 1) + mViewerIndicatorRight->render(parentTrans); + } +} + +void GuiThemeDownloader::onSizeChanged() +{ + const float screenSize {mRenderer->getIsVerticalOrientation() ? mRenderer->getScreenWidth() : + mRenderer->getScreenHeight()}; + mGrid.setRowHeightPerc(0, (mTitle->getFont()->getLetterHeight() + screenSize * 0.2f) / mSize.y / + 4.0f); + mGrid.setRowHeightPerc(1, (mTitle->getFont()->getLetterHeight() + screenSize * 0.2f) / mSize.y / + 4.0f); + mGrid.setRowHeightPerc(2, (mList->getRowHeight() * 9.0f) / mSize.y); + + mCenterGrid->setRowHeightPerc( + 0, (mVariantsLabel->getFont()->getLetterHeight() + screenSize * 0.115f) / mSize.y / 2.0f); + mCenterGrid->setRowHeightPerc( + 1, + (mColorSchemesLabel->getFont()->getLetterHeight() + screenSize * 0.09f) / mSize.y / 2.0f); + mCenterGrid->setRowHeightPerc( + 2, (mDownloadStatus->getFont()->getLetterHeight() + screenSize * 0.115f) / mSize.y / 2.0f); + mCenterGrid->setRowHeightPerc(3, 0.7f); + + mGrid.setColWidthPerc(1, 0.04f); + mCenterGrid->setColWidthPerc(0, 0.01f); + mCenterGrid->setColWidthPerc(1, (mRenderer->getScreenAspectRatio() < 1.6f ? 0.21f : 0.18f)); + mCenterGrid->setColWidthPerc(2, 0.05f); + mCenterGrid->setColWidthPerc(3, 0.18f); + mCenterGrid->setColWidthPerc(4, 0.04f); + mCenterGrid->setColWidthPerc(5, 0.005f); + mCenterGrid->setColWidthPerc(7, 0.04f); + + mGrid.setSize(mSize); + + mCenterGrid->setSize( + glm::vec2 {std::round(mSize.x), (mList->getRowHeight() * 9.0f) + + std::round(mRenderer->getScreenHeightModifier())}); + mCenterGrid->setPosition(glm::vec3 {0.0f, mGrid.getRowHeight(0) + mGrid.getRowHeight(1), 0.0f}); + mBackground.fitTo(mSize); + mScreenshot->setMaxSize(mCenterGrid->getColWidth(1) + mCenterGrid->getColWidth(2) + + mCenterGrid->getColWidth(3) + mCenterGrid->getColWidth(4), + mCenterGrid->getRowHeight(3)); + + mGrayRectangleCoords.clear(); + mGrayRectangleCoords.emplace_back(0.0f); + mGrayRectangleCoords.emplace_back(mCenterGrid->getPosition().y); + mGrayRectangleCoords.emplace_back(mSize.x); + mGrayRectangleCoords.emplace_back(mList->getRowHeight() * 9.0f); +} + +bool GuiThemeDownloader::input(InputConfig* config, Input input) +{ + if (mFetching && input.value) + return false; + + if (mFullscreenViewing && input.value) { + if (config->isMappedLike("left", input)) { + if (mFullscreenViewerIndex > 0) + --mFullscreenViewerIndex; + return true; + } + else if (config->isMappedLike("right", input)) { + if (mViewerScreenshots.size() > mFullscreenViewerIndex + 1) + ++mFullscreenViewerIndex; + return true; + } + else if (config->isMappedLike("lefttrigger", input)) { + mFullscreenViewerIndex = 0; + return true; + } + else if (config->isMappedLike("righttrigger", input)) { + mFullscreenViewerIndex = mViewerScreenshots.size() - 1; + return true; + } + else { + mViewerScreenshots.clear(); + mViewerCaptions.clear(); + mFullscreenViewing = false; + mFullscreenViewerIndex = 0; + return true; + } + } + + if (config->isMappedTo("b", input) && input.value) { + delete this; + return true; + } + + if (config->isMappedTo("x", input) && input.value && + mGrid.getSelectedComponent() == mCenterGrid) { + setupFullscreenViewer(); + return true; + } + + if (config->isMappedTo("y", input) && input.value && + mGrid.getSelectedComponent() == mCenterGrid && mThemes[mList->getCursorId()].isCloned) { + mWindow->pushGui(new GuiMsgBox( + getHelpStyle(), + "THIS WILL COMPLETELY DELETE THE THEME INCLUDING ANY " + "LOCAL CUSTOMIZATIONS", + "PROCEED", + [this] { + const std::filesystem::path themeDirectory {mThemeDirectory + + mThemes[mList->getCursorId()].reponame}; + LOG(LogInfo) << "Deleting theme directory \"" << themeDirectory.string() << "\""; + if (!Utils::FileSystem::removeDirectory(themeDirectory.string(), true)) { + mWindow->pushGui(new GuiMsgBox( + getHelpStyle(), "COULDN'T DELETE THEME, PERMISSION PROBLEMS?", "OK", + [] { return; }, "", nullptr, "", nullptr, nullptr, true)); + } + else { + mMessage = "THEME WAS DELETED"; + } + mHasThemeUpdates = true; + makeInventory(); + updateGUI(); + }, + "CANCEL", nullptr, "", nullptr, nullptr, false, true, + (mRenderer->getIsVerticalOrientation() ? + 0.70f : + 0.44f * (1.778f / mRenderer->getScreenAspectRatio())))); + return true; + } + + return GuiComponent::input(config, input); +} + +std::vector GuiThemeDownloader::getHelpPrompts() +{ + std::vector prompts; + + if (mList->size() > 0) { + prompts = mGrid.getHelpPrompts(); + prompts.push_back(HelpPrompt("b", "close")); + + if (mGrid.getSelectedComponent() == mCenterGrid) + prompts.push_back(HelpPrompt("x", "view screenshots")); + + if (mThemes[mList->getCursorId()].isCloned) { + prompts.push_back(HelpPrompt("a", "fetch updates")); + if (mGrid.getSelectedComponent() == mCenterGrid) + prompts.push_back(HelpPrompt("y", "delete")); + } + else { + prompts.push_back(HelpPrompt("a", "download")); + } + } + else { + prompts.push_back(HelpPrompt("b", "close")); + } + + return prompts; +} + +bool GuiThemeDownloader::fetchThemesList() +{ + const std::string repositoryName {"themes-list"}; + const std::string url {"https://gitlab.com/es-de/themes/themes-list.git"}; + const std::string path {mThemeDirectory + "themes-list"}; + + if (Utils::FileSystem::exists(path)) { + git_repository* repository {nullptr}; + int errorCode {git_repository_open(&repository, &path[0])}; + + if (errorCode != 0 || checkCorruptRepository(repository)) { + mWindow->pushGui(new GuiMsgBox( + getHelpStyle(), + "IT SEEMS AS IF THE THEMES LIST REPOSITORY IS CORRUPT, WHICH COULD HAVE BEEN " + "CAUSED BY AN INTERRUPTION OF A PREVIOUS DOWNLOAD OR UPDATE, FOR EXAMPLE IF THE " + "ES-DE PROCESS WAS KILLED. A FRESH DOWNLOAD IS REQUIRED AND THE OLD DIRECTORY " + "\"themes-list\" WILL BE RENAMED TO \"themes-list_CORRUPT_DISABLED\"", + "PROCEED", + [this, repositoryName, url] { + if (renameDirectory(mThemeDirectory + "themes-list", "_CORRUPT_DISABLED")) { + mGrid.removeEntry(mCenterGrid); + mGrid.setCursorTo(mButtons); + return true; + } + LOG(LogInfo) + << "GuiThemeDownloader: Creating initial themes list repository clone"; + mFetchThread = std::thread(&GuiThemeDownloader::cloneRepository, this, + repositoryName, url); + mStatusType = StatusType::STATUS_DOWNLOADING; + mStatusText = "DOWNLOADING THEMES LIST"; + return false; + }, + "CANCEL", + [&] { + delete this; + return false; + }, + "", nullptr, nullptr, true, true, + (mRenderer->getIsVerticalOrientation() ? + 0.75f : + 0.50f * (1.778f / mRenderer->getScreenAspectRatio())))); + } + else { + // We always hard reset the themes list as it should never contain any local changes. + resetRepository(repository); + + mFetchThread = + std::thread(&GuiThemeDownloader::fetchRepository, this, repositoryName, false); + mStatusType = StatusType::STATUS_UPDATING; + mStatusText = "UPDATING THEMES LIST"; + } + git_repository_free(repository); + } + else { + mWindow->pushGui(new GuiMsgBox( + getHelpStyle(), + "IT SEEMS AS IF YOU'RE USING THE THEME DOWNLOADER FOR THE FIRST TIME. " + "AS SUCH THE THEMES LIST REPOSITORY WILL BE DOWNLOADED WHICH WILL TAKE A LITTLE " + "WHILE. SUBSEQUENT RUNS WILL HOWEVER BE MUCH FASTER AS ONLY NEW OR MODIFIED FILES " + "WILL BE FETCHED. THE SAME IS TRUE FOR ANY THEMES YOU DOWNLOAD. NOTE THAT YOU CAN'T " + "ABORT AN ONGOING DOWNLOAD AS THAT COULD LEAD TO DATA CORRUPTION.", + "PROCEED", + [this, repositoryName, url] { + LOG(LogInfo) << "GuiThemeDownloader: Creating initial themes list repository clone"; + mFetchThread = + std::thread(&GuiThemeDownloader::cloneRepository, this, repositoryName, url); + mStatusType = StatusType::STATUS_DOWNLOADING; + mStatusText = "DOWNLOADING THEMES LIST"; + return false; + }, + "CANCEL", + [&] { + delete this; + return false; + }, + "", nullptr, nullptr, true, true, + (mRenderer->getIsVerticalOrientation() ? + 0.85f : + 0.54f * (1.778f / mRenderer->getScreenAspectRatio())))); + } + + return false; +} + +bool GuiThemeDownloader::cloneRepository(const std::string& repositoryName, const std::string& url) +{ + int errorCode {0}; + git_repository* repository {nullptr}; + const std::string path {mThemeDirectory + repositoryName}; + +#if LIBGIT2_VER_MAJOR >= 1 + auto fetchProgressFunc = [](const git_indexer_progress* stats, void* payload) -> int { +#else + auto fetchProgressFunc = [](const git_transfer_progress* stats, void* payload) -> int { +#endif + (void)payload; + if (stats->received_objects == stats->total_objects) { +#if (DEBUG_CLONING) + LOG(LogDebug) << "Indexed deltas: " << stats->indexed_deltas + << " Total deltas: " << stats->total_deltas; +#endif + mReceivedObjectsProgress = 1.0f; + if (stats->total_deltas > 0) { + mResolveDeltaProgress = static_cast(stats->indexed_deltas) / + static_cast(stats->total_deltas); + } + } + else if (stats->total_objects > 0) { +#if (DEBUG_CLONING) + LOG(LogDebug) << "Received objects: " << stats->received_objects + << " Total objects: " << stats->total_objects + << " Indexed objects: " << stats->indexed_objects + << " Received bytes: " << stats->received_bytes; +#endif + if (stats->total_objects > 0) { + mReceivedObjectsProgress = static_cast(stats->received_objects) / + static_cast(stats->total_objects); + } + } + return 0; + }; + +#if LIBGIT2_VER_MAJOR >= 1 + git_clone_options cloneOptions; + git_clone_options_init(&cloneOptions, GIT_CLONE_OPTIONS_VERSION); +#else + git_clone_options cloneOptions = GIT_CLONE_OPTIONS_INIT; +#endif + + cloneOptions.checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE; + cloneOptions.fetch_opts.callbacks.transfer_progress = fetchProgressFunc; + + mReceivedObjectsProgress = 0.0f; + mResolveDeltaProgress = 0.0f; + + mFetching = true; + errorCode = git_clone(&repository, &url[0], &path[0], &cloneOptions); + git_repository_free(repository); + + if (errorCode != 0) { + const git_error* gitError {git_error_last()}; + LOG(LogError) << "GuiThemeDownloader: Couldn't clone repository \"" << repositoryName + << "\", error code: " << errorCode << ", error message: \"" + << gitError->message << "\""; + mRepositoryError = RepositoryError::CLONE_ERROR; + mMessage = gitError->message; + git_error_clear(); + mPromise.set_value(true); + return true; + } + + if (repositoryName != "themes-list") { + LOG(LogInfo) << "GuiThemeDownloader: Downloaded theme \"" << repositoryName << "\""; + mHasThemeUpdates = true; + } + + mLatestThemesList = true; + mPromise.set_value(true); + return false; +} diff --git a/rd-submodules/es-de/patches-tmp/ViewController.cpp b/rd-submodules/es-de/patches-tmp/ViewController.cpp new file mode 100644 index 00000000..68458d7d --- /dev/null +++ b/rd-submodules/es-de/patches-tmp/ViewController.cpp @@ -0,0 +1,1390 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// ViewController.cpp +// +// Handles overall system navigation including animations and transitions. +// Creates the gamelist views and handles refresh and reloads of these when needed +// (for example when metadata has been changed or when a list sorting has taken place). +// Initiates the launching of games, calling FileData to do the actual launch. +// Displays a dialog when there are no games found on startup. +// + +#include "views/ViewController.h" + +#include "ApplicationUpdater.h" +#include "CollectionSystemsManager.h" +#include "FileFilterIndex.h" +#include "InputManager.h" +#include "Log.h" +#include "Scripting.h" +#include "Settings.h" +#include "Sound.h" +#include "SystemData.h" +#include "SystemView.h" +#include "UIModeController.h" +#include "Window.h" +#include "animations/Animation.h" +#include "animations/LambdaAnimation.h" +#include "animations/MoveCameraAnimation.h" +#include "guis/GuiApplicationUpdater.h" +#include "guis/GuiMenu.h" +#include "guis/GuiTextEditKeyboardPopup.h" +#include "guis/GuiTextEditPopup.h" +#include "views/GamelistView.h" +#include "views/SystemView.h" + +ViewController::ViewController() noexcept + : mRenderer {Renderer::getInstance()} + , mNoGamesMessageBox {nullptr} + , mCurrentView {nullptr} + , mPreviousView {nullptr} + , mSkipView {nullptr} + , mLastTransitionAnim {ViewTransitionAnimation::INSTANT} + , mGameToLaunch {nullptr} + , mCamera {Renderer::getIdentity()} + , mSystemViewTransition {false} + , mWrappedViews {false} + , mFadeOpacity {0} + , mCancelledTransition {false} + , mNextSystem {false} +{ + mState.viewing = ViewMode::NOTHING; + mState.previouslyViewed = ViewMode::NOTHING; +} + +ViewController* ViewController::getInstance() +{ + static ViewController instance; + return &instance; +} + +void ViewController::setMenuColors() +{ + if (Settings::getInstance()->getString("MenuColorScheme") == "light") { + mMenuColorFrame = 0xEFEFEFFF; + mMenuColorFrameLaunchScreen = 0xDFDFDFFF; + mMenuColorFrameBusyComponent = 0xFFFFFFFF; + mMenuColorPanelDimmed = 0x00000009; + + mMenuColorTitle = 0x555555FF; + mMenuColorPrimary = 0x777777FF; + mMenuColorSecondary = 0x888888FF; + mMenuColorTertiary = 0x666666FF; + mMenuColorRed = 0x992222FF; + mMenuColorGreen = 0x449944FF; + mMenuColorBlue = 0x222299FF; + + mMenuColorSelector = 0xFFFFFFFF; + mMenuColorSeparators = 0xC6C7C6FF; + mMenuColorBusyComponent = 0xB8B8B8FF; + mMenuColorScrollIndicators = 0x888888FF; + mMenuColorPopupText = 0x444444FF; + + mMenuColorButtonFocused = 0x777777FF; + mMenuColorButtonTextFocused = 0xFFFFFFFF; + mMenuColorButtonTextUnfocused = 0x777777FF; + mMenuColorButtonFlatFocused = 0x878787FF; + mMenuColorButtonFlatUnfocused = 0xDADADAFF; + + mMenuColorKeyboardModifier = 0xF26767FF; + mMenuColorKeyboardCursorFocused = 0x777777FF; + mMenuColorKeyboardCursorUnfocused = 0xC7C7C7FF; + mMenuColorKeyboardText = 0x77777700; + mMenuColorTextInputFrameFocused = 0xFFFFFFFF; + mMenuColorTextInputFrameUnfocused = 0xFFFFFFFF; + + mMenuColorSliderKnobDisabled = 0xC9C9C9FF; + mMenuColorDateTimeEditMarker = 0x00000022; + mMenuColorDetectDeviceHeld = 0x44444400; + } + else { + mMenuColorFrame = 0x191919FF; + mMenuColorFrameLaunchScreen = 0x121212FF; + mMenuColorFrameBusyComponent = 0x090909FF; + mMenuColorPanelDimmed = 0x00000024; + + mMenuColorTitle = 0x909090FF; + mMenuColorPrimary = 0x808080FF; + mMenuColorSecondary = 0x939393FF; + mMenuColorTertiary = 0x909090FF; + mMenuColorRed = 0xCA3E3EFF; + mMenuColorGreen = 0x449944FF; + mMenuColorBlue = 0x4757ddff; + + mMenuColorSelector = 0x000000FF; + mMenuColorSeparators = 0x303030FF; + mMenuColorBusyComponent = 0x888888FF; + mMenuColorScrollIndicators = 0x707070FF; + mMenuColorPopupText = 0xBBBBBBFF; + + mMenuColorButtonFocused = 0x050505FF; + mMenuColorButtonTextFocused = 0xAFAFAFFF; + mMenuColorButtonTextUnfocused = 0x808080FF; + mMenuColorButtonFlatFocused = 0x090909FF; + mMenuColorButtonFlatUnfocused = 0x242424FF; + + mMenuColorKeyboardModifier = 0xC62F2FFF; + mMenuColorKeyboardCursorFocused = 0xAAAAAAFF; + mMenuColorKeyboardCursorUnfocused = 0x666666FF; + mMenuColorKeyboardText = 0x92929200; + mMenuColorTextInputFrameFocused = 0x090909FF; + mMenuColorTextInputFrameUnfocused = 0x242424FF; + + mMenuColorSliderKnobDisabled = 0x393939FF; + mMenuColorDateTimeEditMarker = 0xFFFFFF22; + mMenuColorDetectDeviceHeld = 0x99999900; + } +} + +void ViewController::unsafeUpgradeDialog() +{ + const std::string upgradeMessage { + "IT SEEMS AS IF AN UNSAFE UPGRADE HAS BEEN MADE, POSSIBLY BY " + "UNPACKING THE NEW RELEASE ON TOP OF THE OLD ONE? THIS MAY CAUSE " + "VARIOUS PROBLEMS, SOME OF WHICH MAY NOT BE APPARENT IMMEDIATELY. " + "MAKE SURE TO ALWAYS FOLLOW THE UPGRADE INSTRUCTIONS IN THE " + "README.TXT FILE THAT CAN BE FOUND IN THE EMULATIONSTATION-DE " + "DIRECTORY."}; + mWindow->pushGui(new GuiMsgBox( + HelpStyle(), upgradeMessage.c_str(), "OK", [] {}, "", nullptr, "", nullptr, nullptr, true, + true, + (mRenderer->getIsVerticalOrientation() ? + 0.85f : + 0.55f * (1.778f / mRenderer->getScreenAspectRatio())))); +} + +void ViewController::invalidSystemsFileDialog() +{ + const std::string errorMessage {"COULDN'T PARSE THE SYSTEMS CONFIGURATION FILE. " + "IF YOU HAVE A CUSTOMIZED es_systems.xml FILE, THEN " + "SOMETHING IS LIKELY WRONG WITH YOUR XML SYNTAX. " + "IF YOU DON'T HAVE A CUSTOM SYSTEMS FILE, THEN THE " + "EMULATIONSTATION INSTALLATION IS BROKEN. SEE THE " + "APPLICATION LOG FILE es_log.txt FOR ADDITIONAL INFO"}; + + mWindow->pushGui(new GuiMsgBox( + HelpStyle(), errorMessage.c_str(), "QUIT", + [] { + SDL_Event quit; + quit.type = SDL_QUIT; + SDL_PushEvent(&quit); + }, + "", nullptr, "", nullptr, nullptr, true, true, + (mRenderer->getIsVerticalOrientation() ? + 0.85f : + 0.55f * (1.778f / mRenderer->getScreenAspectRatio())))); +} + +void ViewController::noGamesDialog() +{ + mNoGamesErrorMessage = "NO GAME FILES WERE FOUND. EITHER PLACE YOUR GAMES IN " + "THE CURRENTLY CONFIGURED ROM DIRECTORY OR CHANGE " + "ITS PATH USING THE BUTTON BELOW. OPTIONALLY THE ROM " + "DIRECTORY STRUCTURE CAN BE GENERATED WHICH WILL " + "CREATE A TEXT FILE FOR EACH SYSTEM PROVIDING SOME " + "INFORMATION SUCH AS THE SUPPORTED FILE EXTENSIONS.\n" + "THIS IS THE CURRENTLY CONFIGURED ROM DIRECTORY:\n"; + +#if defined(_WIN64) + mRomDirectory = Utils::String::replace(FileData::getROMDirectory(), "/", "\\"); +#else + mRomDirectory = FileData::getROMDirectory(); +#endif + + mNoGamesMessageBox = new GuiMsgBox( + HelpStyle(), mNoGamesErrorMessage + mRomDirectory, "QUIT", + [] { + SDL_Event quit; + quit.type = SDL_QUIT; + SDL_PushEvent(&quit); + }, + "", nullptr, "", nullptr, nullptr, true, true, + (mRenderer->getIsVerticalOrientation() ? + 0.90f : + 0.62f * (1.778f / mRenderer->getScreenAspectRatio()))); + + mWindow->pushGui(mNoGamesMessageBox); +} + +void ViewController::invalidAlternativeEmulatorDialog() +{ + cancelViewTransitions(); + mWindow->pushGui(new GuiMsgBox(getHelpStyle(), + "AT LEAST ONE OF YOUR SYSTEMS HAS AN\n" + "INVALID ALTERNATIVE EMULATOR CONFIGURED\n" + "WITH NO MATCHING ENTRY IN THE SYSTEMS\n" + "CONFIGURATION FILE, PLEASE REVIEW YOUR\n" + "SETUP USING THE 'ALTERNATIVE EMULATORS'\n" + "INTERFACE IN THE 'OTHER SETTINGS' MENU", + "OK", nullptr, "", nullptr, "", nullptr, nullptr, true, true)); +} + +void ViewController::updateAvailableDialog() +{ + cancelViewTransitions(); + + std::string results {ApplicationUpdater::getInstance().getResultsString()}; + ApplicationUpdater::Package package {ApplicationUpdater::getInstance().getPackageInfo()}; + + if (package.name != "") { + LOG(LogDebug) << "ViewController::updateAvailableDialog(): Package filename \"" + << package.filename << "\""; + LOG(LogDebug) << "ViewController::updateAvailableDialog(): Package url \"" << package.url + << "\""; + LOG(LogDebug) << "ViewController::updateAvailableDialog(): Package md5 \"" << package.md5 + << "\""; + + mWindow->pushGui(new GuiMsgBox( + getHelpStyle(), results, "UPDATE", + [this, package] { + mWindow->pushGui(new GuiApplicationUpdater()); + + if (package.name != "LinuxAppImage" && package.name != "LinuxSteamDeckAppImage") { + std::string upgradeMessage; + if (package.name == "WindowsPortable") { + upgradeMessage = + "THE APPLICATION UPDATER WILL DOWNLOAD THE LATEST PORTABLE WINDOWS " + "RELEASE FOR YOU, BUT YOU WILL NEED TO MANUALLY PERFORM THE UPGRADE. " + "SEE THE README.TXT FILE INSIDE THE DOWNLOADED ZIP FILE FOR " + "INSTRUCTIONS ON HOW THIS IS ACCOMPLISHED. AS IS ALSO DESCRIBED IN " + "THAT DOCUMENT, NEVER UNPACK A NEW RELEASE ON TOP OF AN OLD " + "INSTALLATION AS THAT MAY COMPLETELY BREAK THE APPLICATION."; + } + else if (package.name == "WindowsInstaller") { + upgradeMessage = + "THE APPLICATION UPDATER WILL DOWNLOAD THE LATEST WINDOWS INSTALLER " + "RELEASE FOR YOU, BUT YOU WILL NEED TO MANUALLY RUN IT TO PERFORM " + "THE UPGRADE. WHEN DOING THIS, MAKE SURE THAT YOU ANSWER YES TO THE " + "QUESTION OF WHETHER TO UNINSTALL THE OLD VERSION, OR YOU MAY " + "END UP WITH A BROKEN SETUP."; + } + else if (package.name == "macOSApple" || package.name == "macOSIntel") { + upgradeMessage = + "THE APPLICATION UPDATER WILL DOWNLOAD THE LATEST RELEASE FOR " + "YOU, BUT YOU WILL NEED TO MANUALLY INSTALL THE DMG FILE TO PERFORM " + "THE UPGRADE."; + } + mWindow->pushGui(new GuiMsgBox( + getHelpStyle(), upgradeMessage.c_str(), "OK", [] {}, "", nullptr, "", + nullptr, nullptr, true, true, + (mRenderer->getIsVerticalOrientation() ? + 0.85f : + 0.535f * (1.778f / mRenderer->getScreenAspectRatio())))); + } + }, + "CANCEL", [] { return; }, "", nullptr, nullptr, true, true, + (mRenderer->getIsVerticalOrientation() ? + 0.70f : + 0.45f * (1.778f / mRenderer->getScreenAspectRatio())))); + } + else { + mWindow->pushGui(new GuiMsgBox(getHelpStyle(), results, "OK", nullptr, "", nullptr, "", + nullptr, nullptr, true, true, + (mRenderer->getIsVerticalOrientation() ? + 0.70f : + 0.45f * (1.778f / mRenderer->getScreenAspectRatio())))); + } +} + +void ViewController::goToStart(bool playTransition) +{ + // Needed to avoid segfaults during emergency shutdown. + if (mRenderer->getSDLWindow() == nullptr) + return; + + // If the system view does not exist, then create it. We do this here as it would + // otherwise not be done if jumping directly into a specific game system on startup. + if (!mSystemListView) + getSystemListView(); + + // If a specific system is requested, go directly to its game list. + auto requestedSystem = Settings::getInstance()->getString("StartupSystem"); + if (requestedSystem != "") { + for (auto it = SystemData::sSystemVector.cbegin(); // Line break. + it != SystemData::sSystemVector.cend(); ++it) { + if ((*it)->getName() == requestedSystem) { + goToGamelist(*it); + if (!playTransition) + cancelViewTransitions(); + return; + } + } + + // Requested system doesn't exist. + Settings::getInstance()->setString("StartupSystem", ""); + } + // Get the first system entry. + goToSystemView(getSystemListView()->getFirstSystem(), false); +} + +void ViewController::ReloadAndGoToStart() +{ + mWindow->renderSplashScreen(Window::SplashScreenState::RELOADING, 0.0f); + reloadAll(); + if (mState.viewing == ViewMode::GAMELIST) { + goToSystemView(SystemData::sSystemVector.front(), false); + goToSystem(SystemData::sSystemVector.front(), false); + } + else { + goToSystem(SystemData::sSystemVector.front(), false); + } +} + +bool ViewController::isCameraMoving() +{ + if (mCurrentView) { + if (mCamera[3].x - -mCurrentView->getPosition().x != 0.0f || + mCamera[3].y - -mCurrentView->getPosition().y != 0.0f) + return true; + } + return false; +} + +void ViewController::cancelViewTransitions() +{ + if (mLastTransitionAnim == ViewTransitionAnimation::SLIDE) { + if (isCameraMoving()) { + mCamera[3].x = -mCurrentView->getPosition().x; + mCamera[3].y = -mCurrentView->getPosition().y; + stopAllAnimations(); + } + // mSkipView is used when skipping through the gamelists in quick succession. + // Without this, the game video (or static image) would not get rendered during + // the slide transition animation. + else if (mSkipView) { + mSkipView.reset(); + mSkipView = nullptr; + } + } + else if (mLastTransitionAnim == ViewTransitionAnimation::FADE) { + if (isAnimationPlaying(0)) { + finishAnimation(0); + mCancelledTransition = true; + mFadeOpacity = 0; + mWindow->invalidateCachedBackground(); + } + } +} + +void ViewController::stopScrolling() +{ + if (mRenderer->getSDLWindow() == nullptr) + return; + + mSystemListView->stopScrolling(); + mCurrentView->stopListScrolling(); + + if (mSystemListView->isSystemAnimationPlaying(0)) + mSystemListView->finishSystemAnimation(0); +} + +int ViewController::getSystemId(SystemData* system) +{ + std::vector& sysVec {SystemData::sSystemVector}; + return static_cast(std::find(sysVec.cbegin(), sysVec.cend(), system) - sysVec.cbegin()); +} + +void ViewController::restoreViewPosition() +{ + if (mPreviousView) { + glm::vec3 restorePosition {mPreviousView->getPosition()}; + restorePosition.x = mWrapPreviousPositionX; + mPreviousView->setPosition(restorePosition); + mWrapPreviousPositionX = 0; + mWrappedViews = false; + } +} + +void ViewController::goToSystemView(SystemData* system, bool playTransition) +{ + bool applicationStartup {false}; + + if (mState.viewing == ViewMode::NOTHING) + applicationStartup = true; + + // Restore the X position for the view, if it was previously moved. + if (mWrappedViews) + restoreViewPosition(); + + if (mPreviousView) { + mPreviousView.reset(); + mPreviousView = nullptr; + } + + if (mCurrentView != nullptr) + mCurrentView->onTransition(); + + mPreviousView = mCurrentView; + + if (system->isGroupedCustomCollection()) + system = system->getRootFolder()->getParent()->getSystem(); + + mState.previouslyViewed = mState.viewing; + mState.viewing = ViewMode::SYSTEM_SELECT; + mState.system = system; + mSystemViewTransition = true; + + auto systemList = getSystemListView(); + systemList->setPosition(getSystemId(system) * Renderer::getScreenWidth(), + systemList->getPosition().y); + + systemList->goToSystem(system, false); + mCurrentView = systemList; + mCurrentView->onShow(); + + // Application startup animation. + if (applicationStartup) { + const ViewTransitionAnimation transitionAnim {static_cast( + Settings::getInstance()->getInt("TransitionsStartupToSystem"))}; + + mCamera = glm::translate(mCamera, glm::round(-mCurrentView->getPosition())); + if (transitionAnim == ViewTransitionAnimation::SLIDE) { + if (getSystemListView()->getPrimaryType() == SystemView::PrimaryType::CAROUSEL) { + if (getSystemListView()->getCarouselType() == + CarouselComponent::CarouselType::HORIZONTAL || + getSystemListView()->getCarouselType() == + CarouselComponent::CarouselType::HORIZONTAL_WHEEL) + mCamera[3].y += Renderer::getScreenHeight(); + else + mCamera[3].x -= Renderer::getScreenWidth(); + } + else if (getSystemListView()->getPrimaryType() == SystemView::PrimaryType::TEXTLIST || + getSystemListView()->getPrimaryType() == SystemView::PrimaryType::GRID) { + mCamera[3].y += Renderer::getScreenHeight(); + } + updateHelpPrompts(); + } + else if (transitionAnim == ViewTransitionAnimation::FADE) { + if (getSystemListView()->getPrimaryType() == SystemView::PrimaryType::CAROUSEL) { + if (getSystemListView()->getCarouselType() == + CarouselComponent::CarouselType::HORIZONTAL || + getSystemListView()->getCarouselType() == + CarouselComponent::CarouselType::HORIZONTAL_WHEEL) + mCamera[3].y += Renderer::getScreenHeight(); + else + mCamera[3].x += Renderer::getScreenWidth(); + } + else if (getSystemListView()->getPrimaryType() == SystemView::PrimaryType::TEXTLIST || + getSystemListView()->getPrimaryType() == SystemView::PrimaryType::GRID) { + mCamera[3].y += Renderer::getScreenHeight(); + } + } + else { + updateHelpPrompts(); + } + } + + if (applicationStartup) + playViewTransition(ViewTransition::STARTUP_TO_SYSTEM); + else if (playTransition) + playViewTransition(ViewTransition::GAMELIST_TO_SYSTEM); + else + playViewTransition(ViewTransition::GAMELIST_TO_SYSTEM, true); +} + +void ViewController::goToSystem(SystemData* system, bool animate) +{ + mSystemListView->goToSystem(system, animate); +} + +void ViewController::goToNextGamelist() +{ + assert(mState.viewing == ViewMode::GAMELIST); + SystemData* system {getState().getSystem()}; + assert(system); + NavigationSounds::getInstance().playThemeNavigationSound(QUICKSYSSELECTSOUND); + mNextSystem = true; + goToGamelist(system->getNext()); +} + +void ViewController::goToPrevGamelist() +{ + assert(mState.viewing == ViewMode::GAMELIST); + SystemData* system {getState().getSystem()}; + assert(system); + NavigationSounds::getInstance().playThemeNavigationSound(QUICKSYSSELECTSOUND); + mNextSystem = false; + goToGamelist(system->getPrev()); +} + +void ViewController::goToGamelist(SystemData* system) +{ + bool wrapFirstToLast {false}; + bool wrapLastToFirst {false}; + bool slideTransitions {false}; + bool fadeTransitions {false}; + + if (mCurrentView != nullptr) + mCurrentView->onTransition(); + + ViewTransition transitionType; + ViewTransitionAnimation transitionAnim; + + if (mState.viewing == ViewMode::SYSTEM_SELECT) { + transitionType = ViewTransition::SYSTEM_TO_GAMELIST; + transitionAnim = static_cast( + Settings::getInstance()->getInt("TransitionsSystemToGamelist")); + } + else if (mState.viewing == ViewMode::NOTHING) { + transitionType = ViewTransition::STARTUP_TO_GAMELIST; + transitionAnim = static_cast( + Settings::getInstance()->getInt("TransitionsStartupToGamelist")); + } + else { + transitionType = ViewTransition::GAMELIST_TO_GAMELIST; + transitionAnim = static_cast( + Settings::getInstance()->getInt("TransitionsGamelistToGamelist")); + } + + if (transitionAnim == ViewTransitionAnimation::SLIDE) + slideTransitions = true; + + if (transitionAnim == ViewTransitionAnimation::FADE) + fadeTransitions = true; + + // Restore the X position for the view, if it was previously moved. + if (mWrappedViews) + restoreViewPosition(); + + if (mPreviousView && fadeTransitions && isAnimationPlaying(0)) + mPreviousView->onHide(); + + if (mPreviousView) { + mSkipView = mPreviousView; + mPreviousView.reset(); + mPreviousView = nullptr; + } + else if (!mPreviousView && mState.viewing == ViewMode::GAMELIST) { + // This is needed as otherwise the static image would not get rendered during the + // first Slide transition when coming from the System view. + mSkipView = getGamelistView(system); + } + + if (mState.viewing != ViewMode::SYSTEM_SELECT) { + mPreviousView = mCurrentView; + mSystemViewTransition = false; + } + else { + mSystemViewTransition = true; + } + + // Find if we're wrapping around the first and last systems, which requires the gamelist + // to be moved in order to avoid weird camera movements. This is only needed for the + // slide transition style. + if (mState.viewing == ViewMode::GAMELIST && SystemData::sSystemVector.size() > 1 && + slideTransitions) { + if (SystemData::sSystemVector.front() == mState.getSystem()) { + if (SystemData::sSystemVector.back() == system) + wrapFirstToLast = true; + } + else if (SystemData::sSystemVector.back() == mState.getSystem()) { + if (SystemData::sSystemVector.front() == system) + wrapLastToFirst = true; + } + } + + // Stop any scrolling, animations and camera movements. + if (mState.viewing == ViewMode::SYSTEM_SELECT) { + mSystemListView->stopScrolling(); + if (mSystemListView->isSystemAnimationPlaying(0)) + mSystemListView->finishSystemAnimation(0); + } + + if (slideTransitions || + (!fadeTransitions && mLastTransitionAnim == ViewTransitionAnimation::FADE)) + cancelViewTransitions(); + + if (mState.viewing == ViewMode::SYSTEM_SELECT) { + // Move the system list. + auto sysList = getSystemListView(); + float offsetX {sysList->getPosition().x}; + int sysId {getSystemId(system)}; + + sysList->setPosition(sysId * Renderer::getScreenWidth(), sysList->getPosition().y); + offsetX = sysList->getPosition().x - offsetX; + mCamera[3].x -= offsetX; + } + + // If we are wrapping around, either from the first to last system, or the other way + // around, we need to temporarily move the gamelist view location so that the camera + // movements will be correct. This is accomplished by simply offsetting the X position + // with the position of the first or last system plus the screen width. + if (wrapFirstToLast) { + glm::vec3 currentPosition {mCurrentView->getPosition()}; + mWrapPreviousPositionX = currentPosition.x; + float offsetX {getGamelistView(system)->getPosition().x}; + // This is needed to move the camera in the correct direction if there are only two systems. + if (SystemData::sSystemVector.size() == 2 && mNextSystem) + offsetX -= Renderer::getScreenWidth(); + else + offsetX += Renderer::getScreenWidth(); + currentPosition.x = offsetX; + mCurrentView->setPosition(currentPosition); + mCamera[3].x -= offsetX; + mWrappedViews = true; + } + else if (wrapLastToFirst) { + glm::vec3 currentPosition {mCurrentView->getPosition()}; + mWrapPreviousPositionX = currentPosition.x; + float offsetX {getGamelistView(system)->getPosition().x}; + if (SystemData::sSystemVector.size() == 2 && !mNextSystem) + offsetX += Renderer::getScreenWidth(); + else + offsetX -= Renderer::getScreenWidth(); + currentPosition.x = offsetX; + mCurrentView->setPosition(currentPosition); + mCamera[3].x = -offsetX; + mWrappedViews = true; + } + + mCurrentView = getGamelistView(system); + mCurrentView->finishAnimation(0); + + // Application startup animation, if starting in a gamelist rather than in the system view. + if (mState.viewing == ViewMode::NOTHING) { + if (mLastTransitionAnim == ViewTransitionAnimation::FADE) + cancelViewTransitions(); + mCamera = glm::translate(mCamera, glm::round(-mCurrentView->getPosition())); + if (transitionAnim == ViewTransitionAnimation::SLIDE) { + mCamera[3].y -= Renderer::getScreenHeight(); + updateHelpPrompts(); + } + else if (transitionAnim == ViewTransitionAnimation::FADE) { + mCamera[3].y += Renderer::getScreenHeight() * 2.0f; + } + else { + updateHelpPrompts(); + } + } + + mState.previouslyViewed = mState.viewing; + mState.viewing = ViewMode::GAMELIST; + mState.system = system; + + if (mCurrentView) + mCurrentView->onShow(); + + playViewTransition(transitionType); +} + +void ViewController::playViewTransition(ViewTransition transitionType, bool instant) +{ + mCancelledTransition = false; + + glm::vec3 target {0.0f, 0.0f, 0.0f}; + if (mCurrentView) + target = mCurrentView->getPosition(); + + // No need to animate, we're not going anywhere (probably due to goToNextGamelist() + // or goToPrevGamelist() being called when there's only 1 system). + if (target == static_cast(-mCamera[3]) && !isAnimationPlaying(0)) + return; + + ViewTransitionAnimation transitionAnim {ViewTransitionAnimation::INSTANT}; + + if (transitionType == ViewTransition::SYSTEM_TO_SYSTEM) + transitionAnim = static_cast( + Settings::getInstance()->getInt("TransitionsSystemToSystem")); + else if (transitionType == ViewTransition::SYSTEM_TO_GAMELIST) + transitionAnim = static_cast( + Settings::getInstance()->getInt("TransitionsSystemToGamelist")); + else if (transitionType == ViewTransition::GAMELIST_TO_GAMELIST) + transitionAnim = static_cast( + Settings::getInstance()->getInt("TransitionsGamelistToGamelist")); + else if (transitionType == ViewTransition::GAMELIST_TO_SYSTEM) + transitionAnim = static_cast( + Settings::getInstance()->getInt("TransitionsGamelistToSystem")); + else if (transitionType == ViewTransition::STARTUP_TO_SYSTEM) + transitionAnim = static_cast( + Settings::getInstance()->getInt("TransitionsStartupToSystem")); + else + transitionAnim = static_cast( + Settings::getInstance()->getInt("TransitionsStartupToGamelist")); + + mLastTransitionAnim = transitionAnim; + + if (instant || transitionAnim == ViewTransitionAnimation::INSTANT) { + setAnimation(new LambdaAnimation( + [this, target](float /*t*/) { + this->mCamera[3].x = -target.x; + this->mCamera[3].y = -target.y; + this->mCamera[3].z = -target.z; + if (mPreviousView) + mPreviousView->onHide(); + }, + 1)); + updateHelpPrompts(); + } + else if (transitionAnim == ViewTransitionAnimation::FADE) { + // Stop whatever's currently playing, leaving mFadeOpacity wherever it is. + cancelAnimation(0); + + auto fadeFunc = [this](float t) { + // The flag mCancelledTransition is required only when cancelViewTransitions() + // cancels the animation, and it's only needed for the Fade transitions. + // Without this, a (much shorter) fade transition would still play as + // finishedCallback is calling this function. + if (!mCancelledTransition) + mFadeOpacity = glm::mix(0.0f, 1.0f, t); + }; + + auto fadeCallback = [this]() { + if (mPreviousView) + mPreviousView->onHide(); + }; + + const static int FADE_DURATION {120}; // Fade in/out time. + const static int FADE_WAIT {200}; // Time to wait between in/out. + setAnimation(new LambdaAnimation(fadeFunc, FADE_DURATION), 0, + [this, fadeFunc, fadeCallback, target] { + this->mCamera[3].x = -target.x; + this->mCamera[3].y = -target.y; + this->mCamera[3].z = -target.z; + updateHelpPrompts(); + setAnimation(new LambdaAnimation(fadeFunc, FADE_DURATION), FADE_WAIT, + fadeCallback, true); + }); + + // Fast-forward animation if we're partially faded. + if (target == static_cast(-mCamera[3])) { + // Not changing screens, so cancel the first half entirely. + advanceAnimation(0, FADE_DURATION); + advanceAnimation(0, FADE_WAIT); + advanceAnimation(0, FADE_DURATION - static_cast(mFadeOpacity * FADE_DURATION)); + } + else { + advanceAnimation(0, static_cast(mFadeOpacity * FADE_DURATION)); + } + } + else if (transitionAnim == ViewTransitionAnimation::SLIDE) { + auto slideCallback = [this]() { + if (mSkipView) { + mSkipView->onHide(); + mSkipView.reset(); + mSkipView = nullptr; + } + else if (mPreviousView) { + mPreviousView->onHide(); + } + }; + setAnimation(new MoveCameraAnimation(mCamera, target), 0, slideCallback); + updateHelpPrompts(); // Update help prompts immediately. + } +} + +void ViewController::onFileChanged(FileData* file, bool reloadGamelist) +{ + auto it = mGamelistViews.find(file->getSystem()); + if (it != mGamelistViews.cend()) + it->second->onFileChanged(file, reloadGamelist); +} + +void ViewController::launch(FileData* game) +{ + if (game->getType() != GAME) { + LOG(LogError) << "Tried to launch something that isn't a game"; + return; + } + + // Disable text scrolling and stop any Lottie animations. These will be enabled again in + // FileData upon returning from the game. + mWindow->setAllowTextScrolling(false); + mWindow->setAllowFileAnimation(false); + + stopAnimation(1); // Make sure the fade in isn't still playing. + mWindow->stopInfoPopup(); // Make sure we disable any existing info popup. + + int duration {0}; + std::string durationString {Settings::getInstance()->getString("LaunchScreenDuration")}; + + if (durationString == "disabled") { + // If the game launch screen has been set as disabled, show a simple info popup + // notification instead. + mWindow->queueInfoPopup( + "LAUNCHING GAME '" + Utils::String::toUpper(game->metadata.get("name") + "'"), 10000); + duration = 1700; + } + else if (durationString == "brief") { + duration = 1700; + } + else if (durationString == "long") { + duration = 4500; + } + else { + // Normal duration. + duration = 3000; + } + + if (durationString != "disabled") + mWindow->displayLaunchScreen(game->getSourceFileData()); + + NavigationSounds::getInstance().playThemeNavigationSound(LAUNCHSOUND); + + // This is just a dummy animation in order for the launch screen or notification popup + // to be displayed briefly, and for the navigation sound playing to be able to complete. + // During this time period, all user input is blocked. + setAnimation(new LambdaAnimation([](float t) {}, duration), 0, [this, game] { + game->launchGame(); + // If the launch screen is disabled then this will do nothing. + mWindow->closeLaunchScreen(); + onFileChanged(game, true); + // This is a workaround so that any keys or button presses used for exiting the emulator + // are not captured upon returning. + setAnimation(new LambdaAnimation([](float t) {}, 1), 0, + [this] { mWindow->setBlockInput(false); }); + }); +} + +void ViewController::removeGamelistView(SystemData* system) +{ + auto exists = mGamelistViews.find(system); + if (exists != mGamelistViews.cend()) { + exists->second.reset(); + mGamelistViews.erase(system); + } +} + +std::shared_ptr ViewController::getGamelistView(SystemData* system) +{ + // If we have already created an entry for this system, then return that one. + auto exists = mGamelistViews.find(system); + if (exists != mGamelistViews.cend()) + return exists->second; + + system->getIndex()->setKidModeFilters(); + // If there's no entry, then create it and return it. + std::shared_ptr view; + + if (Settings::getInstance()->getBool("ThemeVariantTriggers")) { + const auto overrides = system->getTheme()->getCurrentThemeSelectedVariantOverrides(); + + if (!overrides.empty()) { + ThemeTriggers::TriggerType noVideosTriggerType {ThemeTriggers::TriggerType::NONE}; + ThemeTriggers::TriggerType noMediaTriggerType {ThemeTriggers::TriggerType::NONE}; + + const std::vector files { + system->getRootFolder()->getFilesRecursive(GAME | FOLDER)}; + + if (overrides.find(ThemeTriggers::TriggerType::NO_VIDEOS) != overrides.end()) { + noVideosTriggerType = ThemeTriggers::TriggerType::NO_VIDEOS; + + for (auto it = files.cbegin(); it != files.cend(); ++it) { + if (!(*it)->getVideoPath().empty()) { + noVideosTriggerType = ThemeTriggers::TriggerType::NONE; + break; + } + } + } + + if (overrides.find(ThemeTriggers::TriggerType::NO_MEDIA) != overrides.end()) { + noMediaTriggerType = ThemeTriggers::TriggerType::NO_MEDIA; + + for (auto imageType : overrides.at(ThemeTriggers::TriggerType::NO_MEDIA).second) { + for (auto it = files.cbegin(); it != files.cend(); ++it) { + if (imageType == "miximage") { + if (!(*it)->getMiximagePath().empty()) { + noMediaTriggerType = ThemeTriggers::TriggerType::NONE; + goto BREAK; + } + } + else if (imageType == "marquee") { + if (!(*it)->getMarqueePath().empty()) { + noMediaTriggerType = ThemeTriggers::TriggerType::NONE; + goto BREAK; + } + } + else if (imageType == "screenshot") { + if (!(*it)->getScreenshotPath().empty()) { + noMediaTriggerType = ThemeTriggers::TriggerType::NONE; + goto BREAK; + } + } + else if (imageType == "titlescreen") { + if (!(*it)->getTitleScreenPath().empty()) { + noMediaTriggerType = ThemeTriggers::TriggerType::NONE; + goto BREAK; + } + } + else if (imageType == "cover") { + if (!(*it)->getCoverPath().empty()) { + noMediaTriggerType = ThemeTriggers::TriggerType::NONE; + goto BREAK; + } + } + else if (imageType == "backcover") { + if (!(*it)->getBackCoverPath().empty()) { + noMediaTriggerType = ThemeTriggers::TriggerType::NONE; + goto BREAK; + } + } + else if (imageType == "3dbox") { + if (!(*it)->get3DBoxPath().empty()) { + noMediaTriggerType = ThemeTriggers::TriggerType::NONE; + goto BREAK; + } + } + else if (imageType == "physicalmedia") { + if (!(*it)->getPhysicalMediaPath().empty()) { + noMediaTriggerType = ThemeTriggers::TriggerType::NONE; + goto BREAK; + } + } + else if (imageType == "fanart") { + if (!(*it)->getFanArtPath().empty()) { + noMediaTriggerType = ThemeTriggers::TriggerType::NONE; + goto BREAK; + } + } + else if (imageType == "video") { + if (!(*it)->getVideoPath().empty()) { + noMediaTriggerType = ThemeTriggers::TriggerType::NONE; + goto BREAK; + } + } + } + } + } + BREAK: + // noMedia takes precedence over the noVideos trigger. + if (noMediaTriggerType == ThemeTriggers::TriggerType::NO_MEDIA) + system->loadTheme(noMediaTriggerType); + else + system->loadTheme(noVideosTriggerType); + } + } + + view = std::shared_ptr(new GamelistView(system->getRootFolder())); + view->setTheme(system->getTheme()); + + std::vector& sysVec {SystemData::sSystemVector}; + int id {static_cast(std::find(sysVec.cbegin(), sysVec.cend(), system) - sysVec.cbegin())}; + view->setPosition(id * Renderer::getScreenWidth(), Renderer::getScreenHeight() * 2.0f); + + addChild(view.get()); + + mGamelistViews[system] = view; + return view; +} + +std::shared_ptr ViewController::getSystemListView() +{ + // If we have already created a system view entry, then return it. + if (mSystemListView) + return mSystemListView; + + mSystemListView = std::shared_ptr(new SystemView); + addChild(mSystemListView.get()); + mSystemListView->setPosition(0, Renderer::getScreenHeight()); + return mSystemListView; +} + +bool ViewController::input(InputConfig* config, Input input) +{ + // If using the %RUNINBACKGROUND% variable in a launch command or if enabling the + // RunInBackground setting, ES-DE will run in the background while a game is launched. + // If we're in this state and then register some input, it means that the user is back in ES-DE. + // Therefore unset the game launch flag and update all the GUI components. This will re-enable + // the video player and scrolling of game names and game descriptions as well as letting the + // screensaver start on schedule. + if (mWindow->getGameLaunchedState()) { + mWindow->setAllowTextScrolling(true); + mWindow->setAllowFileAnimation(true); + mWindow->setLaunchedGame(false); + // Filter out the "a" button so the game is not restarted if there was such a button press + // queued when leaving the game. + if (config->isMappedTo("a", input) && input.value != 0) + return true; + // Trigger the game-end event. + if (mGameEndEventParams.size() == 5) { + Scripting::fireEvent(mGameEndEventParams[0], mGameEndEventParams[1], + mGameEndEventParams[2], mGameEndEventParams[3], + mGameEndEventParams[4]); + mGameEndEventParams.clear(); + } + } + + // Open the main menu. + if (!(UIModeController::getInstance()->isUIModeKid() && + !Settings::getInstance()->getBool("EnableMenuKidMode")) && + config->isMappedTo("start", input) && input.value != 0 && mCurrentView != nullptr) { + // If we don't stop the scrolling here, it will continue to + // run after closing the menu. + if (mSystemListView->isScrolling()) + mSystemListView->stopScrolling(); + // Finish the animation too, so that it doesn't continue + // to play when we've closed the menu. + if (mSystemListView->isSystemAnimationPlaying(0)) + mSystemListView->finishSystemAnimation(0); + // Stop the gamelist scrolling as well as it would otherwise continue to run after + // closing the menu. + mCurrentView->stopListScrolling(); + // Pause all videos as they would otherwise continue to play beneath the menu. + mCurrentView->pauseViewVideos(); + mCurrentView->stopGamelistFadeAnimations(); + + mWindow->setAllowTextScrolling(false); + mWindow->setAllowFileAnimation(false); + + // Finally, if the camera is currently moving, reset its position. + cancelViewTransitions(); + + mWindow->pushGui(new GuiMenu); + return true; + } + + if (!mWindow->isScreensaverActive()) { + mWindow->setAllowTextScrolling(true); + mWindow->setAllowFileAnimation(true); + } + + // Check if UI mode has changed due to passphrase completion. + if (UIModeController::getInstance()->listen(config, input)) + return true; + + if (mCurrentView) + return mCurrentView->input(config, input); + + return false; +} + +void ViewController::update(int deltaTime) +{ + if (mWindow->getChangedTheme()) + cancelViewTransitions(); + + if (mCurrentView) + mCurrentView->update(deltaTime); + + updateSelf(deltaTime); + + if (mGameToLaunch) { + launch(mGameToLaunch); + mGameToLaunch = nullptr; + } +} + +void ViewController::render(const glm::mat4& parentTrans) +{ + glm::mat4 trans {mCamera * parentTrans}; + glm::mat4 transInverse {glm::inverse(trans)}; + + // Camera position, position + size. + const glm::vec3 viewStart {transInverse[3]}; + const glm::vec3 viewEnd {std::fabs(trans[3].x) + Renderer::getScreenWidth(), + std::fabs(trans[3].y) + Renderer::getScreenHeight(), 0.0f}; + + // Keep track of UI mode changes. + UIModeController::getInstance()->monitorUIMode(); + + // Render the system view if it's the currently displayed view, or if we're in the progress + // of transitioning to or from this view. + if (mSystemListView == mCurrentView || (mSystemViewTransition && isCameraMoving())) + getSystemListView()->render(trans); + + auto gamelistRenderFunc = [trans, viewStart, viewEnd](auto it) { + const glm::vec3 guiStart {it->second->getPosition()}; + const glm::vec3 guiEnd {it->second->getPosition() + + glm::vec3 {it->second->getSize().x, it->second->getSize().y, 0.0f}}; + if (guiEnd.x >= viewStart.x && guiEnd.y >= viewStart.y && guiStart.x <= viewEnd.x && + guiStart.y <= viewEnd.y) + it->second->render(trans); + }; + + // Draw the gamelists. In the same manner as for the system view, limit the rendering only + // to what needs to be drawn. + for (auto it = mGamelistViews.cbegin(); it != mGamelistViews.cend(); ++it) { + if (it->second == mPreviousView && isCameraMoving()) + gamelistRenderFunc(it); + } + + // Always render the currently selected system last so that any stationary elements will get + // correctly rendered on top. + for (auto it = mGamelistViews.cbegin(); it != mGamelistViews.cend(); ++it) { + if (it->second == mCurrentView) + gamelistRenderFunc(it); + } + + if (mWindow->peekGui() == this) + mWindow->renderHelpPromptsEarly(); + + // Fade out. + if (mFadeOpacity) { + unsigned int fadeColor {0x00000000 | static_cast(mFadeOpacity * 255.0f)}; + mRenderer->setMatrix(parentTrans); + mRenderer->drawRect(0.0f, 0.0f, Renderer::getScreenWidth(), Renderer::getScreenHeight(), + fadeColor, fadeColor); + } +} + +void ViewController::preload() +{ + const unsigned int systemCount {static_cast(SystemData::sSystemVector.size())}; + // This reduces the amount of texture pop-in when loading theme extras. + if (!SystemData::sSystemVector.empty()) + getSystemListView(); + + const bool splashScreen {Settings::getInstance()->getBool("SplashScreen")}; + float loadedSystems {0.0f}; + unsigned int lastTime {0}; + unsigned int accumulator {0}; + SDL_Event event {}; + + for (auto it = SystemData::sSystemVector.cbegin(); // Line break. + it != SystemData::sSystemVector.cend(); ++it) { + // Poll events so that the OS doesn't think the application is hanging on startup, + // this is required as the main application loop hasn't started yet. + while (SDL_PollEvent(&event)) { + InputManager::getInstance().parseEvent(event); + if (event.type == SDL_QUIT) { + SystemData::sStartupExitSignal = true; + return; + } + }; + + const std::string entryType {(*it)->isCustomCollection() ? "custom collection" : "system"}; + LOG(LogDebug) << "ViewController::preload(): Populating gamelist for " << entryType << " \"" + << (*it)->getName() << "\""; + if (splashScreen) { + const unsigned int curTime {SDL_GetTicks()}; + accumulator += curTime - lastTime; + lastTime = curTime; + ++loadedSystems; + // This prevents Renderer::swapBuffers() from being called excessively which + // could lead to significantly longer application startup times. + if (accumulator > 20) { + accumulator = 0; + const float progress { + glm::mix(0.5f, 1.0f, loadedSystems / static_cast(systemCount))}; + mWindow->renderSplashScreen(Window::SplashScreenState::POPULATING, progress); + lastTime += SDL_GetTicks() - curTime; + } + } + (*it)->getIndex()->resetFilters(); + getGamelistView(*it)->preloadGamelist(); + } + + if (splashScreen && SystemData::sSystemVector.size() > 0) + Window::getInstance()->renderSplashScreen(Window::SplashScreenState::POPULATING, 1.0f); + + // Short delay so that the full progress bar is always visible before proceeding. + SDL_Delay(100); + + if (SystemData::sSystemVector.size() > 0) + ThemeData::setThemeTransitions(); + + // Load navigation sounds, either from the theme if it supports it, or otherwise from + // the bundled fallback sound files. + bool themeSoundSupport {false}; + for (auto system : SystemData::sSystemVector) { + if (!themeSoundSupport && system->getTheme()->hasView("all")) { + NavigationSounds::getInstance().loadThemeNavigationSounds(system->getTheme().get()); + themeSoundSupport = true; + } + if (system->getRootFolder()->getName() == "recent") { + CollectionSystemsManager::getInstance()->trimCollectionCount(system->getRootFolder(), + LAST_PLAYED_MAX); + } + } + if (!SystemData::sSystemVector.empty() && !themeSoundSupport) + NavigationSounds::getInstance().loadThemeNavigationSounds(nullptr); +} + +void ViewController::reloadGamelistView(GamelistView* view, bool reloadTheme) +{ + for (auto it = mGamelistViews.cbegin(); it != mGamelistViews.cend(); ++it) { + if (it->second.get() == view) { + bool isCurrent {mCurrentView == it->second}; + SystemData* system {it->first}; + FileData* cursor {view->getCursor()}; + + // Retain the cursor history for the view. + std::vector cursorHistoryTemp; + it->second->copyCursorHistory(cursorHistoryTemp); + + mGamelistViews.erase(it); + + if (isCurrent) + mCurrentView = nullptr; + + if (reloadTheme) + system->loadTheme(ThemeTriggers::TriggerType::NONE); + system->getIndex()->setKidModeFilters(); + std::shared_ptr newView {getGamelistView(system)}; + + // Make sure we don't attempt to set the cursor to a nonexistent entry. + auto children = system->getRootFolder()->getChildrenRecursive(); + if (std::find(children.cbegin(), children.cend(), cursor) != children.cend()) + newView->setCursor(cursor); + + if (isCurrent) + mCurrentView = newView; + + newView->populateCursorHistory(cursorHistoryTemp); + // This is required to get the game count updated if the favorite metadata value has + // been changed for any game that is part of a custom collection. + if (system->isCollection() && system->getName() == "collections") { + std::pair gameCount {0, 0}; + system->getRootFolder()->countGames(gameCount); + } + updateHelpPrompts(); + break; + } + } + + // If using the %RUNINBACKGROUND% variable in a launch command or if enabling the + // RunInBackground setting, ES-DE will run in the background while a game is launched. + // If this flag has been set, then update all the GUI components. This will block the + // video player, prevent scrolling of game names and game descriptions and prevent the + // screensaver from starting on schedule. + if (mWindow->getGameLaunchedState()) + mWindow->setLaunchedGame(true); + + // Redisplay the current view. + if (mCurrentView) + mCurrentView->onShow(); +} + +void ViewController::reloadAll() +{ + if (mRenderer->getSDLWindow() == nullptr) + return; + + cancelViewTransitions(); + + // Clear all GamelistViews. + std::map cursorMap; + for (auto it = mGamelistViews.cbegin(); it != mGamelistViews.cend(); ++it) { + if (std::find(SystemData::sSystemVector.cbegin(), SystemData::sSystemVector.cend(), + (*it).first) != SystemData::sSystemVector.cend()) + cursorMap[it->first] = it->second->getCursor(); + } + + mGamelistViews.clear(); + mCurrentView = nullptr; + + // Load themes, create GamelistViews and reset filters. + for (auto it = cursorMap.cbegin(); it != cursorMap.cend(); ++it) { + it->first->loadTheme(ThemeTriggers::TriggerType::NONE); + it->first->getIndex()->resetFilters(); + } + + ThemeData::setThemeTransitions(); + + // Rebuild SystemListView. + mSystemListView.reset(); + getSystemListView(); + + // Restore cursor positions for all systems. + for (auto it = cursorMap.cbegin(); it != cursorMap.cend(); ++it) { + const std::string entryType {(*it).first->isCustomCollection() ? "custom collection" : + "system"}; + LOG(LogDebug) << "ViewController::reloadAll(): Populating gamelist for " << entryType + << " \"" << (*it).first->getName() << "\""; + getGamelistView(it->first)->setCursor(it->second); + } + + // Update mCurrentView since the pointers changed. + if (mState.viewing == ViewMode::GAMELIST) { + mCurrentView = getGamelistView(mState.getSystem()); + } + else if (mState.viewing == ViewMode::SYSTEM_SELECT) { + SystemData* system {mState.getSystem()}; + mSystemListView->goToSystem(system, false); + mCurrentView = mSystemListView; + mCamera[3].x = 0.0f; + } + else { + goToSystemView(SystemData::sSystemVector.front(), false); + } + + // Load navigation sounds, either from the theme if it supports it, or otherwise from + // the bundled fallback sound files. + NavigationSounds::getInstance().deinit(); + bool themeSoundSupport {false}; + for (SystemData* system : SystemData::sSystemVector) { + if (system->getTheme()->hasView("all")) { + NavigationSounds::getInstance().loadThemeNavigationSounds(system->getTheme().get()); + themeSoundSupport = true; + break; + } + } + if (!SystemData::sSystemVector.empty() && !themeSoundSupport) + NavigationSounds::getInstance().loadThemeNavigationSounds(nullptr); + + ThemeData::themeLoadedLogOutput(); + + mCurrentView->onShow(); + updateHelpPrompts(); +} + +void ViewController::rescanROMDirectory() +{ + mWindow->setBlockInput(true); + resetCamera(); + + mState.viewing = ViewMode::NOTHING; + mGamelistViews.clear(); + mSystemListView.reset(); + mCurrentView.reset(); + mPreviousView.reset(); + mSkipView.reset(); + + mWindow->renderSplashScreen(Window::SplashScreenState::SCANNING, 0.0f); + CollectionSystemsManager::getInstance()->deinit(false); + SystemData::loadConfig(); + + if (SystemData::sStartupExitSignal) { + SDL_Event quit; + quit.type = SDL_QUIT; + SDL_PushEvent(&quit); + return; + } + + if (SystemData::sSystemVector.empty()) { + // It's possible that there are no longer any games. + mWindow->setBlockInput(false); + mWindow->invalidateCachedBackground(); + noGamesDialog(); + } + else { + preload(); + if (SystemData::sStartupExitSignal) { + SDL_Event quit; + quit.type = SDL_QUIT; + SDL_PushEvent(&quit); + return; + } + mWindow->setBlockInput(false); + goToStart(false); + } +} + +std::vector ViewController::getHelpPrompts() +{ + std::vector prompts; + if (!mCurrentView) + return prompts; + + prompts = mCurrentView->getHelpPrompts(); + if (!(UIModeController::getInstance()->isUIModeKid() && + !Settings::getInstance()->getBool("EnableMenuKidMode"))) + prompts.push_back(HelpPrompt("start", "menu")); + return prompts; +} + +HelpStyle ViewController::getHelpStyle() +{ + if (!mCurrentView) + return GuiComponent::getHelpStyle(); + + return mCurrentView->getHelpStyle(); +} + +HelpStyle ViewController::getViewHelpStyle() +{ + if (mState.viewing == ViewMode::GAMELIST) + return getGamelistView(mState.getSystem())->getHelpStyle(); + else + return getSystemListView()->getHelpStyle(); +} diff --git a/rd-submodules/es-de/patches-tmp/Window.cpp b/rd-submodules/es-de/patches-tmp/Window.cpp new file mode 100644 index 00000000..4ef169f3 --- /dev/null +++ b/rd-submodules/es-de/patches-tmp/Window.cpp @@ -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; +} diff --git a/rd-submodules/retroarch b/rd-submodules/retroarch index 717b7809..9dc43930 160000 --- a/rd-submodules/retroarch +++ b/rd-submodules/retroarch @@ -1 +1 @@ -Subproject commit 717b78093797270877ec416e58082f1c71d435d8 +Subproject commit 9dc439300e92338d5b2bc3fc680eedd4fe1d3b0c diff --git a/rd-submodules/shared-modules b/rd-submodules/shared-modules index a2441b96..76809270 160000 --- a/rd-submodules/shared-modules +++ b/rd-submodules/shared-modules @@ -1 +1 @@ -Subproject commit a2441b964afefd8cd1cebcdf562c7878670daf42 +Subproject commit 76809270588f87c6c14df96fef2dde9a7bac84f4