NSTextView自定义双击选择

时间:2014-02-25 14:32:25

标签: cocoa selection osx-mountain-lion nstextview

如果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];
  }
}

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

2 个答案:

答案 0 :(得分:1)

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

答案 1 :(得分:0)

这是@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
    }
}