validateMenuItem или menuWillOpen не вызывается для NSMenu

В моем приложении Mac есть NSMenu, чьи функции делегата validateMenuItem и menuWillOpen никогда не вызываются. Пока ни одно из решений в сети не помогло.

Вроде все делаю правильно:

  • Селекторы пункта меню относятся к тому же классу.
  • Класс, управляющий им, наследуется от 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)
    }
}

person yesthisisjoe    schedule 19.07.2016    source источник
comment
Кажется, вы нигде в своем коде не устанавливаете делегат меню.   -  person Paul Patterson    schedule 19.07.2016
comment
Спасибо, это исправило проблему с menuWillOpen. Однако это не решает мою проблему с validateMenuItem.   -  person yesthisisjoe    schedule 19.07.2016


Ответы (2)


menuWillOpen: принадлежит протоколу NSMenuDelegate; для его вызова рассматриваемому меню нужен делегат:

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

validateMenuItem: относится к NSMenuValidation неофициальному протоколу; чтобы он вызывался, соответствующие элементы меню должны иметь target. Следующий отрывок взят из Меню приложений и всплывающих окон Apple. Документация -up List Programming Topics:

Когда вы используете автоматическое включение меню, NSMenu обновляет статус каждого пункта меню всякий раз, когда происходит пользовательское событие. Чтобы обновить статус пункта меню, NSMenu сначала определяет цель элемента, а затем определяет, реализует ли цель validateMenuItem: или validateUserInterfaceItem: (в указанном порядке).

let myMenuItem = NSMenuItem()
myMenuItem.target = self
myMenuItem.action = #selector(doSomething)
person Paul Patterson    schedule 19.07.2016
comment
Спасибо. В моем случае я добавил строку statusBarMenu.delegate = self и добавил myMenuItem.target = self после инициализации каждого пункта меню, у которого было действие. - person yesthisisjoe; 19.07.2016
comment
если элемент меню не имеет действия, не вызывается ли validatemenuitem? - person tofutim; 03.10.2017
comment
Объявите поддержку NSMenuItemValidation в вашей цели (например, класс MyTargetClass: NSObject, NSMenuItemValidation...). Многие проблемы связаны с тем, что swift сильно отличается от obj-c... он продолжает кусать меня. - person Klajd Deda; 01.10.2018

Приведенный выше (принятый) ответ гласит, что цель должна быть установлена, немного вводит в заблуждение. Не обязательно ставить цель. Вы можете (например) также сделать первого ответчика без явной установки цели.

Подробности можно найти в документации по апелляции, которую можно найти здесь: 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
}
}
person Johannes    schedule 06.03.2019
comment
Самое жалкое, почему я должен проверять пункт меню, который уже включен в раскадровке и который я всегда хотел включить? Это требуется только в том случае, если я хочу отключить какой-либо пункт меню. Apple и их психотические мании. - person Duck; 01.09.2019
comment
Что ж, validateMenuItem() очень удобна, когда вы хотите переименовать пункт меню на основе контекста первого респондента. - person Jay Koutavas; 06.12.2020