NSMenu selector in Swift - swift

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.

Related

How to implement a NSButton like the Finder button

Please refer to the screenshot, I want to implement a NSButton like the buttons on Finder window. It seems that the button style is similar with a NSButton recessed. I have tried the recessed button but it is a little different from the Finder button, even the one which have a drop down menu. I have no idea how to make a drop down menu with recessed button.
If I choose to make a bevel style borderless NSButton with a special image, I cannot set a highlighted hover background whose size is larger than the image inside. The hover area is always the same with the image.
So any body known how to implement this. Thanks
Use NSMenuToolbarItem instead and check the inline comment in the sample source:
Assign this custom class MainWindowController to the initial NSWindowController controller:
class MainWindowController: NSWindowController {
// `NSMenuToolbarItem` Items
var dropDownMenu: NSMenu = {
var menu = NSMenu(title: "DropDown")
let item1 = NSMenuItem(title: "Item 1", action: nil, keyEquivalent: "")
let item2 = NSMenuItem(title: "Item 1", action: nil, keyEquivalent: "")
let item3 = NSMenuItem(title: "Item 3", action: nil, keyEquivalent: "")
menu.items = [item1, item2, item3]
return menu
}()
override func windowDidLoad() {
super.windowDidLoad()
configureToolbar()
}
private func configureToolbar() {
if let safeWindow = self.window {
let toolbar = NSToolbar(identifier: "mainWindowToolbar")
toolbar.delegate = self
toolbar.displayMode = .default
safeWindow.toolbarStyle = .automatic
safeWindow.toolbar = toolbar
safeWindow.toolbar?.validateVisibleItems()
}
}
}
extension MainWindowController: NSToolbarDelegate {
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
let toolbarItem = NSMenuToolbarItem(itemIdentifier: itemIdentifier)
toolbarItem.showsIndicator = true // To display DropDown Indicator
toolbarItem.menu = self.dropDownMenu
toolbarItem.isBordered = true // Show ToolBar Item Background on Mouse Hover
toolbarItem.image = NSImage(systemSymbolName: "ellipsis.circle", accessibilityDescription: "")
return toolbarItem
}
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [NSToolbarItem.Identifier("DropDownAction")]
}
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [NSToolbarItem.Identifier("DropDownAction")]
}
}
Output:

second level menu NSView to link NSMenuItem problems

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
πŸ™πŸ™πŸ™πŸ™πŸ™πŸ™πŸ™πŸ™πŸ™

In 2020, is there a way to deal with left and right click for menu bar separately using Swift? [duplicate]

I have the following code: (can be copy-pasted to New macOS project)
import Cocoa
import SwiftUI
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var statusBarItem: NSStatusItem!
func applicationDidFinishLaunching(_ aNotification: Notification) {
let statusBar = NSStatusBar.system
statusBarItem = statusBar.statusItem(
withLength: NSStatusItem.squareLength)
statusBarItem.button?.title = "🍎"
// Setting action
statusBarItem.button?.action = #selector(self.statusBarButtonClicked(sender:))
statusBarItem.button?.sendAction(on: [.leftMouseUp])
let statusBarMenu = NSMenu(title: "Status Bar Menu")
statusBarMenu.addItem(
withTitle: "Order an apple",
action: #selector(AppDelegate.orderAnApple),
keyEquivalent: "")
statusBarMenu.addItem(
withTitle: "Cancel apple order",
action: #selector(AppDelegate.cancelAppleOrder),
keyEquivalent: "")
// Setting menu
statusBarItem.menu = statusBarMenu
}
#objc func statusBarButtonClicked(sender: NSStatusBarButton) {
let event = NSApp.currentEvent!
if event.type == NSEvent.EventType.rightMouseUp {
print("Right click!")
} else {
print("Left click!")
}
}
#objc func orderAnApple() {
print("Ordering a apple!")
}
#objc func cancelAppleOrder() {
print("Canceling your order :(")
}
}
Actual behaviour: Menu opens on both left and right click, statusBarButtonClicked is not triggered.
After removing this line:
statusBarItem.menu = statusBarMenu
statusBarButtonClicked triggers on left click, menu doesn't show up (as expected)
Desired behaviour: Menu opens on right click, on left click menu doesn't open, action is triggered. How do I achieve it?
EDIT
I managed to achieve desired behavior with help of #red_menace comment:
import Cocoa
import SwiftUI
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var statusBarItem: NSStatusItem!
var menu: NSMenu!
func applicationDidFinishLaunching(_ aNotification: Notification) {
let statusBar = NSStatusBar.system
statusBarItem = statusBar.statusItem(
withLength: NSStatusItem.squareLength)
statusBarItem.button?.title = "🍎"
// Setting action
statusBarItem.button?.action = #selector(self.statusBarButtonClicked(sender:))
statusBarItem.button?.sendAction(on: [.leftMouseUp, .rightMouseUp])
let statusBarMenu = NSMenu(title: "Status Bar Menu")
statusBarMenu.addItem(
withTitle: "Order an apple",
action: #selector(AppDelegate.orderAnApple),
keyEquivalent: "")
statusBarMenu.addItem(
withTitle: "Cancel apple order",
action: #selector(AppDelegate.cancelAppleOrder),
keyEquivalent: "")
// Setting menu
menu = statusBarMenu
}
#objc func statusBarButtonClicked(sender: NSStatusBarButton) {
let event = NSApp.currentEvent!
if event.type == NSEvent.EventType.rightMouseUp {
statusBarItem.popUpMenu(menu)
} else {
print("Left click!")
}
}
#objc func orderAnApple() {
print("Ordering a apple!")
}
#objc func cancelAppleOrder() {
print("Canceling your order :(")
}
}
But Xcode says that openMenu func is deprecated in 10.14 and tells me to Use the menu property instead. Is there I way to achieve desired behaviour with new API?
The usual way a to show a menu is to assign a menu to the status item, where it will be shown when the status item button is clicked. Since popUpMenu is deprecated, another way is needed to show the menu under different conditions. If you want the right click to use an actual status item menu instead of just showing a contextual menu at the status item location, the status item menu property can be kept nil until you want to show it.
I've adapted your code to keep the statusBarItem and statusBarMenu references separate, only adding the menu to the status item in the clicked action method. In the action method, once the menu is added, a normal click is performed on the status button to drop the menu. Since the status item will then always show its menu when the button is clicked, an NSMenuDelegate method is added to set the menu property to nil when the menu is closed, restoring the original operation:
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
// keep status item and menu separate
var statusBarItem: NSStatusItem!
var statusBarMenu: NSMenu!
func applicationDidFinishLaunching(_ aNotification: Notification) {
let statusBar = NSStatusBar.system
statusBarItem = statusBar.statusItem(withLength: NSStatusItem.squareLength)
statusBarItem.button?.title = "🍎"
statusBarItem.button?.action = #selector(self.statusBarButtonClicked(sender:))
statusBarItem.button?.sendAction(on: [.leftMouseUp, .rightMouseUp])
statusBarMenu = NSMenu(title: "Status Bar Menu")
statusBarMenu.delegate = self
statusBarMenu.addItem(
withTitle: "Order an apple",
action: #selector(AppDelegate.orderAnApple),
keyEquivalent: "")
statusBarMenu.addItem(
withTitle: "Cancel apple order",
action: #selector(AppDelegate.cancelAppleOrder),
keyEquivalent: "")
}
#objc func statusBarButtonClicked(sender: NSStatusBarButton) {
let event = NSApp.currentEvent!
if event.type == NSEvent.EventType.rightMouseUp {
print("Right click!")
statusBarItem.menu = statusBarMenu // add menu to button...
statusBarItem.button?.performClick(nil) // ...and click
} else {
print("Left click!")
}
}
#objc func menuDidClose(_ menu: NSMenu) {
statusBarItem.menu = nil // remove menu so button works as before
}
#objc func orderAnApple() {
print("Ordering a apple!")
}
#objc func cancelAppleOrder() {
print("Canceling your order :(")
}
}
Here is possible approach. There might be more accurate calculations for menu position, including taking into account possible differences of userInterfaceLayoutDirection, but the idea remains the same - take possible events under manual control and make own decision about what to do on each event.
Important places commented in code. (Tested on Xcode 11.2, macOS 10.15)
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var statusBarItem: NSStatusItem!
func applicationDidFinishLaunching(_ aNotification: Notification) {
let statusBar = NSStatusBar.system
statusBarItem = statusBar.statusItem(
withLength: NSStatusItem.squareLength)
statusBarItem.button?.title = "🍎"
// Setting action
statusBarItem.button?.action = #selector(self.statusBarButtonClicked(sender:))
statusBarItem.button?.sendAction(on: [.leftMouseUp, .rightMouseUp]) // << send action in both cases
let statusBarMenu = NSMenu(title: "Status Bar Menu")
statusBarMenu.addItem(
withTitle: "Order an apple",
action: #selector(AppDelegate.orderAnApple),
keyEquivalent: "")
statusBarMenu.addItem(
withTitle: "Cancel apple order",
action: #selector(AppDelegate.cancelAppleOrder),
keyEquivalent: "")
// Setting menu
statusBarItem.button?.menu = statusBarMenu // << store menu in button, not item
}
#objc func statusBarButtonClicked(sender: NSStatusBarButton) {
let event = NSApp.currentEvent!
if event.type == NSEvent.EventType.rightMouseUp {
print("Right click!")
if let button = statusBarItem.button { // << pop up menu programmatically
button.menu?.popUp(positioning: nil, at: CGPoint(x: -1, y: button.bounds.maxY + 5), in: button)
}
} else {
print("Left click!")
}
}
#objc func orderAnApple() {
print("Ordering a apple!")
}
#objc func cancelAppleOrder() {
print("Canceling your order :(")
}
}
I'm using this code on macOS Catalina. 10.15.2. ( Xcode 11.3).
On left click It trigger action.
On right click it show menu.
//HEADER FILE
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
NS_ASSUME_NONNULL_BEGIN
#protocol MOSMainStatusBarDelegate
- (void) menuBarControllerStatusChanged: (BOOL) active;
#end
#interface MOSMainStatusBar : NSObject
#property (strong) NSMenu *menu;
#property (strong, nonatomic) NSImage *image;
#property (unsafe_unretained, nonatomic) id<MOSMainStatusBarDelegate> delegate;
- (instancetype) initWithImage: (NSImage *) image menu: (NSMenu *) menu;
- (NSStatusBarButton *) statusItemView;
- (void) showStatusItem;
- (void) hideStatusItem;
#end
//IMPLEMANTION FILE.
#import "MOSMainStatusBar.h"
#interface MOSMainStatusBar ()
#property (strong, nonatomic) NSStatusItem *statusItem;
#end
#implementation MOSMainStatusBar
- (instancetype) initWithImage: (NSImage *) image menu: (NSMenu *) menu {
self = [super init];
if (self) {
self.image = image;
self.menu = menu;
}
return self;
}
- (void) setImage: (NSImage *) image {
_image = image;
self.statusItem.button.image = image;
}
- (NSStatusBarButton *) statusItemView {
return self.statusItem.button;
}
- (void) showStatusItem {
if (!self.statusItem) {
self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
[self initStatusItem10];
}
}
- (void) hideStatusItem {
if (self.statusItem) {
[[NSStatusBar systemStatusBar] removeStatusItem:self.statusItem];
self.statusItem = nil;
}
}
- (void) initStatusItem10 {
self.statusItem.button.image = self.image;
self.statusItem.button.imageScaling = NSImageScaleAxesIndependently;
self.statusItem.button.appearsDisabled = NO;
self.statusItem.button.target = self;
self.statusItem.button.action = #selector(leftClick10:);
__unsafe_unretained MOSMainStatusBar *weakSelf = self;
[NSEvent addLocalMonitorForEventsMatchingMask:
(NSEventMaskRightMouseDown | NSEventModifierFlagOption | NSEventMaskLeftMouseDown) handler:^(NSEvent *incomingEvent) {
if (incomingEvent.type == NSEventTypeLeftMouseDown) {
weakSelf.statusItem.menu = nil;
}
if (incomingEvent.type == NSEventTypeRightMouseDown || [incomingEvent modifierFlags] & NSEventModifierFlagOption) {
weakSelf.statusItem.menu = weakSelf.menu;
}
return incomingEvent;
}];
}
- (void)leftClick10:(id)sender {
[self.delegate menuBarControllerStatusChanged:YES];
}
I'm using this code on Catalina. Rather than performClick like some of the other answers suggest, I had to manually position the popup. This works for me with external monitors.
func applicationDidFinishLaunching(_ aNotification: Notification) {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusItem.button?.action = #selector(onClick)
statusItem.button?.sendAction(on: [.leftMouseUp, .rightMouseUp])
menu = NSMenu()
menu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
menu.delegate = self
}
#objc func onClick(sender: NSStatusItem) {
let event = NSApp.currentEvent!
if event.type == NSEvent.EventType.rightMouseUp {
// right click, show quit menu
statusItem.menu = menu;
menu.popUp(positioning: nil,
at: NSPoint(x: 0, y: statusItem.statusBar!.thickness),
in: statusItem.button)
} else {
// main click
}
}
#objc func menuDidClose(_ menu: NSMenu) {
// remove menu when closed so we can override left click behavior
statusItem.menu = nil
}
Learning from the great answers already given in here I came up with an alternative.
The advantage of this approach is that you only set the NSMenu once and don't have to juggle with setting it or removing it anymore.
Setting up the NSStatusBarButton
let status = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
status.button!.image = NSImage(named: "status")
status.button!.target = self
status.button!.action = #selector(triggerStatus)
status.button!.menu = /* your NSMenu */
status.button!.sendAction(on: [.leftMouseUp, .rightMouseUp])
Receiving the action
#objc private func triggerStatus(_ button: NSStatusBarButton) {
guard let event = NSApp.currentEvent else { return }
switch event.type {
case .rightMouseUp:
NSMenu.popUpContextMenu(button.menu!, with: event, for: button)
case .leftMouseUp:
/* show your Popup or any other action */
default:
break
}
}

Swift: Try to use closure instead #selector but not working

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)

Actions assigned to NSMenuItem dont seem to work

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
}