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

Hello everyone

For a long time, I was looking for a way to create custom dropdown or popup menu through apple script or jxa but couldn’t find answer to this.
I know about “choose from list” function, but that’s not what I need.
Basicly, I need it to work like contextual menu, on key press, this popup menu should open with some items, and that’s it.
Although it sounds simple I have no idea how to do it.

I know it’s not possible with actual apple script, I have to use App kit probably or something like that, but honestly I have no idea how.
I’d kindly ask you guys for some directions or examples whatever.

The reason why I need something like this is speciffic. Working on the accessibility enhancement tool for blind people, it’s all gui scripting and it’s for speciffic application, to enhance its accessibility with VoiceOver, apple’s screen reader.
The project is pretty much done and should be out soon, but taking one more opportunity to try to find the answer for this custom popup menu, as it’s great way to present list of some items for blind users in very efficient way, as first letter navigation can be used in menus, navigating with arrow keys etc.
I’m blind user myself, and the reason why “Choose from list” is not ideal solution is because it has some issues with VoiceOver’s focus, which makes it not ideal solution.

So once more, I’d greatly appreciate any kind of help with this guys, either you give me some examples or directions where to try to find the solution to this.

Thanks a lot

I would say in my own endeavors, I crossed paths with Late Night Softwares Dialog Toolkit Plus (Freeware | Late Night Software) that allowed for customization of drop downs and the like which might be helpful to you, personally it was more than I needed on my own project but still gave a swathe of customization that may be beneficial to you.

Hey Marc

Thanks for suggesting this toolkit. I just downloaded and checked provided examples.
This is cool, it may be really useful for some projects.
However, from what I can see, this is for customizing and creating advance dialogs, don’t think this could help me.
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.

Do you maybe know is this auto activation of popup possible with this toolkit or in some other way? I’ll certainly check this library more to see in what ways I can use it, but it would certainly save some time if you have some suggestion on this.

Once more thanks a lot. Your help is greatly appreciated.

Are you talking about a NSStatusItem in the main menu bar, or something like a floating widow you click for the menu?

5 years ago, I wrote this on my blog.
5 years.

http://piyocast.com/as/archives/8046

Hey Piyomaru

This is not what I’m looking for.
When I ran this example, I got a dialog with scroll area, with popUp buttons. I could modify this to get my list of items to only one popUp button, awesome thing and can be useful in some of my future scripts, but still it’s not what I need.
I need just contextual menu, which will immediately open and show me the list of items, then do something based on what item is selected. Second part I’m sure I can do somehow, but I don’t know how to create a contextual menu.
If I use your example, I still have to navigate to that popUp button and click on it to get the list of items.

There are awesome examples of apple scripts on your blog, I bookmarked it to studdy those examples. Some of it can be useful for my needs.

Thanks

Not that.
Right click contextual menu is something I want. I use keyboard maestro to run all these scripts, so when I press a key command, one or more scripts run with speech feedback or whatever is necessary.

To give better idea, the app all scripts run for is logic pro x, which is digital audio workstation. So, let say I want to grab list of all effects on one track, that’s easy part. Now I want to show list of all these effects to user who obviously uses screen reader. User will then select the effect, hit return and the effect window will open. I can do this with choose from list command, but that command works fine in script editor, but when I’m focused in logic pro, screen reader focus gets messed up so I have to firstly navigate to the end of choose from list dialog then navigate back to the list of items, where I can then use first letter navigation to select desired item and hit return on it.
So contextual menu, one which immediately opens as right click mene is perfect for these needs, as screen reader focus will stay on that openned menu. Hope it’s clearer now what I really need.
English is not my native language so I sometimes get stuck when explaining stuff :P. Hope it’s clear now.

You’d better to use Xcode to make what you want.

I have a sample Xcode AppleScript project written in 2012. 12 years ago.

スクリーンショット 2024-09-16 18.27.09

I’ll use whatever necessary to make this. Did you mean you have an example of context menu in xCode project? If yes is it possible to get that project? Hope it will open in xcode 15.4. No idea what’s the backwards compatibility with xcode versions.

This AppleScript project can build with Xcode 15.2. Maybe also with Xcode 15.4.

Enjoy!

http://piyocast.com/as/wp-content/uploads/2024/09/ContextualMenu.zip

Hey thanks a lot.
The projects opens fine in xcode 15.4.

Now honestly, I don’t quite understand this. I see 3 handlers but no idea how to use them.

aNotification and sender parameters confuze me as they are not used inside the handler.
I mean, it’s possible it’s only because of my lack of knowledge of something, or apple script itself. In this project I’m working on, I wrote close to 300 scripts but all using JXA, and all use script library which I also wrote in jxa.

May I kindly ask you to explain this script to me, I’d like to understand what it actually does?
Also why you said that I should use xcode for this? Isn’t something like this possible in script editor or script debugger? I don’t understand what’s the reason.

What I should set to aTV property as value?
I kind of get what applicationWillFinishLaunching handler does, but I guess property aTV should be set to some value to make it work.
Am I at least going in right direction?

Hi @Lucky_Magician

Try this script from Script Debugger and let me know if it can help.
It should display a unique window with only a pop-up menu, an OK button, a second button and a cancel button. The window title, the button names and the popup menus are customizable.
When the window displays, the pop-up menu is open (just as if you clicked on it).

use framework "Foundation"
use framework "AppKit"
use scripting additions
-- ------------------------------------------------------------------ 
property theWindow : missing value
property theCancel : missing value
property theSecond : missing value
property theOK : missing value
property thePopup : missing value
property theField : missing value
-- ------------------------------------------------------------------ 
property globalResult : missing value
property exitCode : missing value
-- ------------------------------------------------------------------ 

-- buttons titles
set windowTitle to "Pop-up Menu"
set okButton to "OK"
set secondButton to "Second Button"
set cancelButton to "Cancel"

-- field content when no menu has been selected
set emptyChoice to "Empty Choice"

-- menu items
set menuList to {"CacheDelete", "Caches", "CardKit", "Classroom", "Colors", "ColorSync", "Components", "Compositions", "ConfigurationProfiles", "CoreAccessories", "CoreImage", "CoreServices", "CryptoTokenKit", "DefaultsConfigurations", "Desktop Pictures"}


-- ------------------------------------------------------------------ 
my performSelectorOnMainThread:"createWindow:" withObject:{menuList, emptyChoice, windowTitle, okButton, secondButton, cancelButton} waitUntilDone:true
-- ------------------------------------------------------------------ 


-- click on main button
if my exitCode = 1 then
	say "your choice is: " & (my globalResult)
	return (my globalResult)
end if

-- click on second button
if my exitCode = 2 then
	say "you have pressed the second button"
end if


-- ------------------------------------------------------------------ 
on createWindow:theArgs ## build the window and its content 
	set {menuList, choosenMenu, winTitle, okButton, secondButton, cancelButton} to theArgs as list
	set {winW, winH, winHI, winVI, butW, butH} to {560, 120, 20, 10, 120, 32}
	
	-- build the Main Field
	set theField to current application's NSTextField's alloc()'s initWithFrame:(current application's NSMakeRect(winHI, (winH / 2) - 20, (winW - butW - (winHI * 2)), 40))
	tell theField
		its setPlaceholderString:choosenMenu
		its setFont:(current application's NSFont's labelFontOfSize:32)
		its setTextColor:(current application's NSColor's controlAccentColor())
		its setEditable:false
		its setBordered:false
		its setDrawsBackground:false
		its (cell()'s setWraps:false)
		its (cell()'s setLineBreakMode:5) -- NSLineBreakByTruncatingMiddle
		its setFocusRingType:1
	end tell
	
	-- build the Popup button
	set anArray to current application's NSArray's arrayWithArray:menuList
	set thePopup to current application's NSPopUpButton's alloc()'s initWithFrame:(current application's NSMakeRect(winHI, (winH / 2) - 20, (winW - butW - (winHI * 2)), 40)) pullsDown:false
	tell thePopup
		its setTarget:me
		its setAction:("buttonClicked:")
		its setAutoenablesItems:false
		its setTransparent:true
		
		-- check if choosen menu is in meniList
		set theSetA to current application's NSSet's setWithArray:{choosenMenu}
		set theSetB to current application's NSSet's setWithArray:anArray
		if ((theSetA's isSubsetOfSet:theSetB) as boolean) then
			theField's setStringValue:choosenMenu
		else
			theField's setPlaceholderString:choosenMenu
		end if
		
		-- build the menu
		repeat with anEntry in anArray -- menu construction reworked to allow menu separators
			if (anEntry's hasSuffix:"-***") then
				(thePopup's |menu|()'s addItem:(current application's NSMenuItem's separatorItem()))
				set anEntry to (anEntry's substringToIndex:((anEntry's |length|()) - 4))
				(thePopup's addItemWithTitle:anEntry)
				((thePopup's itemWithTitle:anEntry)'s setEnabled:false)
			else if anEntry as text = "" then
				(thePopup's |menu|()'s addItem:(current application's NSMenuItem's separatorItem()))
			else
				(thePopup's addItemWithTitle:anEntry)
				((thePopup's itemWithTitle:anEntry)'s setEnabled:true)
			end if
		end repeat
		thePopup's selectItemWithTitle:choosenMenu
		
	end tell
	
	-- build the Cancel button
	set theCancel to current application's NSButton's buttonWithTitle:cancelButton target:me action:"buttonClicked:"
	tell theCancel
		its setFrame:{{winW - winHI - butW, winVI}, {butW, butH}}
		its setKeyEquivalent:(character id 27)
		its setEnabled:true
		its setTag:0
	end tell
	
	-- build the Other button
	set theSecond to current application's NSButton's buttonWithTitle:secondButton target:me action:"buttonClicked:"
	tell theSecond
		its setFrame:{{winW - winHI - butW, winVI + 29}, {butW, butH}}
		its setKeyEquivalent:tab
		its setEnabled:false
		its setTag:2
	end tell
	
	-- build the OK button
	set theOK to current application's NSButton's buttonWithTitle:okButton target:me action:"buttonClicked:"
	tell theOK
		its setFrame:{{winW - winHI - butW, winVI + 58}, {butW, butH}}
		its setKeyEquivalent:return
		its setEnabled:false
		its setTag:1
	end tell
	
	-- make the container view
	set containerView to current application's NSView's alloc()'s initWithFrame:(current application's NSMakeRect(0, 0, winW, winH))
	containerView's setSubviews:{theField, thePopup, theCancel, theOK, theSecond}
	
	-- make the window
	set theWindow to current application's NSWindow's new()
	tell theWindow
		its setDelegate:me
		its setTitlebarAppearsTransparent:true
		its setHidesOnDeactivate:false
		its setTitle:winTitle
		its setStyleMask:(1 + 2 + 16 + 128 + 32768) -- NSWindowStyleMaskBorderless = 0 ; NSWindowStyleMaskClosable = 2 ; NSWindowStyleMaskUtilityWindow =16 ; NSWindowStyleMaskNonactivatingPanel = 128 ; NSWindowStyleMaskFullSizeContentView = 32768 ; 
		its setContentView:(containerView)
		its setContentSize:{winW, winH}
		its setMovableByWindowBackground:true
		its setLevel:8
		its |center|()
	end tell
	
	-- display the window
	my performSelectorOnMainThread:"windowDisplay:" withObject:theWindow waitUntilDone:true
	delay 0.01 -- force the window to display before the next step in parent script
end createWindow:
-- ------------------------------------------------------------------ 
on windowDisplay:theWindow ## display modal window returning the button's tag
	theWindow's makeKeyAndOrderFront:me
	thePopup's performClick:me
	set my exitCode to current application's NSApp's runModalForWindow:theWindow
	theWindow's orderOut:me
end windowDisplay:
-- ------------------------------------------------------------------ 
on windowClose:theButton ## manage closing mode according to clicked button
	set my exitCode to (theButton's tag())
	(current application's NSApp)'s stopModalWithCode:(theButton's tag())
end windowClose:
-- ------------------------------------------------------------------ 
on windowWillClose:notif ## manage click on titlebar close button (do not rename this delegate method)
	set choosenMenu to theField's stringValue()
	set my globalResult to choosenMenu
	set my exitCode to 0
	(current application's NSApp)'s stopModalWithCode:0
end windowWillClose:
-- ------------------------------------------------------------------ 
on buttonClicked:sender ## retreive results according to clicked button
	if sender's isEqual:theOK then
		set my globalResult to (thePopup's titleOfSelectedItem()) as string
		my windowClose:sender
	else if sender's isEqual:theSecond then
		set my globalResult to (thePopup's titleOfSelectedItem()) as string
		my windowClose:sender
	else if sender's isEqual:theCancel then
		set my globalResult to missing value
		my windowClose:sender
	else if sender's isEqual:thePopup then
		theField's setStringValue:(thePopup's titleOfSelectedItem())
		theOK's setEnabled:true
		theSecond's setEnabled:true
	end if
end buttonClicked:
-- ------------------------------------------------------------------ 

Hey Iomah

This is almost perfect solution.
Yes when it runs, the menu is immediately openned, and screen readers works as in normal menu which is exactly what I wanted to achieve.
However, what I also need is, if I press return on selected item, the menu closes and some code run, but on escape key the menu closes and nothing else happens.
Yes, both return and escape keys close the menu, but the dialog is still openned and I have to click on Cancel or OK button to proceed.
I guess, as it’s possible to have menu openned on display dialog, to close the dialog when menu closes.
That would be the perfect solution for me.

Gonna studdy this example a bit more as not everything is clear to me.
If you have example or idea how to close dialog automaticly when popup menu closes I’d be really grateful to see that.
I’m already grateful so much for this example. Thanks a lot. It’s almost there.

I deeply studdy the example you sent me.
I don’t understand all of this, or I’d rather say I kind of understand it but there are lots of things which I don’t know anything about.
I understood it enough to remove some stuff, or add some new buttons and additional pop-up menu and it all worked.

Now, I’m so close to getting this where I want but need that missing part.
I guess it would be something like on popUpClose or whatever, but the only missing part is, on popup close, close the window and return the selected item as string, or emptyChoice if nothing is selected.

Can you somehow help me with this finishing part?

Also, this is all very interesting to me. Where I can learn more about this, creating windows, uiElements etc?

Thanks in advance

Not sure if it meets Apple’s interface guidelines, but you could probably get by with just the popup button in a NSPanel and set an action for it. The script would do the performClick to drop the menu and then immediately close the window, which would avoid needing any buttons. Clicking a menu item or pressing return will get the selection, escape or clicking outside the popup would cancel the panel.

If you are running Ventura or later, you might even try a NSComboButton, which is like a button but also has a pull-down menu. The main button could be a Cancel, and for a panel the escape also cancels; anything else would be a menu/button press, after which the panel will close.

For the various controls, Apple’s Developer Documentation would be the main source (AppKit, for example), you would just need to convert Objective-C to AppleScriptObjC, which isn’t too terrible since they have similar structures. I’m sure quite a few around here have their own libraries.

Hey, thanks a lot for the suggestions. Both sound interesting, first one probably a bit more.
I don’t care about apple’s interface guidelines too much with this. It’s gonna be used by blind people with screen reader, I’m blind myself and I also explained what really matters. It can look like anything but if it does the job I’ll be fine with it.

All this AppKit stuff is really new to me, trying hard to understand everything. Seems like lots of learning is ahead of me, I must make this, as it’s extremely efficient thing for screen reader users, comparing to navigating windows or whatever. I guess it doesn’t matter with mouse.

Anyway, once more thanks a lot.

Boy, first of all, I judged that you do not even understand the words that you write yourself. I feel like a pediatrician.

Boy, you write a “contextual menu”, but maybe you actually want to create a popup menu, or maybe you want to create a pull down menu. I decided that it probably refers to something else. They don’t have a lot of vocabulary to express it, and they’re probably using the wrong vocabulary for the wrong purpose.

But you write contextual menu. I’m in trouble.

Therefore, I presented a project (written in 2012) that accurately realizes contextual menus in the least labor-intensive way.

This is definitely what displays the contextual menu. You can build and run it with Command-R on Xcode. With the character entered in the text view selected, display the submenu with the mouse or trackpad. This is what is commonly referred to as a contextual menu.

There are many benefits to having an example in Xcode. First of all, since you only need to place the GUI components on Interface Builder as XIB (NIB), you can definitely generate GUI objects. In order to work with GUI objects in the Script Editor or Script Debugger, it is necessary to write the generation procedure in the program code, but this time can be saved.

After that, you can get closer to what you really want to make (although I’m not sure you can definitely describe it in words).

We look forward to your success.

Here is an example of what I was talking about, it uses a couple of UI handlers from my library. The panel size is set to zero so only the popup menu is shown, and a default item can be pre-selected if you want. The location won’t be very intuitive, as the menu adjusts around the button (which is in an invisible window), but it can be placed wherever. Clicking a menu item or pressing return will return the selected item, pressing escape or clicking outside the popup will exit and return missing value. To use with osascript, you might want to do something with standard error, as there will be an error that changes (outlet properties containing Objective-C pointers) couldn’t be saved to the script.

use framework "Foundation"
use scripting additions

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

# script properties
property outcome : missing value -- result
property fail : missing value -- error
property testing : true -- whether to speak the result being returned

on run -- example
   try
      if current application's NSThread's isMainThread() as boolean then
         doStuff()
      else
         my performSelectorOnMainThread:"doStuff" withObject:(missing value) waitUntilDone:true
      end if
      if fail is not missing value then error fail's errmess number fail's errnum
      if testing then if outcome is missing value then
         say "missing"
      else
         say outcome
      end if
      return outcome
   on error errmess number errnum
      display alert "Error " & errnum message errmess
   end try
end run

on doStuff() -- UI stuff needs to be done on the main thread
   try
      set mainWindow to makeWindow at {} with panel given contentSize:{0, 0} --, title:"This is a Test", styleMask:3
      set menuList to {"CacheDelete", "Caches", "CardKit", "Classroom", "Colors", "ColorSync", "Components", "Compositions", "ConfigurationProfiles", "CoreAccessories", "CoreImage", "CoreServices", "CryptoTokenKit", "DefaultsConfigurations", "Desktop Pictures"}
      set theDefault to "Colors"
      set my popupButton to makePopupButton at {20, 20} given itemList:menuList --, title:theDefault
      set subViewItems to {popupButton}
      
      repeat with aSubview in subViewItems -- add subviews in key view order
         (mainWindow's contentView()'s addSubview:aSubview)
      end repeat
      
      mainWindow's setInitialFirstResponder:(mainWindow's contentView) -- or whatever
      mainWindow's makeKeyAndOrderFront:me
      popupButton's performClick:me
      mainWindow's performClose:(missing value)
   on error errmess number errnum
      set my fail to {errmess:errmess, errnum:errnum}
   end try
end doStuff

# Make and return a NSWindow or NSPanel.
# Default styleMask includes 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 NSPopUpButton.
to makePopupButton at (origin as list) given maxWidth:maxWidth as integer : 0, itemList:itemList as list : {}, title:title as text : "", pullDown:pullDown as boolean : false, tag:tag as integer : 0, action:action as text : "popupButtonAction:", target:target : missing value
   if title is "missing value" then set title to ""
   if maxWidth < 0 then set maxWidth to 0 -- a maxWidth of 0 will size to fit the menu
   tell (current application's NSPopUpButton's alloc()'s initWithFrame:{origin, {maxWidth, 32}} pullsDown:pullDown)
      its addItemsWithTitles:itemList
      if pullDown then -- initial title
         its insertItemWithTitle:"" atIndex:0 -- add placeholder
         its setTitle:title
      else -- initial selection
         if title is not "" and title is not in itemList then set title to first item of itemList
      end if
      its selectItemWithTitle:title -- blank title (all items deselected) if empty
      if tag > 0 then its setTag:tag
      if action is not in {"", "missing value"} then
         if target is missing value then set target to me -- 'me' can't be used as an optional default
         its setTarget:target
         its setAction:action -- see the following action handler
      end if
      if maxWidth is 0 then -- sizeToFit works differently for pull-down (title vs menu), so do it manually
         set theSize to width of (its |menu|'s |size| as record)
         if pullDown then set theSize to theSize + 10 -- adjust for checkmark space
         its setFrameSize:{theSize, 25}
      end if
      return it
   end tell
end makePopupButton

# Perform an action when the connected popup button is pressed.
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
   -- whatever
end popupButtonAction:

Edited to add a testing property and say statements to indicate what the script will be trying to return, independent of what is running it.

I’m not mac programmer, I tried to explain what I need the best I can. I’m not sure of real differences between context menu or pop-up menu so I used maybe both terms.
All of these things about custom dialogs is totally new to me.
I’m really willing to learn new stuff, so on that way it’s common to blame yourself. Sorry if my questions were confuzing. I tried to articulate what I wanted the best I could.

I appreciate your help though, and thanks for the clerification on xcode.