569 lines
12 KiB
C++
569 lines
12 KiB
C++
#include "gui/interface/Textbox.h"
|
|
|
|
#include "Config.h"
|
|
#include "Platform.h"
|
|
#include "Format.h"
|
|
#include "PowderToy.h"
|
|
|
|
#include "graphics/Graphics.h"
|
|
|
|
#include "gui/interface/Point.h"
|
|
#include "gui/interface/Keys.h"
|
|
#include "gui/interface/Mouse.h"
|
|
|
|
#include "ContextMenu.h"
|
|
|
|
using namespace ui;
|
|
|
|
Textbox::Textbox(Point position, Point size, String textboxText, String textboxPlaceholder):
|
|
Label(position, size, ""),
|
|
ReadOnly(false),
|
|
inputType(All),
|
|
limit(String::npos),
|
|
keyDown(0),
|
|
characterDown(0),
|
|
mouseDown(false),
|
|
masked(false),
|
|
border(true)
|
|
{
|
|
placeHolder = textboxPlaceholder;
|
|
|
|
SetText(textboxText);
|
|
cursor = text.length();
|
|
|
|
menu->RemoveItem(0);
|
|
menu->AddItem(ContextMenuItem("Cut"_i18n, 1, true));
|
|
menu->AddItem(ContextMenuItem("Copy"_i18n, 0, true));
|
|
menu->AddItem(ContextMenuItem("Paste"_i18n, 2, true));
|
|
}
|
|
|
|
void Textbox::SetHidden(bool hidden)
|
|
{
|
|
menu->RemoveItem(0);
|
|
menu->RemoveItem(1);
|
|
menu->RemoveItem(2);
|
|
menu->AddItem(ContextMenuItem("Cut"_i18n, 1, !hidden));
|
|
menu->AddItem(ContextMenuItem("Copy"_i18n, 0, !hidden));
|
|
menu->AddItem(ContextMenuItem("Paste"_i18n, 2, true));
|
|
|
|
masked = hidden;
|
|
}
|
|
|
|
void Textbox::SetPlaceholder(String text)
|
|
{
|
|
placeHolder = text;
|
|
}
|
|
|
|
void Textbox::SetText(String newText)
|
|
{
|
|
backingText = newText;
|
|
|
|
if(masked)
|
|
{
|
|
String maskedText = newText;
|
|
std::fill(maskedText.begin(), maskedText.end(), 0xE00D);
|
|
Label::SetText(maskedText);
|
|
}
|
|
else
|
|
Label::SetText(newText);
|
|
|
|
cursor = newText.length();
|
|
|
|
if(cursor)
|
|
{
|
|
textWrapper.Index2Point(textWrapper.Clear2Index(cursor), cursorPositionX, cursorPositionY);
|
|
}
|
|
else
|
|
{
|
|
cursorPositionY = cursorPositionX = 0;
|
|
}
|
|
}
|
|
|
|
Textbox::ValidInput Textbox::GetInputType()
|
|
{
|
|
return inputType;
|
|
}
|
|
|
|
void Textbox::SetInputType(ValidInput input)
|
|
{
|
|
inputType = input;
|
|
}
|
|
|
|
void Textbox::SetLimit(size_t limit)
|
|
{
|
|
this->limit = limit;
|
|
}
|
|
|
|
size_t Textbox::GetLimit()
|
|
{
|
|
return limit;
|
|
}
|
|
|
|
String Textbox::GetText()
|
|
{
|
|
return backingText;
|
|
}
|
|
|
|
void Textbox::OnContextMenuAction(int item)
|
|
{
|
|
switch(item)
|
|
{
|
|
case 0:
|
|
copySelection();
|
|
break;
|
|
case 1:
|
|
cutSelection();
|
|
break;
|
|
case 2:
|
|
pasteIntoSelection();
|
|
break;
|
|
}
|
|
}
|
|
|
|
void Textbox::resetCursorPosition()
|
|
{
|
|
textWrapper.Index2Point(textWrapper.Clear2Index(cursor), cursorPositionX, cursorPositionY);
|
|
}
|
|
|
|
void Textbox::TabFocus()
|
|
{
|
|
GetParentWindow()->FocusComponent(this);
|
|
selectAll();
|
|
}
|
|
|
|
void Textbox::cutSelection()
|
|
{
|
|
if (HasSelection())
|
|
{
|
|
if (getLowerSelectionBound() < 0 || getHigherSelectionBound() > (int)backingText.length())
|
|
return;
|
|
String toCopy = backingText.Between(getLowerSelectionBound(), getHigherSelectionBound());
|
|
ClipboardPush(format::CleanString(toCopy, false, true, false).ToUtf8());
|
|
backingText.erase(backingText.begin()+getLowerSelectionBound(), backingText.begin()+getHigherSelectionBound());
|
|
cursor = getLowerSelectionBound();
|
|
}
|
|
else
|
|
{
|
|
if (!backingText.length())
|
|
return;
|
|
ClipboardPush(format::CleanString(backingText, false, true, false).ToUtf8());
|
|
backingText.clear();
|
|
cursor = 0;
|
|
}
|
|
ClearSelection();
|
|
|
|
if(masked)
|
|
{
|
|
String maskedText = backingText;
|
|
std::fill(maskedText.begin(), maskedText.end(), 0xE00D);
|
|
Label::SetText(maskedText);
|
|
}
|
|
else
|
|
{
|
|
text = backingText;
|
|
}
|
|
|
|
updateTextWrapper();
|
|
updateSelection();
|
|
TextPosition(displayTextWrapper.WrappedText());
|
|
|
|
if(cursor)
|
|
{
|
|
textWrapper.Index2Point(textWrapper.Clear2Index(cursor), cursorPositionX, cursorPositionY);
|
|
}
|
|
else
|
|
{
|
|
cursorPositionY = cursorPositionX = 0;
|
|
}
|
|
if (actionCallback.change)
|
|
actionCallback.change();
|
|
}
|
|
|
|
void Textbox::pasteIntoSelection()
|
|
{
|
|
String newText = format::CleanString(ClipboardPull().FromUtf8(), false, true, inputType != Multiline, inputType == Number || inputType == Numeric);
|
|
if (HasSelection())
|
|
{
|
|
if (getLowerSelectionBound() < 0 || getHigherSelectionBound() > (int)backingText.length())
|
|
return;
|
|
backingText.EraseBetween(getLowerSelectionBound(), getHigherSelectionBound());
|
|
cursor = getLowerSelectionBound();
|
|
}
|
|
|
|
int regionWidth = Size.X;
|
|
if (Appearance.icon)
|
|
regionWidth -= 13;
|
|
regionWidth -= Appearance.Margin.Left;
|
|
regionWidth -= Appearance.Margin.Right;
|
|
|
|
if (limit != String::npos)
|
|
{
|
|
newText = newText.Substr(0, limit-backingText.length());
|
|
}
|
|
if (!multiline && Graphics::textwidth(backingText + newText) > regionWidth)
|
|
{
|
|
int pLimit = regionWidth - Graphics::textwidth(backingText);
|
|
int pWidth = 0;
|
|
auto it = newText.begin();
|
|
while (it != newText.end())
|
|
{
|
|
auto w = Graphics::CharWidth(*it);
|
|
if (pWidth + w > pLimit)
|
|
{
|
|
break;
|
|
}
|
|
pWidth += w;
|
|
++it;
|
|
}
|
|
newText = String(newText.begin(), it);
|
|
}
|
|
|
|
backingText.Insert(cursor, newText);
|
|
cursor = cursor+newText.length();
|
|
ClearSelection();
|
|
|
|
if(masked)
|
|
{
|
|
String maskedText = backingText;
|
|
std::fill(maskedText.begin(), maskedText.end(), 0xE00D);
|
|
Label::SetText(maskedText);
|
|
}
|
|
else
|
|
{
|
|
text = backingText;
|
|
}
|
|
|
|
updateTextWrapper();
|
|
updateSelection();
|
|
TextPosition(displayTextWrapper.WrappedText());
|
|
|
|
if(cursor)
|
|
{
|
|
textWrapper.Index2Point(textWrapper.Clear2Index(cursor), cursorPositionX, cursorPositionY);
|
|
}
|
|
else
|
|
{
|
|
cursorPositionY = cursorPositionX = 0;
|
|
}
|
|
if (actionCallback.change)
|
|
actionCallback.change();
|
|
}
|
|
|
|
bool Textbox::CharacterValid(int character)
|
|
{
|
|
switch(inputType)
|
|
{
|
|
case Numeric:
|
|
if (character == '-' && cursor == 0 && backingText[0] != '-')
|
|
return true;
|
|
case Number:
|
|
return (character >= '0' && character <= '9');
|
|
case Multiline:
|
|
if (character == '\n')
|
|
return true;
|
|
case All:
|
|
default:
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// TODO: proper unicode validation
|
|
bool Textbox::StringValid(String text)
|
|
{
|
|
for (String::value_type c : text)
|
|
if (!CharacterValid(c))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
void Textbox::Tick(float dt)
|
|
{
|
|
Label::Tick(dt);
|
|
if (!IsFocused())
|
|
{
|
|
keyDown = 0;
|
|
characterDown = 0;
|
|
}
|
|
unsigned long time_pls = Platform::GetTime();
|
|
if ((keyDown || characterDown) && repeatTime <= time_pls)
|
|
{
|
|
//OnVKeyPress(keyDown, characterDown, false, false, false);
|
|
repeatTime = Platform::GetTime()+30;
|
|
}
|
|
}
|
|
|
|
void Textbox::OnKeyRelease(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt)
|
|
{
|
|
keyDown = 0;
|
|
characterDown = 0;
|
|
}
|
|
|
|
void Textbox::OnKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt)
|
|
{
|
|
characterDown = scan;
|
|
keyDown = key;
|
|
repeatTime = Platform::GetTime()+300;
|
|
OnVKeyPress(key, scan, repeat, shift, ctrl, alt);
|
|
}
|
|
|
|
void Textbox::OnVKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt)
|
|
{
|
|
bool changed = false;
|
|
if (ctrl && scan == SDL_SCANCODE_C && !masked && !repeat)
|
|
{
|
|
copySelection();
|
|
return;
|
|
}
|
|
if (ctrl && scan == SDL_SCANCODE_V && !ReadOnly)
|
|
{
|
|
pasteIntoSelection();
|
|
return;
|
|
}
|
|
if (ctrl && scan == SDL_SCANCODE_X && !masked && !repeat && !ReadOnly)
|
|
{
|
|
cutSelection();
|
|
return;
|
|
}
|
|
if (ctrl && scan == SDL_SCANCODE_A)
|
|
{
|
|
selectAll();
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
switch(key)
|
|
{
|
|
case SDLK_HOME:
|
|
cursor = 0;
|
|
ClearSelection();
|
|
break;
|
|
case SDLK_END:
|
|
cursor = backingText.length();
|
|
ClearSelection();
|
|
break;
|
|
case SDLK_LEFT:
|
|
if(cursor > 0)
|
|
cursor--;
|
|
ClearSelection();
|
|
break;
|
|
case SDLK_RIGHT:
|
|
if (cursor < (int)backingText.length())
|
|
cursor++;
|
|
ClearSelection();
|
|
break;
|
|
case SDLK_DELETE:
|
|
if(ReadOnly)
|
|
break;
|
|
if (HasSelection())
|
|
{
|
|
if (getLowerSelectionBound() < 0 || getHigherSelectionBound() > (int)backingText.length())
|
|
return;
|
|
backingText.Erase(getLowerSelectionBound(), getHigherSelectionBound());
|
|
cursor = getLowerSelectionBound();
|
|
changed = true;
|
|
}
|
|
else if (backingText.length() && cursor < (int)backingText.length())
|
|
{
|
|
if (ctrl)
|
|
{
|
|
size_t stopChar;
|
|
stopChar = backingText.SplitByNot(" .,!?\n", cursor).PositionBefore();
|
|
stopChar = backingText.SplitByAny(" .,!?\n", stopChar).PositionBefore();
|
|
backingText.EraseBetween(cursor, stopChar);
|
|
}
|
|
else
|
|
backingText.erase(cursor, 1);
|
|
changed = true;
|
|
}
|
|
ClearSelection();
|
|
break;
|
|
case SDLK_BACKSPACE:
|
|
if (ReadOnly)
|
|
break;
|
|
if (HasSelection())
|
|
{
|
|
if (getLowerSelectionBound() < 0 || getHigherSelectionBound() > (int)backingText.length())
|
|
return;
|
|
backingText.erase(backingText.begin()+getLowerSelectionBound(), backingText.begin()+getHigherSelectionBound());
|
|
cursor = getLowerSelectionBound();
|
|
changed = true;
|
|
}
|
|
else if (backingText.length() && cursor > 0)
|
|
{
|
|
if (ctrl)
|
|
{
|
|
size_t stopChar;
|
|
stopChar = backingText.SplitFromEndByNot(" .,!?\n", cursor).PositionBefore();
|
|
if (stopChar == backingText.npos)
|
|
stopChar = -1;
|
|
else
|
|
stopChar = backingText.SplitFromEndByAny(" .,!?\n", stopChar).PositionBefore();
|
|
backingText.EraseBetween(stopChar+1, cursor);
|
|
cursor = stopChar+1;
|
|
}
|
|
else
|
|
{
|
|
backingText.erase(cursor-1, 1);
|
|
cursor--;
|
|
}
|
|
changed = true;
|
|
}
|
|
ClearSelection();
|
|
break;
|
|
case SDLK_RETURN:
|
|
OnTextInput("\n");
|
|
break;
|
|
}
|
|
}
|
|
catch (std::out_of_range &e)
|
|
{
|
|
cursor = 0;
|
|
backingText = "";
|
|
}
|
|
AfterTextChange(changed);
|
|
}
|
|
|
|
void Textbox::AfterTextChange(bool changed)
|
|
{
|
|
if (cursor > (int)backingText.length())
|
|
cursor = backingText.length();
|
|
|
|
if (changed)
|
|
{
|
|
if (inputType == Number)
|
|
{
|
|
//Remove extra preceding 0's
|
|
while(backingText[0] == '0' && backingText.length()>1)
|
|
backingText.erase(backingText.begin());
|
|
}
|
|
|
|
if (masked)
|
|
{
|
|
String maskedText = backingText;
|
|
std::fill(maskedText.begin(), maskedText.end(), 0xE00D);
|
|
Label::SetText(maskedText);
|
|
}
|
|
else
|
|
{
|
|
text = backingText;
|
|
}
|
|
}
|
|
|
|
updateTextWrapper();
|
|
updateSelection();
|
|
TextPosition(displayTextWrapper.WrappedText());
|
|
|
|
if(cursor)
|
|
{
|
|
textWrapper.Index2Point(textWrapper.Clear2Index(cursor), cursorPositionX, cursorPositionY);
|
|
}
|
|
else
|
|
{
|
|
cursorPositionY = cursorPositionX = 0;
|
|
}
|
|
if (changed && actionCallback.change)
|
|
actionCallback.change();
|
|
}
|
|
|
|
void Textbox::OnTextInput(String text)
|
|
{
|
|
if (StringValid(text) && !ReadOnly)
|
|
{
|
|
if (HasSelection())
|
|
{
|
|
if (getLowerSelectionBound() < 0 || getHigherSelectionBound() > (int)backingText.length())
|
|
return;
|
|
backingText.erase(backingText.begin()+getLowerSelectionBound(), backingText.begin()+getHigherSelectionBound());
|
|
cursor = getLowerSelectionBound();
|
|
}
|
|
|
|
int regionWidth = Size.X;
|
|
if (Appearance.icon)
|
|
regionWidth -= 13;
|
|
regionWidth -= Appearance.Margin.Left;
|
|
regionWidth -= Appearance.Margin.Right;
|
|
if ((limit==String::npos || backingText.length() < limit) && (Graphics::textwidth(backingText + text) <= regionWidth || multiline))
|
|
{
|
|
if (cursor == (int)backingText.length())
|
|
{
|
|
backingText += text;
|
|
}
|
|
else
|
|
{
|
|
backingText.Insert(cursor, text);
|
|
}
|
|
cursor += text.length();
|
|
}
|
|
ClearSelection();
|
|
AfterTextChange(true);
|
|
}
|
|
}
|
|
|
|
void Textbox::OnMouseClick(int x, int y, unsigned button)
|
|
{
|
|
|
|
if (button != SDL_BUTTON_RIGHT)
|
|
{
|
|
mouseDown = true;
|
|
auto index = textWrapper.Point2Index(x-textPosition.X, y-textPosition.Y);
|
|
cursor = index.raw_index;
|
|
if(cursor)
|
|
{
|
|
textWrapper.Index2Point(index, cursorPositionX, cursorPositionY);
|
|
}
|
|
else
|
|
{
|
|
cursorPositionY = cursorPositionX = 0;
|
|
}
|
|
}
|
|
Label::OnMouseClick(x, y, button);
|
|
}
|
|
|
|
void Textbox::OnMouseUp(int x, int y, unsigned button)
|
|
{
|
|
mouseDown = false;
|
|
Label::OnMouseUp(x, y, button);
|
|
}
|
|
|
|
void Textbox::OnMouseMoved(int localx, int localy, int dx, int dy)
|
|
{
|
|
if(mouseDown)
|
|
{
|
|
auto index = textWrapper.Point2Index(localx-textPosition.X, localy-textPosition.Y);
|
|
cursor = index.raw_index;
|
|
if(cursor)
|
|
{
|
|
textWrapper.Index2Point(index, cursorPositionX, cursorPositionY);
|
|
}
|
|
else
|
|
{
|
|
cursorPositionY = cursorPositionX = 0;
|
|
}
|
|
}
|
|
Label::OnMouseMoved(localx, localy, dx, dy);
|
|
}
|
|
|
|
void Textbox::Draw(const Point& screenPos)
|
|
{
|
|
Label::Draw(screenPos);
|
|
|
|
Graphics * g = GetGraphics();
|
|
if(IsFocused())
|
|
{
|
|
if(border) g->drawrect(screenPos.X, screenPos.Y, Size.X, Size.Y, 255, 255, 255, 255);
|
|
g->draw_line(screenPos.X+textPosition.X+cursorPositionX, screenPos.Y-2+textPosition.Y+cursorPositionY, screenPos.X+textPosition.X+cursorPositionX, screenPos.Y+9+textPosition.Y+cursorPositionY, 255, 255, 255, 255);
|
|
}
|
|
else
|
|
{
|
|
if(!text.length())
|
|
{
|
|
g->drawtext(screenPos.X+textPosition.X, screenPos.Y+textPosition.Y, placeHolder, textColour.Red, textColour.Green, textColour.Blue, 170);
|
|
}
|
|
if(border) g->drawrect(screenPos.X, screenPos.Y, Size.X, Size.Y, 160, 160, 160, 255);
|
|
}
|
|
if(Appearance.icon)
|
|
g->draw_icon(screenPos.X+iconPosition.X, screenPos.Y+iconPosition.Y, Appearance.icon);
|
|
}
|