【问题标题】:NSTextView customizing double click selectionNSTextView自定义双击选择
【发布时间】:2014-03-27 20:31:36
【问题描述】:

如果NSTextView 包含以下内容:

SELECT someTable.someColumn FROM someTable

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

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

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

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

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

【问题讨论】:

    标签: cocoa selection osx-mountain-lion nstextview


    【解决方案1】:

    这是@bhaller 代码的 Swift 5 自定义实现,非常感谢!

    请注意,由于内存效率的原因,它不使用stringNSMutableAttributedString,使用另一个NSTextStorage 更好。更多信息here

    final class MyTextStorage: NSTextStorage {
    
        private var storage = NSTextStorage()
    
        // MARK: - Required overrides for NSTextStorage
    
        override var string: String {
            return storage.string
        }
    
        override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key : Any] {
            return storage.attributes(at: location, effectiveRange: range)
        }
    
        override func replaceCharacters(in range: NSRange, with str: String) {
            beginEditing()
            storage.replaceCharacters(in: range, with: str)
            edited(.editedCharacters, range: range, changeInLength: (str as NSString).length - range.length)
            endEditing()
        }
    
        override func setAttributes(_ attrs: [NSAttributedString.Key : Any]?, range: NSRange) {
            beginEditing()
            storage.setAttributes(attrs, range: range)
            edited(.editedAttributes, range: range, changeInLength: 0)
            endEditing()
        }
    
        // MARK: - DOuble click functionality
    
        override func doubleClick(at location: Int) -> NSRange {
    
            // Call super to get location of the double click
            var range = super.doubleClick(at: location)
            let stringCopy = self.string
    
            // If the user double-clicked a period, just return the range of the period
            let locationIndex = stringCopy.index(stringCopy.startIndex, offsetBy: location)
            guard stringCopy[locationIndex] != "." else {
                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.
            let candidateRangeBeforeLocation = NSMakeRange(range.location, location - range.location)
            let candidateRangeAfterLocation = NSMakeRange(location + 1, NSMaxRange(range) - (location + 1))
            let periodBeforeRange = (stringCopy as NSString).range(of: ".", options: .backwards, range: candidateRangeBeforeLocation)
            let periodAfterRange = (stringCopy as NSString).range(of: ".", options: [], range: candidateRangeAfterLocation)
    
            if periodBeforeRange.location != NSNotFound {
                // Change range to start after the preceding period; fix its length so its end remains unchanged
                range.length -= (periodBeforeRange.location + 1 - range.location)
                range.location = periodBeforeRange.location + 1
            }
            if periodAfterRange.location != NSNotFound {
                // Change range to end before the following period
                range.length -= (NSMaxRange(range) - periodAfterRange.location);
            }
    
            return range
        }
    }
    

    编辑:Google 帮我找到了 this 文章 - 由于性能问题,不建议使用 Swift 版本。

    【讨论】:

      【解决方案2】:

      首先,与之前的答案相反,NSTextViewselectionRangeForProposedRange: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 文本系统使用此方法(在NSAttributedStringNSTextStorage 子类中)来确定单词边界。子类化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 序列(或者更准确地说,我只是不在乎)——所以我没有实现在这里分享。留给读者作为练习。

      【讨论】:

      • 这是一个很好的答案!文本选择存在一个错误,我永远无法确定并基于此,加上您对我能够复制和修复的其他答案的评论。非常感谢您提供完整详细的答案!
      • 仅供参考,有一个小错误,setAttributes 通过 NSTextStorageEditedCharacters 而不是 NSTextStorageEditedAttributes。这在语法突出显示期间给我造成了一些问题,因为通常仅在字符更改时才处理文本。 (语法突出显示添加了字体颜色属性,这些属性被视为文本更改而不是属性更改)。我已更新帖子以解决此问题。
      • 很好,谢谢@Kyle。这也修复了我自己代码中两个地方的错误!该漏洞由来已久; Apple 的 Ali Ozer here 发布的子类化 NSTextStorage 的代码有同样的错误,我就是从那里得到它的!
      【解决方案3】:

      NSTextView 的子类中,您应该覆盖-selectionRangeForProposedRange:granularity:,类似于:

      -(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];
      }
      

      【讨论】:

      • 哦,哇,我只是在寻找提示,但这段代码可以解决问题!
      • @Zenox 你还是可以稍微润色一下... :) (不需要回退给super发消息,如果没有点就返回doubleRange
      • 这不是一个好的解决方案,原因有几个。首先,它直接违反了 Apple 在其Cocoa Text Architecture 文档中的声明,即 selectionRangeForProposedRange:granularity: not 用于更改单词定义,“例如通过双击选择的内容”。其次,它的效果很差。尝试输入“foo.bar.baz”,然后双击“bar”或“baz”。尝试单击拖动以将选择扩展到多个时段。不好。
      猜你喜欢
      • 2016-04-19
      • 2019-06-11
      • 1970-01-01
      • 1970-01-01
      • 2018-03-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多