首先,与之前的答案相反,NSTextView 的selectionRangeForProposedRange:granularity: 方法不是实现此目的的正确覆盖位置。在 Apple 的“Cocoa Text Architecture”文档中(https://developer.apple.com/library/prerelease/mac/documentation/TextFonts/Conceptual/CocoaTextArchitecture/TextEditing/TextEditing.html – 请参阅“子类化 NSTextView”部分)Apple 明确声明“这些机制并非用于更改语言单词定义(例如通过双击选择的内容)。”我不确定苹果为什么会有这种感觉,但我怀疑这是因为selectionRangeForProposedRange:granularity: 没有得到任何关于建议范围的哪一部分是初始点击点以及用户拖动到的位置的任何信息;通过覆盖此方法可能很难使双击拖动行为正确。也许还有其他问题,我不知道;文档有点神秘。也许 Apple 计划稍后对选择机制进行更改,以打破此类覆盖。也许还有其他方面来定义什么是“单词”,而这里的覆盖无法解决。谁知道;但是,当他们做出这样的声明时,遵循 Apple 的指示通常是个好主意。
奇怪的是,Apple 的文档继续说“选择的细节是在文本系统的较低(目前是私有的)级别处理的。”我认为这已经过时了,因为实际上确实存在所需的支持:NSAttributedString 上的doubleClickAtIndex: 方法(在NSAttributedStringKitAdditions 类别中)。 Cocoa 文本系统使用此方法(在NSAttributedString 的NSTextStorage 子类中)来确定单词边界。子类化NSTextStorage 有点棘手,因此我将在此处提供一个名为MyTextStorage 的子类的完整实现。用于子类化NSTextStorage 的大部分代码来自 Apple 的 Ali Ozer。
在MyTextStorage .h:
@interface MyTextStorage : NSTextStorage
- (id)init;
- (id)initWithAttributedString:(NSAttributedString *)attrStr;
@end
在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
然后最后一部分实际上是在您的文本视图中使用您的自定义子类。如果你也有一个 NSTextView 子类,你可以在它的 awakeFromNib 方法中做到这一点;否则,在你的笔尖加载后,在你有机会的任何地方都这样做;例如,在相关窗口或控制器的 awakeFromNib 调用中,或者只是在调用加载包含 textview 的 nib 之后。在任何情况下,您都想这样做(其中 textview 是您的 NSTextView 对象):
[[textview layoutManager] replaceTextStorage:[[[MyTextStorage alloc] init] autorelease]];
有了这个,你应该很高兴,除非我在转录时犯了一个错误!
最后,请注意NSAttributedString 中的另一种方法nextWordFromIndex:forward:,当用户将插入点移动到下一个/上一个单词时,Cocoa 的文本系统会使用该方法。如果您希望此类事物遵循相同的词定义,则还需要对其进行子类化。对于我的应用程序,我没有这样做——我希望下一个/上一个词移动整个 a.b.c.d 序列(或者更准确地说,我只是不在乎)——所以我没有实现在这里分享。留给读者作为练习。