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