Heres whats going on:
I am attempting to build a Mac Status Bar App completely programmatically. Everything seems to be working fine, that is the menu shows up in the Mac status bar, the dropdown menu is displaying how it should. But when I click on the menu items, nothing happens. I even changed the target function to just doing the basic task of printing to the terminal, and nothing.
About the code:
The issue lies somewhere around here I think:
menu.addItem(NSMenuItem(title: val, action: #selector(toggleService), keyEquivalent: ""))
That code should fire off the > toggleService function. But it doesn't do anything. Could the issue be due to the fact that I am only inheriting from the NSObject class?
The Code
// StatusBar.swift
import Cocoa
class StatusBar: NSObject {
var menuButton = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
var menu = NSMenu()
var service = Service()
override init() {
super.init()
menuButton.button?.image = NSImage(named: NSImage.Name("icon"))
menuButton.menu = menu
menu.autoenablesItems = false
for (_, val) in service.list {
menu.addItem(NSMenuItem(title: val, action: #selector(toggleService), keyEquivalent: ""))
}
menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "Quit", action: #selector(quit), keyEquivalent: ""))
}
#objc func toggleService(sender: NSMenuItem) {
print ("Say Something.. anything??")
}
#objc func quit(sender: NSMenuItem) {
print ("Say Something.. anything??")
}
}
menuItem.target = self
You need to set the target to 'self'. NSMenuItems have two basic requirements. An action, and a target,
Action
menuItem.action: #selector(YOURFUNCTION)
Target
menuItem.target = self
So to get your menu items working, replace the for loop (within your init call) with this new one:
for (_, val) in service.list {
let menuItem = menu.addItem(NSMenuItem(title: val, action: #selector(toggleService), keyEquivalent: ""))
menuItem.target = self
}
Related
The first and second level menus are matched one by one through the NSmenuItem's title invocation array, and the first level menu also works properly to display data.
But once the secondary menu into NSView, only the first item will be show the data, the rest are blank, tried a lot of methods, did not succeed, please guide me, how to deal with?
update method:
func updateData() {
for city in cites {
let airQualityApi = AirQualityApi()
airQualityApi.fetchData(query: city, success: {airQuality in
if let airQualityMenyItem = self.statusMenu.item(withTitle: city){
airQualityMenyItem.title = airQuality.description
self.airQualityDetailView.update(airQuality: airQuality)
}
})
}
}
Sec level NSView update method:
func update(airQuality: AirQuality) {
DispatchQueue.main.sync {
self.cityName.stringValue = airQuality.city
self.airQuality.intValue = Int32(airQuality.aqi)
}
}
add NSMenuItem:
override func awakeFromNib() {
updateData()
let icon = NSImage(named: "icon")
icon?.isTemplate = true
if let button = statusItem.button {
button.image = icon
}
statusItem.menu = statusMenu
//let editMenuItem = NSMenuItem()
//editMenuItem.title = "showAqi"
for i in 0 ... cites.count - 1 {
let mainMenu = NSMenuItem(title: cites[i], action: .none, keyEquivalent: "")
statusMenu.insertItem(mainMenu, at: i)
let sub = NSMenu()
let subMenu = NSMenuItem(title: mainMenu.title, action: .none, keyEquivalent: "")
subMenu.view = airQualityDetailView
//print(subMenu.title)
statusMenu.setSubmenu(sub, for: mainMenu)
sub.addItem(subMenu)
//sub.insertItem(subMenu, at: 0)
}
}
you can see the integral code here π the whole project code on GitHub
thank you soooooo much
πππππππππ
I'm trying to use a closure instead selector but it does not work. The print does not work can you help me
My custom Action:
final class Action: NSObject {
private let _action: () -> ()
init(action: #escaping () -> ()) {
_action = action
super.init()
}
#objc func action() {
_action()
}
}
Using:
let menu = NSMenu()
let action = Action { print("My action") }
menu.addItem(NSMenuItem(title: "Delete", action: #selector(action.action), keyEquivalent: ""))
tableView.menu = menu
When I click on the menu, the delete option does not print, why does not it work?
Try setting a target for the NSMenuItem. As per the Apple documentation, this doesnβt seem to be included in the initializer but can be set afterwards.
let menu = NSMenu()
let action = Action { print("My action") }
var menuItem = NSMenuItem(title: "Delete", action: #selector(action), keyEquivalent: "")
menuItem.target = action // This refers to the action instance
menu.addItem(menuItem)
tableView.menu = menu
This is not possible because selectors are just names of methods, not methods themselves.
BUT there is another way to use a closure with #selector
/// Target-Action helper.
final class Action: NSObject {
private let _action: () -> ()
init(action: #escaping () -> ()) {
_action = action
super.init()
}
#objc func action() {
_action()
}
}
let action1 = Action { print("action1 triggered") }
let button = UIButton()
button.addTarget(action1, action: #selector(action1.action), forControlEvents: .TouchUpInside)
I have a problem creating a NSMenuItem actions, I have created function, which creates Menu for me, while using init on NSMenuItem with selector, my described function doesn't work on swift 4, if I use NSApp.terminate - NSMenuItem sees selector works normally()... Here is the code:
mainMenuController: NSObject {
func setUpMenus(){
//...
let firstItemMainMenu = NSMenuItem.init(title: "Test", action: #selector(test), keyEquivalent: "") // This one doesn't work
let fourthItemMainMenu = NSMenuItem.init(title: "Quit", action: #selector(NSApp.terminate), keyEquivalent: "q") //This one NSApp.terminate - works
//...
}
and here is the function:
#objc func test(_ sender: Any?){
let alert = NSAlert()
alert.addButton(withTitle: "test")
alert.messageText = "test"
alert.runModal()
}
}
Any ideas?
Two suggestions:
Use the syntax Selector("terminate:") which hands the selector over to First Responder (you will get a warning to use #selector syntax)
Create an extra method #objc func quit which calls NSApp.terminate
I'm currently working on a "menu bar only" project and I need to list all mounted Volumes in a submenu in the menubar app. I figured out how to print() all mounted volumes but I need help with the submenu (without .xib or .storyboard work)
This is my "listVolumes func"
func listVolumes(sender: NSMenuItem) {
let keys = [NSURLVolumeNameKey, NSURLVolumeIsRemovableKey, NSURLVolumeIsEjectableKey]
let paths = NSFileManager().mountedVolumeURLsIncludingResourceValuesForKeys(keys, options: [])
if let urls = paths {
for url in urls {
if let components = url.pathComponents
where components.count > 1
&& components[1] == "Volumes" {
print(url)
}
}
}
}
an below is my code for the "menu bar app"
let statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(-2)
func applicationDidFinishLaunching(aNotification: NSNotification) {
if let button = statusItem.button {
button.image = NSImage(named: "StatusBarButtonImage")
}
let menu = NSMenu()
menu.addItem(NSMenuItem(title: "Volumes", action: Selector("volumes:"), keyEquivalent: ""))
menu.addItem(NSMenuItem.separatorItem())
menu.addItem(NSMenuItem(title: "Help", action: Selector("help:"), keyEquivalent: ""))
menu.addItem(NSMenuItem.separatorItem())
menu.addItem(NSMenuItem(title: "Quit", action: Selector("terminate:"), keyEquivalent: "q"))
statusItem.menu = menu
}
so my question is, how can I create a submenu that contains all the mounted drives (programmatically only)
You have to set a submenu to the volumes menu item for example
let volumesMenuItem = NSMenuItem(title: "Volumes", action: Selector("volumes:"), keyEquivalent: "")
menu.addItem(volumesMenuItem)
let volumesMenu = NSMenu(title: "Volumes")
volumesMenuItem.submenu = volumesMenu
I recommend to return an array of the names from your listVolumes() function, then you can add menu items to volumesMenu assuming volumes contains the names
for volumeName in volumes {
volumesMenu.addItem(NSMenuItem(title: volumeName, action: Selector("selectVolume"), keyEquivalent: ""))
}
I'm at a loss to see why this doesn't work. The menu shows, but is grayed out if I leave autoenablesItems at the default, and the actions aren't called if I set it false.
class GameScene: SKScene {
// ...
func action1(sender: AnyObject) {
println("Urk, action 1")
}
func action2(sender: AnyObject) {
println("Urk, action 2")
}
func popUpMenu(#event: NSEvent) {
var theMenu = NSMenu(title: "Contextual menu")
theMenu.addItemWithTitle("Action 1", action: Selector("action1:"), keyEquivalent: "")
theMenu.addItemWithTitle("Action 2", action: Selector("action2:"), keyEquivalent: "")
//theMenu.autoenablesItems = false
NSMenu.popUpContextMenu(theMenu, withEvent:event, forView:self.view)
}
override func mouseDown(theEvent: NSEvent) {
self.popUpMenu(event: theEvent) // The menu shows
}
}
Update
As per #Chuck's answer, you will need to do the following:
func popUpMenu(#event: NSEvent) {
var theMenu = NSMenu(title: "Contextual menu")
theMenu.addItemWithTitle("Action 1", action: Selector("action1:"), keyEquivalent: "")
theMenu.addItemWithTitle("Action 2", action: Selector("action2:"), keyEquivalent: "")
for item: AnyObject in theMenu.itemArray {
if let menuItem = item as? NSMenuItem {
menuItem.target = self
}
}
NSMenu.popUpContextMenu(theMenu, withEvent:event, forView:self.view)
}
It sounds like your problem is that an NSMenuItem created with that method doesn't have a receiver, so it uses the responder chain, and this object is not in the responder chain. You can force it to see your object by setting the menu items' targets to self.