If a NSTextView
contains the following:
SELECT someTable.someColumn FROM someTable
And a user double-clicks someTable.someColumn
, the entire thing gets selected (both sides of the period). In this specific case (a query editor), it would make more sense for either the someTable
or the someColumn
to be selected.
I've tried looking around to see if I can figure out a way to customize the selection, but I have been unable to so far.
At the moment what I'm thinking of doing is subclassing NSTextView
and doing something such as:
- (void)mouseDown:(NSEvent *)theEvent
{
if(theEvent.clickCount == 2)
{
// TODO: Handle double click selection.
}
else
{
[super mouseDown:theEvent];
}
}
Does anyone have any thoughts or alternatives to this? (Is there another method I am missing that may be better for overriding)?
First of all, contrary to a previous answer, NSTextView
's selectionRangeForProposedRange:granularity:
method is not the correct place to override to achieve this. In Apple's "Cocoa Text Architecture" doc (https://developer.apple.com/library/prerelease/mac/documentation/TextFonts/Conceptual/CocoaTextArchitecture/TextEditing/TextEditing.html – see the "Subclassing NSTextView" section) Apple states explicitly "These mechanisms aren’t meant for changing language word definitions (such as what’s selected by a double click)." I'm not sure why Apple feels that way, but I suspect it is because selectionRangeForProposedRange:granularity:
does not get any information regarding what part of the proposed range is the initial click point, versus what part is a place the user dragged to; making double-click-drags behave correctly might be hard to do with an override of this method. Perhaps there are other issues as well, I don't know; the doc is a bit cryptic. Perhaps Apple plans to make changes to the selection mechanism later that would break such overrides. Perhaps there are other aspects of defining what a "word" is that overriding here fails to address. Who knows; but it is generally a good idea to follow Apple's instructions when they make a statement like this.
Oddly, Apple's doc goes on to say "That detail of selection is handled at a lower (and currently private) level of the text system." I think that is outdated, because in fact the needed support does exist: the doubleClickAtIndex:
method on NSAttributedString
(in the NSAttributedStringKitAdditions
category). This method is used (in the NSTextStorage
subclass of NSAttributedString
) by the Cocoa text system to determine word boundaries. Subclassing NSTextStorage
is a bit tricky, so I'll provide a full implementation here for a subclass called MyTextStorage
. Much of this code for subclassing NSTextStorage
comes from Ali Ozer at Apple.
In MyTextStorage .h
:
@interface MyTextStorage : NSTextStorage
- (id)init;
- (id)initWithAttributedString:(NSAttributedString *)attrStr;
@end
In MyTextStorage.m
:
@interface MyTextStorage ()
{
NSMutableAttributedString *contents;
}
@end
@implementation MyTextStorage
- (id)initWithAttributedString:(NSAttributedString *)attrStr
{
if (self = [super init])
{
contents = attrStr ? [attrStr mutableCopy] : [[NSMutableAttributedString alloc] init];
}
return self;
}
- init
{
return [self initWithAttributedString:nil];
}
- (void)dealloc
{
[contents release];
[super dealloc];
}
// The next set of methods are the primitives for attributed and mutable attributed string...
- (NSString *)string
{
return [contents string];
}
- (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRange *)range
{
return [contents attributesAtIndex:location effectiveRange:range];
}
- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str
{
NSUInteger origLen = [self length];
[contents replaceCharactersInRange:range withString:str];
[self edited:NSTextStorageEditedCharacters range:range changeInLength:[self length] - origLen];
}
- (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range
{
[contents setAttributes:attrs range:range];
[self edited:NSTextStorageEditedAttributes range:range changeInLength:0];
}
// And now the actual reason for this subclass: to provide code-aware word selection behavior
- (NSRange)doubleClickAtIndex:(NSUInteger)location
{
// Start by calling super to get a proposed range. This is documented to raise if location >= [self length]
// or location < 0, so in the code below we can assume that location indicates a valid character position.
NSRange superRange = [super doubleClickAtIndex:location];
NSString *string = [self string];
// If the user has actually double-clicked a period, we want to just return the range of the period.
if ([string characterAtIndex:location] == '.')
return NSMakeRange(location, 1);
// The case where super's behavior is wrong involves the dot operator; x.y should not be considered a word.
// So we check for a period before or after the anchor position, and trim away the periods and everything
// past them on both sides. This will correctly handle longer sequences like foo.bar.baz.is.a.test.
NSRange candidateRangeBeforeLocation = NSMakeRange(superRange.location, location - superRange.location);
NSRange candidateRangeAfterLocation = NSMakeRange(location + 1, NSMaxRange(superRange) - (location + 1));
NSRange periodBeforeRange = [string rangeOfString:@"." options:NSBackwardsSearch range:candidateRangeBeforeLocation];
NSRange periodAfterRange = [string rangeOfString:@"." options:(NSStringCompareOptions)0 range:candidateRangeAfterLocation];
if (periodBeforeRange.location != NSNotFound)
{
// Change superRange to start after the preceding period; fix its length so its end remains unchanged.
superRange.length -= (periodBeforeRange.location + 1 - superRange.location);
superRange.location = periodBeforeRange.location + 1;
}
if (periodAfterRange.location != NSNotFound)
{
// Change superRange to end before the following period
superRange.length -= (NSMaxRange(superRange) - periodAfterRange.location);
}
return superRange;
}
@end
And then the last part is actually using your custom subclass in your textview. If you have an NSTextView subclass as well, you can do this in its awakeFromNib method; otherwise, do this wherever else you get a chance, right after your nib loads; in the awakeFromNib call for a related window or controller, for example, or simply after your call to load the nib that contains the textview. In any case, you want to do this (where textview is your NSTextView
object):
[[textview layoutManager] replaceTextStorage:[[[MyTextStorage alloc] init] autorelease]];
And with that, you should be good to go, unless I've made a mistake in transcibing this!
Finally, note that there is another method in NSAttributedString
, nextWordFromIndex:forward:
, that is used by Cocoa's text system when the user moves the insertion point to the next/previous word. If you want that sort of thing to follow the same word definition, you will need to subclass it as well. For my application I did not do that – I wanted next/previous word to move over whole a.b.c.d sequences (or more accurately I just didn't care) – so I don't have an implementation of that to share here. Left as an exercise for the reader.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With