Created
February 1, 2011 08:07
-
-
Save allending/805570 to your computer and use it in GitHub Desktop.
Some expository code from Notekata as a UITextInputTokenizer example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//Copyright (c) 2010, Allen Ding | |
//All rights reserved. | |
// | |
//Redistribution and use in source and binary forms, with or without | |
//modification, are permitted provided that the following conditions are met: | |
// | |
//1. Redistributions of source code must retain the above copyright notice, this | |
//list of conditions and the following disclaimer. | |
// | |
//2. Redistributions in binary form must reproduce the above copyright notice, | |
//this list of conditions and the following disclaimer in the documentation | |
//and/or other materials provided with the distribution. | |
// | |
//3. Neither the name of Allen Ding nor the names of any contributors may be | |
//used to endorse or promote products derived from this software without specific | |
//prior written permission. | |
// | |
//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | |
//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | |
//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |
//FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
//DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
//CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
//OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
//OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
#import "KobaUI.h" | |
@class NKTTextView; | |
// NKTTextViewTokenizer is the tokenizer used by NKTTextView's implementation of the UITextInput | |
// protocol. | |
@interface NKTTextViewTokenizer : UITextInputStringTokenizer | |
{ | |
@private | |
NKTTextView *textView_; | |
} | |
#pragma mark Initializing | |
- (id)initWithTextView:(NKTTextView *)textView; | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//Copyright (c) 2010, Allen Ding | |
//All rights reserved. | |
// | |
//Redistribution and use in source and binary forms, with or without | |
//modification, are permitted provided that the following conditions are met: | |
// | |
//1. Redistributions of source code must retain the above copyright notice, this | |
//list of conditions and the following disclaimer. | |
// | |
//2. Redistributions in binary form must reproduce the above copyright notice, | |
//this list of conditions and the following disclaimer in the documentation | |
//and/or other materials provided with the distribution. | |
// | |
//3. Neither the name of Allen Ding nor the names of any contributors may be | |
//used to endorse or promote products derived from this software without specific | |
//prior written permission. | |
// | |
//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | |
//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | |
//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |
//FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
//DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
//CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
//OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
//OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
#define KBC_LOGGING_DISABLE_DEBUG_OUTPUT 1 | |
#import "NKTTextViewTokenizer.h" | |
#import "KobaText.h" | |
#import "NKTTextPosition.h" | |
#import "NKTTextRange.h" | |
#import "NKTTextView.h" | |
// Either I am out of my mind or as of SDK 3.2, the recommended UITextInputStringTokenizer base | |
// implementation for the UITextInputTokenizer protocol is hopelessly broken (or has some weird | |
// semantics at the very least). | |
// | |
// As far as I can understand, theoretically NKTTextViewTokenizer only needs to handle the | |
// cases with line text granularity. Unfortunately, the behavior is absolutely bonkers if that | |
// is all we handle. | |
// | |
// Instead, as it stands, NKTTextViewTokenizer is almost a complete standalone implementation of | |
// the UITextInputTokenizer protocol, except in cases where the behavior provided by | |
// UITextInputStringTokenizer actually works as expected. | |
// | |
// Note that this implementation only supports left-to-right text. | |
// | |
// Overview: | |
// - Forward and backward refer to movement directions. NOT front/back of a text unit (e.g word) | |
// - A text unit has two boundaries - front and back | |
// | |
// Example: | |
// - Consider the phrase 'foobar mules x' and word granularity context | |
// | |
// - The word 'foobar' has two boundaries: 'f' and one-past 'r' | |
// | |
// - 'f' is a word boundary in the backward direction | |
// - one-past 'r' is word boundary in the forward direction | |
// - 'f' is only part of the word in the forward direction | |
// - one-past 'r' is only a part of the text unit in the backward direction | |
// | |
// +-------------+--------------+--------------+--------------+--------------+ | |
// | | isPo:atB:fwd | isPo:atB:bwd | isPo:wTU:fwd | isPo:wTU:bwd | | |
// +-------------+--------------+--------------+--------------+--------------+ | |
// | 'f' | | x | x | | | |
// | 'oobar' | | | x | x | | |
// | one-past'r' | x | | | x | | |
// +-------------+--------------+--------------+--------------+--------------+ | |
// | |
// - one-past 'r' is the next word boundary in the forward direction from 'foobar' | |
// - 'm' is the next word boundary in the forward direction from one-past 'r' | |
// - 'f' is the next word boundary in the backward direction from 'oobar' | |
// - 'f' is the next word boundary in the backward direction from one-past 'r' | |
// | |
// +------------------------+--------------+--------------+ | |
// | | pFP:toBo:fwd | pFP:toBo:bwd | | |
// +------------------------+--------------+--------------+ | |
// | 'f' | one-past 'r' | nil | | |
// | 'oobar' | one-past 'r' | 'f' | | |
// | one-past 'r' | 'm' | 'f' | | |
// +------------------------+--------------+--------------+ | |
// | |
// - for 'f', the enclosing range is nil in the backward direction, and is 'foobar' in the forward direction | |
// - for 'oobar', the enclosing range in the forward and backward direction is 'foobar' | |
// - for one-past 'r', the enclosing range is nil in the forward direction, and is 'foobar' in the backward direction | |
// | |
// +---------------------+--------------+--------------+ | |
// | | range:wG:fwd | range:wG:bwd | | |
// +---------------------+--------------+--------------+ | |
// | 'f' | 'foobar' | nil | | |
// | 'oobar' | 'foobar' | 'foobar' | | |
// | one-past 'r' | nil | 'foobar' | | |
// +---------------------+--------------+--------------+ | |
@interface NKTTextViewTokenizer() | |
#pragma mark Determining Text Positions Relative to Unit Boundaries | |
- (BOOL)isTextPosition:(NKTTextPosition *)textPosition atWordBoundaryInDirection:(UITextDirection)direction; | |
- (BOOL)isTextPosition:(NKTTextPosition *)textPosition atLineBoundaryInDirection:(UITextDirection)direction; | |
- (BOOL)isTextPosition:(NKTTextPosition *)textPosition atParagraphBoundaryInDirection:(UITextDirection)direction; | |
- (BOOL)isTextPosition:(NKTTextPosition *)textPosition withinWordInDirection:(UITextDirection)direction; | |
- (BOOL)isTextPosition:(NKTTextPosition *)textPosition withinLineInDirection:(UITextDirection)direction; | |
- (BOOL)isTextPosition:(NKTTextPosition *)textPosition withinParagraphInDirection:(UITextDirection)direction; | |
#pragma mark Computing Text Position by Unit Boundaries | |
- (UITextPosition *)positionFromTextPosition:(NKTTextPosition *)textPosition | |
toWordBoundaryInDirection:(UITextDirection)direction; | |
- (UITextPosition *)positionFromTextPosition:(NKTTextPosition *)textPosition | |
toLineBoundaryInDirection:(UITextDirection)direction; | |
- (UITextPosition *)positionFromTextPosition:(NKTTextPosition *)textPosition | |
toParagraphBoundaryInDirection:(UITextDirection)direction; | |
#pragma mark Getting Ranges of Specific Text Units | |
- (UITextRange *)textRangeForWordEnclosingTextPosition:(NKTTextPosition *)textPosition | |
inDirection:(UITextDirection)direction; | |
@end | |
#pragma mark - | |
//-------------------------------------------------------------------------------------------------- | |
@implementation NKTTextViewTokenizer | |
#pragma mark Initializing | |
- (id)initWithTextView:(NKTTextView *)textView | |
{ | |
if ((self = [super initWithTextInput:textView])) | |
{ | |
textView_ = textView; | |
} | |
return self; | |
} | |
//-------------------------------------------------------------------------------------------------- | |
#pragma mark Determining Text Positions Relative to Unit Boundaries | |
- (BOOL)isPosition:(NKTTextPosition *)textPosition | |
atBoundary:(UITextGranularity)granularity | |
inDirection:(UITextDirection)direction | |
{ | |
BOOL result = NO; | |
if (granularity == UITextGranularityWord) | |
{ | |
result = [self isTextPosition:textPosition atWordBoundaryInDirection:direction]; | |
} | |
else if (granularity == UITextGranularityLine) | |
{ | |
result = [self isTextPosition:textPosition atLineBoundaryInDirection:direction]; | |
} | |
else if (granularity == UITextGranularityParagraph) | |
{ | |
result = [self isTextPosition:textPosition atParagraphBoundaryInDirection:direction]; | |
} | |
else | |
{ | |
result = [super isPosition:textPosition atBoundary:granularity inDirection:direction]; | |
} | |
KBCLogDebug(@"%d : %@ : %@ -> %d", | |
textPosition.location, | |
KBTStringFromUITextGranularity(granularity), | |
KBTStringFromUITextDirection(direction), | |
result); | |
return result; | |
} | |
// A text position is a word boundary in the forward direction if the text position is the end of | |
// a word. | |
// | |
// A text position is a word boundary in the backward direction if the text position is the start | |
// of a word. | |
- (BOOL)isTextPosition:(NKTTextPosition *)textPosition atWordBoundaryInDirection:(UITextDirection)direction | |
{ | |
NSString *string = [textView_.text string]; | |
NSCharacterSet *alphanumerics = [NSCharacterSet alphanumericCharacterSet]; | |
if (direction == UITextStorageDirectionForward || | |
direction == UITextLayoutDirectionRight || | |
direction == UITextLayoutDirectionDown) | |
{ | |
// Not end of word if index is 0 | |
if (textPosition.location == 0) | |
{ | |
return NO; | |
} | |
// End of word if previous position is alphanumeric, and position is not alphanumeric | |
unichar previousChar = [string characterAtIndex:textPosition.location - 1]; | |
if (textPosition.location == [string length]) | |
{ | |
return [alphanumerics characterIsMember:previousChar]; | |
} | |
unichar theChar = [string characterAtIndex:textPosition.location]; | |
return [alphanumerics characterIsMember:previousChar] && ![alphanumerics characterIsMember:theChar]; | |
} | |
else | |
{ | |
// Not start of word if index is the end | |
if (textPosition.location == [string length]) | |
{ | |
return NO; | |
} | |
// Start of word if previous position is not alphanumeric and position is alphanumeric | |
unichar theChar = [string characterAtIndex:textPosition.location]; | |
if (textPosition.location == 0) | |
{ | |
return [alphanumerics characterIsMember:theChar]; | |
} | |
unichar previousChar = [string characterAtIndex:textPosition.location - 1]; | |
return [alphanumerics characterIsMember:theChar] && ![alphanumerics characterIsMember:previousChar]; | |
} | |
} | |
// A text position is a line boundary in the forward direction if the text position is one before | |
// the end of a line's text range. | |
// | |
// A text position is a line boundary in the backward direction if the text position is the start | |
// of a line's text range. | |
- (BOOL)isTextPosition:(NKTTextPosition *)textPosition atLineBoundaryInDirection:(UITextDirection)direction | |
{ | |
NKTTextRange *lineTextRange = [textView_ textRangeForLineContainingTextPosition:textPosition]; | |
if (lineTextRange == nil) | |
{ | |
return NO; | |
} | |
if (direction == UITextStorageDirectionForward || | |
direction == UITextLayoutDirectionRight || | |
direction == UITextLayoutDirectionDown) | |
{ | |
return [textPosition isEqualToTextPosition:[lineTextRange.end textPositionByApplyingOffset:-1]]; | |
} | |
else | |
{ | |
return [textPosition isEqualToTextPosition:lineTextRange.start]; | |
} | |
} | |
// A text position is a paragraph boundary in the forward direction if the text position is the | |
// end of a paragraph (newline). | |
// | |
// A text position is a paragraph boundary in the backward direction if the text position is the | |
// start of a paragraph (follows a newline). | |
- (BOOL)isTextPosition:(NKTTextPosition *)textPosition atParagraphBoundaryInDirection:(UITextDirection)direction | |
{ | |
NSString *string = [textView_.text string]; | |
NSCharacterSet *newlines = [NSCharacterSet newlineCharacterSet]; | |
if (direction == UITextStorageDirectionForward || | |
direction == UITextLayoutDirectionRight || | |
direction == UITextLayoutDirectionDown) | |
{ | |
// End index is always ends a paragraph | |
if (textPosition.location == [string length]) | |
{ | |
return YES; | |
} | |
// End of paragraph if position is a newline | |
unichar theChar = [string characterAtIndex:textPosition.location]; | |
return [newlines characterIsMember:theChar]; | |
} | |
else | |
{ | |
// Start index always begins a paragraph | |
if (textPosition.location == 0) | |
{ | |
return YES; | |
} | |
// Start of paragraph if previous position is a newline | |
unichar previousChar = [string characterAtIndex:textPosition.location - 1]; | |
return [newlines characterIsMember:previousChar]; | |
} | |
} | |
- (BOOL)isPosition:(NKTTextPosition *)textPosition withinTextUnit:(UITextGranularity)granularity inDirection:(UITextDirection)direction | |
{ | |
BOOL result = NO; | |
if (granularity == UITextGranularityWord) | |
{ | |
result = [self isTextPosition:textPosition withinWordInDirection:direction]; | |
} | |
else if (granularity == UITextGranularityLine) | |
{ | |
result = [self isTextPosition:textPosition withinLineInDirection:direction]; | |
} | |
else if (granularity == UITextGranularityParagraph) | |
{ | |
result = [self isTextPosition:textPosition withinParagraphInDirection:direction]; | |
} | |
else | |
{ | |
result = [super isPosition:textPosition withinTextUnit:granularity inDirection:direction]; | |
} | |
KBCLogDebug(@"%d : %@ : %@ -> %d", | |
textPosition.location, | |
KBTStringFromUITextGranularity(granularity), | |
KBTStringFromUITextDirection(direction), | |
result); | |
return result; | |
} | |
// A text position is within a word in the forward direction if the text position is part of a | |
// word. | |
// | |
// A text position is within a word in the backward direction if the previous text position is part | |
// of a word. | |
- (BOOL)isTextPosition:(NKTTextPosition *)textPosition withinWordInDirection:(UITextDirection)direction | |
{ | |
NSString *string = [textView_.text string]; | |
NSCharacterSet *alphanumerics = [NSCharacterSet alphanumericCharacterSet]; | |
if (direction == UITextStorageDirectionForward || | |
direction == UITextLayoutDirectionRight || | |
direction == UITextLayoutDirectionDown) | |
{ | |
// No words beyond the end index | |
if (textPosition.location == [string length]) | |
{ | |
return NO; | |
} | |
unichar theChar = [string characterAtIndex:textPosition.location]; | |
return [alphanumerics characterIsMember:theChar]; | |
} | |
else | |
{ | |
// No words before the start index | |
if (textPosition.location == 0) | |
{ | |
return NO; | |
} | |
unichar previousChar = [string characterAtIndex:textPosition.location - 1]; | |
return [alphanumerics characterIsMember:previousChar]; | |
} | |
} | |
// If the line containing a text position has a text range with a length equal to one, then the | |
// text position is a boundary in both the forward and backward direction. Otherwise, the | |
// following applies: | |
// | |
// A text position is within a line in the forward direction if the text position is not the | |
// position one before the end of a line's text range. | |
// | |
// A text position is within a line in the backward direction if the text position is not the | |
// start of a line's text range. | |
- (BOOL)isTextPosition:(NKTTextPosition *)textPosition withinLineInDirection:(UITextDirection)direction | |
{ | |
NKTTextRange *lineTextRange = [textView_ textRangeForLineContainingTextPosition:textPosition]; | |
if (lineTextRange == nil) | |
{ | |
return NO; | |
} | |
if (lineTextRange.length == 1) | |
{ | |
return YES; | |
} | |
if (direction == UITextStorageDirectionForward || | |
direction == UITextLayoutDirectionRight || | |
direction == UITextLayoutDirectionDown) | |
{ | |
return ![textPosition isEqualToTextPosition:[lineTextRange.end textPositionByApplyingOffset:-1]]; | |
} | |
else | |
{ | |
return ![textPosition isEqualToTextPosition:lineTextRange.start]; | |
} | |
} | |
// A text position is within a paragraph in the forward direction if the text position is not the | |
// end of a paragraph (newline). | |
// | |
// A text position is within a paragraph in the backward direction if the text position is not the | |
// start of a paragraph (follows a newline). | |
- (BOOL)isTextPosition:(NKTTextPosition *)textPosition withinParagraphInDirection:(UITextDirection)direction | |
{ | |
NSString *string = [textView_.text string]; | |
NSCharacterSet *newlines = [NSCharacterSet newlineCharacterSet]; | |
if (direction == UITextStorageDirectionForward || | |
direction == UITextLayoutDirectionRight || | |
direction == UITextLayoutDirectionDown) | |
{ | |
// End index is always end of a paragraph | |
if (textPosition.location == [string length]) | |
{ | |
return NO; | |
} | |
unichar theChar = [string characterAtIndex:textPosition.location]; | |
return ![newlines characterIsMember:theChar]; | |
} | |
else | |
{ | |
// Start index is always start of a paragraph | |
if (textPosition.location == 0) | |
{ | |
return NO; | |
} | |
unichar previousChar = [string characterAtIndex:textPosition.location - 1]; | |
return ![newlines characterIsMember:previousChar]; | |
} | |
} | |
//-------------------------------------------------------------------------------------------------- | |
#pragma mark Computing Text Position by Unit Boundaries | |
- (UITextPosition *)positionFromPosition:(NKTTextPosition *)textPosition | |
toBoundary:(UITextGranularity)granularity | |
inDirection:(UITextDirection)direction | |
{ | |
UITextPosition *boundaryTextPosition = nil; | |
if (granularity == UITextGranularityWord) | |
{ | |
boundaryTextPosition = [self positionFromTextPosition:textPosition toWordBoundaryInDirection:direction]; | |
} | |
else if (granularity == UITextGranularityLine) | |
{ | |
boundaryTextPosition = [self positionFromTextPosition:textPosition toLineBoundaryInDirection:direction]; | |
} | |
else if (granularity == UITextGranularityParagraph) | |
{ | |
boundaryTextPosition = [self positionFromTextPosition:textPosition toParagraphBoundaryInDirection:direction]; | |
} | |
else | |
{ | |
boundaryTextPosition = [super positionFromPosition:textPosition toBoundary:granularity inDirection:direction]; | |
} | |
KBCLogDebug(@"%d : %@ : %@ -> %d", | |
textPosition.location, | |
KBTStringFromUITextGranularity(granularity), | |
KBTStringFromUITextDirection(direction), | |
((NKTTextPosition *)boundaryTextPosition).location); | |
return boundaryTextPosition; | |
} | |
// The next word boundary in the forward direction is the furthest position in the direction that | |
// has the opposite alphanumeric 'sign' as the given position. | |
// | |
// The next word boundary in the backward direction is the furthest position in the direction that | |
// has the same alphanumeric 'sign' as the given position. | |
- (UITextPosition *)positionFromTextPosition:(NKTTextPosition *)textPosition | |
toWordBoundaryInDirection:(UITextDirection)direction | |
{ | |
NSString *string = [textView_.text string]; | |
NSCharacterSet *alphanumerics = [NSCharacterSet alphanumericCharacterSet]; | |
if (direction == UITextStorageDirectionForward || | |
direction == UITextLayoutDirectionRight || | |
direction == UITextLayoutDirectionDown) | |
{ | |
// The end position is always treated as a word boundary | |
if (textPosition.location == [string length]) | |
{ | |
return textPosition; | |
} | |
unichar referenceChar = [string characterAtIndex:textPosition.location]; | |
BOOL referenceCharSign = [alphanumerics characterIsMember:referenceChar]; | |
NSUInteger currentIndex = textPosition.location + 1; | |
for (; currentIndex < [string length]; ++currentIndex) | |
{ | |
unichar currentChar = [string characterAtIndex:currentIndex]; | |
BOOL currentCharSign = [alphanumerics characterIsMember:currentChar]; | |
if (referenceCharSign != currentCharSign) | |
{ | |
break; | |
} | |
} | |
return [NKTTextPosition textPositionWithLocation:currentIndex affinity:UITextStorageDirectionForward]; | |
} | |
else | |
{ | |
// The start position is always treated a word boundary | |
if (textPosition.location == 0) | |
{ | |
return textPosition; | |
} | |
unichar referenceChar = [string characterAtIndex:textPosition.location - 1]; | |
BOOL referenceCharSign = [alphanumerics characterIsMember:referenceChar]; | |
NSUInteger currentIndex = textPosition.location - 1; | |
for (; currentIndex > 0; --currentIndex) | |
{ | |
unichar currentChar = [string characterAtIndex:currentIndex]; | |
BOOL currentCharSign = [alphanumerics characterIsMember:currentChar]; | |
if (referenceCharSign != currentCharSign) | |
{ | |
++currentIndex; | |
break; | |
} | |
} | |
return [NKTTextPosition textPositionWithLocation:currentIndex affinity:UITextStorageDirectionForward]; | |
} | |
} | |
// If the text position is one before the end of a line's text range, it is already at the | |
// boundary. Otherwise, the next line boundary in the forward direction is one before the | |
// end of the line's text range. | |
// | |
// If the text position is the start of a line's text range, it is already at the boundary. | |
// Otherwise, the next line boundary in the backward direction is the start of the line's text | |
// range. | |
- (UITextPosition *)positionFromTextPosition:(NKTTextPosition *)textPosition | |
toLineBoundaryInDirection:(UITextDirection)direction | |
{ | |
NSString *string = [textView_.text string]; | |
NKTTextRange *lineTextRange = [textView_ textRangeForLineContainingTextPosition:textPosition]; | |
if (lineTextRange == nil) | |
{ | |
return nil; | |
} | |
if (direction == UITextStorageDirectionForward || | |
direction == UITextLayoutDirectionRight || | |
direction == UITextLayoutDirectionDown) | |
{ | |
// PENDING: No boundary position after the end index? | |
if (textPosition.location == [string length]) | |
{ | |
return textPosition; | |
} | |
NKTTextPosition *oneBeforeEnd = [lineTextRange.end textPositionByApplyingOffset:-1]; | |
if ([textPosition isEqualToTextPosition:oneBeforeEnd]) | |
{ | |
// HACK: make sure that UITextInput does not 'skip' over a line | |
return textPosition; | |
} | |
else | |
{ | |
return oneBeforeEnd; | |
} | |
} | |
else | |
{ | |
// No boundary position before the start index | |
if (textPosition.location == 0) | |
{ | |
return textPosition; | |
} | |
if ([textPosition isEqualToTextPosition:lineTextRange.start]) | |
{ | |
// HACK: make sure that UITextInput does not 'skip' over a line | |
return textPosition; | |
} | |
else | |
{ | |
return lineTextRange.start; | |
} | |
} | |
} | |
// If the text position is the end of a paragraph, the next paragraph boundary in the | |
// forward direction is one beyond the end of the paragraph. Otherwise, the next paragraph | |
// boundary in the forward direction is one before the end of the paragraph. | |
// | |
// If the text position is the start of a paragraph, the next paragraph boundary in the backward | |
// direction is one before the start of the paragraph. Otherwise, the next paragraph boundary in | |
// the backward direction is the start of the paragraph. | |
- (UITextPosition *)positionFromTextPosition:(NKTTextPosition *)textPosition | |
toParagraphBoundaryInDirection:(UITextDirection)direction | |
{ | |
NSString *string = [textView_.text string]; | |
NSCharacterSet *newlines = [NSCharacterSet newlineCharacterSet]; | |
if (direction == UITextStorageDirectionForward || | |
direction == UITextLayoutDirectionRight || | |
direction == UITextLayoutDirectionDown) | |
{ | |
if (textPosition.location == [string length]) | |
{ | |
return textPosition; | |
} | |
unichar theChar = [string characterAtIndex:textPosition.location]; | |
if ([newlines characterIsMember:theChar]) | |
{ | |
return [textPosition textPositionByApplyingOffset:+1]; | |
} | |
else | |
{ | |
NSUInteger currentIndex = textPosition.location + 1; | |
for (; currentIndex < [string length]; ++currentIndex) | |
{ | |
unichar currentChar = [string characterAtIndex:currentIndex]; | |
if ([newlines characterIsMember:currentChar]) | |
{ | |
break; | |
} | |
} | |
return [NKTTextPosition textPositionWithLocation:currentIndex affinity:UITextStorageDirectionForward]; | |
} | |
} | |
else | |
{ | |
if (textPosition.location == 0) | |
{ | |
return textPosition; | |
} | |
unichar previousChar = [string characterAtIndex:textPosition.location - 1]; | |
if ([newlines characterIsMember:previousChar]) | |
{ | |
return [textPosition textPositionByApplyingOffset:-1]; | |
} | |
else | |
{ | |
NSUInteger currentIndex = textPosition.location - 1; | |
for (; currentIndex > 0; --currentIndex) | |
{ | |
unichar currentChar = [string characterAtIndex:currentIndex]; | |
if ([newlines characterIsMember:currentChar]) | |
{ | |
++currentIndex; | |
break; | |
} | |
} | |
return [NKTTextPosition textPositionWithLocation:currentIndex affinity:UITextStorageDirectionForward]; | |
} | |
} | |
} | |
//-------------------------------------------------------------------------------------------------- | |
#pragma mark Getting Ranges of Specific Text Units | |
// In this method, return the range for the text enclosing a text position in a text unit of the | |
// given granularity, or nil if there is no such enclosing unit. If the text position is entirely | |
// enclosed within a text unit of the given granularity, it is considered enclosed. If the text | |
// position is at a text-unit boundary, it is considered enclosed only if the next position in | |
// the given direction is entirely enclosed. | |
- (UITextRange *)rangeEnclosingPosition:(NKTTextPosition *)textPosition | |
withGranularity:(UITextGranularity)granularity | |
inDirection:(UITextDirection)direction | |
{ | |
UITextRange *result = nil; | |
// if (granularity == UITextGranularityWord) | |
// { | |
// result = [self textRangeForWordEnclosingTextPosition:textPosition inDirection:direction]; | |
// } | |
// else if (granularity == UITextGranularityLine) | |
// { | |
// // PENDING: it looks like this is never used, so leave it unimplemented for now | |
// result = nil; | |
// } | |
// else | |
// { | |
result = [super rangeEnclosingPosition:textPosition withGranularity:granularity inDirection:direction]; | |
// } | |
KBCLogDebug(@"%d : %@ : %@ : %@", | |
textPosition.location, | |
KBTStringFromUITextGranularity(granularity), | |
KBTStringFromUITextDirection(direction), | |
result); | |
return result; | |
} | |
- (UITextRange *)textRangeForWordEnclosingTextPosition:(NKTTextPosition *)textPosition | |
inDirection:(UITextDirection)direction | |
{ | |
if (direction == UITextStorageDirectionForward || | |
direction == UITextLayoutDirectionRight || | |
direction == UITextLayoutDirectionDown) | |
{ | |
if (![self isTextPosition:textPosition withinWordInDirection:UITextStorageDirectionForward]) | |
{ | |
return nil; | |
} | |
NKTTextPosition *backwardBoundary = nil; | |
if ([self isTextPosition:textPosition atWordBoundaryInDirection:UITextStorageDirectionBackward]) | |
{ | |
backwardBoundary = textPosition; | |
} | |
else | |
{ | |
backwardBoundary = (NKTTextPosition *)[self positionFromTextPosition:textPosition | |
toWordBoundaryInDirection:UITextStorageDirectionBackward]; | |
} | |
NKTTextPosition *forwardBoundary = nil; | |
if ([self isTextPosition:textPosition atWordBoundaryInDirection:UITextStorageDirectionForward]) | |
{ | |
forwardBoundary = textPosition; | |
} | |
else | |
{ | |
forwardBoundary = (NKTTextPosition *)[self positionFromTextPosition:textPosition | |
toWordBoundaryInDirection:UITextStorageDirectionForward]; | |
} | |
return [NKTTextRange textRangeWithTextPosition:backwardBoundary textPosition:forwardBoundary]; | |
} | |
else | |
{ | |
if (![self isTextPosition:textPosition withinWordInDirection:UITextStorageDirectionBackward]) | |
{ | |
return nil; | |
} | |
NKTTextPosition *backwardBoundary = nil; | |
if ([self isTextPosition:textPosition atWordBoundaryInDirection:UITextStorageDirectionBackward]) | |
{ | |
backwardBoundary = textPosition; | |
} | |
else | |
{ | |
backwardBoundary = (NKTTextPosition *)[self positionFromTextPosition:textPosition | |
toWordBoundaryInDirection:UITextStorageDirectionBackward]; | |
} | |
NKTTextPosition *forwardBoundary = nil; | |
if ([self isTextPosition:textPosition atWordBoundaryInDirection:UITextStorageDirectionForward]) | |
{ | |
forwardBoundary = textPosition; | |
} | |
else | |
{ | |
forwardBoundary = (NKTTextPosition *)[self positionFromTextPosition:textPosition | |
toWordBoundaryInDirection:UITextStorageDirectionForward]; | |
} | |
return [NKTTextRange textRangeWithTextPosition:backwardBoundary textPosition:forwardBoundary]; | |
} | |
} | |
@end |
FYI, the UITextInputTokenizer docs melt my brain.
There's hardly anything in them! Shoutout for the best reference I could find on how to implement this interface, it help resolved some virtual keyboard issues in our notes app https://github.com/lockbook/lockbook
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
FYI, the UITextInputTokenizer docs melt my brain.