Can a menubar applet detect whether its menu is open?

With the help of many generous experts, I’ve been writing an AppleScriptObjC applet that changes its behavior when the caps lock key has been pressed and caps lock is active. My applet’s menu also changes when the app detects that caps lock is active.

My question is this: If my applet’s menu is open when I press caps lock, is there any way that my app can detect that the menu is open, so that it can then change the menu and re-open it to reflect the new state of caps lock? I suspect that this is impossible, but I’m constantly being surprised by what expert coders can do with AppleScript. Thanks for any help.

1 Like

You can check when an ‘NSMenu’ is visible, using the NSMenu’s ‘menuBarVisible()’ method.
But I don’t believe there is a way of creating a keyboard monitor with AppleScript or AppleScriptObjC.
It can be done with ObjectiveC or Swift, using NSEvent’s ‘addLocalMonitorForEventsMatchingMask:handler:’ function.
But this function uses blocks in ObjectiveC or Swift, which I don’t think can be handled in AppleScript.
So the question isn’t wether you can query your NSMenu’s state at any time, because that can be done with AppleScript code.
So the real question for your problem, is how you create a keyboard event monitor in AppleScriptObjC.
And I can’t think of a way at the moment of achieving that in AppleScriptObjC.

Regards Mark

Why not put an on idle handler in the app?

You can do that, and query the NSMenu’s state in that on idle handler.
But that doesn’t solve the problem of knowing when the Caps Lock keyboard key has been pressed.

on modifierKeysPressed()
set |⌘| to current application
set currentModifiers to |⌘|'s class "NSEvent"'s modifierFlags() --> Same as the Python result, but integer rather than text.

set commandDown to (currentModifiers div (get |⌘|'s NSCommandKeyMask) mod 2 = 1)
set optionDown to (currentModifiers div (get |⌘|'s NSAlternateKeyMask) mod 2 = 1)
set controlDown to (currentModifiers div (get |⌘|'s NSShiftKeyMask) mod 2 = 1)
set shiftDown to (currentModifiers div (get |⌘|'s NSControlKeyMask) mod 2 = 1)

return {command_down:commandDown, option_down:optionDown, control_down:controlDown, shift_down:shiftDown}
end modifierKeysPressed

Hi - there’s already an on idle handler in the app, and it tests every second (or less) for whether the caps lock state has changed. See the comment that says “-- if caps lock state changed during previous interval” etc. What I’m trying to do is something like this:

if caps lock state has changed then
if menu is open then
close menu
create new menu reflecting changed caps lock state
end if
end if

I think you’ve shown me how to test whether the menu is open - I’ll look into this first thing tomorrow. Thank you!

You should be able to rebuild the menu based on if it changes. @Shane_Stanley may know how to do it without doing through on idle.

Thank you for that. I can already detect whether caps lock is on, and my menu shows different options if caps lock is on or off. What I’m trying to do is this:

If my menu is open when the user presses caps lock, I want the menu to change to show the new options. In my code, if the menu is open, it doesn’t change to show the new options until the user closes it and then opens it again. I’m hoping to make it change immediately after the user presses caps lock.

I think I need to use something like NSMenu’s cancelTracking() but I can’t find the right syntax for it.

I can’t see that being do-able in ASObjC.

@Shane_Stanley - Thank you for that! I won’t spend any time trying to solve it.

It only makes changes when the menu is about to be shown. What @emendelson wants is effectively notification of when a key has been pressed.

@emendelson
I’ve found a way of doing what you wanted, although it doesn’t look pretty.
Let me know if you’re still interested, or have given up on the idea now.

Regards Mark

@Mark_FX

I’m VERY much interested, and the version of the script that I got by e-mail from the site works brilliantly (I had to comment out the parentheses in the display alert messages for some reason that I don’t understand, but when I did that, it worked perfectly).

EDIT: The site sent me Mark_FX’s first version of his posting, which included a script. That script isn’t in the edited version of the post.

I’m deeply impressed by the skill and ingenuity that went into this. If you have a revised version of the script that I got in the mail, I hope you’ll consider posting it here. The technique seems to be useful for many other situations.

Thank you again!

@emendelson
You seem to have given up pretty quickly on this one, so I didn’t post the solution in the end.
Wasn’t it Batman who said " Everything’s impossible until somebody does it."

@mcsprodart came up with the right idea of using the ‘on idle’ handler, to get the modifier key flags.
But the constants he used have all been deprecated now.

@Fredrik71 was correct about the caps log flag number being 65536, but you can’t use that, because if other modifier keys are also pressed, then those modifier codes are included and change that number.
So you need to do the bitwise comparison on the modifier flag to get the correct code number.

The trick to knowing when the menu is open, required that you make the script the delegate 'NSMenuDelegate ’ of the menu, so you could implement the ‘menuWillOpen’ and ‘menuDidClose’ delegate methods, which tell you when the menu is open or closed.

The script needs to run the ‘on idle’ handler to work properly, so it won’t work when run in the Script Editor app, so you will have to copy and paste the code into a new Script Editor file, and save it as a stay open application or applet on your Desktop.
Then you can double click the applet to see it in action.

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

property myApp : a reference to current application

property statusBar : missing value
property statusItem : missing value
property statusItemMenu : missing value
property statusItemMenuOpen : false
property statusItemImage : missing value
property statusItemTitle : "Staus Item Title"

property doStuffMenuItem : missing value
property doThisMenuItem : missing value

on run {}
	-- Remove this Thread check code, once your stand alone App bundle is built and running. 
	if my NSThread's isMainThread() as boolean then
		my createStatusItem:statusItemTitle
	else
		my performSelectorOnMainThread:"createStatusItem:" withObject:statusItemTitle waitUntilDone:true
	end if
end run

on createStatusItem:title
	-- Get the systems Status Bar object
	set my statusBar to myApp's NSStatusBar's systemStatusBar()
	
	-- load a standard MacOS image, but you can load an image from the appBundle
	set my statusItemImage to myApp's NSImage's imageNamed:(myApp's NSImageNameAdvanced)
	
	set statusBarThickness to statusBar's thickness() -- Get thickness of the Status Bar
	
	-- Set the Image size to be 4 pixels less that the thickness of the Status Bar, and square.
	statusItemImage's setSize:(myApp's NSMakeSize((statusBarThickness - 4), (statusBarThickness - 4)))
	
	-- Create the Status Item with a title and image, for just an image, then set empty title string.
	set my statusItem to statusBar's statusItemWithLength:(myApp's NSVariableStatusItemLength)
	statusItem's button's setTitle:title
	statusItem's button's setImage:statusItemImage
	statusItem's button's setImagePosition:(myApp's NSImageLeft)
	
	my createMenuItems()
end createStatusItem:

on createMenuItems()
	set my statusItemMenu to myApp's NSMenu's alloc()'s initWithTitle:""
	set delegate of my statusItemMenu to me
	
	set my doStuffMenuItem to myApp's NSMenuItem's alloc()'s initWithTitle:"DoStuff" action:"doStuff" keyEquivalent:"d"
	set target of my doStuffMenuItem to me
	statusItemMenu's addItem:(my doStuffMenuItem)
	
	set my doThisMenuItem to myApp's NSMenuItem's alloc()'s initWithTitle:"DoThis" action:"doThis" keyEquivalent:"d"
	set target of my doThisMenuItem to me
	
	set seperatorMenuItem to myApp's NSMenuItem's separatorItem()
	statusItemMenu's addItem:seperatorMenuItem
	
	set quitMenuItem to myApp's NSMenuItem's alloc()'s initWithTitle:"Quit" action:"quitStatusItem" keyEquivalent:"q"
	quitMenuItem's setIndentationLevel:2
	quitMenuItem's setTarget:me
	statusItemMenu's addItem:quitMenuItem
	
	statusItem's setMenu:statusItemMenu
end createMenuItems

on doStuff()
	display alert "doStuff() method called"
end doStuff

on doThis()
	display alert "doThis() method called"
end doThis

on quitStatusItem()
	-- Remove this Thread check code, once your stand alone App bundle is built and running. 
	if my NSThread's isMainThread() as boolean then
		my removeStatusItem()
	else
		my performSelectorOnMainThread:"removeStatusItem" withObject:(missing value) waitUntilDone:true
	end if
	if name of myApp does not start with "Script" then
		tell me to quit
	end if
end quitStatusItem

on removeStatusItem()
	statusBar's removeStatusItem:statusItem
end removeStatusItem

-- Start NSMenuDelegate Functions
on menuWillOpen:sender
	set my statusItemMenuOpen to true
end menuWillOpen:

on menuDidClose:sender
	set my statusItemMenuOpen to false
end menuDidClose:
-- End NSMenuDelegate Functions

on idle {}
	if my statusItemMenuOpen then
		set currentModifierFlags to (myApp's NSEvent's modifierFlags())
		set capsLockKeyOn to (currentModifierFlags div (myApp's NSEventModifierFlagCapsLock as integer) mod 2 = 1) as boolean
		if capsLockKeyOn then
			if (my (statusItemMenu's itemAtIndex:0)) = (my doStuffMenuItem) then
				my (statusItemMenu's removeItem:(my doStuffMenuItem))
				my (statusItemMenu's insertItem:(my doThisMenuItem) atIndex:0)
			end if
		else
			if (my (statusItemMenu's itemAtIndex:0)) = (my doThisMenuItem) then
				my (statusItemMenu's removeItem:(my doThisMenuItem))
				my (statusItemMenu's insertItem:(my doStuffMenuItem) atIndex:0)
			end if
		end if
	else
		if (my (statusItemMenu's itemAtIndex:0)) = (my doThisMenuItem) then
			my (statusItemMenu's removeItem:(my doThisMenuItem))
			my (statusItemMenu's insertItem:(my doStuffMenuItem) atIndex:0)
		end if
	end if
	return 1.0
end idle

on quit {}
	continue quit
end quit

Regards Mark

@Mark_FX - I only gave up because Shane said it was unlikely to be do-able, and Shane is right about AppleScript more often than anyone else. So I’m doubly impressed to see a solution. Thank you for posting this. As I said in an earlier poet, Script Debugger refused to compile it with the parentheses/brackets (US: parentheses, UK: brackets) in the doStuff and doThis routines, but it works perfectly with those edited out.

I’ll try this evening to build this into my existing code. Thank you again.

You’re correct that Shane does know more about AppleScriptObjC than most of us.
But sometimes you just have to give a problem some thinking time, for an idea to pop into your head.
I suspect Shane was reacting on his immediate gut instinct on this occasion.
As I know he would have a good knowledge of delegate methods for different ‘NSObject’ sub classes.

Good Luck with it.

Regards Mark

@Mark_FX Batman! Excellent reference and solution!

@Mark_FX - I’ve been slowly reworking my code to match yours, and I hope I’m not being too annoying by asking one more question.

Is there any easy way to integrate into your code the ability to gray out (disable) a menu item? I see that I can do this by changing the target of a menu item to missing value, but that doesn’t seem like an intelligent way to do it, and I wonder if there’s a more correct one. I tried various ways of setting the item’s enabled to false, but couldn’t make it work.

Thank you again for this terrific code.

The parentheses/brackets problem is not a Script Debugger issue, the same thing happens in Script Editor on my macOS 12.0 Monterey disk.
The code I posted was created on my macOS 10.15 Catalina disk, and those parentheses/brackets didn’t cause any issues on that system.
So something has changed with AppleScript on the later macOS’s.

The correct way to enable or disable an ‘NSMenuItem’ is to set the the ‘isEnabled’ property too either true or false.
But if you use ‘isEnabled’ in AppleScript code, you get a runtime error, but I’ll show in a moment how to get around this problem.
Firstly in order to set the enabled status of the ‘NSMenuItem’, you have to set the ‘setAutoenablesItems:’ property of the parent ‘NSMenu’ too false.
Otherwise you can’t set the the enabled property of the child ‘NSMenuItem’.
So based on the code I posted above, I’ve added two lines into the ‘createMenuItems()’ method, to show how to setup the parent ‘NSMenu’, and how to disable the child ‘NSMenuItem’, a bit of pointless code in the context of my script, but shows how to achieve what you want.

on createMenuItems()
	set my statusItemMenu to myApp's NSMenu's alloc()'s initWithTitle:""
	set delegate of my statusItemMenu to me
	
	my (statusItemMenu's setAutoenablesItems:false) -- New Added Line of Code
	
	set my doStuffMenuItem to myApp's NSMenuItem's alloc()'s initWithTitle:"DoStuff" action:"doStuff" keyEquivalent:"d"
	set target of my doStuffMenuItem to me
	statusItemMenu's addItem:(my doStuffMenuItem)
	
	set my doThisMenuItem to myApp's NSMenuItem's alloc()'s initWithTitle:"DoThis" action:"doThis" keyEquivalent:"d"
	set target of my doThisMenuItem to me
	
	set seperatorMenuItem to myApp's NSMenuItem's separatorItem()
	statusItemMenu's addItem:seperatorMenuItem
	
	set quitMenuItem to myApp's NSMenuItem's alloc()'s initWithTitle:"Quit" action:"quitStatusItem" keyEquivalent:"q"
	quitMenuItem's setIndentationLevel:2
	quitMenuItem's setTarget:me
	statusItemMenu's addItem:quitMenuItem
	
	set enabled of quitMenuItem to false -- New Added Line of Code
	
	statusItem's setMenu:statusItemMenu
end createMenuItems

Please be aware that I’ve only tested the new lines on Monterey, you may get different results or problems on other macOS versions, and also if it works correctly, you will have to right click the applet’s Dock icon to quit the applet.
It definatly pays to let people no what version of macOS you’re on these days.
As you can occasionally get different results with AppleScript on different macOS versions.
I have this problem at the moment with Swift Xcode projects I started on OSX, that have issues with the UI code and appearance, along with changes to the system frameworks on the newer macOS’s.

Regards Mark

@Mark_FX - This is enormously generous and exactly what I needed to know. Thank you again and again!

And EDIT: I’m testing this on Ventura 13.1. I should have spelled this out earlier