Skip to content

Commit

Permalink
Merge pull request #1220 from decodism/master-2
Browse files Browse the repository at this point in the history
Stage Manager: fix multi-screen
  • Loading branch information
rxhanson authored Aug 4, 2023
2 parents 0d99c6b + 09f8823 commit f8a964b
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 32 deletions.
16 changes: 11 additions & 5 deletions Rectangle/AccessibilityElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,21 @@ 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? {
return childElements?.first { $0.subrole == subrole }
}

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? {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 }
}

Expand All @@ -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)
}

Expand Down
2 changes: 1 addition & 1 deletion Rectangle/ScreenDetection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Rectangle/Snapping/FootprintWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion Rectangle/Utilities/CGExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
74 changes: 54 additions & 20 deletions Rectangle/Utilities/StageUtil.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
}
}

Expand Down
9 changes: 5 additions & 4 deletions Rectangle/Utilities/WindowUtil.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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()
Expand All @@ -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)
}
}
Expand All @@ -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
}

0 comments on commit f8a964b

Please sign in to comment.