Status bar app window deallocation - swift

I'm developing a simple OSX status / menu bar app using this tutorial: http://footle.org/WeatherBar/
This app is going to have a menu with "Preferences" option, which should open the preferences window.
Since the preferences window is going to be opened rather rarely I would like the window to be created only when needed and then deallocated after closing.
Here is the code for the status menu controller which controls showing and creating the preferences window:
class StatusMenuController: NSObject {
#IBOutlet weak var statusMenu: NSMenu!
var preferencesWindowCtrl: PreferencesWindowController!
let statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(NSVariableStatusItemLength)
override func awakeFromNib() {
statusItem.title = "MyApp"
statusItem.menu = statusMenu
preferencesWindowCtrl= PreferencesWindowController()
}
#IBAction func preferencesClicked(sender: NSMenuItem) {
preferencesWindowCtrl.showWindow(nil)
/*
THIS CAUSES THE WINDOW TO BE DEALLOCATED IMMEDIATELY:
let myPrefWindow = PreferencesWindowController()
myPrefWindow.showWindow(nil)
*/
}
#IBAction func quitClicked(sender: NSMenuItem) {
NSApplication.sharedApplication().terminate(self)
}
}
In this code window is instantiated in the the status menu controller awakeFromNib, which is something I wanted to avoid (since it makes the window alive for the whole app lifetime). However, if I create it as a local variable inside preferencesClicked it gets deallocated immediately as this functions exists (not really surprising).
How can I make sure this window gets deallocated after it is closed? I guess setting release when closed = true for that window will not help, since the reference is held by StatusMenuController.

Related

How to communicate to the window controller from view controller of newly, programmatically created tab?

In my window controller, I implement:
#IBAction override func newWindowForTab(_ sender: Any?) {
if let wc = NSStoryboard.main?.instantiateInitialController() as? WindowController,
let window = wc.window {
self.window?.addTabbedWindow(window, ordered: .above)
window.makeKey()
}
}
In the view controller, I have this code:
let window = self.view.window?.windowController as? WindowController
Also tried:
let window = NSApp.mainWindow?.windowController as? WindowController
If I don't have any tabs, it's able to get the window controller. But on new tabs, it does grab the window controller.
Similarly, I've unsuccessfully tried sending an action to the WindowController:
NSApp.sendAction(#selector(WindowController.pageLabelChange), to: nil, from: label)
Works for the original window, but not for any newly created tabs.
How do the newly created view controller objects communicate with the window controller?
Edit:
For more context in how I am using this code: It's basically a PDFView that's embedded in a window. The window has a tool bar that displays the page number. Using any of the above code, I can set the current page number of the PDFView, but when there's a tab, it does not work. Using the .PDFViewPageChanged notification, I call my func
NSApp.sendAction(#selector(WindowController.pageLabelChange), to: nil, from: pdfView)
Edit 2:
I've created a GitHub with a test project that shows the problem I have. You should be able to see that when you launch the project, the + button will add a number to the textfield in the tab bar. But if you go to the View menu > Add tab, it creates a new tab, but the + does nothing.
You create new window controller on stack so it is destroyed right after return, that is the reason. You need to store somewhere reference to created tab window controller and manage it (the place of storage is up to your app logic).
Here is simple demo that makes code work
Tested with Xcode 11.4 / macOS 10.15.5
final class WindowController: NSWindowController {
#IBOutlet weak var testField: NSTextField!
var tabControllers = [WindowController]() // << storage for child controllers
#IBAction override func newWindowForTab(_ sender: Any?) {
if let wc = NSStoryboard.main?.instantiateInitialController() as? WindowController,
let window = wc.window {
self.window?.addTabbedWindow(window, ordered: .above)
window.makeKey()
tabControllers.append(wc) // keep reference in storage
// TODO: - you are responsible to manage wc, eg: remove
// from storage on window close.
}
}
}

NSWindowController: How to open only one time: macOS App in status bar

I create a simple app for only status bar in macOS Swift.
I create a new NSWindowController and xib, when I call #objc function
self.aboutWindows.showWindow(self)
if I click it opens, but every time I click it opens a new window. How can I avoid it.
Same for another function of an NSMenuItem, I would like to start it only once.
Thanks
EDIT:
ALL IN APPDELEGATE
var aboutWindows = AboutWindows()
...
//TAP ON MENU ITEM
#objc func aboutWindows(_ sender: Any) {
aboutWindows = AboutWindows(windowNibName: "AboutWindows")
self.aboutWindows.showWindow(self)
}

OSX Swift - Show modal second window

I am trying to display a second window after a button click:
var winJ:WinJo // other window NSViewController
#IBAction func BtnNewWin(sender: AnyObject) {
winJ = WinJo()
winJ.showWindow(self)
}
This works fine but I want the new window to be modal. I accomplished this with the Xcode designer but I couldn't figure out how to do this in code.
After I was pointed in the right direction I found the solution to my problem:
NSApp.runModalForWindow(winJ.window!)
Where NSApp is actually the instance of NSApplication.
And very important in the second window:
func windowWillClose(notification: NSNotification) {
NSApp.stopModal()
}
Otherwise your main window will be blocked after closing the second.

Tooltip doesn't show up again

I have a Mac app that exclusively live on the menu bar. It has a progress bar and a label. The label shows the percentage of the progress of the task that's being carried out. I want to show more info when the user hovers the mouse pointer over the progress indicator.
When I set the tooltip initially and hover over, it displays without an issue.
But if I head over somewhere and open the menu app again and hover over again, the tooltip doesn't come up. I can't figure out why. Here's my code.
ProgressMenuController.swift
import Cocoa
class ProgressMenuController: NSObject {
#IBOutlet weak var menu: NSMenu!
#IBOutlet weak var progressView: ProgressView!
let menuItem = NSStatusBar.systemStatusBar().statusItemWithLength(NSVariableStatusItemLength)
var progressMenuItem: NSMenuItem!
override func awakeFromNib() {
menuItem.menu = menu
menuItem.image = NSImage(named: "icon")
progressMenuItem = menu.itemWithTitle("Progress")
progressMenuItem.view = progressView
progressView.update(42)
}
#IBAction func quitClicked(sender: NSMenuItem) {
NSApplication.sharedApplication().terminate(self)
}
}
ProgressView.swift
import Cocoa
class ProgressView: NSView {
#IBOutlet weak var progressIndicator: NSProgressIndicator!
#IBOutlet weak var progressPercentageLabel: NSTextField!
func update(value: Double) {
dispatch_async(dispatch_get_main_queue()) {
self.progressIndicator.doubleValue = value
self.progressIndicator.toolTip = "3 out of 5 files has been copied"
self.progressPercentageLabel.stringValue = "\(value)%"
}
}
}
This is a demo app similar to my actual app. So the update() function is called only once and the values are hardcoded. But in my actual app, the progress is tracked periodically and the update() function gets called with it to update the values. The label's percentage value and the progress indicator's value get updated without a problem. The issue is only with the tooltip.
Is this expected behavior or am I missing something?
I ran into the same problem, and realized the issue was that only the currently focused window will display tool-tips, but after my app lost focus, it would never get it back. Focus usually transfers automatically when the user clicks on your window, but it isn't automatic for menu bar apps. Using NSApp.activate, you can regain focus onto your app:
override func viewWillAppear() {
super.viewWillAppear()
NSApp.activate(ignoringOtherApps: true)
}
sanche's answer worked for me as well, but I ended up moving the tool tips to my NSMenuItems instead so I wouldn't have to steal focus from the foreground app. NSMenuItem's tool tips seem to be handled as a special case so the app doesn't need to be focused.
This solution would make the tool tip apply to everything in the menu item and appear next to the menu rather than over it, but it looks like that might not be a problem in your case.

Menubar with Storyboard - validateMenuItem not get called

I'm trying to setup a menubar Application using storyboard but my validateMenuItem method not get called.
I will try to explain what i did.
First i dragged a Menu Item in my Application Scene. Then one Object for my MenuController. Created a MenuController (MenuController.swift) and filled it with code. Back in my storyboard I set my Menu delegate to MenuController and MenuController Outlet to Menu. (I'm not totally sure whether i have set the delegates correctly.)
When i start the app, the menu icon appears and the first item title is set to test. But when i'm clicking the icon the validateMenuItem method not get called.
MenuController.swift
import Cocoa
class MenuController: NSObject {
var statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(-1)
#IBOutlet weak var statusMenu: NSMenu!
#IBOutlet weak var item1: NSMenuItem!
override func awakeFromNib() {
print("awakeFromNib")
self.item1.title = "Test"
let icon = NSImage(named: "menubarIcon")
statusItem.image = icon
statusItem.menu = statusMenu
}
override func validateMenuItem(menuItem: NSMenuItem) -> Bool {
print("validateMenuItem")
return true
}
}
Storyboard Menu Delegates
(source: picr.de)
Storyboard MenuController Delegates
(source: picr.de)
Has anybody an idea?
Greets from Austria!
The menu/UI validation mechanism does not query the menu's delegate but uses the item's target to determine the enabled state instead.
If the target is not explicitly set, it walks the responder chain.
To get basic validation, you have to make sure that the following things are setup:
"Auto Enables Items" is checked in Interface Builder (on by default)
You control-dragged the menu's action to the first responder
The menu's action is implemented in a class that is part of the responder chain (e.g. a view controller that manages a set of actions for your UI)
A basic implementation of validateUserInterfaceItem: could look like the following for an item with an action selector called test:
func validateUserInterfaceItem(anItem: NSValidatedUserInterfaceItem) -> Bool
{
if anItem.action() == Selector("test:") {
print("validating item \(anItem)")
return true
}
return true
}