Skip to content

Commit

Permalink
4/n Support macOS minimized and hidden apps
Browse files Browse the repository at this point in the history
  • Loading branch information
nikitabobko committed Feb 17, 2024
1 parent 2f807a0 commit 186c748
Show file tree
Hide file tree
Showing 17 changed files with 198 additions and 108 deletions.
4 changes: 4 additions & 0 deletions AeroSpace.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
93D44EA41776738B4758C28D /* util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 976540EBEACF846D598CD6E1 /* util.swift */; };
96FB31F48574713963A45D25 /* testExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8177AB37FF2FCA122C14AF /* testExtensions.swift */; };
991943D50DF9EDBF321A66F1 /* SelectorComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1316FFED5D1044CB693EA45 /* SelectorComparator.swift */; };
997742E8C68FA29868946181 /* MacosInvisibleWindowsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB47C43405542756E2EA1B19 /* MacosInvisibleWindowsContainer.swift */; };
9A138A729245BD2723148583 /* focused.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61FE8B343B068F0FFFC2373 /* focused.swift */; };
9D34BD7DE311254BF52F5EA2 /* testUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77C03164B8BFD1E59779C6E /* testUtil.swift */; };
9FAD09BC5CC390167106563B /* EnableCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 532EA400D1CFBC5C71663FDF /* EnableCommand.swift */; };
Expand Down Expand Up @@ -208,6 +209,7 @@
E761155C73F06E2CF5E292A4 /* FocusCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusCommand.swift; sourceTree = "<group>"; };
E9589EFDEBA4EB9C7DBAFCFD /* MoveNodeToWorkspaceCommandTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveNodeToWorkspaceCommandTest.swift; sourceTree = "<group>"; };
E9FCB66B928701F68AD8CA78 /* ListWindowsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListWindowsCommand.swift; sourceTree = "<group>"; };
EB47C43405542756E2EA1B19 /* MacosInvisibleWindowsContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacosInvisibleWindowsContainer.swift; sourceTree = "<group>"; };
EC2F56249A233EC9806D0F08 /* Bridged-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Bridged-Header.h"; sourceTree = "<group>"; };
EED7EE20910D7BE4D0150CED /* WorkspaceBackAndForthCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceBackAndForthCommand.swift; sourceTree = "<group>"; };
EEDBFFCA7A77D96B18FB0732 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
Expand Down Expand Up @@ -274,6 +276,7 @@
84AB57B5B8014931BD0F30DB /* AbstractApp.swift */,
DAC5AF4019D1F5DAB5AF2B56 /* layoutRecursive.swift */,
B7DB782C527ABE0CF31740EB /* MacApp.swift */,
EB47C43405542756E2EA1B19 /* MacosInvisibleWindowsContainer.swift */,
9245C6FACF389672EA71173B /* MacWindow.swift */,
C704028F402C0DE83EDEABB9 /* normalizeContainers.swift */,
C848D6E57FDF22AAF0FB45E6 /* TilingContainer.swift */,
Expand Down Expand Up @@ -638,6 +641,7 @@
3AFD7EE961B97F38C0914A0C /* ListWorkspacesCommand.swift in Sources */,
BD6301B2CFC16FDE4223ACB8 /* MacApp.swift in Sources */,
EECC59858691B99A95542D72 /* MacWindow.swift in Sources */,
997742E8C68FA29868946181 /* MacosInvisibleWindowsContainer.swift in Sources */,
DEE74487CFB70CEC10D9D3D7 /* MacosNativeFullscreenCommand.swift in Sources */,
7C0ACF53062631B667424927 /* MacosNativeMinimizeCommand.swift in Sources */,
6E4E235FDA41307B19F16182 /* ModeCommand.swift in Sources */,
Expand Down
7 changes: 7 additions & 0 deletions LocalPackage/Sources/Common/util/OptionalEx.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,11 @@ public extension Optional {
return []
}
}

func optionalToPrettyString() -> String {
if let unwrapped = self {
return String(describing: unwrapped)
}
return "nil"
}
}
2 changes: 2 additions & 0 deletions src/command/FocusCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ private extension TreeNode {
} else {
return mostRecentChild?.findFocusTargetRecursive(snappedTo: direction)
}
case .macosInvisibleWindowsContainer:
error("Impossible")
}
}
}
41 changes: 22 additions & 19 deletions src/command/LayoutCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,57 +12,60 @@ struct LayoutCommand: Command {
let targetDescription = args.toggleBetween.val.first(where: { !window.matchesDescription($0) })
?? args.toggleBetween.val.first!
if window.matchesDescription(targetDescription) { return false }
var result = true
switch targetDescription {
case .h_accordion:
result = changeTilingLayout(targetLayout: .accordion, targetOrientation: .h, window: window)
return changeTilingLayout(state, targetLayout: .accordion, targetOrientation: .h, window: window)
case .v_accordion:
result = changeTilingLayout(targetLayout: .accordion, targetOrientation: .v, window: window)
return changeTilingLayout(state, targetLayout: .accordion, targetOrientation: .v, window: window)
case .h_tiles:
result = changeTilingLayout(targetLayout: .tiles, targetOrientation: .h, window: window)
return changeTilingLayout(state, targetLayout: .tiles, targetOrientation: .h, window: window)
case .v_tiles:
result = changeTilingLayout(targetLayout: .tiles, targetOrientation: .v, window: window)
return changeTilingLayout(state, targetLayout: .tiles, targetOrientation: .v, window: window)
case .accordion:
result = changeTilingLayout(targetLayout: .accordion, targetOrientation: nil, window: window)
return changeTilingLayout(state, targetLayout: .accordion, targetOrientation: nil, window: window)
case .tiles:
result = changeTilingLayout(targetLayout: .tiles, targetOrientation: nil, window: window)
return changeTilingLayout(state, targetLayout: .tiles, targetOrientation: nil, window: window)
case .horizontal:
result = changeTilingLayout(targetLayout: nil, targetOrientation: .h, window: window)
return changeTilingLayout(state, targetLayout: nil, targetOrientation: .h, window: window)
case .vertical:
result = changeTilingLayout(targetLayout: nil, targetOrientation: .v, window: window)
return changeTilingLayout(state, targetLayout: nil, targetOrientation: .v, window: window)
case .tiling:
switch window.parent.cases {
case .macosInvisibleWindowsContainer:
state.stderr.append("Can't change layout of macOS invisible windows (hidden application or minimized windows). This behavior is subject to change")
return false
case .tilingContainer:
return true // Nothing to do
case .workspace:
window.lastFloatingSize = window.getSize() ?? window.lastFloatingSize
case .tilingContainer:
error("Impossible")
let data = getBindingDataForNewTilingWindow(window.unbindFromParent().parent.workspace)
window.bind(to: data.parent, adaptiveWeight: data.adaptiveWeight, index: data.index)
return true
}
let data = getBindingDataForNewTilingWindow(window.unbindFromParent().parent.workspace)
window.bind(to: data.parent, adaptiveWeight: data.adaptiveWeight, index: data.index)
case .floating:
let workspace = window.unbindFromParent().parent.workspace
window.bindAsFloatingWindow(to: workspace)
guard let topLeftCorner = window.getTopLeftCorner() else { break }
guard let topLeftCorner = window.getTopLeftCorner() else { return false }
let offset = CGPoint(
x: abs(topLeftCorner.x - workspace.monitor.rect.topLeftX).takeIf { $0 < 30 } ?? 0,
y: abs(topLeftCorner.y - workspace.monitor.rect.topLeftY).takeIf { $0 < 30 } ?? 0
)
window.setFrame(topLeftCorner + offset, window.lastFloatingSize)
return window.setFrame(topLeftCorner + offset, window.lastFloatingSize)
}
return result
}
}

private func changeTilingLayout(targetLayout: Layout?, targetOrientation: Orientation?, window: Window) -> Bool {
private func changeTilingLayout(_ state: CommandMutableState, targetLayout: Layout?, targetOrientation: Orientation?, window: Window) -> Bool {
switch window.parent.cases {
case .tilingContainer(let parent):
let targetOrientation = targetOrientation ?? parent.orientation
let targetLayout = targetLayout ?? parent.layout
parent.layout = targetLayout
parent.changeOrientation(targetOrientation)
return true
case .workspace:
return false // Do nothing for non-tiling windows
case .workspace, .macosInvisibleWindowsContainer:
state.stderr.append("The window is non-tiling")
return false
}
}

Expand Down
43 changes: 24 additions & 19 deletions src/command/MoveCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,33 @@ struct MoveCommand: Command {
let indexOfCurrent = currentWindow.ownIndex
let indexOfSiblingTarget = indexOfCurrent + direction.focusOffset
if parent.orientation == direction.orientation && parent.children.indices.contains(indexOfSiblingTarget) {
switch parent.children[indexOfSiblingTarget].nodeCases {
switch parent.children[indexOfSiblingTarget].nonRootTreeNodeCasesOrThrow() {
case .tilingContainer(let topLevelSiblingTargetContainer):
deepMoveIn(window: currentWindow, into: topLevelSiblingTargetContainer, moveDirection: direction)
case .window: // "swap windows"
let prevBinding = currentWindow.unbindFromParent()
currentWindow.bind(to: parent, adaptiveWeight: prevBinding.adaptiveWeight, index: indexOfSiblingTarget)
case .workspace:
error("Impossible")
}
return true
} else {
moveOut(window: currentWindow, direction: direction)
return moveOut(state, window: currentWindow, direction: direction)
}
case .workspace: // floating window
break // todo support moving floating windows
state.stderr.append("moving floating windows isn't yet supported") // todo
return false
case .macosInvisibleWindowsContainer(_):
state.stderr.append(moveOutInvisibleWindow)
return false
}
return true
}
}

private func moveOut(window: Window, direction: CardinalDirection) {
private let moveOutInvisibleWindow = "moving macOS invisible windows (minimized, or windows of hidden apps) isn't yet supported. This behavior is subject to change"

private func moveOut(_ state: CommandMutableState, window: Window, direction: CardinalDirection) -> Bool {
let innerMostChild = window.parents.first(where: {
switch $0.parent?.cases {
case .workspace, nil:
case .workspace, .macosInvisibleWindowsContainer, nil:
return true // Stop searching
case .tilingContainer(let parent):
return parent.orientation == direction.orientation
Expand All @@ -60,6 +64,9 @@ private func moveOut(window: Window, direction: CardinalDirection) {

bindTo = parent.rootTilingContainer
bindToIndex = direction.insertionOffset
case .macosInvisibleWindowsContainer:
state.stderr.append(moveOutInvisibleWindow)
return false
case .window:
error("Window can't contain children nodes")
}
Expand All @@ -70,11 +77,12 @@ private func moveOut(window: Window, direction: CardinalDirection) {
adaptiveWeight: WEIGHT_AUTO,
index: bindToIndex
)
return true
}

private func deepMoveIn(window: Window, into container: TilingContainer, moveDirection: CardinalDirection) {
let deepTarget = container.findDeepMoveInTargetRecursive(moveDirection.orientation)
switch deepTarget.nodeCases {
let deepTarget = container.nonRootTreeNodeCasesOrThrow().findDeepMoveInTargetRecursive(moveDirection.orientation)
switch deepTarget {
case .tilingContainer(let deepTarget):
window.unbindFromParent()
window.bind(to: deepTarget, adaptiveWeight: WEIGHT_AUTO, index: 0)
Expand All @@ -85,25 +93,22 @@ private func deepMoveIn(window: Window, into container: TilingContainer, moveDir
adaptiveWeight: WEIGHT_AUTO,
index: deepTarget.ownIndex + 1
)
case .workspace:
error("Impossible")
}
}

private extension TreeNode {
func findDeepMoveInTargetRecursive(_ orientation: Orientation) -> TreeNode {
switch nodeCases {
private extension NonRootTreeNodeCases {
func findDeepMoveInTargetRecursive(_ orientation: Orientation) -> NonRootTreeNodeCases {
switch self {
case .window:
return self
case .tilingContainer(let container):
if container.orientation == orientation {
return container
return .tilingContainer(container)
} else {
return (mostRecentChild ?? errorT("Empty containers must be detached during normalization"))
return (container.mostRecentChild ?? errorT("Empty containers must be detached during normalization"))
.nonRootTreeNodeCasesOrThrow()
.findDeepMoveInTargetRecursive(orientation)
}
case .workspace:
error("Impossible")
}
}
}
8 changes: 6 additions & 2 deletions src/command/SplitCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ struct SplitCommand: Command {
}
switch window.parent.cases {
case .workspace:
return false // Nothing to do for floating windows
state.stderr.append("Can't split floating windows")
return false // Nothing to do for floating and macOS native fullscreen windows
case .tilingContainer(let parent):
let orientation: Orientation
switch args.arg.val {
Expand All @@ -39,7 +40,10 @@ struct SplitCommand: Command {
)
window.bind(to: newParent, adaptiveWeight: WEIGHT_AUTO, index: 0)
}
return true
case .macosInvisibleWindowsContainer:
state.stderr.append("Can't split invisible windows (minimized windows or windows of hidden apps)")
return false
}
return true
}
}
6 changes: 4 additions & 2 deletions src/mouse/moveWithMouse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ private func moveWithMouseIfTheCase(_ window: Window) { // todo cover with tests
moveFloatingWindow(window)
case .tilingContainer:
moveTilingWindow(window)
case .macosInvisibleWindowsContainer:
return // Invisible window can't be moved with mouse
}
}

Expand Down Expand Up @@ -85,8 +87,8 @@ extension CGPoint {
case .accordion:
target = tree.mostRecentChild
}
switch target?.nodeCases {
case nil, .workspace:
switch target?.nonRootTreeNodeCasesOrThrow() {
case nil:
return nil
case .window(let window):
return window
Expand Down
4 changes: 2 additions & 2 deletions src/mouse/resizeWithMouse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ private func resizeWithMouseIfTheCase(_ window: Window) { // todo cover with tes
return
}
switch window.parent.cases {
case .workspace:
return // Nothing to do for floating windows
case .workspace, .macosInvisibleWindowsContainer:
return // Nothing to do for floating or invisible windows
case .tilingContainer:
guard let rect = window.getRect() else { return }
guard let lastAppliedLayoutRect = window.lastAppliedLayoutPhysicalRect else { return }
Expand Down
9 changes: 9 additions & 0 deletions src/refresh.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,17 @@ private func normalizeLayoutReason() {
let workspace = Workspace.focused
for window in workspace.allLeafWindowsRecursive {
let isMacosFullscreen = window.isMacosFullscreen
let isMacosInvisible = !isMacosFullscreen && (window.isMacosMinimized || window.macAppUnsafe.nsApp.isHidden)
if isMacosFullscreen && !window.layoutReason.isMacos {
window.layoutReason = .macos(prevParentKind: window.parent.kind)
window.unbindFromParent()
window.bindAsFloatingWindow(to: workspace)
}
if isMacosInvisible && !window.layoutReason.isMacos {
window.layoutReason = .macos(prevParentKind: window.parent.kind)
window.unbindFromParent()
window.bind(to: macosInvisibleWindowsContainer, adaptiveWeight: 1, index: INDEX_BIND_LAST)
}
if case .macos(let prevParentKind) = window.layoutReason, !isMacosFullscreen {
window.layoutReason = .standard
window.unbindFromParent()
Expand All @@ -90,6 +96,9 @@ private func normalizeLayoutReason() {
case .tilingContainer:
let data = getBindingDataForNewTilingWindow(workspace)
window.bind(to: data.parent, adaptiveWeight: data.adaptiveWeight, index: data.index)
case .macosInvisibleWindowsContainer: // wtf case, should never be possible. But If encounter it, let's just re-layout window
let data = getBindingDataForNewWindow(window.asMacWindow().axWindow, workspace, window.macAppUnsafe)
window.bind(to: data.parent, adaptiveWeight: data.adaptiveWeight, index: data.index)
}
}
}
Expand Down
13 changes: 7 additions & 6 deletions src/tree/MacWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,18 +123,18 @@ final class MacWindow: Window, CustomStringConvertible {
prevUnhiddenEmulationPositionRelativeToWorkspaceAssignedRect != nil
}

override func setSize(_ size: CGSize) {
override func setSize(_ size: CGSize) -> Bool {
disableAnimations {
previousSize = getSize()
axWindow.set(Ax.sizeAttr, size)
return axWindow.set(Ax.sizeAttr, size)
}
}

override func getSize() -> CGSize? {
axWindow.get(Ax.sizeAttr)
}

override func setTopLeftCorner(_ point: CGPoint) {
override func setTopLeftCorner(_ point: CGPoint) -> Bool {
disableAnimations {
axWindow.set(Ax.topLeftCornerAttr, point)
}
Expand All @@ -153,16 +153,17 @@ final class MacWindow: Window, CustomStringConvertible {
// Some undocumented magic
// References: https://github.com/koekeishiya/yabai/commit/3fe4c77b001e1a4f613c26f01ea68c0f09327f3a
// https://github.com/rxhanson/Rectangle/pull/285
private func disableAnimations(_ body: () -> Void) {
private func disableAnimations<T>(_ body: () -> T) -> T {
let app = (app as! MacApp).axApp
let wasEnabled = app.get(Ax.enhancedUserInterfaceAttr) == true
if wasEnabled {
app.set(Ax.enhancedUserInterfaceAttr, false)
}
body()
let result = body()
if wasEnabled {
app.set(Ax.enhancedUserInterfaceAttr, true)
}
return result
}
}

Expand Down Expand Up @@ -226,7 +227,7 @@ private func isFullscreenable(_ axWindow: AXUIElement) -> Bool {
return false
}

private func getBindingDataForNewWindow(_ axWindow: AXUIElement, _ workspace: Workspace, _ app: MacApp) -> BindingData {
func getBindingDataForNewWindow(_ axWindow: AXUIElement, _ workspace: Workspace, _ app: MacApp) -> BindingData {
shouldFloat(axWindow, app)
? BindingData(parent: workspace as NonLeafTreeNodeObject, adaptiveWeight: WEIGHT_AUTO, index: INDEX_BIND_LAST)
: getBindingDataForNewTilingWindow(workspace)
Expand Down
10 changes: 10 additions & 0 deletions src/tree/MacosInvisibleWindowsContainer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Common

/// The container for macOS minimized windows and windows of hidden applications
class MacosInvisibleWindowsContainer: TreeNode, NonLeafTreeNodeObject {
fileprivate init() {
super.init(parent: NilTreeNode.instance, adaptiveWeight: 1, index: INDEX_BIND_LAST)
}
}

let macosInvisibleWindowsContainer = MacosInvisibleWindowsContainer()
Loading

0 comments on commit 186c748

Please sign in to comment.