preliminary support for spaces
This commit is contained in:
@@ -15,7 +15,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.2</string>
|
<string>1.3</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>42</string>
|
<string>42</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
|||||||
@@ -3,18 +3,26 @@ import Cocoa
|
|||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
typealias AppPID = Int32 // see kCGWindowOwnerPID
|
typealias AppPID = Int32 // see kCGWindowOwnerPID
|
||||||
typealias WinNum = Int32 // see kCGWindowNumber
|
typealias WinNum = Int // see kCGWindowNumber (Int32) and NSWindow.windowNumber (Int)
|
||||||
typealias WinPos = (WinNum, CGRect) // win-num, bounds
|
typealias WinPos = (WinNum, CGRect) // win-num, bounds
|
||||||
typealias WinConf = [AppPID: [WinPos]] // app-pid, window-list
|
typealias WinConf = [AppPID: [WinPos]] // app-pid, window-list
|
||||||
|
typealias SpaceId = WinNum // see NSWindow.windowNumber (Int)
|
||||||
|
|
||||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
private var statusItem: NSStatusItem!
|
private var statusItem: NSStatusItem!
|
||||||
private var numScreens: Int = NSScreen.screens.count
|
private var numScreens: Int = NSScreen.screens.count
|
||||||
private var state: [Int: WinConf] = [:] // [screencount: [pid: [windows]]]
|
private var state: [Int: WinConf] = [:] // [screencount: [pid: [windows]]]
|
||||||
|
|
||||||
|
private var time: Date = Date.distantPast
|
||||||
|
private var spacesAll: [SpaceId] = [] // keep forever (and keep order)
|
||||||
|
private var spacesNeedRestore: Set<SpaceId> = [] // dropped after restore
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||||
// show Accessibility Permissions popup
|
// show Accessibility Permissions popup
|
||||||
AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt.takeUnretainedValue() : true] as CFDictionary)
|
AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt.takeUnretainedValue() : true] as CFDictionary)
|
||||||
|
// track space changes
|
||||||
|
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(self.activeSpaceChanged), name: NSWorkspace.activeSpaceDidChangeNotification, object: nil)
|
||||||
|
_ = self.currentSpace() // create space-id win for current space
|
||||||
// create status menu icon
|
// create status menu icon
|
||||||
UserDefaults.standard.register(defaults: ["icon": 2])
|
UserDefaults.standard.register(defaults: ["icon": 2])
|
||||||
let icon = UserDefaults.standard.integer(forKey: "icon")
|
let icon = UserDefaults.standard.integer(forKey: "icon")
|
||||||
@@ -28,7 +36,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.statusItem.menu = NSMenu(title: "")
|
self.statusItem.menu = NSMenu(title: "")
|
||||||
self.statusItem.menu!.addItem(withTitle: "Memmon (v1.2)", action: nil, keyEquivalent: "")
|
self.statusItem.menu!.addItem(withTitle: "Memmon (v1.3)", action: nil, keyEquivalent: "")
|
||||||
self.statusItem.menu!.addItem(withTitle: "Hide Status Icon", action: #selector(self.enableInvisbleMode), keyEquivalent: "")
|
self.statusItem.menu!.addItem(withTitle: "Hide Status Icon", action: #selector(self.enableInvisbleMode), keyEquivalent: "")
|
||||||
self.statusItem.menu!.addItem(withTitle: "Quit", action: #selector(NSApp.terminate), keyEquivalent: "q")
|
self.statusItem.menu!.addItem(withTitle: "Quit", action: #selector(NSApp.terminate), keyEquivalent: "q")
|
||||||
}
|
}
|
||||||
@@ -45,13 +53,20 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getWinIds(allSpaces: Bool) -> [WinNum] {
|
private func getWinIds(allSpaces: Bool = false) -> [WinNum] {
|
||||||
NSWindow.windowNumbers(options: allSpaces ? [.allApplications, .allSpaces] : .allApplications)?.map{ $0.int32Value } ?? []
|
NSWindow.windowNumbers(options: allSpaces ? [.allApplications, .allSpaces] : .allApplications)?.map{ $0.intValue } ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Save State (CGWindow) -
|
// MARK: - Save State (CGWindow) -
|
||||||
|
|
||||||
private func saveState() {
|
private func saveState() {
|
||||||
|
self.spacesNeedRestore = Set(self.spacesAll)
|
||||||
|
guard self.time.timeIntervalSinceNow < -5 else {
|
||||||
|
// Last save is less than 5 sec ago.
|
||||||
|
// Do not override a (probably) still correct state.
|
||||||
|
// Otherwise a monitor flicker will forget win-positions in other spaces.
|
||||||
|
return
|
||||||
|
}
|
||||||
let newState = self.getState()
|
let newState = self.getState()
|
||||||
self.state[numScreens] = newState
|
self.state[numScreens] = newState
|
||||||
// update existing
|
// update existing
|
||||||
@@ -70,10 +85,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
}
|
}
|
||||||
self.state[kNum] = tmp_state
|
self.state[kNum] = tmp_state
|
||||||
}
|
}
|
||||||
|
self.time = Date(timeIntervalSinceNow: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getState() -> WinConf {
|
private func getState() -> WinConf {
|
||||||
let allWinNums = self.getWinIds(allSpaces: true)
|
let allWinNums = self.getWinIds(allSpaces: true).filter { !self.spacesAll.contains($0) }
|
||||||
var state: WinConf = [:]
|
var state: WinConf = [:]
|
||||||
let windowList = CGWindowListCopyWindowInfo(.optionAll, kCGNullWindowID) as NSArray? as? [[String: AnyObject]]
|
let windowList = CGWindowListCopyWindowInfo(.optionAll, kCGNullWindowID) as NSArray? as? [[String: AnyObject]]
|
||||||
|
|
||||||
@@ -106,24 +122,26 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
// MARK: - Restore State (AXUIElement) -
|
// MARK: - Restore State (AXUIElement) -
|
||||||
|
|
||||||
private func restoreState() {
|
private func restoreState() {
|
||||||
for (pid, bounds) in self.state[numScreens] ?? [:] {
|
if let space = currentSpace(), self.spacesNeedRestore.contains(space) {
|
||||||
let spaceWinNums = getWinIds(allSpaces: false)
|
self.spacesNeedRestore.remove(space)
|
||||||
self.setWindowSizes(pid, bounds.filter{ spaceWinNums.contains($0.0) })
|
let spaceWinNums = self.getWinIds()
|
||||||
|
for (pid, bounds) in self.state[numScreens] ?? [:] {
|
||||||
|
self.setWindowSizes(pid, bounds.filter{ spaceWinNums.contains($0.0) })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setWindowSizes(_ pid: pid_t, _ sizes: [WinPos]) {
|
private func setWindowSizes(_ pid: pid_t, _ sizes: [WinPos]) {
|
||||||
|
guard sizes.count > 0 else { return }
|
||||||
let win = self.axWinList(pid)
|
let win = self.axWinList(pid)
|
||||||
guard win.count > 0, win.count == sizes.count else {
|
guard win.count == sizes.count else { return }
|
||||||
return
|
|
||||||
}
|
|
||||||
for i in 0 ..< win.count {
|
for i in 0 ..< win.count {
|
||||||
var pt = sizes[i].1
|
var pt = sizes[i].1
|
||||||
if pt.isEmpty { continue } // filter dummy elements
|
if pt.isEmpty { continue } // filter dummy elements
|
||||||
AXUIElementSetAttributeValue(win[i], kAXPositionAttribute as CFString,
|
let origin = AXValueCreate(AXValueType(rawValue: kAXValueCGPointType)!, &pt.origin)!
|
||||||
AXValueCreate(AXValueType(rawValue: kAXValueCGPointType)!, &pt.origin)!);
|
let size = AXValueCreate(AXValueType(rawValue: kAXValueCGSizeType)!, &pt.size)!
|
||||||
AXUIElementSetAttributeValue(win[i], kAXSizeAttribute as CFString,
|
AXUIElementSetAttributeValue(win[i], kAXPositionAttribute as CFString, origin);
|
||||||
AXValueCreate(AXValueType(rawValue: kAXValueCGSizeType)!, &pt.size)!);
|
AXUIElementSetAttributeValue(win[i], kAXSizeAttribute as CFString, size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,6 +162,39 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Space Management -
|
||||||
|
|
||||||
|
@objc func activeSpaceChanged(_ notification: Notification) {
|
||||||
|
self.restoreState()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func currentSpace() -> SpaceId? {
|
||||||
|
let thisSpace = self.getWinIds()
|
||||||
|
var candidates = self.spacesAll.filter { thisSpace.contains($0) }
|
||||||
|
if candidates.count > 0 {
|
||||||
|
let best = candidates.removeFirst()
|
||||||
|
if candidates.count > 0 {
|
||||||
|
// if a full-screen app is closed, win moves to current active space -> remove duplicates
|
||||||
|
self.spacesAll.removeAll { candidates.contains($0) }
|
||||||
|
for oldNum in candidates {
|
||||||
|
NSApp.window(withWindowNumber: oldNum)?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
// create new space-id window (space was not visited yet)
|
||||||
|
let win = NSWindow(contentRect: .zero, styleMask: .borderless, backing: .buffered, defer: false)
|
||||||
|
win.isReleasedWhenClosed = false // win is released either way. But crashes if true.
|
||||||
|
guard win.isOnActiveSpace else {
|
||||||
|
// dashboard or other full-screen app that prohibits display
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
win.collectionBehavior = [.ignoresCycle, .stationary]
|
||||||
|
win.setIsVisible(true)
|
||||||
|
self.spacesAll.append(win.windowNumber)
|
||||||
|
return win.windowNumber
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Status Bar Icon -
|
// MARK: - Status Bar Icon -
|
||||||
|
|||||||
Reference in New Issue
Block a user