Skip to content

Commit

Permalink
5/n WIP! Convert TreeNode.workspace to nillable
Browse files Browse the repository at this point in the history
Because not all windows have an assigned workspace (macOS hidden
windows, sticky windows in the future)

#18
  • Loading branch information
nikitabobko committed Feb 17, 2024
1 parent 186c748 commit 47f51f1
Show file tree
Hide file tree
Showing 23 changed files with 114 additions and 71 deletions.
6 changes: 5 additions & 1 deletion src/command/CloseAllWindowsButCurrentCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ struct CloseAllWindowsButCurrentCommand: Command {
return false
}
var result = true
for window in focused.workspace.allLeafWindowsRecursive {
guard let workspace = focused.workspace else {
state.stderr.append("Focused window '\(focused.title)'")
return false
}
for window in workspace.allLeafWindowsRecursive {
if window != focused {
state.subject = .window(window)
result = CloseCommand(args: args.closeArgs).run(state) && result
Expand Down
2 changes: 1 addition & 1 deletion src/command/Command.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ extension CommandSubject {
var workspace: Workspace {
switch self {
case .window(let window):
return window.workspace
return window.nodeMonitor?.activeWorkspace ?? Workspace.focused
case .emptyWorkspace(let workspaceName):
return Workspace.get(byName: workspaceName)
}
Expand Down
2 changes: 1 addition & 1 deletion src/command/FocusCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ private func makeFloatingWindowsSeenAsTiling(workspace: Workspace) -> [FloatingW
let center = window.isMacosFullscreen ? workspace.monitor.rect.topLeftCorner : window.getCenter()
guard let center else { return nil }
// todo bug: what if there are no tiling windows on the workspace?
guard let target = center.coerceIn(rect: window.workspace.monitor.visibleRectPaddedByOuterGaps).findIn(tree: workspace.rootTilingContainer, virtual: true) else { return nil }
guard let target = center.coerceIn(rect: workspace.monitor.visibleRectPaddedByOuterGaps).findIn(tree: workspace.rootTilingContainer, virtual: true) else { return nil }
guard let targetCenter = target.getCenter() else { return nil }
guard let tilingParent = target.parent as? TilingContainer else { return nil }
let index = center.getProjection(tilingParent.orientation) >= targetCenter.getProjection(tilingParent.orientation)
Expand Down
14 changes: 6 additions & 8 deletions src/command/LayoutCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,19 @@ struct LayoutCommand: Command {
return false
case .tilingContainer:
return true // Nothing to do
case .workspace:
case .workspace(let workspace):
window.lastFloatingSize = window.getSize() ?? window.lastFloatingSize
let data = getBindingDataForNewTilingWindow(window.unbindFromParent().parent.workspace)
window.unbindFromParent()
let data = getBindingDataForNewTilingWindow(workspace)
window.bind(to: data.parent, adaptiveWeight: data.adaptiveWeight, index: data.index)
return true
}
case .floating:
let workspace = window.unbindFromParent().parent.workspace
let workspace = state.subject.workspace // Capture workspace before unbind ID-1A4CF7C5
window.unbindFromParent() // ID-1A4CF7C5
window.bindAsFloatingWindow(to: workspace)
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
)
return window.setFrame(topLeftCorner + offset, window.lastFloatingSize)
return window.setFrame(topLeftCorner, window.lastFloatingSize)
}
}
}
Expand Down
8 changes: 7 additions & 1 deletion src/command/MacosNativeFullscreenCommand.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import Common

struct MacosNativeFullscreenCommand: Command {
/// Problem B6E178F2: It's not first-class citizen command in AeroSpace model, since it interacts with macOS API directly.
/// Consecutive macos-native-fullscreen commands may not works as expected (because macOS may report correct state with a
/// delay), or may flicker
///
/// The same applies to macos-native-minimize command
struct MacosNativeFullscreenCommand: Command { // todo only allow as the latest command in sequence
let args: MacosNativeFullscreenCmdArgs

func _run(_ state: CommandMutableState, stdin: String) -> Bool {
Expand All @@ -12,6 +17,7 @@ struct MacosNativeFullscreenCommand: Command {
let axWindow = window.asMacWindow().axWindow
let success = axWindow.set(Ax.isFullscreenAttr, !window.isMacosFullscreen)
if !success { state.stderr.append("Failed") }
// todo attach or detach to appropriate parent
return success
}
}
1 change: 1 addition & 0 deletions src/command/MacosNativeMinimizeCommand.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Common

/// See: MacosNativeFullscreenCommand. Problem B6E178F2
struct MacosNativeMinimizeCommand: Command {
let args: MacosNativeMinimizeCmdArgs

Expand Down
2 changes: 1 addition & 1 deletion src/command/MoveCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ private func moveOut(_ state: CommandMutableState, window: Window, direction: Ca
case .workspace(let parent): // create implicit container
let prevRoot = parent.rootTilingContainer
prevRoot.unbindFromParent()
// Force list layout
// Force tiles layout
_ = TilingContainer(parent: parent, adaptiveWeight: WEIGHT_AUTO, direction.orientation, .tiles, index: 0)
check(prevRoot != parent.rootTilingContainer)
prevRoot.bind(to: parent.rootTilingContainer, adaptiveWeight: WEIGHT_AUTO, index: 0)
Expand Down
8 changes: 4 additions & 4 deletions src/command/MoveNodeToWorkspaceCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,21 @@ struct MoveNodeToWorkspaceCommand: Command {
state.stderr.append(noWindowIsFocused)
return false
}
let preserveWorkspace = focused.workspace
let prevWorkspace = focused.workspace ?? Workspace.focused
let targetWorkspace: Workspace
switch args.target {
case .relative(let relative):
guard let workspace = getNextPrevWorkspace(current: state.subject.workspace, relative: relative, stdin: stdin) else { return false }
guard let workspace = getNextPrevWorkspace(current: prevWorkspace, relative: relative, stdin: stdin) else { return false }
targetWorkspace = workspace
case .direct(let direct):
targetWorkspace = Workspace.get(byName: direct.name.raw)
}
if preserveWorkspace == targetWorkspace {
if prevWorkspace == targetWorkspace {
return true
}
let targetContainer: NonLeafTreeNodeObject = focused.isFloating ? targetWorkspace : targetWorkspace.rootTilingContainer
focused.unbindFromParent()
focused.bind(to: targetContainer, adaptiveWeight: WEIGHT_AUTO, index: INDEX_BIND_LAST)
return WorkspaceCommand.run(state, preserveWorkspace.name)
return WorkspaceCommand.run(state, prevWorkspace.name)
}
}
2 changes: 1 addition & 1 deletion src/command/MoveWorkspaceToMonitorCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ struct MoveWorkspaceToMonitorCommand: Command {

func _run(_ state: CommandMutableState, stdin: String) -> Bool {
check(Thread.current.isMainThread)
let focusedWorkspace = state.subject.workspace
let focusedWorkspace = state.subject.workspace ?? Workspace.focused
let prevMonitor = focusedWorkspace.monitor
let sortedMonitors = sortedMonitors
guard let index = sortedMonitors.firstIndex(where: { $0.rect.topLeftCorner == prevMonitor.rect.topLeftCorner }) else { return false }
Expand Down
3 changes: 2 additions & 1 deletion src/command/query/DebugWindowsCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ func debugWindowsIfRecording(_ window: Window) {
var result: [String] = []

result.append("\(windowPrefix) windowId: \(window.windowId)")
result.append("\(windowPrefix) workspace: \(window.workspace.name)")
result.append("\(windowPrefix) workspace: \(window.workspace?.name ?? "nil")")
result.append("\(windowPrefix) treeNodeParent: \(window.parent)")
result.append("\(windowPrefix) recognizedAsDialog: \(shouldFloat(window.axWindow, app))")
result.append(dumpAx(window.axWindow, windowPrefix, .window))

Expand Down
2 changes: 1 addition & 1 deletion src/command/query/ListWindowsCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ struct ListWindowsCommand: Command {
}
state.stdout += windows
.map { window in
[String(window.windowId), window.app.name ?? "NULL-APP-NAME", window.title ?? "NULL-TITLE"]
[String(window.windowId), window.app.name ?? "NULL-APP-NAME", window.title]
}
.toPaddingTable()
return true
Expand Down
1 change: 1 addition & 0 deletions src/config/parseConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ private extension ParsedCmd where T == any Command {
}
}

// todo Problem B6E178F2. Make macos-native* commands to be the last commands in the sequence
func parseCommandOrCommands(_ raw: TOMLValueConvertible) -> Parsed<[any Command]> {
if let rawString = raw.string {
return parseCommand(rawString).toEither().map { [$0] }
Expand Down
2 changes: 1 addition & 1 deletion src/mouse/moveWithMouse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ private func moveTilingWindow(_ window: Window) {
window.lastAppliedLayoutPhysicalRect = nil
let mouseLocation = mouseLocation
let targetWorkspace = mouseLocation.monitorApproximation.activeWorkspace
let swapTarget = mouseLocation.findIn(tree: targetWorkspace.workspace.rootTilingContainer, virtual: false)?.takeIf({ $0 != window })
let swapTarget = mouseLocation.findIn(tree: targetWorkspace.rootTilingContainer, virtual: false)?.takeIf({ $0 != window })
if targetWorkspace != window.workspace { // Move window to a different monitor
let index: Int
if let swapTarget, let parent = swapTarget.parent as? TilingContainer, let targetRect = swapTarget.lastAppliedLayoutPhysicalRect {
Expand Down
11 changes: 7 additions & 4 deletions src/refresh.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,19 @@ func takeFocusFromMacOs(_ nativeFocused: Window?, startup: Bool) { // alternativ
}

private func refreshFocusedWorkspaceBasedOnFocusedWindow() { // todo drop. It should no longer be necessary
if let focusedWindow = focusedWindow {
let focusedWorkspace: Workspace = focusedWindow.workspace
if let focusedWindow = focusedWindow, let monitor = focusedWindow.nodeMonitor {
// todo it's rather refresh focused monitor
let focusedWorkspace: Workspace = monitor.activeWorkspace
check(focusedWorkspace.monitor.setActiveWorkspace(focusedWorkspace))
focusedWorkspaceName = focusedWorkspace.name
}
}

private func normalizeLayoutReason() {
let workspace = Workspace.focused
for window in workspace.allLeafWindowsRecursive {
let windows: [Window] = workspace.allLeafWindowsRecursive +
macosInvisibleWindowsContainer.children.filterIsInstance(of: Window.self)
for window in windows {
let isMacosFullscreen = window.isMacosFullscreen
let isMacosInvisible = !isMacosFullscreen && (window.isMacosMinimized || window.macAppUnsafe.nsApp.isHidden)
if isMacosFullscreen && !window.layoutReason.isMacos {
Expand All @@ -87,7 +90,7 @@ private func normalizeLayoutReason() {
window.unbindFromParent()
window.bind(to: macosInvisibleWindowsContainer, adaptiveWeight: 1, index: INDEX_BIND_LAST)
}
if case .macos(let prevParentKind) = window.layoutReason, !isMacosFullscreen {
if case .macos(let prevParentKind) = window.layoutReason, !isMacosFullscreen && !isMacosInvisible {
window.layoutReason = .standard
window.unbindFromParent()
switch prevParentKind {
Expand Down
14 changes: 8 additions & 6 deletions src/tree/MacWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,14 @@ final class MacWindow: Window, CustomStringConvertible {
if MacWindow.allWindowsMap.removeValue(forKey: windowId) == nil {
return
}
let workspace = unbindFromParent().parent.workspace.name
let workspace = unbindFromParent().parent.workspace?.name
for obs in axObservers {
AXObserverRemoveNotification(obs.obs, obs.ax, obs.notif)
}
axObservers = []
// todo the if is an approximation to filter out cases when window just closed itself (or was killed remotely)
// we might want to track the time of the latest workspace switch to make the approximation more accurate
if workspace == previousFocusedWorkspaceName || workspace == focusedWorkspaceName {
if let workspace, workspace == previousFocusedWorkspaceName || workspace == focusedWorkspaceName {
refreshSession(forceFocus: true) {
_ = WorkspaceCommand.run(.focused, workspace)
}
Expand All @@ -83,7 +83,7 @@ final class MacWindow: Window, CustomStringConvertible {
return true
}

override var title: String? { axWindow.get(Ax.titleAttr) }
override var title: String { axWindow.get(Ax.titleAttr) ?? "" }
override var isMacosFullscreen: Bool { axWindow.get(Ax.isFullscreenAttr) == true }
override var isMacosMinimized: Bool { axWindow.get(Ax.minimizedAttr) == true }

Expand All @@ -105,16 +105,18 @@ final class MacWindow: Window, CustomStringConvertible {
if !isHiddenViaEmulation {
debug("hideViaEmulation: Hide \(self)")
guard let topLeftCorner = getTopLeftCorner() else { return }
guard let workspace else { return } // hiding only makes sense for workspace windows
prevUnhiddenEmulationPositionRelativeToWorkspaceAssignedRect =
topLeftCorner - workspace.monitor.rect.topLeftCorner
}
setTopLeftCorner(allMonitorsRectsUnion.bottomRightCorner)
_ = setTopLeftCorner(allMonitorsRectsUnion.bottomRightCorner)
}

func unhideViaEmulation() {
guard let prevUnhiddenEmulationPositionRelativeToWorkspaceAssignedRect else { return }
guard let workspace else { return } // hiding only makes sense for workspace windows

setTopLeftCorner(workspace.monitor.rect.topLeftCorner + prevUnhiddenEmulationPositionRelativeToWorkspaceAssignedRect)
_ = setTopLeftCorner(workspace.monitor.rect.topLeftCorner + prevUnhiddenEmulationPositionRelativeToWorkspaceAssignedRect)

self.prevUnhiddenEmulationPositionRelativeToWorkspaceAssignedRect = nil
}
Expand Down Expand Up @@ -277,7 +279,7 @@ extension WindowDetectedCallback {
if let startupMatcher = matcher.duringAeroSpaceStartup, startupMatcher != startup {
return false
}
if let regex = matcher.windowTitleRegexSubstring, !(window.title ?? "").contains(regex) {
if let regex = matcher.windowTitleRegexSubstring, !(window.title).contains(regex) {
return false
}
if let appId = matcher.appId, appId != window.app.id {
Expand Down
16 changes: 14 additions & 2 deletions src/tree/TreeNodeEx.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,20 @@ extension TreeNode {
var parents: [NonLeafTreeNodeObject] { parent.flatMap { [$0] + $0.parents } ?? [] }
var parentsWithSelf: [TreeNode] { parent.flatMap { [self] + $0.parentsWithSelf } ?? [self] }

var workspace: Workspace {
self as? Workspace ?? parent?.workspace ?? errorT("Unknown type \(Self.self)")
var workspace: Workspace? {
self as? Workspace ?? parent?.workspace
}

var nodeMonitor: Monitor? {
guard let parent else { return nil }
switch parent.cases {
case .workspace(let parent):
return parent.monitor
case .tilingContainer(let parent):
return parent.nodeMonitor
case .macosInvisibleWindowsContainer:
return nil
}
}

var mostRecentWindow: Window? {
Expand Down
12 changes: 8 additions & 4 deletions src/tree/Window.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ class Window: TreeNode, Hashable {

func getTopLeftCorner() -> CGPoint? { error("Not implemented") }
func getSize() -> CGSize? { error("Not implemented") }
var title: String? { error("Not implemented") }
var title: String { error("Not implemented") }
var isMacosFullscreen: Bool { false }
var isMacosMinimized: Bool { false }
var isMacosMinimized: Bool { false } // todo replace with enum MacOsWindowNativeState { normal, fullscreen, invisible }
var isHiddenViaEmulation: Bool { error("Not implemented") }
func setSize(_ size: CGSize) -> Bool { error("Not implemented") }

Expand Down Expand Up @@ -59,10 +59,14 @@ extension Window {

var ownIndex: Int { ownIndexOrNil! }

func focus() { // todo rename: focusWindowAndWorkspace
func focus() -> Bool { // todo rename: focusWindowAndWorkspace
markAsMostRecentChild()
// todo bug make the workspace active first...
focusedWorkspaceName = workspace.name
if let workspace = workspace ?? nodeMonitor?.activeWorkspace { // todo change focusedWorkspaceName to focused monitor
focusedWorkspaceName = workspace.name
return nodeMonitor?.setActiveWorkspace(workspace) ?? true
} // else if We should exit-native-fullscreen/unminimize window if we want to fix B6E178F2
return true
}

func setFrame(_ topLeft: CGPoint?, _ size: CGSize?) -> Bool {
Expand Down
2 changes: 1 addition & 1 deletion src/tree/Workspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class Workspace: TreeNode, NonLeafTreeNodeObject, Hashable, Identifiable, Custom

extension Workspace {
var isVisible: Bool { visibleWorkspaceToScreenPoint.keys.contains(self) }
var monitor: Monitor {
var monitor: Monitor { // todo rename to workspaceMonitor (to distinguish from nodeMonitor)
forceAssignedMonitor
?? visibleWorkspaceToScreenPoint[self]?.monitorApproximation
?? assignedMonitorPoint?.monitorApproximation
Expand Down
11 changes: 1 addition & 10 deletions src/tree/WorkspaceEx.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ extension Workspace {
}

var floatingAndMacosFullscreenWindows: [Window] {
workspace.children.filterIsInstance(of: Window.self)
children.filterIsInstance(of: Window.self)
}

var forceAssignedMonitor: Monitor? {
Expand All @@ -39,13 +39,4 @@ extension Workspace {
.compactMap { $0.resolveMonitor(sortedMonitors: sortedMonitors) }
.first
}

func layoutWorkspace() {
if isEffectivelyEmpty { return }
let rect = monitor.visibleRectPaddedByOuterGaps
// If monitors are aligned vertically and the monitor below has smaller width, then macOS may not allow the
// window on the upper monitor to take full width. rect.height - 1 resolves this problem
// But I also faced this problem in mointors horizontal configuration. ¯\_(ツ)_/¯
layoutRecursive(rect.topLeftCorner, width: rect.width, height: rect.height - 1, virtual: rect)
}
}
Loading

0 comments on commit 47f51f1

Please sign in to comment.