From 9519951c421f0be484ecdd00a1b8f775399a5e9f Mon Sep 17 00:00:00 2001 From: Kai Azim <68963405+MrKai77@users.noreply.github.com> Date: Fri, 1 Sep 2023 16:41:46 -0600 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20New=20class=20called:=20`Window`;?= =?UTF-8?q?=20control=20all=20window=20controls=20from=20there?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Loop.xcodeproj/project.pbxproj | 6 +- Loop/Extensions/NSScreen+Extensions.swift | 17 +++ Loop/Helpers/Loop-Bridging-Header.h | 12 ++ Loop/Helpers/LoopManager.swift | 9 +- Loop/Helpers/Window.swift | 108 ++++++++++++++ Loop/Helpers/WindowEngine.swift | 153 ++++---------------- Loop/Radial Menu/RadialMenuController.swift | 2 +- Loop/Radial Menu/RadialMenuView.swift | 2 +- 8 files changed, 178 insertions(+), 131 deletions(-) create mode 100644 Loop/Helpers/Loop-Bridging-Header.h create mode 100644 Loop/Helpers/Window.swift diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index f2e65fff..f82b69b9 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ A8504D2D2A85832F00C2EFDA /* SoftwareUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8504D2C2A85832F00C2EFDA /* SoftwareUpdater.swift */; }; A86949862A8F2BB70051AAAF /* CGKeyCode+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86949852A8F2BB60051AAAF /* CGKeyCode+Extensions.swift */; }; A86CB7332A3D22E7006A78F2 /* WindowEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86CB7322A3D22E7006A78F2 /* WindowEngine.swift */; }; + A87376F62AA288EB001890F4 /* Window.swift in Sources */ = {isa = PBXBuildFile; fileRef = A87376F52AA288EB001890F4 /* Window.swift */; }; A8789F6729805B190040512E /* RadialMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8789F6629805B190040512E /* RadialMenuView.swift */; }; A8789F6929805B340040512E /* PreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8789F6829805B340040512E /* PreviewView.swift */; }; A87D36982980A89D002B0D2F /* KeybindingSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A87D36972980A89D002B0D2F /* KeybindingSettingsView.swift */; }; @@ -74,6 +75,7 @@ A86949852A8F2BB60051AAAF /* CGKeyCode+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGKeyCode+Extensions.swift"; sourceTree = ""; }; A86AFD7529888B29008F4892 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; A86CB7322A3D22E7006A78F2 /* WindowEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowEngine.swift; sourceTree = ""; }; + A87376F52AA288EB001890F4 /* Window.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Window.swift; sourceTree = ""; }; A8789F6629805B190040512E /* RadialMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadialMenuView.swift; sourceTree = ""; }; A8789F6829805B340040512E /* PreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewView.swift; sourceTree = ""; }; A87D36972980A89D002B0D2F /* KeybindingSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeybindingSettingsView.swift; sourceTree = ""; }; @@ -130,10 +132,11 @@ isa = PBXGroup; children = ( A86CB7322A3D22E7006A78F2 /* WindowEngine.swift */, + A87376F52AA288EB001890F4 /* Window.swift */, + A8330AD32A3AC27600673C8D /* WindowDirection.swift */, A8A2ABE62A3FB0370067B5A9 /* KeybindMonitor.swift */, A82521ED29E235AC00139654 /* AccessibilityAccessManager.swift */, A8EF1F08299C87DF00633440 /* IconManager.swift */, - A8330AD32A3AC27600673C8D /* WindowDirection.swift */, A8E6D2002A416494005751D4 /* LoopTriggerKeys.swift */, A8504D2C2A85832F00C2EFDA /* SoftwareUpdater.swift */, A8A2ABEA2A3FBFBA0067B5A9 /* VisualEffectView.swift */, @@ -378,6 +381,7 @@ A8330AC12A3AC13100673C8D /* Defaults+Extensions.swift in Sources */, A8E1575F298654960005761C /* AboutView.swift in Sources */, A8DCC98A2981F43F00D41065 /* PreviewSettingsView.swift in Sources */, + A87376F62AA288EB001890F4 /* Window.swift in Sources */, A8330AC52A3AC15900673C8D /* Notification+Extensions.swift in Sources */, A8D5A7D82A913862004EA5BB /* DirectionSelectorCircleSegment.swift in Sources */, A83667C82A3D7D910001D630 /* AXUIElement+Extensions.swift in Sources */, diff --git a/Loop/Extensions/NSScreen+Extensions.swift b/Loop/Extensions/NSScreen+Extensions.swift index d1582513..5390aff7 100644 --- a/Loop/Extensions/NSScreen+Extensions.swift +++ b/Loop/Extensions/NSScreen+Extensions.swift @@ -22,4 +22,21 @@ extension NSScreen { return screenWithMouse } + + var safeScreenFrame: CGRect? { + guard let displayID = self.displayID else { return nil } + let screenFrameOrigin = CGDisplayBounds(displayID).origin + var screenFrame: CGRect = self.visibleFrame + + // Set position of the screenFrame (useful for multiple displays) + screenFrame.origin = screenFrameOrigin + + // Move screenFrame's y origin to compensate for menubar & dock, if it's on the bottom + screenFrame.origin.y += (self.frame.size.height - self.visibleFrame.size.height) + + // Move screenFrame's x origin when dock is shown on left/right + screenFrame.origin.x += (self.frame.size.width - self.visibleFrame.size.width) + + return screenFrame + } } diff --git a/Loop/Helpers/Loop-Bridging-Header.h b/Loop/Helpers/Loop-Bridging-Header.h new file mode 100644 index 00000000..83c2d41e --- /dev/null +++ b/Loop/Helpers/Loop-Bridging-Header.h @@ -0,0 +1,12 @@ +// +// Loop-Bridging-Header.h +// Loop +// +// Created by Kai Azim on 2023-09-01. +// + +#ifndef Loop_Bridging_Header_h +#define Loop_Bridging_Header_h + + +#endif /* Loop_Bridging_Header_h */ diff --git a/Loop/Helpers/LoopManager.swift b/Loop/Helpers/LoopManager.swift index 5c8eed0b..e3677e21 100644 --- a/Loop/Helpers/LoopManager.swift +++ b/Loop/Helpers/LoopManager.swift @@ -11,7 +11,6 @@ import Defaults class LoopManager { private let accessibilityAccessManager = AccessibilityAccessManager() - private let windowEngine = WindowEngine() private let keybindMonitor = KeybindMonitor.shared private let iconManager = IconManager() @@ -20,7 +19,7 @@ class LoopManager { private var currentResizingDirection: WindowDirection = .noAction private var isLoopShown: Bool = false - private var frontmostWindow: AXUIElement? + private var frontmostWindow: Window? private var screenWithMouse: NSScreen? func startObservingKeys() { @@ -82,7 +81,7 @@ class LoopManager { // Loop will only open if accessibility access has been granted if accessibilityAccessManager.getStatus() { - self.frontmostWindow = windowEngine.getFrontmostWindow() + self.frontmostWindow = WindowEngine.getFrontmostWindow() self.screenWithMouse = NSScreen.screenWithMouse if Defaults[.previewVisibility] == true && frontmostWindow != nil { @@ -100,6 +99,8 @@ class LoopManager { radialMenuController.close() previewController.close() + + keybindMonitor.resetPressedKeys() keybindMonitor.stop() if self.frontmostWindow != nil && @@ -113,7 +114,7 @@ class LoopManager { isLoopShown = false if willResizeWindow { - windowEngine.resize(window: self.frontmostWindow!, direction: self.currentResizingDirection, screen: self.screenWithMouse!) + WindowEngine.resize(window: self.frontmostWindow!, direction: self.currentResizingDirection, screen: self.screenWithMouse!) NotificationCenter.default.post( name: Notification.Name.finishedLooping, diff --git a/Loop/Helpers/Window.swift b/Loop/Helpers/Window.swift new file mode 100644 index 00000000..504b5d54 --- /dev/null +++ b/Loop/Helpers/Window.swift @@ -0,0 +1,108 @@ +// +// Window.swift +// Loop +// +// Created by Kai Azim on 2023-09-01. +// + +import SwiftUI + +class Window { + private let kAXFullscreenAttribute = "AXFullScreen" + let window: AXUIElement + + init?(window: AXUIElement) { + self.window = window + + if role != kAXWindowRole, + subrole != kAXStandardWindowSubrole { + return nil + } + } + + convenience init?(pid: pid_t) { + let element = AXUIElementCreateApplication(pid) + guard let window = element.copyAttributeValue(attribute: kAXFocusedWindowAttribute) else { return nil } + // swiftlint:disable force_cast + self.init(window: window as! AXUIElement) + // swiftlint:enable force_cast + } + + var role: String? { + return self.window.copyAttributeValue(attribute: kAXRoleAttribute) as? String + } + + var subrole: String? { + return self.window.copyAttributeValue(attribute: kAXSubroleAttribute) as? String + } + + var isFullscreen: Bool { + let result = self.window.copyAttributeValue(attribute: kAXFullscreenAttribute) as? NSNumber + return result?.boolValue ?? false + } + @discardableResult + func setFullscreen(_ state: Bool) -> Bool { + return self.window.setAttributeValue( + attribute: kAXFullscreenAttribute, + value: state ? kCFBooleanTrue : kCFBooleanFalse + ) + } + + var isMinimized: Bool { + let result = self.window.copyAttributeValue(attribute: kAXMinimizedAttribute) as? NSNumber + return result?.boolValue ?? false + } + @discardableResult + func setMinimized(_ state: Bool) -> Bool { + return self.window.setAttributeValue( + attribute: kAXMinimizedAttribute, + value: state ? kCFBooleanTrue : kCFBooleanFalse + ) + } + + var origin: CGPoint { + var point: CGPoint = .zero + guard let value = self.window.copyAttributeValue(attribute: kAXPositionAttribute) else { return point } + // swiftlint:disable force_cast + AXValueGetValue(value as! AXValue, .cgPoint, &point) // Convert to CGPoint + // swiftlint:enable force_cast + return point + } + @discardableResult + func setOrigin(_ origin: CGPoint) -> Bool { + var position = origin + if let value = AXValueCreate(AXValueType.cgPoint, &position) { + return self.window.setAttributeValue(attribute: kAXPositionAttribute, value: value) + } + return false + } + + var size: CGSize { + var size: CGSize = .zero + guard let value = self.window.copyAttributeValue(attribute: kAXSizeAttribute) else { return size } + // swiftlint:disable force_cast + AXValueGetValue(value as! AXValue, .cgSize, &size) // Convert to CGSize + // swiftlint:enable force_cast + return size + } + @discardableResult + func setSize(_ size: CGSize) -> Bool { + var size = size + if let value = AXValueCreate(AXValueType.cgSize, &size) { + return self.window.setAttributeValue(attribute: kAXSizeAttribute, value: value) + } + return false + } + + var frame: CGRect { + return CGRect(origin: self.origin, size: self.size) + } + + @discardableResult + func setFrame(_ rect: CGRect) -> Bool { + if self.setOrigin(rect.origin) && self.setSize(rect.size) { + return true + } + return false + } +} diff --git a/Loop/Helpers/WindowEngine.swift b/Loop/Helpers/WindowEngine.swift index 590c0fad..8d406a80 100644 --- a/Loop/Helpers/WindowEngine.swift +++ b/Loop/Helpers/WindowEngine.swift @@ -9,137 +9,40 @@ import SwiftUI import Defaults struct WindowEngine { + static func resize(window: Window, direction: WindowDirection, screen: NSScreen) { + window.setFullscreen(false) - private let kAXFullscreenAttribute = "AXFullScreen" - - func resizeFrontmostWindow(direction: WindowDirection) { - guard let frontmostWindow = self.getFrontmostWindow(), - let screenWithMouse = NSScreen.screenWithMouse else { return } - resize(window: frontmostWindow, direction: direction, screen: screenWithMouse) - } - - func getFrontmostWindow() -> AXUIElement? { - - #if DEBUG - print("--------------------------------") - guard let app = NSWorkspace.shared.runningApplications.first(where: { $0.isActive }) else { return nil } - print("Frontmost app: \(app)") - guard let window = self.getFocusedWindow(pid: app.processIdentifier) else { return nil } - print("AXUIElement: \(window)") - print("Is kAXWindowRole: \(self.getRole(element: window) == kAXWindowRole)") - print("Is kAXStandardWindowSubrole: \(self.getSubRole(element: window) == kAXStandardWindowSubrole)") - #endif - - guard let app = NSWorkspace.shared.runningApplications.first(where: { $0.isActive }), - let window = self.getFocusedWindow(pid: app.processIdentifier), - self.getRole(element: window) == kAXWindowRole, - self.getSubRole(element: window) == kAXStandardWindowSubrole - else { return nil } - - return window - } - - func resize(window: AXUIElement, direction: WindowDirection, screen: NSScreen) { - self.setFullscreen(element: window, state: false) - - let windowFrame = getRect(element: window) - guard let screenFrame = getScreenFrame(screen: screen), - let newWindowFrame = generateWindowFrame(windowFrame, screenFrame, direction) + let oldWindowFrame = window.frame + guard let screenFrame = screen.safeScreenFrame, + let newWindowFrame = WindowEngine.generateWindowFrame(oldWindowFrame, screenFrame, direction) else { return } - self.setPosition(element: window, position: newWindowFrame.origin) - self.setSize(element: window, size: newWindowFrame.size) + window.setFrame(newWindowFrame) - if self.getRect(element: window) != newWindowFrame { - self.handleSizeConstrainedWindow( - element: window, - windowFrame: self.getRect(element: window), + if window.frame != newWindowFrame { + WindowEngine.handleSizeConstrainedWindow( + window: window, + windowFrame: window.frame, screenFrame: screenFrame ) } - - KeybindMonitor.shared.resetPressedKeys() - } - - private func getFocusedWindow(pid: pid_t) -> AXUIElement? { - let element = AXUIElementCreateApplication(pid) - guard let window = element.copyAttributeValue(attribute: kAXFocusedWindowAttribute) else { return nil } - // swiftlint:disable force_cast - return (window as! AXUIElement) - // swiftlint:enable force_cast - } - private func getRole(element: AXUIElement) -> String? { - return element.copyAttributeValue(attribute: kAXRoleAttribute) as? String - } - private func getSubRole(element: AXUIElement) -> String? { - return element.copyAttributeValue(attribute: kAXSubroleAttribute) as? String - } - - @discardableResult - private func setFullscreen(element: AXUIElement, state: Bool) -> Bool { - return element.setAttributeValue(attribute: kAXFullscreenAttribute, value: state ? kCFBooleanTrue : kCFBooleanFalse) - } - private func getFullscreen(element: AXUIElement) -> Bool { - let result = element.copyAttributeValue(attribute: kAXFullscreenAttribute) as? NSNumber - return result?.boolValue ?? false - } - - @discardableResult - private func setPosition(element: AXUIElement, position: CGPoint) -> Bool { - var position = position - if let value = AXValueCreate(AXValueType.cgPoint, &position) { - return element.setAttributeValue(attribute: kAXPositionAttribute, value: value) - } - return false - } - private func getPosition(element: AXUIElement) -> CGPoint { - var point: CGPoint = .zero - guard let value = element.copyAttributeValue(attribute: kAXPositionAttribute) else { return point } - // swiftlint:disable force_cast - AXValueGetValue(value as! AXValue, .cgPoint, &point) // Convert to CGPoint - // swiftlint:enable force_cast - return point - } - - @discardableResult - private func setSize(element: AXUIElement, size: CGSize) -> Bool { - var size = size - if let value = AXValueCreate(AXValueType.cgSize, &size) { - return element.setAttributeValue(attribute: kAXSizeAttribute, value: value) - } - return false - } - private func getSize(element: AXUIElement) -> CGSize { - var size: CGSize = .zero - guard let value = element.copyAttributeValue(attribute: kAXSizeAttribute) else { return size } - // swiftlint:disable force_cast - AXValueGetValue(value as! AXValue, .cgSize, &size) // Convert to CGSize - // swiftlint:enable force_cast - return size } - private func getRect(element: AXUIElement) -> CGRect { - return CGRect(origin: getPosition(element: element), size: getSize(element: element)) - } - - private func getScreenFrame(screen: NSScreen) -> CGRect? { - guard let displayID = screen.displayID else { return nil } - let screenFrameOrigin = CGDisplayBounds(displayID).origin - var screenFrame: CGRect = screen.visibleFrame - - // Set position of the screenFrame (useful for multiple displays) - screenFrame.origin = screenFrameOrigin - - // Move screenFrame's y origin to compensate for menubar & dock, if it's on the bottom - screenFrame.origin.y += (screen.frame.size.height - screen.visibleFrame.size.height) + static func getFrontmostWindow() -> Window? { + guard let app = NSWorkspace.shared.runningApplications.first(where: { $0.isActive }), + let window = Window(pid: app.processIdentifier) else { return nil } - // Move screenFrame's x origin when dock is shown on left/right - screenFrame.origin.x += (screen.frame.size.width - screen.visibleFrame.size.width) + #if DEBUG + print("=== NEW WINDOW ===") + print("Frontmost app: \(app)") + print("kAXWindowRole: \(window.role ?? "N/A")") + print("kAXStandardWindowSubrole: \(window.subrole ?? "N/A")") + #endif - return screenFrame + return window } - private func generateWindowFrame(_ windowFrame: CGRect, _ screenFrame: CGRect, _ direction: WindowDirection) -> CGRect? { + private static func generateWindowFrame(_ windowFrame: CGRect, _ screenFrame: CGRect, _ direction: WindowDirection) -> CGRect? { let screenWidth = screenFrame.size.width let screenHeight = screenFrame.size.height let screenX = screenFrame.origin.x @@ -149,10 +52,12 @@ struct WindowEngine { case .maximize: return CGRect(x: screenX, y: screenY, width: screenWidth, height: screenHeight) case .center: - return CGRect(x: screenFrame.midX - windowFrame.width/2, - y: screenFrame.midY - windowFrame.height/2, - width: windowFrame.width, - height: windowFrame.height) + return CGRect( + x: screenFrame.midX - windowFrame.width/2, + y: screenFrame.midY - windowFrame.height/2, + width: windowFrame.width, + height: windowFrame.height + ) case .topHalf: return CGRect(x: screenX, y: screenY, width: screenWidth, height: screenHeight/2) case .rightHalf: @@ -194,7 +99,7 @@ struct WindowEngine { } } - private func handleSizeConstrainedWindow(element: AXUIElement, windowFrame: CGRect, screenFrame: CGRect) { + private static func handleSizeConstrainedWindow(window: Window, windowFrame: CGRect, screenFrame: CGRect) { // If the window is fully shown on the screen if (windowFrame.maxX <= screenFrame.maxX) && (windowFrame.maxY <= screenFrame.maxY) { @@ -212,6 +117,6 @@ struct WindowEngine { fixedWindowFrame.origin.y = screenFrame.maxY - fixedWindowFrame.height } - setPosition(element: element, position: fixedWindowFrame.origin) + window.setOrigin(fixedWindowFrame.origin) } } diff --git a/Loop/Radial Menu/RadialMenuController.swift b/Loop/Radial Menu/RadialMenuController.swift index 21e7401f..ec2bead9 100644 --- a/Loop/Radial Menu/RadialMenuController.swift +++ b/Loop/Radial Menu/RadialMenuController.swift @@ -12,7 +12,7 @@ class RadialMenuController { private var loopRadialMenuWindowController: NSWindowController? - func show(frontmostWindow: AXUIElement?) { + func show(frontmostWindow: Window?) { if let windowController = loopRadialMenuWindowController { windowController.window?.orderFrontRegardless() return diff --git a/Loop/Radial Menu/RadialMenuView.swift b/Loop/Radial Menu/RadialMenuView.swift index bca3e595..37b37e6e 100644 --- a/Loop/Radial Menu/RadialMenuView.swift +++ b/Loop/Radial Menu/RadialMenuView.swift @@ -15,7 +15,7 @@ struct RadialMenuView: View { let radialMenuSize: CGFloat = 100 // This will determine whether Loop needs to show a warning (if it's nil) - let frontmostWindow: AXUIElement? + let frontmostWindow: Window? @State var previewMode = false @State var initialMousePosition: CGPoint = CGPoint()