ok after playing around with this I now have the text view behaving almost a 100% like a text field. It doesn’t track the height of it’s text container, it intercepts newline insertion and fakes a disabled look upon setting editable and selectable to false.
I just wanted to share with you guys my TextViewExtensions class, which is based on Hank’s example above:
TextViewExtensions.h
[code]//
// TextViewExtensions.h
//
// Created by Andre Berg on 10.09.08.
// Copyright 2008 Berg Media. All rights reserved.
//
// Some methods have explanatory comments.
// Please see the implemenation part (.m file).
//
#import <Cocoa/Cocoa.h>
@interface TextViewExtensions : NSObject {
}
-
(void) turnWrappingOff:(NSTextView *)textView;
-
(void) enableInsertionPoint:(NSTextView *)textView;
-
(void) disableInsertionPoint:(NSTextView *)textView;
-
(void) showInsertionPoint:(NSTextView *)textView;
-
(void) hideInsertionPoint:(NSTextView *)textView;
-
(void) changeInsertionPointColor:(NSArray *)newColor inTextView:(NSTextView *)textView;
-
(void) selectRangeOfText:(NSRange)range inTextView:(NSTextView *)textView;
-
(void) deselectAllTextInTextView:(NSTextView *)textView;
-
(void) changeTextColorInTextView:(NSTextView *)textView inRange:(NSRange)range toColor:(NSArray *)color;
-
(void) changeTextColorForMultipleRangesInTextView:(NSTextView *)textView rangeColorTuplesArray:(NSArray *)tuples;
-
(void) changeSelectedTextColorInTextView:(NSTextView *)textView toColor:(NSArray *)color;
-
(void) changeFocusRingType:(NSString *)newRingType forControl:(id)control;
-
(void) changeBorderType:(NSString *)newBorderType forControl:(id)control;
-
(void) lowerBaselineByAmount:(int)points inTextView:(NSTextView *)textView needsUndoDisabled:(BOOL)noUndo;
-
(void) raiseBaselineByAmount:(int)points inTextView:(NSTextView *)textView needsUndoDisabled:(BOOL)noUndo;
-
(void) setHiddenState:(BOOL)state inTextView:(NSTextView *)textView;
-
(void) toggleHiddenState:(NSTextView *)textView;
-
(BOOL) hasHiddenState:(NSTextView *)textView;
@end[/code]
TextViewExtensions.m
[code]//
// TextViewExtensions.m
//
// Created by Andre Berg on 10.09.08.
// Copyright 2008 Berg Media. All rights reserved.
//
#import “TextViewExtensions.h”
const float LargeNumberForText = 1.0e7;
@implementation TextViewExtensions
-
(void) turnWrappingOff:(NSTextView *)textView {
//[[textView undoManager] disableUndoRegistration];
// configure the scroll view
NSScrollView * scrollView = [textView enclosingScrollView];
[scrollView setHasVerticalScroller:NO];
[scrollView setHasHorizontalScroller:NO];
[scrollView setFocusRingType:NSFocusRingTypeExterior];
// configure the text container
NSTextContainer * textContainer = [textView textContainer];
[[textContainer layoutManager] setUsesScreenFonts:NO];
[textContainer setWidthTracksTextView:NO];
[textContainer setHeightTracksTextView:NO];
[textContainer setContainerSize:NSMakeSize(LargeNumberForText, LargeNumberForText)];
//NSLog(@“textContainer %@ lineFragmentPadding = %f”, [textContainer description], [textContainer lineFragmentPadding]);
// configure the text view
// Note: most of these can be set in InterfaceBuilder too
//[textView setMinSize:[textView frame].size];
//[textView setMaxSize:NSMakeSize(LargeNumberForText, LargeNumberForText)];
//[textView setHorizontallyResizable:YES];
//[textView setVerticallyResizable:NO];
//[textView lowerBaseline:nil];
//[textView lowerBaseline:nil];
//[[textView undoManager] enableUndoRegistration];
}
// there’s two ways of hiding the insertion point
// one just changes the color from black to white. this ofc depends on the background being white, too.
}
}
// the other is locking the focus (otherwise the draw… method won’t do a thing)
// and really turning off the insertion point
-
(void) enableInsertionPoint:(NSTextView *)textView {
if ([textView isEditable]) {
[textView lockFocus];
[textView drawInsertionPointInRect:[textView frame] color:[NSColor blackColor] turnedOn:YES];
[textView unlockFocus];
}
}
}
-
(void) changeInsertionPointColor:(NSArray *)newColor inTextView:(NSTextView *)textView {
float red = [[newColor objectAtIndex:0] floatValue];
float green = [[newColor objectAtIndex:1] floatValue];
float blue = [[newColor objectAtIndex:2] floatValue];
float alpha = [[newColor objectAtIndex:3] floatValue];
[textView setInsertionPointColor:[NSColor colorWithDeviceRed:red green:green blue:blue alpha:alpha]];
}
-
(void) selectRangeOfText:(NSRange)range inTextView:(NSTextView *)textView {
[textView lockFocus];
[textView setSelectedRange:range];
[textView unlockFocus];
}
-
(void) deselectAllTextInTextView:(NSTextView *)textView {
[textView lockFocus];
[textView setSelectedRange:NSMakeRange(0, 0)];
[textView unlockFocus];
}
// this is a just a drop-in for the “change color of characters x thru y of text of text view” command sequence
// which may or may not execute faster than the AppleScriptKit.sdef native method.
// for the color param in AS you supply a list with four real numbers like {1.0, 0.0, 0.0, 1.0} (would make it a red color)
-
(void) changeTextColorInTextView:(NSTextView *)textView inRange:(NSRange)range toColor:(NSArray *)color {
float red = [[color objectAtIndex:0] floatValue];
float green = [[color objectAtIndex:1] floatValue];
float blue = [[color objectAtIndex:2] floatValue];
float alpha = [[color objectAtIndex:3] floatValue];
[textView setTextColor:[NSColor colorWithDeviceRed:red green:green blue:blue alpha:alpha] range:range];
}
// this is an extension to the “change color of characters x thru y of text of text view” command sequence
// It expects an array/list of range-color tuples. A range-color tuple is comprised of two components, which in turn are lists.
// One list for the range values and one for the color values. You can pass an arbitrary number of tuples for the range-color tuples list.
// A range value consists of two integer number components {from, to} and a color value consists of four real number components
// {red, green, blue, alpha}. For example to color characters 0 thru 5 with red color and characters 10 thru 20 with green color:
// set redColor to {1.0, 0.0, 0.0, 1.0}
// set greenColor to {0.0, 1.0, 0.0, 1.0}
// set theRangeColorTuplesList to {{{0, 5},redColor}, {{10, 20},greenColor}}
// call method “changeTextColorForMultipleRangesInTextView:rangeColorTuplesArray:” of class “TextViewColor” with parameters {theObject, theRangeColorTuplesList}
-
(void) changeTextColorForMultipleRangesInTextView:(NSTextView *)textView rangeColorTuplesArray:(NSArray *)tuples {
if (tuples) {
NSUInteger count = [tuples count], i = 0;
// then process the variadic arguments
while (i < count) {
NSArray * currentTuple = [tuples objectAtIndex:i];
NSArray * rangeArray = [currentTuple objectAtIndex:0];
NSArray * colorArray = [currentTuple objectAtIndex:1];
float red = [[colorArray objectAtIndex:0] floatValue];
float green = [[colorArray objectAtIndex:1] floatValue];
float blue = [[colorArray objectAtIndex:2] floatValue];
float alpha = [[colorArray objectAtIndex:3] floatValue];
[textView setTextColor:[NSColor colorWithDeviceRed:red green:green blue:blue alpha:alpha]
range:NSMakeRange([[rangeArray objectAtIndex:0] intValue], [[rangeArray objectAtIndex:1] intValue])];
i++;
}
}
}
// changes the color of the selected text
-
(void) changeSelectedTextColorInTextView:(NSTextView *)textView toColor:(NSArray *)color {
float red = [[color objectAtIndex:0] floatValue];
float green = [[color objectAtIndex:1] floatValue];
float blue = [[color objectAtIndex:2] floatValue];
float alpha = [[color objectAtIndex:3] floatValue];
[textView setTextColor:[NSColor colorWithDeviceRed:red green:green blue:blue alpha:alpha]
range:[textView selectedRange]];
}
// this method accepts a generic paramater forControl because sometimes
// one needs to change a control itself (NSTextField) and sometimes its enclosing control (NSTextView > NSScrollView
-
(void) changeFocusRingType:(NSString *)newRingType forControl:(id)control {
NSFocusRingType ringType;
if ([newRingType isEqualToString:@“none”]) {
ringType = NSFocusRingTypeNone;
} else if ([newRingType isEqualToString:@“exterior”]) {
ringType = NSFocusRingTypeExterior;
} else {
ringType = NSFocusRingTypeDefault;
}
if ([control respondsToSelector:@selector(setFocusRingType:)]) {
[control setFocusRingType];
}
}
// this method accepts a generic paramater forControl because sometimes
// one needs to change a control itself (NSTextField) and sometimes its enclosing control (NSTextView > NSScrollView)
// Note: AppleScriptKit’s scroll view class has a border type property but you could still use this
// method to do some custom adjustments when the border changes
-
(void) changeBorderType:(NSString *)newBorderType forControl:(id)control {
NSBorderType borderType;
if ([newBorderType isEqualToString:@“none”]) {
borderType = NSNoBorder;
} else if ([newBorderType isEqualToString:@“line”]) {
borderType = NSLineBorder;
} else if ([newBorderType isEqualToString:@“groove”]) {
borderType = NSGrooveBorder;
} else {
borderType = NSBezelBorder;
}
if ([control respondsToSelector:@selector(setBorderType:)]) {
[control setBorderType:borderType];
}
}
// undo needs to be disabled if the text view has Undo turned on in Interface Builder
// and the method is called from an early handler like ‘awake from nib’, or ‘will finish launching’
// as this will cause the textView’s UndoManager to get out of sync.
-
(void) lowerBaselineByAmount:(int)points inTextView:(NSTextView *)textView needsUndoDisabled:(BOOL)noUndo {
if (noUndo) {
[[textView undoManager] disableUndoRegistration];
}
int i;
for (i=0; i < points; i++) {
[textView lowerBaseline:nil];
}
if (noUndo) {
[[textView undoManager] enableUndoRegistration];
}
}
-
(void) raiseBaselineByAmount:(int)points inTextView:(NSTextView *)textView needsUndoDisabled:(BOOL)noUndo {
if (noUndo) {
[[textView undoManager] disableUndoRegistration];
}
int i;
for (i=0; i < points; i++) {
[textView raiseBaseline:nil];
}
if (noUndo) {
[[textView undoManager] enableUndoRegistration];
}
}
-
(void) setHiddenState:(BOOL)state inTextView:(NSTextView *)textView {
//NSLog(@"%@ isKindOfClass NSTextView = %i", [textView description], [textView isKindOfClass:[NSTextView class]]);
[textView setHidden:state];
}
-
(void) toggleHiddenState:(NSTextView *)textView {
if (![textView isHiddenOrHasHiddenAncestor]) {
[textView setHidden:YES];
} else {
[textView setHidden:NO];
}
}
-
(BOOL) hasHiddenState:(NSTextView *)textView {
return [textView isHiddenOrHasHiddenAncestor];
}
@end[/code]
Limiting input to the text view is a bit trickier, but just a tad… you need to setup a delegate for the text view:
- Create a new Objective-C class file and name it TextViewDelegate.h / .m (use whatever name pleases you)
- In InterfaceBuilder drag a NSObject to your Document window (Cmd+0) and on the Identity inspector tab set its class identity to your newly created class.
- Control-Drag from the text view in your NIB/XIB to the the Document window with the NSObject instance representing the class you created and let go. From the popup menu select “delegate”. Your NSObject should blink to confirm the connection.
- Save the NIB/XIB. Just to be sure…
- Back in Xcode add the following code to the still empty .h/.m files
TextViewDelegate.h
[code]#import <Cocoa/Cocoa.h>
@interface TextViewDelegate : NSObject {
}
// MARK: Delegate Methods
- (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector;
- (NSRange)textView:(NSTextView *)textView willChangeSelectionFromCharacterRange:(NSRange)oldRange toCharacterRange:(NSRange)newRange;
@end
[b]TextViewDelegate.m[/b]
#import “TextViewDelegate.h”
@implementation TextViewDelegate
// MARK: Delegate Methods
// intercept insertText:, insertNewLine:, insertTab: etc…
-
(BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector {
// From the docs: If the delegate implements this method and returns YES,
// the text view does nothing further; if the delegate returns NO, the text view
// must try to perform the command itself
BOOL dontPerform = NO;
if (commandSelector == @selector(insertNewline:)) {
dontPerform = YES;
// return key-state to the window
[[textView window] makeFirstResponder:self];
}
return dontPerform;
}
// use this to modify the selection when the user does something that changes the selection
// for example restrict the selection to some words but not others, etc.
- (NSRange)textView:(NSTextView *)textView willChangeSelectionFromCharacterRange:(NSRange)oldRange toCharacterRange:(NSRange)newRange {
return NSMakeRange(newRange.location, newRange.length);
}
@end[/code]
If you want to intercept different keystrokes than newlines, for example a tab, change the selector to insertTab:
If you want to intercept normal keystrokes (a, b, c, d, etc.) user insertText: instead. You could use this to restrict the user to only type in valid patterns.
Used in conjunction with a NSPredicate and SELF MATCHES you can catch quite a many patterns and deal with them.
André
P.S. Unfortunately the indentation is messed up in the examples above, but it should be transformed back to an acceptible reading state when copy+pasted in Xcode with Automatic Indentation on.