Refinement of Palette Placement

I have been playing around with the placement of Keyboard Maestro palettes. There are already plug-ins that allow one to place the palette around the outside or just inside an application window. What I wanted to explore was if it was possible to actually attach palettes to objects within an application window.

This may provide a shorter mouse distance for native functionality with the app, and a listing of native and created keyboard short-cuts on the palette (helpful for those aging minds).

Lately, I have been playing with Xcode, and have developed an ability to attach a palette to the center of the vertical scroll bar within the canvas area of Interface Builder. Tonight, I was also able to attach a palette to a ViewController within the canvas. The challenge is not attaching the palettes to the objects, it is having the palettes automatically move or update as currently the finest level of control is at the window level (title change or frame change). That doesn't limit you from assigning another trigger such as a keyboard shortcut to initiate the update. A repeating time trigger base on app being active will work, choose your poison on the repeat time.

Example of attached to canvas vertical scroll bar in Interface Builder

Example of attached to active view controller in Interface Builder:

There are a number of applications that change their window title based on the content of the window. This can be used to change out palettes that are specific to the content. For example, in Xcode, I can create palettes specific to functionality associated with the various file types of a project, one for .storyboards, one for .swift, one for specific class I may be working with or more generally with viewControllers.

Example of a different palette arriving when switching to a different file type in Navigator of Xcode

Anyone interested?

Hey djgregg,

I've been pushing KM in a more visually-based direction. As the name KEYBOARD Maestro suggests, this isn't Peter's intent or seemingly, interest so we're on our own unless you can use the Custom HTML Prompt to roll your own. I haven't seen many examples to noodle with and am unlikely to learn enough from scratch to satisfactorily scratch this itch with that tool.

I'd really like to recreate Apple's customizable toolbar functionality and insert buttons wherever I choose, with said buttons being the macro trigger. However, this seems a long way off, and, why not just state it clearly?

Here's an example palette with two triggers, inserted blanks, and icons sized to act as buttons that visually and functionally make sense. The two delete icons '⌫' represent delete when clicked on do a select all/delete/select the All inboxes mailbox sequence. It's simple and satisfying each time I use it.

It's hacking both UI and KM and only works well if Mail is left in place. In this model, updating palette location could be another button as long as new window locations are predetermined.

I once tried breaking up these palettes into one 'button' palettes, but KM gets noticeably slow when drawing more than a small number of pallets per app. So again, this seems a direction Peter hasn't and isn't intending to go.

As an early step along this more visual way, would attaching palettes to icons be doable? Also, could areas like the sidebar or toolbar be recognizable as an area that could be subdivided and addressed as @DanThomas did with the entire window?

I know this is ambitious given the current toolset and nothing happens without some call for action.

The work that @DanThomas and others did at the time was using the location of the window to place the palette in relation to a window (top, bottom, left, right). The palette would move with the window to a new location if the user moved the window. What I added was the ability to not only position the palette in relation to the window frame, but also, in the case of Xcode, to certain objects within that window.

This was accomplished through a combination of KM and AppleScript System Events programming, which may or may not be available to all of the applications you wish, and may also depend on the version of macOS/app.

Doing a quick check I can get the locations of the sidebar, toolbar of Mail. Within those areas I can also get the location of the app icons, eg. the junk folder icon in Mail, so a palette could be placed next to a particular icon. The palette could then contain actions specific to that icon if wished.

Please let me know if this helps...

It does depending on the implementation.

How would this work with the junk folder icon as an example?

Below is an example AppleScript System Events script that can be run in Script Editor and provides the coordinates for the top-left corner of:

  1. the Mail Message Viewer window in global screen coordinates;
  2. the icon for the Junk Folder in global screen coordinates;
  3. the icon for the Junk Folder relative to the Mail Message Viewer window.

Using either 2 or 3 could then be used to position a KM palette at exactly those screen coordinates. Note that you would have to decide whether you want to adjust the location to not cover-up the icon. The size of the icon can also be determined if needed.

This script would be combined with the previous palette location and dynamic location work to automatically position and reposition the palette relative to the folder icon.

on run
   
   set folderName to "Junk"
   
   set {windowPosition, globalFolderIconPosition, relativeFolderIconPosition} to my determineMailFolderIconLocation(folderName)
   
   return {windowPosition:windowPosition, globalIconPosition:globalFolderIconPosition, relativeIconPosition:relativeFolderIconPosition}
   
end run

on determineMailFolderIconLocation(folderName)
   tell application "System Events"
      
      tell process "Mail"
         
         set frontmost to true
         
         delay 0.5
         
         tell window 1 to set windowPosition to position
         
         set folderRow to rows of outline 1 of scroll area 1 of splitter group 1 of window 1 whose (value of static text 1 of UI element 1 = folderName)
         
         set globalFolderIconPosition to position of image 1 of UI element 1 of first item of folderRow
         
         set relativeFolderIconPosition to {(item 1 of globalFolderIconPosition) - (item 1 of windowPosition), (item 2 of globalFolderIconPosition) - (item 2 of windowPosition)}
         
         return {windowPosition, globalFolderIconPosition, relativeFolderIconPosition}
         
      end tell
      
   end tell
end determineMailFolderIconLocation

The script was developed for Mail 12.4 on macOS Mojave. You might also have to allow Script Editor to control Mail in the Security & Privacy section of System Preferences.

1 Like

Hey @djgregg,

All AppleScripts have an implicit run handler, so there is never any necessity to explicitly use one – unless you're running an applet that also has an open handler. (E.g. the applet has two modes – open for drag and drop and run.)

-Chris

Hey Folks,

I recommend that no one ever use Apple's Script Editor.app for writing AppleScript on macOS.

Use Script Debugger instead.

The commercial version is not inexpensive ($99.99), BUT it reverts to its FREE β€œLite” version after a 30 day demo period – and the free version still beats the utter pants off of the Script Editor.app.

Script Editor.app does become useful when writing JXA (JavaScript for Automation) scripts – only because Script Debugger doesn't support JXA.

-Chris

I use an explicit run handler to potentially avoid the freeform nature of the implicit handler from executing some loose commands in multiple places through the file. With an explicit run handler any loose commands will be caught at compile time.

I personally use Script Debugger. However, if you don't do much scripting you can save having to go get an extra piece of software and make sure it is up-to-date.

Script Debugger (even the Lite version) is so superior it's worth a little trouble – even for folks who don't write much AppleScript.

Using the Script Editor.app is a miserable experience by comparison.

-Chris

Here is an example of a more fleshed out example...

Apple Mail

Within Mail I have 6 Mailbox folders within the Message Viewer window.

image

Here is what it looks like with the Inbox folder selected. The top of the palette matches the top of the selection, and the left of the palette is just beyond the longest name of the Mailboxes, i.e. Flagged. If the frame of the window changes, i.e., resizes or is moved on the screen, the macro is triggered and recalculates to position the palette in the same relative position with the folder icon. Each time the macro is triggered it first makes sure that the Mailbox area is showing in the Mail Message Viewer window, and sets the width of that area to 300 pixels in order to fit the current menu width without covering up the count of unread emails.

The palette is formatted to more closely resemble a context or drop-down menu. Only showing the text...

image

... and another example with the Junk folder selected.

image

Keyboard Maestro

Six separate palettes with each palette having a single macro that calculates and sets the location of the palette within the Mail Message Viewer window.

image

I set each palette to a different color just to illustrate that the palettes are changing as the different Mailbox folders are selected.

Here is an example of the setup for the Mail - Inbox macro group.

Within each group there is one macro, titled 99) to send it to the bottom of the palette with no label. The macro contains the AppleScript code to dynamically place the pallet with it's top matching the top of the selection banner of the appropriate Mailbox folder icon. The pallet will also be placed a distance to the right of the folder icon to allow the label to be seen. That distance is currently set to 80 pixels, enough to clear the longest folder name, i.e., Flagged.

AppleScript Macro for each Mail Macro Group

This is an example of the Applescript macro coming from the Mail - Inbox Macro group:

This Applescript macro is calling a Script Library which is located in the following location:

~/Library/Script Libraries/SetupMailPalettes.scpt

NOTE: You may have to create that location if it doesn't exist.

and it's contents are....

SetUpMailPalettes.scpt

on run
	set folderName to "Inbox"
	my doSetupOfMailboxFolder(folderName)
end run

on doSetupOfMailboxFolder(folderName)
	set mailboxesAreaWidth to 300
	set palettePositionAdjustment to 80
	
	my setMailBoxesAreaWidth(mailboxesAreaWidth)
	set {windowPosition, globalJunkFolderIconPosition, relativeJunkFolderIconPosition} to my determineMailFolderIconLocation(folderName)
	set palettePosition to {(item 1 of globalJunkFolderIconPosition) + palettePositionAdjustment, item 2 of globalJunkFolderIconPosition}
	my setPalettePosition("Mail - " & folderName, palettePosition)
end doSetupOfMailboxFolder

on setMailBoxesAreaWidth(mailboxesAreaWidth)
	tell application "System Events"
		tell process "Mail"
			
			-- Make sure Mailbox List is showing
			set showHideMailboxList to name of menu item 18 of menu 1 of menu bar item 5 of menu bar 1
			if showHideMailboxList contains "Show" then
				click menu item 18 of menu 1 of menu bar item 5 of menu bar 1
			end if
			
			-- Make sure Mailbox List area is wide enough to hold the palette without covering message count
			if value of splitter 1 of splitter group 1 of window 1 is less than mailboxesAreaWidth then
				set value of splitter 1 of splitter group 1 of window 1 to mailboxesAreaWidth
			end if
			
		end tell
	end tell
end setMailBoxesAreaWidth

on determineMailFolderIconLocation(folderName)
	
	tell application "System Events"
		tell process "Mail"
			
			tell window 1 to set windowPosition to position
			
			set junkFolderRow to rows of outline 1 of scroll area 1 of splitter group 1 of window 1 whose (value of static text 1 of UI element 1 = folderName)
			set globalJunkFolderIconPosition to position of image 1 of UI element 1 of first item of junkFolderRow
			set relativeJunkFolderIconPosition to {(item 1 of globalJunkFolderIconPosition) - (item 1 of windowPosition), (item 2 of globalJunkFolderIconPosition) - (item 2 of windowPosition)}
			
			return {windowPosition, globalJunkFolderIconPosition, relativeJunkFolderIconPosition}
			
		end tell
	end tell
	
end determineMailFolderIconLocation

on setPalettePosition(paletteName, newPalettePosition)
	tell application "System Events"
		tell process "Keyboard Maestro Engine"
			set position of window paletteName to newPalettePosition
		end tell
	end tell
end setPalettePosition

Thank you for the example!!

So far, only the change the sidebar width functions works when I move the window and it only works to make the sidebar wider if set too narrow and does not narrow it when too wide.

The window does not reposition or resize if moved nor does the palette move.

My setup is different and I'm guessing the script needs to be edited.
Here is my sidebar:
Screen Shot 2021-08-25 at 11.16.13 AM

I changed the KM embedded trigger to:

And the first set line to match:

Suggestions?

BernSh
August 25
Thank you for the example!!

So far, only the change the sidebar width functions works when I move the window and it only works to make the sidebar wider if set too narrow and does not narrow it when too wide.

It was only a quick proof of concept for palette location. I can't anticipate all of your needs without further discussion of them.

If the sidebar is changing at least that means the Script Library functionality and location is correct.

The window does not reposition or resize if moved nor does the palette move.

Again, I wasn't aware that you wanted to have the window be repositioned. It doesn't need to be for the script, when it works, to determine where the palette should be moved in relation to the current window position.

My setup is different and I'm guessing the script needs to be edited.

The script was designed to be general enough to determine which row in the list contains the label being sent as a parameter to the script. More testing is required to make sure it is actually doing that with your setup.

Here is my sidebar:

I changed the KM embedded trigger to:

How did you change it? It looks the same to me, checking for window title change, and window frame change.

I noticed after doing some more testing that there should also be a triggers for toolbar state changes(showing/hiding) and opening/closing of any sidebar folder above the one of interest occurs. Unfortunately, at this point, I don't know of anyway to implement those.

And the first set line to match:

It is my understanding that those lines really won't make a difference in this script as I am not using Scripting Additions or ASObjc calls.

Suggestions?

Which version of Mail and macOS are you currently using? As you can imagine, Apple from time to time changes the GUI in their applications, and that can throw off AppleScript Systems Events scripting.

Are you willing and capable of working/debugging with the AppleScript directly in either Script Editor or Script Debugger? Errors are easier to track down outside of KM, as is script editing.

There isn't a way to trigger changes that happen inside an app window via KM. I am currently using the change of the window's title or frame.

Changes within a window can often be detected through System Events programming, such as the toolbar visibility, the opening/closing of folders in the sidebar, and the sizing of the mailbox sidebar area. However, at the moment the only way around this is to introduce a timer trigger within KM. Unfortunately, that often creates other issues such as bogging down the response of the app or overloading the processor.

I have separated the use of the timer trigger into a separate macro "100) " so it is easier to enable/disable if needed. As you can see it simply calls the same script after being triggered by the timer. You can play with the value of the timer to suit your needs.

May have found the solution. It looks like Apple has removed the icon images for the folders in the sidebar of Mail. They are using SF Symbols logos instead and those aren't seen by System Events.

I have switched the code in the determineMailFolderIconLocation handler of the SetupMailPalettes script library to use the text block instead. That still works with macOS Mojave, and I believe should work moving forward. Here is the code for the revised handler.

on determineMailFolderIconLocation(folderName)
	
	tell application "System Events"
		tell process "Mail"
			
			tell window 1 to set windowPosition to position
			
			set folderRow to rows of outline 1 of scroll area 1 of splitter group 1 of window 1 whose (value of static text 1 of UI element 1 = folderName)
			set globalFolderIconPosition to position of static text 1 of UI element 1 of first item of folderRow
			set relativeFolderIconPosition to {(item 1 of globalFolderIconPosition) - (item 1 of windowPosition), (item 2 of globalFolderIconPosition) - (item 2 of windowPosition)}
			
			return {windowPosition, globalFolderIconPosition, relativeFolderIconPosition}
			
		end tell
	end tell
	
end determineMailFolderIconLocation

Refactoring

I decided it might be better to remove the two triggering macros from each of the folder macro groups, and place them in a single macro group I have called "Mail". This cleans up the palettes, provides a central location to control the triggering activity, and provides a natural place to potentially add any further future control to the Mail app.

All of the previous scripting was moved to a script library named "Mail" and the 99) and 100) triggering macros were renamed, within the "Mail" macro group to "Show Palettes - Timer" and "Show Palettes - Window Change". They call the same handler in the Mail script library, and only differ on their triggering mechanism. Be careful with the value of the "repeating" variable in the timer, it can have major effects on the response of your machine.

Keyboard Maestro

Mail Script Library

on run
	my setupMailPalettes()
end run

on setupMailPalettes()
	
	set mailboxesAreaWidth to 300
	set palettePositionAdjustment to 80
	
	tell application "System Events"
		tell process "Mail"
			set windowName to name of window 1
		end tell
	end tell
	
	if windowName contains "Inbox" then
		set folderName to "Inbox"
	else if windowName contains "Drafts" then
		set folderName to "Drafts"
	else if windowName contains "Flagged" then
		set folderName to "Flagged"
	else if windowName contains "Sent" then
		set folderName to "Sent"
	else if windowName contains "Junk" then
		set folderName to "Junk"
	else if windowName contains "Trash" then
		set folderName to "Trash"
	end if
	
	my setMailBoxesAreaWidth(mailboxesAreaWidth)
	set {windowPosition, globalFolderIconPosition, relativeFolderIconPosition} to my determineMailFolderIconLocation(folderName)
	set palettePosition to {(item 1 of globalFolderIconPosition) + palettePositionAdjustment, item 2 of globalFolderIconPosition}
	my setPalettePosition("Mail - " & folderName, palettePosition)
	
end setupMailPalettes

on setMailBoxesAreaWidth(mailboxesAreaWidth)
	tell application "System Events"
		tell process "Mail"
			
			-- Make sure Mailbox List is showing
			set showHideMailboxList to name of menu item 18 of menu 1 of menu bar item 5 of menu bar 1
			if showHideMailboxList contains "Show" then
				click menu item 18 of menu 1 of menu bar item 5 of menu bar 1
			end if
			
			-- Make sure Mailbox List area is wide enough to hold the palette without covering message count
			if value of splitter 1 of splitter group 1 of window 1 is less than mailboxesAreaWidth then
				set value of splitter 1 of splitter group 1 of window 1 to mailboxesAreaWidth
			end if
			
		end tell
	end tell
end setMailBoxesAreaWidth

on determineMailFolderIconLocation(folderName)
	
	tell application "System Events"
		tell process "Mail"
			
			tell window 1 to set windowPosition to position
			
			-- Find the row in the list that contains the folder
			set folderRow to rows of outline 1 of scroll area 1 of splitter group 1 of window 1 whose (value of static text 1 of UI element 1 = folderName)
			
			-- Determine the top left global screen position of that row
			set globalFolderIconPosition to position of static text 1 of UI element 1 of first item of folderRow
			
			-- Convert the global screen position to a relative window position
			set relativeFolderIconPosition to {(item 1 of globalFolderIconPosition) - (item 1 of windowPosition), (item 2 of globalFolderIconPosition) - (item 2 of windowPosition)}
			
			return {windowPosition, globalFolderIconPosition, relativeFolderIconPosition}
			
		end tell
	end tell
	
end determineMailFolderIconLocation

on setPalettePosition(paletteName, newPalettePosition)
	tell application "System Events"
		tell process "Keyboard Maestro Engine"
			set position of window paletteName to newPalettePosition
		end tell
	end tell
end setPalettePosition