NSTextView customizing double click selection

╄→尐↘猪︶ㄣ 提交于 2020-01-04 02:40:15

问题


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)?


回答1:


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.




回答2:


In your subclass of NSTextView, you should override -selectionRangeForProposedRange:granularity:, something like :

-(NSRange)selectionRangeForProposedRange:(NSRange)proposedSelRange granularity:(NSSelectionGranularity)granularity
{
    if (granularity == NSSelectByWord)
    {
        NSRange doubleRange = [[self textStorage] doubleClickAtIndex:proposedSelRange.location];
        if (doubleRange.location != NSNotFound)
        {
            NSRange dotRange = [[[self textStorage] string] rangeOfString:@"." options:0 range:doubleRange];
            if (dotRange.location != NSNotFound)
            {
                // double click after '.' ?
                if (dotRange.location < proposedSelRange.location)
                    return NSMakeRange(dotRange.location + 1, doubleRange.length - (dotRange.location-doubleRange.location) - 1);
                else
                    return NSMakeRange(doubleRange.location, dotRange.location-doubleRange.location);
            }
        }
    }
    return [super selectionRangeForProposedRange:proposedSelRange granularity:granularity];
}


来源:https://stackoverflow.com/questions/22017233/nstextview-customizing-double-click-selection

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!