feat: add min/demin window shortcut
This commit is contained in:
1 parent
7a94e4a124
commit
2b752efb72
7 files changed
+95
-33
No files matched your search
@@ -77,11 +77,24 @@ extension AXUIElement {
|
||||
|
||||
func closeWindow() {
|
||||
if isFullScreen() {
|
||||
AXUIElementSetAttributeValue(self, kAXFullscreenAttribute as CFString, 0 as CFTypeRef)
|
||||
AXUIElementSetAttributeValue(self, kAXFullscreenAttribute as CFString, false as CFTypeRef)
|
||||
}
|
||||
AXUIElementPerformAction(closeButton(), kAXPressAction as CFString)
|
||||
}
|
||||
|
||||
func minDeminWindow() {
|
||||
if isFullScreen() {
|
||||
AXUIElementSetAttributeValue(self, kAXFullscreenAttribute as CFString, false as CFTypeRef)
|
||||
// minimizing is ignored if sent immediatly; we wait for the de-fullscreen animation to be over
|
||||
DispatchQueues.accessibilityCommands.asyncAfter(deadline: .now() + .milliseconds(1000)) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
AXUIElementSetAttributeValue(self, kAXMinimizedAttribute as CFString, true as CFTypeRef)
|
||||
}
|
||||
} else {
|
||||
AXUIElementSetAttributeValue(self, kAXMinimizedAttribute as CFString, !isMinimized() as CFTypeRef)
|
||||
}
|
||||
}
|
||||
|
||||
func focusWindow() {
|
||||
AXUIElementPerformAction(self, kAXRaiseAction as CFString)
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ private func eventApplicationActivated(_ app: App, _ element: AXUIElement) {
|
||||
let appFocusedWindow = element.focusedWindow(),
|
||||
let existingIndex = Windows.list.firstIndexThatMatches(appFocusedWindow) else { return }
|
||||
Windows.list.insert(Windows.list.remove(at: existingIndex), at: 0)
|
||||
app.refreshOpenUi([Windows.list[0], Windows.list[existingIndex]])
|
||||
app.refreshOpenUi([Windows.list[0], Windows.list[existingIndex]], true)
|
||||
}
|
||||
|
||||
private func eventApplicationHiddenOrShown(_ app: App, _ element: AXUIElement, _ type: String) {
|
||||
@@ -120,7 +120,7 @@ private func eventApplicationHiddenOrShown(_ app: App, _ element: AXUIElement, _
|
||||
$0.application.axUiElement!.pid() == element.pid()
|
||||
}
|
||||
windows.forEach { $0.isHidden = type == kAXApplicationHiddenNotification }
|
||||
app.refreshOpenUi(windows)
|
||||
app.refreshOpenUi(windows, true)
|
||||
}
|
||||
|
||||
private func eventWindowCreated(_ app: App, _ element: AXUIElement, _ application: Application) {
|
||||
|
||||
@@ -18,6 +18,7 @@ class Preferences {
|
||||
"cancelShortcut": "⎋",
|
||||
"closeWindowShortcut": "W",
|
||||
"quitAppShortcut": "Q",
|
||||
"minDeminWindowShortcut": "M",
|
||||
"arrowKeysEnabled": true,
|
||||
"mouseHoverEnabled": true,
|
||||
"showMinimizedWindows": true,
|
||||
@@ -55,6 +56,7 @@ class Preferences {
|
||||
static var cancelShortcut: String { defaults.string(forKey: "cancelShortcut")! }
|
||||
static var closeWindowShortcut: String { defaults.string(forKey: "closeWindowShortcut")! }
|
||||
static var quitAppShortcut: String { defaults.string(forKey: "quitAppShortcut")! }
|
||||
static var minDeminWindowShortcut: String { defaults.string(forKey: "minDeminWindowShortcut")! }
|
||||
static var arrowKeysEnabled: Bool { defaults.bool(forKey: "arrowKeysEnabled") }
|
||||
static var mouseHoverEnabled: Bool { defaults.bool(forKey: "mouseHoverEnabled") }
|
||||
static var showMinimizedWindows: Bool { defaults.bool(forKey: "showMinimizedWindows") }
|
||||
|
||||
+10
-2
@@ -25,7 +25,9 @@ class Window {
|
||||
]
|
||||
|
||||
static func stopSubscriptionRetries(_ notification: String, _ cgWindowId: CGWindowID) {
|
||||
Windows.windowsInSubscriptionRetryLoop.removeAll { $0 == (String(cgWindowId) + String(notification)) }
|
||||
Windows.windowsInSubscriptionRetryLoop.removeAll { (subscription: String) -> Bool in
|
||||
subscription == String(cgWindowId) + notification
|
||||
}
|
||||
}
|
||||
|
||||
init(_ axUiElement: AXUIElement, _ application: Application) {
|
||||
@@ -76,6 +78,12 @@ class Window {
|
||||
}
|
||||
}
|
||||
|
||||
func minDemin() {
|
||||
DispatchQueues.accessibilityCommands.async { [weak self] in
|
||||
self?.axUiElement.minDeminWindow()
|
||||
}
|
||||
}
|
||||
|
||||
func focus() {
|
||||
// macOS bug: when switching to a System Preferences window in another space, it switches to that space,
|
||||
// but quickly switches back to another window in that space
|
||||
@@ -147,7 +155,7 @@ private func eventWindowMiniaturizedOrDeminiaturized(_ app: App, _ element: AXUI
|
||||
guard let index = Windows.list.firstIndexThatMatches(element) else { return }
|
||||
let window = Windows.list[index]
|
||||
window.isMinimized = type == kAXWindowMiniaturizedNotification
|
||||
app.refreshOpenUi([window])
|
||||
app.refreshOpenUi([window], true)
|
||||
}
|
||||
|
||||
private func eventWindowTitleChanged(_ app: App, _ element: AXUIElement) {
|
||||
|
||||
+38
-21
@@ -20,6 +20,10 @@ class Windows {
|
||||
}
|
||||
|
||||
static func cycleFocusedWindowIndex(_ step: Int) {
|
||||
updateFocusedWindowIndex(windowIndexAfterCycling(step))
|
||||
}
|
||||
|
||||
static func windowIndexAfterCycling(_ step: Int) -> Int {
|
||||
var iterations = 0
|
||||
var targetIndex = focusedWindowIndex
|
||||
repeat {
|
||||
@@ -27,7 +31,7 @@ class Windows {
|
||||
targetIndex = next < 0 ? list.count + next : next
|
||||
iterations += 1
|
||||
} while !list[targetIndex].shouldShowTheUser && iterations <= list.count
|
||||
updateFocusedWindowIndex(targetIndex)
|
||||
return targetIndex
|
||||
}
|
||||
|
||||
static func moveFocusedWindowIndexAfterWindowDestroyedInBackground(_ destroyedWindowIndex: Int) {
|
||||
@@ -41,17 +45,20 @@ class Windows {
|
||||
// workaround: when Preferences > Mission Control > "Displays have separate Spaces" is unchecked,
|
||||
// switching between displays doesn't trigger .activeSpaceDidChangeNotification; we get the latest manually
|
||||
Spaces.refreshCurrentSpaceId()
|
||||
let spacesMap = Spaces.idsAndIndexes
|
||||
list.forEachAsync { window in
|
||||
let spaceIds = window.cgWindowId.spaces()
|
||||
if spaceIds.count == 1 {
|
||||
window.spaceId = spaceIds.first!
|
||||
window.spaceIndex = spacesMap.first { $0.0 == spaceIds.first! }!.1
|
||||
} else if spaceIds.count > 1 {
|
||||
window.spaceId = Spaces.currentSpaceId
|
||||
window.spaceIndex = Spaces.currentSpaceIndex
|
||||
window.isOnAllSpaces = true
|
||||
}
|
||||
list.forEachAsync { (window: Window) in
|
||||
updatesWindowSpace(window)
|
||||
}
|
||||
}
|
||||
|
||||
static func updatesWindowSpace(_ window: Window) {
|
||||
let spaceIds = window.cgWindowId.spaces()
|
||||
if spaceIds.count == 1 {
|
||||
window.spaceId = spaceIds.first!
|
||||
window.spaceIndex = Spaces.idsAndIndexes.first { $0.0 == spaceIds.first! }!.1
|
||||
} else if spaceIds.count > 1 {
|
||||
window.spaceId = Spaces.currentSpaceId
|
||||
window.spaceIndex = Spaces.currentSpaceIndex
|
||||
window.isOnAllSpaces = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,18 +87,28 @@ class Windows {
|
||||
}
|
||||
|
||||
static func refreshWhichWindowsToShowTheUser(_ screen: NSScreen) {
|
||||
var screenFrameInQuartzCoordinates = screen.frame
|
||||
screenFrameInQuartzCoordinates.origin.y = NSMaxY(NSScreen.screens[0].frame) - NSMaxY(screen.frame)
|
||||
let activeApp = NSWorkspace.shared.frontmostApplication
|
||||
Windows.list.forEach {
|
||||
$0.shouldShowTheUser = !(!Preferences.showMinimizedWindows && $0.isMinimized) &&
|
||||
!(!Preferences.showHiddenWindows && $0.isHidden) &&
|
||||
!(Preferences.appsToShow == .active && $0.application.runningApplication != activeApp) &&
|
||||
!(Preferences.spacesToShow == .active && $0.spaceId != Spaces.currentSpaceId) &&
|
||||
!(Preferences.screensToShow == .showingAltTab && $0.axUiElement.position().map { p in !screenFrameInQuartzCoordinates.contains(p) } ?? true)
|
||||
Windows.list.forEach { (window: Window) in
|
||||
refreshIfWindowShouldBeShownToTheUser(window, screen)
|
||||
}
|
||||
}
|
||||
|
||||
static func refreshIfWindowShouldBeShownToTheUser(_ window: Window, _ screen: NSScreen) {
|
||||
window.shouldShowTheUser = !(!Preferences.showMinimizedWindows && window.isMinimized) &&
|
||||
!(!Preferences.showHiddenWindows && window.isHidden) &&
|
||||
!(Preferences.appsToShow == .active && window.application.runningApplication != NSWorkspace.shared.frontmostApplication) &&
|
||||
!(Preferences.spacesToShow == .active && window.spaceId != Spaces.currentSpaceId) &&
|
||||
!(Preferences.screensToShow == .showingAltTab && !isOnScreen(window, screen))
|
||||
}
|
||||
|
||||
static func isOnScreen(_ window: Window, _ screen: NSScreen) -> Bool {
|
||||
if let position = window.axUiElement.position() {
|
||||
var screenFrameInQuartzCoordinates = screen.frame
|
||||
screenFrameInQuartzCoordinates.origin.y = NSMaxY(NSScreen.screens[0].frame) - NSMaxY(screen.frame)
|
||||
return screenFrameInQuartzCoordinates.contains(position)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static func refreshAllExistingThumbnails() {
|
||||
refreshAllThumbnails()
|
||||
guard App.app.uiWorkShouldBeDone else { return }
|
||||
|
||||
+23
-4
@@ -104,6 +104,10 @@ class App: NSApplication, NSApplicationDelegate {
|
||||
Windows.focusedWindow()?.quitApp()
|
||||
}
|
||||
|
||||
func minDeminSelectedWindow() {
|
||||
Windows.focusedWindow()?.minDemin()
|
||||
}
|
||||
|
||||
func focusTarget() {
|
||||
debugPrint("focusTarget")
|
||||
focusSelectedWindow(Windows.focusedWindow())
|
||||
@@ -148,15 +152,14 @@ class App: NSApplication, NSApplicationDelegate {
|
||||
rebuildUi()
|
||||
}
|
||||
|
||||
func refreshOpenUi(_ windowsToRefresh: [Window]? = nil) {
|
||||
func refreshOpenUi(_ windowsToUpdate: [Window]? = nil, _ updateWindowsInfo: Bool = false) {
|
||||
guard appIsBeingUsed else { return }
|
||||
windowsToRefresh?.forEach { $0.refreshThumbnail() }
|
||||
guard uiWorkShouldBeDone else { return }
|
||||
let currentScreen = Screen.preferred() // fix screen between steps since it could change (e.g. mouse moved to another screen)
|
||||
// workaround: when Preferences > Mission Control > "Displays have separate Spaces" is unchecked,
|
||||
// switching between displays doesn't trigger .activeSpaceDidChangeNotification; we get the latest manually
|
||||
Spaces.refreshCurrentSpaceId()
|
||||
refreshSpecificWindows(windowsToUpdate, updateWindowsInfo, currentScreen)
|
||||
guard uiWorkShouldBeDone else { return }
|
||||
let currentScreen = Screen.preferred() // fix screen between steps since it could change (e.g. mouse moved to another screen)
|
||||
thumbnailsPanel.thumbnailsView.updateItems(currentScreen)
|
||||
guard uiWorkShouldBeDone else { return }
|
||||
thumbnailsPanel.setFrame(thumbnailsPanel.thumbnailsView.frame, display: false)
|
||||
@@ -164,6 +167,22 @@ class App: NSApplication, NSApplicationDelegate {
|
||||
Screen.repositionPanel(thumbnailsPanel, currentScreen, .appleCentered)
|
||||
}
|
||||
|
||||
private func refreshSpecificWindows(_ windowsToUpdate: [Window]?, _ updateWindowsInfo: Bool, _ currentScreen: NSScreen) -> ()? {
|
||||
windowsToUpdate?.forEach { (window: Window) in
|
||||
guard uiWorkShouldBeDone else { return }
|
||||
window.refreshThumbnail()
|
||||
if updateWindowsInfo {
|
||||
Windows.refreshIfWindowShouldBeShownToTheUser(window, currentScreen)
|
||||
if !window.shouldShowTheUser && window.cgWindowId == Windows.focusedWindow()!.cgWindowId {
|
||||
let stepWithClosestWindow = Windows.windowIndexAfterCycling(-1) > Windows.focusedWindowIndex ? 1 : -1
|
||||
Windows.cycleFocusedWindowIndex(stepWithClosestWindow)
|
||||
} else {
|
||||
Windows.updatesWindowSpace(window)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func showUiOrCycleSelection(_ step: Int) {
|
||||
debugPrint("showUiOrCycleSelection", step)
|
||||
if isFirstSummon {
|
||||
|
||||
@@ -19,6 +19,7 @@ class GeneralTab: NSViewController, PreferencePane {
|
||||
let cancelShortcut = LabelAndControl.makeLabelWithRecorder(NSLocalizedString("Cancel and hide", comment: ""), "cancelShortcut", Preferences.cancelShortcut, labelPosition: .right)
|
||||
let closeWindowShortcut = LabelAndControl.makeLabelWithRecorder(NSLocalizedString("Close window", comment: ""), "closeWindowShortcut", Preferences.closeWindowShortcut, labelPosition: .right)
|
||||
let quitAppShortcut = LabelAndControl.makeLabelWithRecorder(NSLocalizedString("Quit app", comment: ""), "quitAppShortcut", Preferences.quitAppShortcut, labelPosition: .right)
|
||||
let minDeminWindowShortcut = LabelAndControl.makeLabelWithRecorder(NSLocalizedString("Minimize/Deminimize window", comment: ""), "minDeminWindowShortcut", Preferences.minDeminWindowShortcut, labelPosition: .right)
|
||||
let enableArrows = LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Arrow keys", comment: ""), "arrowKeysEnabled", extraAction: GeneralTab.arrowKeysEnabledCallback, labelPosition: .right)
|
||||
let enableMouse = LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Mouse hover", comment: ""), "mouseHoverEnabled", labelPosition: .right)
|
||||
let holdAndPress = StackView(holdShortcut)
|
||||
@@ -29,7 +30,7 @@ class GeneralTab: NSViewController, PreferencePane {
|
||||
let screensToShow = dropdown("screensToShow", ScreensToShowPreference.allCases)
|
||||
let showMinimizedWindows = StackView(LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Minimized", comment: ""), "showMinimizedWindows", labelPosition: .right))
|
||||
let showHiddenWindows = StackView(LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Hidden", comment: ""), "showHiddenWindows", labelPosition: .right))
|
||||
let shortcuts = StackView([nextWindowShortcut, previousWindowShortcut, cancelShortcut, closeWindowShortcut, quitAppShortcut].map { (view: [NSView]) in StackView(view) }, .vertical)
|
||||
let shortcuts = StackView([nextWindowShortcut, previousWindowShortcut, cancelShortcut, closeWindowShortcut, quitAppShortcut, minDeminWindowShortcut].map { (view: [NSView]) in StackView(view) }, .vertical)
|
||||
let toShowDropdowns = StackView([appsToShow, spacesToShow, screensToShow], .vertical)
|
||||
let toShowCheckboxes = StackView([showMinimizedWindows, showHiddenWindows], .vertical)
|
||||
let toShowExplanations = LabelAndControl.makeLabel(NSLocalizedString("Show the following windows:", comment: ""))
|
||||
@@ -46,7 +47,7 @@ class GeneralTab: NSViewController, PreferencePane {
|
||||
grid.fit()
|
||||
|
||||
GeneralTab.shortcutsDependentOnHoldShortcut.append(contentsOf: [enableArrows[0] as! NSControl] +
|
||||
[nextWindowShortcut, previousWindowShortcut, cancelShortcut, closeWindowShortcut, quitAppShortcut].map { $0[0] as! NSControl })
|
||||
[nextWindowShortcut, previousWindowShortcut, cancelShortcut, closeWindowShortcut, quitAppShortcut, minDeminWindowShortcut].map { $0[0] as! NSControl })
|
||||
GeneralTab.arrowKeysEnabledCallback(enableArrows[0] as! NSControl)
|
||||
startAtLoginCallback(startAtLogin[1] as! NSControl)
|
||||
|
||||
@@ -62,7 +63,7 @@ class GeneralTab: NSViewController, PreferencePane {
|
||||
removeShortcutIfExists(controlId, type) // remove the previous shortcut
|
||||
shortcutActions[controlId] = ShortcutAction(shortcut: shortcut, actionHandler: { _ in
|
||||
let isShortcutInitiatingTheApp = ["previousWindowShortcut", "nextWindowShortcut"].contains(controlId)
|
||||
let isShortcutWithUiShown = ["closeWindowShortcut", "quitAppShortcut"].contains(controlId)
|
||||
let isShortcutWithUiShown = ["←", "→", "closeWindowShortcut", "quitAppShortcut", "minDeminWindowShortcut"].contains(controlId)
|
||||
App.app.uiWorkShouldBeDone = isShortcutInitiatingTheApp || isShortcutWithUiShown
|
||||
if isShortcutInitiatingTheApp {
|
||||
App.app.appIsBeingUsed = true
|
||||
@@ -103,6 +104,8 @@ class GeneralTab: NSViewController, PreferencePane {
|
||||
return { App.app.closeSelectedWindow() }
|
||||
} else if controlId == "quitAppShortcut" {
|
||||
return { App.app.quitSelectedApp() }
|
||||
} else if controlId == "minDeminWindowShortcut" {
|
||||
return { App.app.minDeminSelectedWindow() }
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user