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:
- While moving a Google Chrome window using the
moveAndResize
function in theWinWin
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. - While moving a Google Chrome window to another screen using
moveToScreen()
inWinWin
, 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 theWinWin
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.
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
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:
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:
hammerspoon/extensions/window/libwindow.m
Line 227 in 1bd0c18
Can anybody shed some light on what's happening under the hood?
Thanks!
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 totrue
on theirAXApplication
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 settingAXEnhancedUserInterface
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!
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!
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?
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
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.
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.
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.
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).