123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 |
- //
- // CYRTextView.m
- //
- // Version 0.2.0
- //
- // Created by Illya Busigin on 01/05/2014.
- // Copyright (c) 2014 Cyrillian, Inc.
- // Copyright (c) 2013 Dominik Hauser
- // Copyright (c) 2013 Sam Rijs
- //
- // Distributed under MIT license.
- // Get the latest version from here:
- //
- // https://github.com/illyabusigin/CYRTextView
- //
- // The MIT License (MIT)
- //
- // Copyright (c) 2014 Cyrillian, Inc.
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy of
- // this software and associated documentation files (the "Software"), to deal in
- // the Software without restriction, including without limitation the rights to
- // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
- // the Software, and to permit persons to whom the Software is furnished to do so,
- // subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in all
- // copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
- // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
- // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
- // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
- // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- #import "CYRTextView.h"
- #import "CYRLayoutManager.h"
- #import "CYRTextStorage.h"
- #define RGB(r,g,b) [UIColor colorWithRed:r/255.0f green:g/255.0f blue:b/255.0f alpha:1.0f]
- static void *CYRTextViewContext = &CYRTextViewContext;
- static const float kCursorVelocity = 1.0f/8.0f;
- @interface CYRTextView ()
- @property (nonatomic, strong) CYRLayoutManager *lineNumberLayoutManager;
- @property (nonatomic, strong) CYRTextStorage *syntaxTextStorage;
- @end
- @implementation CYRTextView
- {
- NSRange startRange;
- }
- #pragma mark - Initialization & Setup
- - (id)initWithFrame:(CGRect)frame
- {
- CYRTextStorage *textStorage = [CYRTextStorage new];
- CYRLayoutManager *layoutManager = [CYRLayoutManager new];
-
- self.lineNumberLayoutManager = layoutManager;
-
- NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)];
-
- // Wrap text to the text view's frame
- textContainer.widthTracksTextView = YES;
-
- [layoutManager addTextContainer:textContainer];
- [textStorage removeLayoutManager:textStorage.layoutManagers.firstObject];
- [textStorage addLayoutManager:layoutManager];
-
- self.syntaxTextStorage = textStorage;
-
- if ((self = [super initWithFrame:frame textContainer:textContainer]))
- {
- self.contentMode = UIViewContentModeRedraw; // causes drawRect: to be called on frame resizing and device rotation
-
- [self _commonSetup];
- }
-
- return self;
- }
- - (void)_commonSetup
- {
- // Setup observers
- [self addObserver:self forKeyPath:NSStringFromSelector(@selector(font)) options:NSKeyValueObservingOptionNew context:CYRTextViewContext];
- [self addObserver:self forKeyPath:NSStringFromSelector(@selector(selectedTextRange)) options:NSKeyValueObservingOptionNew context:CYRTextViewContext];
- [self addObserver:self forKeyPath:NSStringFromSelector(@selector(selectedRange)) options:NSKeyValueObservingOptionNew context:CYRTextViewContext];
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleTextViewDidChangeNotification:) name:UITextViewTextDidChangeNotification object:self];
-
- // Setup defaults
- self.font = [UIFont systemFontOfSize:16.0f];
- self.autocapitalizationType = UITextAutocapitalizationTypeNone;
- self.autocorrectionType = UITextAutocorrectionTypeNo;
- self.lineCursorEnabled = YES;
- self.gutterBackgroundColor = [UIColor colorWithWhite:0.95 alpha:1];
- self.gutterLineColor = [UIColor lightGrayColor];
-
- // Inset the content to make room for line numbers
- self.textContainerInset = UIEdgeInsetsMake(8, self.lineNumberLayoutManager.gutterWidth, 8, 0);
-
- // Setup the gesture recognizers
- _singleFingerPanRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(singleFingerPanHappend:)];
- _singleFingerPanRecognizer.maximumNumberOfTouches = 1;
- [self addGestureRecognizer:_singleFingerPanRecognizer];
-
- _doubleFingerPanRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(doubleFingerPanHappend:)];
- _doubleFingerPanRecognizer.minimumNumberOfTouches = 2;
- [self addGestureRecognizer:_doubleFingerPanRecognizer];
- }
- #pragma mark - Cleanup
- - (void)dealloc
- {
- [self removeObserver:self forKeyPath:NSStringFromSelector(@selector(font))];
- [self removeObserver:self forKeyPath:NSStringFromSelector(@selector(selectedTextRange))];
- [self removeObserver:self forKeyPath:NSStringFromSelector(@selector(selectedRange))];
- }
- #pragma mark - KVO
- - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
- {
- if ([keyPath isEqualToString:NSStringFromSelector(@selector(font))] && context == CYRTextViewContext)
- {
- // Whenever the UITextView font is changed we want to keep a reference in the stickyFont ivar. We do this to counteract a bug where the underlying font can be changed without notice and cause undesired behaviour.
- self.syntaxTextStorage.defaultFont = self.font;
- }
- else if (([keyPath isEqualToString:NSStringFromSelector(@selector(selectedTextRange))] ||
- [keyPath isEqualToString:NSStringFromSelector(@selector(selectedRange))]) && context == CYRTextViewContext)
- {
- [self setNeedsDisplay];
- }
- else
- {
- [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
- }
- }
- #pragma mark - Notifications
- - (void)handleTextViewDidChangeNotification:(NSNotification *)notification
- {
- if (notification.object == self)
- {
- CGRect line = [self caretRectForPosition: self.selectedTextRange.start];
- CGFloat overflow = line.origin.y + line.size.height - ( self.contentOffset.y + self.bounds.size.height - self.contentInset.bottom - self.contentInset.top );
-
- if ( overflow > 0 )
- {
- // We are at the bottom of the visible text and introduced a line feed, scroll down (iOS 7 does not do it)
- // Scroll caret to visible area
- CGPoint offset = self.contentOffset;
- offset.y += overflow + 7; // leave 7 pixels margin
- // Cannot animate with setContentOffset:animated: or caret will not appear
- // [UIView animateWithDuration:.2 animations:^{
- // [self setContentOffset:offset];
- // }];
- }
- }
- }
- #pragma mark - Overrides
- - (void)setTokens:(NSMutableArray *)tokens
- {
- [self.syntaxTextStorage setTokens:tokens];
- }
- - (NSArray *)tokens
- {
- CYRTextStorage *syntaxTextStorage = (CYRTextStorage *)self.textStorage;
-
- return syntaxTextStorage.tokens;
- }
- - (void)setText:(NSString *)text
- {
- UITextRange *textRange = [self textRangeFromPosition:self.beginningOfDocument toPosition:self.endOfDocument];
- [self replaceRange:textRange withText:text];
- }
- #pragma mark - Line Drawing
- // Original implementation sourced from: https://github.com/alldritt/TextKit_LineNumbers
- - (void)drawRect:(CGRect)rect
- {
- // Drag the line number gutter background. The line numbers them selves are drawn by LineNumberLayoutManager.
- CGContextRef context = UIGraphicsGetCurrentContext();
- CGRect bounds = self.bounds;
-
- CGFloat height = MAX(CGRectGetHeight(bounds), self.contentSize.height) + 200;
-
- // Set the regular fill
- CGContextSetFillColorWithColor(context, self.gutterBackgroundColor.CGColor);
- CGContextFillRect(context, CGRectMake(bounds.origin.x, bounds.origin.y, self.lineNumberLayoutManager.gutterWidth, height));
-
- // Draw line
- CGContextSetFillColorWithColor(context, self.gutterLineColor.CGColor);
- CGContextFillRect(context, CGRectMake(self.lineNumberLayoutManager.gutterWidth, bounds.origin.y, 0.5, height));
-
- if (_lineCursorEnabled)
- {
- self.lineNumberLayoutManager.selectedRange = self.selectedRange;
-
- NSRange glyphRange = [self.lineNumberLayoutManager.textStorage.string paragraphRangeForRange:self.selectedRange];
- glyphRange = [self.lineNumberLayoutManager glyphRangeForCharacterRange:glyphRange actualCharacterRange:NULL];
- self.lineNumberLayoutManager.selectedRange = glyphRange;
- [self.lineNumberLayoutManager invalidateDisplayForGlyphRange:glyphRange];
- }
-
- [super drawRect:rect];
- }
- #pragma mark - Gestures
- // Sourced from: https://github.com/srijs/NLTextView
- - (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer
- {
- // Only accept horizontal pans for the code navigation to preserve correct scrolling behaviour.
- if (gestureRecognizer == _singleFingerPanRecognizer || gestureRecognizer == _doubleFingerPanRecognizer)
- {
- CGPoint translation = [gestureRecognizer translationInView:self];
-
- float translationX = (float)translation.x;
- float translationY = (float)translation.y;
-
- return fabsf(translationX) > fabsf(translationY);
- }
-
- return YES;
-
- }
- // Sourced from: https://github.com/srijs/NLTextView
- - (void)singleFingerPanHappend:(UIPanGestureRecognizer *)sender
- {
- if (sender.state == UIGestureRecognizerStateBegan)
- {
- startRange = self.selectedRange;
- }
-
- CGFloat cursorLocation = MAX(startRange.location + [sender translationInView:self].x * kCursorVelocity, 0);
-
- self.selectedRange = NSMakeRange(cursorLocation, 0);
- }
- // Sourced from: https://github.com/srijs/NLTextView
- - (void)doubleFingerPanHappend:(UIPanGestureRecognizer *)sender
- {
- if (sender.state == UIGestureRecognizerStateBegan)
- {
- startRange = self.selectedRange;
- }
-
- CGFloat cursorLocation = MAX(startRange.location + [sender translationInView:self].x * kCursorVelocity, 0);
-
- float location = startRange.location - cursorLocation;
-
- if (cursorLocation > startRange.location)
- {
- self.selectedRange = NSMakeRange(startRange.location, fabsf(location));
- }
- else
- {
- self.selectedRange = NSMakeRange(cursorLocation, fabsf(location));
- }
- }
- @end
|