2020-09-18 16:16:12 +00:00
|
|
|
// SPDX-License-Identifier: MIT
|
2020-06-06 14:48:05 +00:00
|
|
|
//
|
2020-09-18 16:16:12 +00:00
|
|
|
// EmulationStation Desktop Edition
|
2020-06-21 12:25:28 +00:00
|
|
|
// TextEditComponent.cpp
|
2020-06-06 14:48:05 +00:00
|
|
|
//
|
2020-06-21 12:25:28 +00:00
|
|
|
// Component for editing text fields in menus.
|
2020-06-06 14:48:05 +00:00
|
|
|
//
|
|
|
|
|
2014-06-20 01:30:09 +00:00
|
|
|
#include "components/TextEditComponent.h"
|
2017-11-01 22:21:10 +00:00
|
|
|
|
2017-11-15 15:59:39 +00:00
|
|
|
#include "utils/StringUtil.h"
|
2014-03-21 02:47:45 +00:00
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
#define TEXT_PADDING_HORIZ 10.0f
|
|
|
|
#define TEXT_PADDING_VERT 2.0f
|
2014-03-21 02:47:45 +00:00
|
|
|
|
|
|
|
#define CURSOR_REPEAT_START_DELAY 500
|
2020-06-06 14:48:05 +00:00
|
|
|
#define CURSOR_REPEAT_SPEED 28 // Lower is faster.
|
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
TextEditComponent::TextEditComponent(Window* window)
|
|
|
|
: GuiComponent(window)
|
|
|
|
, mBox(window, ":/graphics/textinput.svg")
|
|
|
|
, mFocused(false)
|
|
|
|
, mScrollOffset(0.0f, 0.0f)
|
|
|
|
, mCursor(0)
|
|
|
|
, mEditing(false)
|
|
|
|
, mFont(Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT))
|
|
|
|
, mCursorRepeatDir(0)
|
2013-08-18 14:16:11 +00:00
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
addChild(&mBox);
|
|
|
|
onFocusLost();
|
2021-08-16 16:25:01 +00:00
|
|
|
mResolutionAdjustment = -(34.0f * Renderer::getScreenWidthModifier() - 34.0f);
|
2021-01-15 17:53:38 +00:00
|
|
|
setSize(4096, mFont->getHeight() + (TEXT_PADDING_VERT * Renderer::getScreenHeightModifier()));
|
2013-08-18 14:16:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void TextEditComponent::onFocusGained()
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
mFocused = true;
|
2021-01-15 17:53:38 +00:00
|
|
|
mBox.setImagePath(":/graphics/textinput_focused.svg");
|
2013-08-18 14:16:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void TextEditComponent::onFocusLost()
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
mFocused = false;
|
2021-01-15 17:53:38 +00:00
|
|
|
mBox.setImagePath(":/graphics/textinput.svg");
|
2013-08-18 14:16:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void TextEditComponent::onSizeChanged()
|
|
|
|
{
|
2021-08-17 16:41:45 +00:00
|
|
|
mBox.fitTo(mSize, glm::vec3{},
|
|
|
|
glm::vec2{-34.0f + mResolutionAdjustment,
|
|
|
|
-32.0f - (TEXT_PADDING_VERT * Renderer::getScreenHeightModifier())});
|
2020-06-21 12:25:28 +00:00
|
|
|
onTextChanged(); // Wrap point probably changed.
|
2013-08-18 14:16:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void TextEditComponent::setValue(const std::string& val)
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
mText = val;
|
|
|
|
mTextOrig = val;
|
|
|
|
onTextChanged();
|
2013-08-18 14:16:11 +00:00
|
|
|
}
|
|
|
|
|
2020-12-16 22:59:00 +00:00
|
|
|
void TextEditComponent::textInput(const std::string& text)
|
2013-08-18 14:16:11 +00:00
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
if (mEditing) {
|
|
|
|
mCursorRepeatDir = 0;
|
|
|
|
if (text[0] == '\b') {
|
|
|
|
if (mCursor > 0) {
|
|
|
|
size_t newCursor = Utils::String::prevCursor(mText, mCursor);
|
|
|
|
mText.erase(mText.begin() + newCursor, mText.begin() + mCursor);
|
2020-09-18 16:16:12 +00:00
|
|
|
mCursor = static_cast<unsigned int>(newCursor);
|
2020-06-21 12:25:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
mText.insert(mCursor, text);
|
2020-12-16 22:59:00 +00:00
|
|
|
mCursor += static_cast<unsigned int>(text.size());
|
2020-06-21 12:25:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onTextChanged();
|
|
|
|
onCursorChanged();
|
2013-09-07 22:43:36 +00:00
|
|
|
}
|
|
|
|
|
2014-03-21 02:47:45 +00:00
|
|
|
void TextEditComponent::startEditing()
|
|
|
|
{
|
2020-09-18 16:16:12 +00:00
|
|
|
if (!isMultiline())
|
|
|
|
setCursor(mText.size());
|
2020-06-21 12:25:28 +00:00
|
|
|
SDL_StartTextInput();
|
|
|
|
mEditing = true;
|
|
|
|
updateHelpPrompts();
|
2014-03-21 02:47:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void TextEditComponent::stopEditing()
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
SDL_StopTextInput();
|
|
|
|
mEditing = false;
|
|
|
|
updateHelpPrompts();
|
2014-03-21 02:47:45 +00:00
|
|
|
}
|
|
|
|
|
2013-09-07 22:43:36 +00:00
|
|
|
bool TextEditComponent::input(InputConfig* config, Input input)
|
|
|
|
{
|
2021-07-07 18:31:46 +00:00
|
|
|
bool const cursor_left =
|
|
|
|
(config->getDeviceId() != DEVICE_KEYBOARD && config->isMappedLike("left", input)) ||
|
|
|
|
(config->getDeviceId() == DEVICE_KEYBOARD && input.id == SDLK_LEFT);
|
|
|
|
bool const cursor_right =
|
|
|
|
(config->getDeviceId() != DEVICE_KEYBOARD && config->isMappedLike("right", input)) ||
|
|
|
|
(config->getDeviceId() == DEVICE_KEYBOARD && input.id == SDLK_RIGHT);
|
|
|
|
bool const cursor_up =
|
|
|
|
(config->getDeviceId() != DEVICE_KEYBOARD && config->isMappedLike("up", input)) ||
|
|
|
|
(config->getDeviceId() == DEVICE_KEYBOARD && input.id == SDLK_UP);
|
|
|
|
bool const cursor_down =
|
|
|
|
(config->getDeviceId() != DEVICE_KEYBOARD && config->isMappedLike("down", input)) ||
|
|
|
|
(config->getDeviceId() == DEVICE_KEYBOARD && input.id == SDLK_DOWN);
|
2020-06-21 12:25:28 +00:00
|
|
|
bool const shoulder_left = (config->isMappedLike("leftshoulder", input));
|
|
|
|
bool const shoulder_right = (config->isMappedLike("rightshoulder", input));
|
|
|
|
bool const trigger_left = (config->isMappedLike("lefttrigger", input));
|
|
|
|
bool const trigger_right = (config->isMappedLike("righttrigger", input));
|
|
|
|
|
|
|
|
if (input.value == 0) {
|
2021-07-07 18:31:46 +00:00
|
|
|
if (cursor_left || cursor_right || cursor_up || cursor_down || shoulder_left ||
|
|
|
|
shoulder_right | trigger_left || trigger_right) {
|
2020-06-21 12:25:28 +00:00
|
|
|
mCursorRepeatDir = 0;
|
2021-07-07 18:31:46 +00:00
|
|
|
}
|
2020-06-21 12:25:28 +00:00
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
if ((config->isMappedTo("a", input) ||
|
|
|
|
(config->getDeviceId() == DEVICE_KEYBOARD && input.id == SDLK_RETURN)) &&
|
|
|
|
mFocused && !mEditing) {
|
2020-06-21 12:25:28 +00:00
|
|
|
startEditing();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mEditing) {
|
|
|
|
if (config->getDeviceId() == DEVICE_KEYBOARD && input.id == SDLK_RETURN) {
|
|
|
|
if (isMultiline())
|
|
|
|
textInput("\n");
|
|
|
|
else
|
|
|
|
stopEditing();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Done editing (accept changes).
|
|
|
|
if ((config->getDeviceId() == DEVICE_KEYBOARD && input.id == SDLK_ESCAPE) ||
|
2021-07-07 18:31:46 +00:00
|
|
|
(config->getDeviceId() != DEVICE_KEYBOARD &&
|
|
|
|
(config->isMappedTo("a", input) || config->isMappedTo("b", input)))) {
|
2020-06-21 12:25:28 +00:00
|
|
|
mTextOrig = mText;
|
|
|
|
stopEditing();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
else if (cursor_left || cursor_right) {
|
|
|
|
mCursorRepeatDir = cursor_left ? -1 : 1;
|
|
|
|
mCursorRepeatTimer = -(CURSOR_REPEAT_START_DELAY - CURSOR_REPEAT_SPEED);
|
|
|
|
moveCursor(mCursorRepeatDir);
|
|
|
|
}
|
|
|
|
else if (cursor_up) {
|
|
|
|
// TODO
|
|
|
|
}
|
|
|
|
else if (cursor_down) {
|
|
|
|
// TODO
|
|
|
|
}
|
|
|
|
else if (shoulder_left || shoulder_right) {
|
|
|
|
mCursorRepeatDir = shoulder_left ? -10 : 10;
|
|
|
|
mCursorRepeatTimer = -(CURSOR_REPEAT_START_DELAY - CURSOR_REPEAT_SPEED);
|
|
|
|
moveCursor(mCursorRepeatDir);
|
|
|
|
}
|
|
|
|
// Jump to beginning of text.
|
|
|
|
else if (trigger_left) {
|
|
|
|
setCursor(0);
|
|
|
|
}
|
|
|
|
// Jump to end of text.
|
|
|
|
else if (trigger_right) {
|
|
|
|
setCursor(mText.length());
|
|
|
|
}
|
|
|
|
else if (config->getDeviceId() != DEVICE_KEYBOARD && config->isMappedTo("y", input)) {
|
|
|
|
textInput("\b");
|
|
|
|
}
|
|
|
|
else if (config->getDeviceId() == DEVICE_KEYBOARD) {
|
|
|
|
switch (input.id) {
|
2021-07-07 18:31:46 +00:00
|
|
|
case SDLK_HOME: {
|
|
|
|
setCursor(0);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case SDLK_END: {
|
|
|
|
setCursor(std::string::npos);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case SDLK_DELETE: {
|
|
|
|
if (mCursor < mText.length()) {
|
|
|
|
// Fake as Backspace one char to the right.
|
|
|
|
moveCursor(1);
|
|
|
|
textInput("\b");
|
|
|
|
}
|
|
|
|
break;
|
2020-06-21 12:25:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Consume all input when editing text.
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
2013-08-21 19:49:33 +00:00
|
|
|
}
|
|
|
|
|
2014-03-21 02:47:45 +00:00
|
|
|
void TextEditComponent::update(int deltaTime)
|
2013-08-21 19:49:33 +00:00
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
updateCursorRepeat(deltaTime);
|
|
|
|
GuiComponent::update(deltaTime);
|
2014-03-21 02:47:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void TextEditComponent::updateCursorRepeat(int deltaTime)
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
if (mCursorRepeatDir == 0)
|
|
|
|
return;
|
|
|
|
|
|
|
|
mCursorRepeatTimer += deltaTime;
|
|
|
|
while (mCursorRepeatTimer >= CURSOR_REPEAT_SPEED) {
|
|
|
|
moveCursor(mCursorRepeatDir);
|
|
|
|
mCursorRepeatTimer -= CURSOR_REPEAT_SPEED;
|
|
|
|
}
|
2014-03-21 02:47:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void TextEditComponent::moveCursor(int amt)
|
|
|
|
{
|
2020-09-18 16:16:12 +00:00
|
|
|
mCursor = static_cast<unsigned int>(Utils::String::moveCursor(mText, mCursor, amt));
|
2020-06-21 12:25:28 +00:00
|
|
|
onCursorChanged();
|
2014-03-21 02:47:45 +00:00
|
|
|
}
|
2013-08-22 01:08:36 +00:00
|
|
|
|
2014-06-15 17:55:30 +00:00
|
|
|
void TextEditComponent::setCursor(size_t pos)
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
if (pos == std::string::npos)
|
2020-09-18 16:16:12 +00:00
|
|
|
mCursor = static_cast<unsigned int>(mText.length());
|
2020-06-21 12:25:28 +00:00
|
|
|
else
|
2020-09-18 16:16:12 +00:00
|
|
|
mCursor = static_cast<int>(pos);
|
2014-06-15 17:55:30 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
moveCursor(0);
|
2014-06-15 17:55:30 +00:00
|
|
|
}
|
|
|
|
|
2014-03-21 02:47:45 +00:00
|
|
|
void TextEditComponent::onTextChanged()
|
|
|
|
{
|
2021-08-16 16:25:01 +00:00
|
|
|
std::string wrappedText = (isMultiline() ? mFont->wrapText(mText, getTextAreaSize().x) : mText);
|
2021-07-07 18:31:46 +00:00
|
|
|
mTextCache = std::unique_ptr<TextCache>(
|
2021-08-16 16:25:01 +00:00
|
|
|
mFont->buildTextCache(wrappedText, 0.0f, 0.0f, 0x77777700 | getOpacity()));
|
2013-09-20 23:55:05 +00:00
|
|
|
|
2020-09-18 16:16:12 +00:00
|
|
|
if (mCursor > static_cast<int>(mText.length()))
|
|
|
|
mCursor = static_cast<unsigned int>(mText.length());
|
2013-08-21 19:49:33 +00:00
|
|
|
}
|
|
|
|
|
2013-09-07 22:43:36 +00:00
|
|
|
void TextEditComponent::onCursorChanged()
|
2013-08-21 19:49:33 +00:00
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
if (isMultiline()) {
|
2021-08-17 16:41:45 +00:00
|
|
|
glm::vec2 textSize{mFont->getWrappedTextCursorOffset(mText, getTextAreaSize().x, mCursor)};
|
2020-06-21 12:25:28 +00:00
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
// Need to scroll down?
|
2021-08-16 16:25:01 +00:00
|
|
|
if (mScrollOffset.y + getTextAreaSize().y < textSize.y + mFont->getHeight())
|
|
|
|
mScrollOffset.y = textSize.y - getTextAreaSize().y + mFont->getHeight();
|
2021-07-07 18:31:46 +00:00
|
|
|
// Need to scroll up?
|
2021-08-16 16:25:01 +00:00
|
|
|
else if (mScrollOffset.y > textSize.y)
|
|
|
|
mScrollOffset.y = textSize.y;
|
2020-06-21 12:25:28 +00:00
|
|
|
}
|
|
|
|
else {
|
2021-08-17 16:41:45 +00:00
|
|
|
glm::vec2 cursorPos{mFont->sizeText(mText.substr(0, mCursor))};
|
2020-06-21 12:25:28 +00:00
|
|
|
|
2021-08-16 16:25:01 +00:00
|
|
|
if (mScrollOffset.x + getTextAreaSize().x < cursorPos.x)
|
|
|
|
mScrollOffset.x = cursorPos.x - getTextAreaSize().x;
|
|
|
|
else if (mScrollOffset.x > cursorPos.x)
|
|
|
|
mScrollOffset.x = cursorPos.x;
|
2020-06-21 12:25:28 +00:00
|
|
|
}
|
2013-08-19 15:36:48 +00:00
|
|
|
}
|
|
|
|
|
2021-08-15 17:30:31 +00:00
|
|
|
void TextEditComponent::render(const glm::mat4& parentTrans)
|
2013-08-19 15:36:48 +00:00
|
|
|
{
|
2021-08-17 16:41:45 +00:00
|
|
|
glm::mat4 trans{getTransform() * parentTrans};
|
2020-06-21 12:25:28 +00:00
|
|
|
renderChildren(trans);
|
|
|
|
|
|
|
|
// Text + cursor rendering.
|
|
|
|
// Offset into our "text area" (padding).
|
2021-08-17 16:41:45 +00:00
|
|
|
trans = glm::translate(trans, glm::vec3{getTextAreaPos().x, getTextAreaPos().y, 0.0f});
|
2020-06-21 12:25:28 +00:00
|
|
|
|
2021-08-17 16:41:45 +00:00
|
|
|
glm::ivec2 clipPos{static_cast<int>(trans[3].x), static_cast<int>(trans[3].y)};
|
2020-06-21 12:25:28 +00:00
|
|
|
// Use "text area" size for clipping.
|
2021-08-17 16:41:45 +00:00
|
|
|
glm::vec3 dimScaled{};
|
2021-08-16 16:25:01 +00:00
|
|
|
dimScaled.x = std::fabs(trans[3].x + getTextAreaSize().x);
|
|
|
|
dimScaled.y = std::fabs(trans[3].y + getTextAreaSize().y);
|
2021-08-15 17:30:31 +00:00
|
|
|
|
2021-08-17 16:41:45 +00:00
|
|
|
glm::ivec2 clipDim{static_cast<int>(dimScaled.x - trans[3].x),
|
|
|
|
static_cast<int>(dimScaled.y - trans[3].y)};
|
2020-06-21 12:25:28 +00:00
|
|
|
Renderer::pushClipRect(clipPos, clipDim);
|
|
|
|
|
2021-08-17 16:41:45 +00:00
|
|
|
trans = glm::translate(trans, glm::vec3{-mScrollOffset.x, -mScrollOffset.y, 0.0f});
|
2020-06-21 12:25:28 +00:00
|
|
|
Renderer::setMatrix(trans);
|
|
|
|
|
|
|
|
if (mTextCache)
|
|
|
|
mFont->renderTextCache(mTextCache.get());
|
|
|
|
|
|
|
|
// Pop the clip early to allow the cursor to be drawn outside of the "text area".
|
|
|
|
Renderer::popClipRect();
|
|
|
|
|
|
|
|
// Draw cursor.
|
|
|
|
if (mEditing) {
|
2021-08-16 16:25:01 +00:00
|
|
|
glm::vec2 cursorPos;
|
2020-06-21 12:25:28 +00:00
|
|
|
if (isMultiline()) {
|
2021-08-16 16:25:01 +00:00
|
|
|
cursorPos = mFont->getWrappedTextCursorOffset(mText, getTextAreaSize().x, mCursor);
|
2020-06-21 12:25:28 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
cursorPos = mFont->sizeText(mText.substr(0, mCursor));
|
|
|
|
cursorPos[1] = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
float cursorHeight = mFont->getHeight() * 0.8f;
|
2021-08-16 16:25:01 +00:00
|
|
|
Renderer::drawRect(cursorPos.x, cursorPos.y + (mFont->getHeight() - cursorHeight) / 2.0f,
|
|
|
|
2.0f * Renderer::getScreenWidthModifier(), cursorHeight, 0x000000FF,
|
|
|
|
0x000000FF);
|
2020-06-21 12:25:28 +00:00
|
|
|
}
|
2013-08-19 15:36:48 +00:00
|
|
|
}
|
|
|
|
|
2021-08-16 16:25:01 +00:00
|
|
|
glm::vec2 TextEditComponent::getTextAreaPos() const
|
2013-09-12 21:35:44 +00:00
|
|
|
{
|
2021-08-17 16:41:45 +00:00
|
|
|
return glm::vec2{
|
2021-07-07 18:31:46 +00:00
|
|
|
(-mResolutionAdjustment + (TEXT_PADDING_HORIZ * Renderer::getScreenWidthModifier())) / 2.0f,
|
2021-08-17 16:41:45 +00:00
|
|
|
(TEXT_PADDING_VERT * Renderer::getScreenHeightModifier()) / 2.0f};
|
2013-09-12 21:35:44 +00:00
|
|
|
}
|
|
|
|
|
2021-08-16 16:25:01 +00:00
|
|
|
glm::vec2 TextEditComponent::getTextAreaSize() const
|
2013-09-20 23:55:05 +00:00
|
|
|
{
|
2021-08-17 16:41:45 +00:00
|
|
|
return glm::vec2{mSize.x + mResolutionAdjustment -
|
2021-08-16 16:25:01 +00:00
|
|
|
(TEXT_PADDING_HORIZ * Renderer::getScreenWidthModifier()),
|
2021-08-17 16:41:45 +00:00
|
|
|
mSize.y - (TEXT_PADDING_VERT * Renderer::getScreenHeightModifier())};
|
2013-09-20 23:55:05 +00:00
|
|
|
}
|
2014-01-25 23:34:29 +00:00
|
|
|
|
|
|
|
std::vector<HelpPrompt> TextEditComponent::getHelpPrompts()
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
std::vector<HelpPrompt> prompts;
|
|
|
|
if (mEditing) {
|
|
|
|
prompts.push_back(HelpPrompt("up/down/left/right", "move cursor"));
|
|
|
|
prompts.push_back(HelpPrompt("y", "backspace"));
|
|
|
|
prompts.push_back(HelpPrompt("a", "accept changes"));
|
|
|
|
prompts.push_back(HelpPrompt("b", "accept changes"));
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
prompts.push_back(HelpPrompt("a", "edit"));
|
|
|
|
}
|
|
|
|
return prompts;
|
2014-01-25 23:34:29 +00:00
|
|
|
}
|