How to detect Option key down in Status bar app - swift

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.

Related

How do you get a text field input in you view controller code?

I’m trying to make Xcode print "Nice!" when you type in "Hi". I've used a IBOutlet, but I don’t know how to use the user input in my code. Also BTW I'm using Storyboard and not SwiftUI. It also gives me an error when I try to compare the datatype UIViewController and a String. Here is my view controller code(with the default App Delegate and Scene Delegate code):
import UIKit
class ViewController: UIViewController {
#IBOutlet var yeet: [UITextField]!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
func fuel(_ yeet:UIViewController) -> Int {
if yeet == ("hi") {
print("Nice!")
}
}
}
your textfield show be setup as
#IBOutlet weak var textFeildName: UITextField!
you will need to change a couple things inside of your file to prevent a crash. I'd delete the textfield and drag it into the assistant view and give it a new name.
but before you press "connect" press the "outlet" tab and change it to "Action" and then a new selector should come up select "Editing Did End" and go to the top and press "Did End On Exit"
after that is done would want to reference the variable of the text field:
example:
#IBAction func TextFieldName(_ sender: Any) {
if(self.TextFeildName.text!.contains("hi")){
print("Nice!")
}
}
On top of all this, you do not compare strings with == that's only if you compare 2 separate strings for example stringOne == stringTwo if you are comparing or asking if a string contains anything you'd want to use the developing language specific string container IE: .contains
Also, please do not include "Xcode" as a tag with your question, as that should be reserved for Xcode related problems. not Swift or objective-c coding issues.

How do I keep NSMenuItem selected in a NSPopover with NSMenu?

I have a NSPopUpButton with a built up NSMenu.
However, when an NSMenuItem is selected, it keeps going back to the first item.
In this example, I'd expect "Maged" to be selected.
Any ideas?
Related question/answer: https://stackoverflow.com/a/53387962/157656
Duplicate:
It's been suggested that this is a duplicate of the question
How do I change/modify the displayed title of an NSPopUpButton
"I would like an NSPopUpButton to display a different title than the title of the menu item that is selected."
However, my question was about getting the NSPopUpButton to show the selected item.
In the end, I changed how I did it.
I am using a NSButton to show the menu and NSTextField to display the results.
If anyone is interested in the details, here they are.
Build the menu up and use .representedObject to store whatever you need to access at the other end. I used a struct with the name and code the in it.
You need to assign the NSMenu to the NSButton.menu
Then have a click, something like this.
#IBAction func changeVoiceClicked(_ sender: NSButton)
{
if let event = NSApplication.shared.currentEvent {
NSMenu.popUpContextMenu(sender.menu!, with: event, for: sender)
}
}
Your NSMenuItem should have an action on it, which using a selector points to a function.
Something like this:
#objc func voiceChanged(sender: NSMenuItem)
{
// cope will nil
var voice : VoiceDetail = VoiceDetail();
if (sender.representedObject != nil) {
voice = sender.representedObject as! VoiceDetail;
}
// Do what you need to on menu select.
// update text field.
}

Custom Carbon key event handler fails after mouse events

I'm trying to write a custom NSMenu which will be able to list for key input and intercept the necessary events. This is to provide a simple search-as-you-type functionality for my open source clipboard manager.
It seems like the only way to do this is to install a custom Carbon event handler which will listen for key events and handler them accordingly, but it seems like there is an issue with such a custom handler.
Normally, I can propagate events downwards to other handlers (e.g. system ones) and they should be gracefully handled. This can be done by a simple callback:
let eventHandlerCallback: EventHandlerUPP = { eventHandlerCallRef, eventRef, userData in
let response = CallNextEventHandler(eventHandlerCallRef, eventRef!)
print("Response \(response)")
return response
}
This callback works perfectly and prints Response 0 all the time. This response means that the event is handled correctly.
However, things get weird once we send mouse events before keyboard events. In such case, the callback fails and prints Response -9874. This response means that the event was not handled correctly.
It seems like the event fails to be handled somewhere below my custom view and I don't know where exactly or how to overcome this issue.
To reproduce, I've uploaded the code to Gist which can be added to XCode playground and run. Once you see menu popup, press some keys (preferably arrow keys as they won't close the menu) and observe Response 0 in the console. After that, move cursor inside the menu and press more arrow keys. You should see Response -9874 in the console now.
Unclear if you have an NSTextField as your menu view, but if you use one then it's easy to setup a delegate for that text field that can get the current contents of the field as the user types (this takes care of them moving backwards with the arrow keys and then deleting characters, using the delete key, etc). Your delegate implements the appropriate delegate method and gets called each time the text changes:
extension CustomMenuItemViewController: NSTextFieldDelegate {
func controlTextDidChange( _ obj: Notification) {
if let postingObject = obj.object as? NSTextField {
let text = postingObject.stringValue
print("the text is now: \(text)")
}
}
}
Just to confirm this works as expected, I created the ViewController class for the custom menu item views (label + edit field) in a xib file and then dynamically built a simple test menu with a single menu item that has the custom view controller's view and added it to the menubar inside my app delegate:
func installCustomMenuItem() {
let menuBarItem = NSMenuItem(title: "Test", action: nil, keyEquivalent: "")
let menu = NSMenu(title: "TestMenu" )
let subMenuBarItem = NSMenuItem(title: "Custom View", action: nil, keyEquivalent: "")
subMenuBarItem.view = menuItemVC.view
menu.addItem(subMenuBarItem)
menuBarItem.submenu = menu
NSApp.mainMenu?.addItem(menuBarItem)
}
Looks like this after I typed "hello":
And you can from the console that my handler got called for every character typed:
the text is now: H
the text is now: He
the text is now: Hel
the text is now: Hell
the text is now: Hello
Your situation is probably a little different, but it seems like this approach is very clean and might work for you. If it won't for some reason, add a clarifying comment and we'll see if we can't make it work for you.
Addition:
It occurred to me that you might wish to not use NSTextField and so I was curious if it was as easy to do this with a custom view and it's relatively easy.
Make a subclass of NSView:
class CustomMenuView: NSView {
override var acceptsFirstResponder: Bool {
return true
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// Drawing code here.
}
override func keyDown(with event: NSEvent) {
print("key down with character: \(String(describing: event.characters)) " )
}
}
Set the class of your root view in the custom view controller to be of this type of class and then all done as before - view controller loaded in applicationDidFinishLaunching and menus built and view controller's view (which is now a CustomMenuView) is set as the menuBarItem.view.
That's it. You now get your keyDown method called for every key down when the menu is dropped down.
key down with character: Optional("H")
key down with character: Optional("e")
key down with character: Optional("l")
key down with character: Optional("l")
key down with character: Optional("o")
key down with character: Optional(" ")
key down with character: Optional("T")
key down with character: Optional("h")
key down with character: Optional("i")
key down with character: Optional("s")
key down with character: Optional(" ")
key down with character: Optional("i")
key down with character: Optional("s")
key down with character: Optional(" ")
key down with character: Optional("c")
key down with character: Optional("o")
key down with character: Optional("o")
key down with character: Optional("l")
:)
Now your custom view (and subviews if you like) can do their own drawing and so on.
Addition with requested sample without the ViewController:
// Simple swift playground test
// The pop-up menu will show up onscreen in the playground at a fixed location.
// Click in the popup and then all key commands will be logged.
// The ViewController in my example above may be taking care of putting the custom view in the responder chain, or the fact that it's in a menubar and being invoked via a MenuItem might be.
// I'd suggest trying it in the actual environment rather than in a playground. In my test app you click the menu name in the menubar to drop down the menu and it is added to the responder chain and works as expected without having to click in the menu first to get the events flowing.
// There is no reason you need to be hooking events either with carbon events or the newer format. If you're in the responder chain of and implement the necessary, method then you'll get the key events you're looking for.
import AppKit
class CustomMenuView: NSView {
override var acceptsFirstResponder: Bool {
return true
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// Drawing code here.
}
override func keyDown(with event: NSEvent) {
print("key down with character: \(String(describing: event.characters)) " )
}
}
func installCustomMenuItem() -> NSMenu {
// let menuBarItem = NSMenuItem(title: "Test", action: nil, keyEquivalent: "")
let resultMenu = NSMenu(title: "TestMenu" )
let subMenuBarItem = NSMenuItem(title: "Custom View", action: nil, keyEquivalent: "")
subMenuBarItem.view = CustomMenuView(frame: NSRect(x: 0, y: 0, width: 40, height: 44))
resultMenu.addItem(subMenuBarItem)
// menuBarItem.submenu = menu
return resultMenu
}
var menu = installCustomMenuItem()
menu.popUp(positioning: nil, at: NSPoint(x:600,y:400), in: nil)
I didn't manage to figure out why this issue was happening or how to fix it, but I understood that it's possible to work around this issue by intercepting all the keys and simulating their behavior manually.
For example, this is how I now handle down arrow key which is supposed to select next item in menu list:
class Menu: NSMenu {
func selectNext() {
var indexToHighlight = 1
if let item = highlightedItem {
indexToHighlight = index(of: item) + 1
}
if let itemToHighlight = self.item(at: indexToHighlight) {
let highlightItemSelector = NSSelectorFromString("highlightItem:")
perform(highlightItemSelector, with: itemToHighlight)
if itemToHighlight.isSeparatorItem || !itemToHighlight.isEnabled || itemToHighlight.isHidden {
selectNext()
}
}
}
}
This way, when I receive a key down event with down arrow key - I can just call the function and return true to prevent the event from reaching default NSMenu handler. Similarly, up arrow key can be done.
In case of a return key, I ended up with the following code:
class Menu: NSMenu {
func select() {
if let item = highlightedItem {
performActionForItem(at: index(of: item))
cancelTracking()
}
}
}
The full commit implementing this is https://github.com/p0deje/Maccy/commit/158610d1d.

Get reference to current object

My goal is to safe a reference from the button, label or textfield inside a variable.
The problem is that I don't know on which control the user tapped.
I am having a simple application which looks like this:
The user can touch any control.
It is easy enough with just those three controls because I can drag in a action. But if I am having many of them I can't handle them all over the action methods. Is there a general way in which I can safe a reference to the control in a variable so that I can know which of the controls is the active one?
Edit
As suggested I am using a function and assigning the variable to the sender of the function. This is how it looks in code:
var currentObject: NSTextField!
override func viewDidLoad() {
super.viewDidLoad()
myTextfield.action = #selector(myAction)
}
func myAction(sender: NSTextField)
{
print("aktuell: \(sender)")
currentObject = sender
}
As you can see this only works for a NSTextfield. Is there a way in which the function works for every control?
Set the tag attribute for each item, and then you can check sender.tag to identify which object is calling it.
To set the tag, select the Attributes inspector in Storyboard (upper right side - middle button of Utilities) and look for this section:

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.