带有 NSLinkAttributeName 的 NSAttributedString 中的颜色属性被忽略

Color attribute is ignored in NSAttributedString with NSLinkAttributeName

NSAttributedString 中,一系列字母具有 link 属性和自定义颜色属性。

在 Xcode 7 和 Swift 2 中,有效:

在 Xcode 8 和 Swift 3 中,link 的自定义属性颜色始终被忽略(在屏幕截图中应该是橙色)。

这是测试代码。

Swift2,Xcode7:

import Cocoa
import XCPlayground

let text = "Hey @user!"

let attr = NSMutableAttributedString(string: text)
let range = NSRange(location: 4, length: 5)
attr.addAttribute(NSForegroundColorAttributeName, value: NSColor.orangeColor(), range: range)
attr.addAttribute(NSLinkAttributeName, value: "http://somesite.com/", range: range)

let tf = NSTextField(frame: NSRect(x: 0, y: 0, width: 200, height: 50))
tf.allowsEditingTextAttributes = true
tf.selectable = true
tf.stringValue = text
tf.attributedStringValue = attr

XCPlaygroundPage.currentPage.liveView = tf

Swift3,Xcode8:

import Cocoa
import PlaygroundSupport

let text = "Hey @user!"

let attr = NSMutableAttributedString(string: text)
let range = NSRange(location: 4, length: 5)
attr.addAttribute(NSForegroundColorAttributeName, value: NSColor.orange, range: range)
attr.addAttribute(NSLinkAttributeName, value: "http://somesite.com/", range: range)

let tf = NSTextField(frame: NSRect(x: 0, y: 0, width: 200, height: 50))
tf.allowsEditingTextAttributes = true
tf.isSelectable = true
tf.stringValue = text
tf.attributedStringValue = attr

PlaygroundPage.current.liveView = tf

我已经向 Apple 发送了错误报告,但与此同时,如果有人对 Xcode 8 中的修复或解决方法有想法,那就太好了。

此答案无法解决 NSLinkAttributeName 忽略自定义颜色的问题,它是在 NSAttributedString.

中使用彩色可点击字词的替代解决方案

有了这个变通办法,我们根本不使用 NSLinkAttributeName,因为它会强制使用我们不想要的样式。

相反,我们使用自定义属性,并将 NSTextField/NSTextView 子类化以检测鼠标单击下的属性并采取相应行动。

显然有几个限制条件:您必须能够子类化 field/view、覆盖 mouseDown 等,但是 "it works for me" 在等待修复时。

在准备 NSMutableAttributedString 时,您应该设置 NSLinkAttributeName,将 link 设置为具有自定义键的属性:

theAttributedString.addAttribute("CUSTOM", value: theLink, range: theLinkRange)
theAttributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.orange, range: theLinkRange)
theAttributedString.addAttribute(NSCursorAttributeName, value: NSCursor.arrow(), range: theLinkRange)

link 的颜色和内容已设置。现在我们必须让它可以点击。

为此,子类化您的 NSTextView 并覆盖 mouseDown(with event: NSEvent)

我们将获取鼠标事件在window中的位置,在该位置找到文本视图中的字符索引,并在文本视图的属性中询问该索引处字符的属性字符串。

class MyTextView: NSTextView {

    override func mouseDown(with event: NSEvent) {
        // the location of the click event in the window
        let point = self.convert(event.locationInWindow, from: nil)
        // the index of the character in the view at this location
        let charIndex = self.characterIndexForInsertion(at: point)
        // if we are not outside the string...
        if charIndex < super.attributedString().length {
            // ask for the attributes of the character at this location
            let attributes = super.attributedString().attributes(at: charIndex, effectiveRange: nil)
            // if the attributes contain our key, we have our link
            if let link = attributes["CUSTOM"] as? String {
                // open the link, or send it via delegate/notification
            }
        }
        // cascade the event to super (optional)
        super.mouseDown(with: event)
    }

}

就是这样。

在我的例子中,我需要用不同的颜色和 link 类型来自定义不同的词,所以我传递了一个包含 link 的结构,而不是仅将 link 作为字符串传递和其他元信息,但思路是一样的。

如果您必须使用 NSTextField 而不是 NSTextView,找到点击事件位置会有点棘手。一个解决方案是在 NSTextField 中创建一个 NSTextView 并从那里使用与以前相同的技术。

class MyTextField: NSTextField {

    var referenceView: NSTextView {
        let theRect = self.cell!.titleRect(forBounds: self.bounds)
        let tv = NSTextView(frame: theRect)
        tv.textStorage!.setAttributedString(self.attributedStringValue)
        return tv
    }

    override func mouseDown(with event: NSEvent) {
        let point = self.convert(event.locationInWindow, from: nil)
        let charIndex = referenceView.textContainer!.textView!.characterIndexForInsertion(at: point)
        if charIndex < self.attributedStringValue.length {
            let attributes = self.attributedStringValue.attributes(at: charIndex, effectiveRange: nil)
            if let link = attributes["CUSTOM"] as? String {
                // ...
            }
        }
        super.mouseDown(with: event)
    }

}

Apple 开发者已回答:

Please know that our engineering team has determined that this issue behaves as intended based on the information provided.

他们解释了为什么它以前有效但现在无效了:

Unfortunately, the previous behavior (attributed string ranges with NSLinkAttributeName rendering in a custom color) was not explicitly supported. It happened to work because NSTextField was only rendering the link when the field editor was present; without the field editor, we fall back to the color specified by NSForegroundColorAttributeName.

Version 10.12 updated NSLayoutManager and NSTextField to render links using the default link appearance, similar to iOS. (see AppKit release notes for 10.12.)

To promote consistency, the intended behavior is for ranges that represent links (specified via NSLinkAttributeName) to be drawn using the default link appearance. So the current behavior is the expected behavior.

(强调我的)