NSTextView 选择文本高亮显示所有字符,甚至包括段落缩进

4

找不到任何线索来管理这个问题。

NSTextView默认情况下选择整个文本容器的大小。它忽略了行间距、头部或尾部缩进等等。但在Pages应用程序中,选择不会突出显示那些辅助部分,只会突出显示字符。即使文本容器的高度较小(段前段后间距),它也会突出显示该行的所有高度。

我想实现这种行为,但不知道从哪里开始。我已经在这里搜索过,在Apple的文档中搜索过,在示例项目中尝试过。什么都没有。

也许有人可以引导我走向正确的方向?谢谢!

2个回答

7
我发现hamstergene的答案是不正确的。实际上,NSTextView逐行突出其文本容器边界。
因此,如果您使用段落头缩进,则段落领先的空白将被突出显示。如果选择EOL字符,则段落的尾随空格也将被突出显示。
我的解决方案是使段落样式的头部和尾部缩进失效(我将它们缓存到私有变量中,并在访问我的文本存储以进行打印时将它们放回),然后通过我的NSTextContainer子类的覆盖的lineFragmentRectForProposedRect: atIndex: writingDirection: remainingRect方法简单地调整文本容器线的框架。
但后来我找到了更适当的方法。只需覆盖NSLayoutManager的func fillBackgroundRectArray(_ rectArray: UnsafePointer<NSRect>, count rectCount: Int, forCharacterRange charRange: NSRange, color: NSColor) 方法,计算您的矩形并使用这些矩形调用super。如果您正确计算选择矩形,则会获得与Apple Pages或MS Word相同的精确选择行为。
简单易行!
更新 这是我用于计算选择矩形的代码:
public override func fillBackgroundRectArray(_ rectArray: UnsafePointer<CGRect>, count rectCount: Int, forCharacterRange charRange: NSRange, color: OSColor) {
    
    // if characters are selected, make sure that we draw selection of those characters only, not the whole text container bounds
    guard let textView = textContainer(forCharacterIndex: charRange.location)?.textView,
        NSIntersectionRange(textView.selectedRange(), charRange).length > 0,
        let textStorage = self.textStorage as? ParagraphTextStorage else {
        super.fillBackgroundRectArray(rectArray, count: rectCount, forCharacterRange: charRange, color: color)
        return
    }
    
    let selectedGlyphRange = self.glyphRange(forCharacterRange: charRange, actualCharacterRange: nil)
    var selectionRectArray: [CGRect] = []

    enumerateLineFragments(forGlyphRange: selectedGlyphRange) { (rect, usedRect, textContainer, glyphRange, stop) in
        let lineCharRange = self.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)
        let intersection = NSIntersectionRange(charRange, lineCharRange)
        
        // if selected all characters of the line, then we already have teir layout rects
        if intersection == lineCharRange {
            let paragraphIndex = textStorage.paragraphIndex(at: intersection.location)
            let paragraphRange = textStorage.paragraphRanges[paragraphIndex]
            
            let hasNewLineChar = lineCharRange.max == paragraphRange.max && paragraphRange.max < textStorage.length ||
                paragraphRange.max == lineCharRange.max && intersection.max == textStorage.length && paragraphIndex < textStorage.paragraphRanges.count - 1

            let newLineCharSize = hasNewLineChar ? self.newLineCharSize : .zero

            let lineRect = CGRect(x: usedRect.origin.x + textView.textContainerInset.width + textContainer.lineFragmentPadding,
                                  y: usedRect.origin.y + textView.textContainerInset.height - (rect.height - usedRect.height),
                                  width: usedRect.width + newLineCharSize.width - textContainer.lineFragmentPadding * 2,
                                  height: rect.height)
            selectionRectArray.append(lineRect)
        } else {
            // calculate rect for partially selected characters of the line
            let partialRect = self.usedLineRect(forCharacterRange: intersection, in: textContainer)
            selectionRectArray.append(partialRect)
        }
    }
    super.fillBackgroundRectArray(selectionRectArray, count: selectionRectArray.count, forCharacterRange: charRange, color: color)
}

public func usedLineRect(forCharacterRange charRange: NSRange, in textContainer: NSTextContainer) -> CGRect {
    guard let textView = textContainer.textView, let textStorage = textStorage as? ParagraphTextStorage else { return .zero }
            
    let glyphRange = self.glyphRange(forCharacterRange: charRange, actualCharacterRange: nil)
    let textContainer = self.textContainer(forGlyphAt: glyphRange.location, effectiveRange: nil) as! ModernTextContainer
    
    let paragraphIndex = textStorage.paragraphIndex(at: charRange.location)
    let paragraphRange = textStorage.paragraphRanges[paragraphIndex]
    let hasNewLine = paragraphRange.max == charRange.max && charRange.max < textStorage.length ||
        paragraphRange.max == charRange.max && charRange.max == textStorage.length && paragraphIndex < textStorage.paragraphRanges.count - 1
    let newLineCharSize = hasNewLine ? self.newLineCharSize : .zero

    // if new line is in range, boundingRect will return the whole width of the text container, fix that
    let noNewLineGlyphRange = hasNewLine ? NSRange(location: glyphRange.location, length: glyphRange.length - 1) : glyphRange
    
    let charRect = boundingRect(forGlyphRange: noNewLineGlyphRange, in: textContainer)
    let lineRect = lineFragmentRect(forGlyphAt: noNewLineGlyphRange.location, effectiveRange: nil, withoutAdditionalLayout: true)
    
    #if os(macOS)
    // respect the flipped coordinate system with abs function
    let rect = CGRect(x: charRect.origin.x + textView.textContainerInset.width,
                      y: abs(charRect.origin.y + textView.textContainerInset.height - (lineRect.height - charRect.height)),
                      width: charRect.width + newLineCharSize.width,
                      height: lineRect.height)
    #else
    let rect = CGRect(x: charRect.origin.x + textView.textContainerInset.left,
                      y: abs(charRect.origin.y + textView.textContainerInset.top - (lineRect.height - charRect.height)),
                      width: charRect.width + newLineCharSize.width,
                      height: lineRect.height)
    #endif
    
    return rect
}

这个极快的计算中的重要部分是我使用了自己的ParagraphTextStorage实现。它的目的是在文本存储被编辑时实时计算段落范围。知道正确的段落范围使我能够使用简单的整数(如NSRange),同时计算所选矩形。否则,我将不得不执行一堆子字符串操作来确定是否选择了换行符。而这些操作非常缓慢。
我的ParagraphTextStorage的实现在这里:https://github.com/CineDev/ParagraphTextKit

1
嗨,Vitaliy,你能分享一下从选择矩形中排除不可见字符的计算方法吗? - Lukáš Kubánek

3
我们只能猜测闭源的 Pages 使用了什么,但我怀疑它并没有使用 NSTextView——作为一个文字处理器,它必须使用更先进的自定义解决方案。
从 Cocoa 文本架构指南 开始,你主要需要关注 NSLayoutManager 类(它伴随着 NSTextContainer 和 NSTextStorage)。
NSTextView 可能通过临时属性(- [NSLayoutManager addTemporaryAttribute:value:forCharacterRange:])来实现其选择。如果你子类化 NSTextView 并截取每个选择改变事件,你应该能够检测到并删除负责显示换行符选择的临时属性,而不干扰文本视图的逻辑选择范围。
如果由于某种原因上述建议无效,总是可以从头开始重新实现NSTextView,使用NSLayoutManager来处理所有的布局和绘制。 NSLayoutManager处理所有的unicode / bidi怪异之处,给出字形运行和单个字形的精确像素坐标,以及绘制它们的方法。 临时属性可能不足以实现不同的选择高度; 在这种情况下,您应该能够自己绘制选择(在文本字形下方的背景上)。 然而,对于这样一个小的UI细节,这肯定会是很多工作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接