06/04/13 20:57:55

"Is Text Selected?" in AppleScript and Keyboard Maestro

Text selections in Keyboard Maestro or AppleScript are really a tough thing. Ever since I started writing my Markdown library I needed some way to figure out if there was text selected, and act accordingly. Scripts like “wrap text in …” or the Link macro wouldn’t be possible if I don’t know whether some text was selected already.
In the past I’ve settled with the basic approach of checking whether a certain menu item was enabled. Most (Cocoa) apps disable the Cut menu item if there is no text selected.

This has the downside that international users won’t be able to use these macros. Their menu entry for “Cut” might read “Ausschneiden” or “Couper”. I decided that this is still the best approach at the time.

Now that I’m rewriting my Markdown library for version 2, I want to tackle this problem again. Previously I was in the assumption that pressing ⌘C was no universal shortcut for all languages.

I looked up ways to check for text selections. Writing a System Service in Cocoa would have been an option. Making a Service in Automator, too.

Now, how would you call this Service from within AppleScript or Keyboard Maestro? A simple answer is you don’t. Keyboard Maestro can’t click an app’s menu. (That’s the menu where you find Preferences and the Services entry.) Something like this doesn’t work1:

We’re not going to give up here! Let’s write a little AppleScript that clicks the menubar for us. It can’t be that hard, can it?

-- put here your code to find frontmost app
-- here’s an AppleScript solution
-- http://macscripter.net/viewtopic.php?id=24540

tell application "Keyboard Maestro Engine"
    set frontApp to make variable with properties {name:"MMDFrontApp"}
    set frontAppName to value of frontApp
end tell

tell application frontAppName to activate
delay 0.3 -- wait for app to be frontmost
tell application "System Events"
        click menu item "Get Selection" of ((process frontAppName)'s (menu bar 1)'s ¬
        (menu bar item frontAppName)'s (menu frontAppName)'s ¬
        (menu item "Services")'s (menu "Services"))
    on error errMsg
        display dialog errMsg
    end try
end tell

This code gets a variable MMDFrontApp from Keyboard Maestro and puts that app frontmost. It waits a little for things to catch up and then selects the apps’ menu, then it selects Services in that menu and then it selects “Get Selection” in that menu. Hor-ri-bly slow. No matter whether this code is compiled or doesn’t use variables (e.g. click menu item "Get Selection" of ((process TextEdit)’s (menu bar 1)'s), it will always take about 1 second to execute.
The second negative effect of this code is that it works most of the time. If everything is quick enough it works reliably. If things aren’t quick enough, well… it’s not working so great.
Adding delay’s would result in even slower code. No, thank you.

Then, yesterday, I was corrected by my assumption ⌘C wasn’t a universal, international, shortcut. It is! So hooray, right? Put the current clipboard contents away somewhere, set the clipboard contents to something predefined, ⌘C, and then check whether the clipboard is still set to whatever it was set to.

Or as AppleScript:

set currentClipboard to the clipboard
set the clipboard to ""

tell application "System Events" to keystroke "c" using {command down}

if the clipboard as string is "" then
    return "there was no text selected"
    return "there was text selected"
end if

set the clipboard to currentClipboard

I could have been just happy here, but I wasn’t. Quickly after I wrote this I realized why I didn’t like this approach about a year ago when I decided against it.
When this code executes and System Events presses ⌘C and there was no text selection the user will hear a short error beep, making them assume there was something wrong. From the script’s point of view though, there was just no selection.

The quick fix: in the short moment this script presses ⌘C, turn down the volume of all system errors and turn it back up when it finishes.

set currentVolume to alert volume of (get volume settings)
set volume alert volume 0

    tell application "System Events" to keystroke "c" using {command down}
on error errMsg
    return errMsg
end try

set volume alert volume currentVolume

Adding one little delay of 0.3 seconds makes sure that the system clipboard had enough time to put things on itself. The comparison works pretty well then. The only obvious downside2 is that other error sounds are also muted by this script.

This would be good enough, if only MacScripter wasn’t so helpful.

The following solution is fast and reliable. (as far as my first tests show)

set frontAppName to “TextEdit”

tell application "System Events"
    tell application process frontAppName
        set frontmost to true
        set selectionExists to (enabled of first menu item of (menu 1 of menu bar item 4 of menu bar 1) ¬
            where (value of attribute "AXMenuItemCmdChar" is "C") ¬
            and (value of attribute "AXMenuItemCmdModifiers" is 0))
    end tell
end tell

return selectionExists

No beeps with this version anymore. I was able to make this macro even faster, because the output is written directly to a variable. Working with the system clipboard (and its delays) is not required anymore. Now we can tell for sure that there was a text selection and press ⌘X.

  1. Peter, if you read this, that’s a feature request right there. 

  2. Another not so apparent problem is that ⌘C works on everything that is selected, not just text. If you are in Preview and have a rectangular selection there, this script assumes there’s a super snazzy text selection.