diff --git a/Rectangle/AccessibilityElement.swift b/Rectangle/AccessibilityElement.swift index c5929e14b..ffe75da63 100644 --- a/Rectangle/AccessibilityElement.swift +++ b/Rectangle/AccessibilityElement.swift @@ -155,7 +155,10 @@ class AccessibilityElement { } func getChildElements(_ role: NSAccessibility.Role) -> [AccessibilityElement]? { - return childElements?.filter { $0.role == role } + guard let elements = (childElements?.filter { $0.role == role }), elements.count > 0 else { + return nil + } + return elements } func getChildElement(_ subrole: NSAccessibility.Subrole) -> AccessibilityElement? { @@ -163,7 +166,10 @@ class AccessibilityElement { } func getChildElements(_ subrole: NSAccessibility.Subrole) -> [AccessibilityElement]? { - return childElements?.filter { $0.subrole == subrole } + guard let elements = (childElements?.filter { $0.subrole == subrole }), elements.count > 0 else { + return nil + } + return elements } var windowId: CGWindowID? { @@ -291,7 +297,7 @@ extension AccessibilityElement { } private static func getWindowInfo(_ location: CGPoint) -> WindowInfo? { - let infos = WindowUtil.getWindowList().filter { !["com.apple.dock", "com.apple.WindowManager"].contains($0.bundleIdentifier) } + let infos = WindowUtil.getWindowList().filter { !["Dock", "WindowManager"].contains($0.processName) } if let info = (infos.first { $0.frame.contains(location) }) { return info } @@ -336,7 +342,7 @@ extension AccessibilityElement { } static func getWindowElement(_ windowId: CGWindowID) -> AccessibilityElement? { - guard let pid = WindowUtil.getWindowList([windowId]).first?.pid else { return nil } + guard let pid = WindowUtil.getWindowList(ids: [windowId]).first?.pid else { return nil } return AccessibilityElement(pid).windowElements?.first { $0.windowId == windowId } } @@ -356,7 +362,7 @@ class StageWindowAccessibilityElement: AccessibilityElement { override var frame: CGRect { let frame = super.frame - guard !frame.isNull, let windowId = windowId, let info = WindowUtil.getWindowList([windowId]).first else { return frame } + guard !frame.isNull, let windowId = windowId, let info = WindowUtil.getWindowList(ids: [windowId]).first else { return frame } return .init(origin: info.frame.origin, size: frame.size) } diff --git a/Rectangle/ScreenDetection.swift b/Rectangle/ScreenDetection.swift index 1377ef6c4..a9cbcb3c9 100644 --- a/Rectangle/ScreenDetection.swift +++ b/Rectangle/ScreenDetection.swift @@ -130,7 +130,7 @@ extension NSScreen { var newFrame = visibleFrame if !ignoreStage && Defaults.stageSize.value > 0 { - if StageUtil.stageCapable && StageUtil.stageEnabled && StageUtil.stageStripShow && StageUtil.getStageStripWindowGroups().count > 0 { + if StageUtil.stageCapable && StageUtil.stageEnabled && StageUtil.stageStripShow && StageUtil.isStageStripVisible(self) { let stageSize = Defaults.stageSize.value < 1 ? newFrame.size.width * Defaults.stageSize.cgFloat : Defaults.stageSize.cgFloat diff --git a/Rectangle/Snapping/FootprintWindow.swift b/Rectangle/Snapping/FootprintWindow.swift index 350f34653..cd4d316f4 100644 --- a/Rectangle/Snapping/FootprintWindow.swift +++ b/Rectangle/Snapping/FootprintWindow.swift @@ -52,7 +52,7 @@ class FootprintWindow: NSWindow { override var isVisible: Bool { // Workaround for footprint getting pushed off of Stage Manager - if StageUtil.stageCapable && StageUtil.stageEnabled && StageUtil.stageStripShow && StageUtil.getStageStripWindowGroups().count > 0 { + if StageUtil.stageCapable && StageUtil.stageEnabled && StageUtil.stageStripShow && StageUtil.isStageStripVisible() { return true } return realIsVisible diff --git a/Rectangle/Utilities/CGExtension.swift b/Rectangle/Utilities/CGExtension.swift index 018dee551..223dee569 100644 --- a/Rectangle/Utilities/CGExtension.swift +++ b/Rectangle/Utilities/CGExtension.swift @@ -15,7 +15,10 @@ extension CGPoint { extension CGRect { var screenFlipped: CGRect { - .init(origin: .init(x: origin.x, y: NSScreen.screens[0].frame.maxY - maxY), size: size) + guard !isNull else { + return self + } + return .init(origin: .init(x: origin.x, y: NSScreen.screens[0].frame.maxY - maxY), size: size) } var isLandscape: Bool { width > height } diff --git a/Rectangle/Utilities/StageUtil.swift b/Rectangle/Utilities/StageUtil.swift index d368a2ea3..9feb718b5 100644 --- a/Rectangle/Utilities/StageUtil.swift +++ b/Rectangle/Utilities/StageUtil.swift @@ -8,43 +8,77 @@ import Foundation class StageUtil { + private static let windowManagerDefaults = UserDefaults(suiteName: "com.apple.WindowManager") + private static let dockDefaults = UserDefaults(suiteName: "com.apple.dock") + static var stageCapable: Bool { - guard #available(macOS 13, *) else { return false } + guard #available(macOS 13, *) else { + return false + } return true } static var stageEnabled: Bool { - guard let defaults = UserDefaults(suiteName: "com.apple.WindowManager"), defaults.object(forKey: "GloballyEnabled") != nil - else { return false } - return defaults.bool(forKey: "GloballyEnabled") + guard let value = windowManagerDefaults?.object(forKey: "GloballyEnabled") as? Bool else { + return false + } + return value } static var stageStripShow: Bool { - guard let defaults = UserDefaults(suiteName: "com.apple.WindowManager"), defaults.object(forKey: "AutoHide") != nil - else { return false } - return !defaults.bool(forKey: "AutoHide") + guard let value = windowManagerDefaults?.object(forKey: "AutoHide") as? Bool else { + return false + } + return !value } static var stageStripPosition: StageStripPosition { - guard let defaults = UserDefaults(suiteName: "com.apple.dock"), defaults.object(forKey: "orientation") != nil - else { return .left } - return defaults.string(forKey: "orientation") == "left" ? .right : .left + guard let value = dockDefaults?.object(forKey: "orientation") as? String else { + return .left + } + return value == "left" ? .right : .left } - static func getStageStripWindowGroups() -> [[CGWindowID]] { - var groups = [[CGWindowID]]() - if let appElement = AccessibilityElement("com.apple.WindowManager"), - let groupElements = appElement.getChildElement(.group)?.getChildElement(.list)?.getChildElements(.button) { - for groupElement in groupElements { - guard let windowIds = groupElement.windowIds else { continue } - groups.append(windowIds) + static func isStageStripVisible(_ screen: NSScreen? = .main) -> Bool { + guard let screen else { + return false + } + let infos = WindowUtil.getWindowList(all: true).filter { info in + guard info.processName == "WindowManager" else { + return false + } + let frame = info.frame.screenFlipped + let screens = NSScreen.screens.filter { $0.frame.minY <= frame.minY && frame.maxY <= $0.frame.maxY } + var infoScreen: NSScreen? + if stageStripPosition == .left { + infoScreen = screens.min { abs(frame.minX - $0.frame.minX) < abs(frame.minX - $1.frame.minX) } + } else { + infoScreen = screens.min { abs($0.frame.maxX - frame.maxX) < abs($1.frame.maxX - frame.maxX) } } + return infoScreen == screen + } + // A single window could be for the dragged window + return infos.count >= 2 + } + + private static func getStageStripWindowGroups(_ screen: NSScreen? = .main) -> [[CGWindowID]] { + guard + let screen, + let appElement = AccessibilityElement("com.apple.WindowManager"), + let stripElements = appElement.getChildElements(.group), + let stripElement = (stripElements.first { + let frame = $0.frame.screenFlipped + return !frame.isNull && screen.frame.contains(frame) + }), + let groupElements = stripElement.getChildElement(.list)?.getChildElements(.button) + else { + return [] } - return groups + return groupElements.compactMap { $0.windowIds } } - static func getStageStripWindowGroup(_ windowId: CGWindowID) -> [CGWindowID]? { - return getStageStripWindowGroups().first { $0.contains(windowId) } + static func getStageStripWindowGroup(_ windowId: CGWindowID, _ screen: NSScreen? = .main) -> [CGWindowID]? { + return getStageStripWindowGroups(screen).first { $0.contains(windowId) } } } diff --git a/Rectangle/Utilities/WindowUtil.swift b/Rectangle/Utilities/WindowUtil.swift index 0796914ca..c216915ac 100644 --- a/Rectangle/Utilities/WindowUtil.swift +++ b/Rectangle/Utilities/WindowUtil.swift @@ -10,7 +10,7 @@ import Foundation class WindowUtil { private static var windowListCache = TimeoutCache<[CGWindowID]?, [WindowInfo]>(timeout: 100) - static func getWindowList(_ ids: [CGWindowID]? = nil) -> [WindowInfo] { + static func getWindowList(ids: [CGWindowID]? = nil, all: Bool = false) -> [WindowInfo] { if let infos = windowListCache[ids] { return infos } var infos = [WindowInfo]() var array: CFArray? @@ -22,7 +22,7 @@ class WindowUtil { let ids = CFArrayCreate(kCFAllocatorDefault, ptr, ids.count, nil) array = CGWindowListCreateDescriptionFromArray(ids) } else { - array = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) + array = CGWindowListCopyWindowInfo([all ? .optionAll : .optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) } if let array = array { let count = array.getCount() @@ -31,8 +31,9 @@ class WindowUtil { let id = dictionary.getValue(kCGWindowNumber) as CFNumber let frame = (dictionary.getValue(kCGWindowBounds) as CFDictionary).toRect() let pid = dictionary.getValue(kCGWindowOwnerPID) as CFNumber + let processName = dictionary.getValue(kCGWindowOwnerName) as CFString if let frame = frame { - let info = WindowInfo(id: id as! CGWindowID, frame: frame, pid: pid as! pid_t) + let info = WindowInfo(id: CGWindowID(truncating: id), frame: frame, pid: pid_t(truncating: pid), processName: String(processName)) infos.append(info) } } @@ -46,5 +47,5 @@ struct WindowInfo { let id: CGWindowID let frame: CGRect let pid: pid_t - var bundleIdentifier: String? { NSRunningApplication(processIdentifier: pid)?.bundleIdentifier } + let processName: String }