From 7b02207537d35bfa1714bf8beafc921f717d100a Mon Sep 17 00:00:00 2001 From: 单军华 Date: Wed, 11 Jul 2018 10:47:42 +0800 Subject: [PATCH] 首次上传 --- screendisplay/Pods/YYText/YYText/YYTextView.m | 3830 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 3,830 insertions(+), 0 deletions(-) diff --git a/screendisplay/Pods/YYText/YYText/YYTextView.m b/screendisplay/Pods/YYText/YYText/YYTextView.m new file mode 100755 index 0000000..01b1629 --- /dev/null +++ b/screendisplay/Pods/YYText/YYText/YYTextView.m @@ -0,0 +1,3830 @@ +// +// YYTextView.m +// YYText <https://github.com/ibireme/YYText> +// +// Created by ibireme on 15/2/25. +// Copyright (c) 2015 ibireme. +// +// This source code is licensed under the MIT-style license found in the +// LICENSE file in the root directory of this source tree. +// + +#import "YYTextView.h" +#import "YYTextInput.h" +#import "YYTextContainerView.h" +#import "YYTextSelectionView.h" +#import "YYTextMagnifier.h" +#import "YYTextEffectWindow.h" +#import "YYTextKeyboardManager.h" +#import "YYTextUtilities.h" +#import "YYTextTransaction.h" +#import "YYTextWeakProxy.h" +#import "NSAttributedString+YYText.h" +#import "UIPasteboard+YYText.h" +#import "UIView+YYText.h" + + +static double _YYDeviceSystemVersion() { + static double version; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + version = [UIDevice currentDevice].systemVersion.doubleValue; + }); + return version; +} + +#ifndef kSystemVersion +#define kSystemVersion _YYDeviceSystemVersion() +#endif + +#ifndef kiOS6Later +#define kiOS6Later (kSystemVersion >= 6) +#endif + +#ifndef kiOS7Later +#define kiOS7Later (kSystemVersion >= 7) +#endif + +#ifndef kiOS8Later +#define kiOS8Later (kSystemVersion >= 8) +#endif + +#ifndef kiOS9Later +#define kiOS9Later (kSystemVersion >= 9) +#endif + + + +#define kDefaultUndoLevelMax 20 // Default maximum undo level + +#define kAutoScrollMinimumDuration 0.1 // Time in seconds to tick auto-scroll. +#define kLongPressMinimumDuration 0.5 // Time in seconds the fingers must be held down for long press gesture. +#define kLongPressAllowableMovement 10.0 // Maximum movement in points allowed before the long press fails. + +#define kMagnifierRangedTrackFix -6.0 // Magnifier ranged offset fix. +#define kMagnifierRangedPopoverOffset 4.0 // Magnifier ranged popover offset. +#define kMagnifierRangedCaptureOffset -6.0 // Magnifier ranged capture center offset. + +#define kHighlightFadeDuration 0.15 // Time in seconds for highlight fadeout animation. + +#define kDefaultInset UIEdgeInsetsMake(6, 4, 6, 4) +#define kDefaultVerticalInset UIEdgeInsetsMake(4, 6, 4, 6) + + +NSString *const YYTextViewTextDidBeginEditingNotification = @"YYTextViewTextDidBeginEditing"; +NSString *const YYTextViewTextDidChangeNotification = @"YYTextViewTextDidChange"; +NSString *const YYTextViewTextDidEndEditingNotification = @"YYTextViewTextDidEndEditing"; + + +typedef NS_ENUM (NSUInteger, YYTextGrabberDirection) { + kStart = 1, + kEnd = 2, +}; + +typedef NS_ENUM(NSUInteger, YYTextMoveDirection) { + kLeft = 1, + kTop = 2, + kRight = 3, + kBottom = 4, +}; + + +/// An object that captures the state of the text view. Used for undo and redo. +@interface _YYTextViewUndoObject : NSObject +@property (nonatomic, strong) NSAttributedString *text; +@property (nonatomic, assign) NSRange selectedRange; +@end +@implementation _YYTextViewUndoObject ++ (instancetype)objectWithText:(NSAttributedString *)text range:(NSRange)range { + _YYTextViewUndoObject *obj = [self new]; + obj.text = text ? text : [NSAttributedString new]; + obj.selectedRange = range; + return obj; +} +@end + + +@interface YYTextView () <UIScrollViewDelegate, UIAlertViewDelegate, YYTextDebugTarget, YYTextKeyboardObserver> { + + YYTextRange *_selectedTextRange; /// nonnull + YYTextRange *_markedTextRange; + + __weak id<YYTextViewDelegate> _outerDelegate; + + UIImageView *_placeHolderView; + + NSMutableAttributedString *_innerText; ///< nonnull, inner attributed text + NSMutableAttributedString *_delectedText; ///< detected text for display + YYTextContainer *_innerContainer; ///< nonnull, inner text container + YYTextLayout *_innerLayout; ///< inner text layout, the text in this layout is longer than `_innerText` by appending '\n' + + YYTextContainerView *_containerView; ///< nonnull + YYTextSelectionView *_selectionView; ///< nonnull + YYTextMagnifier *_magnifierCaret; ///< nonnull + YYTextMagnifier *_magnifierRanged; ///< nonnull + + NSMutableAttributedString *_typingAttributesHolder; ///< nonnull, typing attributes + NSDataDetector *_dataDetector; + CGFloat _magnifierRangedOffset; + + NSRange _highlightRange; ///< current highlight range + YYTextHighlight *_highlight; ///< highlight attribute in `_highlightRange` + YYTextLayout *_highlightLayout; ///< when _state.showingHighlight=YES, this layout should be displayed + YYTextRange *_trackingRange; ///< the range in _innerLayout, may out of _innerText. + + BOOL _insetModifiedByKeyboard; ///< text is covered by keyboard, and the contentInset is modified + UIEdgeInsets _originalContentInset; ///< the original contentInset before modified + UIEdgeInsets _originalScrollIndicatorInsets; ///< the original scrollIndicatorInsets before modified + + NSTimer *_longPressTimer; + NSTimer *_autoScrollTimer; + CGFloat _autoScrollOffset; ///< current auto scroll offset which shoud add to scroll view + NSInteger _autoScrollAcceleration; ///< an acceleration coefficient for auto scroll + NSTimer *_selectionDotFixTimer; ///< fix the selection dot in window if the view is moved by parents + CGPoint _previousOriginInWindow; + + CGPoint _touchBeganPoint; + CGPoint _trackingPoint; + NSTimeInterval _touchBeganTime; + NSTimeInterval _trackingTime; + + NSMutableArray *_undoStack; + NSMutableArray *_redoStack; + NSRange _lastTypeRange; + + struct { + unsigned int trackingGrabber : 2; ///< YYTextGrabberDirection, current tracking grabber + unsigned int trackingCaret : 1; ///< track the caret + unsigned int trackingPreSelect : 1; ///< track pre-select + unsigned int trackingTouch : 1; ///< is in touch phase + unsigned int swallowTouch : 1; ///< don't forward event to next responder + unsigned int touchMoved : 3; ///< YYTextMoveDirection, move direction after touch began + unsigned int selectedWithoutEdit : 1; ///< show selected range but not first responder + unsigned int deleteConfirm : 1; ///< delete a binding text range + unsigned int ignoreFirstResponder : 1; ///< ignore become first responder temporary + unsigned int ignoreTouchBegan : 1; ///< ignore begin tracking touch temporary + + unsigned int showingMagnifierCaret : 1; + unsigned int showingMagnifierRanged : 1; + unsigned int showingMenu : 1; + unsigned int showingHighlight : 1; + + unsigned int typingAttributesOnce : 1; ///< apply the typing attributes once + unsigned int clearsOnInsertionOnce : 1; ///< select all once when become first responder + unsigned int autoScrollTicked : 1; ///< auto scroll did tick scroll at this timer period + unsigned int firstShowDot : 1; ///< the selection grabber dot has displayed at least once + unsigned int needUpdate : 1; ///< the layout or selection view is 'dirty' and need update + unsigned int placeholderNeedUpdate : 1; ///< the placeholder need update it's contents + + unsigned int insideUndoBlock : 1; + unsigned int firstResponderBeforeUndoAlert : 1; + } _state; +} + +@end + + +@implementation YYTextView + +#pragma mark - @protocol UITextInputTraits +@synthesize autocapitalizationType = _autocapitalizationType; +@synthesize autocorrectionType = _autocorrectionType; +@synthesize spellCheckingType = _spellCheckingType; +@synthesize keyboardType = _keyboardType; +@synthesize keyboardAppearance = _keyboardAppearance; +@synthesize returnKeyType = _returnKeyType; +@synthesize enablesReturnKeyAutomatically = _enablesReturnKeyAutomatically; +@synthesize secureTextEntry = _secureTextEntry; + +#pragma mark - @protocol UITextInput +@synthesize selectedTextRange = _selectedTextRange; //copy nonnull (YYTextRange*) +@synthesize markedTextRange = _markedTextRange; //readonly (YYTextRange*) +@synthesize markedTextStyle = _markedTextStyle; //copy +@synthesize inputDelegate = _inputDelegate; //assign +@synthesize tokenizer = _tokenizer; //readonly + +#pragma mark - @protocol UITextInput optional +@synthesize selectionAffinity = _selectionAffinity; + + +#pragma mark - Private + +/// Update layout and selection before runloop sleep/end. +- (void)_commitUpdate { +#if !TARGET_INTERFACE_BUILDER + _state.needUpdate = YES; + [[YYTextTransaction transactionWithTarget:self selector:@selector(_updateIfNeeded)] commit]; +#else + [self _update]; +#endif +} + +/// Update layout and selection view if needed. +- (void)_updateIfNeeded { + if (_state.needUpdate) { + [self _update]; + } +} + +/// Update layout and selection view immediately. +- (void)_update { + _state.needUpdate = NO; + [self _updateLayout]; + [self _updateSelectionView]; +} + +/// Update layout immediately. +- (void)_updateLayout { + NSMutableAttributedString *text = _innerText.mutableCopy; + _placeHolderView.hidden = text.length > 0; + if ([self _detectText:text]) { + _delectedText = text; + } else { + _delectedText = nil; + } + [text replaceCharactersInRange:NSMakeRange(text.length, 0) withString:@"\r"]; // add for nextline caret + [text yy_removeDiscontinuousAttributesInRange:NSMakeRange(_innerText.length, 1)]; + [text removeAttribute:YYTextBorderAttributeName range:NSMakeRange(_innerText.length, 1)]; + [text removeAttribute:YYTextBackgroundBorderAttributeName range:NSMakeRange(_innerText.length, 1)]; + if (_innerText.length == 0) { + [text yy_setAttributes:_typingAttributesHolder.yy_attributes]; // add for empty text caret + } + if (_selectedTextRange.end.offset == _innerText.length) { + [_typingAttributesHolder.yy_attributes enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { + [text yy_setAttribute:key value:value range:NSMakeRange(_innerText.length, 1)]; + }]; + } + [self willChangeValueForKey:@"textLayout"]; + _innerLayout = [YYTextLayout layoutWithContainer:_innerContainer text:text]; + [self didChangeValueForKey:@"textLayout"]; + CGSize size = [_innerLayout textBoundingSize]; + CGSize visibleSize = [self _getVisibleSize]; + if (_innerContainer.isVerticalForm) { + size.height = visibleSize.height; + if (size.width < visibleSize.width) size.width = visibleSize.width; + } else { + size.width = visibleSize.width; + } + + [_containerView setLayout:_innerLayout withFadeDuration:0]; + _containerView.frame = (CGRect){.size = size}; + _state.showingHighlight = NO; + self.contentSize = size; +} + +/// Update selection view immediately. +/// This method should be called after "layout update" finished. +- (void)_updateSelectionView { + _selectionView.frame = _containerView.frame; + _selectionView.caretBlinks = NO; + _selectionView.caretVisible = NO; + _selectionView.selectionRects = nil; + [[YYTextEffectWindow sharedWindow] hideSelectionDot:_selectionView]; + if (!_innerLayout) return; + + NSMutableArray *allRects = [NSMutableArray new]; + BOOL containsDot = NO; + + YYTextRange *selectedRange = _selectedTextRange; + if (_state.trackingTouch && _trackingRange) { + selectedRange = _trackingRange; + } + + if (_markedTextRange) { + NSArray *rects = [_innerLayout selectionRectsWithoutStartAndEndForRange:_markedTextRange]; + if (rects) [allRects addObjectsFromArray:rects]; + if (selectedRange.asRange.length > 0) { + rects = [_innerLayout selectionRectsWithOnlyStartAndEndForRange:selectedRange]; + if (rects) [allRects addObjectsFromArray:rects]; + containsDot = rects.count > 0; + } else { + CGRect rect = [_innerLayout caretRectForPosition:selectedRange.end]; + _selectionView.caretRect = [self _convertRectFromLayout:rect]; + _selectionView.caretVisible = YES; + _selectionView.caretBlinks = YES; + } + } else { + if (selectedRange.asRange.length == 0) { // only caret + if (self.isFirstResponder || _state.trackingPreSelect) { + CGRect rect = [_innerLayout caretRectForPosition:selectedRange.end]; + _selectionView.caretRect = [self _convertRectFromLayout:rect]; + _selectionView.caretVisible = YES; + if (!_state.trackingCaret && !_state.trackingPreSelect) { + _selectionView.caretBlinks = YES; + } + } + } else { // range selected + if ((self.isFirstResponder && !_state.deleteConfirm) || + (!self.isFirstResponder && _state.selectedWithoutEdit)) { + NSArray *rects = [_innerLayout selectionRectsForRange:selectedRange]; + if (rects) [allRects addObjectsFromArray:rects]; + containsDot = rects.count > 0; + } else if ((!self.isFirstResponder && _state.trackingPreSelect) || + (self.isFirstResponder && _state.deleteConfirm)){ + NSArray *rects = [_innerLayout selectionRectsWithoutStartAndEndForRange:selectedRange]; + if (rects) [allRects addObjectsFromArray:rects]; + } + } + } + [allRects enumerateObjectsUsingBlock:^(YYTextSelectionRect *rect, NSUInteger idx, BOOL *stop) { + rect.rect = [self _convertRectFromLayout:rect.rect]; + }]; + _selectionView.selectionRects = allRects; + if (!_state.firstShowDot && containsDot) { + _state.firstShowDot = YES; + /* + The dot position may be wrong at the first time displayed. + I can't find the reason. Here's a workaround. + */ + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.02 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [[YYTextEffectWindow sharedWindow] showSelectionDot:_selectionView]; + }); + } + [[YYTextEffectWindow sharedWindow] showSelectionDot:_selectionView]; + + if (containsDot) { + [self _startSelectionDotFixTimer]; + } else { + [self _endSelectionDotFixTimer]; + } +} + +/// Update inner contains's size. +- (void)_updateInnerContainerSize { + CGSize size = [self _getVisibleSize]; + if (_innerContainer.isVerticalForm) size.width = CGFLOAT_MAX; + else size.height = CGFLOAT_MAX; + _innerContainer.size = size; +} + +/// Update placeholder before runloop sleep/end. +- (void)_commitPlaceholderUpdate { +#if !TARGET_INTERFACE_BUILDER + _state.placeholderNeedUpdate = YES; + [[YYTextTransaction transactionWithTarget:self selector:@selector(_updatePlaceholderIfNeeded)] commit]; +#else + [self _updatePlaceholder]; +#endif +} + +/// Update placeholder if needed. +- (void)_updatePlaceholderIfNeeded { + if (_state.placeholderNeedUpdate) { + _state.placeholderNeedUpdate = NO; + [self _updatePlaceholder]; + } +} + +/// Update placeholder immediately. +- (void)_updatePlaceholder { + CGRect frame = CGRectZero; + _placeHolderView.image = nil; + _placeHolderView.frame = frame; + if (_placeholderAttributedText.length > 0) { + YYTextContainer *container = _innerContainer.copy; + container.size = self.bounds.size; + container.truncationType = YYTextTruncationTypeEnd; + container.truncationToken = nil; + YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:_placeholderAttributedText]; + CGSize size = [layout textBoundingSize]; + BOOL needDraw = size.width > 1 && size.height > 1; + if (needDraw) { + UIGraphicsBeginImageContextWithOptions(size, NO, 0); + CGContextRef context = UIGraphicsGetCurrentContext(); + [layout drawInContext:context size:size debug:self.debugOption]; + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + _placeHolderView.image = image; + frame.size = image.size; + if (container.isVerticalForm) { + frame.origin.x = self.bounds.size.width - image.size.width; + } else { + frame.origin = CGPointZero; + } + _placeHolderView.frame = frame; + } + } +} + +/// Update the `_selectedTextRange` to a single position by `_trackingPoint`. +- (void)_updateTextRangeByTrackingCaret { + if (!_state.trackingTouch) return; + + CGPoint trackingPoint = [self _convertPointToLayout:_trackingPoint]; + YYTextPosition *newPos = [_innerLayout closestPositionToPoint:trackingPoint]; + if (newPos) { + newPos = [self _correctedTextPosition:newPos]; + if (_markedTextRange) { + if ([newPos compare:_markedTextRange.start] == NSOrderedAscending) { + newPos = _markedTextRange.start; + } else if ([newPos compare:_markedTextRange.end] == NSOrderedDescending) { + newPos = _markedTextRange.end; + } + } + YYTextRange *newRange = [YYTextRange rangeWithRange:NSMakeRange(newPos.offset, 0) affinity:newPos.affinity]; + _trackingRange = newRange; + } +} + +/// Update the `_selectedTextRange` to a new range by `_trackingPoint` and `_state.trackingGrabber`. +- (void)_updateTextRangeByTrackingGrabber { + if (!_state.trackingTouch || !_state.trackingGrabber) return; + + BOOL isStart = _state.trackingGrabber == kStart; + CGPoint magPoint = _trackingPoint; + magPoint.y += kMagnifierRangedTrackFix; + magPoint = [self _convertPointToLayout:magPoint]; + YYTextPosition *position = [_innerLayout positionForPoint:magPoint + oldPosition:(isStart ? _selectedTextRange.start : _selectedTextRange.end) + otherPosition:(isStart ? _selectedTextRange.end : _selectedTextRange.start)]; + if (position) { + position = [self _correctedTextPosition:position]; + if ((NSUInteger)position.offset > _innerText.length) { + position = [YYTextPosition positionWithOffset:_innerText.length]; + } + YYTextRange *newRange = [YYTextRange rangeWithStart:(isStart ? position : _selectedTextRange.start) + end:(isStart ? _selectedTextRange.end : position)]; + _trackingRange = newRange; + } +} + +/// Update the `_selectedTextRange` to a new range/position by `_trackingPoint`. +- (void)_updateTextRangeByTrackingPreSelect { + if (!_state.trackingTouch) return; + YYTextRange *newRange = [self _getClosestTokenRangeAtPoint:_trackingPoint]; + _trackingRange = newRange; +} + +/// Show or update `_magnifierCaret` based on `_trackingPoint`, and hide `_magnifierRange`. +- (void)_showMagnifierCaret { + if (YYTextIsAppExtension()) return; + + if (_state.showingMagnifierRanged) { + _state.showingMagnifierRanged = NO; + [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierRanged]; + } + + _magnifierCaret.hostPopoverCenter = _trackingPoint; + _magnifierCaret.hostCaptureCenter = _trackingPoint; + if (!_state.showingMagnifierCaret) { + _state.showingMagnifierCaret = YES; + [[YYTextEffectWindow sharedWindow] showMagnifier:_magnifierCaret]; + } else { + [[YYTextEffectWindow sharedWindow] moveMagnifier:_magnifierCaret]; + } +} + +/// Show or update `_magnifierRanged` based on `_trackingPoint`, and hide `_magnifierCaret`. +- (void)_showMagnifierRanged { + if (YYTextIsAppExtension()) return; + + if (_verticalForm) { // hack for vertical form... + [self _showMagnifierCaret]; + return; + } + + if (_state.showingMagnifierCaret) { + _state.showingMagnifierCaret = NO; + [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierCaret]; + } + + CGPoint magPoint = _trackingPoint; + if (_verticalForm) { + magPoint.x += kMagnifierRangedTrackFix; + } else { + magPoint.y += kMagnifierRangedTrackFix; + } + + YYTextRange *selectedRange = _selectedTextRange; + if (_state.trackingTouch && _trackingRange) { + selectedRange = _trackingRange; + } + + YYTextPosition *position; + if (_markedTextRange) { + position = selectedRange.end; + } else { + position = [_innerLayout positionForPoint:[self _convertPointToLayout:magPoint] + oldPosition:(_state.trackingGrabber == kStart ? selectedRange.start : selectedRange.end) + otherPosition:(_state.trackingGrabber == kStart ? selectedRange.end : selectedRange.start)]; + } + + NSUInteger lineIndex = [_innerLayout lineIndexForPosition:position]; + if (lineIndex < _innerLayout.lines.count) { + YYTextLine *line = _innerLayout.lines[lineIndex]; + CGRect lineRect = [self _convertRectFromLayout:line.bounds]; + if (_verticalForm) { + magPoint.x = YYTEXT_CLAMP(magPoint.x, CGRectGetMinX(lineRect), CGRectGetMaxX(lineRect)); + } else { + magPoint.y = YYTEXT_CLAMP(magPoint.y, CGRectGetMinY(lineRect), CGRectGetMaxY(lineRect)); + } + CGPoint linePoint = [_innerLayout linePositionForPosition:position]; + linePoint = [self _convertPointFromLayout:linePoint]; + + CGPoint popoverPoint = linePoint; + if (_verticalForm) { + popoverPoint.x = linePoint.x + _magnifierRangedOffset; + } else { + popoverPoint.y = linePoint.y + _magnifierRangedOffset; + } + + CGPoint capturePoint; + if (_verticalForm) { + capturePoint.x = linePoint.x + kMagnifierRangedCaptureOffset; + capturePoint.y = linePoint.y; + } else { + capturePoint.x = linePoint.x; + capturePoint.y = linePoint.y + kMagnifierRangedCaptureOffset; + } + + _magnifierRanged.hostPopoverCenter = popoverPoint; + _magnifierRanged.hostCaptureCenter = capturePoint; + if (!_state.showingMagnifierRanged) { + _state.showingMagnifierRanged = YES; + [[YYTextEffectWindow sharedWindow] showMagnifier:_magnifierRanged]; + } else { + [[YYTextEffectWindow sharedWindow] moveMagnifier:_magnifierRanged]; + } + } +} + +/// Update the showing magnifier. +- (void)_updateMagnifier { + if (YYTextIsAppExtension()) return; + + if (_state.showingMagnifierCaret) { + [[YYTextEffectWindow sharedWindow] moveMagnifier:_magnifierCaret]; + } + if (_state.showingMagnifierRanged) { + [[YYTextEffectWindow sharedWindow] moveMagnifier:_magnifierRanged]; + } +} + +/// Hide the `_magnifierCaret` and `_magnifierRanged`. +- (void)_hideMagnifier { + if (YYTextIsAppExtension()) return; + + if (_state.showingMagnifierCaret || _state.showingMagnifierRanged) { + // disable touch began temporary to ignore caret animation overlap + _state.ignoreTouchBegan = YES; + __weak typeof(self) _self = self; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.15 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + __strong typeof(_self) self = _self; + if (self) self->_state.ignoreTouchBegan = NO; + }); + } + + if (_state.showingMagnifierCaret) { + _state.showingMagnifierCaret = NO; + [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierCaret]; + } + if (_state.showingMagnifierRanged) { + _state.showingMagnifierRanged = NO; + [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierRanged]; + } +} + +/// Show and update the UIMenuController. +- (void)_showMenu { + CGRect rect; + if (_selectionView.caretVisible) { + rect = _selectionView.caretView.frame; + } else if (_selectionView.selectionRects.count > 0) { + YYTextSelectionRect *sRect = _selectionView.selectionRects.firstObject; + rect = sRect.rect; + for (NSUInteger i = 1; i < _selectionView.selectionRects.count; i++) { + sRect = _selectionView.selectionRects[i]; + rect = CGRectUnion(rect, sRect.rect); + } + + CGRect inter = CGRectIntersection(rect, self.bounds); + if (!CGRectIsNull(inter) && inter.size.height > 1) { + rect = inter; //clip to bounds + } else { + if (CGRectGetMinY(rect) < CGRectGetMinY(self.bounds)) { + rect.size.height = 1; + rect.origin.y = CGRectGetMinY(self.bounds); + } else { + rect.size.height = 1; + rect.origin.y = CGRectGetMaxY(self.bounds); + } + } + + YYTextKeyboardManager *mgr = [YYTextKeyboardManager defaultManager]; + if (mgr.keyboardVisible) { + CGRect kbRect = [mgr convertRect:mgr.keyboardFrame toView:self]; + CGRect kbInter = CGRectIntersection(rect, kbRect); + if (!CGRectIsNull(kbInter) && kbInter.size.height > 1 && kbInter.size.width > 1) { + // self is covered by keyboard + if (CGRectGetMinY(kbInter) > CGRectGetMinY(rect)) { // keyboard at bottom + rect.size.height -= kbInter.size.height; + } else if (CGRectGetMaxY(kbInter) < CGRectGetMaxY(rect)) { // keyboard at top + rect.origin.y += kbInter.size.height; + rect.size.height -= kbInter.size.height; + } + } + } + } else { + rect = _selectionView.bounds; + } + + if (!self.isFirstResponder) { + if (!_containerView.isFirstResponder) { + [_containerView becomeFirstResponder]; + } + } + + if (self.isFirstResponder || _containerView.isFirstResponder) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + UIMenuController *menu = [UIMenuController sharedMenuController]; + [menu setTargetRect:CGRectStandardize(rect) inView:_selectionView]; + [menu update]; + if (!_state.showingMenu || !menu.menuVisible) { + _state.showingMenu = YES; + [menu setMenuVisible:YES animated:YES]; + } + }); + } +} + +/// Hide the UIMenuController. +- (void)_hideMenu { + if (_state.showingMenu) { + _state.showingMenu = NO; + UIMenuController *menu = [UIMenuController sharedMenuController]; + [menu setMenuVisible:NO animated:YES]; + } + if (_containerView.isFirstResponder) { + _state.ignoreFirstResponder = YES; + [_containerView resignFirstResponder]; // it will call [self becomeFirstResponder], ignore it temporary. + _state.ignoreFirstResponder = NO; + } +} + +/// Show highlight layout based on `_highlight` and `_highlightRange` +/// If the `_highlightLayout` is nil, try to create. +- (void)_showHighlightAnimated:(BOOL)animated { + NSTimeInterval fadeDuration = animated ? kHighlightFadeDuration : 0; + if (!_highlight) return; + if (!_highlightLayout) { + NSMutableAttributedString *hiText = (_delectedText ? _delectedText : _innerText).mutableCopy; + NSDictionary *newAttrs = _highlight.attributes; + [newAttrs enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { + [hiText yy_setAttribute:key value:value range:_highlightRange]; + }]; + _highlightLayout = [YYTextLayout layoutWithContainer:_innerContainer text:hiText]; + if (!_highlightLayout) _highlight = nil; + } + + if (_highlightLayout && !_state.showingHighlight) { + _state.showingHighlight = YES; + [_containerView setLayout:_highlightLayout withFadeDuration:fadeDuration]; + } +} + +/// Show `_innerLayout` instead of `_highlightLayout`. +/// It does not destory the `_highlightLayout`. +- (void)_hideHighlightAnimated:(BOOL)animated { + NSTimeInterval fadeDuration = animated ? kHighlightFadeDuration : 0; + if (_state.showingHighlight) { + _state.showingHighlight = NO; + [_containerView setLayout:_innerLayout withFadeDuration:fadeDuration]; + } +} + +/// Show `_innerLayout` and destory the `_highlight` and `_highlightLayout`. +- (void)_removeHighlightAnimated:(BOOL)animated { + [self _hideHighlightAnimated:animated]; + _highlight = nil; + _highlightLayout = nil; +} + +/// Scroll current selected range to visible. +- (void)_scrollSelectedRangeToVisible { + [self _scrollRangeToVisible:_selectedTextRange]; +} + +/// Scroll range to visible, take account into keyboard and insets. +- (void)_scrollRangeToVisible:(YYTextRange *)range { + if (!range) return; + CGRect rect = [_innerLayout rectForRange:range]; + if (CGRectIsNull(rect)) return; + rect = [self _convertRectFromLayout:rect]; + rect = [_containerView convertRect:rect toView:self]; + + if (rect.size.width < 1) rect.size.width = 1; + if (rect.size.height < 1) rect.size.height = 1; + CGFloat extend = 3; + + BOOL insetModified = NO; + YYTextKeyboardManager *mgr = [YYTextKeyboardManager defaultManager]; + + if (mgr.keyboardVisible && self.window && self.superview && self.isFirstResponder && !_verticalForm) { + CGRect bounds = self.bounds; + bounds.origin = CGPointZero; + CGRect kbRect = [mgr convertRect:mgr.keyboardFrame toView:self]; + kbRect.origin.y -= _extraAccessoryViewHeight; + kbRect.size.height += _extraAccessoryViewHeight; + + kbRect.origin.x -= self.contentOffset.x; + kbRect.origin.y -= self.contentOffset.y; + CGRect inter = CGRectIntersection(bounds, kbRect); + if (!CGRectIsNull(inter) && inter.size.height > 1 && inter.size.width > extend) { // self is covered by keyboard + if (CGRectGetMinY(inter) > CGRectGetMinY(bounds)) { // keyboard below self.top + + UIEdgeInsets originalContentInset = self.contentInset; + UIEdgeInsets originalScrollIndicatorInsets = self.scrollIndicatorInsets; + if (_insetModifiedByKeyboard) { + originalContentInset = _originalContentInset; + originalScrollIndicatorInsets = _originalScrollIndicatorInsets; + } + + if (originalContentInset.bottom < inter.size.height + extend) { + insetModified = YES; + if (!_insetModifiedByKeyboard) { + _insetModifiedByKeyboard = YES; + _originalContentInset = self.contentInset; + _originalScrollIndicatorInsets = self.scrollIndicatorInsets; + } + UIEdgeInsets newInset = originalContentInset; + UIEdgeInsets newIndicatorInsets = originalScrollIndicatorInsets; + newInset.bottom = inter.size.height + extend; + newIndicatorInsets.bottom = newInset.bottom; + UIViewAnimationOptions curve; + if (kiOS7Later) { + curve = 7 << 16; + } else { + curve = UIViewAnimationOptionCurveEaseInOut; + } + [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction | curve animations:^{ + [super setContentInset:newInset]; + [super setScrollIndicatorInsets:newIndicatorInsets]; + [self scrollRectToVisible:CGRectInset(rect, -extend, -extend) animated:NO]; + } completion:NULL]; + } + } + } + } + if (!insetModified) { + [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationCurveEaseOut animations:^{ + [self _restoreInsetsAnimated:NO]; + [self scrollRectToVisible:CGRectInset(rect, -extend, -extend) animated:NO]; + } completion:NULL]; + } +} + +/// Restore contents insets if modified by keyboard. +- (void)_restoreInsetsAnimated:(BOOL)animated { + if (_insetModifiedByKeyboard) { + _insetModifiedByKeyboard = NO; + if (animated) { + [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationCurveEaseOut animations:^{ + [super setContentInset:_originalContentInset]; + [super setScrollIndicatorInsets:_originalScrollIndicatorInsets]; + } completion:NULL]; + } else { + [super setContentInset:_originalContentInset]; + [super setScrollIndicatorInsets:_originalScrollIndicatorInsets]; + } + } +} + +/// Keyboard frame changed, scroll the caret to visible range, or modify the content insets. +- (void)_keyboardChanged { + if (!self.isFirstResponder) return; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + if ([YYTextKeyboardManager defaultManager].keyboardVisible) { + [self _scrollRangeToVisible:_selectedTextRange]; + } else { + [self _restoreInsetsAnimated:YES]; + } + [self _updateMagnifier]; + if (_state.showingMenu) { + [self _showMenu]; + } + }); +} + +/// Start long press timer, used for 'highlight' range text action. +- (void)_startLongPressTimer { + [_longPressTimer invalidate]; + _longPressTimer = [NSTimer timerWithTimeInterval:kLongPressMinimumDuration + target:[YYTextWeakProxy proxyWithTarget:self] + selector:@selector(_trackDidLongPress) + userInfo:nil + repeats:NO]; + [[NSRunLoop currentRunLoop] addTimer:_longPressTimer forMode:NSRunLoopCommonModes]; +} + +/// Invalidate the long press timer. +- (void)_endLongPressTimer { + [_longPressTimer invalidate]; + _longPressTimer = nil; +} + +/// Long press detected. +- (void)_trackDidLongPress { + [self _endLongPressTimer]; + + BOOL dealLongPressAction = NO; + if (_state.showingHighlight) { + [self _hideMenu]; + + if (_highlight.longPressAction) { + dealLongPressAction = YES; + CGRect rect = [_innerLayout rectForRange:[YYTextRange rangeWithRange:_highlightRange]]; + rect = [self _convertRectFromLayout:rect]; + _highlight.longPressAction(self, _innerText, _highlightRange, rect); + [self _endTouchTracking]; + } else { + BOOL shouldHighlight = YES; + if ([self.delegate respondsToSelector:@selector(textView:shouldLongPressHighlight:inRange:)]) { + shouldHighlight = [self.delegate textView:self shouldLongPressHighlight:_highlight inRange:_highlightRange]; + } + if (shouldHighlight && [self.delegate respondsToSelector:@selector(textView:didLongPressHighlight:inRange:rect:)]) { + dealLongPressAction = YES; + CGRect rect = [_innerLayout rectForRange:[YYTextRange rangeWithRange:_highlightRange]]; + rect = [self _convertRectFromLayout:rect]; + [self.delegate textView:self didLongPressHighlight:_highlight inRange:_highlightRange rect:rect]; + [self _endTouchTracking]; + } + } + } + + if (!dealLongPressAction){ + [self _removeHighlightAnimated:NO]; + if (_state.trackingTouch) { + if (_state.trackingGrabber) { + self.panGestureRecognizer.enabled = NO; + [self _hideMenu]; + [self _showMagnifierRanged]; + } else if (self.isFirstResponder){ + self.panGestureRecognizer.enabled = NO; + _selectionView.caretBlinks = NO; + _state.trackingCaret = YES; + CGPoint trackingPoint = [self _convertPointToLayout:_trackingPoint]; + YYTextPosition *newPos = [_innerLayout closestPositionToPoint:trackingPoint]; + newPos = [self _correctedTextPosition:newPos]; + if (newPos) { + if (_markedTextRange) { + if ([newPos compare:_markedTextRange.start] != NSOrderedDescending) { + newPos = _markedTextRange.start; + } else if ([newPos compare:_markedTextRange.end] != NSOrderedAscending) { + newPos = _markedTextRange.end; + } + } + _trackingRange = [YYTextRange rangeWithRange:NSMakeRange(newPos.offset, 0) affinity:newPos.affinity]; + [self _updateSelectionView]; + } + [self _hideMenu]; + + if (_markedTextRange) { + [self _showMagnifierRanged]; + } else { + [self _showMagnifierCaret]; + } + } else if (self.selectable) { + self.panGestureRecognizer.enabled = NO; + _state.trackingPreSelect = YES; + _state.selectedWithoutEdit = NO; + [self _updateTextRangeByTrackingPreSelect]; + [self _updateSelectionView]; + [self _showMagnifierCaret]; + } + } + } +} + +/// Start auto scroll timer, used for auto scroll tick. +- (void)_startAutoScrollTimer { + if (!_autoScrollTimer) { + [_autoScrollTimer invalidate]; + _autoScrollTimer = [NSTimer timerWithTimeInterval:kAutoScrollMinimumDuration + target:[YYTextWeakProxy proxyWithTarget:self] + selector:@selector(_trackDidTickAutoScroll) + userInfo:nil + repeats:YES]; + [[NSRunLoop currentRunLoop] addTimer:_autoScrollTimer forMode:NSRunLoopCommonModes]; + } +} + +/// Invalidate the auto scroll, and restore the text view state. +- (void)_endAutoScrollTimer { + if (_state.autoScrollTicked) [self flashScrollIndicators]; + [_autoScrollTimer invalidate]; + _autoScrollTimer = nil; + _autoScrollOffset = 0; + _autoScrollAcceleration = 0; + _state.autoScrollTicked = NO; + + if (_magnifierCaret.captureDisabled) { + _magnifierCaret.captureDisabled = NO; + if (_state.showingMagnifierCaret) { + [self _showMagnifierCaret]; + } + } + if (_magnifierRanged.captureDisabled) { + _magnifierRanged.captureDisabled = NO; + if (_state.showingMagnifierRanged) { + [self _showMagnifierRanged]; + } + } +} + +/// Auto scroll ticked by timer. +- (void)_trackDidTickAutoScroll { + if (_autoScrollOffset != 0) { + _magnifierCaret.captureDisabled = YES; + _magnifierRanged.captureDisabled = YES; + + CGPoint offset = self.contentOffset; + if (_verticalForm) { + offset.x += _autoScrollOffset; + + if (_autoScrollAcceleration > 0) { + offset.x += ((_autoScrollOffset > 0 ? 1 : -1) * _autoScrollAcceleration * _autoScrollAcceleration * 0.5); + } + _autoScrollAcceleration++; + offset.x = round(offset.x); + if (_autoScrollOffset < 0) { + if (offset.x < -self.contentInset.left) offset.x = -self.contentInset.left; + } else { + CGFloat maxOffsetX = self.contentSize.width - self.bounds.size.width + self.contentInset.right; + if (offset.x > maxOffsetX) offset.x = maxOffsetX; + } + if (offset.x < -self.contentInset.left) offset.x = -self.contentInset.left; + } else { + offset.y += _autoScrollOffset; + if (_autoScrollAcceleration > 0) { + offset.y += ((_autoScrollOffset > 0 ? 1 : -1) * _autoScrollAcceleration * _autoScrollAcceleration * 0.5); + } + _autoScrollAcceleration++; + offset.y = round(offset.y); + if (_autoScrollOffset < 0) { + if (offset.y < -self.contentInset.top) offset.y = -self.contentInset.top; + } else { + CGFloat maxOffsetY = self.contentSize.height - self.bounds.size.height + self.contentInset.bottom; + if (offset.y > maxOffsetY) offset.y = maxOffsetY; + } + if (offset.y < -self.contentInset.top) offset.y = -self.contentInset.top; + } + + BOOL shouldScroll; + if (_verticalForm) { + shouldScroll = fabs(offset.x -self.contentOffset.x) > 0.5; + } else { + shouldScroll = fabs(offset.y -self.contentOffset.y) > 0.5; + } + + if (shouldScroll) { + _state.autoScrollTicked = YES; + _trackingPoint.x += offset.x - self.contentOffset.x; + _trackingPoint.y += offset.y - self.contentOffset.y; + [UIView animateWithDuration:kAutoScrollMinimumDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionCurveLinear animations:^{ + [self setContentOffset:offset]; + } completion:^(BOOL finished) { + if (_state.trackingTouch) { + if (_state.trackingGrabber) { + [self _showMagnifierRanged]; + [self _updateTextRangeByTrackingGrabber]; + } else if (_state.trackingPreSelect) { + [self _showMagnifierCaret]; + [self _updateTextRangeByTrackingPreSelect]; + } else if (_state.trackingCaret) { + if (_markedTextRange) { + [self _showMagnifierRanged]; + } else { + [self _showMagnifierCaret]; + } + [self _updateTextRangeByTrackingCaret]; + } + [self _updateSelectionView]; + } + }]; + } else { + [self _endAutoScrollTimer]; + } + } else { + [self _endAutoScrollTimer]; + } +} + +/// End current touch tracking (if is tracking now), and update the state. +- (void)_endTouchTracking { + if (!_state.trackingTouch) return; + + _state.trackingTouch = NO; + _state.trackingGrabber = NO; + _state.trackingCaret = NO; + _state.trackingPreSelect = NO; + _state.touchMoved = NO; + _state.deleteConfirm = NO; + _state.clearsOnInsertionOnce = NO; + _trackingRange = nil; + _selectionView.caretBlinks = YES; + + [self _removeHighlightAnimated:YES]; + [self _hideMagnifier]; + [self _endLongPressTimer]; + [self _endAutoScrollTimer]; + [self _updateSelectionView]; + + self.panGestureRecognizer.enabled = self.scrollEnabled; +} + +/// Start a timer to fix the selection dot. +- (void)_startSelectionDotFixTimer { + [_selectionDotFixTimer invalidate]; + _longPressTimer = [NSTimer timerWithTimeInterval:1/15.0 + target:[YYTextWeakProxy proxyWithTarget:self] + selector:@selector(_fixSelectionDot) + userInfo:nil + repeats:NO]; + [[NSRunLoop currentRunLoop] addTimer:_longPressTimer forMode:NSRunLoopCommonModes]; +} + +/// End the timer. +- (void)_endSelectionDotFixTimer { + [_selectionDotFixTimer invalidate]; + _selectionDotFixTimer = nil; +} + +/// If it shows selection grabber and this view was moved by super view, +/// update the selection dot in window. +- (void)_fixSelectionDot { + if (YYTextIsAppExtension()) return; + CGPoint origin = [self yy_convertPoint:CGPointZero toViewOrWindow:[YYTextEffectWindow sharedWindow]]; + if (!CGPointEqualToPoint(origin, _previousOriginInWindow)) { + _previousOriginInWindow = origin; + [[YYTextEffectWindow sharedWindow] hideSelectionDot:_selectionView]; + [[YYTextEffectWindow sharedWindow] showSelectionDot:_selectionView]; + } +} + +/// Try to get the character range/position with word granularity from the tokenizer. +- (YYTextRange *)_getClosestTokenRangeAtPosition:(YYTextPosition *)position { + position = [self _correctedTextPosition:position]; + if (!position) return nil; + YYTextRange *range = nil; + if (_tokenizer) { + range = (id)[_tokenizer rangeEnclosingPosition:position withGranularity:UITextGranularityWord inDirection:UITextStorageDirectionForward]; + if (range.asRange.length == 0) { + range = (id)[_tokenizer rangeEnclosingPosition:position withGranularity:UITextGranularityWord inDirection:UITextStorageDirectionBackward]; + } + } + + if (!range || range.asRange.length == 0) { + range = [_innerLayout textRangeByExtendingPosition:position inDirection:UITextLayoutDirectionRight offset:1]; + range = [self _correctedTextRange:range]; + if (range.asRange.length == 0) { + range = [_innerLayout textRangeByExtendingPosition:position inDirection:UITextLayoutDirectionLeft offset:1]; + range = [self _correctedTextRange:range]; + } + } else { + YYTextRange *extStart = [_innerLayout textRangeByExtendingPosition:range.start]; + YYTextRange *extEnd = [_innerLayout textRangeByExtendingPosition:range.end]; + if (extStart && extEnd) { + NSArray *arr = [@[extStart.start, extStart.end, extEnd.start, extEnd.end] sortedArrayUsingSelector:@selector(compare:)]; + range = [YYTextRange rangeWithStart:arr.firstObject end:arr.lastObject]; + } + } + + range = [self _correctedTextRange:range]; + if (range.asRange.length == 0) { + range = [YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)]; + } + + return [self _correctedTextRange:range]; +} + +/// Try to get the character range/position with word granularity from the tokenizer. +- (YYTextRange *)_getClosestTokenRangeAtPoint:(CGPoint)point { + point = [self _convertPointToLayout:point]; + YYTextRange *touchRange = [_innerLayout closestTextRangeAtPoint:point]; + touchRange = [self _correctedTextRange:touchRange]; + + if (_tokenizer && touchRange) { + YYTextRange *encEnd = (id)[_tokenizer rangeEnclosingPosition:touchRange.end withGranularity:UITextGranularityWord inDirection:UITextStorageDirectionBackward]; + YYTextRange *encStart = (id)[_tokenizer rangeEnclosingPosition:touchRange.start withGranularity:UITextGranularityWord inDirection:UITextStorageDirectionForward]; + if (encEnd && encStart) { + NSArray *arr = [@[encEnd.start, encEnd.end, encStart.start, encStart.end] sortedArrayUsingSelector:@selector(compare:)]; + touchRange = [YYTextRange rangeWithStart:arr.firstObject end:arr.lastObject]; + } + } + + if (touchRange) { + YYTextRange *extStart = [_innerLayout textRangeByExtendingPosition:touchRange.start]; + YYTextRange *extEnd = [_innerLayout textRangeByExtendingPosition:touchRange.end]; + if (extStart && extEnd) { + NSArray *arr = [@[extStart.start, extStart.end, extEnd.start, extEnd.end] sortedArrayUsingSelector:@selector(compare:)]; + touchRange = [YYTextRange rangeWithStart:arr.firstObject end:arr.lastObject]; + } + } + + if (!touchRange) touchRange = [YYTextRange defaultRange]; + + if (_innerText.length && touchRange.asRange.length == 0) { + touchRange = [YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)]; + } + + return touchRange; +} + +/// Try to get the highlight property. If exist, the range will be returnd by the range pointer. +/// If the delegate ignore the highlight, returns nil. +- (YYTextHighlight *)_getHighlightAtPoint:(CGPoint)point range:(NSRangePointer)range { + if (!_highlightable || !_innerLayout.containsHighlight) return nil; + point = [self _convertPointToLayout:point]; + YYTextRange *textRange = [_innerLayout textRangeAtPoint:point]; + textRange = [self _correctedTextRange:textRange]; + if (!textRange) return nil; + NSUInteger startIndex = textRange.start.offset; + if (startIndex == _innerText.length) { + if (startIndex == 0) return nil; + else startIndex--; + } + NSRange highlightRange = {0}; + NSAttributedString *text = _delectedText ? _delectedText : _innerText; + YYTextHighlight *highlight = [text attribute:YYTextHighlightAttributeName + atIndex:startIndex + longestEffectiveRange:&highlightRange + inRange:NSMakeRange(0, _innerText.length)]; + + if (!highlight) return nil; + + BOOL shouldTap = YES, shouldLongPress = YES; + if (!highlight.tapAction && !highlight.longPressAction) { + if ([self.delegate respondsToSelector:@selector(textView:shouldTapHighlight:inRange:)]) { + shouldTap = [self.delegate textView:self shouldTapHighlight:highlight inRange:highlightRange]; + } + if ([self.delegate respondsToSelector:@selector(textView:shouldLongPressHighlight:inRange:)]) { + shouldLongPress = [self.delegate textView:self shouldLongPressHighlight:highlight inRange:highlightRange]; + } + } + if (!shouldTap && !shouldLongPress) return nil; + if (range) *range = highlightRange; + return highlight; +} + +/// Return the ranged magnifier popover offset from the baseline, base on `_trackingPoint`. +- (CGFloat)_getMagnifierRangedOffset { + CGPoint magPoint = _trackingPoint; + magPoint = [self _convertPointToLayout:magPoint]; + if (_verticalForm) { + magPoint.x += kMagnifierRangedTrackFix; + } else { + magPoint.y += kMagnifierRangedTrackFix; + } + YYTextPosition *position = [_innerLayout closestPositionToPoint:magPoint]; + NSUInteger lineIndex = [_innerLayout lineIndexForPosition:position]; + if (lineIndex < _innerLayout.lines.count) { + YYTextLine *line = _innerLayout.lines[lineIndex]; + if (_verticalForm) { + magPoint.x = YYTEXT_CLAMP(magPoint.x, line.left, line.right); + return magPoint.x - line.position.x + kMagnifierRangedPopoverOffset; + } else { + magPoint.y = YYTEXT_CLAMP(magPoint.y, line.top, line.bottom); + return magPoint.y - line.position.y + kMagnifierRangedPopoverOffset; + } + } else { + return 0; + } +} + +/// Return a YYTextMoveDirection from `_touchBeganPoint` to `_trackingPoint`. +- (unsigned int)_getMoveDirection { + CGFloat moveH = _trackingPoint.x - _touchBeganPoint.x; + CGFloat moveV = _trackingPoint.y - _touchBeganPoint.y; + if (fabs(moveH) > fabs(moveV)) { + if (fabs(moveH) > kLongPressAllowableMovement) { + return moveH > 0 ? kRight : kLeft; + } + } else { + if (fabs(moveV) > kLongPressAllowableMovement) { + return moveV > 0 ? kBottom : kTop; + } + } + return 0; +} + +/// Get the auto scroll offset in one tick time. +- (CGFloat)_getAutoscrollOffset { + if (!_state.trackingTouch) return 0; + + CGRect bounds = self.bounds; + bounds.origin = CGPointZero; + YYTextKeyboardManager *mgr = [YYTextKeyboardManager defaultManager]; + if (mgr.keyboardVisible && self.window && self.superview && self.isFirstResponder && !_verticalForm) { + CGRect kbRect = [mgr convertRect:mgr.keyboardFrame toView:self]; + kbRect.origin.y -= _extraAccessoryViewHeight; + kbRect.size.height += _extraAccessoryViewHeight; + + kbRect.origin.x -= self.contentOffset.x; + kbRect.origin.y -= self.contentOffset.y; + CGRect inter = CGRectIntersection(bounds, kbRect); + if (!CGRectIsNull(inter) && inter.size.height > 1 && inter.size.width > 1) { + if (CGRectGetMinY(inter) > CGRectGetMinY(bounds)) { + bounds.size.height -= inter.size.height; + } + } + } + + CGPoint point = _trackingPoint; + point.x -= self.contentOffset.x; + point.y -= self.contentOffset.y; + + CGFloat maxOfs = 32; // a good value ~ + CGFloat ofs = 0; + if (_verticalForm) { + if (point.x < self.contentInset.left) { + ofs = (point.x - self.contentInset.left - 5) * 0.5; + if (ofs < -maxOfs) ofs = -maxOfs; + } else if (point.x > bounds.size.width) { + ofs = ((point.x - bounds.size.width) + 5) * 0.5; + if (ofs > maxOfs) ofs = maxOfs; + } + } else { + if (point.y < self.contentInset.top) { + ofs = (point.y - self.contentInset.top - 5) * 0.5; + if (ofs < -maxOfs) ofs = -maxOfs; + } else if (point.y > bounds.size.height) { + ofs = ((point.y - bounds.size.height) + 5) * 0.5; + if (ofs > maxOfs) ofs = maxOfs; + } + } + return ofs; +} + +/// Visible size based on bounds and insets +- (CGSize)_getVisibleSize { + CGSize visibleSize = self.bounds.size; + visibleSize.width -= self.contentInset.left - self.contentInset.right; + visibleSize.height -= self.contentInset.top - self.contentInset.bottom; + if (visibleSize.width < 0) visibleSize.width = 0; + if (visibleSize.height < 0) visibleSize.height = 0; + return visibleSize; +} + +/// Returns whether the text view can paste data from pastboard. +- (BOOL)_isPasteboardContainsValidValue { + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + if (pasteboard.string.length > 0) { + return YES; + } + if (pasteboard.yy_AttributedString.length > 0) { + if (_allowsPasteAttributedString) { + return YES; + } + } + if (pasteboard.image || pasteboard.yy_ImageData.length > 0) { + if (_allowsPasteImage) { + return YES; + } + } + return NO; +} + +/// Save current selected attributed text to pasteboard. +- (void)_copySelectedTextToPasteboard { + if (_allowsCopyAttributedString) { + NSAttributedString *text = [_innerText attributedSubstringFromRange:_selectedTextRange.asRange]; + if (text.length) { + [UIPasteboard generalPasteboard].yy_AttributedString = text; + } + } else { + NSString *string = [_innerText yy_plainTextForRange:_selectedTextRange.asRange]; + if (string.length) { + [UIPasteboard generalPasteboard].string = string; + } + } +} + +/// Update the text view state when pasteboard changed. +- (void)_pasteboardChanged { + if (_state.showingMenu) { + UIMenuController *menu = [UIMenuController sharedMenuController]; + [menu update]; + } +} + +/// Whether the position is valid (not out of bounds). +- (BOOL)_isTextPositionValid:(YYTextPosition *)position { + if (!position) return NO; + if (position.offset < 0) return NO; + if (position.offset > _innerText.length) return NO; + if (position.offset == 0 && position.affinity == YYTextAffinityBackward) return NO; + if (position.offset == _innerText.length && position.affinity == YYTextAffinityBackward) return NO; + return YES; +} + +/// Whether the range is valid (not out of bounds). +- (BOOL)_isTextRangeValid:(YYTextRange *)range { + if (![self _isTextPositionValid:range.start]) return NO; + if (![self _isTextPositionValid:range.end]) return NO; + return YES; +} + +/// Correct the position if it out of bounds. +- (YYTextPosition *)_correctedTextPosition:(YYTextPosition *)position { + if (!position) return nil; + if ([self _isTextPositionValid:position]) return position; + if (position.offset < 0) { + return [YYTextPosition positionWithOffset:0]; + } + if (position.offset > _innerText.length) { + return [YYTextPosition positionWithOffset:_innerText.length]; + } + if (position.offset == 0 && position.affinity == YYTextAffinityBackward) { + return [YYTextPosition positionWithOffset:position.offset]; + } + if (position.offset == _innerText.length && position.affinity == YYTextAffinityBackward) { + return [YYTextPosition positionWithOffset:position.offset]; + } + return position; +} + +/// Correct the range if it out of bounds. +- (YYTextRange *)_correctedTextRange:(YYTextRange *)range { + if (!range) return nil; + if ([self _isTextRangeValid:range]) return range; + YYTextPosition *start = [self _correctedTextPosition:range.start]; + YYTextPosition *end = [self _correctedTextPosition:range.end]; + return [YYTextRange rangeWithStart:start end:end]; +} + +/// Convert the point from this view to text layout. +- (CGPoint)_convertPointToLayout:(CGPoint)point { + CGSize boundingSize = _innerLayout.textBoundingSize; + if (_innerLayout.container.isVerticalForm) { + CGFloat w = _innerLayout.textBoundingSize.width; + if (w < self.bounds.size.width) w = self.bounds.size.width; + point.x += _innerLayout.container.size.width - w; + if (boundingSize.width < self.bounds.size.width) { + if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) { + point.x += (self.bounds.size.width - boundingSize.width) * 0.5; + } else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) { + point.x += (self.bounds.size.width - boundingSize.width); + } + } + return point; + } else { + if (boundingSize.height < self.bounds.size.height) { + if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) { + point.y -= (self.bounds.size.height - boundingSize.height) * 0.5; + } else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) { + point.y -= (self.bounds.size.height - boundingSize.height); + } + } + return point; + } +} + +/// Convert the point from text layout to this view. +- (CGPoint)_convertPointFromLayout:(CGPoint)point { + CGSize boundingSize = _innerLayout.textBoundingSize; + if (_innerLayout.container.isVerticalForm) { + CGFloat w = _innerLayout.textBoundingSize.width; + if (w < self.bounds.size.width) w = self.bounds.size.width; + point.x -= _innerLayout.container.size.width - w; + if (boundingSize.width < self.bounds.size.width) { + if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) { + point.x -= (self.bounds.size.width - boundingSize.width) * 0.5; + } else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) { + point.x -= (self.bounds.size.width - boundingSize.width); + } + } + return point; + } else { + if (boundingSize.height < self.bounds.size.height) { + if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) { + point.y += (self.bounds.size.height - boundingSize.height) * 0.5; + } else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) { + point.y += (self.bounds.size.height - boundingSize.height); + } + } + return point; + } +} + +/// Convert the rect from this view to text layout. +- (CGRect)_convertRectToLayout:(CGRect)rect { + rect.origin = [self _convertPointToLayout:rect.origin]; + return rect; +} + +/// Convert the rect from text layout to this view. +- (CGRect)_convertRectFromLayout:(CGRect)rect { + rect.origin = [self _convertPointFromLayout:rect.origin]; + return rect; +} + +/// Replace the range with the text, and change the `_selectTextRange`. +/// The caller should make sure the `range` and `text` are valid before call this method. +- (void)_replaceRange:(YYTextRange *)range withText:(NSString *)text notifyToDelegate:(BOOL)notify{ + if (NSEqualRanges(range.asRange, _selectedTextRange.asRange)) { + if (notify) [_inputDelegate selectionWillChange:self]; + NSRange newRange = NSMakeRange(0, 0); + newRange.location = _selectedTextRange.start.offset + text.length; + _selectedTextRange = [YYTextRange rangeWithRange:newRange]; + if (notify) [_inputDelegate selectionDidChange:self]; + } else { + if (range.asRange.length != text.length) { + if (notify) [_inputDelegate selectionWillChange:self]; + NSRange unionRange = NSIntersectionRange(_selectedTextRange.asRange, range.asRange); + if (unionRange.length == 0) { + // no intersection + if (range.end.offset <= _selectedTextRange.start.offset) { + NSInteger ofs = (NSInteger)text.length - (NSInteger)range.asRange.length; + NSRange newRange = _selectedTextRange.asRange; + newRange.location += ofs; + _selectedTextRange = [YYTextRange rangeWithRange:newRange]; + } + } else if (unionRange.length == _selectedTextRange.asRange.length) { + // target range contains selected range + _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(range.start.offset + text.length, 0)]; + } else if (range.start.offset >= _selectedTextRange.start.offset && + range.end.offset <= _selectedTextRange.end.offset) { + // target range inside selected range + NSInteger ofs = (NSInteger)text.length - (NSInteger)range.asRange.length; + NSRange newRange = _selectedTextRange.asRange; + newRange.length += ofs; + _selectedTextRange = [YYTextRange rangeWithRange:newRange]; + } else { + // interleaving + if (range.start.offset < _selectedTextRange.start.offset) { + NSRange newRange = _selectedTextRange.asRange; + newRange.location = range.start.offset + text.length; + newRange.length -= unionRange.length; + _selectedTextRange = [YYTextRange rangeWithRange:newRange]; + } else { + NSRange newRange = _selectedTextRange.asRange; + newRange.length -= unionRange.length; + _selectedTextRange = [YYTextRange rangeWithRange:newRange]; + } + } + _selectedTextRange = [self _correctedTextRange:_selectedTextRange]; + if (notify) [_inputDelegate selectionDidChange:self]; + } + } + if (notify) [_inputDelegate textWillChange:self]; + NSRange newRange = NSMakeRange(range.asRange.location, text.length); + [_innerText replaceCharactersInRange:range.asRange withString:text]; + [_innerText yy_removeDiscontinuousAttributesInRange:newRange]; + if (notify) [_inputDelegate textDidChange:self]; +} + +/// Save current typing attributes to the attributes holder. +- (void)_updateAttributesHolder { + if (_innerText.length > 0) { + NSUInteger index = _selectedTextRange.end.offset == 0 ? 0 : _selectedTextRange.end.offset - 1; + NSDictionary *attributes = [_innerText yy_attributesAtIndex:index]; + if (!attributes) attributes = @{}; + _typingAttributesHolder.yy_attributes = attributes; + [_typingAttributesHolder yy_removeDiscontinuousAttributesInRange:NSMakeRange(0, _typingAttributesHolder.length)]; + [_typingAttributesHolder removeAttribute:YYTextBorderAttributeName range:NSMakeRange(0, _typingAttributesHolder.length)]; + [_typingAttributesHolder removeAttribute:YYTextBackgroundBorderAttributeName range:NSMakeRange(0, _typingAttributesHolder.length)]; + } +} + +/// Update outer properties from current inner data. +- (void)_updateOuterProperties { + [self _updateAttributesHolder]; + NSParagraphStyle *style = _innerText.yy_paragraphStyle; + if (!style) style = _typingAttributesHolder.yy_paragraphStyle; + if (!style) style = [NSParagraphStyle defaultParagraphStyle]; + + UIFont *font = _innerText.yy_font; + if (!font) font = _typingAttributesHolder.yy_font; + if (!font) font = [self _defaultFont]; + + UIColor *color = _innerText.yy_color; + if (!color) color = _typingAttributesHolder.yy_color; + if (!color) color = [UIColor blackColor]; + + [self _setText:[_innerText yy_plainTextForRange:NSMakeRange(0, _innerText.length)]]; + [self _setFont:font]; + [self _setTextColor:color]; + [self _setTextAlignment:style.alignment]; + [self _setSelectedRange:_selectedTextRange.asRange]; + [self _setTypingAttributes:_typingAttributesHolder.yy_attributes]; + [self _setAttributedText:_innerText]; +} + +/// Parse text with `textParser` and update the _selectedTextRange. +/// @return Whether changed (text or selection) +- (BOOL)_parseText { + if (self.textParser) { + YYTextRange *oldTextRange = _selectedTextRange; + NSRange newRange = _selectedTextRange.asRange; + + [_inputDelegate textWillChange:self]; + BOOL textChanged = [self.textParser parseText:_innerText selectedRange:&newRange]; + [_inputDelegate textDidChange:self]; + + YYTextRange *newTextRange = [YYTextRange rangeWithRange:newRange]; + newTextRange = [self _correctedTextRange:newTextRange]; + + if (![oldTextRange isEqual:newTextRange]) { + [_inputDelegate selectionWillChange:self]; + _selectedTextRange = newTextRange; + [_inputDelegate selectionDidChange:self]; + } + return textChanged; + } + return NO; +} + +/// Returns whether the text should be detected by the data detector. +- (BOOL)_shouldDetectText { + if (!_dataDetector) return NO; + if (!_highlightable) return NO; + if (_linkTextAttributes.count == 0 && _highlightTextAttributes.count == 0) return NO; + if (self.isFirstResponder || _containerView.isFirstResponder) return NO; + return YES; +} + +/// Detect the data in text and add highlight to the data range. +/// @return Whether detected. +- (BOOL)_detectText:(NSMutableAttributedString *)text { + if (![self _shouldDetectText]) return NO; + if (text.length == 0) return NO; + __block BOOL detected = NO; + [_dataDetector enumerateMatchesInString:text.string options:kNilOptions range:NSMakeRange(0, text.length) usingBlock: ^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { + switch (result.resultType) { + case NSTextCheckingTypeDate: + case NSTextCheckingTypeAddress: + case NSTextCheckingTypeLink: + case NSTextCheckingTypePhoneNumber: { + detected = YES; + if (_highlightTextAttributes.count) { + YYTextHighlight *highlight = [YYTextHighlight highlightWithAttributes:_highlightTextAttributes]; + [text yy_setTextHighlight:highlight range:result.range]; + } + if (_linkTextAttributes.count) { + [_linkTextAttributes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + [text yy_setAttribute:key value:obj range:result.range]; + }]; + } + } break; + default: + break; + } + }]; + return detected; +} + +/// Returns the `root` view controller (returns nil if not found). +- (UIViewController *)_getRootViewController { + UIViewController *ctrl = nil; + UIApplication *app = YYTextSharedApplication(); + if (!ctrl) ctrl = app.keyWindow.rootViewController; + if (!ctrl) ctrl = [app.windows.firstObject rootViewController]; + if (!ctrl) ctrl = self.yy_viewController; + if (!ctrl) return nil; + + while (!ctrl.view.window && ctrl.presentedViewController) { + ctrl = ctrl.presentedViewController; + } + if (!ctrl.view.window) return nil; + return ctrl; +} + +/// Clear the undo and redo stack, and capture current state to undo stack. +- (void)_resetUndoAndRedoStack { + [_undoStack removeAllObjects]; + [_redoStack removeAllObjects]; + _YYTextViewUndoObject *object = [_YYTextViewUndoObject objectWithText:_innerText.copy range:_selectedTextRange.asRange]; + _lastTypeRange = _selectedTextRange.asRange; + [_undoStack addObject:object]; +} + +/// Clear the redo stack. +- (void)_resetRedoStack { + [_redoStack removeAllObjects]; +} + +/// Capture current state to undo stack. +- (void)_saveToUndoStack { + if (!_allowsUndoAndRedo) return; + _YYTextViewUndoObject *lastObject = _undoStack.lastObject; + if ([lastObject.text isEqualToAttributedString:self.attributedText]) return; + + _YYTextViewUndoObject *object = [_YYTextViewUndoObject objectWithText:_innerText.copy range:_selectedTextRange.asRange]; + _lastTypeRange = _selectedTextRange.asRange; + [_undoStack addObject:object]; + while (_undoStack.count > _maximumUndoLevel) { + [_undoStack removeObjectAtIndex:0]; + } +} + +/// Capture current state to redo stack. +- (void)_saveToRedoStack { + if (!_allowsUndoAndRedo) return; + _YYTextViewUndoObject *lastObject = _redoStack.lastObject; + if ([lastObject.text isEqualToAttributedString:self.attributedText]) return; + + _YYTextViewUndoObject *object = [_YYTextViewUndoObject objectWithText:_innerText.copy range:_selectedTextRange.asRange]; + [_redoStack addObject:object]; + while (_redoStack.count > _maximumUndoLevel) { + [_redoStack removeObjectAtIndex:0]; + } +} + +- (BOOL)_canUndo { + if (_undoStack.count == 0) return NO; + _YYTextViewUndoObject *object = _undoStack.lastObject; + if ([object.text isEqualToAttributedString:_innerText]) return NO; + return YES; +} + +- (BOOL)_canRedo { + if (_redoStack.count == 0) return NO; + _YYTextViewUndoObject *object = _undoStack.lastObject; + if ([object.text isEqualToAttributedString:_innerText]) return NO; + return YES; +} + +- (void)_undo { + if (![self _canUndo]) return; + [self _saveToRedoStack]; + _YYTextViewUndoObject *object = _undoStack.lastObject; + [_undoStack removeLastObject]; + + _state.insideUndoBlock = YES; + self.attributedText = object.text; + self.selectedRange = object.selectedRange; + _state.insideUndoBlock = NO; +} + +- (void)_redo { + if (![self _canRedo]) return; + [self _saveToUndoStack]; + _YYTextViewUndoObject *object = _redoStack.lastObject; + [_redoStack removeLastObject]; + + _state.insideUndoBlock = YES; + self.attributedText = object.text; + self.selectedRange = object.selectedRange; + _state.insideUndoBlock = NO; +} + +- (void)_restoreFirstResponderAfterUndoAlert { + if (_state.firstResponderBeforeUndoAlert) { + [self performSelector:@selector(becomeFirstResponder) withObject:nil afterDelay:0]; + } +} + +/// Show undo alert if it can undo or redo. +#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED +- (void)_showUndoRedoAlert NS_EXTENSION_UNAVAILABLE_IOS(""){ + _state.firstResponderBeforeUndoAlert = self.isFirstResponder; + __weak typeof(self) _self = self; + NSArray *strings = [self _localizedUndoStrings]; + BOOL canUndo = [self _canUndo]; + BOOL canRedo = [self _canRedo]; + + UIViewController *ctrl = [self _getRootViewController]; + + if (canUndo && canRedo) { + if (kiOS8Later) { + UIAlertController *alert = [UIAlertController alertControllerWithTitle:strings[4] message:@"" preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:strings[3] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + [_self _undo]; + [_self _restoreFirstResponderAfterUndoAlert]; + }]]; + [alert addAction:[UIAlertAction actionWithTitle:strings[2] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + [_self _redo]; + [_self _restoreFirstResponderAfterUndoAlert]; + }]]; + [alert addAction:[UIAlertAction actionWithTitle:strings[0] style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { + [_self _restoreFirstResponderAfterUndoAlert]; + }]]; + [ctrl presentViewController:alert animated:YES completion:nil]; + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:strings[4] message:@"" delegate:self cancelButtonTitle:strings[0] otherButtonTitles:strings[3], strings[2], nil]; + [alert show]; +#pragma clang diagnostic pop + } + } else if (canUndo) { + if (kiOS8Later) { + UIAlertController *alert = [UIAlertController alertControllerWithTitle:strings[4] message:@"" preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:strings[3] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + [_self _undo]; + [_self _restoreFirstResponderAfterUndoAlert]; + }]]; + [alert addAction:[UIAlertAction actionWithTitle:strings[0] style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { + [_self _restoreFirstResponderAfterUndoAlert]; + }]]; + [ctrl presentViewController:alert animated:YES completion:nil]; + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:strings[4] message:@"" delegate:self cancelButtonTitle:strings[0] otherButtonTitles:strings[3], nil]; + [alert show]; +#pragma clang diagnostic pop + } + } else if (canRedo) { + if (kiOS8Later) { + UIAlertController *alert = [UIAlertController alertControllerWithTitle:strings[2] message:@"" preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:strings[1] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + [_self _redo]; + [_self _restoreFirstResponderAfterUndoAlert]; + }]]; + [alert addAction:[UIAlertAction actionWithTitle:strings[0] style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { + [_self _restoreFirstResponderAfterUndoAlert]; + }]]; + [ctrl presentViewController:alert animated:YES completion:nil]; + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:strings[2] message:@"" delegate:self cancelButtonTitle:strings[0] otherButtonTitles:strings[1], nil]; + [alert show]; +#pragma clang diagnostic pop + } + } +} +#endif + +/// Get the localized undo alert strings based on app's main bundle. +- (NSArray *)_localizedUndoStrings { + static NSArray *strings = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSDictionary *dic = @{ + @"ar" : @[ @"����������", @"����������", @"���������� ��������������", @"����������", @"���������� ���� ��������������" ], + @"ca" : @[ @"Cancel��lar", @"Refer", @"Refer l���escriptura", @"Desfer", @"Desfer l���escriptura" ], + @"cs" : @[ @"Zru��it", @"Opakovat akci", @"Opakovat akci Ps��t", @"Odvolat akci", @"Odvolat akci Ps��t" ], + @"da" : @[ @"Annuller", @"Gentag", @"Gentag Indtastning", @"Fortryd", @"Fortryd Indtastning" ], + @"de" : @[ @"Abbrechen", @"Wiederholen", @"Eingabe wiederholen", @"Widerrufen", @"Eingabe widerrufen" ], + @"el" : @[ @"��������������", @"������������������", @"������������������ ����������������������������", @"����������������", @"���������������� ����������������������������" ], + @"en" : @[ @"Cancel", @"Redo", @"Redo Typing", @"Undo", @"Undo Typing" ], + @"es" : @[ @"Cancelar", @"Rehacer", @"Rehacer escritura", @"Deshacer", @"Deshacer escritura" ], + @"es_MX" : @[ @"Cancelar", @"Rehacer", @"Rehacer escritura", @"Deshacer", @"Deshacer escritura" ], + @"fi" : @[ @"Kumoa", @"Tee sittenkin", @"Kirjoita sittenkin", @"Peru", @"Peru kirjoitus" ], + @"fr" : @[ @"Annuler", @"R��tablir", @"R��tablir la saisie", @"Annuler", @"Annuler la saisie" ], + @"he" : @[ @"����������", @"�������� ���� ������������ ��������������", @"�������� ���� ����������", @"������", @"������ ����������" ], + @"hr" : @[ @"Odustani", @"Ponovi", @"Ponovno upi��i", @"Poni��ti", @"Poni��ti upisivanje" ], + @"hu" : @[ @"M��gsem", @"Ism��tl��s", @"G��pel��s ism��tl��se", @"Visszavon��s", @"G��pel��s visszavon��sa" ], + @"id" : @[ @"Batalkan", @"Ulang", @"Ulang Pengetikan", @"Kembalikan", @"Batalkan Pengetikan" ], + @"it" : @[ @"Annulla", @"Ripristina originale", @"Ripristina Inserimento", @"Annulla", @"Annulla Inserimento" ], + @"ja" : @[ @"���������������", @"������������", @"������������ - ������", @"������������", @"������������ - ������" ], + @"ko" : @[ @"������", @"������ ������", @"������ ������", @"������ ������", @"������ ������ ������" ], + @"ms" : @[ @"Batal", @"Buat semula", @"Ulang Penaipan", @"Buat asal", @"Buat asal Penaipan" ], + @"nb" : @[ @"Avbryt", @"Utf��r likevel", @"Utf��r skriving likevel", @"Angre", @"Angre skriving" ], + @"nl" : @[ @"Annuleer", @"Opnieuw", @"Opnieuw typen", @"Herstel", @"Herstel typen" ], + @"pl" : @[ @"Anuluj", @"Przywr����", @"Przywr���� Wpisz", @"Cofnij", @"Cofnij Wpisz" ], + @"pt" : @[ @"Cancelar", @"Refazer", @"Refazer Digita����o", @"Desfazer", @"Desfazer Digita����o" ], + @"pt_PT" : @[ @"Cancelar", @"Refazer", @"Refazer digitar", @"Desfazer", @"Desfazer digitar" ], + @"ro" : @[ @"Renun����", @"Ref��", @"Ref�� tastare", @"Anuleaz��", @"Anuleaz�� tastare" ], + @"ru" : @[ @"����������������", @"������������������", @"������������������ ���������� ���� ��������������������", @"����������������", @"���������������� ���������� ���� ��������������������" ], + @"sk" : @[ @"Zru��i��", @"Obnovi��", @"Obnovi�� p��sanie", @"Odvola��", @"Odvola�� p��sanie" ], + @"sv" : @[ @"Avbryt", @"G��r om", @"G��r om skriven text", @"��ngra", @"��ngra skriven text" ], + @"th" : @[ @"������������������", @"������������������������������������", @"������������������������������������������", @"������������������", @"������������������������" ], + @"tr" : @[ @"Vazge��", @"Yinele", @"Yazmay�� Yinele", @"Geri Al", @"Yazmay�� Geri Al" ], + @"uk" : @[ @"������������������", @"������������������", @"������������������ ����������������", @"������������������", @"������������������ ����������������" ], + @"vi" : @[ @"H���y", @"L��m l���i", @"L��m l���i thao t��c Nh���p", @"Ho��n t��c", @"Ho��n t��c thao t��c Nh���p" ], + @"zh" : @[ @"������", @"������", @"������������", @"������", @"������������" ], + @"zh_CN" : @[ @"������", @"������", @"������������", @"������", @"������������" ], + @"zh_HK" : @[ @"������", @"������", @"������������", @"������", @"������������" ], + @"zh_TW" : @[ @"������", @"������", @"������������", @"������", @"������������" ] + }; + NSString *preferred = [[NSBundle mainBundle] preferredLocalizations].firstObject; + if (preferred.length == 0) preferred = @"English"; + NSString *canonical = [NSLocale canonicalLocaleIdentifierFromString:preferred]; + if (canonical.length == 0) canonical = @"en"; + strings = dic[canonical]; + if (!strings && ([canonical rangeOfString:@"_"].location != NSNotFound)) { + NSString *prefix = [canonical componentsSeparatedByString:@"_"].firstObject; + if (prefix.length) strings = dic[prefix]; + } + if (!strings) strings = dic[@"en"]; + }); + return strings; +} + +/// Returns the default font for text view (same as CoreText). +- (UIFont *)_defaultFont { + return [UIFont systemFontOfSize:12]; +} + +/// Returns the default tint color for text view (used for caret and select range background). +- (UIColor *)_defaultTintColor { + return [UIColor colorWithRed:69/255.0 green:111/255.0 blue:238/255.0 alpha:1]; +} + +/// Returns the default placeholder color for text view (same as UITextField). +- (UIColor *)_defaultPlaceholderColor { + return [UIColor colorWithRed:0 green:0 blue:25/255.0 alpha:44/255.0]; +} + +#pragma mark - Private Setter + +- (void)_setText:(NSString *)text { + if (_text == text || [_text isEqualToString:text]) return; + [self willChangeValueForKey:@"text"]; + _text = text.copy; + if (!_text) _text = @""; + [self didChangeValueForKey:@"text"]; + self.accessibilityLabel = _text; +} + +- (void)_setFont:(UIFont *)font { + if (_font == font || [_font isEqual:font]) return; + [self willChangeValueForKey:@"font"]; + _font = font; + [self didChangeValueForKey:@"font"]; +} + +- (void)_setTextColor:(UIColor *)textColor { + if (_textColor == textColor) return; + if (_textColor && textColor) { + if (CFGetTypeID(_textColor.CGColor) == CFGetTypeID(textColor.CGColor) && + CFGetTypeID(_textColor.CGColor) == CGColorGetTypeID()) { + if ([_textColor isEqual:textColor]) { + return; + } + } + } + [self willChangeValueForKey:@"textColor"]; + _textColor = textColor; + [self didChangeValueForKey:@"textColor"]; +} + +- (void)_setTextAlignment:(NSTextAlignment)textAlignment { + if (_textAlignment == textAlignment) return; + [self willChangeValueForKey:@"textAlignment"]; + _textAlignment = textAlignment; + [self didChangeValueForKey:@"textAlignment"]; +} + +- (void)_setDataDetectorTypes:(UIDataDetectorTypes)dataDetectorTypes { + if (_dataDetectorTypes == dataDetectorTypes) return; + [self willChangeValueForKey:@"dataDetectorTypes"]; + _dataDetectorTypes = dataDetectorTypes; + [self didChangeValueForKey:@"dataDetectorTypes"]; +} + +- (void)_setLinkTextAttributes:(NSDictionary *)linkTextAttributes { + if (_linkTextAttributes == linkTextAttributes || [_linkTextAttributes isEqual:linkTextAttributes]) return; + [self willChangeValueForKey:@"linkTextAttributes"]; + _linkTextAttributes = linkTextAttributes.copy; + [self didChangeValueForKey:@"linkTextAttributes"]; +} + +- (void)_setHighlightTextAttributes:(NSDictionary *)highlightTextAttributes { + if (_highlightTextAttributes == highlightTextAttributes || [_highlightTextAttributes isEqual:highlightTextAttributes]) return; + [self willChangeValueForKey:@"highlightTextAttributes"]; + _highlightTextAttributes = highlightTextAttributes.copy; + [self didChangeValueForKey:@"highlightTextAttributes"]; +} +- (void)_setTextParser:(id<YYTextParser>)textParser { + if (_textParser == textParser || [_textParser isEqual:textParser]) return; + [self willChangeValueForKey:@"textParser"]; + _textParser = textParser; + [self didChangeValueForKey:@"textParser"]; +} + +- (void)_setAttributedText:(NSAttributedString *)attributedText { + if (_attributedText == attributedText || [_attributedText isEqual:attributedText]) return; + [self willChangeValueForKey:@"attributedText"]; + _attributedText = attributedText.copy; + if (!_attributedText) _attributedText = [NSAttributedString new]; + [self didChangeValueForKey:@"attributedText"]; +} + +- (void)_setTextContainerInset:(UIEdgeInsets)textContainerInset { + if (UIEdgeInsetsEqualToEdgeInsets(_textContainerInset, textContainerInset)) return; + [self willChangeValueForKey:@"textContainerInset"]; + _textContainerInset = textContainerInset; + [self didChangeValueForKey:@"textContainerInset"]; +} + +- (void)_setExclusionPaths:(NSArray *)exclusionPaths { + if (_exclusionPaths == exclusionPaths || [_exclusionPaths isEqual:exclusionPaths]) return; + [self willChangeValueForKey:@"exclusionPaths"]; + _exclusionPaths = exclusionPaths.copy; + [self didChangeValueForKey:@"exclusionPaths"]; +} + +- (void)_setVerticalForm:(BOOL)verticalForm { + if (_verticalForm == verticalForm) return; + [self willChangeValueForKey:@"verticalForm"]; + _verticalForm = verticalForm; + [self didChangeValueForKey:@"verticalForm"]; +} + +- (void)_setLinePositionModifier:(id<YYTextLinePositionModifier>)linePositionModifier { + if (_linePositionModifier == linePositionModifier) return; + [self willChangeValueForKey:@"linePositionModifier"]; + _linePositionModifier = [(NSObject *)linePositionModifier copy]; + [self didChangeValueForKey:@"linePositionModifier"]; +} + +- (void)_setSelectedRange:(NSRange)selectedRange { + if (NSEqualRanges(_selectedRange, selectedRange)) return; + [self willChangeValueForKey:@"selectedRange"]; + _selectedRange = selectedRange; + [self didChangeValueForKey:@"selectedRange"]; + if ([self.delegate respondsToSelector:@selector(textViewDidChangeSelection:)]) { + [self.delegate textViewDidChangeSelection:self]; + } +} + +- (void)_setTypingAttributes:(NSDictionary *)typingAttributes { + if (_typingAttributes == typingAttributes || [_typingAttributes isEqual:typingAttributes]) return; + [self willChangeValueForKey:@"typingAttributes"]; + _typingAttributes = typingAttributes.copy; + [self didChangeValueForKey:@"typingAttributes"]; +} + +#pragma mark - Private Init + +- (void)_initTextView { + self.delaysContentTouches = NO; + self.canCancelContentTouches = YES; + self.multipleTouchEnabled = NO; + self.clipsToBounds = YES; + [super setDelegate:self]; + + _text = @""; + _attributedText = [NSAttributedString new]; + + // UITextInputTraits + _autocapitalizationType = UITextAutocapitalizationTypeSentences; + _autocorrectionType = UITextAutocorrectionTypeDefault; + _spellCheckingType = UITextSpellCheckingTypeDefault; + _keyboardType = UIKeyboardTypeDefault; + _keyboardAppearance = UIKeyboardAppearanceDefault; + _returnKeyType = UIReturnKeyDefault; + _enablesReturnKeyAutomatically = NO; + _secureTextEntry = NO; + + // UITextInput + _selectedTextRange = [YYTextRange defaultRange]; + _markedTextRange = nil; + _markedTextStyle = nil; + _tokenizer = [[UITextInputStringTokenizer alloc] initWithTextInput:self]; + + _editable = YES; + _selectable = YES; + _highlightable = YES; + _allowsCopyAttributedString = YES; + _textAlignment = NSTextAlignmentNatural; + + _innerText = [NSMutableAttributedString new]; + _innerContainer = [YYTextContainer new]; + _innerContainer.insets = kDefaultInset; + _textContainerInset = kDefaultInset; + _typingAttributesHolder = [[NSMutableAttributedString alloc] initWithString:@" "]; + _linkTextAttributes = @{NSForegroundColorAttributeName : [self _defaultTintColor], + (id)kCTForegroundColorAttributeName : (id)[self _defaultTintColor].CGColor}; + + YYTextHighlight *highlight = [YYTextHighlight new]; + YYTextBorder * border = [YYTextBorder new]; + border.insets = UIEdgeInsetsMake(-2, -2, -2, -2); + border.fillColor = [UIColor colorWithWhite:0.1 alpha:0.2]; + border.cornerRadius = 3; + [highlight setBorder:border]; + _highlightTextAttributes = highlight.attributes.copy; + + _placeHolderView = [UIImageView new]; + _placeHolderView.userInteractionEnabled = NO; + _placeHolderView.hidden = YES; + + _containerView = [YYTextContainerView new]; + _containerView.hostView = self; + + _selectionView = [YYTextSelectionView new]; + _selectionView.userInteractionEnabled = NO; + _selectionView.hostView = self; + _selectionView.color = [self _defaultTintColor]; + + _magnifierCaret = [YYTextMagnifier magnifierWithType:YYTextMagnifierTypeCaret]; + _magnifierCaret.hostView = _containerView; + _magnifierRanged = [YYTextMagnifier magnifierWithType:YYTextMagnifierTypeRanged]; + _magnifierRanged.hostView = _containerView; + + [self addSubview:_placeHolderView]; + [self addSubview:_containerView]; + [self addSubview:_selectionView]; + + _undoStack = [NSMutableArray new]; + _redoStack = [NSMutableArray new]; + _allowsUndoAndRedo = YES; + _maximumUndoLevel = kDefaultUndoLevelMax; + + self.debugOption = [YYTextDebugOption sharedDebugOption]; + [YYTextDebugOption addDebugTarget:self]; + + [self _updateInnerContainerSize]; + [self _update]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_pasteboardChanged) name:UIPasteboardChangedNotification object:nil]; + [[YYTextKeyboardManager defaultManager] addObserver:self]; + + self.isAccessibilityElement = YES; +} + +#pragma mark - Public + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (!self) return nil; + [self _initTextView]; + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIPasteboardChangedNotification object:nil]; + [[YYTextKeyboardManager defaultManager] removeObserver:self]; + + [[YYTextEffectWindow sharedWindow] hideSelectionDot:_selectionView]; + [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierCaret]; + [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierRanged]; + + [YYTextDebugOption removeDebugTarget:self]; + + [_longPressTimer invalidate]; + [_autoScrollTimer invalidate]; + [_selectionDotFixTimer invalidate]; +} + +- (void)scrollRangeToVisible:(NSRange)range { + YYTextRange *textRange = [YYTextRange rangeWithRange:range]; + textRange = [self _correctedTextRange:textRange]; + [self _scrollRangeToVisible:textRange]; +} + +#pragma mark - Property + +- (void)setText:(NSString *)text { + if (_text == text || [_text isEqualToString:text]) return; + [self _setText:text]; + + _state.selectedWithoutEdit = NO; + _state.deleteConfirm = NO; + [self _endTouchTracking]; + [self _hideMenu]; + [self _resetUndoAndRedoStack]; + [self replaceRange:[YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)] withText:text]; +} + +- (void)setFont:(UIFont *)font { + if (_font == font || [_font isEqual:font]) return; + [self _setFont:font]; + + _state.typingAttributesOnce = NO; + _typingAttributesHolder.yy_font = font; + _innerText.yy_font = font; + [self _resetUndoAndRedoStack]; + [self _commitUpdate]; +} + +- (void)setTextColor:(UIColor *)textColor { + if (_textColor == textColor || [_textColor isEqual:textColor]) return; + [self _setTextColor:textColor]; + + _state.typingAttributesOnce = NO; + _typingAttributesHolder.yy_color = textColor; + _innerText.yy_color = textColor; + [self _resetUndoAndRedoStack]; + [self _commitUpdate]; +} + +- (void)setTextAlignment:(NSTextAlignment)textAlignment { + if (_textAlignment == textAlignment) return; + [self _setTextAlignment:textAlignment]; + + _typingAttributesHolder.yy_alignment = textAlignment; + _innerText.yy_alignment = textAlignment; + [self _resetUndoAndRedoStack]; + [self _commitUpdate]; +} + +- (void)setDataDetectorTypes:(UIDataDetectorTypes)dataDetectorTypes { + if (_dataDetectorTypes == dataDetectorTypes) return; + [self _setDataDetectorTypes:dataDetectorTypes]; + NSTextCheckingType type = YYTextNSTextCheckingTypeFromUIDataDetectorType(dataDetectorTypes); + _dataDetector = type ? [NSDataDetector dataDetectorWithTypes:type error:NULL] : nil; + [self _resetUndoAndRedoStack]; + [self _commitUpdate]; +} + +- (void)setLinkTextAttributes:(NSDictionary *)linkTextAttributes { + if (_linkTextAttributes == linkTextAttributes || [_linkTextAttributes isEqual:linkTextAttributes]) return; + [self _setLinkTextAttributes:linkTextAttributes]; + if (_dataDetector) { + [self _commitUpdate]; + } +} + +- (void)setHighlightTextAttributes:(NSDictionary *)highlightTextAttributes { + if (_highlightTextAttributes == highlightTextAttributes || [_highlightTextAttributes isEqual:highlightTextAttributes]) return; + [self _setHighlightTextAttributes:highlightTextAttributes]; + if (_dataDetector) { + [self _commitUpdate]; + } +} + +- (void)setTextParser:(id<YYTextParser>)textParser { + if (_textParser == textParser || [_textParser isEqual:textParser]) return; + [self _setTextParser:textParser]; + if (textParser && _text.length) { + [self replaceRange:[YYTextRange rangeWithRange:NSMakeRange(0, _text.length)] withText:_text]; + } + [self _resetUndoAndRedoStack]; + [self _commitUpdate]; +} + +- (void)setTypingAttributes:(NSDictionary *)typingAttributes { + [self _setTypingAttributes:typingAttributes]; + _state.typingAttributesOnce = YES; + [typingAttributes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + [_typingAttributesHolder yy_setAttribute:key value:obj]; + }]; + [self _commitUpdate]; +} + +- (void)setAttributedText:(NSAttributedString *)attributedText { + if (_attributedText == attributedText) return; + [self _setAttributedText:attributedText]; + _state.typingAttributesOnce = NO; + + NSMutableAttributedString *text = attributedText.mutableCopy; + if (text.length == 0) { + [self replaceRange:[YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)] withText:@""]; + return; + } + if ([self.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:)]) { + BOOL should = [self.delegate textView:self shouldChangeTextInRange:NSMakeRange(0, _innerText.length) replacementText:text.string]; + if (!should) return; + } + + _state.selectedWithoutEdit = NO; + _state.deleteConfirm = NO; + [self _endTouchTracking]; + [self _hideMenu]; + + [_inputDelegate selectionWillChange:self]; + [_inputDelegate textWillChange:self]; + _innerText = text; + [self _parseText]; + _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)]; + [_inputDelegate textDidChange:self]; + [_inputDelegate selectionDidChange:self]; + + [self _setAttributedText:text]; + if (_innerText.length > 0) { + _typingAttributesHolder.yy_attributes = [_innerText yy_attributesAtIndex:_innerText.length - 1]; + } + + [self _updateOuterProperties]; + [self _updateLayout]; + [self _updateSelectionView]; + + if (self.isFirstResponder) { + [self _scrollRangeToVisible:_selectedTextRange]; + } + + if ([self.delegate respondsToSelector:@selector(textViewDidChange:)]) { + [self.delegate textViewDidChange:self]; + } + [[NSNotificationCenter defaultCenter] postNotificationName:YYTextViewTextDidChangeNotification object:self]; + + if (!_state.insideUndoBlock) { + [self _resetUndoAndRedoStack]; + } +} + +- (void)setTextVerticalAlignment:(YYTextVerticalAlignment)textVerticalAlignment { + if (_textVerticalAlignment == textVerticalAlignment) return; + [self willChangeValueForKey:@"textVerticalAlignment"]; + _textVerticalAlignment = textVerticalAlignment; + [self didChangeValueForKey:@"textVerticalAlignment"]; + _containerView.textVerticalAlignment = textVerticalAlignment; + [self _commitUpdate]; +} + +- (void)setTextContainerInset:(UIEdgeInsets)textContainerInset { + if (UIEdgeInsetsEqualToEdgeInsets(_textContainerInset, textContainerInset)) return; + [self _setTextContainerInset:textContainerInset]; + _innerContainer.insets = textContainerInset; + [self _commitUpdate]; +} + +- (void)setExclusionPaths:(NSArray *)exclusionPaths { + if (_exclusionPaths == exclusionPaths || [_exclusionPaths isEqual:exclusionPaths]) return; + [self _setExclusionPaths:exclusionPaths]; + _innerContainer.exclusionPaths = exclusionPaths; + if (_innerContainer.isVerticalForm) { + CGAffineTransform trans = CGAffineTransformMakeTranslation(_innerContainer.size.width - self.bounds.size.width, 0); + [_innerContainer.exclusionPaths enumerateObjectsUsingBlock:^(UIBezierPath *path, NSUInteger idx, BOOL *stop) { + [path applyTransform:trans]; + }]; + } + [self _commitUpdate]; +} + +- (void)setVerticalForm:(BOOL)verticalForm { + if (_verticalForm == verticalForm) return; + [self _setVerticalForm:verticalForm]; + _innerContainer.verticalForm = verticalForm; + _selectionView.verticalForm = verticalForm; + + [self _updateInnerContainerSize]; + + if (verticalForm) { + if (UIEdgeInsetsEqualToEdgeInsets(_innerContainer.insets, kDefaultInset)) { + _innerContainer.insets = kDefaultVerticalInset; + [self _setTextContainerInset:kDefaultVerticalInset]; + } + } else { + if (UIEdgeInsetsEqualToEdgeInsets(_innerContainer.insets, kDefaultVerticalInset)) { + _innerContainer.insets = kDefaultInset; + [self _setTextContainerInset:kDefaultInset]; + } + } + + _innerContainer.exclusionPaths = _exclusionPaths; + if (verticalForm) { + CGAffineTransform trans = CGAffineTransformMakeTranslation(_innerContainer.size.width - self.bounds.size.width, 0); + [_innerContainer.exclusionPaths enumerateObjectsUsingBlock:^(UIBezierPath *path, NSUInteger idx, BOOL *stop) { + [path applyTransform:trans]; + }]; + } + + [self _keyboardChanged]; + [self _commitUpdate]; +} + +- (void)setLinePositionModifier:(id<YYTextLinePositionModifier>)linePositionModifier { + if (_linePositionModifier == linePositionModifier) return; + [self _setLinePositionModifier:linePositionModifier]; + _innerContainer.linePositionModifier = linePositionModifier; + [self _commitUpdate]; +} + +- (void)setSelectedRange:(NSRange)selectedRange { + if (NSEqualRanges(_selectedRange, selectedRange)) return; + if (_markedTextRange) return; + _state.typingAttributesOnce = NO; + + YYTextRange *range = [YYTextRange rangeWithRange:selectedRange]; + range = [self _correctedTextRange:range]; + [self _endTouchTracking]; + _selectedTextRange = range; + [self _updateSelectionView]; + + [self _setSelectedRange:range.asRange]; + + if (!_state.insideUndoBlock) { + [self _resetUndoAndRedoStack]; + } +} + +- (void)setHighlightable:(BOOL)highlightable { + if (_highlightable == highlightable) return; + [self willChangeValueForKey:@"highlightable"]; + _highlightable = highlightable; + [self didChangeValueForKey:@"highlightable"]; + [self _commitUpdate]; +} + +- (void)setEditable:(BOOL)editable { + if (_editable == editable) return; + [self willChangeValueForKey:@"editable"]; + _editable = editable; + [self didChangeValueForKey:@"editable"]; + if (!editable) { + [self resignFirstResponder]; + } +} + +- (void)setSelectable:(BOOL)selectable { + if (_selectable == selectable) return; + [self willChangeValueForKey:@"selectable"]; + _selectable = selectable; + [self didChangeValueForKey:@"selectable"]; + if (!selectable) { + if (self.isFirstResponder) { + [self resignFirstResponder]; + } else { + _state.selectedWithoutEdit = NO; + [self _endTouchTracking]; + [self _hideMenu]; + [self _updateSelectionView]; + } + } +} + +- (void)setClearsOnInsertion:(BOOL)clearsOnInsertion { + if (_clearsOnInsertion == clearsOnInsertion) return; + _clearsOnInsertion = clearsOnInsertion; + if (clearsOnInsertion) { + if (self.isFirstResponder) { + self.selectedRange = NSMakeRange(0, _attributedText.length); + } else { + _state.clearsOnInsertionOnce = YES; + } + } +} + +- (void)setDebugOption:(YYTextDebugOption *)debugOption { + _containerView.debugOption = debugOption; +} + +- (YYTextDebugOption *)debugOption { + return _containerView.debugOption; +} + +- (YYTextLayout *)textLayout { + [self _updateIfNeeded]; + return _innerLayout; +} + +- (void)setPlaceholderText:(NSString *)placeholderText { + if (_placeholderAttributedText.length > 0) { + if (placeholderText.length > 0) { + [((NSMutableAttributedString *)_placeholderAttributedText) replaceCharactersInRange:NSMakeRange(0, _placeholderAttributedText.length) withString:placeholderText]; + } else { + [((NSMutableAttributedString *)_placeholderAttributedText) replaceCharactersInRange:NSMakeRange(0, _placeholderAttributedText.length) withString:@""]; + } + ((NSMutableAttributedString *)_placeholderAttributedText).yy_font = _placeholderFont; + ((NSMutableAttributedString *)_placeholderAttributedText).yy_color = _placeholderTextColor; + } else { + if (placeholderText.length > 0) { + NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:placeholderText]; + if (!_placeholderFont) _placeholderFont = _font; + if (!_placeholderFont) _placeholderFont = [self _defaultFont]; + if (!_placeholderTextColor) _placeholderTextColor = [self _defaultPlaceholderColor]; + atr.yy_font = _placeholderFont; + atr.yy_color = _placeholderTextColor; + _placeholderAttributedText = atr; + } + } + _placeholderText = [_placeholderAttributedText yy_plainTextForRange:NSMakeRange(0, _placeholderAttributedText.length)]; + [self _commitPlaceholderUpdate]; +} + +- (void)setPlaceholderFont:(UIFont *)placeholderFont { + _placeholderFont = placeholderFont; + ((NSMutableAttributedString *)_placeholderAttributedText).yy_font = _placeholderFont; + [self _commitPlaceholderUpdate]; +} + +- (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor { + _placeholderTextColor = placeholderTextColor; + ((NSMutableAttributedString *)_placeholderAttributedText).yy_color = _placeholderTextColor; + [self _commitPlaceholderUpdate]; +} + +- (void)setPlaceholderAttributedText:(NSAttributedString *)placeholderAttributedText { + _placeholderAttributedText = placeholderAttributedText.mutableCopy; + _placeholderText = [_placeholderAttributedText yy_plainTextForRange:NSMakeRange(0, _placeholderAttributedText.length)]; + _placeholderFont = _placeholderAttributedText.yy_font; + _placeholderTextColor = _placeholderAttributedText.yy_color; + [self _commitPlaceholderUpdate]; +} + +#pragma mark - Override For Protect + +- (void)setMultipleTouchEnabled:(BOOL)multipleTouchEnabled { + [super setMultipleTouchEnabled:NO]; // must not enabled +} + +- (void)setContentInset:(UIEdgeInsets)contentInset { + UIEdgeInsets oldInsets = self.contentInset; + if (_insetModifiedByKeyboard) { + _originalContentInset = contentInset; + } else { + [super setContentInset:contentInset]; + BOOL changed = !UIEdgeInsetsEqualToEdgeInsets(oldInsets, contentInset); + if (changed) { + [self _updateInnerContainerSize]; + [self _commitUpdate]; + [self _commitPlaceholderUpdate]; + } + } +} + +- (void)setScrollIndicatorInsets:(UIEdgeInsets)scrollIndicatorInsets { + if (_insetModifiedByKeyboard) { + _originalScrollIndicatorInsets = scrollIndicatorInsets; + } else { + [super setScrollIndicatorInsets:scrollIndicatorInsets]; + } +} + +- (void)setFrame:(CGRect)frame { + CGSize oldSize = self.bounds.size; + [super setFrame:frame]; + CGSize newSize = self.bounds.size; + BOOL changed = _innerContainer.isVerticalForm ? (oldSize.height != newSize.height) : (oldSize.width != newSize.width); + if (changed) { + [self _updateInnerContainerSize]; + [self _commitUpdate]; + } + if (!CGSizeEqualToSize(oldSize, newSize)) { + [self _commitPlaceholderUpdate]; + } +} + +- (void)setBounds:(CGRect)bounds { + CGSize oldSize = self.bounds.size; + [super setBounds:bounds]; + CGSize newSize = self.bounds.size; + BOOL changed = _innerContainer.isVerticalForm ? (oldSize.height != newSize.height) : (oldSize.width != newSize.width); + if (changed) { + [self _updateInnerContainerSize]; + [self _commitUpdate]; + } + if (!CGSizeEqualToSize(oldSize, newSize)) { + [self _commitPlaceholderUpdate]; + } +} + +- (void)tintColorDidChange { + if ([self respondsToSelector:@selector(tintColor)]) { + UIColor *color = self.tintColor; + NSMutableDictionary *attrs = _highlightTextAttributes.mutableCopy; + NSMutableDictionary *linkAttrs = _linkTextAttributes.mutableCopy; + if (!linkAttrs) linkAttrs = @{}.mutableCopy; + if (!color) { + [attrs removeObjectForKey:NSForegroundColorAttributeName]; + [attrs removeObjectForKey:(id)kCTForegroundColorAttributeName]; + [linkAttrs setObject:[self _defaultTintColor] forKey:NSForegroundColorAttributeName]; + [linkAttrs setObject:(id)[self _defaultTintColor].CGColor forKey:(id)kCTForegroundColorAttributeName]; + } else { + [attrs setObject:color forKey:NSForegroundColorAttributeName]; + [attrs setObject:(id)color.CGColor forKey:(id)kCTForegroundColorAttributeName]; + [linkAttrs setObject:color forKey:NSForegroundColorAttributeName]; + [linkAttrs setObject:(id)color.CGColor forKey:(id)kCTForegroundColorAttributeName]; + } + self.highlightTextAttributes = attrs; + _selectionView.color = color ? color : [self _defaultTintColor]; + _linkTextAttributes = linkAttrs; + [self _commitUpdate]; + } +} + +- (CGSize)sizeThatFits:(CGSize)size { + if (!_verticalForm && size.width <= 0) size.width = YYTextContainerMaxSize.width; + if (_verticalForm && size.height <= 0) size.height = YYTextContainerMaxSize.height; + + if ((!_verticalForm && size.width == self.bounds.size.width) || + (_verticalForm && size.height == self.bounds.size.height)) { + [self _updateIfNeeded]; + if (!_verticalForm) { + if (_containerView.bounds.size.height <= size.height) { + return _containerView.bounds.size; + } + } else { + if (_containerView.bounds.size.width <= size.width) { + return _containerView.bounds.size; + } + } + } + + if (!_verticalForm) { + size.height = YYTextContainerMaxSize.height; + } else { + size.width = YYTextContainerMaxSize.width; + } + + YYTextContainer *container = [_innerContainer copy]; + container.size = size; + + YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:_innerText]; + return layout.textBoundingSize; +} + +#pragma mark - Override UIResponder + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { + [self _updateIfNeeded]; + UITouch *touch = touches.anyObject; + CGPoint point = [touch locationInView:_containerView]; + + _touchBeganTime = _trackingTime = touch.timestamp; + _touchBeganPoint = _trackingPoint = point; + _trackingRange = _selectedTextRange; + + _state.trackingGrabber = NO; + _state.trackingCaret = NO; + _state.trackingPreSelect = NO; + _state.trackingTouch = YES; + _state.swallowTouch = YES; + _state.touchMoved = NO; + + if (!self.isFirstResponder && !_state.selectedWithoutEdit && self.highlightable) { + _highlight = [self _getHighlightAtPoint:point range:&_highlightRange]; + _highlightLayout = nil; + } + + if ((!self.selectable && !_highlight) || _state.ignoreTouchBegan) { + _state.swallowTouch = NO; + _state.trackingTouch = NO; + } + + if (_state.trackingTouch) { + [self _startLongPressTimer]; + if (_highlight) { + [self _showHighlightAnimated:NO]; + } else { + if ([_selectionView isGrabberContainsPoint:point]) { // track grabber + self.panGestureRecognizer.enabled = NO; // disable scroll view + [self _hideMenu]; + _state.trackingGrabber = [_selectionView isStartGrabberContainsPoint:point] ? kStart : kEnd; + _magnifierRangedOffset = [self _getMagnifierRangedOffset]; + } else { + if (_selectedTextRange.asRange.length == 0 && self.isFirstResponder) { + if ([_selectionView isCaretContainsPoint:point]) { // track caret + _state.trackingCaret = YES; + self.panGestureRecognizer.enabled = NO; // disable scroll view + } + } + } + } + [self _updateSelectionView]; + } + + if (!_state.swallowTouch) [super touchesBegan:touches withEvent:event]; +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { + [self _updateIfNeeded]; + UITouch *touch = touches.anyObject; + CGPoint point = [touch locationInView:_containerView]; + + _trackingTime = touch.timestamp; + _trackingPoint = point; + + if (!_state.touchMoved) { + _state.touchMoved = [self _getMoveDirection]; + if (_state.touchMoved) [self _endLongPressTimer]; + } + _state.clearsOnInsertionOnce = NO; + + if (_state.trackingTouch) { + BOOL showMagnifierCaret = NO; + BOOL showMagnifierRanged = NO; + + if (_highlight) { + + YYTextHighlight *highlight = [self _getHighlightAtPoint:_trackingPoint range:NULL]; + if (highlight == _highlight) { + [self _showHighlightAnimated:YES]; + } else { + [self _hideHighlightAnimated:YES]; + } + + } else { + _trackingRange = _selectedTextRange; + if (_state.trackingGrabber) { + self.panGestureRecognizer.enabled = NO; + [self _hideMenu]; + [self _updateTextRangeByTrackingGrabber]; + showMagnifierRanged = YES; + } else if (_state.trackingPreSelect) { + [self _updateTextRangeByTrackingPreSelect]; + showMagnifierCaret = YES; + } else if (_state.trackingCaret || _markedTextRange || self.isFirstResponder) { + if (_state.trackingCaret || _state.touchMoved) { + _state.trackingCaret = YES; + [self _hideMenu]; + if (_verticalForm) { + if (_state.touchMoved == kTop || _state.touchMoved == kBottom) { + self.panGestureRecognizer.enabled = NO; + } + } else { + if (_state.touchMoved == kLeft || _state.touchMoved == kRight) { + self.panGestureRecognizer.enabled = NO; + } + } + [self _updateTextRangeByTrackingCaret]; + if (_markedTextRange) { + showMagnifierRanged = YES; + } else { + showMagnifierCaret = YES; + } + } + } + } + [self _updateSelectionView]; + if (showMagnifierCaret) [self _showMagnifierCaret]; + if (showMagnifierRanged) [self _showMagnifierRanged]; + } + + CGFloat autoScrollOffset = [self _getAutoscrollOffset]; + if (_autoScrollOffset != autoScrollOffset) { + if (fabs(autoScrollOffset) < fabs(_autoScrollOffset)) { + _autoScrollAcceleration *= 0.5; + } + _autoScrollOffset = autoScrollOffset; + if (_autoScrollOffset != 0 && _state.touchMoved) { + [self _startAutoScrollTimer]; + } + } + + if (!_state.swallowTouch) [super touchesMoved:touches withEvent:event]; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { + [self _updateIfNeeded]; + + UITouch *touch = touches.anyObject; + CGPoint point = [touch locationInView:_containerView]; + + _trackingTime = touch.timestamp; + _trackingPoint = point; + + if (!_state.touchMoved) { + _state.touchMoved = [self _getMoveDirection]; + } + if (_state.trackingTouch) { + [self _hideMagnifier]; + + if (_highlight) { + if (_state.showingHighlight) { + if (_highlight.tapAction) { + CGRect rect = [_innerLayout rectForRange:[YYTextRange rangeWithRange:_highlightRange]]; + rect = [self _convertRectFromLayout:rect]; + _highlight.tapAction(self, _innerText, _highlightRange, rect); + } else { + BOOL shouldTap = YES; + if ([self.delegate respondsToSelector:@selector(textView:shouldTapHighlight:inRange:)]) { + shouldTap = [self.delegate textView:self shouldTapHighlight:_highlight inRange:_highlightRange]; + } + if (shouldTap && [self.delegate respondsToSelector:@selector(textView:didTapHighlight:inRange:rect:)]) { + CGRect rect = [_innerLayout rectForRange:[YYTextRange rangeWithRange:_highlightRange]]; + rect = [self _convertRectFromLayout:rect]; + [self.delegate textView:self didTapHighlight:_highlight inRange:_highlightRange rect:rect]; + } + } + [self _removeHighlightAnimated:YES]; + } + } else { + if (_state.trackingCaret) { + if (_state.touchMoved) { + [self _updateTextRangeByTrackingCaret]; + [self _showMenu]; + } else { + if (_state.showingMenu) [self _hideMenu]; + else [self _showMenu]; + } + } else if (_state.trackingGrabber) { + [self _updateTextRangeByTrackingGrabber]; + [self _showMenu]; + } else if (_state.trackingPreSelect) { + [self _updateTextRangeByTrackingPreSelect]; + if (_trackingRange.asRange.length > 0) { + _state.selectedWithoutEdit = YES; + [self _showMenu]; + } else { + [self performSelector:@selector(becomeFirstResponder) withObject:nil afterDelay:0]; + } + } else if (_state.deleteConfirm || _markedTextRange) { + [self _updateTextRangeByTrackingCaret]; + [self _hideMenu]; + } else { + if (!_state.touchMoved) { + if (_state.selectedWithoutEdit) { + _state.selectedWithoutEdit = NO; + [self _hideMenu]; + } else { + if (self.isFirstResponder) { + YYTextRange *_oldRange = _trackingRange; + [self _updateTextRangeByTrackingCaret]; + if ([_oldRange isEqual:_trackingRange]) { + if (_state.showingMenu) [self _hideMenu]; + else [self _showMenu]; + } else { + [self _hideMenu]; + } + } else { + [self _hideMenu]; + if (_state.clearsOnInsertionOnce) { + _state.clearsOnInsertionOnce = NO; + _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)]; + [self _setSelectedRange:_selectedTextRange.asRange]; + } else { + [self _updateTextRangeByTrackingCaret]; + } + [self performSelector:@selector(becomeFirstResponder) withObject:nil afterDelay:0]; + } + } + } + } + } + + if (_trackingRange && (![_trackingRange isEqual:_selectedTextRange] || _state.trackingPreSelect)) { + if (![_trackingRange isEqual:_selectedTextRange]) { + [_inputDelegate selectionWillChange:self]; + _selectedTextRange = _trackingRange; + [_inputDelegate selectionDidChange:self]; + [self _updateAttributesHolder]; + [self _updateOuterProperties]; + } + if (!_state.trackingGrabber && !_state.trackingPreSelect) { + [self _scrollRangeToVisible:_selectedTextRange]; + } + } + + [self _endTouchTracking]; + } + + if (!_state.swallowTouch) [super touchesEnded:touches withEvent:event]; +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { + [self _endTouchTracking]; + [self _hideMenu]; + + if (!_state.swallowTouch) [super touchesCancelled:touches withEvent:event]; +} + +- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event { + if (motion == UIEventSubtypeMotionShake && _allowsUndoAndRedo) { + if (!YYTextIsAppExtension()) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wundeclared-selector" + [self performSelector:@selector(_showUndoRedoAlert)]; +#pragma clang diagnostic pop + } + } else { + [super motionEnded:motion withEvent:event]; + } +} + +- (BOOL)canBecomeFirstResponder { + if (!self.isSelectable) return NO; + if (!self.isEditable) return NO; + if (_state.ignoreFirstResponder) return NO; + if ([self.delegate respondsToSelector:@selector(textViewShouldBeginEditing:)]) { + if (![self.delegate textViewShouldBeginEditing:self]) return NO; + } + return YES; +} + +- (BOOL)becomeFirstResponder { + BOOL isFirstResponder = self.isFirstResponder; + if (isFirstResponder) return YES; + BOOL shouldDetectData = [self _shouldDetectText]; + BOOL become = [super becomeFirstResponder]; + if (!isFirstResponder && become) { + [self _endTouchTracking]; + [self _hideMenu]; + + _state.selectedWithoutEdit = NO; + if (shouldDetectData != [self _shouldDetectText]) { + [self _update]; + } + [self _updateIfNeeded]; + [self _updateSelectionView]; + [self performSelector:@selector(_scrollSelectedRangeToVisible) withObject:nil afterDelay:0]; + if ([self.delegate respondsToSelector:@selector(textViewDidBeginEditing:)]) { + [self.delegate textViewDidBeginEditing:self]; + } + [[NSNotificationCenter defaultCenter] postNotificationName:YYTextViewTextDidBeginEditingNotification object:self]; + } + return become; +} + +- (BOOL)canResignFirstResponder { + if (!self.isFirstResponder) return YES; + if ([self.delegate respondsToSelector:@selector(textViewShouldEndEditing:)]) { + if (![self.delegate textViewShouldEndEditing:self]) return NO; + } + return YES; +} + +- (BOOL)resignFirstResponder { + BOOL isFirstResponder = self.isFirstResponder; + if (!isFirstResponder) return YES; + BOOL resign = [super resignFirstResponder]; + if (resign) { + if (_markedTextRange) { + _markedTextRange = nil; + [self _parseText]; + [self _setText:[_innerText yy_plainTextForRange:NSMakeRange(0, _innerText.length)]]; + } + _state.selectedWithoutEdit = NO; + if ([self _shouldDetectText]) { + [self _update]; + } + [self _endTouchTracking]; + [self _hideMenu]; + [self _updateIfNeeded]; + [self _updateSelectionView]; + [self _restoreInsetsAnimated:YES]; + if ([self.delegate respondsToSelector:@selector(textViewDidEndEditing:)]) { + [self.delegate textViewDidEndEditing:self]; + } + [[NSNotificationCenter defaultCenter] postNotificationName:YYTextViewTextDidEndEditingNotification object:self]; + } + return resign; +} + +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender { + /* + ------------------------------------------------------ + Default menu actions list: + cut: Cut + copy: Copy + select: Select + selectAll: Select All + paste: Paste + delete: Delete + _promptForReplace: Replace... + _transliterateChinese: ��������� + _showTextStyleOptions: ������������ + _define: Define + _addShortcut: Add... + _accessibilitySpeak: Speak + _accessibilitySpeakLanguageSelection: Speak... + _accessibilityPauseSpeaking: Pause Speak + makeTextWritingDirectionRightToLeft: ��� + makeTextWritingDirectionLeftToRight: ��� + + ------------------------------------------------------ + Default attribute modifier list: + toggleBoldface: + toggleItalics: + toggleUnderline: + increaseSize: + decreaseSize: + */ + + if (_selectedTextRange.asRange.length == 0) { + if (action == @selector(select:) || + action == @selector(selectAll:)) { + return _innerText.length > 0; + } + if (action == @selector(paste:)) { + return [self _isPasteboardContainsValidValue]; + } + } else { + if (action == @selector(cut:)) { + return self.isFirstResponder && self.editable; + } + if (action == @selector(copy:)) { + return YES; + } + if (action == @selector(selectAll:)) { + return _selectedTextRange.asRange.length < _innerText.length; + } + if (action == @selector(paste:)) { + return self.isFirstResponder && self.editable && [self _isPasteboardContainsValidValue]; + } + NSString *selString = NSStringFromSelector(action); + if ([selString hasSuffix:@"define:"] && [selString hasPrefix:@"_"]) { + return [self _getRootViewController] != nil; + } + } + return NO; +} + +- (void)reloadInputViews { + [super reloadInputViews]; + if (_markedTextRange) { + [self unmarkText]; + } +} + +#pragma mark - Override NSObject(UIResponderStandardEditActions) + +- (void)cut:(id)sender { + [self _endTouchTracking]; + if (_selectedTextRange.asRange.length == 0) return; + + [self _copySelectedTextToPasteboard]; + [self _saveToUndoStack]; + [self _resetRedoStack]; + [self replaceRange:_selectedTextRange withText:@""]; +} + +- (void)copy:(id)sender { + [self _endTouchTracking]; + [self _copySelectedTextToPasteboard]; +} + +- (void)paste:(id)sender { + [self _endTouchTracking]; + UIPasteboard *p = [UIPasteboard generalPasteboard]; + NSAttributedString *atr = nil; + + if (_allowsPasteAttributedString) { + atr = p.yy_AttributedString; + if (atr.length == 0) atr = nil; + } + if (!atr && _allowsPasteImage) { + UIImage *img = nil; + + Class cls = NSClassFromString(@"YYImage"); + if (cls) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wundeclared-selector" + if (p.yy_GIFData) { + img = [(id)cls performSelector:@selector(imageWithData:scale:) withObject:p.yy_GIFData withObject:nil]; + } + if (!img && p.yy_PNGData) { + img = [(id)cls performSelector:@selector(imageWithData:scale:) withObject:p.yy_PNGData withObject:nil]; + } + if (!img && p.yy_WEBPData) { + img = [(id)cls performSelector:@selector(imageWithData:scale:) withObject:p.yy_WEBPData withObject:nil]; + } +#pragma clang diagnostic pop + } + + if (!img) { + img = p.image; + } + if (!img && p.yy_ImageData) { + img = [UIImage imageWithData:p.yy_ImageData scale:YYTextScreenScale()]; + } + if (img && img.size.width > 1 && img.size.height > 1) { + id content = img; + + if (cls) { + if ([img conformsToProtocol:NSProtocolFromString(@"YYAnimatedImage")]) { + NSNumber *frameCount = [img valueForKey:@"animatedImageFrameCount"]; + if (frameCount.integerValue > 1) { + Class viewCls = NSClassFromString(@"YYAnimatedImageView"); + UIImageView *imgView = [(id)viewCls new]; + imgView.image = img; + imgView.frame = CGRectMake(0, 0, img.size.width, img.size.height); + if (imgView) { + content = imgView; + } + } + } + } + + if ([content isKindOfClass:[UIImage class]] && img.images.count > 1) { + UIImageView *imgView = [UIImageView new]; + imgView.image = img; + imgView.frame = CGRectMake(0, 0, img.size.width, img.size.height); + if (imgView) { + content = imgView; + } + } + + NSMutableAttributedString *attText = [NSAttributedString yy_attachmentStringWithContent:content contentMode:UIViewContentModeScaleToFill width:img.size.width ascent:img.size.height descent:0]; + NSDictionary *attrs = _typingAttributesHolder.yy_attributes; + if (attrs) [attText addAttributes:attrs range:NSMakeRange(0, attText.length)]; + atr = attText; + } + } + + if (atr) { + NSUInteger endPosition = _selectedTextRange.start.offset + atr.length; + NSMutableAttributedString *text = _innerText.mutableCopy; + [text replaceCharactersInRange:_selectedTextRange.asRange withAttributedString:atr]; + self.attributedText = text; + YYTextPosition *pos = [self _correctedTextPosition:[YYTextPosition positionWithOffset:endPosition]]; + YYTextRange *range = [_innerLayout textRangeByExtendingPosition:pos]; + range = [self _correctedTextRange:range]; + if (range) { + self.selectedRange = NSMakeRange(range.end.offset, 0); + } + } else { + NSString *string = p.string; + if (string.length > 0) { + [self _saveToUndoStack]; + [self _resetRedoStack]; + [self replaceRange:_selectedTextRange withText:string]; + } + } +} + +- (void)select:(id)sender { + [self _endTouchTracking]; + + if (_selectedTextRange.asRange.length > 0 || _innerText.length == 0) return; + YYTextRange *newRange = [self _getClosestTokenRangeAtPosition:_selectedTextRange.start]; + if (newRange.asRange.length > 0) { + [_inputDelegate selectionWillChange:self]; + _selectedTextRange = newRange; + [_inputDelegate selectionDidChange:self]; + } + + [self _updateIfNeeded]; + [self _updateOuterProperties]; + [self _updateSelectionView]; + [self _hideMenu]; + [self _showMenu]; +} + +- (void)selectAll:(id)sender { + _trackingRange = nil; + [_inputDelegate selectionWillChange:self]; + _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)]; + [_inputDelegate selectionDidChange:self]; + + [self _updateIfNeeded]; + [self _updateOuterProperties]; + [self _updateSelectionView]; + [self _hideMenu]; + [self _showMenu]; +} + +- (void)_define:(id)sender { + [self _hideMenu]; + + NSString *string = [_innerText yy_plainTextForRange:_selectedTextRange.asRange]; + if (string.length == 0) return; + BOOL resign = [self resignFirstResponder]; + if (!resign) return; + + UIReferenceLibraryViewController* ref = [[UIReferenceLibraryViewController alloc] initWithTerm:string]; + ref.view.backgroundColor = [UIColor whiteColor]; + [[self _getRootViewController] presentViewController:ref animated:YES completion:^{}]; +} + + +#pragma mark - Overrice NSObject(NSKeyValueObservingCustomization) + ++ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { + static NSSet *keys = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + keys = [NSSet setWithArray:@[ + @"text", + @"font", + @"textColor", + @"textAlignment", + @"dataDetectorTypes", + @"linkTextAttributes", + @"highlightTextAttributes", + @"textParser", + @"attributedText", + @"textVerticalAlignment", + @"textContainerInset", + @"exclusionPaths", + @"verticalForm", + @"linePositionModifier", + @"selectedRange", + @"typingAttributes" + ]]; + }); + if ([keys containsObject:key]) { + return NO; + } + return [super automaticallyNotifiesObserversForKey:key]; +} + +#pragma mark - @protocol NSCoding + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + self = [super initWithCoder:aDecoder]; + [self _initTextView]; + self.attributedText = [aDecoder decodeObjectForKey:@"attributedText"]; + self.selectedRange = ((NSValue *)[aDecoder decodeObjectForKey:@"selectedRange"]).rangeValue; + self.textVerticalAlignment = [aDecoder decodeIntegerForKey:@"textVerticalAlignment"]; + self.dataDetectorTypes = [aDecoder decodeIntegerForKey:@"dataDetectorTypes"]; + self.textContainerInset = ((NSValue *)[aDecoder decodeObjectForKey:@"textContainerInset"]).UIEdgeInsetsValue; + self.exclusionPaths = [aDecoder decodeObjectForKey:@"exclusionPaths"]; + self.verticalForm = [aDecoder decodeBoolForKey:@"verticalForm"]; + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [super encodeWithCoder:aCoder]; + [aCoder encodeObject:self.attributedText forKey:@"attributedText"]; + [aCoder encodeObject:[NSValue valueWithRange:self.selectedRange] forKey:@"selectedRange"]; + [aCoder encodeInteger:self.textVerticalAlignment forKey:@"textVerticalAlignment"]; + [aCoder encodeInteger:self.dataDetectorTypes forKey:@"dataDetectorTypes"]; + [aCoder encodeUIEdgeInsets:self.textContainerInset forKey:@"textContainerInset"]; + [aCoder encodeObject:self.exclusionPaths forKey:@"exclusionPaths"]; + [aCoder encodeBool:self.verticalForm forKey:@"verticalForm"]; +} + +#pragma mark - @protocol UIScrollViewDelegate + +- (id<YYTextViewDelegate>)delegate { + return _outerDelegate; +} + +- (void)setDelegate:(id<YYTextViewDelegate>)delegate { + _outerDelegate = delegate; +} + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView { + [[YYTextEffectWindow sharedWindow] hideSelectionDot:_selectionView]; + + if ([_outerDelegate respondsToSelector:_cmd]) { + [_outerDelegate scrollViewDidScroll:scrollView]; + } +} + +- (void)scrollViewDidZoom:(UIScrollView *)scrollView { + if ([_outerDelegate respondsToSelector:_cmd]) { + [_outerDelegate scrollViewDidZoom:scrollView]; + } +} + +- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { + if ([_outerDelegate respondsToSelector:_cmd]) { + [_outerDelegate scrollViewWillBeginDragging:scrollView]; + } +} + +- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { + if ([_outerDelegate respondsToSelector:_cmd]) { + [_outerDelegate scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset]; + } +} + +- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { + if (!decelerate) { + [[YYTextEffectWindow sharedWindow] showSelectionDot:_selectionView]; + } + + if ([_outerDelegate respondsToSelector:_cmd]) { + [_outerDelegate scrollViewDidEndDragging:scrollView willDecelerate:decelerate]; + } +} + +- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView { + if ([_outerDelegate respondsToSelector:_cmd]) { + [_outerDelegate scrollViewWillBeginDecelerating:scrollView]; + } +} + +- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { + [[YYTextEffectWindow sharedWindow] showSelectionDot:_selectionView]; + + if ([_outerDelegate respondsToSelector:_cmd]) { + [_outerDelegate scrollViewDidEndDecelerating:scrollView]; + } +} + +- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView { + if ([_outerDelegate respondsToSelector:_cmd]) { + [_outerDelegate scrollViewDidEndScrollingAnimation:scrollView]; + } +} + +- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView { + if ([_outerDelegate respondsToSelector:_cmd]) { + return [_outerDelegate viewForZoomingInScrollView:scrollView]; + } else { + return nil; + } +} + +- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view{ + if ([_outerDelegate respondsToSelector:_cmd]) { + [_outerDelegate scrollViewWillBeginZooming:scrollView withView:view]; + } +} + +- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale { + if ([_outerDelegate respondsToSelector:_cmd]) { + [_outerDelegate scrollViewDidEndZooming:scrollView withView:view atScale:scale]; + } +} + +- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView { + if ([_outerDelegate respondsToSelector:_cmd]) { + return [_outerDelegate scrollViewShouldScrollToTop:scrollView]; + } + return YES; +} + +- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView { + if ([_outerDelegate respondsToSelector:_cmd]) { + [_outerDelegate scrollViewDidScrollToTop:scrollView]; + } +} + +#pragma mark - @protocol YYTextKeyboardObserver + +- (void)keyboardChangedWithTransition:(YYTextKeyboardTransition)transition { + [self _keyboardChanged]; +} + +#pragma mark - @protocol UIALertViewDelegate + +- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { + NSString *title = [alertView buttonTitleAtIndex:buttonIndex]; + if (title.length == 0) return; + NSArray *strings = [self _localizedUndoStrings]; + if ([title isEqualToString:strings[1]] || [title isEqualToString:strings[2]]) { + [self _redo]; + } else if ([title isEqualToString:strings[3]] || [title isEqualToString:strings[4]]) { + [self _undo]; + } + [self _restoreFirstResponderAfterUndoAlert]; +} + +#pragma mark - @protocol UIKeyInput + +- (BOOL)hasText { + return _innerText.length > 0; +} + +- (void)insertText:(NSString *)text { + if (text.length == 0) return; + if (!NSEqualRanges(_lastTypeRange, _selectedTextRange.asRange)) { + [self _saveToUndoStack]; + [self _resetRedoStack]; + } + [self replaceRange:_selectedTextRange withText:text]; +} + +- (void)deleteBackward { + [self _updateIfNeeded]; + NSRange range = _selectedTextRange.asRange; + if (range.location == 0 && range.length == 0) return; + _state.typingAttributesOnce = NO; + + // test if there's 'TextBinding' before the caret + if (!_state.deleteConfirm && range.length == 0 && range.location > 0) { + NSRange effectiveRange; + YYTextBinding *binding = [_innerText attribute:YYTextBindingAttributeName atIndex:range.location - 1 longestEffectiveRange:&effectiveRange inRange:NSMakeRange(0, _innerText.length)]; + if (binding && binding.deleteConfirm) { + _state.deleteConfirm = YES; + [_inputDelegate selectionWillChange:self]; + _selectedTextRange = [YYTextRange rangeWithRange:effectiveRange]; + _selectedTextRange = [self _correctedTextRange:_selectedTextRange]; + [_inputDelegate selectionDidChange:self]; + + [self _updateOuterProperties]; + [self _updateSelectionView]; + return; + } + } + + _state.deleteConfirm = NO; + if (range.length == 0) { + YYTextRange *extendRange = [_innerLayout textRangeByExtendingPosition:_selectedTextRange.end inDirection:UITextLayoutDirectionLeft offset:1]; + if ([self _isTextRangeValid:extendRange]) { + range = extendRange.asRange; + } + } + if (!NSEqualRanges(_lastTypeRange, _selectedTextRange.asRange)) { + [self _saveToUndoStack]; + [self _resetRedoStack]; + } + [self replaceRange:[YYTextRange rangeWithRange:range] withText:@""]; +} + +#pragma mark - @protocol UITextInput + +- (void)setInputDelegate:(id<UITextInputDelegate>)inputDelegate { + _inputDelegate = inputDelegate; +} + +- (void)setSelectedTextRange:(YYTextRange *)selectedTextRange { + if (!selectedTextRange) return; + selectedTextRange = [self _correctedTextRange:selectedTextRange]; + if ([selectedTextRange isEqual:_selectedTextRange]) return; + [self _updateIfNeeded]; + [self _endTouchTracking]; + [self _hideMenu]; + _state.deleteConfirm = NO; + _state.typingAttributesOnce = NO; + + [_inputDelegate selectionWillChange:self]; + _selectedTextRange = selectedTextRange; + _lastTypeRange = _selectedTextRange.asRange; + [_inputDelegate selectionDidChange:self]; + + [self _updateOuterProperties]; + [self _updateSelectionView]; + + if (self.isFirstResponder) { + [self _scrollRangeToVisible:_selectedTextRange]; + } +} + +- (void)setMarkedTextStyle:(NSDictionary *)markedTextStyle { + _markedTextStyle = markedTextStyle.copy; +} + +/* + Replace current markedText with the new markedText + @param markedText New marked text. + @param selectedRange The range from the '_markedTextRange' + */ +- (void)setMarkedText:(NSString *)markedText selectedRange:(NSRange)selectedRange { + [self _updateIfNeeded]; + [self _endTouchTracking]; + [self _hideMenu]; + + if ([self.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:)]) { + NSRange range = _markedTextRange ? _markedTextRange.asRange : NSMakeRange(_selectedTextRange.end.offset, 0); + BOOL should = [self.delegate textView:self shouldChangeTextInRange:range replacementText:markedText]; + if (!should) return; + } + + + if (!NSEqualRanges(_lastTypeRange, _selectedTextRange.asRange)) { + [self _saveToUndoStack]; + [self _resetRedoStack]; + } + + BOOL needApplyHolderAttribute = NO; + if (_innerText.length > 0 && _markedTextRange) { + [self _updateAttributesHolder]; + } else { + needApplyHolderAttribute = YES; + } + + if (_selectedTextRange.asRange.length > 0) { + [self replaceRange:_selectedTextRange withText:@""]; + } + + [_inputDelegate textWillChange:self]; + [_inputDelegate selectionWillChange:self]; + + if (!markedText) markedText = @""; + if (_markedTextRange == nil) { + _markedTextRange = [YYTextRange rangeWithRange:NSMakeRange(_selectedTextRange.end.offset, markedText.length)]; + [_innerText replaceCharactersInRange:NSMakeRange(_selectedTextRange.end.offset, 0) withString:markedText]; + _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(_selectedTextRange.start.offset + selectedRange.location, selectedRange.length)]; + } else { + _markedTextRange = [self _correctedTextRange:_markedTextRange]; + [_innerText replaceCharactersInRange:_markedTextRange.asRange withString:markedText]; + _markedTextRange = [YYTextRange rangeWithRange:NSMakeRange(_markedTextRange.start.offset, markedText.length)]; + _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(_markedTextRange.start.offset + selectedRange.location, selectedRange.length)]; + } + + _selectedTextRange = [self _correctedTextRange:_selectedTextRange]; + _markedTextRange = [self _correctedTextRange:_markedTextRange]; + if (_markedTextRange.asRange.length == 0) { + _markedTextRange = nil; + } else { + if (needApplyHolderAttribute) { + [_innerText setAttributes:_typingAttributesHolder.yy_attributes range:_markedTextRange.asRange]; + } + [_innerText yy_removeDiscontinuousAttributesInRange:_markedTextRange.asRange]; + } + + [_inputDelegate selectionDidChange:self]; + [_inputDelegate textDidChange:self]; + + [self _updateOuterProperties]; + [self _updateLayout]; + [self _updateSelectionView]; + [self _scrollRangeToVisible:_selectedTextRange]; + + if ([self.delegate respondsToSelector:@selector(textViewDidChange:)]) { + [self.delegate textViewDidChange:self]; + } + [[NSNotificationCenter defaultCenter] postNotificationName:YYTextViewTextDidChangeNotification object:self]; + + _lastTypeRange = _selectedTextRange.asRange; +} + +- (void)unmarkText { + _markedTextRange = nil; + [self _endTouchTracking]; + [self _hideMenu]; + if ([self _parseText]) _state.needUpdate = YES; + + [self _updateIfNeeded]; + [self _updateOuterProperties]; + [self _updateSelectionView]; + [self _scrollRangeToVisible:_selectedTextRange]; +} + +- (void)replaceRange:(YYTextRange *)range withText:(NSString *)text { + if (!range) return; + if (!text) text = @""; + if (range.asRange.length == 0 && text.length == 0) return; + range = [self _correctedTextRange:range]; + + if ([self.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:)]) { + BOOL should = [self.delegate textView:self shouldChangeTextInRange:range.asRange replacementText:text]; + if (!should) return; + } + + BOOL useInnerAttributes = NO; + if (_innerText.length > 0) { + if (range.start.offset == 0 && range.end.offset == _innerText.length) { + if (text.length == 0) { + NSMutableDictionary *attrs = [_innerText yy_attributesAtIndex:0].mutableCopy; + [attrs removeObjectsForKeys:[NSMutableAttributedString yy_allDiscontinuousAttributeKeys]]; + _typingAttributesHolder.yy_attributes = attrs; + } + } + } else { // no text + useInnerAttributes = YES; + } + BOOL applyTypingAttributes = NO; + if (_state.typingAttributesOnce) { + _state.typingAttributesOnce = NO; + if (!useInnerAttributes) { + if (range.asRange.length == 0 && text.length > 0) { + applyTypingAttributes = YES; + } + } + } + + _state.selectedWithoutEdit = NO; + _state.deleteConfirm = NO; + [self _endTouchTracking]; + [self _hideMenu]; + + [self _replaceRange:range withText:text notifyToDelegate:YES]; + if (useInnerAttributes) { + [_innerText yy_setAttributes:_typingAttributesHolder.yy_attributes]; + } else if (applyTypingAttributes) { + NSRange newRange = NSMakeRange(range.asRange.location, text.length); + [_typingAttributesHolder.yy_attributes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + [_innerText yy_setAttribute:key value:obj range:newRange]; + }]; + } + [self _parseText]; + [self _updateOuterProperties]; + [self _update]; + + if (self.isFirstResponder) { + [self _scrollRangeToVisible:_selectedTextRange]; + } + + if ([self.delegate respondsToSelector:@selector(textViewDidChange:)]) { + [self.delegate textViewDidChange:self]; + } + [[NSNotificationCenter defaultCenter] postNotificationName:YYTextViewTextDidChangeNotification object:self]; + + _lastTypeRange = _selectedTextRange.asRange; +} + +- (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection forRange:(YYTextRange *)range { + if (!range) return; + range = [self _correctedTextRange:range]; + [_innerText yy_setBaseWritingDirection:(NSWritingDirection)writingDirection range:range.asRange]; + [self _commitUpdate]; +} + +- (NSString *)textInRange:(YYTextRange *)range { + range = [self _correctedTextRange:range]; + if (!range) return @""; + return [_innerText.string substringWithRange:range.asRange]; +} + +- (UITextWritingDirection)baseWritingDirectionForPosition:(YYTextPosition *)position inDirection:(UITextStorageDirection)direction { + [self _updateIfNeeded]; + position = [self _correctedTextPosition:position]; + if (!position) return UITextWritingDirectionNatural; + if (_innerText.length == 0) return UITextWritingDirectionNatural; + NSUInteger idx = position.offset; + if (idx == _innerText.length) idx--; + + NSDictionary *attrs = [_innerText yy_attributesAtIndex:idx]; + CTParagraphStyleRef paraStyle = (__bridge CFTypeRef)(attrs[NSParagraphStyleAttributeName]); + if (paraStyle) { + CTWritingDirection baseWritingDirection; + if (CTParagraphStyleGetValueForSpecifier(paraStyle, kCTParagraphStyleSpecifierBaseWritingDirection, sizeof(CTWritingDirection), &baseWritingDirection)) { + return (UITextWritingDirection)baseWritingDirection; + } + } + + return UITextWritingDirectionNatural; +} + +- (YYTextPosition *)beginningOfDocument { + return [YYTextPosition positionWithOffset:0]; +} + +- (YYTextPosition *)endOfDocument { + return [YYTextPosition positionWithOffset:_innerText.length]; +} + +- (YYTextPosition *)positionFromPosition:(YYTextPosition *)position offset:(NSInteger)offset { + if (offset == 0) return position; + + NSUInteger location = position.offset; + NSInteger newLocation = (NSInteger)location + offset; + if (newLocation < 0 || newLocation > _innerText.length) return nil; + + if (newLocation != 0 && newLocation != _innerText.length) { + // fix emoji + [self _updateIfNeeded]; + YYTextRange *extendRange = [_innerLayout textRangeByExtendingPosition:[YYTextPosition positionWithOffset:newLocation]]; + if (extendRange.asRange.length > 0) { + if (offset < 0) { + newLocation = extendRange.start.offset; + } else { + newLocation = extendRange.end.offset; + } + } + } + + YYTextPosition *p = [YYTextPosition positionWithOffset:newLocation]; + return [self _correctedTextPosition:p]; +} + +- (YYTextPosition *)positionFromPosition:(YYTextPosition *)position inDirection:(UITextLayoutDirection)direction offset:(NSInteger)offset { + [self _updateIfNeeded]; + YYTextRange *range = [_innerLayout textRangeByExtendingPosition:position inDirection:direction offset:offset]; + + BOOL forward; + if (_innerContainer.isVerticalForm) { + forward = direction == UITextLayoutDirectionLeft || direction == UITextLayoutDirectionDown; + } else { + forward = direction == UITextLayoutDirectionDown || direction == UITextLayoutDirectionRight; + } + if (!forward && offset < 0) { + forward = -forward; + } + + YYTextPosition *newPosition = forward ? range.end : range.start; + if (newPosition.offset > _innerText.length) { + newPosition = [YYTextPosition positionWithOffset:_innerText.length affinity:YYTextAffinityBackward]; + } + + return [self _correctedTextPosition:newPosition]; +} + +- (YYTextRange *)textRangeFromPosition:(YYTextPosition *)fromPosition toPosition:(YYTextPosition *)toPosition { + return [YYTextRange rangeWithStart:fromPosition end:toPosition]; +} + +- (NSComparisonResult)comparePosition:(YYTextPosition *)position toPosition:(YYTextPosition *)other { + return [position compare:other]; +} + +- (NSInteger)offsetFromPosition:(YYTextPosition *)from toPosition:(YYTextPosition *)toPosition { + return toPosition.offset - from.offset; +} + +- (YYTextPosition *)positionWithinRange:(YYTextRange *)range farthestInDirection:(UITextLayoutDirection)direction { + NSRange nsRange = range.asRange; + if (direction == UITextLayoutDirectionLeft | direction == UITextLayoutDirectionUp) { + return [YYTextPosition positionWithOffset:nsRange.location]; + } else { + return [YYTextPosition positionWithOffset:nsRange.location + nsRange.length affinity:YYTextAffinityBackward]; + } +} + +- (YYTextRange *)characterRangeByExtendingPosition:(YYTextPosition *)position inDirection:(UITextLayoutDirection)direction { + [self _updateIfNeeded]; + YYTextRange *range = [_innerLayout textRangeByExtendingPosition:position inDirection:direction offset:1]; + return [self _correctedTextRange:range]; +} + +- (YYTextPosition *)closestPositionToPoint:(CGPoint)point { + [self _updateIfNeeded]; + point = [self _convertPointToLayout:point]; + YYTextPosition *position = [_innerLayout closestPositionToPoint:point]; + return [self _correctedTextPosition:position]; +} + +- (YYTextPosition *)closestPositionToPoint:(CGPoint)point withinRange:(YYTextRange *)range { + YYTextPosition *pos = (id)[self closestPositionToPoint:point]; + if (!pos) return nil; + + range = [self _correctedTextRange:range]; + if ([pos compare:range.start] == NSOrderedAscending) { + pos = range.start; + } else if ([pos compare:range.end] == NSOrderedDescending) { + pos = range.end; + } + return pos; +} + +- (YYTextRange *)characterRangeAtPoint:(CGPoint)point { + [self _updateIfNeeded]; + point = [self _convertPointToLayout:point]; + YYTextRange *r = [_innerLayout closestTextRangeAtPoint:point]; + return [self _correctedTextRange:r]; +} + +- (CGRect)firstRectForRange:(YYTextRange *)range { + [self _updateIfNeeded]; + CGRect rect = [_innerLayout firstRectForRange:range]; + if (CGRectIsNull(rect)) rect = CGRectZero; + return [self _convertRectFromLayout:rect]; +} + +- (CGRect)caretRectForPosition:(YYTextPosition *)position { + [self _updateIfNeeded]; + CGRect caretRect = [_innerLayout caretRectForPosition:position]; + if (!CGRectIsNull(caretRect)) { + caretRect = [self _convertRectFromLayout:caretRect]; + caretRect = CGRectStandardize(caretRect); + if (_verticalForm) { + if (caretRect.size.height == 0) { + caretRect.size.height = 2; + caretRect.origin.y -= 2 * 0.5; + } + if (caretRect.origin.y < 0) { + caretRect.origin.y = 0; + } else if (caretRect.origin.y + caretRect.size.height > self.bounds.size.height) { + caretRect.origin.y = self.bounds.size.height - caretRect.size.height; + } + } else { + if (caretRect.size.width == 0) { + caretRect.size.width = 2; + caretRect.origin.x -= 2 * 0.5; + } + if (caretRect.origin.x < 0) { + caretRect.origin.x = 0; + } else if (caretRect.origin.x + caretRect.size.width > self.bounds.size.width) { + caretRect.origin.x = self.bounds.size.width - caretRect.size.width; + } + } + return YYTextCGRectPixelRound(caretRect); + } + return CGRectZero; +} + +- (NSArray *)selectionRectsForRange:(YYTextRange *)range { + [self _updateIfNeeded]; + NSArray *rects = [_innerLayout selectionRectsForRange:range]; + [rects enumerateObjectsUsingBlock:^(YYTextSelectionRect *rect, NSUInteger idx, BOOL *stop) { + rect.rect = [self _convertRectFromLayout:rect.rect]; + }]; + return rects; +} + +#pragma mark - @protocol UITextInput optional + +- (UITextStorageDirection)selectionAffinity { + if (_selectedTextRange.end.affinity == YYTextAffinityForward) { + return UITextStorageDirectionForward; + } else { + return UITextStorageDirectionBackward; + } +} + +- (void)setSelectionAffinity:(UITextStorageDirection)selectionAffinity { + _selectedTextRange = [YYTextRange rangeWithRange:_selectedTextRange.asRange affinity:selectionAffinity == UITextStorageDirectionForward ? YYTextAffinityForward : YYTextAffinityBackward]; + [self _updateSelectionView]; +} + +- (NSDictionary *)textStylingAtPosition:(YYTextPosition *)position inDirection:(UITextStorageDirection)direction { + if (!position) return nil; + if (_innerText.length == 0) return _typingAttributesHolder.yy_attributes; + NSDictionary *attrs = nil; + if (0 <= position.offset && position.offset <= _innerText.length) { + NSUInteger ofs = position.offset; + if (position.offset == _innerText.length || + direction == UITextStorageDirectionBackward) { + ofs--; + } + attrs = [_innerText attributesAtIndex:ofs effectiveRange:NULL]; + } + return attrs; +} + +- (YYTextPosition *)positionWithinRange:(YYTextRange *)range atCharacterOffset:(NSInteger)offset { + if (!range) return nil; + if (offset < range.start.offset || offset > range.end.offset) return nil; + if (offset == range.start.offset) return range.start; + else if (offset == range.end.offset) return range.end; + else return [YYTextPosition positionWithOffset:offset]; +} + +- (NSInteger)characterOffsetOfPosition:(YYTextPosition *)position withinRange:(YYTextRange *)range { + return position ? position.offset : NSNotFound; +} + +@end + + + +@interface YYTextView(IBInspectableProperties) +@end + +@implementation YYTextView(IBInspectableProperties) + +- (BOOL)fontIsBold_:(UIFont *)font { + if (![font respondsToSelector:@selector(fontDescriptor)]) return NO; + return (font.fontDescriptor.symbolicTraits & UIFontDescriptorTraitBold) > 0; +} + +- (UIFont *)boldFont_:(UIFont *)font { + if (![font respondsToSelector:@selector(fontDescriptor)]) return font; + return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold] size:font.pointSize]; +} + +- (UIFont *)normalFont_:(UIFont *)font { + if (![font respondsToSelector:@selector(fontDescriptor)]) return font; + return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:0] size:font.pointSize]; +} + +- (void)setFontName_:(NSString *)fontName { + if (!fontName) return; + UIFont *font = self.font; + if (!font) font = [self _defaultFont]; + if ((fontName.length == 0 || [fontName.lowercaseString isEqualToString:@"system"]) && ![self fontIsBold_:font]) { + font = [UIFont systemFontOfSize:font.pointSize]; + } else if ([fontName.lowercaseString isEqualToString:@"system bold"]) { + font = [UIFont boldSystemFontOfSize:font.pointSize]; + } else { + if ([self fontIsBold_:font] && ([fontName.lowercaseString rangeOfString:@"bold"].location == NSNotFound)) { + font = [UIFont fontWithName:fontName size:font.pointSize]; + font = [self boldFont_:font]; + } else { + font = [UIFont fontWithName:fontName size:font.pointSize]; + } + } + if (font) self.font = font; +} + +- (void)setFontSize_:(CGFloat)fontSize { + if (fontSize <= 0) return; + UIFont *font = self.font; + if (!font) font = [self _defaultFont]; + if (!font) font = [self _defaultFont]; + font = [font fontWithSize:fontSize]; + if (font) self.font = font; +} + +- (void)setFontIsBold_:(BOOL)fontBold { + UIFont *font = self.font; + if (!font) font = [self _defaultFont]; + if ([self fontIsBold_:font] == fontBold) return; + if (fontBold) { + font = [self boldFont_:font]; + } else { + font = [self normalFont_:font]; + } + if (font) self.font = font; +} + +- (void)setPlaceholderFontName_:(NSString *)fontName { + if (!fontName) return; + UIFont *font = self.placeholderFont; + if (!font) font = [self _defaultFont]; + if ((fontName.length == 0 || [fontName.lowercaseString isEqualToString:@"system"]) && ![self fontIsBold_:font]) { + font = [UIFont systemFontOfSize:font.pointSize]; + } else if ([fontName.lowercaseString isEqualToString:@"system bold"]) { + font = [UIFont boldSystemFontOfSize:font.pointSize]; + } else { + if ([self fontIsBold_:font] && ([fontName.lowercaseString rangeOfString:@"bold"].location == NSNotFound)) { + font = [UIFont fontWithName:fontName size:font.pointSize]; + font = [self boldFont_:font]; + } else { + font = [UIFont fontWithName:fontName size:font.pointSize]; + } + } + if (font) self.placeholderFont = font; +} + +- (void)setPlaceholderFontSize_:(CGFloat)fontSize { + if (fontSize <= 0) return; + UIFont *font = self.placeholderFont; + if (!font) font = [self _defaultFont]; + font = [font fontWithSize:fontSize]; + if (font) self.placeholderFont = font; +} + +- (void)setPlaceholderFontIsBold_:(BOOL)fontBold { + UIFont *font = self.placeholderFont; + if (!font) font = [self _defaultFont]; + if ([self fontIsBold_:font] == fontBold) return; + if (fontBold) { + font = [self boldFont_:font]; + } else { + font = [self normalFont_:font]; + } + if (font) self.placeholderFont = font; +} + +- (void)setInsetTop_:(CGFloat)textInsetTop { + UIEdgeInsets insets = self.textContainerInset; + insets.top = textInsetTop; + self.textContainerInset = insets; +} + +- (void)setInsetBottom_:(CGFloat)textInsetBottom { + UIEdgeInsets insets = self.textContainerInset; + insets.bottom = textInsetBottom; + self.textContainerInset = insets; +} + +- (void)setInsetLeft_:(CGFloat)textInsetLeft { + UIEdgeInsets insets = self.textContainerInset; + insets.left = textInsetLeft; + self.textContainerInset = insets; + +} + +- (void)setInsetRight_:(CGFloat)textInsetRight { + UIEdgeInsets insets = self.textContainerInset; + insets.right = textInsetRight; + self.textContainerInset = insets; +} + +- (void)setDebugEnabled_:(BOOL)enabled { + if (!enabled) { + self.debugOption = nil; + } else { + YYTextDebugOption *debugOption = [YYTextDebugOption new]; + debugOption.baselineColor = [UIColor redColor]; + debugOption.CTFrameBorderColor = [UIColor redColor]; + debugOption.CTLineFillColor = [UIColor colorWithRed:0.000 green:0.463 blue:1.000 alpha:0.180]; + debugOption.CGGlyphBorderColor = [UIColor colorWithRed:1.000 green:0.524 blue:0.000 alpha:0.200]; + self.debugOption = debugOption; + } +} + +@end -- Gitblit v1.8.0