Browse Source
feat: smart per-app window state memory with default full-screen and manual desktop override persistence
main
feat: smart per-app window state memory with default full-screen and manual desktop override persistence
main
3 changed files with 191 additions and 13 deletions
@ -0,0 +1,101 @@ |
|||||
|
import Cocoa |
||||
|
import ApplicationServices |
||||
|
|
||||
|
public final class WindowStateManager { |
||||
|
public static let shared = WindowStateManager() |
||||
|
|
||||
|
private let defaults = UserDefaults.standard |
||||
|
private let prefKey = "SonioxAppFullScreenPreferences" |
||||
|
private var observer: NSObjectProtocol? |
||||
|
|
||||
|
private init() { |
||||
|
startObservingAppSwitches() |
||||
|
} |
||||
|
|
||||
|
deinit { |
||||
|
if let obs = observer { |
||||
|
NSWorkspace.shared.notificationCenter.removeObserver(obs) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - Preference Query |
||||
|
/// Returns true by default unless the user has explicitly exited full screen for this app. |
||||
|
public func prefersFullScreen(for identifier: String) -> Bool { |
||||
|
let prefs = defaults.dictionary(forKey: prefKey) as? [String: Bool] ?? [:] |
||||
|
let cleanId = identifier.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) |
||||
|
return prefs[cleanId] ?? true // DEFAULT IS FULL SCREEN! |
||||
|
} |
||||
|
|
||||
|
public func setPrefersFullScreen(_ fullScreen: Bool, for identifier: String) { |
||||
|
let cleanId = identifier.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) |
||||
|
guard !cleanId.isEmpty else { return } |
||||
|
|
||||
|
var prefs = defaults.dictionary(forKey: prefKey) as? [String: Bool] ?? [:] |
||||
|
prefs[cleanId] = fullScreen |
||||
|
defaults.set(prefs, forKey: prefKey) |
||||
|
print("WindowStateManager: Saved full screen preference for '\(cleanId)': \(fullScreen)") |
||||
|
} |
||||
|
|
||||
|
public func resetAllPreferences() { |
||||
|
defaults.removeObject(forKey: prefKey) |
||||
|
print("WindowStateManager: Reset all app window preferences to default (Full Screen).") |
||||
|
} |
||||
|
|
||||
|
// MARK: - Automatic Learning via Deactivation |
||||
|
private func startObservingAppSwitches() { |
||||
|
let center = NSWorkspace.shared.notificationCenter |
||||
|
observer = center.addObserver( |
||||
|
forName: NSWorkspace.didDeactivateApplicationNotification, |
||||
|
object: nil, |
||||
|
queue: .main |
||||
|
) { [weak self] notif in |
||||
|
guard let self = self, |
||||
|
let app = notif.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication, |
||||
|
app.activationPolicy == .regular else { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
self.captureCurrentWindowState(app: app) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public func captureCurrentWindowState(app: NSRunningApplication) { |
||||
|
guard let bid = app.bundleIdentifier ?? app.localizedName, !bid.isEmpty else { return } |
||||
|
|
||||
|
if let isFull = isWindowFullScreen(app: app) { |
||||
|
setPrefersFullScreen(isFull, for: bid) |
||||
|
if let name = app.localizedName { |
||||
|
setPrefersFullScreen(isFull, for: name) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - Window Inspection Helper |
||||
|
public func isWindowFullScreen(app: NSRunningApplication) -> Bool? { |
||||
|
let appElem = AXUIElementCreateApplication(app.processIdentifier) |
||||
|
|
||||
|
var windowObj: CFTypeRef? |
||||
|
let hasWin = (AXUIElementCopyAttributeValue(appElem, kAXFocusedWindowAttribute as CFString, &windowObj) == .success && windowObj != nil) || |
||||
|
(AXUIElementCopyAttributeValue(appElem, kAXMainWindowAttribute as CFString, &windowObj) == .success && windowObj != nil) |
||||
|
|
||||
|
if hasWin, let winElem = windowObj { |
||||
|
var fsObj: CFTypeRef? |
||||
|
if AXUIElementCopyAttributeValue(winElem as! AXUIElement, "AXFullScreen" as CFString, &fsObj) == .success, |
||||
|
let isFull = fsObj as? Bool { |
||||
|
return isFull |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var winListObj: CFTypeRef? |
||||
|
if AXUIElementCopyAttributeValue(appElem, kAXWindowsAttribute as CFString, &winListObj) == .success, |
||||
|
let wins = winListObj as? [AXUIElement], let firstWin = wins.first { |
||||
|
var fsObj: CFTypeRef? |
||||
|
if AXUIElementCopyAttributeValue(firstWin, "AXFullScreen" as CFString, &fsObj) == .success, |
||||
|
let isFull = fsObj as? Bool { |
||||
|
return isFull |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue