NSTextView自定义双击选择 [英] NSTextView customizing double click selection

查看:234
本文介绍了NSTextView自定义双击选择的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

如果 NSTextView 包含以下内容:

  SELECT someTable。 someColumn FROM someTable 

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



我试过四处看看是否可以找出一种自定义选择的方法,但我已经无法实现。



目前我想做的是子类化 NSTextView 并执行如下操作:

   - (void)mouseDown:(NSEvent *)theEvent 
{
if(theEvent.clickCount == 2)
{
// TODO:处理双击选择。
}
else
{
[super mouseDown:theEvent];
}
}

有没有人有任何想法或替代品? (有没有另一种方法,我失踪,可能更好的重写)?

解决方案

首先, , NSTextView selectionRangeForProposedRange:granularity:方法不是覆盖的正确位置。在Apple的Cocoa Text Architecture文档中( https://developer.apple.com/library/prerelease/mac/documentation/TextFonts/Conceptual/CocoaTextArchitecture/TextEditing/TextEditing.html - 请参阅Subclassing NSTextView部分)苹果状态显式这些机制aren' t意味着改变语言单词定义(例如双击选择的内容)。我不知道为什么苹果觉得这样的方式,但我怀疑是因为 selectionRangeForProposedRange:granularity:没有得到任何信息,建议范围的什么部分是初始点击点,而什么部分是用户拖动到的地方;使双击拖动行为正确可能很难做到这个方法的覆盖。也许还有其他问题,我不知道;文档有点神秘。也许苹果计划稍后改变选择机制,将打破这种覆盖。也许还有其他方面定义什么是字是重叠在这里未能解决。谁知道;但是一般来说,当他们做出这样的声明时,遵循苹果的说明是一个好主意。



奇怪的是,苹果的文档继续说:选择的细节被处理文本系统的较低(并且当前是私人)级别。我认为是过时的,因为事实上需要的支持确实存在: doubleClickAtIndex:方法在 NSAttributedString NSAttributedStringKitAdditions category)。这个方法(在 NSTextStorage NSAttributedString 的子类中)由Cocoa文本系统使用以确定字边界。子类化 NSTextStorage 有点棘手,因此我将在这里为一个名为 MyTextStorage 的子类提供一个完整的实现。这些代码的大部分代码 NSTextStorage 来自Apple的Ali Ozer。



MyTextStorage .h

  @interface MyTextStorage:NSTextStorage 
-
- (id)initWithAttributedString:(NSAttributedString *)attrStr;
@end

MyTextStorage.m

  @interface MyTextStorage()
{
NSMutableAttributedString *
}
@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
{
[content release];
[super dealloc]
}

//下一组方法是属性和可变属性字符串的原语...

- (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];
[自编辑:NSTextStorageEditedCharacters范围:range changeInLength:[self length] - origLen];
}

- (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range
{
[content setAttributes:attrs range:range];
[self edited:NSTextStorageEditedAttributes range:range changeInLength:0];
}

//现在这个子类的实际原因:提供代码感知词选择行为

- (NSRange)doubleClickAtIndex:(NSUInteger)location
{
//通过调用super获取建议的范围开始。如果位置> = [自身长度]
//或位置< 0,所以在下面的代码中我们可以假设位置指示一个有效的字符位置。
NSRange superRange = [super doubleClickAtIndex:location];
NSString * string = [self string];

//如果用户实际上双击了一个句点,我们只想返回句点的范围。
if([string characterAtIndex:location] =='。')
return NSMakeRange(location,1);

//超级行为错误的情况涉及点运算符; x.y不应该被视为一个词。
//所以我们检查锚点位置之前或之后的一段时间,并修剪周期和每个
//两边的边。这将正确处理更长的序列,如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)
{
//将superRange更改为在前一个句点之后开始;固定其长度,使其末端保持不变。
superRange.length - =(periodBeforeRange.location + 1 - superRange.location);
superRange.location = periodBeforeRange.location + 1;
}

if(periodAfterRange.location!= NSNotFound)
{
//将超范围改为在下一个周期之前结束
superRange.length - = (NSMaxRange(superRange) - periodAfterRange.location);
}

return superRange;
}

@end

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

 code> [[textview layoutManager] replaceTextStorage:[[[MyTextStorage alloc] init] autorelease]]; 

这样,你应该很好去,除非我犯了错误。最后,请注意 NSAttributedString nextWordFromIndex中的另一个方法:forward: ,当用户将插入点移动到下一个/上一个单词时,由Cocoa的文本系统使用。如果你想让这种东西遵循同样的单词定义,你需要把它子类化。对于我的应用程序,我没有这样做 - 我想下一个/上一个单词移动整个a.b.c.d序列(或更准确地说,我只是不在乎) - 所以我没有一个实现在这里分享。作为一个练习留给读者。


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.

这篇关于NSTextView自定义双击选择的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆