mirror of
https://github.com/leanote/leanote-ios.git
synced 2025-10-17 16:43:52 +00:00
303 lines
11 KiB
Objective-C
Executable File
303 lines
11 KiB
Objective-C
Executable File
//
|
|
// 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]
|
|
|
|
#ifdef CGFLOAT_IS_DOUBLE
|
|
#define CYRAbs fabs
|
|
#else
|
|
#define CYRAbs fabsf
|
|
#endif
|
|
|
|
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];
|
|
layoutManager.shouldCompletelyHideGutter = self.lineNumbersEnabled;
|
|
layoutManager.delegate = self;
|
|
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) setLineNumbersEnabled:(BOOL)pLineNumbersEnabled
|
|
{
|
|
_lineNumbersEnabled = pLineNumbersEnabled;
|
|
self.lineNumberLayoutManager.shouldCompletelyHideGutter = !self.lineNumbersEnabled;
|
|
}
|
|
|
|
- (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 fontWithName: @"Menlo-Regular" size:14.0f];
|
|
self.autocapitalizationType = UITextAutocapitalizationTypeNone;
|
|
self.autocorrectionType = UITextAutocorrectionTypeNo;
|
|
self.lineCursorEnabled = NO;
|
|
|
|
// Inset the content to make room for line numbers
|
|
if (self.lineNumbersEnabled) {
|
|
self.gutterBackgroundColor = [UIColor colorWithWhite:0.95 alpha:1];
|
|
self.gutterLineColor = [UIColor lightGrayColor];
|
|
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
|
|
{
|
|
// DRM: the call to replaceRange:withText: doesn't like at all text being nil.
|
|
//
|
|
if (!text) {
|
|
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
|
|
{
|
|
if (self.lineNumbersEnabled) {
|
|
// 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];
|
|
return CYRAbs(translation.x) > CYRAbs(translation.y);
|
|
}
|
|
|
|
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);
|
|
|
|
if (cursorLocation > startRange.location)
|
|
{
|
|
self.selectedRange = NSMakeRange(startRange.location, CYRAbs(startRange.location - cursorLocation));
|
|
}
|
|
else
|
|
{
|
|
self.selectedRange = NSMakeRange(cursorLocation, CYRAbs(startRange.location - cursorLocation));
|
|
}
|
|
}
|
|
|
|
#pragma mark - NSLayoutManagerDelegate
|
|
|
|
- (CGFloat)layoutManager:(NSLayoutManager *)layoutManager lineSpacingAfterGlyphAtIndex:(NSUInteger)glyphIndex withProposedLineFragmentRect:(CGRect)rect
|
|
{
|
|
return 10.0f;
|
|
}
|
|
|
|
@end
|