Added a virtual keyboard.

This commit is contained in:
Leon Styhre 2021-09-17 22:23:41 +02:00
parent bbaf2739d4
commit c4e6d3cac1
16 changed files with 1182 additions and 308 deletions

View file

@ -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.

View file

@ -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);

View file

@ -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<SwitchComponent>(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<SwitchComponent>(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);

View file

@ -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;
}
}

View file

@ -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,

View file

@ -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);

View file

@ -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] {

View file

@ -15,7 +15,6 @@
#include "FileData.h"
#include "GuiComponent.h"
#include "guis/GuiComplexTextEditPopup.h"
#include "guis/GuiMsgBox.h"
#include "renderers/Renderer.h"

View file

@ -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

View file

@ -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};

View file

@ -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<void(const std::string&)>& 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<TextComponent>(mWindow, Utils::String::toUpper(title),
Font::get(FONT_SIZE_MEDIUM), 0x555555FF, ALIGN_CENTER);
mInfoString1 = std::make_shared<TextComponent>(mWindow, infoString1, Font::get(FONT_SIZE_SMALL),
0x555555FF, ALIGN_CENTER);
mInfoString2 = std::make_shared<TextComponent>(mWindow, infoString2, Font::get(FONT_SIZE_SMALL),
0x555555FF, ALIGN_CENTER);
mText = std::make_shared<TextEditComponent>(mWindow);
mText->setValue(initValue);
std::vector<std::shared_ptr<ButtonComponent>> buttons;
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, acceptBtnText, acceptBtnText,
[this, okCallback] {
okCallback(mText->getValue());
delete this;
}));
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, loadBtnText, loadBtnHelpText,
[this, infoString2] {
mText->setValue(infoString2);
mText->setCursor(0);
mText->setCursor(infoString2.size());
}));
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, clearBtnText, clearBtnHelpText,
[this] { mText->setValue(""); }));
if (!mHideCancelButton)
buttons.push_back(std::make_shared<ButtonComponent>(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<HelpPrompt> GuiComplexTextEditPopup::getHelpPrompts()
{
std::vector<HelpPrompt> prompts = mGrid.getHelpPrompts();
if (!mHideCancelButton)
prompts.push_back(HelpPrompt("b", "back"));
return prompts;
}

View file

@ -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<void(const std::string&)>& 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<HelpPrompt> getHelpPrompts() override;
HelpStyle getHelpStyle() override { return mHelpStyle; }
private:
NinePatchComponent mBackground;
ComponentGrid mGrid;
std::shared_ptr<TextComponent> mTitle;
std::shared_ptr<TextComponent> mInfoString1;
std::shared_ptr<TextComponent> mInfoString2;
std::shared_ptr<TextEditComponent> mText;
std::shared_ptr<ComponentGrid> mButtonGrid;
HelpStyle mHelpStyle;
bool mMultiLine;
bool mHideCancelButton;
std::string mInitValue;
std::function<void(const std::string&)> mOkCallback;
std::string mSaveConfirmationText;
};
#endif // ES_CORE_GUIS_GUI_COMPLEX_TEXT_EDIT_POPUP_H

View file

@ -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<std::vector<const char*>> 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<std::vector<const char*>> 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<std::vector<const char*>> 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<void(const std::string&)>& 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<TextComponent>(mWindow, Utils::String::toUpper(title),
Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER);
std::vector<std::vector<const char*>> 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<int>(kbLayout[0].size());
mKeyboardGrid = std::make_shared<ComponentGrid>(
mWindow, glm::ivec2(mHorizontalKeyCount, static_cast<int>(kbLayout.size()) / 3));
mText = std::make_shared<TextEditComponent>(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<TextComponent>(
mWindow, infoString, Font::get(FONT_SIZE_MEDIUM), 0x555555FF, ALIGN_CENTER);
mGrid.setEntry(mInfoString, glm::ivec2{0, yPos}, false, true);
mDefaultValue = std::make_shared<TextComponent>(
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<std::vector<std::shared_ptr<ButtonComponent>>> buttonList;
// Create keyboard.
for (int i = 0; i < static_cast<int>(kbLayout.size()) / 4; i++) {
std::vector<std::shared_ptr<ButtonComponent>> buttons;
for (int j = 0; j < static_cast<int>(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<ButtonComponent> 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<ButtonComponent>(
mWindow, (SHIFT_SYMBOL), ("SHIFT"), [this] { shiftKeys(); }, false, true);
button = mShiftButton;
}
else if (lower == "ALT") {
mAltButton = std::make_shared<ButtonComponent>(
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<int>(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<int>(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<float>(Renderer::getScreenWidth()) - mSize.x) / 2.0f,
(static_cast<float>(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<float>(Renderer::getScreenWidth()) - mSize.x) / 2.0f,
(static_cast<float>(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<HelpPrompt> GuiTextEditKeyboardPopup::getHelpPrompts()
{
std::vector<HelpPrompt> 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<ButtonComponent> GuiTextEditKeyboardPopup::makeButton(
const std::string& key,
const std::string& shiftedKey,
const std::string& altedKey,
const std::string& altshiftedKey)
{
std::shared_ptr<ButtonComponent> button = std::make_shared<ButtonComponent>(
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;
}

View file

@ -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<void(const std::string&)>& 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<HelpPrompt> getHelpPrompts() override;
HelpStyle getHelpStyle() override { return mHelpStyle; }
private:
class KeyboardButton
{
public:
std::shared_ptr<ButtonComponent> button;
const std::string key;
const std::string shiftedKey;
const std::string altedKey;
const std::string altshiftedKey;
KeyboardButton(const std::shared_ptr<ButtonComponent> 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<ButtonComponent> makeButton(const std::string& key,
const std::string& shiftedKey,
const std::string& altedKey,
const std::string& altshiftedKey);
std::vector<KeyboardButton> mKeyboardButtons;
std::shared_ptr<ButtonComponent> mShiftButton;
std::shared_ptr<ButtonComponent> mAltButton;
NinePatchComponent mBackground;
ComponentGrid mGrid;
HelpStyle mHelpStyle;
std::shared_ptr<TextComponent> mTitle;
std::shared_ptr<TextComponent> mInfoString;
std::shared_ptr<TextComponent> mDefaultValue;
std::shared_ptr<TextEditComponent> mText;
std::shared_ptr<ComponentGrid> mKeyboardGrid;
std::string mInitValue;
std::string mAcceptBtnHelpText;
std::string mSaveConfirmationText;
std::string mLoadBtnHelpText;
std::string mClearBtnHelpText;
std::string mCancelBtnHelpText;
std::function<void(const std::string&)> 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

View file

@ -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<void(const std::string&)>& 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<TextComponent>(mWindow, Utils::String::toUpper(title),
Font::get(FONT_SIZE_MEDIUM), 0x555555FF, ALIGN_CENTER);
if (mComplexMode) {
mInfoString = std::make_shared<TextComponent>(
mWindow, infoString, Font::get(FONT_SIZE_SMALL), 0x555555FF, ALIGN_CENTER);
mDefaultValue = std::make_shared<TextComponent>(
mWindow, defaultValue, Font::get(FONT_SIZE_SMALL), 0x555555FF, ALIGN_CENTER);
}
mText = std::make_shared<TextEditComponent>(mWindow);
mText->setValue(initValue);
@ -46,55 +66,116 @@ GuiTextEditPopup::GuiTextEditPopup(Window* window,
okCallback(mText->getValue());
delete this;
}));
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "CLEAR", "clear",
if (mComplexMode) {
buttons.push_back(std::make_shared<ButtonComponent>(
mWindow, "load", loadBtnHelpText, [this, defaultValue] {
mText->setValue(defaultValue);
mText->setCursor(0);
mText->setCursor(defaultValue.size());
}));
}
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "clear", clearBtnHelpText,
[this] { mText->setValue(""); }));
buttons.push_back(std::make_shared<ButtonComponent>(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<HelpPrompt> GuiTextEditPopup::getHelpPrompts()
{
std::vector<HelpPrompt> 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;
}
}

View file

@ -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<void(const std::string&)>& 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<HelpPrompt> getHelpPrompts() override;
HelpStyle getHelpStyle() override { return mHelpStyle; }
private:
void updateDeleteRepeat(int deltaTime);
NinePatchComponent mBackground;
ComponentGrid mGrid;
HelpStyle mHelpStyle;
std::shared_ptr<TextComponent> mTitle;
std::shared_ptr<TextComponent> mInfoString;
std::shared_ptr<TextComponent> mDefaultValue;
std::shared_ptr<TextEditComponent> mText;
std::shared_ptr<ComponentGrid> mButtonGrid;
HelpStyle mHelpStyle;
bool mMultiLine;
std::string mInitValue;
std::function<void(const std::string&)> mOkCallback;
std::string mAcceptBtnText;
std::string mSaveConfirmationText;
std::string mLoadBtnHelpText;
std::string mClearBtnHelpText;
std::string mCancelBtnHelpText;
std::function<void(const std::string&)> mOkCallback;
bool mMultiLine;
bool mComplexMode;
bool mDeleteRepeat;
int mDeleteRepeatTimer;
};
#endif // ES_CORE_GUIS_GUI_TEXT_EDIT_POPUP_H