NSTextView自定义双击选择

Kyl*_*yle 3 cocoa selection nstextview osx-mountain-lion

如果a NSTextView包含以下内容:

SELECT someTable.someColumn FROM someTable
Run Code Online (Sandbox Code Playgroud)

并且用户双击someTable.someColumn,整个事物被选中(期间的两侧).在这种特定情况下(查询编辑器),对于选择someTable或者someColumn选择更有意义.

我试着四处寻找,看看我是否能找到一种方法来自定义选择,但到目前为止我一直无法做到.

目前我正在考虑的是进行子类化NSTextView并执行以下操作:

- (void)mouseDown:(NSEvent *)theEvent
{
  if(theEvent.clickCount == 2)
  {
    // TODO: Handle double click selection.
  }
  else
  {
    [super mouseDown:theEvent];
  }
}
Run Code Online (Sandbox Code Playgroud)

有没有人对此有任何想法或替代方案?(我是否还有另一种方法,可能更适合覆盖)?

bha*_*ler 6

首先,违背了先前的回答,NSTextViewselectionRangeForProposedRange:granularity:方法不重写实现这一目标的正确的位置.在Apple的"Cocoa Text Architecture"文档中(https://developer.apple.com/library/prerelease/mac/documentation/TextFonts/Conceptual/CocoaTextArchitecture/TextEditing/TextEditing.html - 请参阅"Subclassing NSTextView"部分)Apple明确声明"这些机制不适用于改变语言词定义(例如通过双击选择的内容)." 我不确定苹果为什么会这样,但我怀疑是因为selectionRangeForProposedRange:granularity:没有得到关于建议范围的哪个部分是初始点击点的任何信息,而不是用户拖动的地方的哪个部分; 使双击拖动行为正确可能很难超越此方法.也许还有其他问题,我不知道; 该文档有点神秘.也许苹果计划稍后改变选择机制,以打破这种覆盖.也许还有其他方面来定义什么是"字",这里的覆盖无法解决.谁知道; 但是当他们做出这样的陈述时,通常最好遵循Apple的指示.

奇怪的是,Apple的文档继续说"选择的细节是在文本系统的较低(当前是私有的)级别处理的." 我认为这是过时的,因为实际上必要的支持确实存在:在doubleClickAtIndex:上法NSAttributedString(在NSAttributedStringKitAdditions类别).该方法由Cocoa文本系统(在NSTextStorage子类中NSAttributedString)用于确定单词边界.子类化NSTextStorage有点棘手,因此我将在这里为一个名为的子类提供完整的实现MyTextStorage.这个子类化代码大部分NSTextStorage来自Apple的Ali Ozer.

MyTextStorage .h:

@interface MyTextStorage : NSTextStorage
- (id)init;
- (id)initWithAttributedString:(NSAttributedString *)attrStr;
@end
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

然后最后一部分实际上是在textview中使用自定义子类.如果你有一个NSTextView子类,你可以在它的awakeFromNib方法中执行此操作; 否则,在你的笔尖加载之后,在你有机会的任何地方做这个; 例如,在awakeFromNib调用相关窗口或控制器时,或者只是在调用后加载包含textview的nib.无论如何,你想要这样做(textview是你的NSTextView对象):

[[textview layoutManager] replaceTextStorage:[[[MyTextStorage alloc] init] autorelease]];
Run Code Online (Sandbox Code Playgroud)

除此之外,你应该好好去,除非我在解释这个错误时犯了错误!

最后,注意,是另一种方法NSAttributedString,nextWordFromIndex:forward:即使用由可可的文字系统,当用户将插入点移动到下一个/前一个字.如果你想要那样的东西遵循相同的单词定义,你也需要将它子类化.对于我的应用程序,我没有这样做 - 我希望下一个/上一个单词移动整个abcd序列(或者更准确地说我只是不关心) - 所以我没有在此分享的实现.留给读者练习.