Creating custom dropdown menus with apple script or JXA, but not with choose from list function

I also gave Keyboard Maestro a quick look, and these scripts seem to work OK on my system (Sonoma 14.7). If you are just pasting the script into an Execute an AppleScript action, another idea would be to try the Execute a Shell Script action and use osascript to run the script file from there. You could also try running from a new user account to see if there is a conflict with something else that has been installed.

Hello Jonas (@ionah) :wave:

Maybe you did not read carefully enough what I mentioned above…

Dialogs created via AppleScript and the Objective-C API‘s are not supported in Keyboard Maestro‘s Execute an AppleScript Action. It’s something that you will always have to write some code to call from a Macro when the Dialog related code is in its own file until Peter Lewis has integrated the support for such dialogs.

Another thing I would try is create a script object with the dialog related code that is compiled to the temp folder and called from the same AppleScript. This is less code to write because it’s only one Script instead of two … but it’s still the same way such dialogs have to be called from Keyboard Maestro.

If this doesn’t work on newer OS‘ses (speaking of the dialog is not rendered at all), then there might be a big issue related to osascript. On the particular OS.

But since I am still running Monterey this is only a guess from my side.

Greetings from Germany :de:

Tobias

Then why does it work here on Monterey 12.7.5?
Triggered by a “hot key” as they call it in KM…

It’s not the first time I encounter this kind of issue. Most of time it’s the application that’s not following macOS novelties. In these cases, a fix is possible. In the other case…

Maybe the dialog is rendered and showing … that’s not what I mean by isn’t supported… it’s the fact that Keyboard Maestro doesn’t execute the script the way like the System’s Script Menu or FastScripts or if you just Safe the script as a File elsewhere and call it from another Script where the Dialog is running on the main thread like it is supposed to be.

Compared to the older CocoaDialog or Pashua or the newer swiftDialog where this job is done by the CLI Tool or the Application which renders the dialog and puts it on the main thread automatically - with Keyboard Maestro you always have to ensure that this will happen thru the code you write and how it’s executed on your own.

Greetings from Germany :de:

Tobias

Hi @red_menace. I don’t understand how I missed your post.
The fact that it’s running on your Sonoma config is encouraging.
Your suggestions are very clever (as always) and @Lucky_Magician.should follow them.

Thank you for taking the time to install KM.

Hi, I’m new here but saw your earlier post that said:

In fact, if I can make a dialog with only one popup button, which will open immediately after running the script, without firstly clicking on it, then it could actually work for my case perfectly well.

There is a way to make a button automatically select after a certain amount of time with the property “giving up after”:
set userResponse to display dialog “Hello” buttons {“ok”, “cancel”} default button 1 giving up after 2

This pops up a dialog box with the text “hello” and puts up two buttons. Button 1 is automatically “clicked” after 2 seconds.

I hope that helps.

Hello Greg (@gxwalsh) :wave:

What you’re describing here is the optional feature of the standard AppleScript display dialog command.

The code above you’ll referred to is based on AppleScript with Apple‘s Objective-C API and therefor something like timeout of the Dialog would have to be written quite differently.

My Skills to write that aren’t quite on that level so I won’t post any code. Maybe any other person see‘s this and can show you the difference to your suggestion.

Greetings from Germany :de:

Tobias

Greetings from Baltimore!

Yes, I was just trying to answer OP’s request to “open immediately without first clicking on it”

I may have missed the OP’s actual intention.

Hello guys

I was on a trip, just arrived back home. I’m going to read all posts, and retry all examples. Maybe I’m missing something along the lines, don’t know.

I’ll report back how it goes, whether it works or not.
Thank you all for all suggestions and will to help, I greatly appreciate it.

I also came up with a variation that uses NSMenu (for submenus, etc), but I’m not that great at using VoiceOver to test it. Let me know if you want to take a look at it.

I want to try it absolutely.
If it would work then I would save lots of time, I planned to go through all examples you guys shared, got one more macbook with sonoma on it so planned to try it there as well.

Here is a variation that uses NSMenu instead of NSPopUpButton. It works more-or-less the same, but instead of the panel containing a control, it is just used to position a contextual menu (if you are using your own windows, they can be used).


use framework "Foundation"
use scripting additions

# UI item outlets
property mainWindow : missing value
property customMenu : missing value

# script properties
property outcome : missing value -- selection result
property failure : missing value -- error record with keys {handlerName, errorMessage, errorNumber}
property testing : true -- whether to provide feedback for the result being returned

on run -- example 
   set contextualMenu to {"Lorem Ipsum", "Donec Laoreet", {"Single Submenu", "", "Suspendisse Tempus", "Mauris Iaculis"}, "Quisque Convallis", {"Multiple Submenus", "", "Vivamus Consectetur", "Aenean Pulvinar", {"Aliquam Dignissim", "Try Not To Do This", {"No, Really, Don't Do This!"}}, "Nullam Sollicitudin"}, "Curabitur Mollis"}
   return (getSelection from contextualMenu)
end run

# Get a menu selection - performSelector doesn't return anything and actions are asynchronous, so properties are used for results.
to getSelection from menuList
   try
      if current application's NSThread's isMainThread() as boolean then
         my doStuff:menuList
      else -- UI stuff needs to be done on the main thread
         my performSelectorOnMainThread:"doStuff:" withObject:menuList waitUntilDone:true
      end if
      if failure is not missing value then error
      if testing then
         set output to item (((outcome is missing value) as integer) + 1) of {outcome, "missing value"}
         activate me
         -- display dialog output with title "Result" buttons {"OK"} giving up after 10
         say output
      end if
      return outcome
   on error errmess number errnum
      if failure is missing value then
         -- display alert "NSMenu Script Error " & errnum message errmess
         say "NSMenu Script Error," & errnum & "," & first paragraph of errmess
      else -- use keys from the failure record
         -- display alert "NSMenu Script Error " & failure's errorNumber message quoted form of failure's errorMessage & " from handler " & failure's handlerName
         say "NSMenu Script Error," & failure's errorNumber & "," & first paragraph of failure's errorMessage & " from handler " & failure's handlerName
      end if
      return missing value
   end try
end getSelection

to doStuff:(menuList as list) -- do the menu stuff
   try
      set my customMenu to makeMenu for menuList without usingTags
      
      # create a view for the menu
      set mainWindow to makeWindow at {} with panel and floats given contentSize:{0, 0}, styleMask:3
      mainWindow's makeKeyAndOrderFront:me -- show the panel
      
      # create a right-click mouse event positioned at the view with the location offset
      set uptime to current application's NSProcessInfo's processInfo's systemUptime
      set theEvent to current application's NSEvent's mouseEventWithType:(current application's NSEventTypeRightMouseDown) location:{0, 0} modifierFlags:0 timestamp:uptime windowNumber:(mainWindow's windowNumber) context:(missing value) eventNumber:1 clickCount:1 pressure:0.0
      current application's NSMenu's popUpContextMenu:customMenu withEvent:theEvent forView:(mainWindow's contentView)
      
      # mainWindow's performClose:(missing value) -- uncomment to dismiss the positioning panel
   on error errmess number errnum
      set my failure to {handlerName:"doStuff", errorMessage:errmess, errorNumber:errnum}
      log result
   end try
end doStuff:

# Make and return a NSWindow or NSPanel.
# Default styleMask includes a title, close and minimize buttons, and is not resizeable.
# If no origin is given the window will be centered.
to makeWindow at (origin as list) given contentSize:contentSize as list : {400, 200}, styleMask:styleMask as integer : 15, title:title as text : "", panel:panel as boolean : false, floats:floats as boolean : false, aShadow:aShadow as boolean : true, minimumSize:minimumSize as list : {}, maximumSize:maximumSize as list : {}, backgroundColor:backgroundColor : missing value
   tell current application to set theClass to item ((panel as integer) + 1) of {its NSWindow, its NSPanel}
   tell (theClass's alloc()'s initWithContentRect:{{0, 0}, contentSize} styleMask:styleMask backing:2 defer:true)
      if origin is {} then
         tell it to |center|()
      else
         its setFrameOrigin:origin
      end if
      if title is not "" then its setTitle:title
      if panel and floats then its setFloatingPanel:true
      its setHasShadow:aShadow
      if minimumSize is not {} then its setContentMinSize:minimumSize
      if maximumSize is not {} then its setContentMaxSize:maximumSize
      if backgroundColor is not missing value then its setBackgroundColor:backgroundColor
      its setAutorecalculatesKeyViewLoop:true -- include added items in the key loop
      return it
   end tell
end makeWindow

# Make and return a menu from a (possibly nested) list of menu item names.
to makeMenu for menuItems given title:title : "Menu", usingTags:usingTags : true, baseTag:baseTag : 1
   set theMenu to current application's NSMenu's alloc()'s initWithTitle:(title as text)
   addMenuList to theMenu given itemList:menuItems, usingTags:usingTags, baseTag:baseTag
   return theMenu
end makeMenu

# Add a list of menu items to a menu (recursive).
# The itemList can contain nested lists, with any given list of items being the submenu for the previous item.
# If using tags, the menu items (excluding separators) are tagged in the order they are created.
# Menu items use a common action handler and have no key equivalent by default, but individual
#  items can be changed as needed by using NSMenu's itemWithTitle: method to get desired items.
to addMenuList to theMenu given itemList:itemList : {}, previousItem:previousItem : missing value, usingTags:usingTags : true, baseTag:baseTag : 1
   repeat with anItem in (itemList as list)
      if (contents of anItem) is in {"", {}, missing value} then
         (theMenu's addItem:(current application's NSMenuItem's separatorItem()))
      else if (class of anItem) is list then -- submenu items
         if previousItem is not missing value then -- set menu for the submenu as needed
            if not (previousItem's hasSubmenu) as boolean then -- create submenu
               set submenu to (current application's NSMenu's alloc's initWithTitle:(previousItem's title))
               (previousItem's setSubmenu:submenu) -- for any following lists
            end if
            set baseTag to (addMenuList to submenu given itemList:anItem, previousItem:previousItem, usingTags:usingTags, baseTag:baseTag)
         end if
      else -- treat as a menu item title
         set menuItem to (theMenu's addItemWithTitle:(anItem as text) action:"menuAction:" keyEquivalent:"")
         (menuItem's setTarget:me) -- for autoenable
         set previousItem to menuItem -- potential submenu
         if usingTags then
            (menuItem's setTag:baseTag)
            # log "Tag " & baseTag & " is menuItem '" & anItem & "' of menu '" & theMenu's title & "'" -- for reference
         end if
         set baseTag to baseTag + 1
      end if
   end repeat
   return baseTag
end addMenuList

# Common menu action - can use sender's title or tag for comparisons.
on menuAction:sender
   try -- do something with the selected menu item - example just sets the outcome property to info about the menu item
      set {tagText, submenuText, parentText} to {", with no tag value set.", "", ", of the main menu "}
      if ((sender's tag) as integer) is not 0 then set tagText to ", with a tag set to " & sender's tag & "."
      if sender's hasSubmenu then set submenuText to ", (which is also a menu)"
      try
         set parentText to ", of menu '" & ((sender's parentItem's title) as text) & "'" -- immediate parent menu
      end try
      
      set my outcome to "Menu item '" & (sender's title as text) & "'" & submenuText & parentText -- & tagText
      -- whatever (remember that properties are being used for results)
      
   on error errmess number errnum
      set my failure to {handlerName:"menuAction", errorMessage:errmess, errorNumber:errnum}
   end try
end menuAction:

Ok, I tried this code too. In script editor it works, but not from keyboard maestro.
When I run it from keyboard maestro, something get openned, like application, and after few seconds I get missing value spoken by voiceOver.
In any case, menu doesn’t open as it certainly does from script editor.

I didn’t understand when you said if I use my own window. What did you mean?

The popup button is a control that needs to be added to a view, while NSMenu just needs to be given a view to use as a reference for positioning. If you happen to already be using a window, you can use that, otherwise the example creates a panel.

To try and isolate the script from the Keyboard Maestro runtime, have you tried running it using osascript in a shell script, or as an application?

Hello @Lucky_Magician

the NSMenu API is there to create Application Menus. Code based on this API is supposed to be used in Applications only.

If you want to build Windows or dialogs (based on NSWindow or NSAlert) and isolate the Code from KM you can use these in KM Macros.

To isolate it always make sure the code is saved as a Script or Script-Bundle File and use another Script from Keyboard Maestro to call the isolated code. Also remember that you ensure the code runs on the main thread.

Greetings from Germany :de:

Tobias

Here is my displayContextMenu-Function from my ContextMenu Script Library (inspired by Piyomaru):

image

set menuList to {"Menu Item 1", "Menu Item 2", "Menu item 3", "", "Menu item 4"}

-- show at mouse location
log my ContextMenu's displayContextMenu(menuList, "mouse")
-- show at center screen
log my ContextMenu's displayContextMenu(menuList, "center")
-- show at position
log my ContextMenu's displayContextMenu(menuList, {100, 100})

script ContextMenu
	use framework "Foundation"
	property parent : a reference to current application
	property menuResult : missing value
	
	on displayContextMenu(menuList, atPoint)
		if (parent's NSThread's isMainThread) then
			my showMenu:{menuList, atPoint}
		else
			my performSelectorOnMainThread:"showMenu:" withObject:({menuList, atPoint}) waitUntilDone:true
		end if
		return menuResult
	end displayContextMenu
	
	on showMenu:args
		set args to args as list
		set alist to item 1 of args
		set atPoint to item 2 of args
		set aMenu to parent's NSMenu's alloc()'s init()
		repeat with i in alist
			if contents of i ≠ "" then
				set aMenuItem to (parent's NSMenuItem's alloc()'s initWithTitle:i action:"actionHandler:" keyEquivalent:"")
			else
				set aMenuItem to (parent's NSMenuItem's separatorItem())
			end if
			(aMenuItem's setTarget:me)
			(aMenu's addItem:aMenuItem)
		end repeat
		if (class of atPoint) is text then
			if atPoint is "center" then
				set currWindow to parent's NSWindow's alloc()'s initWithContentRect:(parent's NSMakeRect(0, 0, 0, 0)) styleMask:0 backing:(parent's NSBackingStoreBuffered) defer:false
				set currView to currWindow's contentView
				currWindow's |center|()
				set currPoint to parent's NSPoint's NSMakePoint(-50, (((length of alist) * 16) / 2) - 50) -- calc center pos
				aMenu's popUpMenuPositioningItem:(missing value) atLocation:currPoint inView:(currView)
			else
				set currPoint to parent's NSEvent's mouseLocation()
				aMenu's popUpMenuPositioningItem:(missing value) atLocation:currPoint inView:(missing value)
			end if
		else if class of atPoint is list then
			set currPoint to parent's NSPoint's NSMakePoint(item 1 of atPoint, (parent's NSHeight(parent's NSScreen's screens's firstObject()'s frame)) - (item 2 of atPoint))
			aMenu's popUpMenuPositioningItem:(missing value) atLocation:currPoint inView:(missing value)
		end if
	end showMenu:
	on actionHandler:sender
		set menuResult to (title of sender as string)
	end actionHandler:
end script
1 Like

Even though designed for NSPopUpButton, using the popUpContextMenu: method seems to be a better choice as it is simpler and allows an initial main menu item selection (although the menu will reposition if other than the first item).

Something came to my memories this morning when working on a script for FileMaker Pro: if you run an AppleScriptObjC script from within FMP, you need to migrate cocoa terms to properties (wich is one of the best function from Script Debugger).
Maybe it’s the same for KM?

You should try this:

use framework "Foundation"
use framework "AppKit"
use scripting additions

-- classes, constants, and enums used
property NSPopUpButton : a reference to current application's NSPopUpButton
property NSThread : a reference to current application's NSThread
property NSPanel : a reference to current application's NSPanel

-- UI item outlets
property thePanel : missing value
property thePopup : missing value

-- script properties
property outcome : missing value
property fail : missing value

on run
	try
		-- make sure LPX is open
		if application "Logic Pro X" is not running then error "Logic Pro is not running" number -2700
		
		-- build and show the menu on main thread
		if NSThread's isMainThread() as boolean then
			displayMenu()
		else
			my performSelectorOnMainThread:"displayMenu" withObject:(missing value) waitUntilDone:true
		end if
		
		-- return result or display alert
		if fail is not missing value then error fail's theMess number fail's theNum
		return outcome
	on error theMess number theNum
		tell application "Logic Pro X" to display alert theMess message "Error " & theNum
	end try
end run

-- UI stuff needs to be done on the main thread
on displayMenu()
	try
		-- build the container window
		set thePanel to (NSPanel's alloc()'s initWithContentRect:{{0, 0}, {400, 200}} styleMask:15 backing:2 defer:true)
		tell thePanel
			its |center|()
			its setFloatingPanel:true
			its setAutorecalculatesKeyViewLoop:true -- include added items in the key loop
		end tell
		
		-- get the list of effects in Logic Pro
		set menuList to getEffects()
		set menuList to menuList
		if menuList = {} then error "Menu list is empty" number -2700 2700 is a generic Applescript error
		
		-- build the pop up menu
		set thePopup to (NSPopUpButton's alloc()'s initWithFrame:{{20, 20}, {0, 32}} pullsDown:true)
		tell thePopup
			its addItemsWithTitles:({""} & menuList) -- the menu needs a first empty item because it's a pull down 
			its setAction:"popupButtonAction:"
			set {theW, theH} to its |menu|()'s |size|() as list
			its setFrameSize:{theW, 0} -- fit the menu size to its content
			its setTarget:me
		end tell
		
		-- add the pop up menu to the window
		(thePanel's contentView()'s addSubview:thePopup)
		
		-- display the menu only by closing its container window 
		thePanel's setInitialFirstResponder:(thePanel's contentView())
		thePanel's makeKeyAndOrderFront:me
		thePopup's performClick:me
		thePanel's performClose:(missing value)
		
	on error errMess number errNum
		set my fail to {theMess:errMess, theNum:errNum}
	end try
end displayMenu

-- Perform an action when a menu item is chosen
on popupButtonAction:sender
	set selected to sender's titleOfSelectedItem as text
	if (sender's pullsDown as boolean) then -- for pull-down
		sender's setTitle:selected -- synchronizeTitleAndSelectedItem doesn't want to work
		sender's sizeToFit() -- sized according to the title
	end if
	set my outcome to selected
end popupButtonAction:

-- Get the list of effects by UI scripting 
on getEffects()
	activate application "Logic Pro X"
	tell application "System Events" to tell process "Logic Pro X"
		tell (window 1 whose title ends with "- Pistes")
			set theParent to a reference to (UI element 1 of UI element 1 of last group of list 1 of (group 1 whose description is "Inspecteur"))
			if not (exists theParent) then error "Parent UI element is missing" number -2700
			tell theParent
				set theEffects to {}
				set theCount to (count of groups)
				if theCount < 1 then error "No children found" number -2700
				repeat with theItem from 1 to theCount
					set counter to count of (value of (attribute "AXChildren" of group theItem))
					if counter = 3 then
						set beginning of theEffects to (description of group theItem) as string
					end if
				end repeat
			end tell
		end tell
	end tell
	
	return theEffects
end getEffects

Hello Jonas (@ionah)

No, it’s not trivial … you can use Properties or not within ASObjC Scripts in KM Macros … but I’ve spottet better and more stable and also quicker execution times when Properties in these Scripts were used.

Even though I don’t have Logic - based on the Code you provided which is based on the NSPanel API - it should normally work when saved to a File and called from an AppleScript from within a KM Macro without any issues, since it is simply some sort of floating Panel that your code creates.

This is what I can say about it …

Greetings from Germany :de:

Tobias

There shouldn’t be a need to use that particular shortcut. The standard is to use current application to refer to the AppleScript runtime/script runner, which is where the API stuff is declared, and is why ASObjC can be used in any script. The first shortcut was to use a property declared as that, the next was to use a property declared as the class, e.g. class "NSView", and the current favorite is to use a reference.

My test, which I haven’t heard anything back about, was to try to isolate the environment the script is being run in by using the osascript shell utility. That runs the script in its own instance and just returns text, so there shouldn’t be any issues with conflicting threads, terminology, or references.