Index: doc/entry.n ================================================================== --- doc/entry.n +++ doc/entry.n @@ -90,13 +90,23 @@ and relief. The \fBentry\fR command returns its \fIpathName\fR argument. At the time this command is invoked, there must not exist a window named \fIpathName\fR, but \fIpathName\fR's parent must exist. .PP -An entry is a widget that displays a one-line text string and -allows that string to be edited using widget commands described below, which +An entry is a widget that displays one line of text and +allows that text to be edited using widget commands described below, which are typically bound to keystrokes and mouse actions. +.PP +The text is represented internally as a sequence of unicode code points. If +Tk is compiled with the flag USE_GLYPH_INDEXES then the text is viewed as a +sequence of glyphs, each represented by a unicode grapheme cluster which may +contain several unicode code points. (For example, the flag of Wales requires +seven code points while some emojis require four.) In this case, the word +"character" should be understood to mean a glyph in the discussion below. If +the USE_GLYPH_INDEXES flag is not defined then it refers to a single unicode +code point. +.PP When first created, an entry's string is empty. A portion of the entry may be selected as described below. If an entry is exporting its selection (see the \fB\-exportselection\fR option), then it will observe the standard X11 protocols for handling the selection; entry selections are available as type \fBSTRING\fR. @@ -294,10 +304,18 @@ .TP \fIpathName \fBinsert \fIindex string\fR Insert the characters of \fIstring\fR just before the character indicated by \fIindex\fR. Returns an empty string. .TP +\fIpathName \fBrange \fIfirst last\fR +Returns a string consisting of the characters in the entry beginning with the +character represented by the index \fIfirst\fR and ending with the character +represented by the index \fIlast\fR. The numerical values of \fIfirst\fR and +\fIlast\fR can be arbitrary, but negative values are replaced by 0 while +values larger than the length of the entry string is the length. An empyt +string is returned if \fIfirst\fR > \fIlast\fR. +.TP \fIpathName \fBscan\fR \fIoption args\fR This command is used to implement scanning on entries. It has two forms, depending on \fIoption\fR: .RS .TP Index: generic/tkEntry.c ================================================================== --- generic/tkEntry.c +++ generic/tkEntry.c @@ -329,16 +329,16 @@ * dispatch the entry widget command. */ static const char *const entryCmdNames[] = { "bbox", "cget", "configure", "delete", "get", "icursor", "index", - "insert", "scan", "selection", "validate", "xview", NULL + "insert", "range", "scan", "selection", "validate", "xview", NULL }; enum entryCmd { COMMAND_BBOX, COMMAND_CGET, COMMAND_CONFIGURE, COMMAND_DELETE, - COMMAND_GET, COMMAND_ICURSOR, COMMAND_INDEX, COMMAND_INSERT, + COMMAND_GET, COMMAND_ICURSOR, COMMAND_INDEX, COMMAND_INSERT, COMMAND_RANGE, COMMAND_SCAN, COMMAND_SELECTION, COMMAND_VALIDATE, COMMAND_XVIEW }; static const char *const selCmdNames[] = { "adjust", "clear", "from", "present", "range", "to", NULL @@ -482,19 +482,18 @@ *-------------------------------------------------------------- */ int Tk_EntryObjCmd( - ClientData dummy, /* NULL. */ + ClientData dummy, /* NULL. */ Tcl_Interp *interp, /* Current interpreter. */ int objc, /* Number of arguments. */ Tcl_Obj *const objv[]) /* Argument objects. */ { Entry *entryPtr; Tk_OptionTable optionTable; Tk_Window tkwin; - char *tmp; (void)dummy; if (objc < 2) { Tcl_WrongNumArgs(interp, 1, objv, "pathName ?-option value ...?"); return TCL_ERROR; @@ -529,13 +528,19 @@ entryPtr->widgetCmd = Tcl_CreateObjCommand(interp, Tk_PathName(entryPtr->tkwin), EntryWidgetObjCmd, entryPtr, EntryCmdDeletedProc); entryPtr->optionTable = optionTable; entryPtr->type = TK_ENTRY; - tmp = (char *)ckalloc(1); - tmp[0] = '\0'; - entryPtr->string = tmp; +#ifndef USE_GLYPH_INDEXES + { + char *tmp = (char *)ckalloc(1); + tmp[0] = '\0'; + entryPtr->string = tmp; + } +#else + entryPtr->manager = TkpTextManagerCreate(&entryPtr->string); +#endif entryPtr->selectFirst = -1; entryPtr->selectLast = -1; entryPtr->cursor = NULL; entryPtr->exportSelection = 1; @@ -733,10 +738,15 @@ } if (GetEntryIndex(interp, entryPtr, objv[2], &index) != TCL_OK) { goto error; } + +#ifdef USE_GLYPH_INDEXES + index = TkpTextManagerContainingCluster(entryPtr->manager, index, NULL); +#endif + Tcl_SetObjResult(interp, Tcl_NewWideIntObj(index)); break; } case COMMAND_INSERT: { @@ -756,10 +766,52 @@ goto error; } } break; } + + case COMMAND_RANGE: { + const char *substring; + int first, last; + if (objc != 4) { + Tcl_WrongNumArgs(interp, 2, objv, "first last"); + goto error; + } + if (Tcl_GetIntFromObj(interp, objv[2], &first) != TCL_OK) { + goto error; + } + if (Tcl_GetIntFromObj(interp, objv[3], &last) != TCL_OK) { + goto error; + } +#ifndef USE_GLYPH_INDEXES + if (first < 0) { + first = 0; + } + if (last < first) { + Tcl_SetObjResult(interp, Tcl_NewObj()); + } else { + const char *end; + int length; + Tcl_UniChar ch = 0; + substring = Tcl_UtfAtIndex(entryPtr->displayString, first); + length = Tcl_NumUtfChars(entryPtr->displayString, TCL_INDEX_NONE); + if (last >= length) { + Tcl_SetObjResult(interp, Tcl_NewStringObj(substring, TCL_INDEX_NONE)); + } else { + end = Tcl_UtfAtIndex(entryPtr->displayString, last); + end += Tcl_UtfToUniChar(end, &ch); + Tcl_SetObjResult(interp, Tcl_NewStringObj(substring, + end - substring)); + } + } +#else + substring = TkpTextManagerUTF8StringForClusterRange(entryPtr->manager, + first, last); + Tcl_SetObjResult(interp, Tcl_NewStringObj(substring, TCL_INDEX_NONE)); +#endif + break; + } case COMMAND_SCAN: { int x; const char *minorCmd; @@ -1039,11 +1091,16 @@ /* * Free up all the stuff that requires special handling, then let * Tk_FreeOptions handle all the standard option-related stuff. */ +#ifndef USE_GLYPH_INDEXES ckfree((char *)entryPtr->string); +#else + TkpTextManagerDestroy(entryPtr->manager); +#endif + if (entryPtr->textVarName != NULL) { Tcl_UntraceVar2(entryPtr->interp, entryPtr->textVarName, NULL, TCL_GLOBAL_ONLY|TCL_TRACE_WRITES|TCL_TRACE_UNSETS, EntryTextVarProc, entryPtr); entryPtr->flags &= ~ENTRY_VAR_TRACED; @@ -1765,11 +1822,10 @@ /* * Draw the text in two pieces: first the unselected portion, then the * selected portion on top of it. */ - if ((entryPtr->numChars != 0) || (entryPtr->placeholderChars == 0)) { Tk_DrawTextLayout(entryPtr->display, pixmap, entryPtr->textGC, entryPtr->textLayout, entryPtr->layoutX, entryPtr->layoutY, entryPtr->leftIndex, entryPtr->numChars); } else { @@ -2113,11 +2169,11 @@ /* *---------------------------------------------------------------------- * * InsertChars -- * - * Add new characters to an entry widget. + * Add new grapheme clusters to an entry widget. * * Results: * A standard Tcl result. If an error occurred then an error message is * left in the interp's result. * @@ -2132,39 +2188,41 @@ InsertChars( Entry *entryPtr, /* Entry that is to get the new elements. */ int index, /* Add the new elements before this character * index. */ const char *value) /* New characters to add (NULL-terminated - * string). */ + * UTF-8 encoded string). */ { - size_t byteIndex, byteCount, newByteCount, oldChars, charsAdded; - const char *string; + int byteCount, oldChars, charsAdded; char *newStr; - string = entryPtr->string; - byteIndex = Tcl_UtfAtIndex(string, index) - string; byteCount = strlen(value); if (byteCount == 0) { return TCL_OK; } +#ifndef USE_GLYPH_INDEXES + size_t byteIndex, newByteCount; + const char *string; + string = entryPtr->string; + byteIndex = Tcl_UtfAtIndex(string, index) - string; + newByteCount = entryPtr->numBytes + byteCount + 1; newStr = (char *)ckalloc(newByteCount); memcpy(newStr, string, byteIndex); strcpy(newStr + byteIndex, value); strcpy(newStr + byteIndex + byteCount, string + byteIndex); if ((entryPtr->validate == VALIDATE_KEY || - entryPtr->validate == VALIDATE_ALL) && - EntryValidateChange(entryPtr, value, newStr, index, - VALIDATE_INSERT) != TCL_OK) { + entryPtr->validate == VALIDATE_ALL) && + EntryValidateChange(entryPtr, value, newStr, index, + VALIDATE_INSERT) != TCL_OK) { ckfree(newStr); return TCL_OK; } ckfree((char *)string); - entryPtr->string = newStr; /* * The following construction is used because inserting improperly formed * UTF-8 sequences between other improperly formed UTF-8 sequences could * result in actually forming valid UTF-8 sequences; the number of @@ -2175,15 +2233,35 @@ oldChars = entryPtr->numChars; entryPtr->numChars = Tcl_NumUtfChars(newStr, TCL_INDEX_NONE); charsAdded = entryPtr->numChars - oldChars; entryPtr->numBytes += byteCount; +#else + oldChars = entryPtr->numChars; + if (entryPtr->validate == VALIDATE_KEY || + entryPtr->validate == VALIDATE_ALL) { + newStr = (char *) TkpTextManagerInsert(entryPtr->manager, index, value, + &entryPtr->numChars, &entryPtr->numBytes, + &entryPtr->string); + if (EntryValidateChange(entryPtr, value, newStr, index, + VALIDATE_INSERT) != TCL_OK) { + entryPtr->string = TkpTextManagerRevert(entryPtr->manager, &entryPtr->numChars, + &entryPtr->numBytes); + return TCL_OK; + } + } else { + newStr = (char *) TkpTextManagerInsert(entryPtr->manager, index, value, + &entryPtr->numChars, &entryPtr->numBytes, NULL); + } + charsAdded = entryPtr->numChars - oldChars; +#endif - if (entryPtr->displayString == string) { + if (entryPtr->displayString == entryPtr->string) { entryPtr->displayString = newStr; entryPtr->numDisplayBytes = entryPtr->numBytes; } + entryPtr->string = newStr; /* * Inserting characters invalidates all indexes into the string. Touch up * the indexes so that they still refer to the same characters (at new * positions). When updating the selection end-points, don't include the @@ -2212,11 +2290,11 @@ /* *---------------------------------------------------------------------- * * DeleteChars -- * - * Remove one or more characters from an entry widget. + * Remove one or more grapheme clusters from an entry widget. * * Results: * A standard Tcl result. If an error occurred then an error message is * left in the interp's result. * @@ -2228,16 +2306,19 @@ */ static int DeleteChars( Entry *entryPtr, /* Entry widget to modify. */ - int index, /* Index of first character to delete. */ - int count) /* How many characters to delete. */ + int index, /* Index of first cluster to delete. */ + int count) /* How many clusters to delete. */ { + char *newStr; + const char *string = entryPtr->string; + char *toDelete; + +#ifndef USE_GLYPH_INDEXES int byteIndex, byteCount, newByteCount; - const char *string; - char *newStr, *toDelete; if ((index + count) > entryPtr->numChars) { count = entryPtr->numChars - index; } if (count <= 0) { @@ -2263,16 +2344,33 @@ VALIDATE_DELETE) != TCL_OK) { ckfree(newStr); ckfree(toDelete); return TCL_OK; } - ckfree(toDelete); ckfree((char *)entryPtr->string); - entryPtr->string = newStr; entryPtr->numChars -= count; entryPtr->numBytes -= byteCount; +#else + if ((entryPtr->validate == VALIDATE_KEY || + entryPtr->validate == VALIDATE_ALL)) { + newStr = (char *) TkpTextManagerDelete(entryPtr->manager, index, count, + &entryPtr->numChars, &entryPtr->numBytes, &count, + &toDelete, &entryPtr->string); + if (EntryValidateChange(entryPtr, toDelete, newStr, index, + VALIDATE_DELETE) != TCL_OK) { + entryPtr->string = TkpTextManagerRevert(entryPtr->manager, &entryPtr->numChars, + &entryPtr->numBytes); + return TCL_OK; + } + } else { + newStr = (char *) TkpTextManagerDelete(entryPtr->manager, index, count, + &entryPtr->numChars, &entryPtr->numBytes, &count, + NULL, NULL); + } +#endif + entryPtr->string = newStr; if (entryPtr->displayString == string) { entryPtr->displayString = newStr; entryPtr->numDisplayBytes = entryPtr->numBytes; } @@ -2461,10 +2559,12 @@ return; } } oldSource = entryPtr->string; + +#ifndef USE_GLYPH_INDEXES ckfree((char *)entryPtr->string); if (malloced) { entryPtr->string = value; } else { @@ -2473,14 +2573,18 @@ strcpy(tmp, value); entryPtr->string = tmp; } entryPtr->numBytes = valueLen; entryPtr->numChars = Tcl_NumUtfChars(value, valueLen); +#else + entryPtr->string = TkpTextManagerSet(entryPtr->manager, value, + &entryPtr->numChars, &entryPtr->numBytes); +#endif if (entryPtr->displayString == oldSource) { entryPtr->displayString = entryPtr->string; - entryPtr->numDisplayBytes = entryPtr->numBytes; + entryPtr->numDisplayBytes = entryPtr->numChars; } if (entryPtr->selectFirst >= 0) { if (entryPtr->selectFirst >= entryPtr->numChars) { entryPtr->selectFirst = -1; @@ -2645,26 +2749,46 @@ static int GetEntryIndex( Tcl_Interp *interp, /* For error messages. */ Entry *entryPtr, /* Entry for which the index is being - * specified. */ - Tcl_Obj *indexObj, /* Specifies character in entryPtr. */ + specified. */ + Tcl_Obj *indexObj, /* Specifies character in entryPtr. */ int *indexPtr) /* Where to store converted character index */ { TkSizeT length, idx; const char *string; +#ifndef USE_GLYPH_INDEXES if (TCL_OK == TkGetIntForIndex(indexObj, entryPtr->numChars - 1, 1, &idx)) { if (idx == TCL_INDEX_NONE) { idx = 0; } else if (idx > (TkSizeT)entryPtr->numChars) { idx = (TkSizeT)entryPtr->numChars; } *indexPtr = (int)idx; return TCL_OK; } +#else + + /* + * If we are doing glyph indexing, integer objects are given as glyph + * indexes so we need to convert them to character indexes. + */ + + TkSizeT clusterLength = (TkSizeT) TkpTextManagerNumClusters( + entryPtr->manager); + if (TCL_OK == TkGetIntForIndex(indexObj, clusterLength, 1, &idx)) { + if (idx == TCL_INDEX_NONE) { + idx = 0; + } else if (idx > clusterLength) { + idx = (TkSizeT) clusterLength + 1; + } + *indexPtr = TkpTextManagerClusterPosition(entryPtr->manager, idx, NULL); + return TCL_OK; + } +#endif string = TkGetStringFromObj(indexObj, &length); switch (string[0]) { case 'a': Index: generic/tkEntry.h ================================================================== --- generic/tkEntry.h +++ generic/tkEntry.h @@ -36,19 +36,18 @@ Tcl_Interp *interp; /* Interpreter associated with entry. */ Tcl_Command widgetCmd; /* Token for entry's widget command. */ Tk_OptionTable optionTable; /* Table that defines configuration options * available for this widget. */ enum EntryType type; /* Specialized type of Entry widget */ + ClientData manager; /* Platform-specific TextManager. */ /* * Fields that are set by widget commands other than "configure". */ - const char *string; /* Pointer to storage for string; - * NULL-terminated; malloc-ed. */ - int insertPos; /* Character index before which next typed - * character will be inserted. */ + const char *string; /* Pointer to storage for string. */ + int insertPos; /* Index of the character after the cursor */ /* * Information about what's selected, if any. */ Index: generic/tkInt.h ================================================================== --- generic/tkInt.h +++ generic/tkInt.h @@ -1347,12 +1347,51 @@ Tcl_Interp *interp, Tcl_Obj *listObj, int toplevel, Tcl_Obj *nameObj); MODULE_SCOPE void TkRotatePoint(double originX, double originY, double sine, double cosine, double *xPtr, double *yPtr); -MODULE_SCOPE int TkGetIntForIndex(Tcl_Obj *, TkSizeT, int lastOK, TkSizeT*); +MODULE_SCOPE int TkGetIntForIndex(Tcl_Obj *, TkSizeT, int lastOK, TkSizeT*); + +/* + * Unicode strings describing text are actually sequences of so-called grapheme + * clusters, each of which describes what the user perceives as a single glyph. + * When editing text, users expect to insert or delete entire glyphs, so the + * underlying string operations should insert or delete entire grapheme clusters. + * Also, indexes into the string, such as the insert cursor, should refer to + * a glyph, not a character in the string and underlying character indexes should + * always point to the base character of a grapheme cluster. + * + * The functions declared below provide an interface to an abstract TextManager + * object which can recognize boundaries of grapheme clusters and manage a + * glyph-based indexing system for Tk text-related widgets. To enable the features + * described above, a platform port should define the conditional compilation + * variable USE_GLYPH_INDEXES and implement these functions. + */ +#if defined(MAC_OSX_TK) +#define USE_GLYPH_INDEXES +MODULE_SCOPE ClientData TkpTextManagerCreate(const char **initialString); +MODULE_SCOPE void TkpTextManagerDestroy(ClientData clientData); +MODULE_SCOPE int TkpTextManagerNumClusters(ClientData clientData); +MODULE_SCOPE int TkpTextManagerClusterPosition(ClientData clientData, + int clusterIndex, int *clusterLength); +MODULE_SCOPE int TkpTextManagerContainingCluster(ClientData clientData, + int charIndex, int *clusterLength); +MODULE_SCOPE const char* TkpTextManagerUTF8StringForClusterRange(ClientData clientData, + int first, int last); +MODULE_SCOPE const char* TkpTextManagerSet(ClientData clientData, + const char *value, int *numChars, int *numBytes); +MODULE_SCOPE const char* TkpTextManagerInsert(ClientData clientData, + int charIndex, const char *value, + int *numChars, int *numBytes, const char **oldString); +MODULE_SCOPE const char* TkpTextManagerDelete(ClientData clientData, int charIndex, + int count, int *numChars, int *numBytes, + int *numDeleted, char** charsDeleted, + const char **oldString); +MODULE_SCOPE const char* TkpTextManagerRevert(ClientData clientData, + int *numChars, int *numBytes); +#endif #ifdef _WIN32 #define TkParseColor XParseColor #else MODULE_SCOPE Status TkParseColor (Display * display, Index: library/entry.tcl ================================================================== --- library/entry.tcl +++ library/entry.tcl @@ -674,11 +674,11 @@ # # Arguments: # w - The entry window from which the text to get proc ::tk::EntryGetSelection {w} { - set entryString [string range [$w get] [$w index sel.first] \ + set entryString [$w range [$w index sel.first] \ [expr {[$w index sel.last] - 1}]] if {[$w cget -show] ne ""} { return [string repeat [string index [$w cget -show] 0] \ [string length $entryString]] } Index: macosx/tkMacOSXFont.c ================================================================== --- macosx/tkMacOSXFont.c +++ macosx/tkMacOSXFont.c @@ -182,11 +182,591 @@ #ifndef __clang__ @synthesize UTF8String = _UTF8String; #endif @end -#define GetNSFontTraitsFromTkFontAttributes(faPtr) \ +/* + * Implementation of the TextManager for macOS. + * + * The TextManager is really little more than an NSMutableString, except + * that Apple says this about the UTF8String property: + * + * "This C string is a pointer to a structure inside the string object, + * which may have a lifetime shorter than the string object and will + * certainly not have a longer lifetime. Therefore, you should copy the C + * string if it needs to be stored outside of the memory context in which + * you use this property." + * + */ + +typedef struct TextManager { + NSMutableString *string; + NSMutableString *backup; + char *utf8string; /* We need to store a copy of the UTF8String */ + int numClusters; +} TextManager; + +/* + * Static functions used to access a TextManager. + */ + +/* + * Called after the NSMutableString has been changed. It resets the counts of + * bytes and chars and saves a copy of the UTF8String of the NSMutableString. + */ + +static char * +TextManagerUpdate( + TextManager *managerPtr, + int *numChars, + int *numBytes) +{ + *numChars = [managerPtr->string length]; + *numBytes = [managerPtr->string lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + if (managerPtr->utf8string) { + ckfree(managerPtr->utf8string); + } + managerPtr->utf8string = ckalloc(*numBytes + 1); + strcpy(managerPtr->utf8string, [managerPtr->string UTF8String]); + managerPtr->numClusters = -1; /* recomputed by TkpTextManagerNumClusters */ + return managerPtr->utf8string; +} + +/* + * Destroy the cached NSString, if there is one. + */ + +static void +TextManagerClearCache( + TextManager *managerPtr) +{ + if (managerPtr->backup) { + [managerPtr->backup release]; + managerPtr->backup = nil; + } +} + +/* + * Cache a copy of the current string, for use when reverting changes after + * validation fails. + */ + +static void +TextManagerCache( + TextManager *managerPtr) +{ + TextManagerClearCache(managerPtr); + managerPtr->backup = [[NSMutableString stringWithString:managerPtr->string] + retain]; +} + +/* + * Return the character range of of the cluster with given index, or a range + * with length 0 and location equal to the string length. + * + * NOTE: A future optimization could cache the character index of the last base + * character which was looked up inside the TextManager, as a hint. Then the + * next time a base character index were needed, the search could begin at the + * hint location instead of the beginning of the string. Since changes to the + * insert cursor are usually small, this would be quite a bit faster. + */ + +static NSRange +ClusterRange( + NSString *string, + NSUInteger clusterIndex) +{ + NSRange clusterRange = NSMakeRange(0, 0); + NSUInteger i, charIndex = 0, end = [string length]; + + if (end == 0) { + return NSMakeRange(0,0); + } + for (i = 0; i < clusterIndex; i++) { + clusterRange = [string rangeOfComposedCharacterSequenceAtIndex:charIndex]; + charIndex = clusterRange.location + clusterRange.length; + if (charIndex >= end) { + return NSMakeRange(charIndex, 0); + break; + } + } + return [string rangeOfComposedCharacterSequenceAtIndex:charIndex]; +} + +/* + * Returns the index of the cluster which constains the the character with + * given index, or the total number of clusters. + * + * NOTE: This could also benefit from a cached hint. + */ + +static NSUInteger +IndexOfContainingCluster( + NSString *string, + NSUInteger charIndex, + int *clusterLength) +{ + NSRange clusterRange; + NSUInteger idx, clusterIndex; + + if (charIndex > string.length) { + charIndex = string.length; + } + for (idx = 0, clusterIndex = 0; idx < charIndex; clusterIndex++) { + clusterRange = [string rangeOfComposedCharacterSequenceAtIndex:idx]; + idx += clusterRange.length; + if (idx > charIndex) { + if (clusterLength) { + clusterLength = 0; + } + return clusterIndex; + } + } + if (clusterLength) { + *clusterLength = clusterRange.length; + } + + return clusterIndex; +} + +/* + * Computes the range of characters filled by a range of clusters of given + * length, such that a given character is contained in the first cluster. + * Used by TkpTextManagerDelete to determine which chars to delete from + * the NSMutableString. + */ + +static NSRange +CharRangeFromClusterRange( + NSString *string, + NSUInteger charIndex, + NSUInteger clusterCount) +{ + NSRange clusterRange; + NSUInteger max = string.length, charLocation, charLength = 0; + + if (max == 0 || charIndex >= max) { + return NSMakeRange(max, 0); + } + clusterRange = [string rangeOfComposedCharacterSequenceAtIndex:charIndex]; + charLocation = clusterRange.location; + charIndex = charLocation; + + while (clusterCount--) { + clusterRange = [string rangeOfComposedCharacterSequenceAtIndex:charIndex]; + charLength += clusterRange.length; + charIndex += clusterRange.length; + if (charIndex >= max) { + return NSMakeRange(charLocation, max - charLocation); + } + } + return NSMakeRange(charLocation, charLength); +} + +/* + *--------------------------------------------------------------------------- + * + * TkpTextManagerCreate -- + * + * Allocate and initialize a TextManager to handle glyph-based indexing. + * + * Results: + * A pointer to a TextManager, cast as an opaque ClientData type. + * + * Side effects: + * Allocates a TextManager, an NSString and a UTF-8 char buffer. + * Also stores a pointer to the initial (empty) UTF-8 string in + * the variable referenced by the initialString parameter. + * + *--------------------------------------------------------------------------- + */ + +ClientData +TkpTextManagerCreate( + const char **initialString) +{ + TextManager *managerPtr = (TextManager *) ckalloc(sizeof(TextManager)); + int dummy; + + managerPtr->string = [[NSMutableString string] retain]; + managerPtr->backup = nil; + managerPtr->utf8string = NULL; + *initialString = TextManagerUpdate(managerPtr, &dummy, &dummy); + return (ClientData) managerPtr; +} + +/* + *--------------------------------------------------------------------------- + * + * TkpTextManagerDestroy -- + * + * Free the resources associated to a TextManager + * + * Results: + * None + * + * Side effects: + * Release the NSString, frees the UTF8 string and the TextManager. + * + *--------------------------------------------------------------------------- + */ + +void +TkpTextManagerDestroy( +ClientData clientData) +{ + TextManager *managerPtr = (TextManager *) clientData; + + [managerPtr->string release]; + if (managerPtr->backup) { + [managerPtr->backup release]; + } + if (managerPtr->utf8string) { + ckfree(managerPtr->utf8string); + } + ckfree(managerPtr); +} + +/* + *--------------------------------------------------------------------------- + * + * TkpTextManagerClusterPosition -- + * + * Computes the character index of the base character of the cluster + * with the given cluster index. If the clusterLength parameter is + * not NULL, the integer variable that it references is set to the + * length of the cluster. + * + * Results: + * A cluster index. + * + * Side effects: + * None. + * + *--------------------------------------------------------------------------- + */ + +int +TkpTextManagerClusterPosition( + ClientData clientData, + int clusterIndex, + int *clusterLength) +{ + TextManager *managerPtr = (TextManager *) clientData; + NSRange range; + if (clusterIndex < 0) { + clusterIndex = 0; + } + range = ClusterRange(managerPtr->string, clusterIndex); + if (clusterLength) { + *clusterLength = range.length; + } + return range.location; +} + +/* + *--------------------------------------------------------------------------- + * + * TkpTextManagerContainingCluster -- + * + * Given a character index, find the index of the cluster which contains + * the indexed character. If the parameter clusterLength is not NULL the + * integer variable that it references is set to the length of the cluster. + * + * Results: + * A cluster index. + * + * Side effects: + * None. + * + *--------------------------------------------------------------------------- + */ + +int +TkpTextManagerContainingCluster( + ClientData clientData, + int charIndex, + int *clusterLength) +{ + TextManager *managerPtr = (TextManager *) clientData; + if (charIndex < 0) { + return 0; + } + return (int) IndexOfContainingCluster(managerPtr->string, charIndex, + clusterLength); +} + +/* + *--------------------------------------------------------------------------- + * + * TkpTextManagerNumClusters -- + * + * Return the (cached) number of clusters in the entire NSMutableString. + * + * Results: + * The number of clusters. + * + * Side effects: + * None. + * + *--------------------------------------------------------------------------- + */ + +int +TkpTextManagerNumClusters( + ClientData clientData) +{ + TextManager *managerPtr = (TextManager *) clientData; + NSString *string = managerPtr->string; + + if (managerPtr->numClusters < 0) { + managerPtr->numClusters = IndexOfContainingCluster(string, + string.length, NULL); + } + return managerPtr->numClusters; +} +/* + *--------------------------------------------------------------------------- + * + * TkpTextManagerCharRangeOfClusterRange -- + * + * Return a pointer to a UTF-8 encoded C string which represents a + * substring of the text manager's string. The result includes all + * clusters with index >= first and <= last. It is allowed for first + * to be negative and last to be greater than or equal to numClusters. + * The resulting pointer has an indeterminate lifespan since it is + * equal to the UTF8String property of an NSString. So it should be + * copied or used before the current iteration of the event loop + * terminates. + * + * Results: + * The number of clusters. + * + * Side effects: + * None. + * + *--------------------------------------------------------------------------- + */ + +const char * +TkpTextManagerUTF8StringForClusterRange( + ClientData clientData, + int firstCluster, + int lastCluster) +{ + TextManager *managerPtr = (TextManager *) clientData; + NSRange clusterRange, fullRange; + NSUInteger location, charIndex, clusterIndex; + int length; + NSString *str = managerPtr->string, *temp; + static char empty = '\0'; + + if (firstCluster < 0) { + firstCluster = 0; + } + if (lastCluster < firstCluster) { + return ∅ + } + clusterIndex = firstCluster; + location = TkpTextManagerClusterPosition(managerPtr, firstCluster, &length); + charIndex = location; + while (clusterIndex <= (unsigned int) lastCluster && + charIndex < [managerPtr->string length]) { + clusterRange = [str rangeOfComposedCharacterSequenceAtIndex:charIndex]; + charIndex += clusterRange.length; + clusterIndex++; + } + fullRange = NSMakeRange(location, charIndex - location); + temp = [str substringWithRange:fullRange]; + return [temp UTF8String]; +} + +/* + *--------------------------------------------------------------------------- + * + * TkpTextManagerSet -- + * + * Set the contents of the NSMutableString from a UTF-8 encoded string. + * + * Results: + * A pointer to the TextManager's cached UTF-8 string. + * + * Side effects: + * The number of unichars and bytes are recorded in the integer variables + * referenced by the numChars and numBytes parameters. + * + *--------------------------------------------------------------------------- + */ + +const char * +TkpTextManagerSet( + ClientData clientData, + const char *value, + int *numChars, + int *numBytes) +{ + TextManager *managerPtr = (TextManager *) clientData; + NSString *valueString = [[NSString alloc] initWithUTF8String: value]; + + [managerPtr->string setString:valueString]; + return TextManagerUpdate(managerPtr, numChars, numBytes); +} + +/* + *--------------------------------------------------------------------------- + * + * TkpTextManagerRevert -- + * + * Restore the previous state of the text from the cache. This assumes + * that the cache was created before making the last change to the text. + * Otherwise it has no effect. + * + * Results: + * A pointer to the UTF-8 string for the restored text. + * + * Side effects: + * The TextManager's string is set to the cached string and the cache is + * cleared. + * + * + *--------------------------------------------------------------------------- + */ + +const char * +TkpTextManagerRevert( + ClientData clientData, + int *numChars, + int *numBytes) +{ + TextManager *managerPtr = (TextManager *) clientData; + if (managerPtr->backup) { + [managerPtr->string release]; + managerPtr->string = managerPtr->backup; + managerPtr->backup = nil; + } + return TextManagerUpdate(managerPtr, numChars, numBytes); +} + +/* + *--------------------------------------------------------------------------- + * + * TkpTextManagerInsert -- + * + * Insert the string, as described by the UTF-8 encoded byte array + * referenced by the value parameter, at the provided character index. + * Inserting a cluster may be done with sequential calls that each provide + * a single unicode code point. The macOS port always provides both + * surrogate pairs in a single XEvent, so there should not be misplaced + * surrogates, but the modifiers following the base char may be incomplete + * after calling this. Also, the provided character index may not refer + * to a base character in some calls. If the oldString parameter is not + * NULL the pointer which it references will be set to the address of a + * UTF-8 encoding of the text prior to the insertion. This is intended for + * use in validating the change to the string, and may not persist after + * the next iteration of the event loop. + * + * Results: + * A pointer to the TextManager's UTF-8 string. + * + * Side effects: + * The number of unichars and bytes are recorded in the integer variables + * referenced by the numChars and numBytes parameters. The pointer + * referenced by the parameter oldString may be set to the address of an + * encoded byte sequence representing the cached prior state of the + * string. + * + *--------------------------------------------------------------------------- + */ + +const char* +TkpTextManagerInsert( + ClientData clientData, + int charIndex, + const char *value, + int *numChars, + int *numBytes, + const char **oldString) +{ + TextManager *managerPtr = (TextManager *) clientData; + NSString *valueString = [[NSString alloc] initWithUTF8String: value]; + if (oldString) { + TextManagerCache(managerPtr); + [managerPtr->string insertString:valueString atIndex:charIndex]; + *oldString = (char *) [managerPtr->backup UTF8String]; + } else { + TextManagerClearCache(managerPtr); + [managerPtr->string insertString:valueString atIndex:charIndex]; + } + return TextManagerUpdate(managerPtr, numChars, numBytes); +} + +/* + *--------------------------------------------------------------------------- + * + * TkpTextManagerDelete -- + * + * Delete the number of clusters specified by the count parameter, + * starting at the cluster containing the character referenced by + * the charIndex parameter. If that character is not a base character + * the entire cluster which contains it will be deleted, so the + * resulting string is a sequence of well-formed clusters. + * + * Results: + * A pointer to the TextManager's cached UTF-8 string. + * + * Side effects: + + * The number of unichars and bytes in the modified NSMutableString are + * recorded in the integer variables referenced by the numChars and + * numBytes parameters. Also the number of characters which were removed + * is recorded in the integer variable charsDeleted. The index of each + * remaining base character will change by addition or subtraction of this + * number. If the parameter charsDeleted is a non-null pointer then it + * will be set to the address of a UTF-8 encoded C string containing the + * deleted characters. If the parameter oldString is non-NULL then the + * value of the string will be cached before the characters are deleted + * and the pointer referenced by oldString be set to a UTF8-encoded + * C-string representing the cached string. These strings are meant for + * immediate use in validating the change, and may not persist after the + * next iteration of the event loop. + * + *--------------------------------------------------------------------------- + */ + +const char* +TkpTextManagerDelete( + ClientData clientData, + int charIndex, + int count, + int *numChars, + int *numBytes, + int *numDeleted, + char **charsDeleted, + const char **oldString) +{ + TextManager *managerPtr = (TextManager *) clientData; + NSRange deleteRange = CharRangeFromClusterRange(managerPtr->string, + charIndex, count); + if (charsDeleted || oldString) { + NSString *diffString = [managerPtr->string + substringWithRange:deleteRange]; + TextManagerCache(managerPtr); + if (charsDeleted) { + *charsDeleted = (char *) [diffString UTF8String]; + } + if (oldString) { + *oldString = [managerPtr->backup UTF8String]; + } + } else { + TextManagerClearCache(managerPtr); + } + [managerPtr->string deleteCharactersInRange: deleteRange]; + *numDeleted = deleteRange.length; + return TextManagerUpdate(managerPtr, numChars, numBytes); +} + +#define GetNSFontTraitsFromTkFontAttributes(faPtr) \ ((faPtr)->weight == TK_FW_BOLD ? NSBoldFontMask : NSUnboldFontMask) | \ ((faPtr)->slant == TK_FS_ITALIC ? NSItalicFontMask : NSUnitalicFontMask) /* *--------------------------------------------------------------------------- Index: tests/entry.test ================================================================== --- tests/entry.test +++ tests/entry.test @@ -982,11 +982,11 @@ entry .e } -body { .e in } -cleanup { destroy .e -} -returnCodes error -result {ambiguous option "in": must be bbox, cget, configure, delete, get, icursor, index, insert, scan, selection, validate, or xview} +} -returnCodes error -result {ambiguous option "in": must be bbox, cget, configure, delete, get, icursor, index, insert, range, scan, selection, validate, or xview} test entry-3.32 {EntryWidgetCmd procedure, "index" widget command} -setup { entry .e } -body { .e index } -cleanup { @@ -1578,11 +1578,71 @@ update } -body { .e gorp } -cleanup { destroy .e -} -returnCodes error -result {bad option "gorp": must be bbox, cget, configure, delete, get, icursor, index, insert, scan, selection, validate, or xview} +} -returnCodes error -result {bad option "gorp": must be bbox, cget, configure, delete, get, icursor, index, insert, range, scan, selection, validate, or xview} +test entry-3.87 {EntryWidgetCmd procedure, "range" widget command} -setup { + entry .e + pack .e + update +} -body { + .e insert end "01234567890" + .e range 1 +} -cleanup { + destroy .e +} -returnCodes 1 -result {wrong # args: should be ".e range first last"} +test entry-3.88 {EntryWidgetCmd procedure, "range" widget command} -setup { + entry .e + pack .e + update +} -body { + .e insert end "01234567890" + .e range 1 5 +} -cleanup { + destroy .e +} -result {12345} +test entry-3.89 {EntryWidgetCmd procedure, "range" widget command} -setup { + entry .e + pack .e + update +} -body { + .e insert end "01234567890" + .e range 5 1 +} -cleanup { + destroy .e +} -result {} +test entry-3.90 {EntryWidgetCmd procedure, "range" widget command} -setup { + entry .e + pack .e + update +} -body { + .e insert end "01234567890" + .e range 5 1 +} -cleanup { + destroy .e +} -result {} +test entry-3.91 {EntryWidgetCmd procedure, "range" widget command} -setup { + entry .e + pack .e + update +} -body { + .e insert end "01234567890" + .e range -10 5 +} -cleanup { + destroy .e +} -result {012345} +test entry-3.92 {EntryWidgetCmd procedure, "range" widget command} -setup { + entry .e + pack .e + update +} -body { + .e insert end "01234567890" + .e range -10 20 +} -cleanup { + destroy .e +} -result {01234567890} # The test below doesn't actually check anything directly, but if run # with Purify or some other memory-allocation-checking program it will # ensure that resources get properly freed.