如果 NSTextView
包含以下内容:
SELECT someTable.someColumn FROM someTable
用户双击someTable.someColumn
,整个内容都会被选中(句点的两侧)。在这种特定情况下(查询编辑器),选择 someTable
或 someColumn
更有意义。
我尝试环顾四周,看看是否能找到一种自定义选择的方法,但到目前为止我还无法做到。
目前我想做的是子类化 NSTextView
并执行以下操作:
- (void)mouseDown:(NSEvent *)theEvent
{
if(theEvent.clickCount == 2)
{
// TODO: Handle double click selection.
}
else
{
[super mouseDown:theEvent];
}
}
有人对此有任何想法或替代方案吗? (我是否缺少另一种可能更适合覆盖的方法)?
最佳答案
首先,与之前的答案相反,NSTextView
的 selectionRangeForProfusedRange:capsularity:
方法不是实现此目的的正确重写位置。在 Apple 的“Cocoa Text Architecture”文档(https://developer.apple.com/library/prerelease/mac/documentation/TextFonts/Conceptual/CocoaTextArchitecture/TextEditing/TextEditing.html – 请参阅“子类化 NSTextView”部分)中,Apple 明确指出“这些机制并不用于更改语言单词定义(例如通过双击选择的内容)。”我不确定为什么苹果会有这样的感觉,但我怀疑这是因为 selectionRangeForProfusedRange:capsularity:
没有获得任何关于建议范围的哪一部分是初始点击点以及哪一部分是初始点击点的信息用户拖动到的位置;通过覆盖此方法可能很难使双击拖动行为正确。也许还有其他问题,我不知道;该文档有点神秘。也许苹果计划稍后对选择机制进行更改,以打破此类覆盖。也许这里的重写无法解决定义“词”的其他方面。谁知道;但当苹果公司做出这样的声明时,遵循他们的指示通常是一个好主意。
奇怪的是,苹果的文档接着说“选择的细节是在文本系统的较低(目前是私有(private)的)级别处理的。”我认为这已经过时了,因为事实上所需的支持确实存在: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
最后一部分实际上是在 TextView 中使用自定义子类。如果您也有 NSTextView 子类,则可以在其 awakeFromNib 方法中执行此操作;否则,只要有机会,就在 Nib 加载后立即执行此操作;例如,在相关窗口或 Controller 的 awakeFromNib 调用中,或者在调用加载包含 TextView 的 nib 之后。无论如何,您想要执行此操作(其中 textview 是您的 NSTextView
对象):
[[textview layoutManager] replaceTextStorage:[[[MyTextStorage alloc] init] autorelease]];
这样,你就可以开始了,除非我在转录时犯了一个错误!
最后,请注意,NSAttributedString
中还有另一个方法 nextWordFromIndex:forward:
,当用户将插入点移动到下一个时,Cocoa 的文本系统会使用该方法。/上一个词。如果您希望此类事物遵循相同的单词定义,则还需要对其进行子类化。对于我的应用程序,我没有这样做 - 我希望下一个/上一个单词移动整个 a.b.c.d 序列(或更准确地说,我只是不在乎) - 所以我没有在这里分享的实现。留给读者作为练习。
关于cocoa - NSTextView自定义双击选择,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/22017233/