NSComboBox disappears immediately after click

Viewed 147

I'm working on an application which behaves similarly to Spotlight. However, currently I'm having a trouble with NSComboBox.

To show the application after hitting a hotkey I use activate(ignoringOtherApps: true) and NSWindowController(window: window).showWindow(self). Then when user hits Escape I do window.close() and hide(self).

Everything works great, previous application gets focus. However, when I open again the window, first click on NSComboBox causes a very strange behavior (like on the movie below). First time it instantly disappears.

bug

I found out that it happens because of NSApp.hide. When I don't call it, everything works well. However, I need to call it, because I want the previous app to get the focus.

To workaround this issue I can replace NSWindow with nonactivating NSPanel. It resolves the problem. However, it's not possible in my case because I need to use it also with presentAsModalWindow and presentAsSheet where I can't control whether it's a window or panel.

I also discovered that a single click on window's background before clicking on ComboBox helps. So it seems like this window doesn't have focus, but looks like focused. I also tried all methods like makeKeyAndOrderFront, becomeFirstResponder, NSApp.unhide etc. etc. Nothing helps.

Under the hood NSComboBox has its own window NSComboBoxWindow, so my guess is that when I click it opens its window and then it receives information that the parent window took focus and dismisses itself for some reason.

I'm not sure if this is Cocoa bug or what. Is there any way to fix it?


Minimum Reproducible Example

Create new macOS project with NSComboBox and NSButton. Connect button to IBAction.

import Cocoa

class ViewController: NSViewController {
    @IBAction func close(_ sender: Any) {
        view.window?.close()
        NSApp.hide(self)

        DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
            self.view.window?.makeKeyAndOrderFront(self)
            NSApp.activate(ignoringOtherApps: true)
        }
    }
}

Workaround

Finally, I managed to create a workaround. It's ugly but it works. I cover arrow part with a transparent view, intercept click and invoke two times expand via accessibility...

import Cocoa

final class ClickView: NSView {
    var onMouseDown: () -> (Bool) = { return false }

    override func mouseDown(with event: NSEvent) {
        if !onMouseDown() {
            super.mouseDown(with: event)
        }
    }
}

final class FixedComboBox: NSComboBox {
    private let clickView = ClickView()

    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        fix()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }

    override func awakeFromNib() {
        super.awakeFromNib()
        fix()
    }

    private func fix() {
        clickView.onMouseDown = { [weak self] in
            guard let cell = self?.cell else { return false }

            // first expand will be immediately closed because of Cocoa bug
            cell.setAccessibilityExpanded(true)

            // we need to schedule another one with a small delay to let it close the first call

            // this one almost immediately to avoid blinking
            DispatchQueue.main.async { cell.setAccessibilityExpanded(true) }

            // in case the first one didn't "catch" the right moment (sometimes it happens)
            DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { cell.setAccessibilityExpanded(true) }

            return true
        }

        addSubview(clickView)

        clickView.snp.makeConstraints { make in
            make.width.equalTo(20)
            make.trailing.top.bottom.equalToSuperview()
        }
    }
}
0 Answers
Related