简体   繁体   中英

validateMenuItem or menuWillOpen not called for NSMenu

My Mac app has an NSMenu whose delegate functions validateMenuItem and menuWillOpen are never called. So far none of the solutions online have helped.

It seems like I'm doing everything right:

  • The menu item's selectors are in the same class.
  • The class managing it inherits from NSMenuDelegate

I suppose the best way to describe my problem is to post the relevant code. Any help would be appreciated.

import Cocoa

class UIManager: NSObject, NSMenuDelegate {    
    var statusBarItem = NSStatusBar.system().statusItem(withLength: -2)
    var statusBarMenu = NSMenu()
    var titleMenuItem = NSMenuItem()
    var descriptionMenuItem = NSMenuItem()

    // ...

    override init() {            
        super.init()

        createStatusBarMenu()
    }

    // ...

    func createStatusBarMenu() {
        // Status bar icon
        guard let icon = NSImage(named: "iconFrame44")
            else { NSLog("Error setting status bar icon image."); return }
        icon.isTemplate = true
        statusBarItem.image = icon

        // Create Submenu items
        let viewOnRedditMenuItem = NSMenuItem(title: "View on Reddit...", action: #selector(viewOnRedditAction), keyEquivalent: "")
        let saveThisImageMenuItem = NSMenuItem(title: "Save This Image...", action: #selector(saveThisImageAction), keyEquivalent: "")

        // Add to title submenu
        let titleSubmenu = NSMenu(title: "")
        titleSubmenu.addItem(descriptionMenuItem)
        titleSubmenu.addItem(NSMenuItem.separator())
        titleSubmenu.addItem(viewOnRedditMenuItem)
        titleSubmenu.addItem(saveThisImageMenuItem)

        // Create main menu items
        titleMenuItem = NSMenuItem(title: "No Wallpaperer Image", action: nil, keyEquivalent: "")
        titleMenuItem.submenu = titleSubmenu
        getNewWallpaperMenuItem = NSMenuItem(title: "Update Now", action: #selector(getNewWallpaperAction), keyEquivalent: "")
        let preferencesMenuItem = NSMenuItem(title: "Preferences...", action: #selector(preferencesAction), keyEquivalent: "")
        let quitMenuItem = NSMenuItem(title: "Quit Wallpaperer", action: #selector(quitAction), keyEquivalent: "")

        // Add to main menu
        let statusBarMenu = NSMenu(title: "")
        statusBarMenu.addItem(titleMenuItem)
        statusBarMenu.addItem(NSMenuItem.separator())
        statusBarMenu.addItem(getNewWallpaperMenuItem)
        statusBarMenu.addItem(NSMenuItem.separator())
        statusBarMenu.addItem(preferencesMenuItem)
        statusBarMenu.addItem(quitMenuItem)

        statusBarItem.menu = statusBarMenu
    }

    // ...

    // Called whenever the menu is about to show. we use it to change the menu based on the current UI mode (offline/updating/etc)
    override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
        NSLog("Validating menu item")
        if (menuItem == getNewWallpaperMenuItem) {
            if wallpaperUpdater!.state == .Busy {
                DispatchQueue.main.async {
                    self.getNewWallpaperMenuItem.title = "Updating Wallpaper..."
                }
                return false
            } else if wallpaperUpdater!.state == .Offline {
                DispatchQueue.main.async {
                    self.getNewWallpaperMenuItem.title = "No Internet Connection"
                }
                return false
            } else {
                DispatchQueue.main.async {
                    self.preferencesViewController.updateNowButton.title = "Update Now"
                }
                return true
            }
        }

        return true
    }

    // Whenever the menu is opened, we update the submitted time
    func menuWillOpen(_ menu: NSMenu) {
        NSLog("Menu will open")
        if !noWallpapererImageMode {
            DispatchQueue.main.async {
                self.descriptionMenuItem.title = "Submitted \(self.dateSimplifier(self.updateManager!.thisPost.attributes.created_utc as Date)) by \(self.updateManager!.thisPost.attributes.author) to /r/\(self.updateManager!.thisPost.attributes.subreddit)"
            }
        }
    }

    // ...

    // MARK: User-initiated actions

    func viewOnRedditAction() {
        guard let url = URL(string: "http://www.reddit.com\(updateManager!.thisPost.permalink)")
            else { NSLog("Could not convert post permalink to URL."); return }
        NSWorkspace.shared().open(url)
    }

    // Present a save panel to let the user save the current wallpaper
    func saveThisImageAction() {
        DispatchQueue.main.async {
            let savePanel = NSSavePanel()
            savePanel.makeKeyAndOrderFront(self)

            savePanel.nameFieldStringValue = self.updateManager!.thisPost.id + ".png"
            let result = savePanel.runModal()

            if result == NSFileHandlingPanelOKButton {
                let exportedFileURL = savePanel.url!
                guard let lastImagePath = UserDefaults.standard.string(forKey: "lastImagePath")
                    else { NSLog("Error getting last post ID from persistent storage."); return }
                let imageData = try! Data(contentsOf: URL(fileURLWithPath: lastImagePath))
                if (try? imageData.write(to: exportedFileURL, options: [.atomic])) == nil {
                    NSLog("Error saving image to user-specified folder.")
                }
            }
        }
    }

    func getNewWallpaperAction() {
        updateManager!.refreshAndReschedule(userInitiated: true)
    }

    func preferencesAction() {
        preferencesWindow.makeKeyAndOrderFront(nil)
        NSApp.activateIgnoringOtherApps(true)
    }

    func quitAction() {
        NSApplication.shared().terminate(self)
    }
}

menuWillOpen: belongs to the NSMenuDelegate protocol; for it to be called the menu in question needs a delegate:

let statusBarMenu = NSMenu(title: "")
statusBarMenu.delegate = self

validateMenuItem: belongs to the NSMenuValidation informal protocol; for it to be called the relevant menu items must have a target . The following passage is taken from Apple's Application Menu and Pop-up List Programming Topics documentation:

When you use automatic menu enabling, NSMenu updates the status of every menu item whenever a user event occurs. To update the status of a menu item, NSMenu first determines the target of the item and then determines whether the target implements validateMenuItem: or validateUserInterfaceItem: (in that order).

let myMenuItem = NSMenuItem()
myMenuItem.target = self
myMenuItem.action = #selector(doSomething)

The above (accepted) answer states that a target must be set, is a bit misleading. It is not required to set a target. You can (for example) also make the first responder, without explicitly setting a target.

Details can be found in the appel documentation, which can be found here: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MenuList/Articles/EnablingMenuItems.html

There is one tricky part though when using swift:

if validateMenuItem does not get called, then make sure your class not only declares conformance to NSMenuDelegate, but also to NSMenuItemValidation.

class SomeClass: NSMenuDelegate, NSMenuItemValidation {
...
func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
   return true // or whatever, on whichever condition
}
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM