EDIT (June 14, 2021). This thread contains numerous versions of my script but, IMO, the script in post 16 is by far the best. So, if anyone is interested in testing this script, I would suggest they skip to post 16, copy and paste that script into a script editor, and run the script. Its operation requires no real explanation.
This script displays a dialog in which the user can select an AppleScript dictionary to open. The available dictionaries consist of three default dictionaries–Finder, System Events, and Standard Additions–plus dictionaries for currently-running visible apps. Additional default dictionaries such as Safari are easily added, although a very few such as Python will not work.
I run this script by way of FastScripts, but it can also be saved and run as an application. This script is designed for use with Script Editor and assumes that Script Editor is running.
I received help writing this script in the following MacScripter thread:
set defaultApps to {"Script Editor", "StandardAdditions", "System Events"}
tell application "System Events"
set activeApps to name of every process whose visible is true and has scripting terminology is true
end tell
set appList to sortList(activeApps & defaultApps)
choose from list appList with title "AppleScript" with prompt "Select a dictionary to open:" default items {item 1 of appList} OK button name "Open"
if result = false then
error number -128
else
set selectedApp to item 1 of result
end if
if selectedApp = "StandardAdditions" then
set appFile to ((path to scripting additions folder as text) & "StandardAdditions.osax:") as alias
else
set appFile to path to application selectedApp
end if
tell application "Script Editor"
set allDictionaries to name of every document whose path ends with ".sdef"
if selectedApp is in allDictionaries then
focusWindow(selectedApp) of me
else if (selectedApp & ".sdef") is in allDictionaries then
focusWindow((selectedApp & ".sdef")) of me
else
activate
open appFile
end if
end tell
on focusWindow(selectedApp)
tell application "Script Editor" to activate
tell application "System Events" to tell process "Script Editor" to perform action "AXRaise" of window selectedApp
end focusWindow
on sortList(unsortedList) # obtained from developer.apple.com then modified
set sortedList to {}
set indexList to {}
set listCount to (count unsortedList)
repeat listCount times
set lowTextItem to missing value
repeat with i from 1 to listCount
if i is not in indexList then
set anItem to (item i of unsortedList)
if lowTextItem is missing value then
set lowTextItem to anItem
set lowIndexItem to i
else if anItem < lowTextItem then
set lowTextItem to anItem
set lowIndexItem to i
end if
end if
end repeat
if lowTextItem is not in sortedList then set end of sortedList to lowTextItem
set end of indexList to lowIndexItem
end repeat
return sortedList
end sortList
It’s very similar in concept to a script I’ve been using myself for the past twenty years or so! I’ve had to rewrite mine a few times as various things have either stopped working or been reinstated over the systems, but the basic idea hasn’t changed. For historical reasons, mine presents the Finder selected at the top of the choose dialog, with the other open scriptable apps sorted alphabetically below it. I have similar scripts for scripting additions and scriptable-but-not-necessarily-open applications in the CoreServices folder; but of course there’s now only one OSAX dictionary worth opening and the CoreServices script keeps opening the wrong dictionaries for reasons I’ve never been able to determine!
When you get round to adapting yours for multiple dictionaries, you should find the windows will cascade naturally.
Nigel. Thanks for looking at my script and for the compliment.
When I began work on this project, my script operated in a manner similar to one written by Daniel, the developer of FastScripts. I then happened upon your script and was inspired to take a similar approach, which seemed more useful overall. Anyways, I make note of this and provide a link to the thread containing your script in post 25 of the MacScripter thread linked in post 1 above.
I get an error on the ‘set bounds’ line at the bottom. The names of Script Editor’s dictionary windows end with “.sdef”, which isn’t allowed for in your handlers.
I think you’ll find that’s only if you have Show all filename extensions on in the Finder. (Actually, it probably depends on the state when SE was launched.)
it also supports my working hypothesis that the Finder is the root of all scripting evil ;).
More seriously, this is yet another good reason why it’s best not to refer to windows by name, unless the app in question uses some kind of persistent ID as a name. It’s not an issue confined to Script Editor or dictionaries.
The operation of this script is similar to that in post 1 above except that the user specifies dictionary names/paths in a text file. A few comments:
The first time the script is run, it creates two small text files in the user’s preferences folder.
The user should then enter in the data file the desired dictionary names/paths (see below).
In versions of macOS prior to Catalina, the path set in the script by editorApp will need to be changed. The script will not work properly otherwise.
-- Revised 2020.09.29
main()
on main()
set preferencePath to POSIX path of (path to preferences)
set dataFile to preferencePath & name of me & " Data.txt"
set settingFile to preferencePath & name of me & " Setting.txt"
set editorApp to "/System/Applications/Utilities/Script Editor.app"
set {defaultSelection, dictionaryNames, dictionaryPaths} to readFile(dataFile, settingFile)
set dialogList to {"Active App", "--"} & dictionaryNames & {"--", "Data File"}
choose from list dialogList default items defaultSelection OK button name "Open" with title "AppleScript Dictionaries"
set selectedApp to result as text
if selectedApp = "false" or selectedApp = "--" then
error number -128
else if selectedApp = "Active App" then
openActiveApp(editorApp)
else if selectedApp = "Data File" then
do shell script "open " & quoted form of dataFile
else
openSelectedApp(dictionaryNames, dictionaryPaths, editorApp, selectedApp)
end if
writeFile(settingFile, selectedApp)
end main
on readFile(dataFile, settingFile)
try
set defaultSelection to paragraph 1 of (read POSIX file settingFile)
on error
set defaultSelection to "Data File"
end try
set {dictionaryNames, dictionaryPaths} to {{}, {}}
try
set openedFile to paragraphs of (read POSIX file dataFile)
on error
writeFile(dataFile, linefeed & linefeed)
errorDialog("The dictionary data file could not be found and has been created.", "Please rerun this script and add dictionary names and paths to the data file.")
error number -128
end try
repeat with i from 1 to (count openedFile) by 2
if item i of openedFile > "" then
set end of dictionaryNames to item i of openedFile
set end of dictionaryPaths to item (i + 1) of openedFile
else
exit repeat
end if
end repeat
return {defaultSelection, dictionaryNames, dictionaryPaths}
end readFile
on openActiveApp(editorApp)
try
tell application "System Events" to set activeApp to POSIX path of (application file of (first process whose frontmost is true and has scripting terminology is true))
do shell script "open -a " & quoted form of editorApp & space & quoted form of activeApp
on error
tell application "System Events" to set activeApp to name of first process whose frontmost is true
errorDialog("Dictionary not found for " & quote & activeApp & quote, "This app may not be scriptable")
end try
end openActiveApp
on openSelectedApp(dictionaryNames, dictionaryPaths, editorApp, selectedApp)
repeat with i from 1 to (count dictionaryNames)
if item i of dictionaryNames = selectedApp then exit repeat
end repeat
try
do shell script "open -a " & quoted form of editorApp & space & quoted form of (item i of dictionaryPaths)
on error
errorDialog("Dictionary not found for " & quote & selectedApp & quote, "Check the path in the data file")
end try
end openSelectedApp
on writeFile(theFile, theText)
try
set openedFile to open for access (POSIX file theFile) with write permission
set eof of openedFile to 0
write theText to openedFile
close access openedFile
on error
try
close access openedFile
end try
end try
delay 0.5
end writeFile
on errorDialog(textOne, textTwo)
display alert textOne message textTwo buttons {"OK"} default button 1 as critical
end errorDialog
Just by way of example, the following are the contents of my data file. Many of the paths may not work on versions of macOS prior to Catalina and will need to be edited.
This script is a revision of my script in post 10. This revision was done only to incorporate ASObjC code.
use framework "AppKit"
use framework "Foundation"
use scripting additions
on main()
set preferencePath to POSIX path of (path to preferences)
set dataFile to preferencePath & "AppleScript Dictionary Data.txt"
set settingFile to preferencePath & "AppleScript Dictionary Setting.txt"
set activeApp to current application's NSWorkspace's sharedWorkspace()'s frontmostApplication()
set defaultSelection to readSettingFile(settingFile)
set {dictionaryNames, dictionaryPaths} to readDataFile(dataFile)
set dialogList to {"Active App", "--"} & dictionaryNames & {"--", "Data File"}
choose from list dialogList default items defaultSelection OK button name "Open" with title "AppleScript Dictionaries"
set selectedApp to result as text
if selectedApp = "false" or selectedApp = "--" then
error number -128
else if selectedApp = "Active App" then
openActiveApp(activeApp)
else if selectedApp = "Data File" then
set theWorkSpace to current application's NSWorkspace's sharedWorkspace()
set theFile to current application's |NSURL|'s fileURLWithPath:dataFile
theWorkSpace's openURL:theFile
else
openSelectedApp(dictionaryNames, dictionaryPaths, selectedApp)
end if
writeFile(settingFile, selectedApp)
delay 1
end main
on readSettingFile(theFile)
set theText to current application's NSString's stringWithContentsOfFile:(theFile) encoding:(current application's NSUTF8StringEncoding) |error|:(missing value)
if theText = missing value then
return "Data File"
else
return theText as text
end if
end readSettingFile
on readDataFile(theFile)
set theText to current application's NSString's stringWithContentsOfFile:(theFile) encoding:(current application's NSUTF8StringEncoding) |error|:(missing value)
if theText = missing value then
writeFile(theFile, linefeed & linefeed)
errorDialog("The dictionary data file could not be found and has been created.", "Please rerun this script and add dictionary names and paths to the data file.")
end if
set theArray to theText's componentsSeparatedByCharactersInSet:(current application's NSCharacterSet's characterSetWithCharactersInString:(linefeed))
set theList to theArray as list
set {dictionaryNames, dictionaryPaths} to {{}, {}}
repeat with i from 1 to (count theList) by 2
if item i of theList > "" then
set end of dictionaryNames to item i of theList
set end of dictionaryPaths to item (i + 1) of theList
else
exit repeat
end if
end repeat
return {dictionaryNames, dictionaryPaths}
end readDataFile
on openActiveApp(activeApp)
set theBundle to current application's NSBundle's bundleWithURL:(activeApp's bundleURL())
set dictionaryExists to (theBundle's objectForInfoDictionaryKey:"NSAppleScriptEnabled") -- thanks Shane
if dictionaryExists = missing value then
errorDialog("Dictionary not found", quote & (activeApp's localizedName as text) & quote & " may not be scriptable")
end if
set activeAppPath to (activeApp's valueForKeyPath:"bundleURL.path") as text
set theWorkSpace to current application's NSWorkspace's sharedWorkspace()
theWorkSpace's openFile:activeAppPath withApplication:"Script Editor"
end openActiveApp
on openSelectedApp(dictionaryNames, dictionaryPaths, selectedApp)
repeat with i from 1 to (count dictionaryNames)
if item i of dictionaryNames = selectedApp then exit repeat
end repeat
try
set theWorkSpace to current application's NSWorkspace's sharedWorkspace()
theWorkSpace's openFile:(item i of dictionaryPaths) withApplication:"Script Editor"
on error
errorDialog("Dictionary not found for " & quote & selectedApp & quote, "Check the path in the data file")
end try
end openSelectedApp
on writeFile(theFile, theString)
set theString to current application's NSString's stringWithString:theString
theString's writeToFile:theFile atomically:true encoding:(current application's NSUTF8StringEncoding) |error|:(missing value)
end writeFile
on errorDialog(textOne, textTwo)
display alert textOne message textTwo buttons {"OK"} default button 1 as critical
error number -128
end errorDialog
main()
When first using this script, the user has to place desired dictionary names and app paths in a data file. This is easily done, but the location of a few apps requires some research. So, I have included the following, which can be copied directly into the data file. These paths work under Catalina but may need to be changed with other versions of macOS.
How come this has to be done ? Not that it’s a bad idea at all, but your comment about “research” infers detective work over path to application … or to the Finder, which provides the application file class to access properties for applications registered with the system, including its path, and whether or not it’s scriptable.
CK. Thanks for looking at my script and for the suggestion.
The following is a simple script that logs whether an app has an AppleScript dictionary and copies the app’s path to the clipboard. The clipboard’s contents can then be pasted directly into the data file.
set theApp to text returned of (display dialog "Enter the name of an application:" default answer "")
if theApp = "" then error number -128
set thePath to POSIX path of (path to application theApp)
delay 0.5 -- try different values
tell application "System Events" to tell process theApp to has scripting terminology
set hasScriptingTerminology to result
set the clipboard to thePath
log theApp
log thePath
log hasScriptingTerminology
I revised my script to simplify it’s operation. I’ve tested it with many macOS apps without issue, but there will be a few where the script does not work as expected. Python is an example. Standard Additions is included in the script as a default dictionary, and, if deleted, it can only be added back by removing the script’s plist files.
-- Revised 2021.06.11
use framework "AppKit"
use framework "Foundation"
use scripting additions
on main()
set preferencesFolder to POSIX path of (path to preferences as text)
set settingPlist to preferencesFolder & "AppleScriptDictionarySetting.plist"
set dataPlist to preferencesFolder & "AppleScriptDictionaryData.plist"
set theWorkspace to current application's NSWorkspace's sharedWorkspace()
set dialogDefault to readPlist(settingPlist) as text
if dialogDefault = "missing value" then set dialogDefault to "Add a Dictionary"
set dictionaryNames to readPlist(dataPlist)
if dictionaryNames = missing value then
set dictionaryNames to {"Standard Additions"}
else
set dictionaryNames to (dictionaryNames's sortedArrayUsingSelector:"caseInsensitiveCompare:") as list
end if
set dialogList to dictionaryNames & {"--", "Add a Dictionary", "Delete a Dictionary"}
choose from list dialogList default items dialogDefault with title "AppleScript Dictionary"
set selectedItem to result as text
if selectedItem = "false" or selectedItem = "--" then
error number -128
else if selectedItem = "Add a Dictionary" then
set dictionaryNames to addDictionary(dictionaryNames, theWorkspace)
writePlist(dataPlist, dictionaryNames)
else if selectedItem = "Delete a Dictionary" then
set dictionaryNames to deleteDictionary(dictionaryNames)
writePlist(dataPlist, dictionaryNames)
else
openDictionary(selectedItem, theWorkspace)
end if
writePlist(settingPlist, {selectedItem})
end main
on openDictionary(theApp, theWorkspace)
set thePath to (theWorkspace's fullPathForApplication:theApp) as text
if thePath = "missing value" then set thePath to getThePath(theApp)
theWorkspace's openFile:thePath withApplication:"Script Editor"
delay 0.5
end openDictionary
on addDictionary(dictionaryNames, theWorkspace)
display dialog "Enter the name of the app:" default answer "" buttons {"Cancel", "OK"} cancel button 1 default button 2 with title "AppleScript Dictionary" with icon note
set theApp to text returned of result
if theApp = "" or theApp is in dictionaryNames then error number -128
set thePath to (theWorkspace's fullPathForApplication:theApp) as text
set theBundle to current application's NSBundle's bundleWithPath:(thePath)
if theBundle = missing value then errorDialog("An app named " & quote & theApp & quote & " was not found")
set dictionaryExists to (theBundle's objectForInfoDictionaryKey:"NSAppleScriptEnabled") -- thanks Shane
if dictionaryExists = missing value then errorDialog("A dictionary was not found for " & quote & theApp & quote)
set end of dictionaryNames to theApp
return dictionaryNames
end addDictionary
on deleteDictionary(dictionaryNames)
if dictionaryNames = {} then errorDialog("There are no dictionaries to delete")
set deleteNames to (choose from list dictionaryNames with title "Finder Bookmark" with prompt "Select dictionaries to delete:" default items {item 1 of dictionaryNames} with multiple selections allowed)
if deleteNames = false then error number -128
set dictionaryNames to current application's NSMutableArray's arrayWithArray:dictionaryNames
set deleteNames to current application's NSArray's arrayWithArray:deleteNames
dictionaryNames's removeObjectsInArray:deleteNames
return (dictionaryNames as list)
end deleteDictionary
on getThePath(theApp)
if theApp = "Standard Additions" then
return "/System/Library/ScriptingAdditions/StandardAdditions.osax"
else
errorDialog("A path was not found for the app " & quote & theApp & quote)
end if
end getThePath
on readPlist(thePath)
set theArray to current application's NSMutableArray's arrayWithContentsOfFile:thePath
return theArray
end readPlist
on writePlist(thePath, theList)
set theArray to current application's NSMutableArray's arrayWithArray:theList
theArray's writeToFile:thePath atomically:true
end writePlist
on errorDialog(dialogText)
display dialog dialogText buttons {"OK"} cancel button 1 default button 1 with title "AppleScript Dictionary" with icon stop
end errorDialog
main()