New file |
| | |
| | | // |
| | | // 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 |