add history to TextBox
This commit is contained in:
parent
223a846784
commit
4c493aff25
@ -108,7 +108,7 @@ void GUI::actMouse(float delta) {
|
||||
doubleClicked = false;
|
||||
doubleClickTimer += delta + mouseDelta * 0.1f;
|
||||
|
||||
auto hover = container->getAt(Events::cursor, nullptr);
|
||||
auto hover = container->getAt(Events::cursor);
|
||||
if (this->hover && this->hover != hover) {
|
||||
this->hover->setHover(false);
|
||||
}
|
||||
|
||||
@ -17,9 +17,7 @@ Container::~Container() {
|
||||
Container::clear();
|
||||
}
|
||||
|
||||
std::shared_ptr<UINode> Container::getAt(
|
||||
const glm::vec2& pos, const std::shared_ptr<UINode>& self
|
||||
) {
|
||||
std::shared_ptr<UINode> Container::getAt(const glm::vec2& pos) {
|
||||
if (!isInteractive() || !isEnabled()) {
|
||||
return nullptr;
|
||||
}
|
||||
@ -28,19 +26,19 @@ std::shared_ptr<UINode> Container::getAt(
|
||||
}
|
||||
int diff = (actualLength-size.y);
|
||||
if (scrollable && diff > 0 && pos.x > calcPos().x + getSize().x - scrollBarWidth) {
|
||||
return UINode::getAt(pos, self);
|
||||
return UINode::getAt(pos);
|
||||
}
|
||||
|
||||
for (int i = nodes.size()-1; i >= 0; i--) {
|
||||
auto& node = nodes[i];
|
||||
if (!node->isVisible())
|
||||
continue;
|
||||
auto hover = node->getAt(pos, node);
|
||||
auto hover = node->getAt(pos);
|
||||
if (hover != nullptr) {
|
||||
return hover;
|
||||
}
|
||||
}
|
||||
return UINode::getAt(pos, self);
|
||||
return UINode::getAt(pos);
|
||||
}
|
||||
|
||||
void Container::mouseMove(GUI* gui, int x, int y) {
|
||||
|
||||
@ -28,7 +28,7 @@ namespace gui {
|
||||
virtual void act(float delta) override;
|
||||
virtual void drawBackground(const DrawContext& pctx, const Assets& assets);
|
||||
virtual void draw(const DrawContext& pctx, const Assets& assets) override;
|
||||
virtual std::shared_ptr<UINode> getAt(const glm::vec2& pos, const std::shared_ptr<UINode>& self) override;
|
||||
virtual std::shared_ptr<UINode> getAt(const glm::vec2& pos) override;
|
||||
virtual void add(const std::shared_ptr<UINode>& node);
|
||||
virtual void add(const std::shared_ptr<UINode>& node, glm::vec2 pos);
|
||||
virtual void clear();
|
||||
|
||||
@ -13,18 +13,177 @@
|
||||
#include "util/stringutil.hpp"
|
||||
#include "window/Events.hpp"
|
||||
#include "window/Window.hpp"
|
||||
#include "devtools/actions.hpp"
|
||||
#include "../markdown.hpp"
|
||||
|
||||
using namespace gui;
|
||||
|
||||
inline constexpr int LINE_NUMBERS_PANE_WIDTH = 40;
|
||||
|
||||
TextBox::TextBox(std::wstring placeholder, glm::vec4 padding)
|
||||
: Container(glm::vec2(200,32)),
|
||||
padding(padding),
|
||||
input(L""),
|
||||
placeholder(std::move(placeholder))
|
||||
{
|
||||
class InputAction : public Action {
|
||||
std::weak_ptr<TextBox> textbox;
|
||||
size_t position;
|
||||
std::wstring string;
|
||||
public:
|
||||
InputAction(
|
||||
std::weak_ptr<TextBox> textbox, size_t position, std::wstring string
|
||||
)
|
||||
: textbox(std::move(textbox)),
|
||||
position(position),
|
||||
string(std::move(string)) {
|
||||
}
|
||||
void apply() override {
|
||||
if (auto box = textbox.lock()) {
|
||||
box->select(position, position);
|
||||
box->paste(string);
|
||||
}
|
||||
}
|
||||
|
||||
void revert() override {
|
||||
if (auto box = textbox.lock()) {
|
||||
box->select(position, position);
|
||||
box->erase(position, string.length());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class SelectionAction : public Action {
|
||||
std::weak_ptr<TextBox> textbox;
|
||||
size_t start;
|
||||
size_t end;
|
||||
public:
|
||||
SelectionAction(std::weak_ptr<TextBox> textbox, size_t start, size_t end)
|
||||
: textbox(std::move(textbox)), start(start), end(end) {}
|
||||
|
||||
void apply() override {
|
||||
if (auto box = textbox.lock()) {
|
||||
box->select(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
void revert() override {
|
||||
if (auto box = textbox.lock()) {
|
||||
box->select(0, 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
namespace gui {
|
||||
/// @brief Accumulates small changes into words for InputAction creation
|
||||
class TextBoxHistorian {
|
||||
public:
|
||||
TextBoxHistorian(TextBox& textBox, ActionsHistory& history)
|
||||
: textBox(textBox), history(history) {
|
||||
}
|
||||
|
||||
void onPaste(size_t pos, std::wstring_view text) {
|
||||
if (locked) {
|
||||
return;
|
||||
}
|
||||
if (erasing) {
|
||||
sync();
|
||||
}
|
||||
if (this->pos == static_cast<size_t>(-1)) {
|
||||
this->pos = pos;
|
||||
}
|
||||
if (this->pos + length != pos || text == L" " || text == L"\n") {
|
||||
sync();
|
||||
this->pos = pos;
|
||||
}
|
||||
ss << text;
|
||||
length += text.length();
|
||||
}
|
||||
|
||||
void onErase(size_t pos, std::wstring_view text, bool selection=false) {
|
||||
if (locked) {
|
||||
return;
|
||||
}
|
||||
if (!erasing) {
|
||||
sync();
|
||||
erasing = true;
|
||||
}
|
||||
if (selection) {
|
||||
history.store(
|
||||
std::make_unique<SelectionAction>(
|
||||
getTextBoxWeakptr(),
|
||||
textBox.getSelectionStart(),
|
||||
textBox.getSelectionEnd()
|
||||
),
|
||||
true
|
||||
);
|
||||
}
|
||||
if (this->pos == static_cast<size_t>(-1)) {
|
||||
this->pos = pos;
|
||||
} else if (this->pos - text.length() != pos) {
|
||||
sync();
|
||||
erasing = true;
|
||||
this->pos = pos;
|
||||
}
|
||||
if (text == L" " || text == L"\n") {
|
||||
sync();
|
||||
erasing = true;
|
||||
this->pos = pos;
|
||||
}
|
||||
auto str = ss.str();
|
||||
ss.seekp(0);
|
||||
ss << text << str;
|
||||
|
||||
this->pos = pos;
|
||||
length += text.length();
|
||||
}
|
||||
|
||||
/// @brief Flush buffer and push all changes to the ActionsHistory
|
||||
void sync() {
|
||||
auto string = ss.str();
|
||||
if (string.empty()) {
|
||||
return;
|
||||
}
|
||||
auto action =
|
||||
std::make_unique<InputAction>(getTextBoxWeakptr(), pos, string);
|
||||
history.store(std::move(action), erasing);
|
||||
pos = -1;
|
||||
length = 0;
|
||||
ss = {};
|
||||
erasing = false;
|
||||
}
|
||||
|
||||
void undo() {
|
||||
sync();
|
||||
locked = true;
|
||||
history.undo();
|
||||
locked = false;
|
||||
}
|
||||
|
||||
void redo() {
|
||||
sync();
|
||||
locked = true;
|
||||
history.redo();
|
||||
locked = false;
|
||||
}
|
||||
private:
|
||||
TextBox& textBox;
|
||||
ActionsHistory& history;
|
||||
std::wstringstream ss;
|
||||
size_t pos = -1;
|
||||
size_t length = 0;
|
||||
bool erasing = false;
|
||||
bool locked = false;
|
||||
|
||||
std::weak_ptr<TextBox> getTextBoxWeakptr() {
|
||||
return std::weak_ptr<TextBox>(std::dynamic_pointer_cast<TextBox>(
|
||||
textBox.shared_from_this()
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
TextBox::TextBox(std::wstring placeholder, glm::vec4 padding)
|
||||
: Container(glm::vec2(200, 32)),
|
||||
history(std::make_shared<ActionsHistory>()),
|
||||
historian(std::make_unique<TextBoxHistorian>(*this, *history)),
|
||||
padding(padding),
|
||||
input(L""),
|
||||
placeholder(std::move(placeholder)) {
|
||||
setCursor(CursorShape::TEXT);
|
||||
setOnUpPressed(nullptr);
|
||||
setOnDownPressed(nullptr);
|
||||
@ -49,6 +208,8 @@ TextBox::TextBox(std::wstring placeholder, glm::vec4 padding)
|
||||
scrollStep = 0;
|
||||
}
|
||||
|
||||
TextBox::~TextBox() = default;
|
||||
|
||||
void TextBox::draw(const DrawContext& pctx, const Assets& assets) {
|
||||
Container::draw(pctx, assets);
|
||||
|
||||
@ -71,6 +232,7 @@ void TextBox::draw(const DrawContext& pctx, const Assets& assets) {
|
||||
auto batch = pctx.getBatch2D();
|
||||
batch->texture(nullptr);
|
||||
batch->setColor(glm::vec4(1.0f));
|
||||
|
||||
if (editable && int((Window::time() - caretLastMove) * 2) % 2 == 0) {
|
||||
uint line = rawTextCache.getLineByTextIndex(caret);
|
||||
uint lcaret = caret - rawTextCache.getTextLineOffset(line);
|
||||
@ -260,18 +422,22 @@ void TextBox::refreshLabel() {
|
||||
|
||||
/// @brief Insert text at the caret. Also selected text will be erased
|
||||
/// @param text Inserting text
|
||||
void TextBox::paste(const std::wstring& text) {
|
||||
void TextBox::paste(const std::wstring& text, bool history) {
|
||||
eraseSelected();
|
||||
auto inputText = text;
|
||||
inputText.erase(
|
||||
std::remove(inputText.begin(), inputText.end(), '\r'), inputText.end()
|
||||
);
|
||||
historian->onPaste(caret, inputText);
|
||||
if (caret >= input.length()) {
|
||||
input += text;
|
||||
input += inputText;
|
||||
} else {
|
||||
auto left = input.substr(0, caret);
|
||||
auto right = input.substr(caret);
|
||||
input = left + text + right;
|
||||
input = left + inputText + right;
|
||||
}
|
||||
input.erase(std::remove(input.begin(), input.end(), '\r'), input.end());
|
||||
refreshLabel();
|
||||
setCaret(caret + text.length());
|
||||
setCaret(caret + inputText.length());
|
||||
if (validate()) {
|
||||
onInput();
|
||||
}
|
||||
@ -296,6 +462,11 @@ bool TextBox::eraseSelected() {
|
||||
if (selectionStart == selectionEnd) {
|
||||
return false;
|
||||
}
|
||||
historian->onErase(
|
||||
selectionStart,
|
||||
input.substr(selectionStart, selectionEnd - selectionStart),
|
||||
true
|
||||
);
|
||||
erase(selectionStart, selectionEnd-selectionStart);
|
||||
resetSelection();
|
||||
onInput();
|
||||
@ -336,7 +507,9 @@ void TextBox::setTextOffset(uint x) {
|
||||
|
||||
void TextBox::typed(unsigned int codepoint) {
|
||||
if (editable) {
|
||||
paste(std::wstring({(wchar_t)codepoint}));
|
||||
// Combine deleting selected text and inserting a symbol
|
||||
auto combination = history->beginCombination();
|
||||
paste(std::wstring({static_cast<wchar_t>(codepoint)}));
|
||||
}
|
||||
}
|
||||
|
||||
@ -383,6 +556,15 @@ bool TextBox::isEditable() const {
|
||||
return editable;
|
||||
}
|
||||
|
||||
size_t TextBox::getSelectionStart() const {
|
||||
return selectionStart;
|
||||
}
|
||||
|
||||
size_t TextBox::getSelectionEnd() const {
|
||||
return selectionEnd;
|
||||
}
|
||||
|
||||
|
||||
void TextBox::setOnEditStart(runnable oneditstart) {
|
||||
onEditStart = oneditstart;
|
||||
}
|
||||
@ -615,6 +797,7 @@ void TextBox::performEditingKeyboardEvents(keycode key) {
|
||||
if (caret > input.length()) {
|
||||
caret = input.length();
|
||||
}
|
||||
historian->onErase(caret - 1, input.substr(caret - 1, caret));
|
||||
input = input.substr(0, caret-1) + input.substr(caret);
|
||||
setCaret(caret-1);
|
||||
if (validate()) {
|
||||
@ -623,6 +806,7 @@ void TextBox::performEditingKeyboardEvents(keycode key) {
|
||||
}
|
||||
} else if (key == keycode::DELETE) {
|
||||
if (!eraseSelected() && caret < input.length()) {
|
||||
historian->onErase(caret, input.substr(caret, caret + 1));
|
||||
input = input.substr(0, caret) + input.substr(caret + 1);
|
||||
if (validate()) {
|
||||
onInput();
|
||||
@ -669,7 +853,11 @@ void TextBox::keyPressed(keycode key) {
|
||||
if (key == keycode::V && editable) {
|
||||
const char* text = Window::getClipboardText();
|
||||
if (text) {
|
||||
historian->sync(); // flush buffer before combination
|
||||
// Combine deleting selected text and pasing a clipboard content
|
||||
auto combination = history->beginCombination();
|
||||
paste(util::str2wstr_utf8(text));
|
||||
historian->sync();
|
||||
}
|
||||
}
|
||||
// Select/deselect all
|
||||
@ -680,6 +868,12 @@ void TextBox::keyPressed(keycode key) {
|
||||
resetSelection();
|
||||
}
|
||||
}
|
||||
if (key == keycode::Z) {
|
||||
historian->undo();
|
||||
}
|
||||
if (key == keycode::Y) {
|
||||
historian->redo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -704,10 +898,8 @@ size_t TextBox::getLinePos(uint line) const {
|
||||
return label->getTextLineOffset(line);
|
||||
}
|
||||
|
||||
std::shared_ptr<UINode> TextBox::getAt(
|
||||
const glm::vec2& pos, const std::shared_ptr<UINode>& self
|
||||
) {
|
||||
return UINode::getAt(pos, self);
|
||||
std::shared_ptr<UINode> TextBox::getAt(const glm::vec2& pos) {
|
||||
return UINode::getAt(pos);
|
||||
}
|
||||
|
||||
void TextBox::setOnUpPressed(const runnable &callback) {
|
||||
|
||||
@ -4,10 +4,14 @@
|
||||
#include "Label.hpp"
|
||||
|
||||
class Font;
|
||||
class ActionsHistory;
|
||||
|
||||
namespace gui {
|
||||
class TextBoxHistorian;
|
||||
class TextBox : public Container {
|
||||
LabelCache rawTextCache;
|
||||
std::shared_ptr<ActionsHistory> history;
|
||||
std::unique_ptr<TextBoxHistorian> historian;
|
||||
protected:
|
||||
glm::vec4 focusedColor {0.0f, 0.0f, 0.0f, 1.0f};
|
||||
glm::vec4 invalidColor {0.1f, 0.05f, 0.03f, 1.0f};
|
||||
@ -68,7 +72,6 @@ namespace gui {
|
||||
|
||||
int calcIndexAt(int x, int y) const;
|
||||
void setTextOffset(uint x);
|
||||
void erase(size_t start, size_t length);
|
||||
bool eraseSelected();
|
||||
void resetSelection();
|
||||
void extendSelection(int index);
|
||||
@ -93,8 +96,11 @@ namespace gui {
|
||||
std::wstring placeholder,
|
||||
glm::vec4 padding=glm::vec4(4.0f)
|
||||
);
|
||||
|
||||
virtual ~TextBox();
|
||||
|
||||
void paste(const std::wstring& text);
|
||||
void paste(const std::wstring& text, bool history=true);
|
||||
void erase(size_t start, size_t length);
|
||||
|
||||
virtual void setTextSupplier(wstringsupplier supplier);
|
||||
|
||||
@ -201,6 +207,9 @@ namespace gui {
|
||||
virtual void setPadding(glm::vec4 padding);
|
||||
glm::vec4 getPadding() const;
|
||||
|
||||
size_t getSelectionStart() const;
|
||||
size_t getSelectionEnd() const;
|
||||
|
||||
/// @brief Set runnable called on textbox focus
|
||||
virtual void setOnEditStart(runnable oneditstart);
|
||||
|
||||
@ -221,9 +230,7 @@ namespace gui {
|
||||
virtual void drawBackground(const DrawContext& pctx, const Assets& assets) override;
|
||||
virtual void typed(unsigned int codepoint) override;
|
||||
virtual void keyPressed(keycode key) override;
|
||||
virtual std::shared_ptr<UINode> getAt(
|
||||
const glm::vec2& pos, const std::shared_ptr<UINode>& self
|
||||
) override;
|
||||
virtual std::shared_ptr<UINode> getAt(const glm::vec2& pos) override;
|
||||
virtual void setOnUpPressed(const runnable &callback);
|
||||
virtual void setOnDownPressed(const runnable &callback);
|
||||
|
||||
|
||||
@ -111,11 +111,11 @@ bool UINode::isInside(glm::vec2 point) {
|
||||
point.x < pos.x + size.x && point.y < pos.y + size.y);
|
||||
}
|
||||
|
||||
std::shared_ptr<UINode> UINode::getAt(const glm::vec2& point, const std::shared_ptr<UINode>& self) {
|
||||
std::shared_ptr<UINode> UINode::getAt(const glm::vec2& point) {
|
||||
if (!isInteractive() || !enabled) {
|
||||
return nullptr;
|
||||
}
|
||||
return isInside(point) ? self : nullptr;
|
||||
return isInside(point) ? shared_from_this() : nullptr;
|
||||
}
|
||||
|
||||
bool UINode::isInteractive() const {
|
||||
|
||||
@ -63,7 +63,7 @@ namespace gui {
|
||||
};
|
||||
|
||||
/// @brief Base abstract class for all UI elements
|
||||
class UINode {
|
||||
class UINode : public std::enable_shared_from_this<UINode> {
|
||||
/// @brief element identifier used for direct access in UiDocument
|
||||
std::string id = "";
|
||||
/// @brief element enabled state
|
||||
@ -195,7 +195,7 @@ namespace gui {
|
||||
/// @param pos cursor screen position
|
||||
/// @param self shared pointer to element
|
||||
/// @return self, sub-element or nullptr if element is not interractive
|
||||
virtual std::shared_ptr<UINode> getAt(const glm::vec2& pos, const std::shared_ptr<UINode>& self);
|
||||
virtual std::shared_ptr<UINode> getAt(const glm::vec2& pos);
|
||||
|
||||
/// @brief Check if element is opaque for cursor
|
||||
virtual bool isInteractive() const;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user