/* * CTextInput.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "CTextInput.h" #include "Images.h" #include "TextControls.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../render/Graphics.h" #include "../render/IFont.h" #include "../render/IRenderHandler.h" #include "../../lib/texts/TextOperations.h" std::list CFocusable::focusables; CFocusable * CFocusable::inputWithFocus; CTextInput::CTextInput(const Rect & Pos) :originalAlignment(ETextAlignment::CENTERLEFT) { pos += Pos.topLeft(); pos.h = Pos.h; pos.w = Pos.w; addUsedEvents(LCLICK | SHOW_POPUP | KEYBOARD | TEXTINPUT); } void CTextInput::createLabel(bool giveFocusToInput) { OBJECT_CONSTRUCTION; label = std::make_shared(); label->pos = pos; label->alignment = originalAlignment; #if !defined(VCMI_MOBILE) if(giveFocusToInput) giveFocus(); #endif } CTextInput::CTextInput(const Rect & Pos, EFonts font, ETextAlignment alignment, bool giveFocusToInput) : CTextInput(Pos) { originalAlignment = alignment; setRedrawParent(true); createLabel(giveFocusToInput); setFont(font); setAlignment(alignment); } CTextInput::CTextInput(const Rect & Pos, const Point & bgOffset, const ImagePath & bgName) : CTextInput(Pos) { OBJECT_CONSTRUCTION; if (!bgName.empty()) background = std::make_shared(bgName, bgOffset.x, bgOffset.y); else setRedrawParent(true); createLabel(true); } CTextInput::CTextInput(const Rect & Pos, std::shared_ptr srf) : CTextInput(Pos) { OBJECT_CONSTRUCTION; background = std::make_shared(srf, Pos); pos.w = background->pos.w; pos.h = background->pos.h; background->pos = pos; createLabel(true); } void CTextInput::setFont(EFonts font) { label->font = font; } void CTextInput::setColor(const ColorRGBA & color) { label->color = color; } void CTextInput::setAlignment(ETextAlignment alignment) { originalAlignment = alignment; label->alignment = alignment; } const std::string & CTextInput::getText() const { return currentText; } void CTextInput::setCallback(const TextEditedCallback & cb) { assert(!onTextEdited); onTextEdited = cb; } void CTextInput::setPopupCallback(const std::function & cb) { callbackPopup = cb; } void CTextInput::setFilterFilename() { assert(!onTextFiltering); onTextFiltering = std::bind(&CTextInput::filenameFilter, _1, _2); } void CTextInput::setFilterNumber(int minValue, int maxValue) { onTextFiltering = std::bind(&CTextInput::numberFilter, _1, _2, minValue, maxValue); } std::string CTextInput::getVisibleText() const { return hasFocus() ? currentText + composedText + "_" : currentText; } void CTextInput::showPopupWindow(const Point & cursorPosition) { if(callbackPopup) callbackPopup(); } void CTextInput::clickPressed(const Point & cursorPosition) { // attempt to give focus unconditionally, even if we already have it // this forces on-screen keyboard to show up again, even if player have closed it before giveFocus(); } void CTextInput::keyPressed(EShortcut key) { if(!hasFocus()) return; if(key == EShortcut::GLOBAL_MOVE_FOCUS) { moveFocus(); return; } bool redrawNeeded = false; switch(key) { case EShortcut::GLOBAL_BACKSPACE: if(!composedText.empty()) { TextOperations::trimRightUnicode(composedText); redrawNeeded = true; } else if(!currentText.empty()) { TextOperations::trimRightUnicode(currentText); redrawNeeded = true; } break; default: break; } if(redrawNeeded) { updateLabel(); if(onTextEdited) onTextEdited(currentText); } } void CTextInput::setText(const std::string & nText) { currentText = nText; updateLabel(); } void CTextInput::updateLabel() { std::string visibleText = getVisibleText(); label->alignment = originalAlignment; const auto & font = GH.renderHandler().loadFont(label->font); while (font->getStringWidth(visibleText) > pos.w) { label->alignment = ETextAlignment::CENTERRIGHT; visibleText = visibleText.substr(TextOperations::getUnicodeCharacterSize(visibleText[0])); } label->setText(visibleText); } void CTextInput::textInputted(const std::string & enteredText) { if(!hasFocus()) return; std::string oldText = currentText; setText(getText() + enteredText); if(onTextFiltering) onTextFiltering(currentText, oldText); if(currentText != oldText) { updateLabel(); if(onTextEdited) onTextEdited(currentText); } composedText.clear(); } void CTextInput::textEdited(const std::string & enteredText) { if(!hasFocus()) return; composedText = enteredText; updateLabel(); } void CTextInput::filenameFilter(std::string & text, const std::string &oldText) { static const std::string forbiddenChars = "<>:\"/\\|?*\r\n"; //if we are entering a filename, some special characters won't be allowed size_t pos; while((pos = text.find_first_of(forbiddenChars)) != std::string::npos) text.erase(pos, 1); } void CTextInput::numberFilter(std::string & text, const std::string & oldText, int minValue, int maxValue) { assert(minValue < maxValue); if(text.empty()) text = "0"; size_t pos = 0; if(text[0] == '-') //allow '-' sign as first symbol only pos++; while(pos < text.size()) { if(text[pos] < '0' || text[pos] > '9') { text = oldText; return; //new text is not number. } pos++; } try { int value = boost::lexical_cast(text); if(value < minValue) text = std::to_string(minValue); else if(value > maxValue) text = std::to_string(maxValue); } catch(boost::bad_lexical_cast &) { //Should never happen. Unless I missed some cases logGlobal->warn("Warning: failed to convert %s to number!", text); text = oldText; } } void CTextInput::activate() { CFocusable::activate(); if (hasFocus()) { #if defined(VCMI_MOBILE) //giveFocus(); #else GH.startTextInput(pos); #endif } } void CTextInput::deactivate() { CFocusable::deactivate(); if (hasFocus()) { #if defined(VCMI_MOBILE) removeFocus(); #else GH.stopTextInput(); #endif } } void CTextInput::onFocusGot() { updateLabel(); } void CTextInput::onFocusLost() { updateLabel(); } void CFocusable::focusGot() { if (isActive()) GH.startTextInput(pos); onFocusGot(); } void CFocusable::focusLost() { if (isActive()) GH.stopTextInput(); onFocusLost(); } CFocusable::CFocusable() { focusables.push_back(this); } CFocusable::~CFocusable() { if(hasFocus()) inputWithFocus = nullptr; focusables -= this; } bool CFocusable::hasFocus() const { return inputWithFocus == this; } void CFocusable::giveFocus() { auto previousInput = inputWithFocus; inputWithFocus = this; if(previousInput) previousInput->focusLost(); focusGot(); } void CFocusable::moveFocus() { auto i = vstd::find(focusables, this); auto ourIt = i; for(i++; i != ourIt; i++) { if(i == focusables.end()) i = focusables.begin(); if(*i == this) return; if((*i)->isActive()) { (*i)->giveFocus(); break; } } } void CFocusable::removeFocus() { if(this == inputWithFocus) { inputWithFocus = nullptr; focusLost(); } }