From 86e33a871da33a12a0e40215ddac228a52f26b6e Mon Sep 17 00:00:00 2001 From: relikd Date: Thu, 21 Oct 2021 15:34:01 +0200 Subject: [PATCH] Fix window order restore + hidden windows (issue #1) --- README.md | 8 ++- src/Info.plist | 2 +- src/main.swift | 132 +++++++++++++++++++++++++++++++------------------ 3 files changed, 92 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 9135597..831f594 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![macOS 10.10+](https://img.shields.io/badge/macOS-10.10+-888)](#install) [![Current release](https://img.shields.io/github/release/relikd/Memmon)](https://github.com/relikd/Memmon/releases) - + # Memmon @@ -34,6 +34,12 @@ Secondly, it does one thing and one thing only: Save and restore window position 2. Grant Memmon the Accessibility privilege. Go to "System Preference" > "Security & Privacy" > "Accessibility" and add Memmon to that list. Otherwise, you can't move other application windows around and the app has no purpose. 3. Thats it. The app runs in your status bar. +### Develop + +You can either run the `main.swift` file directly with `swift main.swift`, via Terminal `./main.swift` (`chmod 755 main.swift`), or create a new Xcode project. Select the Command-Line template and after creation replace the existing `main.swift` with the bundled one. + +To build the app run `make`. + ### Hide Status Icon diff --git a/src/Info.plist b/src/Info.plist index 9e08c90..634b8a6 100644 --- a/src/Info.plist +++ b/src/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0 + 1.1 CFBundleVersion 42 LSMinimumSystemVersion diff --git a/src/main.swift b/src/main.swift index b6bb277..bcb0999 100755 --- a/src/main.swift +++ b/src/main.swift @@ -2,10 +2,13 @@ import Cocoa import AppKit +typealias WinPos = (Int32, CGRect) // win-num, bounds +typealias WinConf = [Int32: [WinPos]] // app-pid, window-list + class AppDelegate: NSObject, NSApplicationDelegate { private var statusItem: NSStatusItem! private var numScreens: Int = NSScreen.screens.count - private var state: [Int: [Int32: [CGRect]]] = [:] // [screencount: [pid: [windows]]] + private var state: [Int: WinConf] = [:] // [screencount: [pid: [windows]]] func applicationDidFinishLaunching(_ aNotification: Notification) { if UserDefaults.standard.bool(forKey: "invisible") == true { @@ -13,92 +16,101 @@ class AppDelegate: NSObject, NSApplicationDelegate { } self.statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) if let button = self.statusItem.button { - button.image = self.statusMenuIcon() + button.image = NSImage.statusIconMonitor } self.statusItem.menu = NSMenu(title: "") - self.statusItem.menu!.addItem(withTitle: "Memmon", action: nil, keyEquivalent: "") + self.statusItem.menu!.addItem(withTitle: "Memmon (v1.1)", 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") } - func statusMenuIcon() -> NSImage { - let img = NSImage.init(size: .init(width: 21, height: 14), flipped: true) { - let ctx = NSGraphicsContext.current!.cgContext - let w = $0.width - let h = $0.height - let ssw = 0.025 * w // small stroke width - let lsw = 0.05 * w // large stroke width - // main screen - ctx.stroke(CGRect(x: 0.1 * w, y: 0.0 * h, width: 0.8 * w, height: 0.8 * h).insetBy(dx: lsw / 2, dy: lsw / 2), width: lsw) - ctx.clear(CGRect(x: 0.0 * w, y: 0.2 * h, width: 1.0 * w, height: 0.4 * h)) - ctx.fill(CGRect(x: 0.41 * w, y: 0.8 * h, width: 0.18 * w, height: 0.12 * h)) - ctx.fill(CGRect(x: 0.27 * w, y: 0.92 * h, width: 0.46 * w, height: 0.08 * h)) - // three windows - ctx.stroke(CGRect(x: 0.0 * w, y: 0.28 * h, width: 0.27 * w, height: 0.24 * h).insetBy(dx: ssw / 2, dy: ssw / 2), width: ssw) - ctx.stroke(CGRect(x: 0.34 * w, y: 0.2 * h, width: 0.32 * w, height: 0.4 * h).insetBy(dx: ssw / 2, dy: ssw / 2), width: ssw) - ctx.stroke(CGRect(x: 0.73 * w, y: 0.28 * h, width: 0.27 * w, height: 0.24 * h).insetBy(dx: ssw / 2, dy: ssw / 2), width: ssw) - return true - } - img.isTemplate = true - return img - } - @objc func enableInvisbleMode() { self.statusItem = nil } func applicationDidChangeScreenParameters(_ notification: Notification) { if numScreens != NSScreen.screens.count { - // save state - self.state[numScreens] = self.getState() + self.saveState() numScreens = NSScreen.screens.count - // restore state if let previous = self.state[numScreens] { self.restoreState(previous) } } } - private func getState() -> [Int32: [CGRect]] { - var state: [Int32: [CGRect]] = [:] - let windowList = CGWindowListCopyWindowInfo(.optionOnScreenOnly, kCGNullWindowID) as NSArray? as? [[String: AnyObject]] + private func saveState() { + let newState = self.getState() + self.state[numScreens] = newState + // update existing + let dummy: WinPos = (0, CGRect.zero) + for kNum in self.state.keys { + if kNum == numScreens { continue } // current state, already set above + var tmp_state: WinConf = [:] + for (n_app, new_val) in newState { + if let old_val = self.state[kNum]![n_app] { + tmp_state[n_app] = [] + for (n_win, _) in new_val { + let old_pos = old_val.first { $0.0 == n_win } + tmp_state[n_app]!.append(old_pos ?? dummy) + } + } + } + self.state[kNum] = tmp_state + } + } + + private func restoreState(_ state: WinConf) { + for (pid, bounds) in state { + self.setWindowSizes(pid, bounds) + } + } + + private func getState() -> WinConf { + var allWinNums: [Int32] = [] + for winNum in NSWindow.windowNumbers(options: [.allApplications, .allSpaces]) ?? [] { + allWinNums.append(winNum.int32Value) + } + var state: WinConf = [:] + let windowList = CGWindowListCopyWindowInfo(.optionAll, kCGNullWindowID) as NSArray? as? [[String: AnyObject]] + for entry in windowList! { - let pid = entry[kCGWindowOwnerPID as String] as! Int32 - let layer = entry[kCGWindowLayer as String] as! Int32 // let owner = entry[kCGWindowOwnerName as String] as! String - if layer != 0 { + if entry[kCGWindowLayer as String] as! CGWindowLevel != kCGNormalWindowLevel { + continue + } + let winNum = entry[kCGWindowNumber as String] as! Int32 + guard let insIdx = allWinNums.firstIndex(of: winNum) else { continue } + let pid = entry[kCGWindowOwnerPID as String] as! Int32 let b = entry[kCGWindowBounds as String] as! [String: Int] let bounds = CGRect(x: b["X"]!, y: b["Y"]!, width: b["Width"]!, height: b["Height"]!) if (state[pid] == nil) { - state[pid] = [bounds] + state[pid] = [(winNum, bounds)] } else { - state[pid]!.append(bounds) + // allWinNums is sorted by recent activity, windowList is not. Keep order while appending. + if let idx = state[pid]!.firstIndex(where: { insIdx < allWinNums.firstIndex(of: $0.0)! }) { + state[pid]!.insert((winNum, bounds), at: idx) + } else { + state[pid]!.append((winNum, bounds)) + } } } return state } - private func restoreState(_ state: [Int32: [CGRect]]) { - for (pid, bounds) in state { - self.setWindowSizes(pid, bounds) - } - } - - private func setWindowSizes(_ pid: Int32, _ sizes: [CGRect]) { + private func setWindowSizes(_ pid: Int32, _ sizes: [WinPos]) { let win = self.axWinList(pid) guard win.count > 0, win.count == sizes.count else { - print(pid, win.count, sizes.count) return } for i in 0 ..< win.count { - var newPoint = sizes[i].origin - var newSize = sizes[i].size + var pt = sizes[i].1 + if pt.isEmpty { continue } // filter dummy elements AXUIElementSetAttributeValue(win[i], kAXPositionAttribute as CFString, - AXValueCreate(AXValueType(rawValue: kAXValueCGPointType)!, &newPoint)!); + AXValueCreate(AXValueType(rawValue: kAXValueCGPointType)!, &pt.origin)!); AXUIElementSetAttributeValue(win[i], kAXSizeAttribute as CFString, - AXValueCreate(AXValueType(rawValue: kAXValueCGSizeType)!, &newSize)!); + AXValueCreate(AXValueType(rawValue: kAXValueCGSizeType)!, &pt.size)!); } } @@ -121,6 +133,30 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } +extension NSImage { + static var statusIconMonitor: NSImage { + let img = NSImage.init(size: .init(width: 21, height: 14), flipped: true) { + let ctx = NSGraphicsContext.current!.cgContext + let w = $0.width + let h = $0.height + let ssw = 0.025 * w // small stroke width + let lsw = 0.05 * w // large stroke width + // main screen + ctx.stroke(CGRect(x: 0.1 * w, y: 0.0 * h, width: 0.8 * w, height: 0.8 * h).insetBy(dx: lsw / 2, dy: lsw / 2), width: lsw) + ctx.clear(CGRect(x: 0.0 * w, y: 0.2 * h, width: 1.0 * w, height: 0.4 * h)) + ctx.fill(CGRect(x: 0.41 * w, y: 0.8 * h, width: 0.18 * w, height: 0.12 * h)) + ctx.fill(CGRect(x: 0.27 * w, y: 0.92 * h, width: 0.46 * w, height: 0.08 * h)) + // three windows + ctx.stroke(CGRect(x: 0.0 * w, y: 0.28 * h, width: 0.27 * w, height: 0.24 * h).insetBy(dx: ssw / 2, dy: ssw / 2), width: ssw) + ctx.stroke(CGRect(x: 0.34 * w, y: 0.2 * h, width: 0.32 * w, height: 0.4 * h).insetBy(dx: ssw / 2, dy: ssw / 2), width: ssw) + ctx.stroke(CGRect(x: 0.73 * w, y: 0.28 * h, width: 0.27 * w, height: 0.24 * h).insetBy(dx: ssw / 2, dy: ssw / 2), width: ssw) + return true + } + img.isTemplate = true + return img + } +} + let delegate = AppDelegate() NSApplication.shared.delegate = delegate NSApplication.shared.run()