Programmatically selecting a range of characters in a text field

Hello,

I have a simple application that has a preference panel in it’s own NIB/XIB file. During runtime this preference panel is called attached to the main window. On this preference panel there is a text field where one can enter some AppleScript commands as normal text. Those AppleScript statements are passed as the direct paramater to a “run script” command later in the main window .applescript source code.
So far so good.

Now I have a button near the text field, which when pressed will check the syntax of those AppleScript commands and in case those are not valid syntax, it will return a checkResult record containing error number, the error message, brief error message, a boolean value indicating the compilation result, and the character range where the error occured, stored in two numbers (from - to).
I added this functionality by adding a Cocoa Category to NSApplication and adding my custom checkSyntaxAndReturnErrorInfo Cocoa method that I call with the “call method” command in the .applescript source code. That’s the background.

What I am now looking for is a way to use the two numbers in the checkResult record to programmatically select the text in the text field and maybe change the “text color” property for those characters to red.

The problem, as I see it, is that the AppleScriptKit.sdef does not list a way to do some adjustment for text contained in a text field based on a character or word level.

So I am hoping there is maybe a hack of some sort that allows changing properties of text field content on a character level. If I were able to add instance variables to a Cocoa Category I could maybe use Cocoa Bindings to change the affected Value and Text Color for the text field but I see no way to do all that in a custom Category method without being able to set an IBOutlet so that my custom Cocoa method knows which text field instance I mean.

Whether you decide to reply or not, let me still thank you very much for your valued time reading through my post.

André

Model: Mac Pro 2008
AppleScript: 2.01
Browser: Firefox 3.0.1
Operating System: Mac OS X (10.5)

I agree. I couldn’t get it to work either… but I could get highlighting to work in a text view… so if you can change your text fields to text views then this will work.

Here’s what to do. In IB place a text view in a window. Give the scroll view the applescript name “sv” and the text view the applescript name “tv”. The add a button and hook it to the ‘on clicked’ handler. Here’s the code. It will add the text “some text” to the text view. It will make that text all black. Then it will search the text view for the hlWord “some” and turn it red.

on clicked theObject
	set myTextView to text view "tv" of scroll view "sv" of window 1
	set content of myTextView to "some text"
	set color of text of myTextView to {0, 0, 0}
	
	set hlWord to "some"
	set tfTextRef to (a reference to (words of myTextView))
	repeat with i from 1 to count of tfTextRef
		if (item i of tfTextRef) contains hlWord then
			set color of (item i of tfTextRef) to {65535, 0, 0}
		end if
	end repeat
end clicked

Thank you very much for posting your solution :slight_smile: Using your example I could get the highlighting to work smoothly.
The text view class doesn’t seem to have an “enabled” property. That’s not much of a problem however, the behaviour of text field’s enabled property is easily faked using the editable property of text view and setting text color to something about {50069, 50069, 50069} which makes it a light grey.

The only thing thats keeping me from using text views all the way is that I couldn’t find a way to disable the vertical arranging behaviour (e.g. spanning text across multiple lines) if the text view is too small to have the text fit in. Which is irritating because from the design the scroll view needs to look like a normal one-line text field that can be expanded in width.
As it is now, if the user places a mouse cursor inside the text view and drags downwards the text begins to disappear at the upper edge of the text view. I found a way to disable the vertical scroll bar of the scroll view container, I could disable the font and colors panel, rich text capability and what not, I could even fixate the font type and size using cocoa bindings which is very nice but you wouldn’t happen to know a way to disable the multiple lines behaviour of text views?

In that context, if I may ask one last question, what does the text views Width and Height (under Text View Size) do? The label is obvious and the tooltip is quite generic (“sets the recievers width”) but playing around with the values doesn’t seem to change anything. I also tried zeroing out Line Scroll and Page Scroll of the scroll views attributes, but as far as I could tell this didn’t have any effect either.

Again, thank you very much for helping.

André

Well that’s funny. Back on August 5th I actually worked on this situation for one of my projects. I wrote an objective-c class to make a text view not wrap its text. So here’s how to do that… add a new file to your project and make it an “objective-c class”. Create both the “.h” file and the “.m” file. Add the following to those two files. I named my files “TextViewWrap”.

======= TextViewWrap.h =========

#import <Cocoa/Cocoa.h>
@interface TextViewWrap : NSObject {
}
+(void)turnWrappingOff:(NSTextView *)textView;
@end

======= TextViewWrap.m =========

#import "TextViewWrap.h"

const float LargeNumberForText = 1.0e7;

@implementation TextViewWrap
+(void)turnWrappingOff:(NSTextView *)textView {
	// configure the scroll view
	NSScrollView *scrollView = [textView enclosingScrollView];
	[scrollView setHasVerticalScroller:NO];
    [scrollView setHasHorizontalScroller:NO];
	
	// configure the text container
	NSTextContainer *textContainer = [textView textContainer];
	[textContainer setWidthTracksTextView: NO];
	[textContainer setHeightTracksTextView:NO];
	NSSize size = [textContainer containerSize];
	size.width = LargeNumberForText;
	size.height = LargeNumberForText;
	[textContainer setContainerSize: size];
	
	// configure the text view
	NSRect frame = [textView frame];
	[textView setMinSize:frame.size];
    [textView setMaxSize:NSMakeSize(LargeNumberForText, LargeNumberForText)];
	[textView setHorizontallyResizable:YES];
    [textView setVerticallyResizable:YES];
}
@end

Then in Interface Builder I hooked the text view up to the ‘awake from nib’ handler. Then in my applescript code I added the following to that handler.

on awake from nib theObject
	if name of theObject is "textView" then
		call method "turnWrappingOff:" of class "TextViewWrap" with parameter theObject
	end if
end awake from nib

This should make the text view behave as you want. Note that you can play with the following two lines in the objective-c code to turn the scroll bars on/off. I actually liked having the horizontal scroll bar on. You can set the value to YES or NO.

[scrollView setHasVerticalScroller:NO];
[scrollView setHasHorizontalScroller:NO];

I hope this helps you.

Oh my god, you’re a genious! :smiley: This is it! … you know your code answers one of the biggest questions yet I had about AppleScript Studio and Cocoa integration… I was never aware you could just pass theObject from AppleScript code into a Cocoa method. That’s how you make the connection that you would otherwise do with IBOutlets in InterfaceBuilder.

Even if you could add any instance variables to a Cocoa Category (on, say, NSApplication), you don’t need them… just look for an event you want the connection to happen in, write your custom method and pass theObject as paramater. This is so exciting!

Thanks for taking the time to post your examples! I’m gonna implement this right away. (I mean, if that’s ok with you. The project is of personal nature and might be public domain eventually. Might happen if and when I get my website up and running ;). Of course you will be named as the sole creator of these examples).

André

Yes. Having the capability to use obj-c in combination with applescript is very exciting and powerful. Now I understand what you said in your original post… “but I see no way to do all that in a custom Category method without being able to set an IBOutlet”. I didn’t understand what you meant by that but now I do. I’m glad I could help you with that.

You’re free to use this code however you wish with no restrictions. If you credit me that would be nice but it’s not necessary. Lots of people help me and I’m glad to be able to contribute back. That’s the only thing I would ask of you, is that you help others as you have been helped.

Good luck with your project.

Thank you very much. Rest assured, as my knowledge of AppleScript and Xcode grows larger, I will always strive to live up to that promise.

Good luck with your own projects as well :slight_smile:

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.

  • (void) showInsertionPoint:(NSTextView *)textView {

    if ([textView isEditable]) {
    [textView setInsertionPointColor:[NSColor blackColor]];
    //[textView setNeedsDisplay:YES];
    }

}

  • (void) hideInsertionPoint:(NSTextView *)textView {

    [textView setInsertionPointColor:[NSColor whiteColor]];
    //[textView setNeedsDisplay:YES];

}

// 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) disableInsertionPoint:(NSTextView *)textView {

    [textView lockFocus];
    [textView drawInsertionPointInRect:[textView frame] color:[NSColor blackColor] turnedOn:NO];
    [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:

  1. Create a new Objective-C class file and name it TextViewDelegate.h / .m (use whatever name pleases you)
  2. 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.
  3. 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.
  4. Save the NIB/XIB. Just to be sure…
  5. 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.

Reserved Space for Future Additions

Last Update for TextViewExtensions:
2008-09-29

Last Update for TextViewDelegate:
2008-09-17