I have an non-editable NSTextField
which I'm filling with NSTextAttachment
s, each of which is styled like a token. I now want to be able to mouseover a token and get a cancel button in the token, which will let me remove it from the list by pressing the button.
I have fallen at the first hurdle because I cannot figure out how to get mouse tracking to work. The NSTextAttachmentCellProtocol
(used by NSTextAttachmentCell
) declares wantsToTrackMouse()
, which I've implemented and returned true
from. But the documentation also claims that the default implementation returns true
, so I shouldn't have to do that. Regardless, this method is never being called. I've tried both the simple implementation and the more complex one.
I have implemented trackMouse(with:in:of:untilMouseUp:)
, but this is also never being called.
This answer from a few years back claims that you won't get trackMouse
events until you implement hitTest
, so I tried using a super simple implementation of that:
override func hitTest(for event: NSEvent, in cellFrame: NSRect, of controlView: NSView) -> NSCell.HitResult {
return NSCell.HitResult.trackableArea
}
This method is never called either.
Here's my code, with all of the token styling stripped out for simplicity:
import Cocoa
class TokenCellTest: NSTextAttachmentCell {
static let titleMargin: CGFloat = 0
override func cellSize() -> NSSize {
let attribs = [NSAttributedString.Key.font: font ?? NSFont.systemFont(ofSize: 13)]
let titleSize = (stringValue as NSString).size(withAttributes: attribs)
return cellSize(forTitleSize: titleSize)
}
override func titleRect(forBounds rect: NSRect) -> NSRect {
var bounds = rect
bounds.size.width = max(bounds.size.width, TokenCellTest.titleMargin * 2 + bounds.size.height)
bounds = bounds.insetBy(dx: TokenCellTest.titleMargin + bounds.size.height / 2, dy: 0)
return bounds
}
override func draw(withFrame cellFrame: NSRect, in controlView: NSView?, characterIndex charIndex: Int) {
guard let controlView = controlView else { return }
if charIndex >= 0, let textView = controlView as? NSTextView {
for rangeValue in textView.selectedRanges {
let range = rangeValue.rangeValue
guard NSLocationInRange(charIndex, range) else { continue }
if textView.window?.isKeyWindow ?? false {
break
}
}
}
drawToken(with: cellFrame, in: controlView)
}
// MARK: - private methods
private func cellSize(forTitleSize titleSize: NSSize) -> NSSize {
var size = titleSize
size.height += 1
size.width += size.height + TokenCellTest.titleMargin * 2
let rect = NSRect(origin: .zero, size: size)
return NSIntegralRect(rect).size;
}
private func drawToken(with rect: NSRect, in view: NSView) {
NSGraphicsContext.current?.saveGraphicsState()
drawTitle(with: titleRect(forBounds: rect), in: view)
NSGraphicsContext.current?.restoreGraphicsState()
}
private func drawTitle(with rect: NSRect, in view: NSView) {
(stringValue as NSString).draw(in: rect)
}
// MARK: - Mouse tracking
override func trackMouse(with theEvent: NSEvent, in cellFrame: NSRect, of controlView: NSView?, untilMouseUp flag: Bool) -> Bool {
print("trackMouse")
return true
}
override func wantsToTrackMouse() -> Bool {
print("wantsToTrackMouse")
return true
}
override func hitTest(for event: NSEvent, in cellFrame: NSRect, of controlView: NSView) -> NSCell.HitResult {
print("hit test")
return NSCell.HitResult.trackableArea
}
}
The code to add these tokens to a text field:
let attachment = NSTextAttachment()
let cell = TokenCellTest(textCell: "Blue")
attachment.attachmentCell = cell
let attStr = NSAttributedString(attachment:attachment)
field.attributedStringValue = attStr
And here's what the tokens look like with this:
And no mouse tracking. :-(
I am using this approach, instead of an NSTokenField
, because I wanted to draw the tokens myself, and it seemed like it might be simpler to just do all of that in an attachment cell. And indeed, that part is working great. But now that I want to add interactivity, I don't understand why the attachment cell's mouse handling methods aren't being called.
Ultimately, I'm expecting to need to declare a button cell inside the attachment cell and delegate the button cell drawing in the draw method. But I want the button to only show on hover, hence the token needs to know when the mouse is in its bounds. And I have a vague idea that the click event will need to be passed to the button cell, but I'm not certain how this will work either.
If there's a better way to create interactive, custom-drawn tokens in a non-editable view, I guess I'm all ears. This code looked promising, but it won't open on current versions of Xcode. And anyway, it doesn't touch token drawing itself. I took lots of inspiration from OEXTokenAttachmentCell
so far, but that doesn't do mouse handling either.