diff --git a/guichan/include/guichan/widgets/textfield.h b/guichan/include/guichan/widgets/textfield.h index 44de480ff..12e569189 100644 --- a/guichan/include/guichan/widgets/textfield.h +++ b/guichan/include/guichan/widgets/textfield.h @@ -97,6 +97,37 @@ namespace gcn */ virtual const std::string& getText() const; + /** + * Sets the maximum number of bytes that the user can type in + * the TextField. Because TextField uses the UTF-8 encoding, + * each character may require several bytes. + * + * If the text in the field is already too long, this function + * truncates it immediately. + * + * @param maxLengthBytes The new maximum length, in bytes. + * Must not be negative. + * + * In addition to this UTF-8 byte limit, we could also + * implement limits on the number of code points, the number + * of UTF-16 code units, the number of grapheme clusters, or + * the width of the string in a given font. However, there + * haven't been any use cases for those so far. + * + * @seealso #getMaxLengthBytes + */ + void setMaxLengthBytes(int maxLengthBytes); + + /** + * Gets the maximum number of bytes that the user can type in + * the TextField. + * + * @return The current maximum length, in bytes. + * + * @seealso #setMaxLengthBytes + */ + int getMaxLengthBytes() const; + /** * Draws the caret (the little marker in the text that shows where the * letters you type will appear). Easily overloaded if you want to @@ -163,10 +194,49 @@ namespace gcn */ void fixScroll(); + /** + * Truncates #mText to #mMaxLengthBytes, and #mCaretPosition + * and #mSelectStart to the length of #mText. Marks the + * widget dirty if it changes anything. + */ + void truncateToMaxLength(); + + /** + * Inserts a string at the caret, replacing the selection if any. + * + * This function also marks the widget dirty. However, it + * does not call #fixScroll because that is somewhat + * expensive and should not be called redundantly. Therefore, + * the caller must do that. + * + * @param text The text to insert. + */ + void insertAtCaret(const std::string& str); + + /** + * Finds the last UTF-8 character boundary in the first + * @a maxBytes bytes of @a str. + * + * @param str UTF-8 string. + * + * @param maxBytes How many bytes to consider at the beginning + * of @a str. Must not be negative. + * + * @return The position of the last character boundary. Always + * <= @a maxBytes. + */ + static int UTF8LastCharacterBoundary(const std::string &str, int maxBytes); + std::string mText; int mCaretPosition; int mXScroll; int mSelectStart; + + /** + * The maximum length of #mText, in bytes. Might be zero but + * never negative. + */ + int mMaxLengthBytes; }; } diff --git a/guichan/widgets/textfield.cpp b/guichan/widgets/textfield.cpp index 2510fd575..4ff8bbff7 100644 --- a/guichan/widgets/textfield.cpp +++ b/guichan/widgets/textfield.cpp @@ -56,6 +56,7 @@ * For comments regarding functions please see the header file. */ +#include <limits> #include "guichan/keyinput.h" #include "guichan/mouseinput.h" #include "guichan/widgets/textfield.h" @@ -69,6 +70,7 @@ namespace gcn mCaretPosition = 0; mXScroll = 0; mSelectStart = 0; + mMaxLengthBytes = std::numeric_limits<int>::max(); setFocusable(true); @@ -83,8 +85,11 @@ namespace gcn mCaretPosition = 0; mXScroll = 0; mSelectStart = 0; + mMaxLengthBytes = std::numeric_limits<int>::max(); mText = text; + // std::string::max_size() might exceed std::numeric_limits<int>::max(). + truncateToMaxLength(); adjustSize(); setBorderSize(1); @@ -96,14 +101,10 @@ namespace gcn void TextField::setText(const std::string& text) { - if ((int)text.size() < mCaretPosition ) - { - mCaretPosition = text.size(); - } - + mText = text; + truncateToMaxLength(); mSelectStart = mCaretPosition; - - mText = text; + setDirty(true); } void TextField::draw(Graphics* graphics) @@ -191,9 +192,10 @@ namespace gcn { std::string str; if (GetClipboard(str) >= 0) { - for (size_t i = 0; i < str.size(); ++i) { - keyPress(Key(str[i])); - } + // GetClipboard ensures that the string does not + // contain control characters. + insertAtCaret(str); + fixScroll(); } } } @@ -319,36 +321,20 @@ namespace gcn { std::string str; - if (selLen > 0) { - mText.erase(selFirst, selLen); - mCaretPosition = selFirst; - mSelectStart = selFirst; - } - if (GetClipboard(str) >= 0) { - for (size_t i = 0; i < str.size(); ++i) { - keyPress(Key(str[i])); - } + // GetClipboard ensures that the string does not + // contain control characters. + insertAtCaret(str); } + // Even if GetClipboard failed, we did recognize the key + // and the caller should not treat it as a hot key. ret = true; } else if (key.isCharacter()) { - if (selLen > 0) { - mText.erase(selFirst, selLen); - mCaretPosition = selFirst; - mSelectStart = selFirst; - } - - mText.insert(mCaretPosition,key.toString()); - int newpos = UTF8GetNext(mText, mCaretPosition); - if (newpos > (int)mText.size()) { - throw GCN_EXCEPTION("Invalid UTF8."); - } - mCaretPosition = newpos; - mSelectStart = newpos; + insertAtCaret(key.toString()); ret = true; } @@ -434,4 +420,131 @@ namespace gcn { fixScroll(); } + + int TextField::getMaxLengthBytes() const + { + return mMaxLengthBytes; + } + + void TextField::setMaxLengthBytes(int maxLengthBytes) + { + if (maxLengthBytes < 0) + { + maxLengthBytes = 0; + } + + mMaxLengthBytes = maxLengthBytes; + truncateToMaxLength(); + } + + void TextField::truncateToMaxLength() + { + bool changedSomething = false; + + // Because we never let mMaxLengthBytes become negative, + // the following static_cast cannot wrap around. + if (mText.size() > static_cast<unsigned int>(mMaxLengthBytes)) + { + int newLength = UTF8LastCharacterBoundary(mText, mMaxLengthBytes); + mText.resize(newLength); + changedSomething = true; + } + + // Because of mMaxLengthBytes, the following static_casts + // cannot overflow. + if (mCaretPosition > static_cast<int>(mText.size())) + { + mCaretPosition = mText.size(); + changedSomething = true; + } + if (mSelectStart > static_cast<int>(mText.size())) + { + mSelectStart = mText.size(); + changedSomething = true; + } + + if (changedSomething) + { + fixScroll(); + setDirty(true); + } + } + + int TextField::UTF8LastCharacterBoundary(const std::string &str, + int maxBytes) + { + if (maxBytes < 0) + { + // A bug in the caller. + maxBytes = 0; + } + + // The following static_cast cannot wrap around because + // maxBytes cannot be negative at this point. + if (str.size() < static_cast<unsigned int>(maxBytes)) + { + // The following static_cast cannot overflow because + // str.size() is less than int maxBytes. + maxBytes = static_cast<int>(str.size()); + } + + if (maxBytes == 0) + { + // There is no space for any character. + return 0; + } + + // The following would be easy to implement as: + // return UTF8GetPrev(str, maxBytes + 1); + // but maxBytes + 1 might overflow, so don't do that. + + // Find the previous character boundary. + int prev = UTF8GetPrev(str, maxBytes); + // Assert(prev < maxBytes); + + // If maxBytes already is at a character + // boundary, then that character will fit. + int next = UTF8GetNext(str, prev); + if (next == maxBytes) + { + return maxBytes; + } + else + { + return prev; + } + } + + void TextField::insertAtCaret(const std::string& str) + { + int bytesFree = mMaxLengthBytes - mText.size(); + if (bytesFree < 0) + { + bytesFree = 0; + } + + int bytesToCopy; + if (str.size() > static_cast<unsigned int>(bytesFree)) + { + bytesToCopy = UTF8LastCharacterBoundary(str, bytesFree); + } + else + { + bytesToCopy = static_cast<int>(str.size()); + } + + unsigned int selFirst; + unsigned int selLen; + getTextSelectionPositions(&selFirst, &selLen); + if (selLen > 0) { + mText.erase(selFirst, selLen); + mCaretPosition = selFirst; + mSelectStart = selFirst; + } + + mText.insert(mCaretPosition, str, 0, bytesToCopy); + mCaretPosition += bytesToCopy; + mSelectStart = mCaretPosition; + setDirty(true); + } } diff --git a/tolua/tolua.cpp b/tolua/tolua.cpp index f39cf9490..399228124 100644 --- a/tolua/tolua.cpp +++ b/tolua/tolua.cpp @@ -1,6 +1,6 @@ /* ** Lua binding: stratagus -** Generated automatically by tolua++-1.0.92 on Sat Jul 16 22:41:54 2011. +** Generated automatically by tolua++-1.0.93 on Sun Oct 2 03:06:13 2011. */ #ifndef __cplusplus @@ -9830,6 +9830,71 @@ static int tolua_stratagus_TextField_getText00(lua_State* tolua_S) } #endif //#ifndef TOLUA_DISABLE +/* method: setMaxLengthBytes of class TextField */ +#ifndef TOLUA_DISABLE_tolua_stratagus_TextField_setMaxLengthBytes00 +static int tolua_stratagus_TextField_setMaxLengthBytes00(lua_State* tolua_S) +{ +#ifndef TOLUA_RELEASE + tolua_Error tolua_err; + if ( + !tolua_isusertype(tolua_S,1,"TextField",0,&tolua_err) || + !tolua_isnumber(tolua_S,2,0,&tolua_err) || + !tolua_isnoobj(tolua_S,3,&tolua_err) + ) + goto tolua_lerror; + else +#endif + { + TextField* self = (TextField*) tolua_tousertype(tolua_S,1,0); + int maxLengthBytes = ((int) tolua_tonumber(tolua_S,2,0)); +#ifndef TOLUA_RELEASE + if (!self) tolua_error(tolua_S,"invalid 'self' in function 'setMaxLengthBytes'", NULL); +#endif + { + self->setMaxLengthBytes(maxLengthBytes); + } + } + return 0; +#ifndef TOLUA_RELEASE + tolua_lerror: + tolua_error(tolua_S,"#ferror in function 'setMaxLengthBytes'.",&tolua_err); + return 0; +#endif +} +#endif //#ifndef TOLUA_DISABLE + +/* method: getMaxLengthBytes of class TextField */ +#ifndef TOLUA_DISABLE_tolua_stratagus_TextField_getMaxLengthBytes00 +static int tolua_stratagus_TextField_getMaxLengthBytes00(lua_State* tolua_S) +{ +#ifndef TOLUA_RELEASE + tolua_Error tolua_err; + if ( + !tolua_isusertype(tolua_S,1,"const TextField",0,&tolua_err) || + !tolua_isnoobj(tolua_S,2,&tolua_err) + ) + goto tolua_lerror; + else +#endif + { + const TextField* self = (const TextField*) tolua_tousertype(tolua_S,1,0); +#ifndef TOLUA_RELEASE + if (!self) tolua_error(tolua_S,"invalid 'self' in function 'getMaxLengthBytes'", NULL); +#endif + { + int tolua_ret = (int) self->getMaxLengthBytes(); + tolua_pushnumber(tolua_S,(lua_Number)tolua_ret); + } + } + return 1; +#ifndef TOLUA_RELEASE + tolua_lerror: + tolua_error(tolua_S,"#ferror in function 'getMaxLengthBytes'.",&tolua_err); + return 0; +#endif +} +#endif //#ifndef TOLUA_DISABLE + /* method: new of class ListBoxWidget */ #ifndef TOLUA_DISABLE_tolua_stratagus_ListBoxWidget_new00 static int tolua_stratagus_ListBoxWidget_new00(lua_State* tolua_S) @@ -20179,6 +20244,8 @@ TOLUA_API int tolua_stratagus_open (lua_State* tolua_S) tolua_function(tolua_S,".call",tolua_stratagus_TextField_new00_local); tolua_function(tolua_S,"setText",tolua_stratagus_TextField_setText00); tolua_function(tolua_S,"getText",tolua_stratagus_TextField_getText00); + tolua_function(tolua_S,"setMaxLengthBytes",tolua_stratagus_TextField_setMaxLengthBytes00); + tolua_function(tolua_S,"getMaxLengthBytes",tolua_stratagus_TextField_getMaxLengthBytes00); tolua_endmodule(tolua_S); tolua_cclass(tolua_S,"ListBox","ListBox","Widget",NULL); tolua_beginmodule(tolua_S,"ListBox"); diff --git a/tolua/ui.pkg b/tolua/ui.pkg index 34d449347..63f99a4f6 100644 --- a/tolua/ui.pkg +++ b/tolua/ui.pkg @@ -442,6 +442,8 @@ class TextField : public Widget TextField(const std::string text); virtual void setText(const std::string text); virtual std::string &getText(); + void setMaxLengthBytes(int maxLengthBytes); + int getMaxLengthBytes() const; }; class ListBox : public Widget