Tooltip doesn't show up again - swift

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.

Related

How to detect Option key down in Status bar app

I have a Swift Cocoa app that runs as an NSStatusItem, i.e., it presents a dropdown menu in Mac's top-right status bar. I would like to display extra "advanced" menu items when the user holds down the Option key while opening my status item menu. I can detect presses of the Option key, but my problem is that I cannot determine whether the key is down or up.
Here is the code I have so far in my AppDelegate.swift:
func applicationDidFinishLaunching(_ aNotification: Notification) {
NSEvent.addGlobalMonitorForEvents(matching: NSEvent.EventTypeMask.flagsChanged, handler: keyEvent);
}
func keyEvent(event: NSEvent) {
if let event = NSApp.currentEvent {
if event.modifierFlags.contains(.option) {
print("option key up/down event")
}
}
}
This works insofar as it prints the message when the Option key is either pressed down or released back up. The problem is that I don't know how to determine which state the Option key is in at any given time.
How can I adapt this code to know if the Option key is down or up?
EDIT: SO suggested that I edit my question to explain why it is not answered by Hide/Show menu item in application's main menu by pressing Option key. Briefly, the answers on that page, while probably workable, seemed either overly complex (adding timers, adding run loop observers, adding zero height custom views) or they were creating menu item alternates whereas I was trying to add additional menu items. Nevertheless, it is a good page and I recommend studying it if you also have this question.
Just ask for the current event and return a Bool, it's true when the key is pressed at the moment
func isOptionKeyPressed() -> Bool {
return NSEvent.modifierFlags.contains(.option)
}
addGlobalMonitorForEvents is not needed
I was able to find a working solution, inspired by (but not the same as) another answer here posted by vadian. This is my solution.
Eliminate the addGlobalMonitorForEvents and keyEvent code in my original question.
Create an an NSMenuDelegate like so:
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var extrasSeparator: NSMenuItem?
#IBOutlet weak var extrasSubmenu: NSMenuItem?
[...rest of AppDelegate...]
}
extension AppDelegate: NSMenuDelegate {
func menuWillOpen(_ menu: NSMenu) {
if NSEvent.modifierFlags.contains(.option) {
extrasSeparator?.isHidden = false
extrasSubmenu?.isHidden = false
} else {
extrasSeparator?.isHidden = true
extrasSubmenu?.isHidden = true
}
}
}
Caveat: this solution only works if the user presses and holds the Option key before clicking the NSStatusItem's icon. That's what was asked in the original question, but it may not be desirable in all cases.

Swift: How to link Touch Bar controls to main window controls

I'm new to Swift/macOS dev, plenty of dev experience otherwise though. Just trying to make something rudimentary.
Here's my app storyboard:
I'm trying to get:
the Touch Bar slider to change when the slider on the main window changes
vice versa
update the Touch Bar Label button with the Int value of the slider.
Q) How do I achieve this?
Note: The main window slider control is wired up and working when I manipulate it e.g.
#IBOutlet weak var mySlider: NSSlider!
#IBAction func mySlider_Changed(_ sender: NSSlider) {
//... stuff happens here.
}
You'll want your view controller to have some explicit model/state of what the value of these sliders have. e.g.
class ViewController : NSViewController {
var value: Double
}
Then you can connect the sliders and textfield to update or display this value.
Approach 1: Target/Action/SetValue
This follows the use of explicit IBActions that you had started. In response to that action, we'll pull the doubleValue from the slider and update the ViewController's model from that:
#IBAction func sliderValueChanged(_ sender: NSSlider) {
value = sender.doubleValue
}
The second piece is updating everything to reflect that new value. With Swift, we can just use the didSet observer on the ViewController's value property to know when it changes and update all of the controls, e.g:
#IBOutlet weak var touchBarSlider: NSSlider!
#IBOutlet weak var windowSlider: NSSlider!
#IBOutlet weak var windowTextField: NSTextField!
var value: Double {
didSet {
touchBarSlider.doubleValue = value
windowSlider.doubleValue = value
windowTextField.doubleValue = value
}
}
And that's it. You can add a number formatter to the textfield so it nicely displays the value, which you can do in Interface Builder or programmatically. And any other time you change the value, all of the controls will still get updated since they are updated in the didSet observer instead of just the slider action methods.
Approach 2: Bindings
Bindings can eliminate a lot of this boiler plate code when it comes to connecting model data to your views.
With bindings you can get rid of the outlets and the action methods, and have the only thing left in the view controller be:
class ViewController: NSViewController {
#objc dynamic var value: Double
}
The #objc dynamic makes the property be KVO compliant, which is required when using bindings.
The other piece is establishing bindings from the controls to our ViewController's value property. For all of the controls this is done by through the bindings inspector pane, binding the 'Value' of the control to the View Controller's value key path:
And that's it. Again, you could add a number formatter to the textfield, and any other changes to the value property will still update your controls since it will trigger the bindings to it. (you can also still use the didSet observer for value to make other changes that you can't do with bindings)

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.

Status bar app window deallocation

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.

Why would the action not be able to connect to target class NSViewController?

I'm trying to learn Swift, but I seem to have gotten stuck at this (admittedly, probably very simple) problem - the error as follows:
Could not connect action, target class NSViewController does not respond to -(encbutton/decbutton)
Here is my code. I'm designing my interface in the Storyboard and connecting it to the code through #IB(Outlet/Action).
// ViewController.swift
import Cocoa
import Foundation
class TabViewController: NSTabViewController {
// This has been changed from NSViewController to NSTabViewController as I have replaced the initial single-view with a two-tab-view.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: AnyObject? {
didSet {
// Update the view, if already loaded.
}
}
}
public class EncViewController: NSViewController {
// This is for the first tab, encrypt
#IBOutlet var encdirfield: NSTextField!
#IBOutlet var encpassfield: NSSecureTextField!
#IBOutlet var enclogfield: NSTextField!
#IBOutlet var encbutton: NSButton!
#IBAction func startenc(sender: NSButton) { // here's the problem. the function isn't triggered when the button is pressed
// get values
encdir = encdirfield.stringValue
encpass = encpassfield.stringValue
tarcrypt.enc();
// this is an function that leads to an NSTask that runs a binary I wrote (not related).
// definitely not the cause of the problem because running it independently works fine
}
}
public class DecViewController: NSViewController {
// and this is for the second tab, decrypt
#IBOutlet var decdirfield: NSTextField!
#IBOutlet var decpassfield: NSSecureTextField!
#IBOutlet var declogfield: NSTextField!
#IBOutlet var decbutton: NSButton!
#IBAction func startdec(sender: NSButton) { // here's the same problem, again. the function isn't triggered when the button is pressed
// get values
encdir = encdirfield.stringValue
encpass = encpassfield.stringValue
tarcrypt.dec();
// this is an function that leads to an NSTask that runs a binary I wrote (not related).
// definitely not the cause of the problem because running it independently works fine
}
}
For some reason, upon drawing the scene along with the NSButton, the error message as seen above is generated. What is causing the error, and how do I fix it?
I've figured it out! For anyone else who runs into this problem, here's how to fix it:
It turns out there is a little dropdown under "Custom Class" titled "Module", which is, by default, set to none. Set it to tarcrypt (or whichever available option suits you) and that should fix the errors.
Thanks for all the help!
It sounds as if you connected your UI element to the File's Owner object, which is an instance of NSApplication.
If you haven't done so already, you want to drag a NSObject out of the Object Library palette in Xcode 4 to the margin to the left of your layout. Once you've done that, and have selected it, choose the identity inspector and, in the Class field, enter "WindowController"
Swift 5 and Xcode 13.3
Recently I had the same bug. I solved it by reassigning the viewcontroller class name to the class in my nameclass.swift file and activating the MODULE entry with my project name (following the dropdown menu).