diff --git a/AeroSpace.xcodeproj/project.pbxproj b/AeroSpace.xcodeproj/project.pbxproj index 41398005..405b5998 100644 --- a/AeroSpace.xcodeproj/project.pbxproj +++ b/AeroSpace.xcodeproj/project.pbxproj @@ -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 */; }; @@ -208,6 +209,7 @@ E761155C73F06E2CF5E292A4 /* FocusCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusCommand.swift; sourceTree = ""; }; E9589EFDEBA4EB9C7DBAFCFD /* MoveNodeToWorkspaceCommandTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveNodeToWorkspaceCommandTest.swift; sourceTree = ""; }; E9FCB66B928701F68AD8CA78 /* ListWindowsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListWindowsCommand.swift; sourceTree = ""; }; + EB47C43405542756E2EA1B19 /* MacosInvisibleWindowsContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacosInvisibleWindowsContainer.swift; sourceTree = ""; }; EC2F56249A233EC9806D0F08 /* Bridged-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Bridged-Header.h"; sourceTree = ""; }; EED7EE20910D7BE4D0150CED /* WorkspaceBackAndForthCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceBackAndForthCommand.swift; sourceTree = ""; }; EEDBFFCA7A77D96B18FB0732 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -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 */, @@ -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 */, diff --git a/LocalPackage/Sources/Common/util/OptionalEx.swift b/LocalPackage/Sources/Common/util/OptionalEx.swift index 38904a8e..a9589264 100644 --- a/LocalPackage/Sources/Common/util/OptionalEx.swift +++ b/LocalPackage/Sources/Common/util/OptionalEx.swift @@ -16,4 +16,11 @@ public extension Optional { return [] } } + + func optionalToPrettyString() -> String { + if let unwrapped = self { + return String(describing: unwrapped) + } + return "nil" + } } diff --git a/src/command/FocusCommand.swift b/src/command/FocusCommand.swift index b487c173..dd9f8372 100644 --- a/src/command/FocusCommand.swift +++ b/src/command/FocusCommand.swift @@ -162,6 +162,8 @@ private extension TreeNode { } else { return mostRecentChild?.findFocusTargetRecursive(snappedTo: direction) } + case .macosInvisibleWindowsContainer: + error("Impossible") } } } diff --git a/src/command/LayoutCommand.swift b/src/command/LayoutCommand.swift index ce10adba..cad9a0cd 100644 --- a/src/command/LayoutCommand.swift +++ b/src/command/LayoutCommand.swift @@ -12,48 +12,50 @@ 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 @@ -61,8 +63,9 @@ private func changeTilingLayout(targetLayout: Layout?, targetOrientation: Orient 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 } } diff --git a/src/command/MoveCommand.swift b/src/command/MoveCommand.swift index 4265b31b..bb1dbda0 100644 --- a/src/command/MoveCommand.swift +++ b/src/command/MoveCommand.swift @@ -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 @@ -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") } @@ -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) @@ -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") } } } diff --git a/src/command/SplitCommand.swift b/src/command/SplitCommand.swift index a327f4bc..99f7ce89 100644 --- a/src/command/SplitCommand.swift +++ b/src/command/SplitCommand.swift @@ -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 { @@ -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 } } diff --git a/src/mouse/moveWithMouse.swift b/src/mouse/moveWithMouse.swift index 7a67f4d9..df448123 100644 --- a/src/mouse/moveWithMouse.swift +++ b/src/mouse/moveWithMouse.swift @@ -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 } } @@ -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 diff --git a/src/mouse/resizeWithMouse.swift b/src/mouse/resizeWithMouse.swift index 84e91426..a0f346d2 100644 --- a/src/mouse/resizeWithMouse.swift +++ b/src/mouse/resizeWithMouse.swift @@ -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 } diff --git a/src/refresh.swift b/src/refresh.swift index 25303208..588443b1 100644 --- a/src/refresh.swift +++ b/src/refresh.swift @@ -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() @@ -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) } } } diff --git a/src/tree/MacWindow.swift b/src/tree/MacWindow.swift index ba3d1496..89703230 100644 --- a/src/tree/MacWindow.swift +++ b/src/tree/MacWindow.swift @@ -123,10 +123,10 @@ 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) } } @@ -134,7 +134,7 @@ final class MacWindow: Window, CustomStringConvertible { axWindow.get(Ax.sizeAttr) } - override func setTopLeftCorner(_ point: CGPoint) { + override func setTopLeftCorner(_ point: CGPoint) -> Bool { disableAnimations { axWindow.set(Ax.topLeftCornerAttr, point) } @@ -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(_ 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 } } @@ -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) diff --git a/src/tree/MacosInvisibleWindowsContainer.swift b/src/tree/MacosInvisibleWindowsContainer.swift new file mode 100644 index 00000000..b3dcc834 --- /dev/null +++ b/src/tree/MacosInvisibleWindowsContainer.swift @@ -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() diff --git a/src/tree/TreeNode.swift b/src/tree/TreeNode.swift index 16a55cdb..9f5c4cda 100644 --- a/src/tree/TreeNode.swift +++ b/src/tree/TreeNode.swift @@ -27,38 +27,33 @@ class TreeNode: Equatable { /// See: ``getWeight(_:)`` func setWeight(_ targetOrientation: Orientation, _ newValue: CGFloat) { - switch parent?.cases { - case .tilingContainer(let parent): + guard let parent else { error("Can't change weight if TreeNode doesn't have parent") } + switch getChildParentRelation(child: self, parent: parent) { + case .tiling(let parent): if parent.orientation != targetOrientation { error("You can't change \(targetOrientation) weight of nodes located in \(parent.orientation) container") } if parent.layout != .tiles { - error("Weight can be changed only for nodes whose parent is list") + error("Weight can be changed only for nodes whose parent has 'tiles' layout") } adaptiveWeight = newValue - case .workspace: - error("Can't change weight for floating windows and workspace root containers") - case nil: - error("Can't change weight if TreeNode doesn't have parent") + default: + error("Can't change weight") } } /// Weight itself doesn't make sense. The parent container controls semantics of weight func getWeight(_ targetOrientation: Orientation) -> CGFloat { - switch parent?.cases { - case .tilingContainer(let parent): + guard let parent else { error("Weight doesn't make sense for containers without parent") } + switch getChildParentRelation(child: self, parent: parent) { + case .floatingWindow, .macosNativeFullscreenWindow: + error("Weight doesn't make sense for floating windows") + case .macosNativeInvisibleWindow: + error("Weight doesn't make sense for invisible windows") + case .tiling(let parent): return parent.orientation == targetOrientation ? adaptiveWeight : parent.getWeight(targetOrientation) - case .workspace(let parent): - switch nodeCases { - case .window: // self is a floating window - error("Weight doesn't make sense for floating windows") - case .tilingContainer: // root tiling container - return parent.getWeight(targetOrientation) - case .workspace: - error("Workspaces can't be child") - } - case nil: - error("Weight doesn't make sense for containers without parent") + case .rootTilingContainer: + return parent.getWeight(targetOrientation) } } @@ -76,20 +71,15 @@ class TreeNode: Equatable { return result } if adaptiveWeight == WEIGHT_AUTO { - switch newParent.cases { - case .tilingContainer(let newParent): + switch getChildParentRelation(child: self, parent: newParent) { + case .floatingWindow, .macosNativeFullscreenWindow: + self.adaptiveWeight = WEIGHT_FLOATING + case .tiling(let newParent): self.adaptiveWeight = newParent.children.sumOf { $0.getWeight(newParent.orientation) } .div(newParent.children.count) ?? 1 - case .workspace: - switch nodeCases { - case .window: - self.adaptiveWeight = WEIGHT_FLOATING - case .tilingContainer: - self.adaptiveWeight = 1 - case .workspace: - error("Binding workspace to workspace is illegal") - } + case .rootTilingContainer, .macosNativeInvisibleWindow: + self.adaptiveWeight = 1 } } else { self.adaptiveWeight = adaptiveWeight diff --git a/src/tree/TreeNodeCases.swift b/src/tree/TreeNodeCases.swift index 040c4a1d..6d0bbab9 100644 --- a/src/tree/TreeNodeCases.swift +++ b/src/tree/TreeNodeCases.swift @@ -4,16 +4,24 @@ enum TreeNodeCases { case window(Window) case tilingContainer(TilingContainer) case workspace(Workspace) + case macosInvisibleWindowsContainer(MacosInvisibleWindowsContainer) } enum NonLeafTreeNodeCases { case tilingContainer(TilingContainer) case workspace(Workspace) + case macosInvisibleWindowsContainer(MacosInvisibleWindowsContainer) +} + +enum NonRootTreeNodeCases { + case window(Window) + case tilingContainer(TilingContainer) } enum NonLeafTreeNodeKind: Equatable { case tilingContainer case workspace + case macosInvisibleWindowsContainer } protocol NonLeafTreeNodeObject: TreeNode {} @@ -26,10 +34,22 @@ extension TreeNode { return .workspace(workspace) } else if let tilingContainer = self as? TilingContainer { return .tilingContainer(tilingContainer) + } else if let container = self as? MacosInvisibleWindowsContainer { + return .macosInvisibleWindowsContainer(container) } else { error("Unknown tree") } } + + func nonRootTreeNodeCasesOrThrow() -> NonRootTreeNodeCases { + if let window = self as? Window { + return .window(window) + } else if let tilingContainer = self as? TilingContainer { + return .tilingContainer(tilingContainer) + } else { + illegalChildParentRelation(child: self, parent: parent) + } + } } extension NonLeafTreeNodeObject { @@ -40,6 +60,8 @@ extension NonLeafTreeNodeObject { return .workspace(workspace) } else if let tilingContainer = self as? TilingContainer { return .tilingContainer(tilingContainer) + } else if let container = self as? MacosInvisibleWindowsContainer { + return .macosInvisibleWindowsContainer(container) } else { error("Unknown tree") } @@ -51,6 +73,47 @@ extension NonLeafTreeNodeObject { return .tilingContainer case .workspace: return .workspace + case .macosInvisibleWindowsContainer: + return .macosInvisibleWindowsContainer } } } + +enum ChildParentRelation: Equatable { + case floatingWindow + case macosNativeFullscreenWindow + case macosNativeInvisibleWindow + case tiling(parent: TilingContainer) // todo consider splitting it on 'tiles' and 'accordion' + case rootTilingContainer +} + +func getChildParentRelation(child: TreeNode, parent: NonLeafTreeNodeObject) -> ChildParentRelation { + if let relation = getChildParentRelationOrNil(child: child, parent: parent) { + return relation + } + illegalChildParentRelation(child: child, parent: parent) +} + +func illegalChildParentRelation(child: TreeNode, parent: NonLeafTreeNodeObject?) -> Never { + error("Illegal child-parent relation. Child: \(child), Parent: \((parent ?? child.parent).optionalToPrettyString())") +} + +func getChildParentRelationOrNil(child: TreeNode, parent: NonLeafTreeNodeObject) -> ChildParentRelation? { + switch (child.nodeCases, parent.cases) { + case (.workspace, _): + return nil + case (.window(let window), .workspace): + return window.isMacosFullscreen ? .macosNativeFullscreenWindow : .floatingWindow + case (.window, .macosInvisibleWindowsContainer): + return .macosNativeInvisibleWindow + case (_, .macosInvisibleWindowsContainer): + return nil + case (.tilingContainer, .tilingContainer(let container)), + (.window, .tilingContainer(let container)): + return .tiling(parent: container) + case (.tilingContainer, .workspace): + return .rootTilingContainer + case (.macosInvisibleWindowsContainer, _): + return nil + } +} diff --git a/src/tree/TreeNodeEx.swift b/src/tree/TreeNodeEx.swift index 8a596bcb..21753bf2 100644 --- a/src/tree/TreeNodeEx.swift +++ b/src/tree/TreeNodeEx.swift @@ -31,22 +31,6 @@ extension TreeNode { self as? Window ?? mostRecentChild?.mostRecentWindow } - func allLeafWindowsRecursive(snappedTo direction: CardinalDirection) -> [Window] { - switch nodeCases { - case .workspace(let workspace): - return workspace.rootTilingContainer.allLeafWindowsRecursive(snappedTo: direction) - case .window(let window): - return [window] - case .tilingContainer(let container): - if direction.orientation == container.orientation { - return (direction.isPositive ? container.children.last : container.children.first)? - .allLeafWindowsRecursive(snappedTo: direction) ?? [] - } else { - return children.flatMap { $0.allLeafWindowsRecursive(snappedTo: direction) } - } - } - } - var anyLeafWindowRecursive: Window? { if let window = self as? Window { return window @@ -83,21 +67,19 @@ extension TreeNode { ) -> (parent: TilingContainer, ownIndex: Int)? { let innermostChild = parentsWithSelf.first(where: { (node: TreeNode) -> Bool in switch node.parent?.cases { - case .workspace: - return true + case .workspace, nil, .macosInvisibleWindowsContainer: + return true // stop searching case .tilingContainer(let parent): return (layout == nil || parent.layout == layout) && parent.orientation == direction.orientation && parent.children.indices.contains(node.ownIndexOrNil! + direction.focusOffset) - case nil: - return true } })! switch innermostChild.parent?.cases { case .tilingContainer(let parent): check(parent.orientation == direction.orientation) return (parent, innermostChild.ownIndexOrNil!) - case .workspace, nil: + case .workspace, nil, .macosInvisibleWindowsContainer: return nil } } diff --git a/src/tree/Window.swift b/src/tree/Window.swift index d8eaef18..37adf152 100644 --- a/src/tree/Window.swift +++ b/src/tree/Window.swift @@ -30,14 +30,15 @@ class Window: TreeNode, Hashable { var isMacosFullscreen: Bool { false } var isMacosMinimized: Bool { false } var isHiddenViaEmulation: Bool { error("Not implemented") } - func setSize(_ size: CGSize) { error("Not implemented") } + func setSize(_ size: CGSize) -> Bool { error("Not implemented") } - func setTopLeftCorner(_ point: CGPoint) { error("Not implemented") } + func setTopLeftCorner(_ point: CGPoint) -> Bool { error("Not implemented") } } enum LayoutReason: Equatable { case standard - case macos(prevParentKind: NonLeafTreeNodeKind) // macOS native fullscreen, minimize, or hide + /// Reason for the cur temp layout is macOS native fullscreen, minimize, or hide + case macos(prevParentKind: NonLeafTreeNodeKind) var isMacos: Bool { if case .macos = self { @@ -64,10 +65,12 @@ extension Window { focusedWorkspaceName = workspace.name } - func setFrame(_ topLeft: CGPoint?, _ size: CGSize?) { + func setFrame(_ topLeft: CGPoint?, _ size: CGSize?) -> Bool { // Set size and then the position. The order is important https://github.com/nikitabobko/AeroSpace/issues/143 - if let size { setSize(size) } - if let topLeft { setTopLeftCorner(topLeft) } + var result: Bool = true + if let size { result = setSize(size) && result } + if let topLeft { result = setTopLeftCorner(topLeft) && result } + return result } func asMacWindow() -> MacWindow { self as! MacWindow } diff --git a/src/tree/layoutRecursive.swift b/src/tree/layoutRecursive.swift index e9041d07..3525b1a1 100644 --- a/src/tree/layoutRecursive.swift +++ b/src/tree/layoutRecursive.swift @@ -36,6 +36,8 @@ extension TreeNode { case .accordion: container.layoutAccordion(point, width: width, height: height, virtual: virtual) } + case .macosInvisibleWindowsContainer: + return // Nothing to do for invisible windows } } } diff --git a/test/command/MoveCommandTest.swift b/test/command/MoveCommandTest.swift index c439fa5b..815784c0 100644 --- a/test/command/MoveCommandTest.swift +++ b/test/command/MoveCommandTest.swift @@ -218,6 +218,8 @@ extension TreeNode { } case .workspace: return .workspace(workspace.children.map(\.layoutDescription)) + case .macosInvisibleWindowsContainer: + return .macosInvisible } } } @@ -229,4 +231,5 @@ enum LayoutDescription: Equatable { case h_accordion([LayoutDescription]) case v_accordion([LayoutDescription]) case window(UInt32) + case macosInvisible }