Created
February 4, 2011 14:23
-
-
Save allending/811161 to your computer and use it in GitHub Desktop.
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 "NKTTextView.h" | |
#import "KobaText.h" | |
#import "NKTDragGestureRecognizer.h" | |
#import "NKTFramesetter.h" | |
#import "NKTLine.h" | |
#import "NKTLoupe.h" | |
#import "NKTTextPosition.h" | |
#import "NKTTextRange.h" | |
#import "NKTTextSection.h" | |
#import "NKTTextViewGestureRecognizerDelegate.h" | |
#import "NKTTextViewTokenizer.h" | |
@interface NKTTextView() | |
#pragma mark Initializing | |
- (void)NKTTextView_requiredInit; | |
- (void)initGestureRecognizers; | |
#pragma mark Tiling Sections | |
- (void)tileSections; | |
- (void)untileVisibleSections; | |
- (BOOL)isDisplayingSectionAtIndex:(NSInteger)index; | |
- (NKTTextSection *)dequeueReusableSection; | |
- (void)configureSection:(NKTTextSection *)section atIndex:(NSInteger)index; | |
- (CGRect)frameForSectionAtIndex:(NSInteger)index; | |
#pragma mark Managing the Framesetter | |
@property (nonatomic, readonly) CGFloat lineWidth; | |
@property (nonatomic, readonly) NKTFramesetter *framesetter; | |
- (void)invalidateFramesetter; | |
- (void)updateTextFrame; | |
- (void)updateTextFrameForChangeFromTextPosition:(NKTTextPosition *)textPosition; | |
- (CGPoint)convertPointToFramesetter:(CGPoint)point; | |
- (CGPoint)convertPointFromFramesetter:(CGPoint)point; | |
- (CGAffineTransform)viewToFramesetterTransform; | |
- (CGAffineTransform)framesetterToViewTransform; | |
#pragma mark - | |
#pragma mark Notifying the Delegate | |
- (void)notifyDelegateOfTextWillChangeFromTextPosition:(NKTTextPosition *)textPosition; | |
- (void)notifyDelegateOfTextDidChangeFromTextPosition:(NKTTextPosition *)textPosition; | |
- (void)notifyDelegateOfStyleWillChangeFromTextPosition:(NKTTextPosition *)textPosition; | |
- (void)notifyDelegateOfStyleDidChangeFromTextPosition:(NKTTextPosition *)textPosition; | |
#pragma mark Responding to Gestures | |
@property (nonatomic, retain) NKTTextRange *initialDoubleTapTextRange; | |
- (void)handleTap:(UIGestureRecognizer *)gestureRecognizer; | |
- (void)handleLongPress:(UIGestureRecognizer *)gestureRecognizer; | |
- (void)updateLongPressSelection; | |
- (void)handleDoubleTapDrag:(UIGestureRecognizer *)gestureRecognizer; | |
- (void)updateDoubleTapDragSelection; | |
- (void)handleBackwardHandleDrag:(UIGestureRecognizer *)gestureRecognizer; | |
- (void)updateBackwardHandleDragSelection; | |
- (void)handleForwardHandleDrag:(UIGestureRecognizer *)gestureRecognizer; | |
- (void)updateForwardHandleDragSelection; | |
- (NKTTextRange *)guessedTextRangeAtTextPosition:(NKTTextPosition *)textPosition wordRange:(NKTTextRange **)wordRange; | |
#pragma mark Scrolling | |
- (void)startEdgeScrollCheckWithGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer selector:(SEL)selector; | |
- (void)stopEdgeScrollCheckWithGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer selector:(SEL)selector; | |
- (void)edgeScrollCheckWithInfo:(NSDictionary *)info; | |
- (void)scrollAtEdgeWithPoint:(CGPoint)point; | |
#pragma mark Managing Loupes | |
@property (nonatomic, readonly) NKTLoupe *textRangeLoupe; | |
@property (nonatomic, readonly) NKTLoupe *caretLoupe; | |
- (void)configureLoupe:(NKTLoupe *)loupe toShowPoint:(CGPoint)point anchorToLine:(BOOL)anchorToLine; | |
- (void)configureLoupe:(NKTLoupe *)loupe toShowTextPosition:(NKTTextPosition *)textPosition; | |
#pragma mark Managing Text Ranges | |
@property (nonatomic, readwrite) NKTTextRange *gestureTextRange; | |
- (void)confirmGestureTextRange; | |
- (void)setSelectedTextRange:(NKTTextRange *)textRange notifyInputDelegate:(BOOL)notifyInputDelegate; | |
- (void)setMarkedTextRange:(NKTTextRange *)textRange notifyInputDelegate:(BOOL)notifyInputDelegate; | |
#pragma mark Geometry and Hit-Testing | |
- (CGRect)caretRectForTextPosition:(NKTTextPosition *)textPosition applyInputTextAttributes:(BOOL)applyInputTextAttributes; | |
- (CGRect)caretRectWithOrigin:(CGPoint)origin font:(UIFont *)font; | |
#pragma mark Getting Fonts at Text Positions | |
- (UIFont *)fontAtTextPosition:(NKTTextPosition *)textPosition; | |
@end | |
#pragma mark - | |
@implementation NKTTextView | |
@synthesize text = text_; | |
@synthesize backgroundView = backgroundView_; | |
@synthesize margins = margins_; | |
@synthesize lineHeight = lineHeight_; | |
@synthesize horizontalRulesEnabled = horizontalRulesEnabled_; | |
@synthesize horizontalRuleColor = horizontalRuleColor_; | |
@synthesize horizontalRuleOffset = horizontalRuleOffset_; | |
@synthesize verticalMarginEnabled = verticalMarginEnabled_; | |
@synthesize verticalMarginColor = verticalMarginColor_; | |
@synthesize verticalMarginInset = verticalMarginInset_; | |
@synthesize markedTextStyle = markedTextStyle_; | |
@synthesize inputTextAttributes = inputTextAttributes_; | |
@synthesize inputDelegate = inputDelegate_; | |
@synthesize nonEditTapGestureRecognizer = nonEditTapGestureRecognizer_; | |
@synthesize tapGestureRecognizer = tapGestureRecognizer_; | |
@synthesize longPressGestureRecognizer = longPressGestureRecognizer_; | |
@synthesize doubleTapDragGestureRecognizer = doubleTapDragGestureRecognizer_; | |
@synthesize initialDoubleTapTextRange = initialDoubleTapTextRange_; | |
#pragma mark - | |
#pragma mark Initializing | |
- (id)initWithFrame:(CGRect)frame | |
{ | |
if ((self = [super initWithFrame:frame])) | |
{ | |
self.opaque = NO; | |
self.clearsContextBeforeDrawing = YES; | |
[self NKTTextView_requiredInit]; | |
} | |
return self; | |
} | |
- (void)awakeFromNib | |
{ | |
[self NKTTextView_requiredInit]; | |
} | |
- (void)NKTTextView_requiredInit | |
{ | |
self.alwaysBounceVertical = YES; | |
text_ = [[NSMutableAttributedString alloc] init]; | |
margins_ = UIEdgeInsetsMake(60.0, 80.0, 80.0, 60.0); | |
lineHeight_ = 30.0; | |
horizontalRulesEnabled_ = YES; | |
horizontalRuleColor_ = [[UIColor colorWithRed:0.72 green:0.72 blue:0.59 alpha:1.0] retain]; | |
horizontalRuleOffset_ = 3.0; | |
verticalMarginEnabled_ = YES; | |
verticalMarginColor_ = [[UIColor colorWithRed:1.0 green:0.4 blue:0.4 alpha:1.0] retain]; | |
verticalMarginInset_ = 60.0; | |
visibleSections_ = [[NSMutableSet alloc] init]; | |
reusableSections_ = [[NSMutableSet alloc] init]; | |
underlayViews_ = [[NSMutableSet alloc] init]; | |
overlayViews = [[NSMutableSet alloc] init]; | |
selectionDisplayController_ = [[NKTSelectionDisplayController alloc] init]; | |
selectionDisplayController_.delegate = self; | |
[self initGestureRecognizers]; | |
} | |
- (void)initGestureRecognizers | |
{ | |
gestureRecognizerDelegate_ = [[NKTTextViewGestureRecognizerDelegate alloc] initWithTextView:self]; | |
doubleTapDragGestureRecognizer_ = [[NKTDragGestureRecognizer alloc] initWithTarget:self | |
action:@selector(handleDoubleTapDrag:)]; | |
doubleTapDragGestureRecognizer_.minimumNumberOfTouches = 2; | |
doubleTapDragGestureRecognizer_.maximumNumberOfTouches = 2; | |
doubleTapDragGestureRecognizer_.delegate = gestureRecognizerDelegate_; | |
nonEditTapGestureRecognizer_ = [[UITapGestureRecognizer alloc] initWithTarget:self | |
action:@selector(handleTap:)]; | |
nonEditTapGestureRecognizer_.delegate = gestureRecognizerDelegate_; | |
[nonEditTapGestureRecognizer_ requireGestureRecognizerToFail:doubleTapDragGestureRecognizer_]; | |
tapGestureRecognizer_ = [[UITapGestureRecognizer alloc] initWithTarget:self | |
action:@selector(handleTap:)]; | |
tapGestureRecognizer_.delegate = gestureRecognizerDelegate_; | |
longPressGestureRecognizer_ = [[UILongPressGestureRecognizer alloc] initWithTarget:self | |
action:@selector(handleLongPress:)]; | |
longPressGestureRecognizer_.delegate = gestureRecognizerDelegate_; | |
[self addGestureRecognizer:tapGestureRecognizer_]; | |
[self addGestureRecognizer:nonEditTapGestureRecognizer_]; | |
[self addGestureRecognizer:longPressGestureRecognizer_]; | |
[self addGestureRecognizer:doubleTapDragGestureRecognizer_]; | |
// Add drag gesture recognizers to the selection display controller managed handles | |
backwardHandleGestureRecognizer_ = [[NKTDragGestureRecognizer alloc] initWithTarget:self | |
action:@selector(handleBackwardHandleDrag:)]; | |
[selectionDisplayController_.backwardHandle addGestureRecognizer:backwardHandleGestureRecognizer_]; | |
forwardHandleGestureRecognizer_ = [[NKTDragGestureRecognizer alloc] initWithTarget:self | |
action:@selector(handleForwardHandleDrag:)]; | |
[selectionDisplayController_.forwardHandle addGestureRecognizer:forwardHandleGestureRecognizer_]; | |
} | |
#pragma mark - | |
#pragma mark Memory | |
- (void)dealloc | |
{ | |
[text_ release]; | |
[backgroundView_ release]; | |
[horizontalRuleColor_ release]; | |
[verticalMarginColor_ release]; | |
[framesetter_ release]; | |
[visibleSections_ release]; | |
[reusableSections_ release]; | |
[underlayViews_ release]; | |
[overlayViews release]; | |
[selectionDisplayController_ release]; | |
[textRangeLoupe_ release]; | |
[caretLoupe_ release]; | |
[gestureTextRange_ release]; | |
[selectedTextRange_ release]; | |
[markedTextRange_ release]; | |
[markedTextStyle_ release]; | |
[markedText_ release]; | |
[inputTextAttributes_ release]; | |
[tokenizer_ release]; | |
[gestureRecognizerDelegate_ release]; | |
[nonEditTapGestureRecognizer_ release]; | |
[tapGestureRecognizer_ release]; | |
[longPressGestureRecognizer_ release]; | |
[doubleTapDragGestureRecognizer_ release]; | |
[initialDoubleTapTextRange_ release]; | |
[backwardHandleGestureRecognizer_ release]; | |
[forwardHandleGestureRecognizer_ release]; | |
[super dealloc]; | |
} | |
- (void)purgeCachedResources | |
{ | |
[self.framesetter purgeCachedResources]; | |
} | |
#pragma mark - | |
#pragma mark Configuring the Background | |
- (void)setBackgroundView:(UIView *)backgroundView | |
{ | |
if (backgroundView_ == backgroundView) | |
{ | |
return; | |
} | |
if (backgroundView_ != nil) | |
{ | |
[backgroundView_ removeFromSuperview]; | |
[underlayViews_ removeObject:backgroundView_]; | |
[backgroundView_ release]; | |
} | |
backgroundView_ = [backgroundView retain]; | |
if (backgroundView_ != nil) | |
{ | |
[self insertSubview:backgroundView_ atIndex:0]; | |
backgroundView_.frame = self.bounds; | |
[underlayViews_ addObject:backgroundView_]; | |
} | |
} | |
#pragma mark - | |
#pragma mark Updating the Content Size | |
- (void)updateContentSize | |
{ | |
CGSize size = self.bounds.size; | |
size.height = self.framesetter.frameSize.height + margins_.top + margins_.bottom; | |
self.contentSize = size; | |
} | |
#pragma mark - | |
#pragma mark Modifying the Bounds and Frame Rectangles | |
- (void)setFrame:(CGRect)frame | |
{ | |
if (CGRectEqualToRect(self.frame, frame)) | |
{ | |
return; | |
} | |
[super setFrame:frame]; | |
backgroundView_.frame = self.bounds; | |
// HACK: when the nib loads, -setFrame: gets called, but it happens before awakeFromNib! We just check the text | |
// property instead before doing any further processing. | |
if (text_ != nil) | |
{ | |
[self updateTextFrame]; | |
[selectionDisplayController_ updateSelectionDisplay]; | |
} | |
} | |
#pragma mark - | |
#pragma mark Laying Out Subviews | |
// Called when scrolling occurs (behavior inherited from UIScrollView). We tile the sections as | |
// neccesary whenever scrolling occurs. | |
- (void)layoutSubviews | |
{ | |
[super layoutSubviews]; | |
CGRect frame = backgroundView_.frame; | |
frame.origin = self.contentOffset; | |
backgroundView_.frame = frame; | |
[self tileSections]; | |
} | |
#pragma mark - | |
#pragma mark Accessing the Text | |
- (void)setText:(NSAttributedString *)text | |
{ | |
if (text_ == text) | |
{ | |
return; | |
} | |
self.gestureTextRange = nil; | |
[self setSelectedTextRange:nil notifyInputDelegate:YES]; | |
[self setMarkedTextRange:nil notifyInputDelegate:YES]; | |
[inputDelegate_ textWillChange:self]; | |
[text_ release]; | |
text_ = [[NSMutableAttributedString alloc] initWithAttributedString:text]; | |
[self updateTextFrame]; | |
[inputDelegate_ textDidChange:self]; | |
self.contentOffset = CGPointZero; | |
self.inputTextAttributes = nil; | |
[selectionDisplayController_ updateSelectionDisplay]; | |
} | |
#pragma mark - | |
#pragma mark Styling Text | |
// Input text attributes refer to the text attributes that would be applied to inserted/modified | |
// text. Input text attributes are context dependent and are based on the text and selected text | |
// range. | |
- (NSDictionary *)inputTextAttributes | |
{ | |
// If the inputTextAttributes_ variable has been set, just use it, else we get the appropriate | |
// attributes through some context dependent logic | |
if (inputTextAttributes_ != nil) | |
{ | |
return inputTextAttributes_; | |
} | |
// Use default text attributes if there is no text or selected text range | |
if (![self hasText] || selectedTextRange_ == nil) | |
{ | |
if ([self.delegate respondsToSelector:@selector(defaultCoreTextAttributes)]) | |
{ | |
return [(id <NKTTextViewDelegate>)self.delegate defaultCoreTextAttributes]; | |
} | |
} | |
// Otherwise, get the typing attributes by looking at the text and selected text range | |
NKTTextPosition *textPosition = selectedTextRange_.start; | |
// The typing attributes for a non-empty text range are the attributes for the first character | |
// of the selection | |
if (!self.selectedTextRange.empty) | |
{ | |
return [self.text attributesAtIndex:textPosition.location effectiveRange:NULL]; | |
} | |
// The typing attributes at the beginning of a paragraph are the attributes for the first | |
// character of the paragraph | |
else if ([textPosition compareIgnoringAffinity:(NKTTextPosition *)[self endOfDocument]] == NSOrderedAscending && | |
[self.tokenizer isPosition:textPosition | |
atBoundary:UITextGranularityParagraph | |
inDirection:UITextStorageDirectionBackward]) | |
{ | |
return [self.text attributesAtIndex:textPosition.location effectiveRange:NULL]; | |
} | |
// Selected text range is empty, use the typing attributes for the character preceding the | |
// insertion point if possible | |
else | |
{ | |
NSUInteger index = textPosition.location; | |
if (index > 0) | |
{ | |
--index; | |
} | |
return [self.text attributesAtIndex:index effectiveRange:NULL]; | |
} | |
} | |
- (void)setInputTextAttributes:(NSDictionary *)inputTextAttributes | |
{ | |
if (inputTextAttributes_ == inputTextAttributes) | |
{ | |
return; | |
} | |
[inputTextAttributes_ release]; | |
inputTextAttributes_ = [inputTextAttributes copy]; | |
[selectionDisplayController_ updateSelectionDisplay]; | |
} | |
- (void)styleTextRange:(NKTTextRange *)textRange withTarget:(id)target selector:(SEL)selector | |
{ | |
if (textRange == nil || textRange.empty) | |
{ | |
return; | |
} | |
[self notifyDelegateOfStyleWillChangeFromTextPosition:textRange.start]; | |
NSUInteger currentLocation = textRange.start.location; | |
while (currentLocation < textRange.end.location) | |
{ | |
NSRange longestEffectiveRange; | |
NSDictionary *attributes = [text_ attributesAtIndex:currentLocation longestEffectiveRange:&longestEffectiveRange inRange:selectedTextRange_.nsRange]; | |
NSDictionary *newAttributes = [target performSelector:selector withObject:attributes]; | |
if (newAttributes != attributes) | |
{ | |
[text_ setAttributes:newAttributes range:longestEffectiveRange]; | |
} | |
currentLocation = longestEffectiveRange.location + longestEffectiveRange.length; | |
} | |
[self updateTextFrameForChangeFromTextPosition:textRange.start]; | |
[self notifyDelegateOfStyleDidChangeFromTextPosition:textRange.start]; | |
} | |
#pragma mark - | |
#pragma mark Configuring Text Layout and Style | |
- (void)setMargins:(UIEdgeInsets)margins | |
{ | |
margins_ = margins; | |
[self updateTextFrame]; | |
[selectionDisplayController_ updateSelectionDisplay]; | |
} | |
- (void)setLineHeight:(CGFloat)lineHeight | |
{ | |
lineHeight_ = lineHeight; | |
[self updateTextFrame]; | |
[selectionDisplayController_ updateSelectionDisplay]; | |
} | |
- (void)setHorizontalRulesEnabled:(BOOL)horizontalRulesEnabled | |
{ | |
horizontalRulesEnabled_ = horizontalRulesEnabled; | |
[self untileVisibleSections]; | |
[self tileSections]; | |
} | |
- (void)setHorizontalRuleColor:(UIColor *)horizontalRuleColor | |
{ | |
[horizontalRuleColor_ release]; | |
horizontalRuleColor_ = [horizontalRuleColor retain]; | |
[self untileVisibleSections]; | |
[self tileSections]; | |
} | |
- (void)setHorizontalRuleOffset:(CGFloat)horizontalRuleOffset | |
{ | |
horizontalRuleOffset_ = horizontalRuleOffset; | |
[self untileVisibleSections]; | |
[self tileSections]; | |
} | |
- (void)setVerticalMarginEnabled:(BOOL)verticalMarginEnabled | |
{ | |
verticalMarginEnabled_ = verticalMarginEnabled; | |
[self untileVisibleSections]; | |
[self tileSections]; | |
} | |
- (void)setVerticalMarginColor:(UIColor *)verticalMarginColor | |
{ | |
[verticalMarginColor_ release]; | |
verticalMarginColor_ = [verticalMarginColor retain]; | |
[self untileVisibleSections]; | |
[self tileSections]; | |
} | |
- (void)setVerticalMarginInset:(CGFloat)verticalMarginInset | |
{ | |
verticalMarginInset_ = verticalMarginInset; | |
[self untileVisibleSections]; | |
[self tileSections]; | |
} | |
#pragma mark - | |
#pragma mark Tiling Sections | |
- (void)tileSections | |
{ | |
KBCLogDebug(@"tiling within %@", NSStringFromCGRect(self.bounds)); | |
CGRect bounds = self.bounds; | |
NSInteger firstVisibleSectionIndex = (NSInteger)floorf(CGRectGetMinY(bounds) / CGRectGetHeight(bounds)); | |
NSInteger lastVisibleSectionIndex = (NSInteger)floorf((CGRectGetMaxY(bounds) - 1.0) / CGRectGetHeight(bounds)); | |
// Recycle no longer visible sections | |
for (NKTTextSection *section in visibleSections_) | |
{ | |
if (section.index < firstVisibleSectionIndex || section.index > lastVisibleSectionIndex) | |
{ | |
KBCLogDebug(@"untiling section %d", section.index); | |
[reusableSections_ addObject:section]; | |
[section removeFromSuperview]; | |
} | |
} | |
[visibleSections_ minusSet:reusableSections_]; | |
// Add missing sections | |
for (NSInteger index = firstVisibleSectionIndex; index <= lastVisibleSectionIndex; ++index) | |
{ | |
if (![self isDisplayingSectionAtIndex:index]) | |
{ | |
NKTTextSection *section = [self dequeueReusableSection]; | |
if (section == nil) | |
{ | |
section = [[NKTTextSection alloc] initWithFrame:bounds]; | |
} | |
[self configureSection:section atIndex:index]; | |
NSUInteger insertionIndex = [underlayViews_ count]; | |
KBCLogDebug(@"tiling section %d", section.index); | |
[self insertSubview:section atIndex:insertionIndex]; | |
[visibleSections_ addObject:section]; | |
} | |
} | |
} | |
- (void)untileVisibleSections | |
{ | |
for (NKTTextSection *section in visibleSections_) | |
{ | |
KBCLogDebug(@"untiling section %d", section.index); | |
[reusableSections_ addObject:section]; | |
[section removeFromSuperview]; | |
} | |
[visibleSections_ removeAllObjects]; | |
} | |
- (BOOL)isDisplayingSectionAtIndex:(NSInteger)index | |
{ | |
for (NKTTextSection *section in visibleSections_) | |
{ | |
if (section.index == index) | |
{ | |
return YES; | |
} | |
} | |
return NO; | |
} | |
- (NKTTextSection *)dequeueReusableSection | |
{ | |
NKTTextSection *section = [reusableSections_ anyObject]; | |
if (section != nil) | |
{ | |
[[section retain] autorelease]; | |
[reusableSections_ removeObject:section]; | |
return section; | |
} | |
return nil; | |
} | |
- (void)configureSection:(NKTTextSection *)section atIndex:(NSInteger)index | |
{ | |
section.frame = [self frameForSectionAtIndex:index]; | |
section.index = index; | |
section.framesetter = self.framesetter; | |
section.margins = margins_; | |
section.lineHeight = lineHeight_; | |
section.horizontalRulesEnabled = horizontalRulesEnabled_; | |
section.horizontalRuleColor = horizontalRuleColor_; | |
section.horizontalRuleOffset = horizontalRuleOffset_; | |
section.verticalMarginEnabled = verticalMarginEnabled_; | |
section.verticalMarginColor = verticalMarginColor_; | |
section.verticalMarginInset = verticalMarginInset_; | |
[section setNeedsDisplay]; | |
} | |
- (CGRect)frameForSectionAtIndex:(NSInteger)index | |
{ | |
CGRect sectionFrame = self.bounds; | |
sectionFrame.origin.x = 0.0; | |
sectionFrame.origin.y = (CGFloat)index * CGRectGetHeight(sectionFrame); | |
return sectionFrame; | |
} | |
#pragma mark - | |
#pragma mark Managing the Framesetter | |
- (CGFloat)lineWidth | |
{ | |
return self.bounds.size.width - margins_.left - margins_.right; | |
} | |
- (NKTFramesetter *)framesetter | |
{ | |
if (framesetter_ == nil) | |
{ | |
framesetter_ = [[NKTFramesetter alloc] initWithText:text_ lineWidth:[self lineWidth] lineHeight:lineHeight_]; | |
} | |
return framesetter_; | |
} | |
- (void)invalidateFramesetter | |
{ | |
[framesetter_ release]; | |
framesetter_ = nil; | |
} | |
- (void)updateTextFrame | |
{ | |
[self invalidateFramesetter]; | |
[self untileVisibleSections]; | |
[self tileSections]; | |
[self updateContentSize]; | |
} | |
- (void)updateTextFrameForChangeFromTextPosition:(NKTTextPosition *)textPosition | |
{ | |
[self.framesetter textChangedFromTextPosition:textPosition]; | |
[self untileVisibleSections]; | |
[self tileSections]; | |
[self updateContentSize]; | |
} | |
- (CGPoint)convertPointToFramesetter:(CGPoint)point | |
{ | |
return CGPointMake(point.x - margins_.left, point.y - margins_.top); | |
} | |
- (CGPoint)convertPointFromFramesetter:(CGPoint)point | |
{ | |
return CGPointMake(point.x + margins_.left, point.y + margins_.top); | |
} | |
- (CGAffineTransform)viewToFramesetterTransform | |
{ | |
return CGAffineTransformMakeTranslation(-margins_.left, -margins_.top); | |
} | |
- (CGAffineTransform)framesetterToViewTransform | |
{ | |
return CGAffineTransformMakeTranslation(margins_.left, margins_.top); | |
} | |
#pragma mark - | |
#pragma mark Notifying the Delegate | |
- (void)notifyDelegateOfTextWillChangeFromTextPosition:(NKTTextPosition *)textPosition | |
{ | |
if ([self.delegate respondsToSelector:@selector(textView:willChangeTextFromTextPosition:)]) | |
{ | |
[(id <NKTTextViewDelegate>)self.delegate textView:self willChangeTextFromTextPosition:textPosition]; | |
} | |
} | |
- (void)notifyDelegateOfTextDidChangeFromTextPosition:(NKTTextPosition *)textPosition | |
{ | |
if ([self.delegate respondsToSelector:@selector(textView:didChangeTextFromTextPosition:)]) | |
{ | |
[(id <NKTTextViewDelegate>)self.delegate textView:self didChangeTextFromTextPosition:textPosition]; | |
} | |
} | |
- (void)notifyDelegateOfStyleWillChangeFromTextPosition:(NKTTextPosition *)textPosition | |
{ | |
if ([self.delegate respondsToSelector:@selector(textView:willChangeStyleFromTextPosition:)]) | |
{ | |
[(id <NKTTextViewDelegate>)self.delegate textView:self willChangeStyleFromTextPosition:textPosition]; | |
} | |
} | |
- (void)notifyDelegateOfStyleDidChangeFromTextPosition:(NKTTextPosition *)textPosition | |
{ | |
if ([self.delegate respondsToSelector:@selector(textView:didChangeStyleFromTextPosition:)]) | |
{ | |
[(id <NKTTextViewDelegate>)self.delegate textView:self didChangeStyleFromTextPosition:textPosition]; | |
} | |
} | |
#pragma mark - | |
#pragma mark Managing the Responder Chain | |
- (BOOL)canBecomeFirstResponder | |
{ | |
return YES; | |
} | |
- (BOOL)becomeFirstResponder | |
{ | |
BOOL becameFirstResponder = [super becomeFirstResponder]; | |
if (becameFirstResponder) | |
{ | |
// While editing, there should always be a selected text range | |
if (selectedTextRange_ == nil) | |
{ | |
NKTTextRange *textRange = [(NKTTextPosition *)[self beginningOfDocument] textRange]; | |
[self setSelectedTextRange:textRange notifyInputDelegate:YES]; | |
selectionDisplayController_.caretVisible = YES; | |
[selectionDisplayController_ updateSelectionDisplay]; | |
} | |
if ([self.delegate respondsToSelector:@selector(textViewDidBeginEditing:)]) | |
{ | |
[(id <NKTTextViewDelegate>)self.delegate textViewDidBeginEditing:self]; | |
} | |
} | |
return becameFirstResponder; | |
} | |
- (BOOL)resignFirstResponder | |
{ | |
BOOL resignedFirstResponder = [super resignFirstResponder]; | |
if (!resignedFirstResponder) | |
{ | |
return NO; | |
} | |
self.gestureTextRange = nil; | |
[self setSelectedTextRange:nil notifyInputDelegate:NO]; | |
[self setMarkedTextRange:nil notifyInputDelegate:NO]; | |
[selectionDisplayController_ updateSelectionDisplay]; | |
if ([self.delegate respondsToSelector:@selector(textViewDidEndEditing:)]) | |
{ | |
[(id <NKTTextViewDelegate>)self.delegate textViewDidEndEditing:self]; | |
} | |
return YES; | |
} | |
#pragma mark - | |
#pragma mark Responding to Gestures | |
- (void)handleTap:(UIGestureRecognizer *)gestureRecognizer | |
{ | |
NKTTextRange *previousSelectedTextRange = [[selectedTextRange_ retain] autorelease]; | |
CGPoint point = [gestureRecognizer locationInView:self]; | |
CGPoint framesetterPoint = [self convertPointToFramesetter:point]; | |
NKTTextPosition *textPosition = [self.framesetter closestTextPositionForCaretToPoint:framesetterPoint]; | |
[self setMarkedTextRange:nil notifyInputDelegate:YES]; | |
[self setSelectedTextRange:[textPosition textRange] notifyInputDelegate:YES]; | |
selectionDisplayController_.caretVisible = YES; | |
[selectionDisplayController_ updateSelectionDisplay]; | |
BOOL wasFirstResponder = [self isFirstResponder]; | |
[self becomeFirstResponder]; | |
if (wasFirstResponder && [self.delegate respondsToSelector:@selector(textViewDidRecognizeTap:previousSelectedTextRange:)]) | |
{ | |
[(id <NKTTextViewDelegate>)self.delegate textViewDidRecognizeTap:self previousSelectedTextRange:previousSelectedTextRange]; | |
} | |
} | |
- (void)handleLongPress:(UIGestureRecognizer *)gestureRecognizer | |
{ | |
if (gestureRecognizer.state == UIGestureRecognizerStateBegan) | |
{ | |
if ([self.delegate respondsToSelector:@selector(textViewLongPressDidBegin:)]) | |
{ | |
[(id <NKTTextViewDelegate>)self.delegate textViewLongPressDidBegin:self]; | |
} | |
[self updateLongPressSelection]; | |
[self startEdgeScrollCheckWithGestureRecognizer:gestureRecognizer selector:@selector(updateLongPressSelection)]; | |
} | |
else if (gestureRecognizer.state == UIGestureRecognizerStateChanged) | |
{ | |
[self updateLongPressSelection]; | |
} | |
else | |
{ | |
[self stopEdgeScrollCheckWithGestureRecognizer:gestureRecognizer selector:@selector(updateLongPressSelection)]; | |
[self confirmGestureTextRange]; | |
[self.caretLoupe setHidden:YES animated:YES]; | |
selectionDisplayController_.caretVisible = [self isFirstResponder]; | |
if ([self.delegate respondsToSelector:@selector(textViewLongPressDidEnd:)]) | |
{ | |
[(id <NKTTextViewDelegate>)self.delegate textViewLongPressDidEnd:self]; | |
} | |
} | |
} | |
- (void)updateLongPressSelection | |
{ | |
CGPoint point = [longPressGestureRecognizer_ locationInView:self]; | |
CGPoint framesetterPoint = [self convertPointToFramesetter:point]; | |
NKTTextPosition *textPosition = [self.framesetter closestTextPositionForCaretToPoint:framesetterPoint]; | |
self.gestureTextRange = [textPosition textRange]; | |
[self setMarkedTextRange:nil notifyInputDelegate:YES]; | |
[self configureLoupe:self.caretLoupe toShowPoint:point anchorToLine:NO]; | |
[self.caretLoupe setHidden:NO animated:YES]; | |
selectionDisplayController_.caretVisible = YES; | |
[selectionDisplayController_ updateSelectionDisplay]; | |
} | |
- (void)handleDoubleTapDrag:(UIGestureRecognizer *)gestureRecognizer | |
{ | |
CGPoint point = [doubleTapDragGestureRecognizer_ locationInView:self]; | |
CGPoint framesetterPoint = [self convertPointToFramesetter:point]; | |
NKTTextPosition *textPosition = [self.framesetter closestTextPositionForCaretToPoint:framesetterPoint]; | |
if (gestureRecognizer.state == UIGestureRecognizerStateBegan) | |
{ | |
if ([self.delegate respondsToSelector:@selector(textViewDoubleTapDragDidBegin:)]) | |
{ | |
[(id <NKTTextViewDelegate>)self.delegate textViewDoubleTapDragDidBegin:self]; | |
} | |
NKTTextRange *wordRange = nil; | |
NKTTextRange *guessedTextRange = [self guessedTextRangeAtTextPosition:textPosition wordRange:&wordRange]; | |
if (guessedTextRange == nil) | |
{ | |
return; | |
} | |
if (wordRange == nil) | |
{ | |
wordRange = guessedTextRange; | |
} | |
self.initialDoubleTapTextRange = wordRange; | |
self.gestureTextRange = guessedTextRange; | |
[self setMarkedTextRange:nil notifyInputDelegate:YES]; | |
[self configureLoupe:self.textRangeLoupe toShowPoint:point anchorToLine:YES]; | |
[self.textRangeLoupe setHidden:NO animated:YES]; | |
[selectionDisplayController_ updateSelectionDisplay]; | |
[self startEdgeScrollCheckWithGestureRecognizer:gestureRecognizer selector:@selector(updateDoubleTapDragSelection)]; | |
} | |
else if (gestureRecognizer.state == UIGestureRecognizerStateChanged) | |
{ | |
if (initialDoubleTapTextRange_ == nil) | |
{ | |
return; | |
} | |
[self updateDoubleTapDragSelection]; | |
} | |
else | |
{ | |
if (initialDoubleTapTextRange_ != nil) | |
{ | |
[self stopEdgeScrollCheckWithGestureRecognizer:gestureRecognizer selector:@selector(updateDoubleTapDragSelection)]; | |
self.initialDoubleTapTextRange = nil; | |
[self confirmGestureTextRange]; | |
[self.textRangeLoupe setHidden:YES animated:YES]; | |
} | |
if ([self.delegate respondsToSelector:@selector(textViewDoubleTapDragDidEnd:)]) | |
{ | |
[(id <NKTTextViewDelegate>)self.delegate textViewDoubleTapDragDidEnd:self]; | |
} | |
} | |
} | |
- (void)updateDoubleTapDragSelection | |
{ | |
CGPoint point = [doubleTapDragGestureRecognizer_ locationInView:self]; | |
CGPoint framesetterPoint = [self convertPointToFramesetter:point]; | |
NKTTextPosition *textPosition = [self.framesetter closestTextPositionForCaretToPoint:framesetterPoint]; | |
if ([textPosition compare:initialDoubleTapTextRange_.start] == NSOrderedAscending) | |
{ | |
self.gestureTextRange = [NKTTextRange textRangeWithTextPosition:textPosition textPosition:initialDoubleTapTextRange_.end]; | |
} | |
else if ([textPosition compare:initialDoubleTapTextRange_.end] == NSOrderedDescending) | |
{ | |
self.gestureTextRange = [NKTTextRange textRangeWithTextPosition:initialDoubleTapTextRange_.start textPosition:textPosition]; | |
} | |
else | |
{ | |
self.gestureTextRange = initialDoubleTapTextRange_; | |
} | |
[self configureLoupe:self.textRangeLoupe toShowPoint:point anchorToLine:YES]; | |
[self.textRangeLoupe setHidden:NO animated:YES]; | |
[selectionDisplayController_ updateSelectionDisplay]; | |
} | |
- (void)handleBackwardHandleDrag:(UIGestureRecognizer *)gestureRecognizer | |
{ | |
CGPoint point = [gestureRecognizer locationInView:self]; | |
CGPoint framesetterPoint = [self convertPointToFramesetter:point]; | |
NKTTextPosition *textPosition = [self.framesetter closestTextPositionForCaretToPoint:framesetterPoint]; | |
if (gestureRecognizer.state == UIGestureRecognizerStateBegan) | |
{ | |
if ([self.delegate respondsToSelector:@selector(textViewDragBackwardDidBegin:)]) | |
{ | |
[(id <NKTTextViewDelegate>)self.delegate textViewDragBackwardDidBegin:self]; | |
} | |
self.gestureTextRange = selectedTextRange_; | |
[self configureLoupe:self.textRangeLoupe toShowTextPosition:textPosition]; | |
[self.textRangeLoupe setHidden:NO animated:YES]; | |
[self startEdgeScrollCheckWithGestureRecognizer:gestureRecognizer selector:@selector(updateBackwardHandleDragSelection)]; | |
} | |
else if (gestureRecognizer.state == UIGestureRecognizerStateChanged) | |
{ | |
[self updateBackwardHandleDragSelection]; | |
} | |
else | |
{ | |
[self stopEdgeScrollCheckWithGestureRecognizer:gestureRecognizer selector:@selector(updateBackwardHandleDragSelection)]; | |
[self confirmGestureTextRange]; | |
[self.textRangeLoupe setHidden:YES animated:YES]; | |
if ([self.delegate respondsToSelector:@selector(textViewDragBackwardDidEnd:)]) | |
{ | |
[(id <NKTTextViewDelegate>)self.delegate textViewDragBackwardDidEnd:self]; | |
} | |
} | |
} | |
- (void)updateBackwardHandleDragSelection | |
{ | |
CGPoint point = [backwardHandleGestureRecognizer_ locationInView:self]; | |
CGPoint framesetterPoint = [self convertPointToFramesetter:point]; | |
NKTTextPosition *textPosition = [self.framesetter closestTextPositionForCaretToPoint:framesetterPoint]; | |
if ([textPosition compare:selectedTextRange_.end] == NSOrderedAscending) | |
{ | |
self.gestureTextRange = [NKTTextRange textRangeWithTextPosition:textPosition | |
textPosition:selectedTextRange_.end]; | |
[self setMarkedTextRange:nil notifyInputDelegate:YES]; | |
[selectionDisplayController_ updateSelectionDisplay]; | |
} | |
[self configureLoupe:self.textRangeLoupe toShowPoint:point anchorToLine:YES]; | |
[self.textRangeLoupe setHidden:NO animated:YES]; | |
} | |
- (void)handleForwardHandleDrag:(UIGestureRecognizer *)gestureRecognizer | |
{ | |
CGPoint point = [gestureRecognizer locationInView:self]; | |
CGPoint framesetterPoint = [self convertPointToFramesetter:point]; | |
NKTTextPosition *textPosition = [self.framesetter closestTextPositionForCaretToPoint:framesetterPoint]; | |
if (gestureRecognizer.state == UIGestureRecognizerStateBegan) | |
{ | |
if ([self.delegate respondsToSelector:@selector(textViewDragForwardDidBegin:)]) | |
{ | |
[(id <NKTTextViewDelegate>)self.delegate textViewDragForwardDidBegin:self]; | |
} | |
self.gestureTextRange = selectedTextRange_; | |
[self configureLoupe:self.textRangeLoupe toShowTextPosition:textPosition]; | |
[self.textRangeLoupe setHidden:NO animated:YES]; | |
[self startEdgeScrollCheckWithGestureRecognizer:gestureRecognizer selector:@selector(updateForwardHandleDragSelection)]; | |
} | |
else if (gestureRecognizer.state == UIGestureRecognizerStateChanged) | |
{ | |
[self updateForwardHandleDragSelection]; | |
} | |
else | |
{ | |
[self stopEdgeScrollCheckWithGestureRecognizer:gestureRecognizer selector:@selector(updateForwardHandleDragSelection)]; | |
[self confirmGestureTextRange]; | |
[self.textRangeLoupe setHidden:YES animated:YES]; | |
if ([self.delegate respondsToSelector:@selector(textViewDragForwardDidEnd:)]) | |
{ | |
[(id <NKTTextViewDelegate>)self.delegate textViewDragForwardDidEnd:self]; | |
} | |
} | |
} | |
- (void)updateForwardHandleDragSelection | |
{ | |
CGPoint point = [forwardHandleGestureRecognizer_ locationInView:self]; | |
CGPoint framesetterPoint = [self convertPointToFramesetter:point]; | |
NKTTextPosition *textPosition = [self.framesetter closestTextPositionForCaretToPoint:framesetterPoint]; | |
if ([textPosition compare:selectedTextRange_.start] == NSOrderedDescending) | |
{ | |
self.gestureTextRange = [NKTTextRange textRangeWithTextPosition:selectedTextRange_.start textPosition:textPosition]; | |
[self setMarkedTextRange:nil notifyInputDelegate:YES]; | |
[selectionDisplayController_ updateSelectionDisplay]; | |
} | |
[self configureLoupe:self.textRangeLoupe toShowPoint:point anchorToLine:YES]; | |
[self.textRangeLoupe setHidden:NO animated:YES]; | |
} | |
// Searches for the likely word being indicated at the given text position when the user performs | |
// a selection gesture. | |
// | |
// The search considers words in the following order: | |
// - at the text position | |
// - at a word before the text position | |
// - at a word after the text position | |
// | |
// The search is bounded by the line that contains text position. | |
// | |
// If no match is found, the text range represented by the text position is returned. | |
- (NKTTextRange *)guessedTextRangeAtTextPosition:(NKTTextPosition *)textPosition wordRange:(NKTTextRange **)wordRange | |
{ | |
if (textPosition == nil) | |
{ | |
KBCLogWarning(@"Text position is nil. Returning nil"); | |
return nil; | |
} | |
NKTLine *line = [self.framesetter lineForCaretAtTextPosition:textPosition]; | |
NKTTextRange *lineTextRange = [line textRange]; | |
// Within a word, and the end of word follows the text position | |
if ([self.tokenizer isPosition:textPosition withinTextUnit:UITextGranularityWord inDirection:UITextStorageDirectionForward]) | |
{ | |
// Accept if start of word precedes end of line | |
if ([textPosition compareIgnoringAffinity:lineTextRange.end] == NSOrderedAscending) | |
{ | |
NKTTextRange *textRange = (NKTTextRange *)[self.tokenizer rangeEnclosingPosition:textPosition withGranularity:UITextGranularityWord inDirection:UITextStorageDirectionForward]; | |
if (wordRange != NULL) | |
{ | |
*wordRange = textRange; | |
} | |
return textRange; | |
} | |
} | |
// Within a word, and the start of word precedes the text position | |
else if ([self.tokenizer isPosition:textPosition withinTextUnit:UITextGranularityWord inDirection:UITextStorageDirectionBackward]) | |
{ | |
// Accept if end of word follows start of line | |
if ([lineTextRange.start compareIgnoringAffinity:textPosition] == NSOrderedAscending) | |
{ | |
NKTTextRange *textRange = (NKTTextRange *)[self.tokenizer rangeEnclosingPosition:textPosition withGranularity:UITextGranularityWord inDirection:UITextStorageDirectionBackward]; | |
if (wordRange != NULL) | |
{ | |
*wordRange = textRange; | |
} | |
return textRange; | |
} | |
} | |
NKTTextPosition *previousBoundary = (NKTTextPosition *)[self.tokenizer positionFromPosition:textPosition toBoundary:UITextGranularityWord inDirection:UITextStorageDirectionBackward]; | |
if ([lineTextRange containsTextPosition:previousBoundary]) | |
{ | |
NKTTextRange *textRange = (NKTTextRange *)[self.tokenizer rangeEnclosingPosition:previousBoundary withGranularity:UITextGranularityWord inDirection:UITextStorageDirectionBackward]; | |
// The text range could still be nil if the previous boundary is the start of the document | |
if (textRange != nil) | |
{ | |
if (wordRange != NULL) | |
{ | |
*wordRange = textRange; | |
} | |
return [NKTTextRange textRangeWithTextPosition:textRange.start textPosition:textPosition]; | |
} | |
} | |
NKTTextPosition *nextBoundary = (NKTTextPosition *)[self.tokenizer positionFromPosition:textPosition toBoundary:UITextGranularityWord inDirection:UITextStorageDirectionForward]; | |
if ([lineTextRange containsTextPosition:nextBoundary]) | |
{ | |
NKTTextRange *textRange = (NKTTextRange *)[self.tokenizer rangeEnclosingPosition:nextBoundary withGranularity:UITextGranularityWord inDirection:UITextStorageDirectionForward]; | |
if (textRange != nil) | |
{ | |
if (wordRange != NULL) | |
{ | |
*wordRange = textRange; | |
} | |
return [NKTTextRange textRangeWithTextPosition:textPosition textPosition:textRange.end]; | |
} | |
} | |
if (wordRange != NULL) | |
{ | |
*wordRange = nil; | |
} | |
return lineTextRange; | |
} | |
#pragma mark - | |
#pragma mark Scrolling | |
static NSString * const GestureRecognizerKey = @"GestureRecognizer"; | |
static NSString * const SelectorKey = @"Selector"; | |
static const CGFloat EdgeScrollCheckPeriodSeconds = 0.3; | |
static const CGFloat EdgeScrollThreshold = 40.0; | |
- (void)startEdgeScrollCheckWithGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer selector:(SEL)selector | |
{ | |
NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:gestureRecognizer, GestureRecognizerKey, | |
[NSValue valueWithPointer:selector], SelectorKey, | |
nil]; | |
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(edgeScrollCheckWithInfo:) object:info]; | |
[self performSelector:@selector(edgeScrollCheckWithInfo:) withObject:info afterDelay:EdgeScrollCheckPeriodSeconds]; | |
} | |
- (void)stopEdgeScrollCheckWithGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer selector:(SEL)selector | |
{ | |
NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:gestureRecognizer, GestureRecognizerKey, | |
[NSValue valueWithPointer:selector], SelectorKey, | |
nil]; | |
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(edgeScrollCheckWithInfo:) object:info]; | |
} | |
- (void)edgeScrollCheckWithInfo:(NSDictionary *)info | |
{ | |
UIGestureRecognizer *gestureRecognizer = [info objectForKey:GestureRecognizerKey]; | |
SEL selector = [[info objectForKey:SelectorKey] pointerValue]; | |
if (gestureRecognizer == nil) | |
{ | |
KBCLogWarning(@"gesture recognizer is nil, ignoring"); | |
return; | |
} | |
if (selector == NULL) | |
{ | |
KBCLogWarning(@"selector is NULL, ignoring"); | |
return; | |
} | |
CGPoint point = [gestureRecognizer locationInView:self]; | |
[self scrollAtEdgeWithPoint:point]; | |
[self performSelector:selector]; | |
// Reschedule this method with same arguments | |
[self performSelector:@selector(edgeScrollCheckWithInfo:) withObject:info afterDelay:EdgeScrollCheckPeriodSeconds]; | |
} | |
- (void)scrollAtEdgeWithPoint:(CGPoint)point | |
{ | |
CGPoint framesetterPoint = [self convertPointToFramesetter:point]; | |
CGPoint boundsPoint = CGPointMake(point.x - self.contentOffset.x, point.y - self.contentOffset.y); | |
NKTLine *line = [self.framesetter lineClosestToPoint:framesetterPoint]; | |
if (boundsPoint.y < EdgeScrollThreshold) | |
{ | |
if (line.index > 0) | |
{ | |
NKTLine *previousLine = [self.framesetter lineAtIndex:line.index - 1]; | |
[self scrollTextRangeToVisible:previousLine.textRange animated:YES]; | |
} | |
} | |
else if (boundsPoint.y > (self.bounds.size.height - EdgeScrollThreshold)) | |
{ | |
if (line.index + 1 < [self.framesetter numberOfLines]) | |
{ | |
NKTLine *nextLine = [self.framesetter lineAtIndex:line.index + 1]; | |
[self scrollTextRangeToVisible:nextLine.textRange animated:YES]; | |
} | |
} | |
} | |
- (void)scrollTextRangeToVisible:(NKTTextRange *)textRange animated:(BOOL)animated | |
{ | |
if (textRange == nil) | |
{ | |
return; | |
} | |
CGRect firstCaretRect = [self caretRectForTextPosition:textRange.start applyInputTextAttributes:YES]; | |
CGRect lastCaretRect = [self caretRectForTextPosition:textRange.end applyInputTextAttributes:YES]; | |
CGRect combinedRect = CGRectUnion(firstCaretRect, lastCaretRect); | |
combinedRect.origin.y -= 40.0; | |
combinedRect.size.height += 80.0; | |
[self scrollRectToVisible:combinedRect animated:animated]; | |
} | |
#pragma mark - | |
#pragma mark Managing Loupes | |
- (NKTLoupe *)textRangeLoupe | |
{ | |
if (textRangeLoupe_ == nil) | |
{ | |
textRangeLoupe_ = [[NKTLoupe alloc] initWithStyle:NKTLoupeStyleBand]; | |
textRangeLoupe_.hidden = YES; | |
textRangeLoupe_.zoomedView = self; | |
if ([self.delegate respondsToSelector:@selector(addLoupeView:)]) | |
{ | |
[(id <NKTTextViewDelegate>)self.delegate addLoupe:textRangeLoupe_]; | |
} | |
else | |
{ | |
[self.superview addSubview:textRangeLoupe_]; | |
} | |
// HACK: visual inconsistencies may appear the first time the text range loupe is shown. | |
// I haven't fully dived into unraveling the specific issue, but it probably has to do | |
// with a loupes somewhat esoteric drawing of another view within itself. For now, the | |
// line below forces one redisplay of the loupe when it is created the first time and | |
// seems to fix the problem. | |
[textRangeLoupe_ performSelector:@selector(setNeedsDisplay) withObject:nil afterDelay:0]; | |
} | |
return textRangeLoupe_; | |
} | |
- (NKTLoupe *)caretLoupe | |
{ | |
if (caretLoupe_ == nil) | |
{ | |
caretLoupe_ = [[NKTLoupe alloc] initWithStyle:NKTLoupeStyleRound]; | |
caretLoupe_.hidden = YES; | |
caretLoupe_.zoomedView = self; | |
if ([self.delegate respondsToSelector:@selector(addLoupeView:)]) | |
{ | |
[(id <NKTTextViewDelegate>)self.delegate addLoupe:caretLoupe_]; | |
} | |
else | |
{ | |
[self.superview addSubview:caretLoupe_]; | |
} | |
} | |
return caretLoupe_; | |
} | |
- (void)configureLoupe:(NKTLoupe *)loupe toShowPoint:(CGPoint)point anchorToLine:(BOOL)anchorToLine | |
{ | |
if ([self.delegate respondsToSelector:@selector(loupeFillColor)]) | |
{ | |
loupe.fillColor = [(id <NKTTextViewDelegate>)self.delegate loupeFillColor]; | |
} | |
if (anchorToLine) | |
{ | |
// Set the zoom center of the loupe to the baseline with the same offset as the original point | |
CGPoint framesetterPoint = [self convertPointToFramesetter:point]; | |
NKTLine *line = [self.framesetter lineClosestToPoint:framesetterPoint]; | |
CGPoint baselineOrigin = [self convertPointFromFramesetter:line.baselineOrigin]; | |
CGPoint zoomCenter = CGPointMake(point.x, baselineOrigin.y); | |
loupe.zoomCenter = [self convertPoint:zoomCenter toView:loupe.zoomedView]; | |
// Anchor loupe to a point just on top of the line | |
CGPoint anchor = CGPointMake(point.x, baselineOrigin.y - (lineHeight_ * 0.75)); | |
// PENDING: clamping wrong | |
//anchor = KBCClampPointToRect(anchor, self.bounds); | |
loupe.anchor = [self convertPoint:anchor toView:loupe.superview]; | |
} | |
else | |
{ | |
// No adjusting of point, just use it as both the zoom center and anchor | |
loupe.zoomCenter = [self convertPoint:point toView:loupe.zoomedView]; | |
// PENDING: clamping wrong | |
//CGPoint anchor = KBCClampPointToRect(point, self.bounds); | |
loupe.anchor = [self convertPoint:point toView:loupe.superview]; | |
} | |
} | |
- (void)configureLoupe:(NKTLoupe *)loupe toShowTextPosition:(NKTTextPosition *)textPosition | |
{ | |
if ([self.delegate respondsToSelector:@selector(loupeFillColor)]) | |
{ | |
loupe.fillColor = [(id <NKTTextViewDelegate>)self.delegate loupeFillColor]; | |
} | |
CGPoint framesetterPoint = [self.framesetter baselineOriginForCharAtTextPosition:textPosition]; | |
CGPoint point = [self convertPointFromFramesetter:framesetterPoint]; | |
loupe.zoomCenter = [self convertPoint:point toView:loupe.zoomedView]; | |
// Anchor loupe to a point just on top of the text position | |
CGPoint anchor = CGPointMake(point.x, point.y - (lineHeight_ * 0.75)); | |
// PENDING: clamping wrong | |
//anchor = KBCClampPointToRect(anchor, self.bounds); | |
loupe.anchor = [self convertPoint:anchor toView:loupe.superview]; | |
} | |
#pragma mark - | |
#pragma mark Inserting and Deleting Text | |
// UITextInput method | |
- (BOOL)hasText | |
{ | |
KBCLogDebug(@""); | |
return [text_ length] > 0; | |
} | |
// UITextInput method | |
- (void)insertText:(NSString *)text | |
{ | |
KBCLogDebug(text); | |
if (markedTextRange_ == nil && selectedTextRange_ == nil) | |
{ | |
KBCLogWarning(@"marked text range and selected text range are both nil, ignoring"); | |
return; | |
} | |
// Figure out the attributes that inserted text needs to have | |
NSDictionary *inputTextAttributes = [[self.inputTextAttributes copy] autorelease]; | |
NKTTextRange *insertionTextRange = (markedTextRange_ != nil) ? markedTextRange_ : selectedTextRange_; | |
// In case the underlying text range is nil-ed | |
[[insertionTextRange retain] autorelease]; | |
NSDictionary *inheritedAttributes = nil; | |
if ([self hasText]) | |
{ | |
NSUInteger inheritedAttributesIndex = insertionTextRange.start.location; | |
// If the replacement range is empty, inserted characters inherit the attributes of the | |
// character preceding the range, if any. | |
if (insertionTextRange.empty && inheritedAttributesIndex > 0) | |
{ | |
inheritedAttributesIndex = inheritedAttributesIndex - 1; | |
} | |
else if (inheritedAttributesIndex > [text_ length]) | |
{ | |
inheritedAttributesIndex = [text_ length] - 1; | |
} | |
inheritedAttributes = [text_ attributesAtIndex:inheritedAttributesIndex effectiveRange:NULL]; | |
} | |
[self notifyDelegateOfTextWillChangeFromTextPosition:insertionTextRange.start]; | |
// It is possible to avoid creating a new range of attributed text if the attributes that | |
// would be inherited from the string following insertion match the insertion attributes | |
if ([inheritedAttributes isEqualToDictionary:inputTextAttributes]) | |
{ | |
[text_ replaceCharactersInRange:insertionTextRange.nsRange withString:text]; | |
} | |
else | |
{ | |
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:text attributes:inputTextAttributes]; | |
[text_ replaceCharactersInRange:insertionTextRange.nsRange withAttributedString:attributedString]; | |
[attributedString release]; | |
} | |
[self updateTextFrameForChangeFromTextPosition:insertionTextRange.start]; | |
[self notifyDelegateOfTextDidChangeFromTextPosition:insertionTextRange.start]; | |
NKTTextPosition *textPosition = [NKTTextPosition textPositionWithLocation:insertionTextRange.start.location + [text length] | |
affinity:UITextStorageDirectionForward]; | |
[self setSelectedTextRange:[textPosition textRange] notifyInputDelegate:NO]; | |
[self setMarkedTextRange:nil notifyInputDelegate:NO]; | |
[selectionDisplayController_ updateSelectionDisplay]; | |
// Make caret visible if it is not | |
[self scrollTextRangeToVisible:self.selectedTextRange animated:YES]; | |
} | |
// UITextInput method | |
- (void)deleteBackward | |
{ | |
KBCLogDebug(@""); | |
if (markedTextRange_ == nil && selectedTextRange_ == nil) | |
{ | |
KBCLogWarning(@"marked text range and selected text range are nil, ignoring"); | |
return; | |
} | |
NKTTextRange *deletionTextRange = nil; | |
// PENDING: figure out a clearer refactor for this block | |
if (markedTextRange_ != nil) | |
{ | |
if (markedTextRange_.start.location != 0) | |
{ | |
deletionTextRange = [markedTextRange_ textRangeByApplyingStartOffset:-1]; | |
} | |
else | |
{ | |
deletionTextRange = markedTextRange_; | |
} | |
} | |
else | |
{ | |
if (selectedTextRange_.start.location != 0) | |
{ | |
deletionTextRange = [selectedTextRange_ textRangeByApplyingStartOffset:-1]; | |
} | |
else | |
{ | |
deletionTextRange = selectedTextRange_; | |
} | |
} | |
[self notifyDelegateOfTextWillChangeFromTextPosition:deletionTextRange.start]; | |
[text_ deleteCharactersInRange:deletionTextRange.nsRange]; | |
[self updateTextFrameForChangeFromTextPosition:deletionTextRange.start]; | |
[self notifyDelegateOfTextDidChangeFromTextPosition:deletionTextRange.start]; | |
NKTTextPosition *textPosition = [NKTTextPosition textPositionWithLocation:deletionTextRange.start.location | |
affinity:UITextStorageDirectionForward]; | |
[self setSelectedTextRange:[textPosition textRange] notifyInputDelegate:NO]; | |
[self setMarkedTextRange:nil notifyInputDelegate:NO]; | |
[selectionDisplayController_ updateSelectionDisplay]; | |
// Make caret visible if it is not | |
[self scrollTextRangeToVisible:self.selectedTextRange animated:YES]; | |
} | |
#pragma mark - | |
#pragma mark Replacing and Returning Text | |
// UITextInput method | |
- (NSString *)textInRange:(NKTTextRange *)textRange | |
{ | |
KBCLogDebug(@"%@", textRange); | |
NSString *string = [text_ string]; | |
if (textRange.start.location == 0 && textRange.end.location == [text_ length]) | |
{ | |
return string; | |
} | |
return [string substringWithRange:textRange.nsRange]; | |
} | |
// UITextInput method | |
- (void)replaceRange:(NKTTextRange *)textRange withText:(NSString *)replacementText | |
{ | |
[self replaceRange:textRange withText:replacementText notifyInputDelegate:NO]; | |
[selectionDisplayController_ updateSelectionDisplay]; | |
} | |
- (NSDictionary *)inheritedAttributesForTextRange:(NKTTextRange *)textRange | |
{ | |
NSDictionary *inheritedAttributes = nil; | |
if ([self hasText]) | |
{ | |
NSUInteger inheritedAttributesIndex = textRange.start.location; | |
// If the replacement range is empty, inserted characters inherit the attributes of the | |
// character preceding the range, if any. | |
if (textRange.empty && inheritedAttributesIndex > 0) | |
{ | |
inheritedAttributesIndex = inheritedAttributesIndex - 1; | |
} | |
else if (inheritedAttributesIndex > [text_ length]) | |
{ | |
inheritedAttributesIndex = [text_ length] - 1; | |
} | |
inheritedAttributes = [text_ attributesAtIndex:inheritedAttributesIndex effectiveRange:NULL]; | |
} | |
return inheritedAttributes; | |
} | |
- (NKTTextRange *)selectedTextRangeAfterReplacingRange:(NKTTextRange *)textRange withText:(NSString *)replacementText | |
{ | |
NKTTextRange* nextTextRange = nil; | |
// The text range to be replaced is to the left of the selected text range | |
if ([textRange.end compareIgnoringAffinity:selectedTextRange_.start] != NSOrderedDescending) | |
{ | |
NSInteger changeInLength = [replacementText length] - textRange.length; | |
NKTTextPosition *start = [selectedTextRange_.start textPositionByApplyingOffset:changeInLength]; | |
NKTTextPosition *end = [selectedTextRange_.end textPositionByApplyingOffset:changeInLength]; | |
nextTextRange = [NKTTextRange textRangeWithTextPosition:start textPosition:end]; | |
} | |
// The text range overlaps the selected text range | |
else if ([selectedTextRange_ containsTextPositionIgnoringAffinity:textRange.start]) | |
{ | |
NKTTextPosition *nextTextPosition = [textRange.start textPositionByApplyingOffset:[replacementText length]]; | |
nextTextRange = [nextTextPosition textRange]; | |
} | |
return nextTextRange; | |
} | |
// UITextInput method | |
- (void)replaceRange:(NKTTextRange *)textRange withText:(NSString *)replacementText notifyInputDelegate:(BOOL)notifyInputDelegate | |
{ | |
KBCLogDebug(@"%@ : %@", textRange, replacementText); | |
NKTTextRange* nextTextRange = [self selectedTextRangeAfterReplacingRange:textRange withText:replacementText]; | |
NSDictionary *inheritedAttributes = [self inheritedAttributesForTextRange:textRange]; | |
// Figure out the attributes that replaced text needs to have - this needs to be done before | |
// the text range changes | |
NSDictionary *inputTextAttributes = [[self.inputTextAttributes copy] autorelease]; | |
// We retain and autorelease nextTextRange and textRange in case they are the same as the | |
// selected text range! | |
[[nextTextRange retain] autorelease]; | |
[[textRange retain] autorelease]; | |
if (notifyInputDelegate) | |
{ | |
[inputDelegate_ textWillChange:self]; | |
} | |
[self notifyDelegateOfTextWillChangeFromTextPosition:textRange.start]; | |
// It is possible to avoid creating a new range of attributed text if the attributes that | |
// would be inherited from the string following insertion match the insertion attributes | |
if ([inheritedAttributes isEqualToDictionary:inputTextAttributes]) | |
{ | |
[text_ replaceCharactersInRange:textRange.nsRange withString:replacementText]; | |
} | |
else | |
{ | |
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:replacementText attributes:inputTextAttributes]; | |
[text_ replaceCharactersInRange:textRange.nsRange withAttributedString:attributedString]; | |
[attributedString release]; | |
} | |
[self updateTextFrameForChangeFromTextPosition:textRange.start]; | |
if (notifyInputDelegate) | |
{ | |
[inputDelegate_ textDidChange:self]; | |
} | |
[self notifyDelegateOfTextDidChangeFromTextPosition:textRange.start]; | |
[self setSelectedTextRange:nextTextRange notifyInputDelegate:notifyInputDelegate]; | |
} | |
// PENDING: merge with method above | |
- (void)replaceRange:(NKTTextRange *)textRange withAttributedString:(NSAttributedString *)attributedString notifyInputDelegate:(BOOL)notifyInputDelegate | |
{ | |
KBCLogDebug(@"%@ : %@", textRange, replacementText); | |
NKTTextRange* nextTextRange = [self selectedTextRangeAfterReplacingRange:textRange withText:[attributedString string]]; | |
// We retain and autorelease nextTextRange and textRange in case they are the same as the | |
// selected text range! | |
[[nextTextRange retain] autorelease]; | |
[[textRange retain] autorelease]; | |
if (notifyInputDelegate) | |
{ | |
[inputDelegate_ textWillChange:self]; | |
} | |
[self notifyDelegateOfTextWillChangeFromTextPosition:textRange.start]; | |
[text_ replaceCharactersInRange:textRange.nsRange withAttributedString:attributedString]; | |
[self updateTextFrameForChangeFromTextPosition:textRange.start]; | |
if (notifyInputDelegate) | |
{ | |
[inputDelegate_ textDidChange:self]; | |
} | |
[self notifyDelegateOfTextDidChangeFromTextPosition:textRange.start]; | |
[self setSelectedTextRange:nextTextRange notifyInputDelegate:notifyInputDelegate]; | |
} | |
#pragma mark - | |
#pragma mark Managing Text Ranges | |
- (NKTTextRange *)gestureTextRange | |
{ | |
return gestureTextRange_; | |
} | |
- (void)setGestureTextRange:(NKTTextRange *)textRange | |
{ | |
if (![gestureTextRange_ isEqualToTextRange:textRange]) | |
{ | |
[gestureTextRange_ release]; | |
gestureTextRange_ = [textRange copy]; | |
} | |
} | |
- (void)confirmGestureTextRange | |
{ | |
[self setSelectedTextRange:gestureTextRange_ notifyInputDelegate:YES]; | |
self.gestureTextRange = nil; | |
[selectionDisplayController_ updateSelectionDisplay]; | |
} | |
// UITextInput method | |
- (UITextRange *)selectedTextRange | |
{ | |
return selectedTextRange_; | |
} | |
// UITextInput method | |
- (void)setSelectedTextRange:(NKTTextRange *)textRange | |
{ | |
// PENDING/HACK/NOTE: | |
// Always notify the input delegate when this method is called. The UITextInput system seem | |
// to expect this method to notify the system when the selected text range is set. For | |
// example, the UITextInput autocorrection prompts do not dismiss when the keyboard navigates | |
// away from the text position (through this method) unless we notify the input delegate that | |
// the selected text range has changed. | |
[self setSelectedTextRange:textRange notifyInputDelegate:YES]; | |
[selectionDisplayController_ updateSelectionDisplay]; | |
[self scrollTextRangeToVisible:self.selectedTextRange animated:YES]; | |
} | |
- (void)updateSelectionDisplay | |
{ | |
[selectionDisplayController_ updateSelectionDisplay]; | |
} | |
- (void)setSelectedTextRange:(NKTTextRange *)textRange notifyInputDelegate:(BOOL)notifyInputDelegate | |
{ | |
if (selectedTextRange_ == textRange || [selectedTextRange_ isEqualToTextRange:textRange]) | |
{ | |
return; | |
} | |
if (notifyInputDelegate) | |
{ | |
KBCLogDebug(@"calling input delegate -selectionWillChange:"); | |
[inputDelegate_ selectionWillChange:self]; | |
} | |
[selectedTextRange_ release]; | |
selectedTextRange_ = [textRange copy]; | |
// The input text attributes are cleared when the selected text range changes | |
self.inputTextAttributes = nil; | |
if (notifyInputDelegate) | |
{ | |
KBCLogDebug(@"calling input delegate -selectionDidChange:"); | |
[inputDelegate_ selectionDidChange:self]; | |
} | |
if ([self.delegate respondsToSelector:@selector(textViewDidChangeSelection:)]) | |
{ | |
[(id <NKTTextViewDelegate>)self.delegate textViewDidChangeSelection:self]; | |
} | |
} | |
// UITextInput method | |
- (UITextRange *)markedTextRange | |
{ | |
return markedTextRange_; | |
} | |
- (void)setMarkedTextRange:(NKTTextRange *)textRange notifyInputDelegate:(BOOL)notifyInputDelegate | |
{ | |
if (markedTextRange_ == textRange || [markedTextRange_ isEqualToTextRange:textRange]) | |
{ | |
return; | |
} | |
if (notifyInputDelegate) | |
{ | |
KBCLogDebug(@"calling input delegate -selectionWillChange:"); | |
[inputDelegate_ selectionWillChange:self]; | |
} | |
[markedTextRange_ release]; | |
markedTextRange_ = [textRange copy]; | |
if (notifyInputDelegate) | |
{ | |
KBCLogDebug(@"calling input delegate -selectionDidChange:"); | |
[inputDelegate_ selectionDidChange:self]; | |
} | |
} | |
// UITextInput method | |
- (void)setMarkedText:(NSString *)markedText selectedRange:(NSRange)relativeSelectedRange | |
{ | |
KBCLogDebug(@"%@ : %@", markedText, NSStringFromRange(relativeSelectedRange)); | |
// PENDING: accessor for marked text property | |
[markedText_ autorelease]; | |
markedText_ = [markedText copy]; | |
if (markedText_ == nil) | |
{ | |
markedText_ = @""; | |
[markedText_ retain]; | |
} | |
// Figure out the attributes that inserted text needs to have | |
NSDictionary *inputTextAttributes = [[self.inputTextAttributes copy] autorelease]; | |
NKTTextRange *replacementTextRange = (markedTextRange_ != nil) ? markedTextRange_ : selectedTextRange_; | |
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:markedText_ attributes:inputTextAttributes]; | |
[self notifyDelegateOfTextWillChangeFromTextPosition:replacementTextRange.start]; | |
[text_ replaceCharactersInRange:replacementTextRange.nsRange withAttributedString:attributedString]; | |
[attributedString release]; | |
[self updateTextFrameForChangeFromTextPosition:replacementTextRange.start]; | |
[self notifyDelegateOfTextDidChangeFromTextPosition:replacementTextRange.start]; | |
NKTTextRange *newMarkedTextRange = nil; | |
NKTTextRange *newSelectedTextRange = nil; | |
// Update the marked and selected text ranges | |
if (relativeSelectedRange.location != NSNotFound) | |
{ | |
NKTTextPosition *newMarkedEnd = [NKTTextPosition textPositionWithLocation:replacementTextRange.start.location + [markedText_ length] affinity:UITextStorageDirectionForward]; | |
newMarkedTextRange = [NKTTextRange textRangeWithTextPosition:replacementTextRange.start textPosition:newMarkedEnd]; | |
NKTTextPosition *newSelectedStart = [NKTTextPosition textPositionWithLocation:newMarkedTextRange.start.location + relativeSelectedRange.location affinity:UITextStorageDirectionForward]; | |
NKTTextPosition *newSelectedEnd = [NKTTextPosition textPositionWithLocation:newSelectedStart.location + relativeSelectedRange.length affinity:UITextStorageDirectionForward]; | |
newSelectedTextRange = [NKTTextRange textRangeWithTextPosition:newSelectedStart textPosition:newSelectedEnd]; | |
} | |
else | |
{ | |
NKTTextPosition *newSelectedStart = [NKTTextPosition textPositionWithLocation:replacementTextRange.start.location affinity:UITextStorageDirectionForward]; | |
NKTTextPosition *newSelectedEnd = [NKTTextPosition textPositionWithLocation:newSelectedStart.location + [markedText_ length] affinity:UITextStorageDirectionForward]; | |
newSelectedTextRange = [NKTTextRange textRangeWithTextPosition:newSelectedStart textPosition:newSelectedEnd]; | |
} | |
// Since the selected text range is always within the marked text range, update the selected text range first | |
[self setSelectedTextRange:newSelectedTextRange notifyInputDelegate:NO]; | |
[self setMarkedTextRange:newMarkedTextRange notifyInputDelegate:NO]; | |
// Input text attributes are reset when marked text is set | |
self.inputTextAttributes = nil; | |
[selectionDisplayController_ updateSelectionDisplay]; | |
[self scrollTextRangeToVisible:self.selectedTextRange animated:YES]; | |
} | |
// UITextInput method | |
- (void)unmarkText | |
{ | |
[self setMarkedTextRange:nil notifyInputDelegate:NO]; | |
[markedText_ release]; | |
markedText_ = nil; | |
[selectionDisplayController_ updateSelectionDisplay]; | |
} | |
// UITextInput method | |
- (UITextStorageDirection)selectionAffinity | |
{ | |
// PENDING: this is a bogus computation right now, replace with something that makes sense | |
return (selectedTextRange_ != nil) ? selectedTextRange_.start.affinity : UITextStorageDirectionForward; | |
} | |
// UITextInput method | |
- (void)setSelectionAffinity:(UITextStorageDirection)direction | |
{ | |
KBCLogWarning(@"unexpected method call"); | |
} | |
#pragma mark - | |
#pragma mark Computing Text Ranges and Text Positions | |
// UITextInput method | |
- (UITextRange *)textRangeFromPosition:(NKTTextPosition *)fromPosition toPosition:(NKTTextPosition *)toPosition | |
{ | |
KBCLogDebug(@"%@ : %@", fromPosition, toPosition); | |
return [NKTTextRange textRangeWithTextPosition:fromPosition textPosition:toPosition]; | |
} | |
// UITextInput method | |
- (UITextPosition *)positionFromPosition:(NKTTextPosition *)textPosition offset:(NSInteger)offset | |
{ | |
KBCLogDebug(@"%@ : %d", textPosition, offset); | |
NSInteger location = (NSInteger)textPosition.location + offset; | |
if (location < 0) | |
{ | |
return [NKTTextPosition textPositionWithLocation:0 affinity:UITextStorageDirectionForward]; | |
} | |
else if (location > [text_ length]) | |
{ | |
return [NKTTextPosition textPositionWithLocation:[text_ length] affinity:UITextStorageDirectionForward]; | |
} | |
else | |
{ | |
return [NKTTextPosition textPositionWithLocation:(NSUInteger)location affinity:UITextStorageDirectionForward]; | |
} | |
} | |
// UITextInput method | |
- (UITextPosition *)positionFromPosition:(NKTTextPosition *)textPosition | |
inDirection:(UITextLayoutDirection)direction | |
offset:(NSInteger)offset | |
{ | |
KBCLogDebug(@"%@ : %@ : %d", textPosition, KBTStringFromUITextDirection(direction), offset); | |
switch (direction) | |
{ | |
case UITextLayoutDirectionRight: | |
{ | |
return [self positionFromPosition:textPosition offset:offset]; | |
} | |
case UITextLayoutDirectionLeft: | |
{ | |
return [self positionFromPosition:textPosition offset:-offset]; | |
} | |
case UITextLayoutDirectionUp: | |
{ | |
NKTLine *initialLine = [self.framesetter lineForCaretAtTextPosition:textPosition]; | |
if (offset > initialLine.index) | |
{ | |
return [self beginningOfDocument]; | |
} | |
NSUInteger targetLineIndex = initialLine.index - offset; | |
NKTLine *targetLine = [self.framesetter lineAtIndex:targetLineIndex]; | |
CGPoint framesetterPoint = [self.framesetter baselineOriginForCharAtTextPosition:textPosition]; | |
return [targetLine closestTextPositionForCaretToPoint:framesetterPoint]; | |
} | |
case UITextLayoutDirectionDown: | |
{ | |
NKTLine *initialLine = [self.framesetter lineForCaretAtTextPosition:textPosition]; | |
NSUInteger targetLineIndex = initialLine.index + offset; | |
if (targetLineIndex >= self.framesetter.numberOfLines) | |
{ | |
return [self endOfDocument]; | |
} | |
NKTLine *targetLine = [self.framesetter lineAtIndex:targetLineIndex]; | |
CGPoint framesetterPoint = [self.framesetter baselineOriginForCharAtTextPosition:textPosition]; | |
return [targetLine closestTextPositionForCaretToPoint:framesetterPoint]; | |
} | |
} | |
KBCLogWarning(@"unknown direction, returning nil"); | |
return nil; | |
} | |
// UITextInput method | |
- (UITextPosition *)beginningOfDocument | |
{ | |
return [NKTTextPosition textPositionWithLocation:0 affinity:UITextStorageDirectionForward]; | |
} | |
// UITextInput method | |
- (UITextPosition *)endOfDocument | |
{ | |
return [NKTTextPosition textPositionWithLocation:[text_ length] affinity:UITextStorageDirectionForward]; | |
} | |
#pragma mark - | |
#pragma mark Evaluating Text Positions | |
// UITextInput method | |
- (NSComparisonResult)comparePosition:(NKTTextPosition *)firstTextPosition | |
toPosition:(NKTTextPosition *)secondTextPosition | |
{ | |
KBCLogDebug(@"%@ : %@", firstTextPosition, secondTextPosition); | |
return [firstTextPosition compareIgnoringAffinity:secondTextPosition]; | |
} | |
// UITextInput method | |
- (NSInteger)offsetFromPosition:(NKTTextPosition *)fromPosition toPosition:(NKTTextPosition *)toPosition | |
{ | |
KBCLogDebug(@"%@ : %@", fromPosition, toPosition); | |
return (NSInteger)toPosition.location - (NSInteger)fromPosition.location; | |
} | |
#pragma mark - | |
#pragma mark Determining Layout and Writing Direction | |
// UITextInput method | |
- (UITextPosition *)positionWithinRange:(NKTTextRange *)textRange farthestInDirection:(UITextLayoutDirection)direction | |
{ | |
KBCLogDebug(@"%@ : %@", textRange, KBTStringFromUITextDirection(direction)); | |
switch (direction) | |
{ | |
case UITextLayoutDirectionRight: | |
{ | |
return textRange.end; | |
} | |
case UITextLayoutDirectionLeft: | |
{ | |
return textRange.start; | |
} | |
case UITextLayoutDirectionUp: | |
{ | |
return textRange.start; | |
} | |
case UITextLayoutDirectionDown: | |
{ | |
return textRange.end; | |
} | |
} | |
KBCLogWarning(@"unknown direction, returning nil"); | |
return nil; | |
} | |
// UITextInput method | |
- (UITextRange *)characterRangeByExtendingPosition:(NKTTextPosition *)textPosition | |
inDirection:(UITextLayoutDirection)direction | |
{ | |
KBCLogDebug(@"%@ : %@", textPosition, KBTStringFromUITextDirection(direction)); | |
switch (direction) | |
{ | |
case UITextLayoutDirectionRight: | |
{ | |
NKTLine *line = [self.framesetter lineForCaretAtTextPosition:textPosition]; | |
return [NKTTextRange textRangeWithTextPosition:textPosition textPosition:line.textRange.end]; | |
} | |
case UITextLayoutDirectionLeft: | |
{ | |
NKTLine *line = [self.framesetter lineForCaretAtTextPosition:textPosition]; | |
return [NKTTextRange textRangeWithTextPosition:line.textRange.start textPosition:textPosition]; | |
} | |
case UITextLayoutDirectionUp: | |
{ | |
return [NKTTextRange textRangeWithTextPosition:(NKTTextPosition *)[self beginningOfDocument] | |
textPosition:textPosition]; | |
} | |
case UITextLayoutDirectionDown: | |
{ | |
return [NKTTextRange textRangeWithTextPosition:textPosition | |
textPosition:(NKTTextPosition *)[self endOfDocument]]; | |
} | |
} | |
KBCLogWarning(@"unknown direction, returning nil"); | |
return nil; | |
} | |
// UITextInput method | |
- (UITextWritingDirection)baseWritingDirectionForPosition:(NKTTextPosition *)textPosition | |
inDirection:(UITextStorageDirection)direction | |
{ | |
KBCLogDebug(@"%@ : %@", textPosition, KBTStringFromUITextDirection(direction)); | |
return UITextWritingDirectionNatural; | |
} | |
// UITextInput method | |
- (void)setBaseWritingDirection:(UITextWritingDirection)direction forRange:(NKTTextRange *)textRange | |
{ | |
KBCLogDebug(@"%@ : %@", KBTStringFromUITextDirection(direction), textRange); | |
} | |
#pragma mark - | |
#pragma mark Geometry and Hit-Testing | |
// UITextInput method | |
- (CGRect)firstRectForRange:(NKTTextRange *)textRange | |
{ | |
KBCLogDebug(@"%@", textRange); | |
return [self firstRectForTextRange:textRange]; | |
} | |
- (CGRect)firstRectForTextRange:(NKTTextRange *)textRange | |
{ | |
if (textRange.empty) | |
{ | |
return [self caretRectForTextPosition:textRange.start applyInputTextAttributes:NO]; | |
} | |
CGRect rect = [self.framesetter firstRectForTextRange:(NKTTextRange *)textRange]; | |
CGAffineTransform transform = [self framesetterToViewTransform]; | |
return CGRectApplyAffineTransform(rect, transform); | |
} | |
- (CGRect)lastRectForTextRange:(NKTTextRange *)textRange | |
{ | |
if (textRange.empty) | |
{ | |
return [self caretRectForTextPosition:textRange.start applyInputTextAttributes:NO]; | |
} | |
CGRect rect = [self.framesetter lastRectForTextRange:(NKTTextRange *)textRange]; | |
CGAffineTransform transform = [self framesetterToViewTransform]; | |
return CGRectApplyAffineTransform(rect, transform); | |
} | |
- (NSArray *)rectsForTextRange:(NKTTextRange *)textRange | |
{ | |
return [self.framesetter rectsForTextRange:textRange transform:[self framesetterToViewTransform]]; | |
} | |
// UITextInput method | |
- (CGRect)caretRectForPosition:(NKTTextPosition *)textPosition | |
{ | |
return [self caretRectForTextPosition:textPosition applyInputTextAttributes:NO]; | |
} | |
- (CGRect)caretRectForTextPosition:(NKTTextPosition *)textPosition applyInputTextAttributes:(BOOL)applyInputTextAttributes | |
{ | |
CGPoint framesetterPoint = [self.framesetter baselineOriginForCharAtTextPosition:textPosition]; | |
CGPoint charOrigin = [self convertPointFromFramesetter:framesetterPoint]; | |
UIFont *font = nil; | |
if (applyInputTextAttributes) | |
{ | |
KBTStyleDescriptor *styleDescriptor = [KBTStyleDescriptor styleDescriptorWithCoreTextAttributes:self.inputTextAttributes]; | |
font = [styleDescriptor uiFontForFont]; | |
} | |
else | |
{ | |
font = [self fontAtTextPosition:textPosition]; | |
} | |
return [self caretRectWithOrigin:charOrigin font:font]; | |
} | |
static const CGFloat CaretWidth = 2.0; | |
static const CGFloat CaretTopPadding = 1.0; | |
static const CGFloat CaretBottomPadding = 1.0; | |
// PENDING: move to selection display controller? | |
- (CGRect)caretRectWithOrigin:(CGPoint)origin font:(UIFont *)font | |
{ | |
CGRect caretFrame = CGRectZero; | |
caretFrame.origin.x = origin.x; | |
caretFrame.origin.y = origin.y - font.ascender - CaretTopPadding; | |
caretFrame.size.width = CaretWidth; | |
caretFrame.size.height = font.ascender - font.descender + CaretTopPadding + CaretBottomPadding; | |
return caretFrame; | |
} | |
// UITextInput method | |
- (UITextPosition *)closestPositionToPoint:(CGPoint)point | |
{ | |
KBCLogDebug(@"%@", NSStringFromCGPoint(point)); | |
// PENDING: figure out when this is called by UITextInput, and implement accordingly | |
return [self.framesetter closestTextPositionForCaretToPoint:point]; | |
} | |
// UITextInput method | |
- (UITextPosition *)closestPositionToPoint:(CGPoint)point withinRange:(NKTTextRange *)textRange | |
{ | |
KBCLogDebug(@"%@ : %@", NSStringFromCGPoint(point), textRange); | |
// PENDING: figure out when this is called by UITextInput, and implement accordingly | |
return nil; | |
} | |
// UITextInput method | |
- (UITextRange *)characterRangeAtPoint:(CGPoint)point | |
{ | |
KBCLogDebug(@"%@", NSStringFromCGPoint(point)); | |
// PENDING: figure out when this is called by UITextInput, and implement accordingly | |
NKTTextPosition *textPosition = (NKTTextPosition *)[self closestPositionToPoint:point]; | |
return [textPosition textRange]; | |
} | |
#pragma mark - | |
#pragma mark Text Input Delegate and Text Input Tokenizer | |
- (id <UITextInputTokenizer>)tokenizer | |
{ | |
if (tokenizer_ == nil) | |
{ | |
tokenizer_ = [[NKTTextViewTokenizer alloc] initWithTextView:self]; | |
} | |
return tokenizer_; | |
} | |
#pragma mark - | |
#pragma mark Returning Text Styling Information | |
// UITextInput method | |
- (NSDictionary *)textStylingAtPosition:(NKTTextPosition *)textPosition inDirection:(UITextStorageDirection)direction | |
{ | |
// NOTE: it seems like UITextInput does not actually use the color and background color | |
UIFont *font = [self fontAtTextPosition:textPosition]; | |
UIColor *color = [UIColor blackColor]; | |
UIColor *backgroundColor = self.backgroundColor; | |
return [NSDictionary dictionaryWithObjectsAndKeys:font, UITextInputTextFontKey, | |
color, UITextInputTextColorKey, | |
backgroundColor, UITextInputTextBackgroundColorKey, | |
nil]; | |
} | |
#pragma mark - | |
#pragma mark Getting Fonts at Text Positions | |
- (UIFont *)fontAtTextPosition:(NKTTextPosition *)textPosition | |
{ | |
if (![self hasText]) | |
{ | |
return [UIFont systemFontOfSize:[UIFont systemFontSize]]; | |
} | |
// Read the style information at the character preceding the index because that is the style that | |
// would be used when text is inserted at that position | |
UIFont *font = nil; | |
NSUInteger sourceIndex = textPosition.location; | |
if (sourceIndex > [text_ length]) | |
{ | |
sourceIndex = [text_ length] - 1; | |
} | |
else if (sourceIndex > 0) | |
{ | |
--sourceIndex; | |
} | |
NSDictionary *attributes = [text_ attributesAtIndex:sourceIndex effectiveRange:NULL]; | |
CTFontRef ctFont = (CTFontRef)[attributes objectForKey:(id)kCTFontAttributeName]; | |
if (ctFont != NULL) | |
{ | |
font = KBTUIFontForCTFont(ctFont); | |
if (font == nil) | |
{ | |
KBCLogWarning(@"could not create UIFont for CTFont"); | |
} | |
} | |
if (font == nil) | |
{ | |
font = [UIFont systemFontOfSize:[UIFont systemFontSize]]; | |
} | |
return font; | |
} | |
#pragma mark - | |
#pragma mark Returning the Text Input View | |
- (UIView *)textInputView | |
{ | |
return self; | |
} | |
#pragma mark - | |
#pragma mark Managing Selection Views | |
- (void)addUnderlayView:(UIView *)view | |
{ | |
[self insertSubview:view atIndex:[underlayViews_ count]]; | |
[underlayViews_ addObject:view]; | |
} | |
- (void)addOverlayView:(UIView *)view | |
{ | |
[self insertSubview:view atIndex:[underlayViews_ count] + [visibleSections_ count]]; | |
[overlayViews addObject:view]; | |
} | |
#pragma mark - | |
#pragma mark Tokenizing | |
- (NKTTextRange *)textRangeForLineContainingTextPosition:(NKTTextPosition *)textPosition | |
{ | |
NKTLine *line = [self.framesetter lineForCaretAtTextPosition:textPosition]; | |
return line.textRange; | |
} | |
#pragma mark - | |
#pragma mark Editing | |
- (BOOL)isEditing | |
{ | |
return [self isFirstResponder]; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Yeah, pretty ugly.