Script: Window Switcher (across all spaces)

This script directly switches to a DEVONthink window - even if DEVONthink is not the active app and even if the window is on another space.

All limitations of the previous script are solved, every window can be accessed reliably.

Background:

The previous script was a hack as in general it’s not possible to switch to a window that’s not on the current space via AppleScript, BetterTouchTool, Keyboard Maestro etc.

I’ve tried and searched for a solution over and over just to find again that it is not possible. However I’m not the only one, @jasondm007 is searching too. Although I already knew that it’s not possible I couldn’t resist and tried again when he asked here how to do it. Well, turned out it is possible, at least with DEVONthink.

Although the hacky solution is interesting it later turned out that there’s no need to use it in this script. DEVONthink’s “Window” menu and some UI scripting is all that’s needed. No idea why I’ve never tried that before.

Limitations:

  • The previous script’s problems (see edit history of this post) are all solved.

Features:

  • Windows of the current space are sorted to the top (if all windows have unique names).

  • Long window names can be trimmed with property trimNameTo.

    • If a window has a content record then the suffix is not trimmed.

    • If trimNameTo > 0 then this part in e.g. PDF names [21.0 x 29.7 cm, 3 Seiten, 100%] is always cut before the actual window name is trimmed.

  • The window class can be shown with property showWindowClass.

  • Selecting a window can be done with three keystrokes: your shortcut, the number prefix and enter.

  • Script can be used macOS wide if run via e.g. Alfred, BetterTouchTool or Keyboard Maestro.

Using this script actually doesn’t make much sense if you use it via DEVONthink’s script menu as in this case you could simply use the “Window” menu. Of course it can be used with a shortcut from within DEVONthink, but then you’ll miss the best part:

accessing DEVONthink windows from everywhere.

:slight_smile:

-- Window Switcher (across all spaces)

property trimNameTo : 150 -- set to 0 if you don't want trimmed names. if > 0 then this part in e.g. PDF names "[21.0 x 29.7 cm, 3 Seiten, 100%]"  is always cut 
property showWindowClass : true

tell application id "DNtp"
	try
		tell application "System Events" to tell process "DEVONthink 3" to tell menu bar 1 to tell menu bar item 10 to tell menu 1 to set theMenuItems to name of every menu item
		try
			set theWindowNames_Menu to items 20 thru -1 in theMenuItems
		on error
			error "Please open a window"
		end try
		
		set uniqueNames to true
		repeat with thisName in theWindowNames_Menu
			if my countInstancesOfItemInList(theWindowNames_Menu, thisName as string) > 1 then
				set uniqueNames to false
				exit repeat
			end if
		end repeat
		
		if uniqueNames = true then
			set {theWindowNames_sorted, theMenuPositions} to my sortWindowNames()
			set theDisplayNames to my getDisplayNames(theWindowNames_sorted)
			set thePromptExtension to ""
		else
			set theMenuPositions to {}
			set theCount to 19
			repeat (count theWindowNames_Menu) times
				set theCount to theCount + 1
				set end of theMenuPositions to theCount
			end repeat
			set theDisplayNames to my getDisplayNames(theWindowNames_Menu)
			set thePromptExtension to (ASCII character 9) & (ASCII character 9) & (ASCII character 9) & "-- Unsorted --"
		end if
		
		activate
		set theChoice to choose from list theDisplayNames with prompt "Go to:" & thePromptExtension default items (item 1 of theDisplayNames) with title ""
		if theChoice is false then return
		set theChoice to item 1 of theChoice
		
		set theMenuPosition to item ((characters 1 thru ((offset of (ASCII character 9) in theChoice) - 1) in theChoice as string) as integer) in theMenuPositions
		
		if theMenuPosition ≥ 20 then
			tell application "System Events" to tell process "DEVONthink 3" to tell menu bar 1 to tell menu bar item 10 to tell menu 1 to click menu item theMenuPosition
			return
		else
			error ("Error: Menu Position " & theMenuPosition) as string
		end if
		
	on error error_message number error_number
		activate
		display alert "DEVONthink" message error_message as warning
		return
	end try
end tell

on countInstancesOfItemInList(theList, theItem)
	set theCount to 0
	repeat with a from 1 to count of theList
		if item a of theList is theItem then
			set theCount to theCount + 1
		end if
	end repeat
	return theCount
end countInstancesOfItemInList

on sortWindowNames()
	tell application "System Events" to tell process "DEVONthink 3" to set theWindowNames_CurrentSpace to name of windows
	set theMenuPositions to {}
	repeat with thisName in theWindowNames_CurrentSpace
		set end of theMenuPositions to (19 + (my getPositionOfItemInList(thisName as string, my theWindowNames_Menu))) as integer
	end repeat
	set theWindowNames_OtherSpaces to {}
	set theCount to 19
	repeat with thisName in my theWindowNames_Menu
		set theCount to theCount + 1
		if theCount is not in theMenuPositions then
			set end of theWindowNames_OtherSpaces to thisName as string
			set end of theMenuPositions to (19 + (my getPositionOfItemInList(thisName as string, my theWindowNames_Menu))) as integer
		end if
	end repeat
	set theWindowNames_sorted to theWindowNames_CurrentSpace & theWindowNames_OtherSpaces
	return {theWindowNames_sorted, theMenuPositions}
end sortWindowNames

on getPositionOfItemInList(theItem, theList)
	repeat with a from 1 to count of theList
		if item a of theList is theItem then return a
	end repeat
	return 0
end getPositionOfItemInList

on getDisplayNames(theWindowNames)
	set theDisplayNames to {}
	set theCount to 0
	repeat with thisName in theWindowNames
		set theCount to theCount + 1
		tell application id "DNtp"
			try
				if trimNameTo = 0 then
					set thisDisplayName to thisName as string
				else
					set thisWindow to (first think window whose name = thisName)
					try
						set thisContentRecord to content record of thisWindow
						if thisContentRecord ≠ missing value then
							set thisFileName to filename of thisContentRecord
							set thisSuffix to my getSuffix(thisFileName)
						else
							set thisSuffix to ""
						end if
					on error
						set thisSuffix to ""
					end try
					
					if thisName ends with "%]" then
						set thisName_reverse to (reverse of (characters in thisName)) as string
						set thisName_cleaned_reverse to (characters ((offset of "[ " in thisName_reverse) + 2) thru -1 in thisName_reverse) as string
						set thisName_cleaned to (reverse of (characters in thisName_cleaned_reverse)) as string
					else
						set thisName_cleaned to thisName
					end if
					
					if (length of thisName_cleaned) > trimNameTo then
						set thisName_trimmed to (characters 1 thru (trimNameTo - 2) in thisName_cleaned) as string
						if thisName_trimmed ends with space then set thisName_trimmed to (characters 1 thru -2 in thisName_trimmed) as string
						if thisSuffix ≠ "" then
							set thisDisplayName to (thisName_trimmed & "[...]." & thisSuffix) as string
						else
							set thisDisplayName to (thisName_trimmed & "...") as string
						end if
					else
						set thisDisplayName to thisName_cleaned
					end if
				end if
				
				if showWindowClass = true then
					try
						if class of thisWindow = viewer window then
							set thisClass to ("Viewer" & (ASCII character 9) & (ASCII character 9)) as string
						else
							set thisClass to ("Document" & (ASCII character 9))
						end if
					on error
						set thisClass to (ASCII character 9) & (ASCII character 9) & (ASCII character 9) as string
					end try
				else
					set thisClass to ""
				end if
				
				set end of theDisplayNames to (theCount & (ASCII character 9) & thisClass & thisDisplayName) as string
				
			on error error_message number error_number
				error number -128
			end try
		end tell
	end repeat
	return theDisplayNames
end getDisplayNames

on getSuffix(PathOrName)
	set theSuffix to reverse of characters 1 thru ((offset of "." in (reverse of characters in PathOrName as string)) - 1) in (reverse of characters in PathOrName as string) as string
end getSuffix

2 Likes

You rock, @pete31!!

The new script (edited in post 1) switches reliably to any selected window. All limitations of the previous approach are solved.