8

I'm facing a weird situation. I've got an NSMenu with a submenu in it. The submenu's contents are populated programmatically. In my validateMenuItem: method, I can see all items being checked (the parent's items) as well as the subitems (once I click on a submenu), except for those in my auto-populated submenu.

Why is that? Am I doing something wrong? Any ideas on how to fix that?

jscs
  • 63,694
  • 13
  • 151
  • 195
Dr.Kameleon
  • 22,532
  • 20
  • 115
  • 223
  • And these menu items operate normally otherwise (i.e. when you select them they fire their action method)? – trojanfoe Mar 03 '13 at 10:27
  • @trojanfoe Just found the answer (pretty simple to be honest...) - I'll post it right now. – Dr.Kameleon Mar 03 '13 at 10:30
  • @trojanfoe All of the menu items' actions were implemented in the controller, while that one specific submenu's subitems' actions were not. Thus, the `validateMenuItem:` method was not to be called. (Frustrating... given that I was struggling with it 2 hours before posting it in SO (and then answering it myself after 10 minutes... lol)) – Dr.Kameleon Mar 03 '13 at 10:35
  • Just talking about a problem is often enough to allow you to solve it yourself. – trojanfoe Mar 03 '13 at 10:36
  • @trojanfoe True. Weird but true.. – Dr.Kameleon Mar 03 '13 at 10:36

3 Answers3

17

Here is the solution :

Cocoa looks for the validateMenuItem: method in the Class where the NSMenuItem's action selector is.

So, if your NSMenuItem's action selector (e.g. @selector(someSelector:)) is implemented in SomeClass, then make sure you have a validateMenuItem: method in SomeClass too, if you want to validate the corresponding menu items.

Dr.Kameleon
  • 22,532
  • 20
  • 115
  • 223
  • 2
    Don't you target the *First Responder* with your action? – trojanfoe Mar 03 '13 at 10:35
  • @trojanfoe Nope, I am not. All actions are linked to specific classes. Should I target the First Responder? – Dr.Kameleon Mar 03 '13 at 10:36
  • 1
    I guess it depends on the App, but in my Document Based App, I have all menus targeting *First Responder* I think. – trojanfoe Mar 03 '13 at 10:37
  • Well, First Responder is one of the things I've been tempted to use for quite a long time, but given (probably the control-freak part of my ... coding nature... lol) that I want to know 100% what-goes-where and what-does-what I haven't tried it so far (and perhaps it's not the best decision ever). Btw, the (quite huge) project I'm currently working on **is** indeed a Document-Based app too (but not in the strict Cocoa (see : `NSDocument`) sense...), so your input could be of some help. Thanks, mate! :-) – Dr.Kameleon Mar 03 '13 at 10:39
  • 1
    Id urge you to try first responder :) It helps quite often – Daij-Djan Mar 03 '13 at 10:43
  • Well I believe the real value of using the *First Responder* is the context-sensitive nature of the menu items. You only get one set of menu items, so you want things like `Edit > Copy` to relate to whatever is selected in the current document. This concept can be applied to most menu items I think. – trojanfoe Mar 03 '13 at 10:44
  • @trojanfoe Well, the *Edit* menu is the one I'm most worried about. Since, guess what, my app **is** an editor (see: the one you'll most likely be using in a while ... lol) but not a very Cocoa Controls -compliant one either... So, actions - though seemingly the usual ones - don't correspond to their traditionally defined counterparts... – Dr.Kameleon Mar 03 '13 at 11:01
5

@Dr.Kameleon has the right answer.

I'll add one small point to update it if that's OK? My code broke recently in this area and stopped calling validateMenuItem: when it was working before. I didn't notice because the only symptom was a menu item no longer disabled when it should.

The issue was Swift 4. The method must be attributed with @objc. In earlier versions of Swift, inheriting from NSObject, NSViewController, etc. was enough but that changed with the newer versions of Swift, 4 and 5.

p.s. It seems it is fine to put the method in an extension.

Carl
  • 131
  • 2
  • 4
0

The above answers did not help me to solve my problem. I have created a separate project to understand when the validateMenuItem(:) method is called.

The validateMenuItem(:) method will be called only if:

  1. Conform to NSMenuItemValidation in the class which implements the NSPopUpButton.
  2. All NSMenuItems must have an action and target set to the object which implements the NSMenuItemValidation protocol.
  3. Implement the validateMenuItem(:) method.
  4. Implement the dummyAction(:) method for NSMenuItem which doesn't do anything.
  1. The NSPopUpButton "Items: Autoenables" autoenablesItems must be set

Version 11.5 (11E608c) , Swift 5.0, DP: macOS 10.15.

Code:

import Cocoa
// 1) Conform to NSMenuItemValidation in the class which implements the NSPopUpButton.

class NSMenuItemValidationTestViewController: NSViewController, NSMenuItemValidation {
@IBOutlet weak var popupButton: NSPopUpButton!

// MARK: - ViewController lifecycle

override func viewDidLoad() {
    super.viewDidLoad()
    // .target and .action are set programmatically because menus are mostly build programmatically.
    // 2) All NSMenuItems must have an action and target set to the object which implements the
    // NSMenuItemValidation protocol.
    self.popupButton?.menu?.items.forEach{( $0.target = self )}
    self.popupButton?.menu?.items.forEach{( $0.action = #selector(dummyAction(_:)) )}
}

// MARK: - NSMenuItemValidation
// 3) Implement the validateMenuItem(:) method.
func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
    print("Function: \(#function), line: \(#line)")
    return true
}

// 4) Implement the dummyAction(:) method for NSMenuItem which doesn't do anything
@IBAction func dummyAction(_ sender: NSMenuItem?) {
    print("Function: \(#function), line: \(#line)")    }


}

// 5) The NSPopUpButton "Items: Autoenables" checkbox must be set to true in storyboard.
// or
// self.popupButton?.menu?.autoenablesItems = true

TODO: Github link to source code. (Coming soon).

Darkwonder
  • 1,149
  • 1
  • 13
  • 26