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.

To get a respond if capslock is on…

set theEvent to current application's NSEvent's modifierFlags()
if theEvent is 65536 then
	display alert "CapsLock is On"
end if

I first read this: Detecting the Caps Lock Key
But end up doing something different.

1 Like

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.

@Shane_Stanley
Wouldn’t it be possible to use willOpenMenu:withEvent: method to be able to load other NSMenu. Apple developers documentation say: It provides an opportunity to make any desired changes to the visual state.

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.

@Shane_Stanley
I find this… maybe its a dead water…
Key Modifiers monitor not called during contextual menu

@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!