From 96ae7a761f5f969b21e00345629558e005d125c5 Mon Sep 17 00:00:00 2001 From: Nikita Bobko Date: Sat, 3 Aug 2024 19:03:24 +0200 Subject: [PATCH] Improve windows hiding mechanism _fixes https://github.com/nikitabobko/AeroSpace/issues/149 If the monitor configuration is correct, this commit _fixes https://github.com/nikitabobko/AeroSpace/issues/289 I accidentally managed to reduce number of visible pixels to a single 1-pixel vertial line, so it paritally _fixes https://github.com/nikitabobko/AeroSpace/issues/66 --- Sources/AppBundle/command/EnableCommand.swift | 2 +- Sources/AppBundle/layout/refresh.swift | 32 +++++++- Sources/AppBundle/model/Rect.swift | 14 ---- Sources/AppBundle/tree/MacWindow.swift | 16 +++- Sources/AppBundle/util/util.swift | 4 - docs/assets/monitor-arrangement-1-bad.svg | 49 ++++++++++++ docs/assets/monitor-arrangement-1-good.svg | 28 +++++++ docs/assets/monitor-arrangement-2-bad.svg | 32 ++++++++ docs/assets/monitor-arrangement-2-good.svg | 20 +++++ docs/guide.adoc | 77 +++++++++++++++---- 10 files changed, 235 insertions(+), 39 deletions(-) create mode 100644 docs/assets/monitor-arrangement-1-bad.svg create mode 100644 docs/assets/monitor-arrangement-1-good.svg create mode 100644 docs/assets/monitor-arrangement-2-bad.svg create mode 100644 docs/assets/monitor-arrangement-2-good.svg diff --git a/Sources/AppBundle/command/EnableCommand.swift b/Sources/AppBundle/command/EnableCommand.swift index e44d60d8..30f2120a 100644 --- a/Sources/AppBundle/command/EnableCommand.swift +++ b/Sources/AppBundle/command/EnableCommand.swift @@ -27,7 +27,7 @@ struct EnableCommand: Command { } else { activateMode(nil) for workspace in Workspace.all { - workspace.allLeafWindowsRecursive.forEach { ($0 as! MacWindow).unhideViaEmulation() } // todo as! + workspace.allLeafWindowsRecursive.forEach { ($0 as! MacWindow).unhideFromCorner() } // todo as! workspace.layoutWorkspace() } } diff --git a/Sources/AppBundle/layout/refresh.swift b/Sources/AppBundle/layout/refresh.swift index d1375538..7cc81aa8 100644 --- a/Sources/AppBundle/layout/refresh.swift +++ b/Sources/AppBundle/layout/refresh.swift @@ -58,14 +58,42 @@ func refreshObs(_ obs: AXObserver, ax: AXUIElement, notif: CFString, data: Unsaf refreshAndLayout() } +enum OptimalHideCorner { + case bottomLeftCorner, bottomRightCorner +} + private func layoutWorkspaces() { + let monitors = monitors + var monitorToOptimalHideCorner: [CGPoint: OptimalHideCorner] = [:] + for monitor in monitors { + let xOff = monitor.width * 0.1 + let yOff = monitor.height * 0.1 + // brc = bottomRightCorner + let brc1 = monitor.rect.bottomRightCorner + CGPoint(x: 2, y: -yOff) + let brc2 = monitor.rect.bottomRightCorner + CGPoint(x: -xOff, y: 2) + let brc3 = monitor.rect.bottomRightCorner + CGPoint(x: 2, y: 2) + + // blc = bottomLeftCorner + let blc1 = monitor.rect.bottomLeftCorner + CGPoint(x: -2, y: -yOff) + let blc2 = monitor.rect.bottomLeftCorner + CGPoint(x: xOff, y: 2) + let blc3 = monitor.rect.bottomLeftCorner + CGPoint(x: -2, y: 2) + + let corner: OptimalHideCorner = + monitors.contains(where: { m in m.rect.contains(brc1) || m.rect.contains(brc2) || m.rect.contains(brc3) }) && + monitors.allSatisfy({ m in !m.rect.contains(blc1) && !m.rect.contains(blc2) && !m.rect.contains(blc3) }) + ? .bottomLeftCorner + : .bottomRightCorner + monitorToOptimalHideCorner[monitor.rect.topLeftCorner] = corner + } + // to reduce flicker, first unhide visible workspaces, then hide invisible ones for workspace in Workspace.all where workspace.isVisible { // todo no need to unhide tiling windows (except for keeping hide/unhide state variables invariants) - workspace.allLeafWindowsRecursive.forEach { ($0 as! MacWindow).unhideViaEmulation() } // todo as! + workspace.allLeafWindowsRecursive.forEach { ($0 as! MacWindow).unhideFromCorner() } // todo as! } for workspace in Workspace.all where !workspace.isVisible { - workspace.allLeafWindowsRecursive.forEach { ($0 as! MacWindow).hideViaEmulation() } // todo as! + let corner = monitorToOptimalHideCorner[workspace.workspaceMonitor.rect.topLeftCorner] ?? .bottomRightCorner + workspace.allLeafWindowsRecursive.forEach { ($0 as! MacWindow).hideInCorner(corner) } // todo as! } for monitor in monitors { diff --git a/Sources/AppBundle/model/Rect.swift b/Sources/AppBundle/model/Rect.swift index 3f80f6e2..c6b3633e 100644 --- a/Sources/AppBundle/model/Rect.swift +++ b/Sources/AppBundle/model/Rect.swift @@ -8,20 +8,6 @@ struct Rect: Copyable { var height: CGFloat } -extension [Rect] { - func union() -> Rect { - let rects: [Rect] = self - let topLeftY = rects.map(\.minY).minOrThrow() - let topLeftX = rects.map(\.minX).maxOrThrow() - return Rect( - topLeftX: topLeftX, - topLeftY: topLeftY, - width: rects.map(\.maxX).maxOrThrow() - topLeftX, - height: rects.map(\.maxY).maxOrThrow() - topLeftY - ) - } -} - extension CGRect { func monitorFrameNormalized() -> Rect { let mainMonitorHeight: CGFloat = mainMonitor.height diff --git a/Sources/AppBundle/tree/MacWindow.swift b/Sources/AppBundle/tree/MacWindow.swift index 63ddb1ce..382632c3 100644 --- a/Sources/AppBundle/tree/MacWindow.swift +++ b/Sources/AppBundle/tree/MacWindow.swift @@ -106,8 +106,8 @@ final class MacWindow: Window, CustomStringConvertible { return AXUIElementPerformAction(closeButton, kAXPressAction as CFString) == AXError.success } - func hideViaEmulation() { - //guard let monitorApproximation else { return } + func hideInCorner(_ corner: OptimalHideCorner) { + guard let nodeMonitor else { return } // Don't accidentally override prevUnhiddenEmulationPosition in case of subsequent // `hideEmulation` calls if !isHiddenViaEmulation { @@ -116,10 +116,18 @@ final class MacWindow: Window, CustomStringConvertible { prevUnhiddenEmulationPositionRelativeToWorkspaceAssignedRect = topLeftCorner - workspace.workspaceMonitor.rect.topLeftCorner } - _ = setTopLeftCorner(allMonitorsRectsUnion.bottomRightCorner) + let p: CGPoint + switch corner { + case .bottomLeftCorner: + guard let s = getSize() else { fallthrough } + p = nodeMonitor.visibleRect.bottomLeftCorner + CGPoint(x: 1, y: -1) + CGPoint(x: -s.width, y: s.height) + case .bottomRightCorner: + p = nodeMonitor.visibleRect.bottomRightCorner - CGPoint(x: 1, y: 1) + } + _ = setTopLeftCorner(p) } - func unhideViaEmulation() { + func unhideFromCorner() { guard let prevUnhiddenEmulationPositionRelativeToWorkspaceAssignedRect else { return } guard let workspace else { return } // hiding only makes sense for workspace windows diff --git a/Sources/AppBundle/util/util.swift b/Sources/AppBundle/util/util.swift index 33a19d5c..0792c988 100644 --- a/Sources/AppBundle/util/util.swift +++ b/Sources/AppBundle/util/util.swift @@ -45,10 +45,6 @@ private func makeAllWindowsVisibleAndRestoreSize() { } } -var allMonitorsRectsUnion: Rect { - monitors.map(\.rect).union() -} - extension String? { var isNilOrEmpty: Bool { self == nil || self?.isEmpty == true } } diff --git a/docs/assets/monitor-arrangement-1-bad.svg b/docs/assets/monitor-arrangement-1-bad.svg new file mode 100644 index 00000000..d57efc4b --- /dev/null +++ b/docs/assets/monitor-arrangement-1-bad.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/monitor-arrangement-1-good.svg b/docs/assets/monitor-arrangement-1-good.svg new file mode 100644 index 00000000..6fed2339 --- /dev/null +++ b/docs/assets/monitor-arrangement-1-good.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/monitor-arrangement-2-bad.svg b/docs/assets/monitor-arrangement-2-bad.svg new file mode 100644 index 00000000..019fa138 --- /dev/null +++ b/docs/assets/monitor-arrangement-2-bad.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/monitor-arrangement-2-good.svg b/docs/assets/monitor-arrangement-2-good.svg new file mode 100644 index 00000000..b38541d2 --- /dev/null +++ b/docs/assets/monitor-arrangement-2-good.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/docs/guide.adoc b/docs/guide.adoc index ca0852eb..76a84066 100644 --- a/docs/guide.adoc +++ b/docs/guide.adoc @@ -340,18 +340,41 @@ Native macOS Spaces have a lot of problems * Apple doesn't provide public API to communicate with Spaces (create/delete/reorder/switch Space and move windows between Spaces) Since Spaces are so hard to deal with, AeroSpace reimplements Spaces and calls them "Workspaces". -The idea is that if the workspace isn’t active then all of its windows are placed outside the visible area of the screen, in the bottom right corner. +The idea is that if the workspace isn’t active then all of its windows are placed outside the visible area of the screen, in the bottom right or left corner. Once you switch back to the workspace, (e.g. by the means of xref:commands.adoc#workspace[workspace] command, or `cmd + tab`) windows are placed back to the visible area of the screen. -When you quit the AeroSpace or when the AeroSpace is about to crash, AeroSpace will place all windows back to the visible area of the screen. +When you quit the AeroSpace or when the AeroSpace detects that it's about to crash, AeroSpace will place all windows back to the visible area of the screen. AeroSpace shows the name of currently active workspace in its tray icon (top right corner), to give users a visual feedback on what workspace is currently active. -NOTE: For better or worse, macOS doesn’t allow to place windows outside the visible area entirely. -You will still be able to see a few pixels of "hidden" windows in the bottom right corner of your screen. +The intended workflow of using AeroSpace workspaces is to only have one macOS Space (or as many monitors you have, if `Displays have separate Spaces` is enabled) and don’t interact with macOS Spaces anymore. + +[NOTE] +==== +For better or worse, macOS doesn’t allow to place windows outside the visible area entirely. +You will still be able to see a 1 pixel vertical line of "hidden" windows in the bottom right or left corner of your screen. That means, that if AeroSpace crashes badly you will still be able to manually "unhide" the windows by dragging these few pixels to the center of the screen. -The intended workflow of using AeroSpace workspaces is to only have one macOS Space (or as many monitors you have, if `Displays have separate Spaces` is enabled) and don’t interact with macOS Spaces anymore. +If you want to minimize the visibility of hidden windows, it's recommended to place Dock in the bottom (and additionaly turn automatic hiding) +==== + + +=== Proper monitor arrangement + +Since AeroSpace needs a free space to hide windows in, +please make sure to arrange monitors in a way where *every monitor has free space in the bottom right or left corner.* (`System Settings -> Displays -> Arrange...`) + +.Bad monitor arrangement. Monitor 2 doesn't have free space in either of the bottom corners +image::./assets/monitor-arrangement-1-bad.svg[,,align="center"] + +.Good monitor arrangement. Every monitor has free space in either of the bottom corners +image::./assets/monitor-arrangement-1-good.svg[,,align="center"] + +.Bad monitor arrangement. Monitor 1 doesn't have free space in either of the bottom corners +image::./assets/monitor-arrangement-2-bad.svg[,,align="center"] + +.Good monitor arrangement. Every monitor has free space in either of the bottom corners +image::./assets/monitor-arrangement-2-good.svg[,,align="center"] [#a-note-on-mission-control] === A note on mission control @@ -359,33 +382,59 @@ The intended workflow of using AeroSpace workspaces is to only have one macOS Sp For some reason, mission control doesn't like that AeroSpace puts a lot of windows in the bottom right corner of the screen. Mission control shows windows too small even there is enough space to show them bigger. -To workaround, you can enable `System Settings -> Desktop & Dock -> Group windows by application` setting. -For some weird reason, it helps. +There is a workaround. You can enable `Group windows by application` setting: +[source,bash] +---- +defaults write com.apple.dock "expose-group-apps" -bool "true" && killall Dock +---- +(or in System Settings: `System Settings -> Desktop & Dock -> Group windows by application`). For whatever weird reason, it helps. [#a-note-on-displays-have-separate-spaces] === A note on '`Displays have separate Spaces`' -AeroSpace doesn't care about `System Settings -> Desktop & Dock -> Displays have separate Spaces` setting. -It works equally good whether this option is enabled or disabled. +My opinion is that macOS works better and more stable if you disable `Displays have separate Spaces`. (It's enabled by default) +People report all sorts of weird issues related to focus and performance when this setting is enabled: + +* Wrong window may receive focus in multi monitor setup: https://github.com/nikitabobko/AeroSpace/issues/101[#101] (Bug in Apple API) +* Wrong borderless Alacritty window may receive focus in *single monitor* setup: https://github.com/nikitabobko/AeroSpace/issues/247[#247] (Bug in Apple API) +* Performance issues: https://github.com/nikitabobko/AeroSpace/issues/333[#333] +* macOS randomly switches focus back: https://github.com/nikitabobko/AeroSpace/issues/289[#289] -Overview of '`Displays have separate Spaces`' +When `Displays have separate Spaces` is enabled, +moving windows between monitors causes windows to move between different Spaces which is not correctly handled by the public APIs AeroSpace uses, +apparently, these APIs are not aware about Spaces existence. +Spaces are just cursed in macOS. The less Spaces you have, the better macOS behaves. |=== | |'`Displays have separate Spaces`' is enabled |'`Displays have separate Spaces`' is disabled +|Is it possible for window to span across several monitors? +|😡 No. macOS limitation +|👍 Yes + +|Overal stability and performance +|😡 Weird focus and performance issues may happen (see the list above) +|👍 Public Apple API are more stable (which in turn affects AeroSpace stability) + |When the first monitor is in fullscreen |👍 Second monitor operates independently |😡 Second monitor is unusable black screen -|Is it possible for window to span across several monitors? -|😡 No -|👍 Yes - |macOS status bar ... |... is displayed on both monitors |... is displayed only on main monitor |=== +If you don't care about macOS native fullscreen in multi monitor setup (which is itself clunky anyway, since it creates a separate Space instance), +I recommend disabling `Displays have separate Spaces`. + +You can disable the setting by running: +[source,bash] +---- +defaults write com.apple.spaces "spans-displays" -bool "true" && killall SystemUIServer +---- +(or in System Settings: `System Settings -> Desktop & Dock -> Displays have separate Spaces`). Logout is required for the setting to take effect. + == Callbacks [#on-window-detected-callback]