0
votes

I have a small NSStatusItem only application, with a settings windows. From a menu in the NSStatusItem I want to reactivate the main window.

The app keeps a single NSWindow instance that is set to not be released when closed. Upon closing the window, I change the activation policy of the app to accessory, so the Dock icon is hidden away from the user.

When the user reopens the window, I change the activation policy back to regular and show the window again.

This is the entirety of my app delegate in a sample app that isolates the issue:

import Cocoa
import SwiftUI

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    var window: NSWindow!
    var statusItem: NSStatusItem!

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Create the window and set the content view.
        window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
            styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
            backing: .buffered, defer: false)
        window.center()
        window.setFrameAutosaveName("Main Window")
        window.contentView = NSHostingView(rootView: EmptyView())
        window.makeKeyAndOrderFront(nil)
        window.isReleasedWhenClosed = false
        window.delegate = self

        statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
        statusItem.button?.title = "X"
        statusItem.button?.target = self
        statusItem.button?.action = #selector(toggleWindow(sender:))

        NSApp.setActivationPolicy(.accessory)
    }

    func applicationWillTerminate(_ aNotification: Notification) {
        // Insert code here to tear down your application
    }

    @objc func toggleWindow(sender: AnyObject) {
        if window.isVisible {
            window.close()
        } else {
            window.makeKeyAndOrderFront(nil)
            NSApp.activate(ignoringOtherApps: true)
            NSApp.setActivationPolicy(.regular)
        }
    }
}

extension AppDelegate: NSWindowDelegate {

    func windowWillClose(_ notification: Notification) {
        NSApp.setActivationPolicy(.accessory)
    }

}

The app is the default template from Xcode 11, but I changed the main view to EmptyView() so no extra class is needed.

If I close the window with the semaphore button, and reopen it with status bar icon, it works fine.

However, if I close it through CMD-W and reopen it, I get the window back, but the menu remains selected, as if the previous Close command hadn't completed.

Menu with Edit selected

Since the menu is in this weird state, the app does not respond to events until the menu issue is solved (by clicking another menu item, for example).

Update:

As a workaround, I discovered that if I change the activation policy after a few milliseconds, the menu issue is solved:

    func windowWillClose(_ notification: Notification) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            NSApp.setActivationPolicy(.accessory)
        }
    }

However, once the window appears it is still not properly processing the events (the first click is missed).

1

1 Answers

0
votes

The following runs ok on my system and does not show the problematic behavior; perhaps I have misunderstood the problem. It's a programmatic demo which may be run in Xcode; for some reason none of the menus work when run off of the CommandLine.

import Cocoa

class AppDelegate: NSObject, NSApplicationDelegate {
 var window: NSWindow!
 var statusItem: NSStatusItem!

@objc func showMainWindow() {
    window.makeKeyAndOrderFront(nil)
}

@objc func hideMainWindow() {
    window.orderOut(nil)
}

@objc func myBtnAction(_ sender:AnyObject ) {
  NSSound.beep()
}

@objc func myMenuAction(_ sender:AnyObject ) {
  NSSound.beep()
}

func buildMenu() {
 let mainMenu = NSMenu()
 NSApp.mainMenu = mainMenu
 // **** App menu **** //
 let appMenuItem = NSMenuItem()
 mainMenu.addItem(appMenuItem)
 let appMenu = NSMenu()
 appMenuItem.submenu = appMenu
 appMenu.addItem(withTitle: "Quit", action:#selector(NSApplication.terminate), keyEquivalent: "q") 
 // **** File menu **** //
 let fileMenuItem = NSMenuItem()
 let fileMenu = NSMenu(title: "File")
 fileMenu.addItem(withTitle: "Beep", action:#selector(self.myMenuAction), keyEquivalent: "b")
 fileMenuItem.submenu = fileMenu
 mainMenu.addItem(fileMenuItem)
}

func buildWnd() {

let _wndW : CGFloat = 400
let _wndH : CGFloat = 300

 window = NSWindow(contentRect:NSMakeRect(0,0,_wndW,_wndH),styleMask:[.titled, .miniaturizable, .resizable], backing:.buffered, defer:false)
 window.center()
 window.title = "Swift Test Window"

 // **** TextView **** //
 let scrlView = NSScrollView (frame:NSMakeRect( 10, 60, _wndW - 20, _wndH - 70 ))
 window.contentView!.addSubview (scrlView)
 scrlView.hasVerticalScroller = true
 scrlView.autoresizingMask = [.height, .width]
 let txtView = NSTextView (frame:NSMakeRect( 0, 0, _wndW - 20, _wndH - 70 ))
 txtView.font = NSFont(name:"Menlo Bold", size: 13.0)
 scrlView.documentView = txtView

 // **** Button **** //
 let myBtn = NSButton (frame:NSMakeRect( 150, 15, 95, 30 ))
 myBtn.bezelStyle = .rounded
 myBtn.autoresizingMask = [.minXMargin,.maxYMargin]
 myBtn.title = "Beep"
 myBtn.action = #selector(self.myBtnAction(_:))
 window.contentView!.addSubview (myBtn)

// **** Quit btn **** //
let quitBtn = NSButton (frame:NSMakeRect( _wndW - 50, 10, 40, 40 ))
 quitBtn.bezelStyle = .circular
 quitBtn.autoresizingMask = [.minXMargin,.maxYMargin]
 quitBtn.title = "Q"
 quitBtn.action = #selector(NSApplication.terminate)
 window.contentView!.addSubview(quitBtn)
}

func applicationDidFinishLaunching(_ aNotification: Notification) {
 buildMenu()
 buildWnd()
 statusItem = NSStatusBar.system.statusItem(withLength: 100)
 statusItem.button!.title = "foobar"
 let menu = NSMenu()
 let showMenuItem = NSMenuItem(title: "Show Window", action:#selector(self.showMainWindow), keyEquivalent: "")
 menu.addItem(showMenuItem)
let hideMenuItem = NSMenuItem(title: "Hide Window", action:#selector(self.hideMainWindow), keyEquivalent: "")
 menu.addItem(hideMenuItem)
 menu.addItem(.separator())
 menu.addItem(withTitle: "Quit", action:#selector(NSApplication.terminate), keyEquivalent: "q") 
 statusItem.menu = menu

}
}
let appDelegate = AppDelegate()

// **** main.swift **** //
let app = NSApplication.shared
app.setActivationPolicy(.regular)
app.delegate = appDelegate
app.activate(ignoringOtherApps:true)
app.run()

To run in Xcode create a Swift app and add a main.swift file (File/New/File). Change import Foundation to import Cocoa. Copy/paste the main.swift code from the demo. Delete the old AppDelegate in its entirety and copy/paste the demo's AppDelegate. Hit Run. Should be able to show/hide the main window from the status bar. Text view maintains its focus the entire time on my system. Have not tried using a xib.