diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.cpp b/es-app/src/guis/GuiCollectionSystemsOptions.cpp index 73c77b2fa..f02385551 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.cpp +++ b/es-app/src/guis/GuiCollectionSystemsOptions.cpp @@ -14,6 +14,7 @@ #include "components/SwitchComponent.h" #include "guis/GuiMsgBox.h" #include "guis/GuiSettings.h" +#include "guis/GuiTextEditKeyboardPopup.h" #include "guis/GuiTextEditPopup.h" #include "utils/StringUtil.h" #include "views/ViewController.h" @@ -208,10 +209,21 @@ GuiCollectionSystemsOptions::GuiCollectionSystemsOptions(Window* window, std::st window->removeGui(topGui); createCustomCollection(name); }; - row.makeAcceptInputHandler([this, createCollectionCall] { - mWindow->pushGui(new GuiTextEditPopup(mWindow, getHelpStyle(), "New Collection Name", "", - createCollectionCall, false, "SAVE")); - }); + + if (Settings::getInstance()->getBool("VirtualKeyboard")) { + row.makeAcceptInputHandler([this, createCollectionCall] { + mWindow->pushGui(new GuiTextEditKeyboardPopup( + mWindow, getHelpStyle(), "New Collection Name", "", createCollectionCall, false, + "CREATE", "CREATE COLLECTION?")); + }); + } + else { + row.makeAcceptInputHandler([this, createCollectionCall] { + mWindow->pushGui(new GuiTextEditPopup(mWindow, getHelpStyle(), "New Collection Name", + "", createCollectionCall, false, "CREATE", + "CREATE COLLECTION?")); + }); + } addRow(row); // Delete custom collection. diff --git a/es-app/src/guis/GuiGamelistFilter.cpp b/es-app/src/guis/GuiGamelistFilter.cpp index 61a7361c2..b861cf179 100644 --- a/es-app/src/guis/GuiGamelistFilter.cpp +++ b/es-app/src/guis/GuiGamelistFilter.cpp @@ -12,6 +12,7 @@ #include "SystemData.h" #include "components/OptionListComponent.h" +#include "guis/GuiTextEditKeyboardPopup.h" #include "guis/GuiTextEditPopup.h" #include "views/UIModeController.h" #include "views/ViewController.h" @@ -118,11 +119,20 @@ void GuiGamelistFilter::addFiltersToMenu() mFilterIndex->setTextFilter(Utils::String::toUpper(newVal)); }; - row.makeAcceptInputHandler([this, updateVal] { - mWindow->pushGui(new GuiTextEditPopup(mWindow, getHelpStyle(), "TEXT FILTER (GAME NAME)", - mTextFilterField->getValue(), updateVal, false, "OK", - "APPLY CHANGES?")); - }); + if (Settings::getInstance()->getBool("VirtualKeyboard")) { + row.makeAcceptInputHandler([this, updateVal] { + mWindow->pushGui(new GuiTextEditKeyboardPopup( + mWindow, getHelpStyle(), "TEXT FILTER (GAME NAME)", mTextFilterField->getValue(), + updateVal, false, "OK", "APPLY CHANGES?")); + }); + } + else { + row.makeAcceptInputHandler([this, updateVal] { + mWindow->pushGui(new GuiTextEditPopup( + mWindow, getHelpStyle(), "TEXT FILTER (GAME NAME)", mTextFilterField->getValue(), + updateVal, false, "OK", "APPLY CHANGES?")); + }); + } mMenu.addRow(row); diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 27adc87c8..2aa0d742a 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -22,12 +22,13 @@ #include "components/SwitchComponent.h" #include "guis/GuiAlternativeEmulators.h" #include "guis/GuiCollectionSystemsOptions.h" -#include "guis/GuiComplexTextEditPopup.h" #include "guis/GuiDetectDevice.h" #include "guis/GuiMediaViewerOptions.h" #include "guis/GuiMsgBox.h" #include "guis/GuiScraperMenu.h" #include "guis/GuiScreensaverOptions.h" +#include "guis/GuiTextEditKeyboardPopup.h" +#include "guis/GuiTextEditPopup.h" #include "views/UIModeController.h" #include "views/ViewController.h" #include "views/gamelist/IGameListView.h" @@ -509,6 +510,18 @@ void GuiMenu::openUIOptions() } }); + // Enable virtual (on-screen) keyboard. + auto virtual_keyboard = std::make_shared(mWindow); + virtual_keyboard->setState(Settings::getInstance()->getBool("VirtualKeyboard")); + s->addWithLabel("ENABLE VIRTUAL KEYBOARD", virtual_keyboard); + s->addSaveFunc([virtual_keyboard, s] { + if (virtual_keyboard->getState() != Settings::getInstance()->getBool("VirtualKeyboard")) { + Settings::getInstance()->setBool("VirtualKeyboard", virtual_keyboard->getState()); + s->setNeedsSaving(); + s->setInvalidateCachedBackground(); + } + }); + // Enable the 'Y' button for tagging games as favorites. auto favorites_add_button = std::make_shared(mWindow); favorites_add_button->setState(Settings::getInstance()->getBool("FavoritesAddButton")); @@ -809,10 +822,20 @@ void GuiMenu::openOtherOptions() rowMediaDir.makeAcceptInputHandler([this, titleMediaDir, mediaDirectoryStaticText, defaultDirectoryText, initValueMediaDir, updateValMediaDir, multiLineMediaDir] { - mWindow->pushGui(new GuiComplexTextEditPopup( - mWindow, getHelpStyle(), titleMediaDir, mediaDirectoryStaticText, defaultDirectoryText, - Settings::getInstance()->getString("MediaDirectory"), updateValMediaDir, - multiLineMediaDir, "SAVE", "SAVE CHANGES?")); + if (Settings::getInstance()->getBool("VirtualKeyboard")) { + mWindow->pushGui(new GuiTextEditKeyboardPopup( + mWindow, getHelpStyle(), titleMediaDir, + Settings::getInstance()->getString("MediaDirectory"), updateValMediaDir, + multiLineMediaDir, "SAVE", "SAVE CHANGES?", mediaDirectoryStaticText, + defaultDirectoryText, "load default directory")); + } + else { + mWindow->pushGui(new GuiTextEditPopup( + mWindow, getHelpStyle(), titleMediaDir, + Settings::getInstance()->getString("MediaDirectory"), updateValMediaDir, + multiLineMediaDir, "SAVE", "SAVE CHANGES?", mediaDirectoryStaticText, + defaultDirectoryText, "load default directory")); + } }); s->addRow(rowMediaDir); diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index c5d4168a3..168bdbe24 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -23,9 +23,9 @@ #include "components/RatingComponent.h" #include "components/SwitchComponent.h" #include "components/TextComponent.h" -#include "guis/GuiComplexTextEditPopup.h" #include "guis/GuiGameScraper.h" #include "guis/GuiMsgBox.h" +#include "guis/GuiTextEditKeyboardPopup.h" #include "guis/GuiTextEditPopup.h" #include "resources/Font.h" #include "utils/StringUtil.h" @@ -364,11 +364,20 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, } }; - row.makeAcceptInputHandler([this, title, ed, updateVal, multiLine] { - mWindow->pushGui(new GuiTextEditPopup(mWindow, getHelpStyle(), title, - ed->getValue(), updateVal, multiLine, - "APPLY", "APPLY CHANGES?")); - }); + if (Settings::getInstance()->getBool("VirtualKeyboard")) { + row.makeAcceptInputHandler([this, title, ed, updateVal, multiLine] { + mWindow->pushGui(new GuiTextEditKeyboardPopup( + mWindow, getHelpStyle(), title, ed->getValue(), updateVal, multiLine, + "apply", "APPLY CHANGES?", "", "")); + }); + } + else { + row.makeAcceptInputHandler([this, title, ed, updateVal, multiLine] { + mWindow->pushGui(new GuiTextEditPopup(mWindow, getHelpStyle(), title, + ed->getValue(), updateVal, multiLine, + "APPLY", "APPLY CHANGES?")); + }); + } break; } } diff --git a/es-app/src/guis/GuiScraperSearch.cpp b/es-app/src/guis/GuiScraperSearch.cpp index e12edd803..2b50ffabb 100644 --- a/es-app/src/guis/GuiScraperSearch.cpp +++ b/es-app/src/guis/GuiScraperSearch.cpp @@ -29,6 +29,7 @@ #include "components/ScrollableContainer.h" #include "components/TextComponent.h" #include "guis/GuiMsgBox.h" +#include "guis/GuiTextEditKeyboardPopup.h" #include "guis/GuiTextEditPopup.h" #include "resources/Font.h" #include "utils/StringUtil.h" @@ -808,8 +809,16 @@ void GuiScraperSearch::openInputScreen(ScraperSearchParams& params) searchString = params.nameOverride; } - mWindow->pushGui(new GuiTextEditPopup(mWindow, getHelpStyle(), "REFINE SEARCH", searchString, - searchForFunc, false, "SEARCH", "APPLY CHANGES?")); + if (Settings::getInstance()->getBool("VirtualKeyboard")) { + mWindow->pushGui(new GuiTextEditKeyboardPopup(mWindow, getHelpStyle(), "REFINE SEARCH", + searchString, searchForFunc, false, "SEARCH", + "SEARCH USING REFINED NAME?")); + } + else { + mWindow->pushGui(new GuiTextEditPopup(mWindow, getHelpStyle(), "REFINE SEARCH", + searchString, searchForFunc, false, "SEARCH", + "SEARCH USING REFINED NAME?")); + } } bool GuiScraperSearch::saveMetadata(const ScraperSearchResult& result, diff --git a/es-app/src/guis/GuiSettings.cpp b/es-app/src/guis/GuiSettings.cpp index b5827febb..42a24c624 100644 --- a/es-app/src/guis/GuiSettings.cpp +++ b/es-app/src/guis/GuiSettings.cpp @@ -16,6 +16,7 @@ #include "SystemData.h" #include "Window.h" #include "components/HelpComponent.h" +#include "guis/GuiTextEditKeyboardPopup.h" #include "guis/GuiTextEditPopup.h" #include "views/ViewController.h" #include "views/gamelist/IGameListView.h" @@ -193,15 +194,30 @@ void GuiSettings::addEditableTextComponent(const std::string label, } }; - row.makeAcceptInputHandler([this, label, ed, updateVal, isPassword] { - // Never display the value if it's a password, instead set it to blank. - if (isPassword) - mWindow->pushGui( - new GuiTextEditPopup(mWindow, getHelpStyle(), label, "", updateVal, false)); - else - mWindow->pushGui(new GuiTextEditPopup(mWindow, getHelpStyle(), label, ed->getValue(), - updateVal, false)); - }); + if (Settings::getInstance()->getBool("VirtualKeyboard")) { + row.makeAcceptInputHandler([this, label, ed, updateVal, isPassword] { + // Never display the value if it's a password, instead set it to blank. + if (isPassword) + mWindow->pushGui(new GuiTextEditKeyboardPopup( + mWindow, getHelpStyle(), label, "", updateVal, false, "SAVE", "SAVE CHANGES?")); + else + mWindow->pushGui(new GuiTextEditKeyboardPopup(mWindow, getHelpStyle(), label, + ed->getValue(), updateVal, false, + "SAVE", "SAVE CHANGES?")); + }); + } + else { + row.makeAcceptInputHandler([this, label, ed, updateVal, isPassword] { + if (isPassword) + mWindow->pushGui(new GuiTextEditPopup(mWindow, getHelpStyle(), label, "", updateVal, + false, "SAVE", "SAVE CHANGES?")); + else + mWindow->pushGui(new GuiTextEditPopup(mWindow, getHelpStyle(), label, + ed->getValue(), updateVal, false, "SAVE", + "SAVE CHANGES?")); + }); + } + assert(ed); addRow(row); diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 0227b92cb..376f70b15 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -26,6 +26,8 @@ #include "animations/MoveCameraAnimation.h" #include "guis/GuiInfoPopup.h" #include "guis/GuiMenu.h" +#include "guis/GuiTextEditKeyboardPopup.h" +#include "guis/GuiTextEditPopup.h" #include "views/SystemView.h" #include "views/UIModeController.h" #include "views/gamelist/DetailedGameListView.h" @@ -135,26 +137,52 @@ void ViewController::noGamesDialog() #else currentROMDirectory = FileData::getROMDirectory(); #endif - - mWindow->pushGui(new GuiComplexTextEditPopup( - mWindow, HelpStyle(), "ENTER ROM DIRECTORY PATH", - "Currently configured path:", currentROMDirectory, currentROMDirectory, - [this](const std::string& newROMDirectory) { - Settings::getInstance()->setString("ROMDirectory", newROMDirectory); - Settings::getInstance()->saveFile(); + if (Settings::getInstance()->getBool("VirtualKeyboard")) { + mWindow->pushGui(new GuiTextEditKeyboardPopup( + mWindow, HelpStyle(), "ENTER ROM DIRECTORY PATH", currentROMDirectory, + [this](const std::string& newROMDirectory) { + Settings::getInstance()->setString("ROMDirectory", newROMDirectory); + Settings::getInstance()->saveFile(); #if defined(_WIN64) - mRomDirectory = Utils::String::replace(FileData::getROMDirectory(), "/", "\\"); + mRomDirectory = + Utils::String::replace(FileData::getROMDirectory(), "/", "\\"); #else - mRomDirectory = FileData::getROMDirectory(); + mRomDirectory = FileData::getROMDirectory(); #endif - mNoGamesMessageBox->changeText(mNoGamesErrorMessage + mRomDirectory); - mWindow->pushGui(new GuiMsgBox(mWindow, HelpStyle(), - "ROM DIRECTORY SETTING SAVED, RESTART\n" - "THE APPLICATION TO RESCAN THE SYSTEMS", - "OK", nullptr, "", nullptr, "", nullptr, true)); - }, - false, "SAVE", "SAVE CHANGES?", "LOAD CURRENT", "LOAD CURRENTLY CONFIGURED VALUE", - "CLEAR", "CLEAR (LEAVE BLANK TO RESET TO DEFAULT DIRECTORY)", false)); + mNoGamesMessageBox->changeText(mNoGamesErrorMessage + mRomDirectory); + mWindow->pushGui(new GuiMsgBox(mWindow, HelpStyle(), + "ROM DIRECTORY SETTING SAVED, RESTART\n" + "THE APPLICATION TO RESCAN THE SYSTEMS", + "OK", nullptr, "", nullptr, "", nullptr, + true)); + }, + false, "SAVE", "SAVE CHANGES?", "Currently configured path:", + currentROMDirectory, "LOAD CURRENTLY CONFIGURED PATH", + "CLEAR (LEAVE BLANK TO RESET TO DEFAULT PATH)")); + } + else { + mWindow->pushGui(new GuiTextEditPopup( + mWindow, HelpStyle(), "ENTER ROM DIRECTORY PATH", currentROMDirectory, + [this](const std::string& newROMDirectory) { + Settings::getInstance()->setString("ROMDirectory", newROMDirectory); + Settings::getInstance()->saveFile(); +#if defined(_WIN64) + mRomDirectory = + Utils::String::replace(FileData::getROMDirectory(), "/", "\\"); +#else + mRomDirectory = FileData::getROMDirectory(); +#endif + mNoGamesMessageBox->changeText(mNoGamesErrorMessage + mRomDirectory); + mWindow->pushGui(new GuiMsgBox(mWindow, HelpStyle(), + "ROM DIRECTORY SETTING SAVED, RESTART\n" + "THE APPLICATION TO RESCAN THE SYSTEMS", + "OK", nullptr, "", nullptr, "", nullptr, + true)); + }, + false, "SAVE", "SAVE CHANGES?", "Currently configured path:", + currentROMDirectory, "LOAD CURRENTLY CONFIGURED PATH", + "CLEAR (LEAVE BLANK TO RESET TO DEFAULT PATH)")); + } }, "CREATE DIRECTORIES", [this] { diff --git a/es-app/src/views/ViewController.h b/es-app/src/views/ViewController.h index db08f2d85..69713ef12 100644 --- a/es-app/src/views/ViewController.h +++ b/es-app/src/views/ViewController.h @@ -15,7 +15,6 @@ #include "FileData.h" #include "GuiComponent.h" -#include "guis/GuiComplexTextEditPopup.h" #include "guis/GuiMsgBox.h" #include "renderers/Renderer.h" diff --git a/es-core/CMakeLists.txt b/es-core/CMakeLists.txt index fb1959b8a..43260823d 100644 --- a/es-core/CMakeLists.txt +++ b/es-core/CMakeLists.txt @@ -60,10 +60,10 @@ set(CORE_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/components/VideoVlcComponent.h # GUIs - ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiComplexTextEditPopup.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiDetectDevice.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInputConfig.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMsgBox.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiTextEditKeyboardPopup.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiTextEditPopup.h # Renderers @@ -130,10 +130,10 @@ set(CORE_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/components/VideoVlcComponent.cpp # GUIs - ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiComplexTextEditPopup.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiDetectDevice.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInputConfig.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMsgBox.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiTextEditKeyboardPopup.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiTextEditPopup.cpp # Renderer diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 9e15920c6..c639c23a3 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -180,6 +180,7 @@ void Settings::setDefaults() mBoolMap["FavoritesStar"] = {true, true}; mBoolMap["SpecialCharsASCII"] = {false, false}; mBoolMap["ListScrollOverlay"] = {false, false}; + mBoolMap["VirtualKeyboard"] = {true, true}; mBoolMap["FavoritesAddButton"] = {true, true}; mBoolMap["RandomAddButton"] = {false, false}; mBoolMap["GamelistFilters"] = {true, true}; diff --git a/es-core/src/guis/GuiComplexTextEditPopup.cpp b/es-core/src/guis/GuiComplexTextEditPopup.cpp deleted file mode 100644 index 802f544ee..000000000 --- a/es-core/src/guis/GuiComplexTextEditPopup.cpp +++ /dev/null @@ -1,155 +0,0 @@ -// SPDX-License-Identifier: MIT -// -// EmulationStation Desktop Edition -// GuiComplexTextEditPopup.cpp -// -// Text edit popup with a title, two text strings, a text input box and buttons -// to load the second text string and to clear the input field. -// Intended for updating settings for configuration files and similar. -// - -#include "guis/GuiComplexTextEditPopup.h" - -#include "Window.h" -#include "components/ButtonComponent.h" -#include "components/MenuComponent.h" -#include "components/TextEditComponent.h" -#include "guis/GuiMsgBox.h" - -GuiComplexTextEditPopup::GuiComplexTextEditPopup( - Window* window, - const HelpStyle& helpstyle, - const std::string& title, - const std::string& infoString1, - const std::string& infoString2, - const std::string& initValue, - const std::function& okCallback, - bool multiLine, - const std::string& acceptBtnText, - const std::string& saveConfirmationText, - const std::string& loadBtnText, - const std::string& loadBtnHelpText, - const std::string& clearBtnText, - const std::string& clearBtnHelpText, - bool hideCancelButton) - : GuiComponent(window) - , mHelpStyle(helpstyle) - , mBackground(window, ":/graphics/frame.svg") - , mGrid(window, glm::ivec2{1, 5}) - , mMultiLine(multiLine) - , mInitValue(initValue) - , mOkCallback(okCallback) - , mSaveConfirmationText(saveConfirmationText) - , mHideCancelButton(hideCancelButton) -{ - addChild(&mBackground); - addChild(&mGrid); - - mTitle = std::make_shared(mWindow, Utils::String::toUpper(title), - Font::get(FONT_SIZE_MEDIUM), 0x555555FF, ALIGN_CENTER); - mInfoString1 = std::make_shared(mWindow, infoString1, Font::get(FONT_SIZE_SMALL), - 0x555555FF, ALIGN_CENTER); - mInfoString2 = std::make_shared(mWindow, infoString2, Font::get(FONT_SIZE_SMALL), - 0x555555FF, ALIGN_CENTER); - - mText = std::make_shared(mWindow); - mText->setValue(initValue); - - std::vector> buttons; - buttons.push_back(std::make_shared(mWindow, acceptBtnText, acceptBtnText, - [this, okCallback] { - okCallback(mText->getValue()); - delete this; - })); - buttons.push_back(std::make_shared(mWindow, loadBtnText, loadBtnHelpText, - [this, infoString2] { - mText->setValue(infoString2); - mText->setCursor(0); - mText->setCursor(infoString2.size()); - })); - buttons.push_back(std::make_shared(mWindow, clearBtnText, clearBtnHelpText, - [this] { mText->setValue(""); })); - if (!mHideCancelButton) - buttons.push_back(std::make_shared(mWindow, "CANCEL", "discard changes", - [this] { delete this; })); - - mButtonGrid = makeButtonGrid(mWindow, buttons); - - mGrid.setEntry(mTitle, glm::ivec2{0, 0}, false, true); - mGrid.setEntry(mInfoString1, glm::ivec2{0, 1}, false, true); - mGrid.setEntry(mInfoString2, glm::ivec2{0, 2}, false, false); - mGrid.setEntry(mText, glm::ivec2{0, 3}, true, false, glm::ivec2{1, 1}, - GridFlags::BORDER_TOP | GridFlags::BORDER_BOTTOM); - mGrid.setEntry(mButtonGrid, glm::ivec2{0, 4}, true, false); - mGrid.setRowHeightPerc(1, 0.15f, true); - - float textHeight = mText->getFont()->getHeight(); - - if (multiLine) - textHeight *= 6.0f; - - // Adjust the width relative to the aspect ratio of the screen to make the GUI look coherent - // regardless of screen type. The 1.778 aspect ratio value is the 16:9 reference. - float aspectValue = 1.778f / Renderer::getScreenAspectRatio(); - float infoWidth = glm::clamp(0.70f * aspectValue, 0.60f, 0.85f) * Renderer::getScreenWidth(); - float windowWidth = glm::clamp(0.75f * aspectValue, 0.65f, 0.90f) * Renderer::getScreenWidth(); - - mText->setSize(0, textHeight); - mInfoString2->setSize(infoWidth, mInfoString2->getFont()->getHeight()); - - setSize(windowWidth, mTitle->getFont()->getHeight() + textHeight + mButtonGrid->getSize().y + - mButtonGrid->getSize().y * 1.85f); - setPosition((Renderer::getScreenWidth() - mSize.x) / 2.0f, - (Renderer::getScreenHeight() - mSize.y) / 2.0f); - mText->startEditing(); -} - -void GuiComplexTextEditPopup::onSizeChanged() -{ - mBackground.fitTo(mSize, glm::vec3{}, glm::vec2{-32.0f, -32.0f}); - mText->setSize(mSize.x - 40.0f, mText->getSize().y); - - // Update grid. - mGrid.setRowHeightPerc(0, mTitle->getFont()->getHeight() / mSize.y); - mGrid.setRowHeightPerc(2, mButtonGrid->getSize().y / mSize.y); - mGrid.setSize(mSize); -} - -bool GuiComplexTextEditPopup::input(InputConfig* config, Input input) -{ - if (GuiComponent::input(config, input)) - return true; - - if (!mHideCancelButton) { - // Pressing back when not text editing closes us. - if (config->isMappedTo("b", input) && input.value) { - if (mText->getValue() != mInitValue) { - // Changes were made, ask if the user wants to save them. - mWindow->pushGui(new GuiMsgBox( - mWindow, mHelpStyle, mSaveConfirmationText, "YES", - [this] { - this->mOkCallback(mText->getValue()); - delete this; - return true; - }, - "NO", - [this] { - delete this; - return false; - })); - } - else { - delete this; - } - } - } - return false; -} - -std::vector GuiComplexTextEditPopup::getHelpPrompts() -{ - std::vector prompts = mGrid.getHelpPrompts(); - if (!mHideCancelButton) - prompts.push_back(HelpPrompt("b", "back")); - return prompts; -} diff --git a/es-core/src/guis/GuiComplexTextEditPopup.h b/es-core/src/guis/GuiComplexTextEditPopup.h deleted file mode 100644 index a1ffc8e41..000000000 --- a/es-core/src/guis/GuiComplexTextEditPopup.h +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-License-Identifier: MIT -// -// EmulationStation Desktop Edition -// GuiComplexTextEditPopup.h -// -// Text edit popup with a title, two text strings, a text input box and buttons -// to load the second text string and to clear the input field. -// Intended for updating settings for configuration files and similar. -// - -#ifndef ES_CORE_GUIS_GUI_COMPLEX_TEXT_EDIT_POPUP_H -#define ES_CORE_GUIS_GUI_COMPLEX_TEXT_EDIT_POPUP_H - -#include "GuiComponent.h" -#include "components/ComponentGrid.h" -#include "components/NinePatchComponent.h" - -class TextComponent; -class TextEditComponent; - -class GuiComplexTextEditPopup : public GuiComponent -{ -public: - GuiComplexTextEditPopup(Window* window, - const HelpStyle& helpstyle, - const std::string& title, - const std::string& infoString1, - const std::string& infoString2, - const std::string& initValue, - const std::function& okCallback, - bool multiLine, - const std::string& acceptBtnText = "OK", - const std::string& saveConfirmationText = "SAVE CHANGES?", - const std::string& loadBtnText = "LOAD", - const std::string& loadBtnHelpText = "load default", - const std::string& clearBtnText = "CLEAR", - const std::string& clearBtnHelpText = "clear", - bool hideCancelButton = false); - - bool input(InputConfig* config, Input input) override; - void onSizeChanged() override; - - std::vector getHelpPrompts() override; - HelpStyle getHelpStyle() override { return mHelpStyle; } - -private: - NinePatchComponent mBackground; - ComponentGrid mGrid; - - std::shared_ptr mTitle; - std::shared_ptr mInfoString1; - std::shared_ptr mInfoString2; - std::shared_ptr mText; - std::shared_ptr mButtonGrid; - - HelpStyle mHelpStyle; - bool mMultiLine; - bool mHideCancelButton; - std::string mInitValue; - std::function mOkCallback; - std::string mSaveConfirmationText; -}; - -#endif // ES_CORE_GUIS_GUI_COMPLEX_TEXT_EDIT_POPUP_H diff --git a/es-core/src/guis/GuiTextEditKeyboardPopup.cpp b/es-core/src/guis/GuiTextEditKeyboardPopup.cpp new file mode 100644 index 000000000..f163fc4a0 --- /dev/null +++ b/es-core/src/guis/GuiTextEditKeyboardPopup.cpp @@ -0,0 +1,697 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// GuiTextEditKeyboardPopup.cpp +// +// Text edit popup with a virtual keyboard. +// Has a default mode and a complex mode, both with various options passed as arguments. +// + +#define KEYBOARD_WIDTH Renderer::getScreenWidth() * 0.78f +#define KEYBOARD_HEIGHT Renderer::getScreenHeight() * 0.60f + +#define KEYBOARD_PADDINGX (Renderer::getScreenWidth() * 0.02f) +#define KEYBOARD_PADDINGY (Renderer::getScreenWidth() * 0.01f) + +#define BUTTON_GRID_HORIZ_PADDING (10.0f * Renderer::getScreenHeightModifier()) + +#define NAVIGATION_REPEAT_START_DELAY 400 +#define NAVIGATION_REPEAT_SPEED 70 // Lower is faster. + +#define DELETE_REPEAT_START_DELAY 600 +#define DELETE_REPEAT_SPEED 90 // Lower is faster. + +#if defined(_MSC_VER) // MSVC compiler. +#define DELETE_SYMBOL Utils::String::wideStringToString(L"\uf177") +#define OK_SYMBOL Utils::String::wideStringToString(L"\uf058") +#define SHIFT_SYMBOL Utils::String::wideStringToString(L"\uf176") +#define ALT_SYMBOL Utils::String::wideStringToString(L"\uf141") +#else +#define DELETE_SYMBOL "\uf177" +#define OK_SYMBOL "\uf058" +#define SHIFT_SYMBOL "\uf176" +#define ALT_SYMBOL "\uf141" +#endif + +#include "guis/GuiTextEditKeyboardPopup.h" + +#include "components/MenuComponent.h" +#include "guis/GuiMsgBox.h" +#include "utils/StringUtil.h" + +// clang-format off +std::vector> kbBaseUS{ + {"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "DEL"}, + {"!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "+", "DEL"}, + {"¡", "²", "³", "¤", "€", "¼", "½", "¾", "‘", "’", "¥", "×", "DEL"}, + {"¹", "", "", "£", "", "", "", "", "", "", "", "÷", "DEL"}, + + {"q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "[", "]", "OK"}, + {"Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "{", "}", "OK"}, + {"ä", "å", "é", "®", "þ", "ü", "ú", "í", "ó", "ö", "«", "»", "OK"}, + {"Ä", "Å", "É", "", "Þ", "Ü", "Ú", "Í", "Ó", "Ö", "", "", "OK"}, + + {"a", "s", "d", "f", "g", "h", "j", "k", "l", ";", "'", "\\", "-rowspan-"}, + {"A", "S", "D", "F", "G", "H", "J", "K", "L", ":", "\"", "|", "-rowspan-"}, + {"á", "ß", "ð", "", "", "", "", "", "ø", "¶", "´", "¬", "-rowspan-"}, + {"Á", "§", "Ð", "", "", "", "", "", "Ø", "°", "¨", "¦", "-rowspan-"}, + + {"`", "z", "x", "c", "v", "b", "n", "m", ",", ".", "/", "ALT", "-colspan-"}, + {"~", "Z", "X", "C", "V", "B", "N", "M", "<", ">", "?", "ALT", "-colspan-"}, + {"", "æ", "", "©", "", "", "ñ", "µ", "ç", "", "¿", "ALT", "-colspan-"}, + {"", "Æ", "", "¢", "", "", "Ñ", "Μ", "Ç", "", "", "ALT", "-colspan-"}}; + +std::vector> kbLastRowNormal{ + {"SHIFT", "-colspan-", "SPACE", "-colspan-", "-colspan-", "-colspan-", "-colspan-", "-colspan-", "-colspan-", "CLEAR", "-colspan-", "CANCEL", "-colspan-"}, + {"SHIFT", "-colspan-", "SPACE", "-colspan-", "-colspan-", "-colspan-", "-colspan-", "-colspan-", "-colspan-", "CLEAR", "-colspan-", "CANCEL", "-colspan-"}, + {"SHIFT", "-colspan-", "SPACE", "-colspan-", "-colspan-", "-colspan-", "-colspan-", "-colspan-", "-colspan-", "CLEAR", "-colspan-", "CANCEL", "-colspan-"}, + {"SHIFT", "-colspan-", "SPACE", "-colspan-", "-colspan-", "-colspan-", "-colspan-", "-colspan-", "-colspan-", "CLEAR", "-colspan-", "CANCEL", "-colspan-"}}; + +std::vector> kbLastRowLoad{ + {"SHIFT", "-colspan-", "SPACE", "-colspan-", "-colspan-", "-colspan-", "-colspan-", "LOAD", "-colspan-", "CLEAR", "-colspan-", "CANCEL", "-colspan-"}, + {"SHIFT", "-colspan-", "SPACE", "-colspan-", "-colspan-", "-colspan-", "-colspan-", "LOAD", "-colspan-", "CLEAR", "-colspan-", "CANCEL", "-colspan-"}, + {"SHIFT", "-colspan-", "SPACE", "-colspan-", "-colspan-", "-colspan-", "-colspan-", "LOAD", "-colspan-", "CLEAR", "-colspan-", "CANCEL", "-colspan-"}, + {"SHIFT", "-colspan-", "SPACE", "-colspan-", "-colspan-", "-colspan-", "-colspan-", "LOAD", "-colspan-", "CLEAR", "-colspan-", "CANCEL", "-colspan-"}}; +// clang-format on + +GuiTextEditKeyboardPopup::GuiTextEditKeyboardPopup( + Window* window, + const HelpStyle& helpstyle, + const std::string& title, + const std::string& initValue, + const std::function& okCallback, + bool multiLine, + const std::string& acceptBtnHelpText, + const std::string& saveConfirmationText, + const std::string& infoString, + const std::string& defaultValue, + const std::string& loadBtnHelpText, + const std::string& clearBtnHelpText, + const std::string& cancelBtnHelpText) + : GuiComponent{window} + , mHelpStyle{helpstyle} + , mInitValue{initValue} + , mOkCallback{okCallback} + , mMultiLine{multiLine} + , mAcceptBtnHelpText{acceptBtnHelpText} + , mSaveConfirmationText{saveConfirmationText} + , mLoadBtnHelpText{loadBtnHelpText} + , mClearBtnHelpText{clearBtnHelpText} + , mCancelBtnHelpText{cancelBtnHelpText} + , mBackground{window, ":/graphics/frame.svg"} + , mGrid{window, glm::ivec2{1, (infoString != "" && defaultValue != "" ? 8 : 6)}} + , mComplexMode{(infoString != "" && defaultValue != "")} + , mDeleteRepeat{false} + , mShift{false} + , mAlt{false} + , mAltShift{false} + , mDeleteRepeatTimer{0} + , mNavigationRepeatTimer{0} + , mNavigationRepeatDirX{0} + , mNavigationRepeatDirY{0} +{ + addChild(&mBackground); + addChild(&mGrid); + + mTitle = std::make_shared(mWindow, Utils::String::toUpper(title), + Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); + + std::vector> kbLayout; + + // At the moment there is only the US keyboard layout available. + kbLayout.insert(kbLayout.cend(), kbBaseUS.cbegin(), kbBaseUS.cend()); + + // In complex mode, the last row of the keyboard contains an additional "LOAD" button. + if (mComplexMode) + kbLayout.insert(kbLayout.cend(), kbLastRowLoad.cbegin(), kbLastRowLoad.cend()); + else + kbLayout.insert(kbLayout.cend(), kbLastRowNormal.cbegin(), kbLastRowNormal.cend()); + + mHorizontalKeyCount = static_cast(kbLayout[0].size()); + + mKeyboardGrid = std::make_shared( + mWindow, glm::ivec2(mHorizontalKeyCount, static_cast(kbLayout.size()) / 3)); + + mText = std::make_shared(mWindow); + mText->setValue(initValue); + + if (!multiLine) + mText->setCursor(initValue.size()); + + // Header. + mGrid.setEntry(mTitle, glm::ivec2{0, 0}, false, true); + + int yPos = 1; + + if (mComplexMode) { + mInfoString = std::make_shared( + mWindow, infoString, Font::get(FONT_SIZE_MEDIUM), 0x555555FF, ALIGN_CENTER); + mGrid.setEntry(mInfoString, glm::ivec2{0, yPos}, false, true); + + mDefaultValue = std::make_shared( + mWindow, defaultValue, Font::get(FONT_SIZE_SMALL), 0x555555FF, ALIGN_CENTER); + mGrid.setEntry(mDefaultValue, glm::ivec2{0, yPos + 1}, false, true); + yPos += 2; + } + + // Text edit field. + mGrid.setEntry(mText, glm::ivec2{0, yPos}, true, false, glm::ivec2{1, 1}, + GridFlags::BORDER_TOP); + + std::vector>> buttonList; + + // Create keyboard. + for (int i = 0; i < static_cast(kbLayout.size()) / 4; i++) { + std::vector> buttons; + + for (int j = 0; j < static_cast(kbLayout[i].size()); j++) { + std::string lower = kbLayout[4 * i][j]; + if (lower.empty() || lower == "-rowspan-" || lower == "-colspan-") + continue; + + std::string upper = kbLayout[4 * i + 1][j]; + std::string alted = kbLayout[4 * i + 2][j]; + std::string altshifted = kbLayout[4 * i + 3][j]; + + std::shared_ptr button = nullptr; + + if (lower == "DEL") { + lower = DELETE_SYMBOL; + upper = DELETE_SYMBOL; + alted = DELETE_SYMBOL; + altshifted = DELETE_SYMBOL; + } + else if (lower == "OK") { + lower = OK_SYMBOL; + upper = OK_SYMBOL; + alted = OK_SYMBOL; + altshifted = OK_SYMBOL; + } + else if (lower == "SPACE") { + lower = " "; + upper = " "; + alted = " "; + altshifted = " "; + } + else if (lower != "SHIFT" && lower.length() > 1) { + lower = (lower.c_str()); + upper = (upper.c_str()); + alted = (alted.c_str()); + altshifted = (altshifted.c_str()); + } + + if (lower == "SHIFT") { + mShiftButton = std::make_shared( + mWindow, (SHIFT_SYMBOL), ("SHIFT"), [this] { shiftKeys(); }, false, true); + button = mShiftButton; + } + else if (lower == "ALT") { + mAltButton = std::make_shared( + mWindow, (ALT_SYMBOL), ("ALT"), [this] { altKeys(); }, false, true); + button = mAltButton; + } + else { + button = makeButton(lower, upper, alted, altshifted); + } + + button->setPadding( + glm::vec4(BUTTON_GRID_HORIZ_PADDING / 4.0f, BUTTON_GRID_HORIZ_PADDING / 4.0f, + BUTTON_GRID_HORIZ_PADDING / 4.0f, BUTTON_GRID_HORIZ_PADDING / 4.0f)); + buttons.push_back(button); + + int colSpan = 1; + for (int cs = j + 1; cs < static_cast(kbLayout[i].size()); cs++) { + if (std::string(kbLayout[4 * i][cs]) == "-colspan-") + colSpan++; + else + break; + } + + int rowSpan = 1; + for (int cs = (4 * i) + 4; cs < static_cast(kbLayout.size()); cs += 4) { + if (std::string(kbLayout[cs][j]) == "-rowspan-") + rowSpan++; + else + break; + } + + mKeyboardGrid->setEntry(button, glm::ivec2{j, i}, true, true, + glm::ivec2{colSpan, rowSpan}); + + buttonList.push_back(buttons); + } + } + + mGrid.setEntry(mKeyboardGrid, glm::ivec2{0, yPos + 1}, true, true, glm::ivec2{2, 4}); + + float textHeight = mText->getFont()->getHeight(); + // If the multiLine option has been set, then include three lines of text on screen. + if (multiLine) { + textHeight *= 3.0f; + textHeight += 2.0f * Renderer::getScreenHeightModifier(); + } + + mText->setSize(0.0f, textHeight); + + // If attempting to navigate beyond the edge of the keyboard grid, then wrap around. + mGrid.setPastBoundaryCallback([this, kbLayout](InputConfig* config, Input input) -> bool { + if (config->isMappedLike("left", input)) { + if (mGrid.getSelectedComponent() == mKeyboardGrid) { + mKeyboardGrid->moveCursorTo(mHorizontalKeyCount - 1, -1, true); + return true; + } + } + else if (config->isMappedLike("right", input)) { + if (mGrid.getSelectedComponent() == mKeyboardGrid) { + mKeyboardGrid->moveCursorTo(0, -1); + return true; + } + } + return false; + }); + + // Adapt width to the geometry of the display. The 1.778 aspect ratio is the 16:9 reference. + float aspectValue = 1.778f / Renderer::getScreenAspectRatio(); + float width = glm::clamp(0.78f * aspectValue, 0.35f, 0.90f) * Renderer::getScreenWidth(); + + // The combination of multiLine and complex mode is not supported as there is currently + // no need for that. + if (mMultiLine) { + setSize(width, KEYBOARD_HEIGHT + textHeight - mText->getFont()->getHeight()); + + setPosition((static_cast(Renderer::getScreenWidth()) - mSize.x) / 2.0f, + (static_cast(Renderer::getScreenHeight()) - mSize.y) / 2.0f); + } + else { + if (mComplexMode) + setSize(width, KEYBOARD_HEIGHT + mDefaultValue->getSize().y * 3.0f); + else + setSize(width, KEYBOARD_HEIGHT); + + setPosition((static_cast(Renderer::getScreenWidth()) - mSize.x) / 2.0f, + (static_cast(Renderer::getScreenHeight()) - mSize.y) / 2.0f); + } +} + +void GuiTextEditKeyboardPopup::onSizeChanged() +{ + mBackground.fitTo(mSize, glm::vec3{}, glm::vec2{-32.0f, -32.0f}); + mText->setSize(mSize.x - KEYBOARD_PADDINGX - KEYBOARD_PADDINGX, mText->getSize().y); + + // Update grid. + mGrid.setRowHeightPerc(0, mTitle->getFont()->getHeight() / mSize.y); + + if (mInfoString && mDefaultValue) { + mGrid.setRowHeightPerc(1, (mInfoString->getSize().y * 0.6f) / mSize.y); + mGrid.setRowHeightPerc(2, (mDefaultValue->getSize().y * 1.6f) / mSize.y); + mGrid.setRowHeightPerc(1, (mText->getSize().y * 1.0f) / mSize.y); + } + else if (mMultiLine) { + mGrid.setRowHeightPerc(1, (mText->getSize().y * 1.15f) / mSize.y); + } + + mGrid.setSize(mSize); + + auto pos = mKeyboardGrid->getPosition(); + auto sz = mKeyboardGrid->getSize(); + + // Add a small margin between buttons. + mKeyboardGrid->setSize(mSize.x - KEYBOARD_PADDINGX - KEYBOARD_PADDINGX, + sz.y - KEYBOARD_PADDINGY + 70.0f * Renderer::getScreenHeightModifier()); + mKeyboardGrid->setPosition(KEYBOARD_PADDINGX, pos.y); +} + +bool GuiTextEditKeyboardPopup::input(InputConfig* config, Input input) +{ + // Enter/return key or numpad enter key accepts the changes. + if (config->getDeviceId() == DEVICE_KEYBOARD && mText->isEditing() && !mMultiLine && + input.value && (input.id == SDLK_RETURN || input.id == SDLK_KP_ENTER)) { + this->mOkCallback(mText->getValue()); + delete this; + return true; + } + // Dito for the A button if using a controller. + else if (config->getDeviceId() != DEVICE_KEYBOARD && mText->isEditing() && + config->isMappedTo("a", input) && input.value) { + this->mOkCallback(mText->getValue()); + delete this; + return true; + } + + // If the keyboard has been configured with backspace as the back button (which is the default + // configuration) then ignore this key if we're currently editing or otherwise it would be + // impossible to erase characters using this key. + bool keyboardBackspace = (config->getDeviceId() == DEVICE_KEYBOARD && mText->isEditing() && + input.id == SDLK_BACKSPACE); + + // Pressing back (or the escape key if using keyboard input) closes us. + if ((config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_ESCAPE) || + (!keyboardBackspace && config->isMappedTo("b", input)) && input.value) { + if (mText->getValue() != mInitValue) { + // Changes were made, ask if the user wants to save them. + mWindow->pushGui(new GuiMsgBox( + mWindow, mHelpStyle, mSaveConfirmationText, "YES", + [this] { + this->mOkCallback(mText->getValue()); + delete this; + return true; + }, + "NO", + [this] { + delete this; + return false; + })); + } + else { + delete this; + } + } + + if (mText->isEditing() && config->isMappedLike("down", input) && input.value) { + mText->stopEditing(); + mGrid.setCursorTo(mGrid.getSelectedComponent()); + } + + // Left trigger button outside text editing field toggles Shift key. + if (!mText->isEditing() && config->isMappedLike("lefttrigger", input) && input.value) + shiftKeys(); + + // Right trigger button outside text editing field toggles Alt key. + if (!mText->isEditing() && config->isMappedLike("righttrigger", input) && input.value) + altKeys(); + + // Left shoulder button deletes a character (backspace). + if (config->isMappedTo("leftshoulder", input)) { + if (input.value) { + mDeleteRepeat = true; + mDeleteRepeatTimer = -(DELETE_REPEAT_START_DELAY - DELETE_REPEAT_SPEED); + + bool editing = mText->isEditing(); + if (!editing) + mText->startEditing(); + + mText->textInput("\b"); + + if (!editing) + mText->stopEditing(); + } + else { + mDeleteRepeat = false; + } + return true; + } + + // Right shoulder button inserts a blank space. + if (config->isMappedTo("rightshoulder", input) && input.value) { + bool editing = mText->isEditing(); + if (!editing) + mText->startEditing(); + + mText->textInput(" "); + + if (!editing) + mText->stopEditing(); + + return true; + } + + // Actual navigation of the keyboard grid is done in ComponentGrid, this code only handles + // key repeat while holding the left/right/up/down buttons. + if (!mText->isEditing() && config->isMappedLike("left", input)) { + if (input.value) { + mNavigationRepeatDirX = -1; + mNavigationRepeatTimer = -(NAVIGATION_REPEAT_START_DELAY - NAVIGATION_REPEAT_SPEED); + } + else { + mNavigationRepeatDirX = 0; + } + } + + if (!mText->isEditing() && config->isMappedLike("right", input)) { + if (input.value) { + mNavigationRepeatDirX = 1; + mNavigationRepeatTimer = -(NAVIGATION_REPEAT_START_DELAY - NAVIGATION_REPEAT_SPEED); + } + else { + mNavigationRepeatDirX = 0; + } + } + + if (!mText->isEditing() && config->isMappedLike("up", input)) { + if (input.value) { + mNavigationRepeatDirY = -1; + mNavigationRepeatTimer = -(NAVIGATION_REPEAT_START_DELAY - NAVIGATION_REPEAT_SPEED); + } + else { + mNavigationRepeatDirY = 0; + } + } + + if (!mText->isEditing() && config->isMappedLike("down", input)) { + if (input.value) { + mNavigationRepeatDirY = 1; + mNavigationRepeatTimer = -(NAVIGATION_REPEAT_START_DELAY - NAVIGATION_REPEAT_SPEED); + } + else { + mNavigationRepeatDirY = 0; + } + } + + if (GuiComponent::input(config, input)) + return true; + + return false; +} + +void GuiTextEditKeyboardPopup::update(int deltaTime) +{ + updateNavigationRepeat(deltaTime); + updateDeleteRepeat(deltaTime); + GuiComponent::update(deltaTime); +} + +std::vector GuiTextEditKeyboardPopup::getHelpPrompts() +{ + std::vector prompts = mGrid.getHelpPrompts(); + + if (!mText->isEditing()) { + prompts.push_back(HelpPrompt("lt", "shift")); + prompts.push_back(HelpPrompt("rt", "alt")); + } + else { + prompts.push_back(HelpPrompt("a", mAcceptBtnHelpText)); + } + + prompts.push_back(HelpPrompt("l", "backspace")); + prompts.push_back(HelpPrompt("r", "space")); + prompts.push_back(HelpPrompt("b", "back")); + + if (prompts.size() > 0 && prompts.front().second == OK_SYMBOL) + prompts.front().second = mAcceptBtnHelpText; + + if (prompts.size() > 0 && prompts.front().second == " ") + prompts.front().second = "SPACE"; + + if (prompts.size() > 0 && prompts.front().second == "CLEAR") + prompts.front().second = mClearBtnHelpText; + + if (prompts.size() > 0 && prompts.front().second == "LOAD") + prompts.front().second = mLoadBtnHelpText; + + if (prompts.size() > 0 && prompts.front().second == "CANCEL") + prompts.front().second = mCancelBtnHelpText; + + // If a prompt has no value set, then remove it. + if (prompts.size() > 0 && prompts.front().second == "") + prompts.erase(prompts.begin(), prompts.begin() + 1); + + return prompts; +} + +void GuiTextEditKeyboardPopup::updateDeleteRepeat(int deltaTime) +{ + if (!mDeleteRepeat) + return; + + mDeleteRepeatTimer += deltaTime; + + while (mDeleteRepeatTimer >= DELETE_REPEAT_SPEED) { + bool editing = mText->isEditing(); + if (!editing) + mText->startEditing(); + + mText->textInput("\b"); + + if (!editing) + mText->stopEditing(); + + mDeleteRepeatTimer -= DELETE_REPEAT_SPEED; + } +} + +void GuiTextEditKeyboardPopup::updateNavigationRepeat(int deltaTime) +{ + if (mNavigationRepeatDirX == 0 && mNavigationRepeatDirY == 0) + return; + + mNavigationRepeatTimer += deltaTime; + + while (mNavigationRepeatTimer >= NAVIGATION_REPEAT_SPEED) { + + if (mNavigationRepeatDirX != 0) { + mKeyboardGrid.get()->moveCursor({mNavigationRepeatDirX, 0}); + // If replacing the line above with this code, the keyboard will wrap around the + // edges also when key repeat is active. + // if (!mKeyboardGrid.get()->moveCursor({mNavigationRepeatDirX, 0})) { + // if (mNavigationRepeatDirX < 0) + // mKeyboardGrid->moveCursorTo(mHorizontalKeyCount - 1, -1); + // else + // mKeyboardGrid->moveCursorTo(0, -1); + // } + } + + if (mNavigationRepeatDirY != 0) + mKeyboardGrid.get()->moveCursor({0, mNavigationRepeatDirY}); + + mNavigationRepeatTimer -= NAVIGATION_REPEAT_SPEED; + } +} + +void GuiTextEditKeyboardPopup::shiftKeys() +{ + mShift = !mShift; + + if (mShift) { + mShiftButton->setFlatColorFocused(0xFF2222FF); + mShiftButton->setFlatColorUnfocused(0xFF2222FF); + } + else { + mShiftButton->setFlatColorFocused(0x878787FF); + mShiftButton->setFlatColorUnfocused(0x60606025); + } + + if (mAlt && mShift) { + altShiftKeys(); + return; + } + + // This only happens when Alt was deselected while both Shift and Alt were active. + if (mAlt) { + altKeys(); + altKeys(); + } + else { + for (auto& kb : mKeyboardButtons) { + const std::string& text = mShift ? kb.shiftedKey : kb.key; + auto sz = kb.button->getSize(); + kb.button->setText(text, text, false); + kb.button->setSize(sz); + } + } +} + +void GuiTextEditKeyboardPopup::altKeys() +{ + mAlt = !mAlt; + + if (mAlt) { + mAltButton->setFlatColorFocused(0xFF2222FF); + mAltButton->setFlatColorUnfocused(0xFF2222FF); + } + else { + mAltButton->setFlatColorFocused(0x878787FF); + mAltButton->setFlatColorUnfocused(0x60606025); + } + + if (mShift && mAlt) { + altShiftKeys(); + return; + } + + // This only happens when Shift was deselected while both Shift and Alt were active. + if (mShift) { + shiftKeys(); + shiftKeys(); + } + else { + for (auto& kb : mKeyboardButtons) { + const std::string& text = mAlt ? kb.altedKey : kb.key; + auto sz = kb.button->getSize(); + kb.button->setText(text, text, false); + kb.button->setSize(sz); + } + } +} + +void GuiTextEditKeyboardPopup::altShiftKeys() +{ + for (auto& kb : mKeyboardButtons) { + const std::string& text = kb.altshiftedKey; + auto sz = kb.button->getSize(); + kb.button->setText(text, text, false); + kb.button->setSize(sz); + } +} + +std::shared_ptr GuiTextEditKeyboardPopup::makeButton( + const std::string& key, + const std::string& shiftedKey, + const std::string& altedKey, + const std::string& altshiftedKey) +{ + std::shared_ptr button = std::make_shared( + mWindow, key, key, + [this, key, shiftedKey, altedKey, altshiftedKey] { + if (key == (OK_SYMBOL) || key.find("OK") != std::string::npos) { + mOkCallback(mText->getValue()); + delete this; + return; + } + else if (key == (DELETE_SYMBOL) || key == "DEL") { + mText->startEditing(); + mText->textInput("\b"); + mText->stopEditing(); + return; + } + else if (key == "SPACE" || key == " ") { + mText->startEditing(); + mText->textInput(" "); + mText->stopEditing(); + return; + } + else if (key == "LOAD") { + mText->setValue(mDefaultValue->getValue()); + mText->setCursor(mDefaultValue->getValue().size()); + return; + } + else if (key == "CLEAR") { + mText->setValue(""); + return; + } + else if (key == "CANCEL") { + delete this; + return; + } + + if (mAlt && altedKey.empty()) + return; + + mText->startEditing(); + + if (mShift && mAlt) + mText->textInput(altshiftedKey.c_str()); + else if (mAlt) + mText->textInput(altedKey.c_str()); + else if (mShift) + mText->textInput(shiftedKey.c_str()); + else + mText->textInput(key.c_str()); + + mText->stopEditing(); + }, + false, true); + + KeyboardButton kb(button, key, shiftedKey, altedKey, altshiftedKey); + mKeyboardButtons.push_back(kb); + return button; +} diff --git a/es-core/src/guis/GuiTextEditKeyboardPopup.h b/es-core/src/guis/GuiTextEditKeyboardPopup.h new file mode 100644 index 000000000..1429a4292 --- /dev/null +++ b/es-core/src/guis/GuiTextEditKeyboardPopup.h @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// GuiTextEditKeyboardPopup.h +// +// Text edit popup with a virtual keyboard. +// Has a default mode and a complex mode, both with various options passed as arguments. +// + +#ifndef ES_CORE_GUIS_GUI_TEXT_EDIT_KEYBOARD_POPUP_H +#define ES_CORE_GUIS_GUI_TEXT_EDIT_KEYBOARD_POPUP_H + +#include "GuiComponent.h" +#include "components/ButtonComponent.h" +#include "components/ComponentGrid.h" +#include "components/TextEditComponent.h" + +class GuiTextEditKeyboardPopup : public GuiComponent +{ +public: + GuiTextEditKeyboardPopup(Window* window, + const HelpStyle& helpstyle, + const std::string& title, + const std::string& initValue, + const std::function& okCallback, + bool multiLine, + const std::string& acceptBtnHelpText = "OK", + const std::string& saveConfirmationText = "SAVE CHANGES?", + const std::string& infoString = "", + const std::string& defaultValue = "", + const std::string& loadBtnHelpText = "LOAD DEFAULT", + const std::string& clearBtnHelpText = "CLEAR", + const std::string& cancelBtnHelpText = "DISCARD CHANGES"); + + void onSizeChanged(); + bool input(InputConfig* config, Input input); + void update(int deltaTime) override; + + std::vector getHelpPrompts() override; + HelpStyle getHelpStyle() override { return mHelpStyle; } + +private: + class KeyboardButton + { + public: + std::shared_ptr button; + const std::string key; + const std::string shiftedKey; + const std::string altedKey; + const std::string altshiftedKey; + KeyboardButton(const std::shared_ptr b, + const std::string& k, + const std::string& sk, + const std::string& ak, + const std::string& ask) + : button{b} + , key{k} + , shiftedKey{sk} + , altedKey{ak} + , altshiftedKey{ask} {}; + }; + + void updateDeleteRepeat(int deltaTime); + void updateNavigationRepeat(int deltaTime); + + void shiftKeys(); + void altKeys(); + void altShiftKeys(); + + std::shared_ptr makeButton(const std::string& key, + const std::string& shiftedKey, + const std::string& altedKey, + const std::string& altshiftedKey); + std::vector mKeyboardButtons; + + std::shared_ptr mShiftButton; + std::shared_ptr mAltButton; + + NinePatchComponent mBackground; + ComponentGrid mGrid; + HelpStyle mHelpStyle; + + std::shared_ptr mTitle; + std::shared_ptr mInfoString; + std::shared_ptr mDefaultValue; + std::shared_ptr mText; + std::shared_ptr mKeyboardGrid; + + std::string mInitValue; + std::string mAcceptBtnHelpText; + std::string mSaveConfirmationText; + std::string mLoadBtnHelpText; + std::string mClearBtnHelpText; + std::string mCancelBtnHelpText; + + std::function mOkCallback; + + bool mMultiLine; + bool mComplexMode; + bool mDeleteRepeat; + bool mShift; + bool mAlt; + bool mAltShift; + + int mHorizontalKeyCount; + int mDeleteRepeatTimer; + int mNavigationRepeatTimer; + int mNavigationRepeatDirX; + int mNavigationRepeatDirY; +}; + +#endif // ES_CORE_GUIS_GUI_TEXT_EDIT_KEYBOARD_POPUP_H diff --git a/es-core/src/guis/GuiTextEditPopup.cpp b/es-core/src/guis/GuiTextEditPopup.cpp index d5f832a7c..c84f7cd27 100644 --- a/es-core/src/guis/GuiTextEditPopup.cpp +++ b/es-core/src/guis/GuiTextEditPopup.cpp @@ -3,15 +3,16 @@ // EmulationStation Desktop Edition // GuiTextEditPopup.cpp // -// Simple text edit popup with a title, a text input box and OK and Cancel buttons. +// Text edit popup. +// Has a default mode and a complex mode, both with various options passed as arguments. // +#define DELETE_REPEAT_START_DELAY 600 +#define DELETE_REPEAT_SPEED 90 // Lower is faster. + #include "guis/GuiTextEditPopup.h" -#include "Window.h" -#include "components/ButtonComponent.h" #include "components/MenuComponent.h" -#include "components/TextEditComponent.h" #include "guis/GuiMsgBox.h" GuiTextEditPopup::GuiTextEditPopup(Window* window, @@ -21,15 +22,27 @@ GuiTextEditPopup::GuiTextEditPopup(Window* window, const std::function& okCallback, bool multiLine, const std::string& acceptBtnText, - const std::string& saveConfirmationText) - : GuiComponent(window) - , mHelpStyle(helpstyle) - , mBackground(window, ":/graphics/frame.svg") - , mGrid(window, glm::ivec2{1, 3}) - , mMultiLine(multiLine) - , mInitValue(initValue) - , mOkCallback(okCallback) - , mSaveConfirmationText(saveConfirmationText) + const std::string& saveConfirmationText, + const std::string& infoString, + const std::string& defaultValue, + const std::string& loadBtnHelpText, + const std::string& clearBtnHelpText, + const std::string& cancelBtnHelpText) + : GuiComponent{window} + , mHelpStyle{helpstyle} + , mInitValue{initValue} + , mOkCallback{okCallback} + , mMultiLine{multiLine} + , mAcceptBtnText{acceptBtnText} + , mSaveConfirmationText{saveConfirmationText} + , mLoadBtnHelpText{loadBtnHelpText} + , mClearBtnHelpText{clearBtnHelpText} + , mCancelBtnHelpText{cancelBtnHelpText} + , mBackground{window, ":/graphics/frame.svg"} + , mGrid{window, glm::ivec2{1, (infoString != "" && defaultValue != "" ? 5 : 3)}} + , mComplexMode{(infoString != "" && defaultValue != "")} + , mDeleteRepeat{false} + , mDeleteRepeatTimer{0} { addChild(&mBackground); addChild(&mGrid); @@ -37,6 +50,13 @@ GuiTextEditPopup::GuiTextEditPopup(Window* window, mTitle = std::make_shared(mWindow, Utils::String::toUpper(title), Font::get(FONT_SIZE_MEDIUM), 0x555555FF, ALIGN_CENTER); + if (mComplexMode) { + mInfoString = std::make_shared( + mWindow, infoString, Font::get(FONT_SIZE_SMALL), 0x555555FF, ALIGN_CENTER); + mDefaultValue = std::make_shared( + mWindow, defaultValue, Font::get(FONT_SIZE_SMALL), 0x555555FF, ALIGN_CENTER); + } + mText = std::make_shared(mWindow); mText->setValue(initValue); @@ -46,55 +66,116 @@ GuiTextEditPopup::GuiTextEditPopup(Window* window, okCallback(mText->getValue()); delete this; })); - buttons.push_back(std::make_shared(mWindow, "CLEAR", "clear", + if (mComplexMode) { + buttons.push_back(std::make_shared( + mWindow, "load", loadBtnHelpText, [this, defaultValue] { + mText->setValue(defaultValue); + mText->setCursor(0); + mText->setCursor(defaultValue.size()); + })); + } + + buttons.push_back(std::make_shared(mWindow, "clear", clearBtnHelpText, [this] { mText->setValue(""); })); + buttons.push_back(std::make_shared(mWindow, "CANCEL", "discard changes", [this] { delete this; })); mButtonGrid = makeButtonGrid(mWindow, buttons); mGrid.setEntry(mTitle, glm::ivec2{0, 0}, false, true); - mGrid.setEntry(mText, glm::ivec2{0, 1}, true, false, glm::ivec2{1, 1}, + + int yPos = 1; + + if (mComplexMode) { + mGrid.setEntry(mInfoString, glm::ivec2{0, yPos}, false, true); + mGrid.setEntry(mDefaultValue, glm::ivec2{0, yPos + 1}, false, false); + yPos += 2; + } + + mGrid.setEntry(mText, glm::ivec2{0, yPos}, true, false, glm::ivec2{1, 1}, GridFlags::BORDER_TOP | GridFlags::BORDER_BOTTOM); - mGrid.setEntry(mButtonGrid, glm::ivec2{0, 2}, true, false); + mGrid.setEntry(mButtonGrid, glm::ivec2{0, yPos + 1}, true, false); float textHeight = mText->getFont()->getHeight(); if (multiLine) textHeight *= 6.0f; + mText->setSize(0, textHeight); - // Adjust the width relative to the aspect ratio of the screen to make the GUI look coherent - // regardless of screen type. The 1.778 aspect ratio value is the 16:9 reference. + // Adapt width to the geometry of the display. The 1.778 aspect ratio is the 16:9 reference. float aspectValue = 1.778f / Renderer::getScreenAspectRatio(); - float width = glm::clamp(0.50f * aspectValue, 0.40f, 0.70f) * Renderer::getScreenWidth(); - setSize(width, mTitle->getFont()->getHeight() + textHeight + mButtonGrid->getSize().y + - mButtonGrid->getSize().y / 2.0f); - setPosition((Renderer::getScreenWidth() - mSize.x) / 2.0f, - (Renderer::getScreenHeight() - mSize.y) / 2.0f); + if (mComplexMode) { + float infoWidth = + glm::clamp(0.70f * aspectValue, 0.34f, 0.85f) * Renderer::getScreenWidth(); + float windowWidth = + glm::clamp(0.75f * aspectValue, 0.40f, 0.90f) * Renderer::getScreenWidth(); + + mDefaultValue->setSize(infoWidth, mDefaultValue->getFont()->getHeight()); + + setSize(windowWidth, mTitle->getFont()->getHeight() + textHeight + + mButtonGrid->getSize().y + mButtonGrid->getSize().y * 1.85f); + setPosition((Renderer::getScreenWidth() - mSize.x) / 2.0f, + (Renderer::getScreenHeight() - mSize.y) / 2.0f); + } + else { + float width = glm::clamp(0.54f * aspectValue, 0.20f, 0.70f) * Renderer::getScreenWidth(); + + setSize(width, mTitle->getFont()->getHeight() + textHeight + mButtonGrid->getSize().y + + mButtonGrid->getSize().y / 2.0f); + setPosition((Renderer::getScreenWidth() - mSize.x) / 2.0f, + (Renderer::getScreenHeight() - mSize.y) / 2.0f); + } + + if (!multiLine) + mText->setCursor(initValue.size()); + mText->startEditing(); } void GuiTextEditPopup::onSizeChanged() { mBackground.fitTo(mSize, glm::vec3{}, glm::vec2{-32.0f, -32.0f}); - - mText->setSize(mSize.x - 40.0f, mText->getSize().y); + mText->setSize(mSize.x - 40.0f * Renderer::getScreenHeightModifier(), mText->getSize().y); // Update grid. mGrid.setRowHeightPerc(0, mTitle->getFont()->getHeight() / mSize.y); + + if (mComplexMode) + mGrid.setRowHeightPerc(1, 0.15f); + mGrid.setRowHeightPerc(2, mButtonGrid->getSize().y / mSize.y); mGrid.setSize(mSize); } bool GuiTextEditPopup::input(InputConfig* config, Input input) { - if (GuiComponent::input(config, input)) + // Enter key (main key or via numpad) accepts the changes. + if (config->getDeviceId() == DEVICE_KEYBOARD && mText->isEditing() && !mMultiLine && + input.value && (input.id == SDLK_RETURN || input.id == SDLK_KP_ENTER)) { + this->mOkCallback(mText->getValue()); + delete this; return true; + } + // Dito for the A button if using a controller. + else if (config->getDeviceId() != DEVICE_KEYBOARD && mText->isEditing() && + config->isMappedTo("a", input) && input.value) { + this->mOkCallback(mText->getValue()); + delete this; + return true; + } - // Pressing back when not text editing closes us. - if (config->isMappedTo("b", input) && input.value) { + // If the keyboard has been configured with backspace as the back button (which is the default + // configuration) then ignore this key if we're currently editing or otherwise it would be + // impossible to erase characters using this key. + bool keyboardBackspace = (config->getDeviceId() == DEVICE_KEYBOARD && mText->isEditing() && + input.id == SDLK_BACKSPACE); + + // Pressing back (or the escape key if using keyboard input) closes us. + if ((config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_ESCAPE) || + (!keyboardBackspace && config->isMappedTo("b", input)) && input.value) { if (mText->getValue() != mInitValue) { // Changes were made, ask if the user wants to save them. mWindow->pushGui(new GuiMsgBox( @@ -114,12 +195,89 @@ bool GuiTextEditPopup::input(InputConfig* config, Input input) delete this; } } + + if (mText->isEditing() && config->isMappedLike("down", input) && input.value) { + mText->stopEditing(); + mGrid.setCursorTo(mGrid.getSelectedComponent()); + } + + // Left shoulder button deletes a character (backspace). + if (config->isMappedTo("leftshoulder", input)) { + if (input.value) { + mDeleteRepeat = true; + mDeleteRepeatTimer = -(DELETE_REPEAT_START_DELAY - DELETE_REPEAT_SPEED); + + bool editing = mText->isEditing(); + if (!editing) + mText->startEditing(); + + mText->textInput("\b"); + + if (!editing) + mText->stopEditing(); + } + else { + mDeleteRepeat = false; + } + return true; + } + + // Right shoulder button inserts a blank space. + if (config->isMappedTo("rightshoulder", input) && input.value) { + bool editing = mText->isEditing(); + if (!editing) + mText->startEditing(); + + mText->textInput(" "); + + if (!editing) + mText->stopEditing(); + + return true; + } + + if (GuiComponent::input(config, input)) + return true; + return false; } +void GuiTextEditPopup::update(int deltaTime) +{ + updateDeleteRepeat(deltaTime); + GuiComponent::update(deltaTime); +} + std::vector GuiTextEditPopup::getHelpPrompts() { std::vector prompts = mGrid.getHelpPrompts(); + + if (mText->isEditing()) + prompts.push_back(HelpPrompt("a", mAcceptBtnText)); + + prompts.push_back(HelpPrompt("l", "backspace")); + prompts.push_back(HelpPrompt("r", "space")); prompts.push_back(HelpPrompt("b", "back")); return prompts; } + +void GuiTextEditPopup::updateDeleteRepeat(int deltaTime) +{ + if (!mDeleteRepeat) + return; + + mDeleteRepeatTimer += deltaTime; + + while (mDeleteRepeatTimer >= DELETE_REPEAT_SPEED) { + bool editing = mText->isEditing(); + if (!editing) + mText->startEditing(); + + mText->textInput("\b"); + + if (!editing) + mText->stopEditing(); + + mDeleteRepeatTimer -= DELETE_REPEAT_SPEED; + } +} diff --git a/es-core/src/guis/GuiTextEditPopup.h b/es-core/src/guis/GuiTextEditPopup.h index fe07b53a4..a77623704 100644 --- a/es-core/src/guis/GuiTextEditPopup.h +++ b/es-core/src/guis/GuiTextEditPopup.h @@ -3,18 +3,17 @@ // EmulationStation Desktop Edition // GuiTextEditPopup.h // -// Simple text edit popup with a title, a text input box and OK and Cancel buttons. +// Text edit popup. +// Has a default mode and a complex mode, both with various options passed as arguments. // #ifndef ES_CORE_GUIS_GUI_TEXT_EDIT_POPUP_H #define ES_CORE_GUIS_GUI_TEXT_EDIT_POPUP_H #include "GuiComponent.h" +#include "components/ButtonComponent.h" #include "components/ComponentGrid.h" -#include "components/NinePatchComponent.h" - -class TextComponent; -class TextEditComponent; +#include "components/TextEditComponent.h" class GuiTextEditPopup : public GuiComponent { @@ -26,27 +25,47 @@ public: const std::function& okCallback, bool multiLine, const std::string& acceptBtnText = "OK", - const std::string& saveConfirmationText = "SAVE CHANGES?"); + const std::string& saveConfirmationText = "SAVE CHANGES?", + const std::string& infoString = "", + const std::string& defaultValue = "", + const std::string& loadBtnHelpText = "LOAD DEFAULT", + const std::string& clearBtnHelpText = "CLEAR", + const std::string& cancelBtnHelpText = "DISCARD CHANGES"); - bool input(InputConfig* config, Input input) override; void onSizeChanged() override; + bool input(InputConfig* config, Input input) override; + void update(int deltaTime) override; std::vector getHelpPrompts() override; HelpStyle getHelpStyle() override { return mHelpStyle; } private: + void updateDeleteRepeat(int deltaTime); + NinePatchComponent mBackground; ComponentGrid mGrid; + HelpStyle mHelpStyle; std::shared_ptr mTitle; + std::shared_ptr mInfoString; + std::shared_ptr mDefaultValue; std::shared_ptr mText; std::shared_ptr mButtonGrid; - HelpStyle mHelpStyle; - bool mMultiLine; std::string mInitValue; - std::function mOkCallback; + std::string mAcceptBtnText; std::string mSaveConfirmationText; + std::string mLoadBtnHelpText; + std::string mClearBtnHelpText; + std::string mCancelBtnHelpText; + + std::function mOkCallback; + + bool mMultiLine; + bool mComplexMode; + bool mDeleteRepeat; + + int mDeleteRepeatTimer; }; #endif // ES_CORE_GUIS_GUI_TEXT_EDIT_POPUP_H