Custom menu items with parameters?

Aa a contrived example of what I’m trying to do, this is something that works:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "AppKit"
use scripting additions

set toolsMenu to current application's NSMenu's alloc()'s initWithTitle:"Tools"

set ReturnValue to toolsMenu's addItemWithTitle:"Open Contacts" action:"openApp:" keyEquivalent:""
ReturnValue's setTarget:me
set ReturnValue to toolsMenu's addItemWithTitle:"Open Clock" action:"openApp:" keyEquivalent:""
ReturnValue's setTarget:me

set toolsMenuItem to current application's NSMenuItem's alloc()'s initWithTitle:"Tools" action:"" keyEquivalent:""
toolsMenuItem's setSubmenu:toolsMenu
set RootMenu to current application's NSApp's mainMenu()
RootMenu's addItem:toolsMenuItem

on openApp:sender
	
	set menuItemName to title of sender as string
	set appName to characters 6 thru (length of menuItemName) of menuItemName as string
	tell application appName to activate
	
end openApp:

This way I can have any number of Open App menus commands calling a generic openApp: handler.

But I’d rather do this by sending an appName parameter to the handler. In vanilla AppleScript, this does the trick:

its openApp:"Open Contacts" appName:"Contacts"

on openApp:sender appName:appName
	
	tell application appName to activate
	
end openApp:appName:

But I can’t figure out how to shoehorn appName into addItemWithTitle, if it’s even possible.

An action handler is only sent the object that is calling it (the sender). Each NSMenuItem has title, tag, and representedObject properties to use for identifying or comparing it, so one option would be to set representedObject to the name, id, or whatever, and use that instead of manipulating the title, for example:

use framework "Foundation"
use scripting additions

on run
   my performSelectorOnMainThread:"doStuff" withObject:(missing value) waitUntilDone:true
end run

on doStuff() -- UI stuff needs to be done on the main thread
   set toolsMenu to current application's NSMenu's alloc()'s initWithTitle:""
   repeat with appName in {"Contacts", "Clock"}
      tell (toolsMenu's addItemWithTitle:("Open " & appName) action:"openApp:" keyEquivalent:"")
         (its setTarget:me)
         (its setRepresentedObject:appName)
      end tell
   end repeat
   set toolsMenuItem to current application's NSMenuItem's alloc()'s initWithTitle:"Tools" action:"" keyEquivalent:""
   toolsMenuItem's setSubmenu:toolsMenu
   tell current application's NSApp's mainMenu() to addItem:toolsMenuItem
end doStuff

on openApp:sender
   set appName to sender's representedObject as text
   tell application appName to activate
end openApp:

Ah, so there’s a formal way to pass info to the handler, and multiple menu items can be created in a loop. Even better.

Is that (its setX:y) construction broadly applicable to other NSMenuItem properties, or do we only have access to a subset? I’m still a babe in the woods with ASObjC.

And I see that performSelectorOnMainThread allows the code to run in Script Debugger, so that makes life a bit easier. I’m learning, slowly.

(Probably also a good time to learn how to remove menu items . :stuck_out_tongue:)

Yeah, Script Debugger is a bit touchy and can get weird if you don’t do UI stuff like menus on the main thread. Don’t ask how I know.

In addition to the various class instance methods, properties can be set (if they are writable) using the setX:y setter construction. Note that the

set whatever of someObject to someValue

form may not work for all properties (depending on how they are bridged by AppleScriptObjC), and when it works the object may not appear to be updated to the new setting, while

someObject's setWhatever:someValue

works and is also important for Key-Value Observing (a part of Cocoa bindings), which is a mechanism that enables objects to be notified when a property changes.

Care should be taken if you are wanting to remove menu items that are not yours. In that case a better approach might be to use the hidden property, that way the menu item will still be there, it just won’t be visible.

Good point! But I was thinking more about cleaning up after myself when I’m done debugging a script in SD. Leaving the extra menu stuff hanging around (until I restart SD, anyway) would bug me.

Didn’t occur to me that I could do unnatural things to the built-in menus… :thinking:

Not to mention that every time I rerun the script in SD, it adds another Tools menu. :rofl: Probably should have some code to see if the menu already exists, delete it if it does, and then re-add it.