/src/Three20UI/Sources/TTTextEditor.m
Objective C | 486 lines | 283 code | 123 blank | 80 comment | 36 complexity | f698dcef1789c1a99690b2fc85b9aaa2 MD5 | raw file
- //
- // Copyright 2009-2011 Facebook
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- //
- #import "Three20UI/TTTextEditor.h"
- // UI
- #import "Three20UI/TTTextEditorDelegate.h"
- #import "Three20UI/UIViewAdditions.h"
- #import "Three20Style/UIFontAdditions.h"
- // UI (private)
- #import "Three20UI/private/TTTextView.h"
- #import "Three20UI/private/TTTextEditorInternal.h"
- // Style
- #import "Three20Style/TTGlobalStyle.h"
- #import "Three20Style/TTDefaultStyleSheet.h"
- // Core
- #import "Three20Core/TTCorePreprocessorMacros.h"
- #import "Three20Core/TTGlobalCoreRects.h"
- static const CGFloat kPaddingX = 8.0f;
- static const CGFloat kPaddingY = 9.0f;
- // XXXjoe This number is very sensitive - it is specifically calculated for precise word wrapping
- // with 15pt normal helvetica. If you change this number at all, UITextView may wrap the text
- // before or after the TTTextEditor expands or contracts its height to match. Obviously,
- // hard-coding this value here sucks, and I need to implement a solution that works for any font.
- static const CGFloat kTextViewInset = 31.0f;
- static const CGFloat kUITextViewVerticalPadding = 6.0f;
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- @implementation TTTextEditor
- @synthesize minNumberOfLines = _minNumberOfLines;
- @synthesize maxNumberOfLines = _maxNumberOfLines;
- @synthesize editing = _editing;
- @synthesize autoresizesToText = _autoresizesToText;
- @synthesize showsExtraLine = _showsExtraLine;
- @synthesize delegate = _delegate;
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (id)initWithFrame:(CGRect)frame {
- self = [super initWithFrame:frame];
- if (self) {
- _internal = [[TTTextEditorInternal alloc] initWithTextEditor:self];
- _autoresizesToText = YES;
- _textField = [[UITextField alloc] init];
- _textField.delegate = _internal;
- [self addSubview:_textField];
- }
- return self;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)dealloc {
- TT_RELEASE_SAFELY(_internal);
- TT_RELEASE_SAFELY(_textField);
- TT_RELEASE_SAFELY(_textView);
- [super dealloc];
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- #pragma mark -
- #pragma mark Private
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (UIResponder*)activeTextField {
- if (_textView && !_textView.hidden) {
- return _textView;
- } else {
- return _textField;
- }
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)createTextView {
- if (!_textView) {
- _textView = [[TTTextView alloc] init];
- _textView.delegate = _internal;
- _textView.editable = YES;
- _textView.backgroundColor = [UIColor clearColor];
- _textView.scrollsToTop = NO;
- _textView.showsHorizontalScrollIndicator = NO;
- // UITextViews have extra padding on the top and bottom that we don't want, so we force
- // the content to take up slightly more space. This allows us to mimic the padding of the
- // UITextLabel control.
- _textView.contentInset = UIEdgeInsetsMake(
- -kUITextViewVerticalPadding, 0,
- -kUITextViewVerticalPadding, 0);
- _textView.font = _textField.font;
- _textView.autoresizesToText = _autoresizesToText;
- _textView.textColor = _textField.textColor;
- _textView.autocapitalizationType = _textField.autocapitalizationType;
- _textView.autocorrectionType = _textField.autocorrectionType;
- _textView.enablesReturnKeyAutomatically = _textField.enablesReturnKeyAutomatically;
- _textView.keyboardAppearance = _textField.keyboardAppearance;
- _textView.keyboardType = _textField.keyboardType;
- _textView.returnKeyType = _textField.returnKeyType;
- _textView.secureTextEntry = _textField.secureTextEntry;
- [self addSubview:_textView];
- }
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (CGFloat)heightThatFits:(BOOL*)overflowed numberOfLines:(NSInteger*)numberOfLines {
- CGFloat ttLineHeight = self.font.ttLineHeight;
- CGFloat minHeight = _minNumberOfLines * ttLineHeight;
- CGFloat maxHeight = _maxNumberOfLines * ttLineHeight;
- CGFloat maxWidth = self.width - kTextViewInset;
- NSString* text = _textField.hidden ? _textView.text : _textField.text;
- if (!text.length) {
- text = @"M";
- }
- CGSize textSize = [text sizeWithFont:self.font
- constrainedToSize:CGSizeMake(maxWidth, CGFLOAT_MAX)
- lineBreakMode:UILineBreakModeWordWrap];
- CGFloat newHeight = textSize.height;
- if ([text characterAtIndex:text.length-1] == 10) {
- newHeight += ttLineHeight;
- }
- if (_showsExtraLine) {
- newHeight += ttLineHeight;
- }
- if (overflowed) {
- *overflowed = maxHeight && newHeight > maxHeight;
- }
- if (numberOfLines) {
- *numberOfLines = floor(newHeight / ttLineHeight);
- }
- if (newHeight < minHeight) {
- newHeight = minHeight;
- }
- if (maxHeight && newHeight > maxHeight) {
- newHeight = maxHeight;
- }
- return newHeight + kPaddingY*2;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)stopIgnoringBeginAndEnd {
- _internal.ignoreBeginAndEnd = NO;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)constrainToText {
- NSInteger numberOfLines = 0;
- CGFloat oldHeight = self.height;
- CGFloat newHeight = [self heightThatFits:&_overflowed numberOfLines:&numberOfLines];
- CGFloat diff = newHeight - oldHeight;
- if (numberOfLines > 1 && !_textField.hidden) {
- [self createTextView];
- _textField.hidden = YES;
- _textView.hidden = NO;
- _textView.text = _textField.text;
- _internal.ignoreBeginAndEnd = YES;
- [_textView becomeFirstResponder];
- [self performSelector:@selector(stopIgnoringBeginAndEnd) withObject:nil afterDelay:0];
- } else if (numberOfLines == 1 && _textField.hidden) {
- _textField.hidden = NO;
- _textView.hidden = YES;
- _textField.text = _textView.text;
- _internal.ignoreBeginAndEnd = YES;
- [_textField becomeFirstResponder];
- [self performSelector:@selector(stopIgnoringBeginAndEnd) withObject:nil afterDelay:0];
- }
- _textView.overflowed = _overflowed;
- _textView.scrollEnabled = _overflowed;
- if (oldHeight && diff) {
- if ([_delegate respondsToSelector:@selector(textEditor:shouldResizeBy:)]) {
- if (![_delegate textEditor:self shouldResizeBy:diff]) {
- return;
- }
- }
- self.frame = TTRectContract(self.frame, 0, -diff);
- }
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)didBeginEditing {
- _editing = YES;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)didEndEditing {
- _editing = NO;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)didChangeText:(BOOL)insertReturn {
- if (insertReturn) {
- [self createTextView];
- _textField.hidden = YES;
- _textView.hidden = NO;
- _textView.text = [_textField.text stringByAppendingString:@"\n"];
- _internal.ignoreBeginAndEnd = YES;
- [_textView becomeFirstResponder];
- [self performSelector:@selector(stopIgnoringBeginAndEnd) withObject:nil afterDelay:0];
- }
- if (_autoresizesToText) {
- [self constrainToText];
- }
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- #pragma mark -
- #pragma mark UIResponder
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (BOOL)isFirstResponder {
- return [[self activeTextField] isFirstResponder];
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (BOOL)canBecomeFirstResponder {
- return [[self activeTextField] canBecomeFirstResponder];
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (BOOL)becomeFirstResponder {
- return [[self activeTextField] becomeFirstResponder];
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (BOOL)resignFirstResponder {
- return [[self activeTextField] resignFirstResponder];
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- #pragma mark -
- #pragma mark UIView
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)layoutSubviews {
- CGRect frame = CGRectMake(0, 2, self.width-kPaddingX*2, self.height);
- _textView.frame = CGRectOffset(TTRectContract(frame, 0, 14), 0, 7);
- _textField.frame = CGRectOffset(TTRectContract(frame, 9, 14), 9, 7);
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (CGSize)sizeThatFits:(CGSize)size {
- CGFloat height = [self heightThatFits:nil numberOfLines:nil];
- return CGSizeMake(size.width, height);
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- #pragma mark -
- #pragma mark UITextInputTraits
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (UITextAutocapitalizationType)autocapitalizationType {
- return _textField.autocapitalizationType;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)setAutocapitalizationType:(UITextAutocapitalizationType)autocapitalizationType {
- _textField.autocapitalizationType = autocapitalizationType;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (UITextAutocorrectionType)autocorrectionType {
- return _textField.autocorrectionType;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)setAutocorrectionType:(UITextAutocorrectionType)autocorrectionType {
- _textField.autocorrectionType = autocorrectionType;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (BOOL)enablesReturnKeyAutomatically {
- return _textField.enablesReturnKeyAutomatically;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)setEnablesReturnKeyAutomatically:(BOOL)enablesReturnKeyAutomatically {
- _textField.enablesReturnKeyAutomatically = enablesReturnKeyAutomatically;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (UIKeyboardAppearance)keyboardAppearance {
- return _textField.keyboardAppearance;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)setKeyboardAppearance:(UIKeyboardAppearance)keyboardAppearance {
- _textField.keyboardAppearance = keyboardAppearance;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (UIKeyboardType)keyboardType {
- return _textField.keyboardType;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)setKeyboardType:(UIKeyboardType)keyboardType {
- _textField.keyboardType = keyboardType;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (UIReturnKeyType)returnKeyType {
- return _textField.returnKeyType;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)setReturnKeyType:(UIReturnKeyType)returnKeyType {
- _textField.returnKeyType = returnKeyType;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (BOOL)secureTextEntry {
- return _textField.secureTextEntry;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)setSecureTextEntry:(BOOL)secureTextEntry {
- _textField.secureTextEntry = secureTextEntry;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- #pragma mark -
- #pragma mark Public
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)setDelegate:(id<TTTextEditorDelegate>)delegate {
- _delegate = delegate;
- _internal.delegate = delegate;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (NSString*)text {
- if (_textView && !_textView.hidden) {
- return _textView.text;
- } else {
- return _textField.text;
- }
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)setText:(NSString*)text {
- _textField.text = _textView.text = text;
- if (_autoresizesToText) {
- [self constrainToText];
- }
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (NSString*)placeholder {
- return _textField.placeholder;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)setPlaceholder:(NSString*)placeholder {
- _textField.placeholder = placeholder;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)setAutoresizesToText:(BOOL)autoresizesToText {
- _autoresizesToText = autoresizesToText;
- _textView.autoresizesToText = _autoresizesToText;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (UIFont*)font {
- return _textField.font;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)setFont:(UIFont*)font {
- _textField.font = font;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (UIColor*)textColor {
- return _textField.textColor;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)setTextColor:(UIColor*)textColor {
- _textField.textColor = textColor;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)scrollContainerToCursor:(UIScrollView*)scrollView {
- if (_textView.hasText) {
- if (scrollView.contentSize.height > scrollView.height) {
- NSRange range = _textView.selectedRange;
- if (range.location == _textView.text.length) {
- [scrollView scrollRectToVisible:CGRectMake(0,scrollView.contentSize.height-1,1,1)
- animated:NO];
- }
- } else {
- [scrollView scrollRectToVisible:CGRectMake(0,0,1,1) animated:NO];
- }
- }
- }
- @end