From 28f393b4de12b5f17887092dd3c38a779f9a5449 Mon Sep 17 00:00:00 2001 From: HJfod <60038575+HJfod@users.noreply.github.com> Date: Fri, 16 Feb 2024 23:11:18 +0200 Subject: [PATCH] add sane TextInput class --- VERSION | 2 +- loader/include/Geode/ui/InputNode.hpp | 2 +- loader/include/Geode/ui/TextInput.hpp | 124 ++++++++++++++++++++++ loader/src/cocos2d-ext/CopySizeLayout.cpp | 2 +- loader/src/ui/nodes/InputNode.cpp | 3 + loader/src/ui/nodes/TextInput.cpp | 123 +++++++++++++++++++++ 6 files changed, 253 insertions(+), 3 deletions(-) create mode 100644 loader/include/Geode/ui/TextInput.hpp create mode 100644 loader/src/ui/nodes/TextInput.cpp diff --git a/VERSION b/VERSION index bebf2f28..8a27400e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.0-beta.19 \ No newline at end of file +2.0.0-beta.20 \ No newline at end of file diff --git a/loader/include/Geode/ui/InputNode.hpp b/loader/include/Geode/ui/InputNode.hpp index 6d6568ff..a1d0e72e 100644 --- a/loader/include/Geode/ui/InputNode.hpp +++ b/loader/include/Geode/ui/InputNode.hpp @@ -5,7 +5,7 @@ #include namespace geode { - class GEODE_DLL InputNode : public cocos2d::CCMenuItem { + class GEODE_DLL [[deprecated("Use geode::TextInput from the ui/TextInput.hpp header instead")]] InputNode : public cocos2d::CCMenuItem { protected: cocos2d::extension::CCScale9Sprite* m_bgSprite; CCTextInputNode* m_input; diff --git a/loader/include/Geode/ui/TextInput.hpp b/loader/include/Geode/ui/TextInput.hpp new file mode 100644 index 00000000..86b59c18 --- /dev/null +++ b/loader/include/Geode/ui/TextInput.hpp @@ -0,0 +1,124 @@ +#pragma once + +#include +#include +#include + +namespace geode { + enum class CommonFilter { + // Allow an unsigned integer + Uint, + // Allow a signed integer + Int, + // Allow a floating point number + Float, + // Allow letters, numbers, dashes, underscores, and dots + ID, + // Allow word-like characters & spaces + Name, + // Allows basically anything possible to type in an input + Any, + // Allow a hexadecimal number + Hex, + // Allow a non-URL-safe Base64 number + Base64Normal, + // Allow a URL-safe Base64 number + Base64URL, + }; + + GEODE_DLL const char* getCommonFilterAllowedChars(CommonFilter filter); + + /** + * A single-line text input node + */ + class GEODE_DLL TextInput : public cocos2d::CCNode, public TextInputDelegate { + protected: + cocos2d::extension::CCScale9Sprite* m_bgSprite; + CCTextInputNode* m_input; + std::function m_onInput = nullptr; + + bool init(float width, std::string const& placeholder, std::string const& font); + + void textChanged(CCTextInputNode* input) override; + + public: + /** + * Create a single-line text input with a background. + * Can either be used in delegate or callback mode; + * with callback mode, you don't need to deal with adding + * TextInputDelegate to your class' base list, you just install a + * callback function directly to the input itself + * @param width The width of the input + * @param placeholder Placeholder text for the input + * @param font The font to use + */ + static TextInput* create(float width, std::string const& placeholder, std::string const& font = "bigFont.fnt"); + + /** + * Set the placeholder label for this input + */ + void setPlaceholder(std::string const& placeholder); + /** + * Set the filter (allowed characters) for this input + * @param allowedChars String of allowed characters; each character in + * the string represents one allowed character + */ + void setFilter(std::string const& allowedChars); + /** + * Set a commonly used filter (number, text, etc.) + */ + void setCommonFilter(CommonFilter filter); + /** + * Set the maximum amount of characters for this input. Use 0 for + * infinite length + */ + void setMaxCharCount(size_t length); + /** + * Enable/disable password mode (all input characters are rendered as + * dots rather than the actual characters) + */ + void setPasswordMode(bool enable); + /** + * Set the width of the label. This does not set the maximum character + * count; use `setMaxCharCount` for that + */ + void setWidth(float width); + /** + * Install a delegate that handles input events. Removes any currently + * set direct callbacks + * @param delegate The delegate to install + * @param tag Some legacy delegates use a tag to distinguish between + * inputs; this is a convenience parameter for setting the tag of the + * internal CCTextInputNode for those cases + */ + void setDelegate(TextInputDelegate* delegate, std::optional tag = std::nullopt); + /** + * Set a direct callback function that is called when the user types in + * the input. Overrides any delegate that is currently installed + * @param onInput Function to call when the user changes the value of + * the text input + */ + void setCallback(std::function onInput); + + /** + * Hides the background of this input. Shorthand for + * `input->getBGSprite()->setVisible(false)` + */ + void hideBG(); + + /** + * Set the value of the input + * @param str The new text of the input + * @param triggerCallback Whether this should trigger the callback + * function / delegate's textChanged event or not + */ + void setString(std::string const& str, bool triggerCallback = false); + /** + * Get the current value of the input + */ + std::string getString() const; + + CCTextInputNode* getInputNode() const; + cocos2d::extension::CCScale9Sprite* getBGSprite() const; + }; +} diff --git a/loader/src/cocos2d-ext/CopySizeLayout.cpp b/loader/src/cocos2d-ext/CopySizeLayout.cpp index a8ed26b1..dec9ad5d 100644 --- a/loader/src/cocos2d-ext/CopySizeLayout.cpp +++ b/loader/src/cocos2d-ext/CopySizeLayout.cpp @@ -35,7 +35,7 @@ void CopySizeLayout::apply(CCNode* in) { // Prevent accidental infinite loop if (node == in) continue; node->ignoreAnchorPointForPosition(false); - node->setContentSize(in->getContentSize()); + node->setContentSize(in->getContentSize() * ccp(1 / in->getScaleX(), 1 / in->getScaleY())); node->setPosition(in->getContentSize() / 2); node->updateLayout(); } diff --git a/loader/src/ui/nodes/InputNode.cpp b/loader/src/ui/nodes/InputNode.cpp index d2c7364b..cc800623 100644 --- a/loader/src/ui/nodes/InputNode.cpp +++ b/loader/src/ui/nodes/InputNode.cpp @@ -61,7 +61,10 @@ CCScale9Sprite* InputNode::getBG() const { } void InputNode::activate() { + auto const size = m_input->getContentSize(); + auto const pos = m_input->convertToNodeSpace(getMousePos()) + m_input->m_textField->getAnchorPoint() * size; m_input->onClickTrackNode(true); + m_input->updateCursorPosition(pos, { CCPointZero, size }); } void InputNode::setEnabled(bool enabled) { diff --git a/loader/src/ui/nodes/TextInput.cpp b/loader/src/ui/nodes/TextInput.cpp new file mode 100644 index 00000000..582e5278 --- /dev/null +++ b/loader/src/ui/nodes/TextInput.cpp @@ -0,0 +1,123 @@ +#include +#include +#include + +using namespace geode::prelude; + +const char* geode::getCommonFilterAllowedChars(CommonFilter filter) { + switch (filter) { + default: + case CommonFilter::Uint: return "0123456789"; + case CommonFilter::Int: return "-0123456789"; + case CommonFilter::Float: return "-.0123456789"; + case CommonFilter::ID: return "abcdefghijklmnopqrstuvwxyz0123456789-_."; + case CommonFilter::Name: return "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_ "; + case CommonFilter::Any: return "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-+/\\&$%^~*\'\"{}()[]<>=!?@,;.:|• "; + case CommonFilter::Hex: return "0123456789abcdefABCDEF"; + case CommonFilter::Base64Normal: return "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/="; + case CommonFilter::Base64URL: return "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_="; + } +} + +bool TextInput::init(float width, std::string const& placeholder, std::string const& font) { + if (!CCNode::init()) + return false; + + constexpr float HEIGHT = 30.f; + + this->setContentSize({ width, HEIGHT }); + this->setAnchorPoint({ .5f, .5f }); + + m_bgSprite = cocos2d::extension::CCScale9Sprite::create("square02b_001.png", { 0, 0, 80, 80 }); + m_bgSprite->setScale(.5f); + m_bgSprite->setColor({ 0, 0, 0 }); + m_bgSprite->setOpacity(90); + m_bgSprite->setContentSize({ width * 2, HEIGHT * 2 }); + this->addChildAtPosition(m_bgSprite, cocos2d::Anchor::Center); + + m_input = CCTextInputNode::create(width - 10.f, HEIGHT, placeholder.c_str(), "Thonburi", 24, font.c_str()); + m_input->setLabelPlaceholderColor({ 150, 150, 150 }); + m_input->setLabelPlaceholderScale(.6f); + m_input->setMaxLabelScale(.6f); + this->addChildAtPosition(m_input, cocos2d::Anchor::Center); + + handleTouchPriority(this); + + return true; +} + +TextInput* TextInput::create(float width, std::string const& placeholder, std::string const& font) { + auto ret = new TextInput(); + if (ret && ret->init(width, placeholder, font)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +void TextInput::textChanged(CCTextInputNode* input) { + if (m_onInput) { + m_onInput(input->getString()); + } +} + +void TextInput::setPlaceholder(std::string const& placeholder) { + m_input->m_caption = placeholder; + m_input->refreshLabel(); +} +void TextInput::setFilter(std::string const& allowedChars) { + m_input->m_allowedChars = allowedChars; +} +void TextInput::setCommonFilter(CommonFilter filter) { + this->setFilter(getCommonFilterAllowedChars(filter)); +} +void TextInput::setMaxCharCount(size_t length) { + m_input->m_maxLabelLength = length == 0 ? 9999999 : length; +} +void TextInput::setPasswordMode(bool enable) { + m_input->m_usePasswordChar = enable; + m_input->refreshLabel(); +} +void TextInput::setWidth(float width) { + m_input->m_maxLabelWidth = width - 10; + m_input->setContentWidth(width * 2); + m_bgSprite->setContentWidth(width * 2); +} +void TextInput::setDelegate(TextInputDelegate* delegate, std::optional tag) { + m_input->setDelegate(delegate); + m_onInput = nullptr; + if (tag.has_value()) { + m_input->setTag(tag.value()); + } +} +void TextInput::setCallback(std::function onInput) { + this->setDelegate(this); + m_onInput = onInput; +} + +void TextInput::hideBG() { + m_bgSprite->setVisible(false); +} + +void TextInput::setString(std::string const& str, bool triggerCallback) { + auto oldDelegate = m_input->m_delegate; + // Avoid triggering the callback + m_input->m_delegate = nullptr; + m_input->setString(str); + m_input->m_delegate = oldDelegate; + if (triggerCallback && m_input->m_delegate) { + m_input->m_delegate->textChanged(m_input); + } +} + +std::string TextInput::getString() const { + return m_input->getString(); +} + +CCTextInputNode* TextInput::getInputNode() const { + return m_input; +} +CCScale9Sprite* TextInput::getBGSprite() const { + return m_bgSprite; +}