Search code examples
swiftmacoskey-value-observingnsnotifications

Dynamic NSTooltips: Event Key Modifier(s) Affecting Tooltips


Is there a notification mechanism for tooltips, so then key modifiers can be used to make them dynamic? I do not know of one so went about this path to capture the tooltip view being created and trying to trigger a redraw when a key event occurs.

I monitor key modifier(s) (in my app delegate); focusing on SHIFT here but any key modifier will do:

var localKeyDownMonitor : Any? = nil
var globalKeyDownMonitor : Any? = nil
var shiftKeyDown : Bool = false {
    didSet {
        let notif = Notification(name: Notification.Name(rawValue: "shiftKeyDown"),
                                 object: NSNumber(booleanLiteral: shiftKeyDown));
        NotificationCenter.default.post(notif)
    }
}

which the app's startup process will install ...

// Local/Global Monitor
_ /*accessEnabled*/ = AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary)
globalKeyDownMonitor = NSEvent.addGlobalMonitorForEvents(matching: NSEventMask.flagsChanged) { (event) -> Void in
    _ = self.keyDownMonitor(event: event)
}
localKeyDownMonitor = NSEvent.addLocalMonitorForEvents(matching: NSEventMask.flagsChanged) { (event) -> NSEvent? in
    return self.keyDownMonitor(event: event) ? nil : event
}

and then intercept to post notifications via app delegate didSet above (so when the value changes a notification is sent!):

func keyDownMonitor(event: NSEvent) -> Bool {
    switch event.modifierFlags.intersection(.deviceIndependentFlagsMask) {
    case [.shift]:
        self.shiftKeyDown = true
        return true

    default:
        //  Only clear when true
        if shiftKeyDown { self.shiftKeyDown = false }
        return false
    }
}

For singleton tooltip bindings, this can be dynamic by having the notification inform (KVO of some key) I'd like a view delegate routine to act on this:

func tableView(_ tableView: NSTableView, toolTipFor cell: NSCell, rect: NSRectPointer, tableColumn: NSTableColumn?, row: Int, mouseLocation: NSPoint) -> String {
    if tableView == playlistTableView
    {
        let play = (playlistArrayController.arrangedObjects as! [PlayList])[row]

        if shiftKeyDown {
            return String(format: "%ld play(s)", play.plays)
        }
        else
        {
            return String(format: "%ld item(s)", play.list.count)
        }
    }
    ...

So I'd like my notification handler to do something like

internal func shiftKeyDown(_ note: Notification) {
    let keyPaths = ["cornerImage","cornerTooltip","<table-view>.<column-view>"]
    for keyPath in (keyPaths)
    {
        self.willChangeValue(forKey: keyPath)
    }
    if subview.className == "NSCustomToolTipDrawView" {
        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "CustomToolTipView"), object: subview)
    }
    for keyPath in (keyPaths)
    {
        self.didChangeValue(forKey: keyPath)
    }
}

where I would like to force the tooltip to redraw, but nothing happens.

What I did to obtain the tooltip view, was to watch any notification (use nil name to do this) that looked promising and found one - by monitoring all notifications and so I post to it for interested view controller which is the delegate of the tableView. The view controller is observing for the "CustomToolTipView" and remembers the object send - tooltipView; this is the new tooltip view created.

But nothing happens - shiftKeyDown(), to redraw the view.

I suspect it won't update in the current event loop but I do see the debug output.


Solution

  • With Willeke's suggestion I migrated a tableView to be cell based, added a get property for the tooltip and had the relevant object's init() routine register notifications for the shift key changes. This resulted in less code. A win-win :-)