Search code examples
swiftmacoscocoanspanel

MacOS: how to get NSPanel to resist key focus (with becomesKeyOnlyIfNeeded?)


In MacOS/Cocoa/Swift 4.2/Xcode 10.1 I’m trying to create an NSPanel that floats over my document window as a lightweight markup palette that interacts with a (primarily keyboard-based) editing session happening in the document window.

I have mostly pushbutton controls in the panel but one combo-box that deserves to take focus when User clicks in it. Beyond that, I would like to avoid becoming the key window (avoid both capturing future key events and avoid highlighting the title bar) when any of the other controls are pushed, or when the panel is relocated by dragging, or on panel-clicks in dead space. This sounds to me exactly like what the docs describe as the behavior I should have if my panel was marked becomeKeyOnlyIfNeeded, but I can’t get it to work.

In Interface Builder, I specify my panel as (my own) subclass InspectorPanel, with Style: Regular Panel, Title Bar, Shadow, Close, Restorable, and Hide on Deactivate settings (everything else clear). I specify becomesKeyOnlyIfNeeded in the subclass:

class InspectorPanel : NSPanel {
     override var becomesKeyOnlyIfNeeded: SwiftBool {
        get {
            return true
        }
        set {
        }
    }    
}

(I’ve also tried setting it in the controller’s awakeFromNib, on the controller’s window as? NSPanel.)

This setting seems to have no visible effect. The panel correctly floats above the doc window, but while the combo box successfully grabs key focus away from the document window, so does pressing any control, or clicking in panel white space, or dragging the panel title bar, as well as perhaps other manipulations. While I can write handlers for each of these cases that manually find the active document window and makeKeyWindow(thatWindow) to pass focus back to where it belongs, this seems inelegant from the perspective of inspector design; forces me to write some handlers where I presently need none (e.g. title-bar dragging); and makes me worry about similar cases I’m missing.

Is there instead some way to be successful in becoming key only when my single combo box needs it? If so, how?

Other efforts:

  1. I am aware of needsPanelToBecomeKey and somewhat confused by its documentation. It suggests that once I set becomesKeyOnlyIfNeeded on the panel, NOTHING will permit the panel to grab focus until I enable needsPanelToBecomeKey on specific subviews (e.g. my combobox). In truth, since setting becomesKeyOnlyIfNeeded is having no effect on stopping the window from becoming key, I see no difference in response whether I set comboBox.needsPanelToBecomeKey on a subview or not.)

  2. I’m also aware of refusesFirstResponder, and have tried setting it on various controls (e.g. NSButtons) in the panel to see whether that prevents them from transferring key focus to the window. No luck.)

  3. If I set canBecomeKeyWindow = false, then I don’t EVER get key focus, even in my comboBox. (This seems reasonable and unexpected; I’m just listing it to be comprehensive.)

Lots of other apps appear to have floating windows that behave like I want, but I’m not finding source code that can help my find my mistake. Can you help?


Solution

  • I've discovered the cause of my problem. Apparently I don't understand the implications of the Swift initializers I use to override becomesKeyOnlyIfNeeded above (though I use the same syntax successfully for other properties elsewhere!). If I remove the attempt to define becomesKeyOnlyIfNeeded as always true, and instead dynamically set it (the inherited property) to true along the path of window initialization, the panel behaves exactly as I want, resisting key focus except, well, "if needed" (when clicked in the combo box).

    In other words,

    class InspectorPanel : NSPanel {
    
      override func awakeFromNib() {
        super.awakeFromNib()
        becomesKeyOnlyIfNeeded = true // REPLACES THE BELOW
      }
    
      /* REPLACED BY ABOVE
      override var becomesKeyOnlyIfNeeded: SwiftBool {
        get {
            return true
        }
        set {
    
        }
      }
      */
    }
    

    works perfectly. If anyone understands what I've done wrong with my (commented-out) initializer, I'd appreciate the insight, but with this workaround I can close the open question about panel behavior.