validateMenuItem or menuWillOpen not called for NSMenu
Asked Answered
N

2

5

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)
    }
}
Nitrate answered 19/7, 2016 at 14:31 Comment(2)
You don't appear to set the menu's delegate anywhere in your code.Loidaloin
Thanks, that fixed the menuWillOpen problem. This doesn't fix my problem with validateMenuItem though.Nitrate
L
10

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)
Loidaloin answered 19/7, 2016 at 16:59 Comment(3)
Thanks. In my case I added the line statusBarMenu.delegate = self and added myMenuItem.target = self after the initialization of each menu item that had an action.Nitrate
if the menuitem does not have an action, does validatemenuitem not get called?Yellowknife
Declare support for NSMenuItemValidation, in your target (ie: class MyTargetClass: NSObject, NSMenuItemValidation ...). A lot of issues stem from the fact that swift is fairly different than obj-c ... it keeps biting meCallida
D
4

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
}
}
Divisible answered 6/3, 2019 at 15:32 Comment(3)
The most pathetic thing is why do I have to validate a menu item that is already enabled on storyboard and that I always wanted to be enabled? This should only be required if I wanted to disable some menu item. Apple and their psychotic manias.Predella
Well, validateMenuItem() is super handy when you want to rename a menu item based on the context of a first responder.Array
Another bit to why validateMenuItem may not be called is make sure that it is bound to correct IBAction method.Foodstuff

© 2022 - 2025 — McMap. All rights reserved.