Hammerspoon / hammerspoon

Staggeringly powerful macOS desktop automation with Lua

Home Page:http://www.hammerspoon.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

hs.window:moveToScreen() behaves weirdly in macOS 12.4 when applied to a Google Chrome window.

liancheng opened this issue · comments

After upgrading to macOS 12.4 Monterey, I've noticed two issues:

  1. While moving a Google Chrome window using the moveAndResize function in the WinWin Spoon, the target window shows an animation while moving/resizing. Windows of other applications do not behave like this: they just immediately resize/move to the required shape and location.
  2. While moving a Google Chrome window to another screen using moveToScreen() in WinWin, instead of moving to the target screen, the target window shrinks to roughly half of the original length and width. Same as above, windows of other applications show the desired behavior.

Further debugging showed that calling hs.window:moveToScreen() directly shares the same issue.

You may reproduce this issue by opening a Google Chrome window, navigate to https://www.hammerspoon.org/docs/hs.window.html, and run the following snippet in the Hammerspoon console:

c = hs.window.find("Hammerspoon docs: hs.window")
s = c:screen()
c:moveToScreen(s:next())

Version information:

  • macOS: 12.4
  • Hammerspoon: 0.9.97 (6267)
  • Google Chrome: 101.0.4951.64 (Official Build) (x86_64)

I did not use that function for my code, since i wrote some on my own...

Here is an excerpt from my code. I found some things from that online and derived this from it.

local function initWindowValues()
	local win = hs.window.focusedWindow()
	local f = win:frame()
	local screen = win:screen()
	local max = screen:frame()
	return win, f, screen, max
end

hs.hotkey.bind({"cmd", "alt", "ctrl"}, "/", function ()
	local win, f, screen = initWindowValues()
	local screens = hs.screen.allScreens()
	local i = 0
	local screenNo = 0
	for i = 1, #screens do
		if screens[i]:id()==screen:id() then
			screenNo = i
		end
	end
	local newScreenNo = screenNo+1
	if newScreenNo>#screens then
		newScreenNo = 1
	end
	win:moveToScreen(screens[newScreenNo])
end)

May be this helps you a little bit to fix that function stuff.

While moving a Google Chrome window using the moveAndResize function in the WinWin Spoon, the target window shows an animation while moving/resizing. Windows of other applications do not behave like this: they just immediately resize/move to the required shape and location.

I've recently observed the same thing happening with Firefox (101.0) on macOS 12.4 and Hammerspoon 0.9.97 (6267), but I'm using hs.window:setFrame. I recently upgraded to 1Password 8 around the same time I noticed this happening. I've realized that when I restart Firefox and don't use 1password autofill in the new session, my resizing behaves as expected (instantly, correctly, no animation). But once I use autofill the window movements animate and sometimes require multiple attempts to reach their correct size and location.

So out of curiosity, are you also using 1password 8 with the new Universal Autofill feature? If so, we might be seeing the same issue related to it. If not, sorry for the tangent on your thread. 😄

That's exactly my situation too. 1PW 8, Chrome, latest macOS. I think you're on to something here.

Maybe related: pqrs-org/Karabiner-Elements#3075 (comment). Weirdly, the comment mentions the problem being solved after upgrading to macOS 12.4.

commented

FWIW, I see the same behavior with Safari and 1password with Universal Autofill... and I'm already running 12.4, so...

Not sure what's up, but I'll check out the 1password forums later today and submit a bug report if it's not already known. I'm pretty sure it's not anything Hammerspoon is doing, so it's either a macOS change/bug that we're not aware of or an issue with 1password and how it's tapping into things for the autofill support.

I'm having this issue, and I'm also a 1Password user.

  • Today's 1Password update didn't make a difference.
  • Closing the 1Password application then closing & relaunching Chrome seems to fix the behavior
  • Disabling the 1Password Chrome extension then closing & relaunching Chrome also seems to fix the behavior

I tried a couple other combos inconclusively - it could be that, in some percentage of cases, closing and relaunching Chrome temporarily fixes the problem regardless of whether 1Password is involved.

Just adding another +1 for the weirdness @zackfern mentioned above. Only my browser window shows the animation and requires multiple attempts to move the window to the correct location.

App Versions

  • Hammerspoon 0.9.97
  • MacOS 12.4
  • Firefox 101.1
  • 1Password (8) for Mac 8.7.1

Another +1 here, happens in both chrome and edge and with 1password 7, not 8

App Versions

  • Hammerspoon 0.9.97
  • MacOS 12.3.1
  • Chrome Version 102.0.5005.115
  • Edge Version 102.0.1245.44
  • 1Password (7) for Mac 7.9.5

Same issue here. I do have Karabiner Elements installed as well as 1password 8.
However the remappings I have defined in KE (mainly for switching tabs in Firefox) work as expected.

It's only that the shortcuts I created in hammerspoon to move windows fast to a desired position in a desired size don't work properly for Firefox. It's slow and often misplaced. Only a second hit moves it correctly (most of the times). I'm using setFrame.

  • Hammerspoon 0.9.97
  • MacOS 12.4
  • Firefox 91.10.0esr (for whatever reason not getting a newer version from company software center)
  • 1Password 8.7.1
  • Karabiner Elements 14.4.0

For me, this issue has been resolved. It was in fact an odd interaction between 1Password's MacOS app and Chrome plugin.

I worked on this with Paul from 1Password support, sending him here and explaining how to set up Hammerspoon and reproduce the issue. I installed a desktop app update on July 12th and haven't seen this problem since.

FWIW my 1Password for Mac reports version 8.9.0, 80900001 on BETA channel.

Hmm, I'm not a 1Password user, but I'm using LastPass. I don't remember hitting this issue in Firefox, where LastPass is also installed. Also, after moving to an M1, I'm no longer able to reproduce this issue.

I think this issue is now resolved.

I still have the issue.

  • macOS 12.5
  • Chrome 104.0.5112.79
  • Hammerspoon 0.9.97
  • 1Password Extension 2.3.7

I tested some more, and now I am pretty sure this is not related to 1Password. I was able to reproduce on a machine without 1Password installed, with a fresh installation of google chrome stable.

  • macOS 12.5 on a M1
  • Chrome 104.0.5112.101 (arm64)
  • Hammerspoon 0.9.97

and I found my own personal culprit. It is caused by the Gramarly Desktop app. As soon as I quit this app and restart Chrome, everything is instant again. Just dropping this here in case it helps someone else. It seemed that this can be caused by any app that creates overlay windows on top of electron-based apps.

try this
i solved this way

Accessibility → Zoom

if check this option "animation delay" Occurs immediately.
option check out and Reboot

스크린샷 2022-08-21 오전 9 22 16

nd Reboot!

try this i solved this way

Accessibility → Zoom

if check this option "animation delay" Occurs immediately. option check out and Reboot

스크린샷 2022-08-21 오전 9 22 16

nd Reboot!

Great find!

It seems that both the first two checkboxes should be unchecked and a reboot is required, or restart the application (chrome or so) you want to move.

And this checkbox of advanced settings should also be unchecked:

image

and I found my own personal culprit. It is caused by the Gramarly Desktop app. As soon as I quit this app and restart Chrome, everything is instant again. Just dropping this here in case it helps someone else. It seemed that this can be caused by any app that creates overlay windows on top of electron-based apps.

It was Grammarly for me as well. Thank you so much! This was driving me crazy 😂

I have contacted Grammarly regarding this issue and haven't received any solution. Is there something that can be done from Hammerspoon's side?

I debugged the Lua code for setFrame, and everything seems correct. The problems seem to begin in libwindow.m, but unfortunately, I have no idea how to debug this part:

static int window__settopleft(lua_State* L) {

Can anybody shed some light on what's happening under the hood?

Thanks!

commented

I don't use 1Password or Grammarly myself, but they both sound like they would be using the Accessibility API to see what's going on in other apps. Chrome/Chromium and its derivatives (including Electron) don't create the full tree of accessibility info by default (for performance reasons, if I remember correctly) - they only do so if something sets the AXEnhancedUserInterface attribute to true on their AXApplication element. This is an undocumented property that is usually used to indicate that VoiceOver is running. 1Password and Grammarly are probably setting this themselves to force Chrome to generate the full tree.

Unfortunately, there seems to be a system bug that messes up attempts at moving/resizing a window through the Accessibility API when the window's app has AXEnhancedUserInterface set. This has caused problems for a number of window managers and similar tools. Phoenix is having issues with it right now, and it seems to be caused by 1Password in that case too. (It also caused problems for Hammerspoon in #904, but in that case the app that was setting AXEnhancedUserInterface was changed to not do so.)

Rectangle worked around it in this pull request, by temporarily disabling AXEnhancedUserInterface before moving/resizing, and setting it back afterwards. It might be worth adding similar logic to [HSwindow setTopLeft:] and [HSwindow setSize:]. For now, you can try doing something like this from your config:

-- win = some `hs.window` instance
local axApp = hs.axuielement.applicationElement(win:application())
local wasEnhanced = axApp.AXEnhancedUserInterface
if wasEnhanced then
	axApp.AXEnhancedUserInterface = false
end
win:setFrame(newFrame) -- or win:moveToScreen(someScreen), etc.
if wasEnhanced then
	axApp.AXEnhancedUserInterface = true
end

(Electron added an AXManualAccessibility property that forces the accessibility tree without triggering the bug, but it currently isn't working.)

I don't use 1Password or Grammarly myself, but they both sound like they would be using the Accessibility API to see what's going on in other apps. Chrome/Chromium and its derivatives (including Electron) don't create the full tree of accessibility info by default (for performance reasons, if I remember correctly) - they only do so if something sets the AXEnhancedUserInterface attribute to true on their AXApplication element. This is an undocumented property that is usually used to indicate that VoiceOver is running. 1Password and Grammarly are probably setting this themselves to force Chrome to generate the full tree.

Unfortunately, there seems to be a system bug that messes up attempts at moving/resizing a window through the Accessibility API when the window's app has AXEnhancedUserInterface set. This has caused problems for a number of window managers and similar tools. Phoenix is having issues with it right now, and it seems to be caused by 1Password in that case too. (It also caused problems for Hammerspoon in #904, but in that case the app that was setting AXEnhancedUserInterface was changed to not do so.)

Rectangle worked around it in this pull request, by temporarily disabling AXEnhancedUserInterface before moving/resizing, and setting it back afterwards. It might be worth adding similar logic to [HSwindow setTopLeft:] and [HSwindow setSize:]. For now, you can try doing something like this from your config:

-- win = some `hs.window` instance
local axApp = hs.axuielement.applicationElement(win:application())
local wasEnhanced = axApp.AXEnhancedUserInterface
if wasEnhanced then
	axApp.AXEnhancedUserInterface = false
end
win:setFrame(newFrame) -- or win:moveToScreen(someScreen), etc.
if wasEnhanced then
	axApp.AXEnhancedUserInterface = true
end

(Electron added an AXManualAccessibility property that forces the accessibility tree without triggering the bug, but it currently isn't working.)

This is amazing. Thank you so much for sharing it!

I implemented a couple of helper functions that implement your suggested fix. It is working like a charm:

function axHotfix(win)
  if not win then win = hs.window.frontmostWindow() end

  local axApp = hs.axuielement.applicationElement(win:application())
  local wasEnhanced = axApp.AXEnhancedUserInterface
  if wasEnhanced then
    axApp.AXEnhancedUserInterface = false
  end

  return function()
    if wasEnhanced then
      axApp.AXEnhancedUserInterface = true
    end
  end
end

function withAxHotfix(fn, position)
  if not position then position = 1 end
  return function(...)
    local args = {...}
    local revert = axHotfix(args[position])
    fn(...)
    revert()
  end
end

The former wraps around the received window object, and returns a function that reverts the hack. The latter wraps a function.

The usage is something like this:

local patchedPushWindowUp = withAxHotfix(grid.pushWindowUp)

Or, if the wrapped function receives the window in a different position, something like this:

local patchedAdjustWindow = withAxHotfix(grid.adjustWindow, 2)

If no window is passed, it takes the frontmost window. I think this matches most of hammerspoon APIs.

Cheers!

commented

I think that 'revert' function inside axHotfix needs to be setting AXEnhancedUserInterface back to true instead of false, to make sure Chrome, etc. keep updating the a11y tree for Grammarly/1Password/whatever. Looks good otherwise.

Whoops, I messed up when I copy-pasted some code. I'll edit it, so people don't get confused.

Thanks!

commented

This would be a fairly easy fix to add to the hs.window module (which underpins grid, layout, etc.) if it's a reliable fix for this issue... is it just movement and resizing that needs to make this check, or are there other hs.window methods that could use a wrapper as well?

commented

The only thing I know of is movement and resizing, but I can't say for sure.

Doing some experimenting in the Hammerspoon console, it looks like what's happening is that when AXEnhancedUserInterface is on1, setting AXSize or AXPosition will start smoothly animating the window to the new size/position rather than changing it immediately - but it also interrupts any such animation that's already in progress. So if you set AXSize and AXPosition in rapid succession (like win:setFrame(...) does), whichever one is set first doesn't have a chance to finish animating, and ends up stopping at some intermediate state. That doesn't quite explain why both the size and position seem to be ending up wrong, but I suppose it might be asynchronous enough that the order in which they actually start could vary…

(On a possibly-related note, why does setFrame set the size, position, and then the size again?)

Footnotes

  1. And, presumably, the zoom settings mentioned above have been on since the last boot. Haven't gotten around to rebooting to test this, though, and relaunching the app doesn't seem to be resetting it for me.

Thanks for the explanation @Rhys-T.

Although disabling AXEnhancedUserInterface works, I ended up taking a different approach since it made transitions laggy.

My approach is to move the window to the top left first and then make it a full screen.

The code looks like this:

function fullscreen(win)
  local screenFrame = win:screen():frame()

  win:setTopLeft(screenFrame.x, screenFrame.y)

  -- Waiting 0.4 seconds to make the two step transition work
  -- You might need to adjust this.
  hs.timer.usleep(0.4 * 1000 * 1000) 

  win:setFrame(screenFrame)

  return win
end

This makes it two steps with a delay, but I found it quicker than controlling AXEnhancedUserInterface.

This is quite hacky, but I hope someone finds it helpful.

commented

Good to know. If that workaround is slowing things down, maybe it shouldn't be built into hs.window, or at least should have some sort of flag to enable/disable it.

commented

Possibly related to the lag mentioned above: Chrome apparently has a recent bug (#1364487) causing it to lag whenever anything turns its AXEnhancedUserInterface attribute off. More discussion in rxhanson/Rectangle#912, since it's causing problems for their version of this workaround.

commented

Possibly related to the lag mentioned above: Chrome apparently has a recent bug (#1364487) causing it to lag whenever anything turns its AXEnhancedUserInterface attribute off.

Looks like that particular bug has been fixed in recent Chromium/Chrome versions. May take some time to propagate to other Chromium-based browsers, though (and Electron apps, if they're affected).