Centering front window using only AppleScript when you have multiple screens

Howdy folks, I’m hoping some AppleScript gurus can chime in here (link to original topic in Keyboard Maestro forum).

I’m trying to learn window positioning using AppleScript, particularly when there are multiple screens involved. My reason for this is to be able to streamline certain Keyboard Maestro macros by having a single AppleScript do everything, instead of breaking it up into multiple AppleScripts and Keyboard Maestro actions.

To give an example, the below Keyboard Maestro macro allows me to quickly add an auto-correction to one my Typinator sets. It’s designed to copy the misspelled word, open the Typinator “Create new item” window, select the appropriate set, and click the suggestions menu. It works flawlessly and I’ve been using it for many months. But recently I decided I wanted to center it in the front screen, and I wanted to do that from within the AppleScript so as not to have to break up the AppleScript to allow Keyboard Maestro to position it using the manipulate a window action.

So just below is the part of the script that does this for me.

--set Typinator window position
tell window winName
	set winSize to its size
	set winXsize to item 1 of winSize
	set winYsize to item 2 of winSize
	set its position to {(xRes - winXsize) / 2, (yRes - winYsize) / 2}
end tell

And below this is the entire AppleScript for context.

----------------------------------------------------------
# Author:			Chris Thomerson
# Version:			1.3
# Version History
#					1.3 (Added script to set Typinator window position)
#					1.2 (Added selecting English auto correction)
#					1.1 (Combined all previous scripts into this script)
#					1.0 (Initial script)
# Created:			Monday, September 6, 2021
# Modified:			Friday, December 10, 2021
# macOS:			11.5.2 (Big Sur)
# I claim no responsibility nor guarantee compatibility
# As with any kind of custom scripts, these must be tested thoroughly on each person's device
# May be used and distributed freely
----------------------------------------------------------

--set AppleScript variables
set setName to "English auto correction"
set winName to "Create new Typinator Item"

--get screen variables from Keyboard Maestro
tell application "Keyboard Maestro Engine"
	set xRes to getvariable "Typinator_xRes"
	set yRes to getvariable "Typinator_yRes"
end tell

--create a new entry from clipboard
tell application "Typinator"
	create from selection
end tell

tell application "System Events"
	tell application process "Typinator"
		
		--wait until the Typinator window appears
		repeat until window winName exists
			delay 0.1
		end repeat
		
		--set Typinator window position
		tell window winName
			set winSize to its size
			set winXsize to item 1 of winSize
			set winYsize to item 2 of winSize
			set its position to {(xRes - winXsize) / 2, (yRes - winYsize) / 2}
		end tell
		
		--wait until the set menu appears
		repeat until pop up button 1 of window winName exists
			delay 0.1
		end repeat
		
		if value of pop up button 1 of window winName is setName then
			
			--do nothing if current set is already English auto correction
			
		else
			
			--click the set menu
			click pop up button 1 of window winName
			
			--wait until English auto correction is in the menu
			repeat until menu item setName of menu 1 of pop up button 1 of window winName exists
				delay 0.1
			end repeat
			
			--select the English auto correction menu item
			click menu item setName of menu 1 of pop up button 1 of window winName
			
		end if
		
		--pauses until suggestions button exists
		repeat until menu button 1 of group 1 of window winName exists
			delay 0.1
		end repeat
		
		--clicks the suggestion button
		click menu button 1 of group 1 of window winName
		
	end tell
end tell

tell application "Keyboard Maestro Engine"
	set value of variables whose name starts with "Typinator_" to "%Delete%"
end tell

And below this is the Keyboard Maestro macro itself so if you use Keyboard Maestro you can see how I’m setting the screen resolution variables referenced in the AppleScript.

This works just fine for me, reliably centering the window on whatever screen is frontmost. But what I would like to know is if there’s a better way of doing this that way I can become more efficient.

Any help is appreciated, thanks in advance!

-Chris

Typinator- Create expansion from selection (English).kmmacros (35 KB)
Keyboard Maestro Export

Hey Chris,

You know that Typinator is AppleScriptable – yes?

-ccs

Hey Chris, yes I do, part of my above script uses commands from Typinator’s library (as do a handful of other scripts I wrote for Typinator).

But I’m not aware of a way to set the Typinator window position except for using system events… and my question really has more to do with setting window locations using system events (or any other method that might be easier/more reliable) than it does with Typinator itself. I was just curious if the way I position the window is an efficient way of doing it or if there’s a better method haha.

You could, if you wish, use ASObjC to get the screen dimensions. I also included a method, using System Events, to determine which app is frontmost.

The window with index 1 in System Events, should always be the frontmost window.

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use framework "Foundation"

on run
	set {screenW, screenH} to my getScreenDimensions()
	set {windowX, windowY, windowW, windowH} to my getFrontWindowDimensions()
	set {centeredX, centeredY} to my centerFrontWindow(screenW, screenH, windowW, windowH)
end run

on getScreenDimensions() -- Using AppleScriptObjC command to get main screen's dimensions
	
	set {{screenX, screenY}, {screenW, screenH}} to current application's NSScreen's screens()'s firstObject()'s frame()
	
	return {screenW, screenH}
	
end getScreenDimensions

on getFrontWindowDimensions()
	
	tell application "System Events"
		set frontmostApp to first item of (processes whose frontmost is true)
		tell frontmostApp
			set {windowX, windowY, windowW, windowH} to value of attribute "AXFrame" of window 1
		end tell
	end tell
	
	return {windowX, windowY, windowW, windowH}
	
end getFrontWindowDimensions

on centerFrontWindow(screenW, screenH, windowW, windowH)
	
	set centerX to (screenW - windowW) / 2
	set centerY to (screenH - windowH) / 2
	
	tell application "System Events"
		set frontmostApp to first item of (processes whose frontmost is true)
		tell frontmostApp
			set position of window 1 to {centerX, centerY}
		end tell
	end tell
	
	return {centerX, centerY}
	
end centerFrontWindow
1 Like

Just incase you are interested there is also a way to get the screen dimensions for all of the screens using ASObjC as well.

on getScreens() -- Using AppleScriptObjC command to get each screen's dimensions
	
	set screenCount to current application's NSScreen's screens()'s |count|()
	set screenArray to current application's NSScreen's screens()
	
	set allScreenDimensions to {}
	repeat with i from 1 to screenCount
		set allScreenDimensions to allScreenDimensions & frame() of item i of screenArray
	end repeat
	
	return {screenCount, allScreenDimensions}
	
end getScreens

1 Like

Figured I’d share what we’re using which is slightly different. Perhaps you can glean something useful from it. We created an app to be able to open a set of Finder windows based on the task the user was performing that day. Then from there we use system_profiler SPDisplaysDataType to get info about what displays that user is connected to (laptop retina, iMac retina, widescreen non-retina, etc). Depending on which is their main display we set either specific coordinates or calculate them depending on the user role/display width and open that role’s Finder windows.

It has worked really well for us for a long time. Hope it may be useful.

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

on run
	set userName to short user name of (system info) as text
	set listOfFinderLayouts to {"Photographer", "Capture One Styles", "Capture One Preferences", "Scripting", "Digital Tech"}
	set finderLayout to (choose from list listOfFinderLayouts with prompt "Choose Finder layout you want to load." with title "Finder Layouts")
	
	--determine which monitors are connected and if main display is Retina or not; in our case Retina = laptop monitor; non-Retina means larger screen
	set {scale} to words of (do shell script "system_profiler SPDisplaysDataType | awk '/Main Display: Yes/{found=1} /Resolution/{scale=($5 == \"Retina\" ? 2 : 1)}  /^ {8}[^ ]+/{if(found) {exit}; scale=1} END{printf \"%d\\n\", scale}'")
	
	--if scale = "1" (non-Retina) then
	set {deskWidth, deskHeight} to words of (do shell script "system_profiler SPDisplaysDataType | awk '/Main Display: Yes/{found=1} /Resolution/{width=$2; height=$4}  /^ {8}[^ ]+/{if(found) {exit}; scale=1} END{printf \"%d %d\\n\", width, height}'")
	
	set wideDisplay to false
	if scale = "2" then --if Retina
		if deskWidth is "2880" then --then determine if it is retina laptop or retina iMac
			
			set deskWidth to deskWidth / 1.75
			set deskHeight to deskHeight / 1.75
			
		else
			set deskWidth to deskWidth / 2
			set deskHeight to deskHeight / 2
		end if
	else if scale = "1" then --non-retina wide monitor
		if deskWidth is "3440" and userName is "seanmurp" and finderLayout contains "Scripting" then
			set wideDisplay to true
		end if
	end if
	
	
	if wideDisplay is true then --set coordinates for each Finder window
		set topLeft to {930, 23, 1995, 731}
		set topRight to {1996, 23, 3137, 731}
		set bottomLeft to {930, 732, 1995, 1399}
		set bottomRight to {1996, 732, 3137, 1399}
	else
		set {paddingX, paddingY} to {deskWidth / 23, 25}
		
		if item 1 of finderLayout = "Digital Tech" then
			set {allWindowX, upperWindowY, lowerWindowY} to {(deskWidth - paddingX) / 2, (((deskHeight - paddingY) / 3) * 2), (deskHeight - paddingY) / 3}
		else
			set {allWindowX, upperWindowY, lowerWindowY} to {(deskWidth - paddingX) / 2, ((deskHeight - paddingY) / 2), (deskHeight - paddingY) / 2}
		end if
		
		set topLeft to {paddingX, paddingY, allWindowX, upperWindowY + paddingY}
		set topRight to {allWindowX, paddingY, paddingX + (allWindowX * 2) - paddingX, upperWindowY + paddingY}
		set bottomLeft to {paddingX, paddingY + upperWindowY, allWindowX, upperWindowY + lowerWindowY}
		set bottomRight to {allWindowX, paddingY + upperWindowY, paddingX + (allWindowX * 2) - paddingX, upperWindowY + lowerWindowY}
	end if
	
	------------------------------------------------------------------------------------------------
	--Define which folders to open and in which location
	------------------------------------------------------------------------------------------------
	--Server Apps (top left)
	----------------------------------------------------------------------
	if not (serverStatus = "Offline") then
		set folderPosixPath to POSIX path of ServerBopwarePath
		--open path in new Finder window and set view preferences
		my finderOpenListView(folderPosixPath, topLeft)
	end if
	
	--Local Work Docs Scripting (top right)
	----------------------------------------------------------------------
	set currentDate to do shell script "date '+%Y-%m-%d'"
	set folderHfsPath to "Macintosh HD:Users:" & userName & ":WorkDocs:Scripting:" & currentDate & ":_Editing"
	set folderHfsPath2 to "Macintosh HD:Users:" & userName & ":WorkDocs:Scripting:" & currentDate & ":Complete"
	--Create folder structure if it doesn't exist
	folderCreation(folderHfsPath)
	folderCreation(folderHfsPath2)
	
	set folderPosixPath to "/Users/" & userName & "/WorkDocs/Scripting/"
	tell application "System Events"
		if exists folder folderPosixPath then
			my finderOpenListView(folderPosixPath, topRight)
		end if
	end tell
	
	--Local Work Docs (bottom right)
	----------------------------------------------------------------------
	set folderPosixPath to POSIX path of localBopwareRepoPath & "Apps/"
	my finderOpenListView(folderPosixPath, bottomRight)
	
	--Local  Apps (bottom left)
	----------------------------------------------------------------------
	set folderPosixPath to POSIX path of localBopwarePath
	my finderOpenListView(folderPosixPath, bottomLeft)
	
end run

on finderOpenListView(folderLocation, windowLocation)
	set appfolder to (POSIX file folderLocation) as alias
	tell application "Finder"
		activate
		open folder appfolder
		set the bounds of the front Finder window to windowLocation
		set the the current view of front Finder window to list view
		set the the toolbar visible of front Finder window to true
		set the statusbar visible of front Finder window to true
	end tell
end finderOpenListView

on folderCreation(destinationPath)
	----------------------------------------------------------------------
	--Create folder structure if it doesn't exist
	----------------------------------------------------------------------
	tell application "Finder"
		if not (exists (destinationPath)) then
			--break down the path directory by directory
			set thisFolderString to destinationPath as string
			set AppleScript's text item delimiters to ":"
			set the list_items to every text item of the thisFolderString
			set AppleScript's text item delimiters to ""
			set the max_x to the count of the list_items
			set destinationPathList to {}
			repeat with a in list_items
				set thisItem to a as text
				set end of destinationPathList to thisItem & ":"
				set partialDestinationPath to destinationPathList as string
				tell application "Finder"
					if not (exists (partialDestinationPath)) then
						set directoryCount to count of destinationPathList
						set previousFolderLevel to items 1 thru ((directoryCount) - 1) of destinationPathList as string as alias
						make new folder at folder previousFolderLevel with properties {name:thisItem}
					end if
				end tell
			end repeat
		end if
	end tell
end folderCreation