diff --git a/src/Info.plist b/src/Info.plist
index 368c8e4..8916d53 100644
--- a/src/Info.plist
+++ b/src/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.2
+ 1.3
CFBundleVersion
42
LSMinimumSystemVersion
diff --git a/src/main.swift b/src/main.swift
index c10da69..c9262af 100755
--- a/src/main.swift
+++ b/src/main.swift
@@ -3,18 +3,26 @@ import Cocoa
import AppKit
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 WinConf = [AppPID: [WinPos]] // app-pid, window-list
+typealias SpaceId = WinNum // see NSWindow.windowNumber (Int)
class AppDelegate: NSObject, NSApplicationDelegate {
private var statusItem: NSStatusItem!
private var numScreens: Int = NSScreen.screens.count
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 = [] // dropped after restore
+
func applicationDidFinishLaunching(_ aNotification: Notification) {
// show Accessibility Permissions popup
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
UserDefaults.standard.register(defaults: ["icon": 2])
let icon = UserDefaults.standard.integer(forKey: "icon")
@@ -28,7 +36,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
}
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: "Quit", action: #selector(NSApp.terminate), keyEquivalent: "q")
}
@@ -45,13 +53,20 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
}
- private func getWinIds(allSpaces: Bool) -> [WinNum] {
- NSWindow.windowNumbers(options: allSpaces ? [.allApplications, .allSpaces] : .allApplications)?.map{ $0.int32Value } ?? []
+ private func getWinIds(allSpaces: Bool = false) -> [WinNum] {
+ NSWindow.windowNumbers(options: allSpaces ? [.allApplications, .allSpaces] : .allApplications)?.map{ $0.intValue } ?? []
}
// MARK: - Save State (CGWindow) -
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()
self.state[numScreens] = newState
// update existing
@@ -70,10 +85,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
self.state[kNum] = tmp_state
}
+ self.time = Date(timeIntervalSinceNow: 0)
}
private func getState() -> WinConf {
- let allWinNums = self.getWinIds(allSpaces: true)
+ let allWinNums = self.getWinIds(allSpaces: true).filter { !self.spacesAll.contains($0) }
var state: WinConf = [:]
let windowList = CGWindowListCopyWindowInfo(.optionAll, kCGNullWindowID) as NSArray? as? [[String: AnyObject]]
@@ -106,24 +122,26 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// MARK: - Restore State (AXUIElement) -
private func restoreState() {
- for (pid, bounds) in self.state[numScreens] ?? [:] {
- let spaceWinNums = getWinIds(allSpaces: false)
- self.setWindowSizes(pid, bounds.filter{ spaceWinNums.contains($0.0) })
+ if let space = currentSpace(), self.spacesNeedRestore.contains(space) {
+ self.spacesNeedRestore.remove(space)
+ 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]) {
+ guard sizes.count > 0 else { return }
let win = self.axWinList(pid)
- guard win.count > 0, win.count == sizes.count else {
- return
- }
+ guard win.count == sizes.count else { return }
for i in 0 ..< win.count {
var pt = sizes[i].1
if pt.isEmpty { continue } // filter dummy elements
- AXUIElementSetAttributeValue(win[i], kAXPositionAttribute as CFString,
- AXValueCreate(AXValueType(rawValue: kAXValueCGPointType)!, &pt.origin)!);
- AXUIElementSetAttributeValue(win[i], kAXSizeAttribute as CFString,
- AXValueCreate(AXValueType(rawValue: kAXValueCGSizeType)!, &pt.size)!);
+ let origin = AXValueCreate(AXValueType(rawValue: kAXValueCGPointType)!, &pt.origin)!
+ let size = AXValueCreate(AXValueType(rawValue: kAXValueCGSizeType)!, &pt.size)!
+ AXUIElementSetAttributeValue(win[i], kAXPositionAttribute as CFString, origin);
+ AXUIElementSetAttributeValue(win[i], kAXSizeAttribute as CFString, size);
}
}
@@ -144,6 +162,39 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
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 -