NSMenu 未调用 validateMenuItem 或 menuWillOpen

Posted

技术标签:

【中文标题】NSMenu 未调用 validateMenuItem 或 menuWillOpen【英文标题】:validateMenuItem or menuWillOpen not called for NSMenu 【发布时间】:2016-11-22 12:29:58 【问题描述】:

我的 Mac 应用程序有一个 NSMenu,其委托函数 validateMenuItemmenuWillOpen 永远不会被调用。到目前为止,在线解决方案都没有帮助。

看来我做的一切都是对的:

菜单项的选择器属于同一类。 管理它的类继承自 NSMenuDelegate

我想描述我的问题的最佳方式是发布相关代码。任何帮助将不胜感激。

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 问题。不过,这并不能解决我的 validateMenuItem 问题。 【参考方案1】:

menuWillOpen:属于NSMenuDelegate协议;要让它被称为有问题的菜单需要一个委托:

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

validateMenuItem: 属于NSMenuValidation 非正式协议;要调用它,相关菜单items 必须有target。以下段落摘自 Apple 的 Application Menu and Pop-up List Programming Topics 文档:

当您使用自动菜单启用时,NSMenu 会在用户事件发生时更新每个菜单项的状态。为了更新菜单项的状态,NSMenu 首先确定项目的目标,然后确定目标是实现 validateMenuItem: 还是 validateUserInterfaceItem:(按此顺序)。

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

【讨论】:

谢谢。在我的例子中,我添加了行 statusBarMenu.delegate = self 并在初始化每个具有操作的菜单项之后添加了 myMenuItem.target = self 如果 menuitem 没有操作,是否不会调用 validatemenuitem? 在您的目标中声明对 NSMenuItemValidation 的支持(即:class MyTargetClass: NSObject, NSMenuItemValidation ...)。很多问题源于 swift 与 obj-c 完全不同的事实......它一直在咬我【参考方案2】:

上述(已接受)答案指出必须设置目标,这有点误导。不需要设定目标。您也可以(例如)做出第一响应者,而无需明确设置目标。

详细信息可以在appel文档中找到,可以在这里找到:https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MenuList/Articles/EnablingMenuItems.html

使用 swift 时有一个棘手的部分:

如果 validateMenuItem 没有被调用,那么请确保您的类不仅声明符合 NSMenuDelegate,而且声明符合 NSMenuItemValidation。

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


【讨论】:

最可悲的是为什么我必须验证已经在情节提要上启用并且我一直希望启用的菜单项?仅当我想禁用某些菜单项时才需要这样做。苹果和他们的精神病狂。 好吧,当您想根据第一响应者的上下文重命名菜单项时,validateMenuItem() 非常方便。

以上是关于NSMenu 未调用 validateMenuItem 或 menuWillOpen的主要内容,如果未能解决你的问题,请参考以下文章

拦截 NSMenu 键事件

在扩展坞保持活动状态时隐藏 NSMenu

如何使用延迟的 NSMenu 创建 NSButton?

NSTextfield + NSMenu 和第一响应者

上下文菜单不调用 NSMenuDelegate 方法

判断一个NSMenu是不是打开