feat: add close window shortcut
This commit is contained in:
1 parent
9d531dd487
commit
8da8af8fc3
8 files changed
+58
-18
No files matched your search
@@ -55,6 +55,10 @@ extension AXUIElement {
|
||||
return attribute(kAXHiddenAttribute, Bool.self) == true
|
||||
}
|
||||
|
||||
func isFullScreen() -> Bool {
|
||||
return attribute(kAXFullscreenAttribute, Bool.self) == true
|
||||
}
|
||||
|
||||
func focusedWindow() -> AXUIElement? {
|
||||
return attribute(kAXFocusedWindowAttribute, AXUIElement.self)
|
||||
}
|
||||
@@ -67,6 +71,21 @@ extension AXUIElement {
|
||||
return attribute(kAXSubroleAttribute, String.self)
|
||||
}
|
||||
|
||||
func closeButton() -> AXUIElement {
|
||||
return attribute(kAXCloseButtonAttribute, AXUIElement.self)!
|
||||
}
|
||||
|
||||
func closeWindow() {
|
||||
if isFullScreen() {
|
||||
AXUIElementSetAttributeValue(self, kAXFullscreenAttribute as CFString, 0 as CFTypeRef)
|
||||
}
|
||||
AXUIElementPerformAction(closeButton(), kAXPressAction as CFString)
|
||||
}
|
||||
|
||||
func focusWindow() {
|
||||
AXUIElementPerformAction(self, kAXRaiseAction as CFString)
|
||||
}
|
||||
|
||||
func subscribeWithRetry(_ axObserver: AXObserver, _ notification: String, _ pointer: UnsafeMutableRawPointer?, _ callback: (() -> Void)? = nil, _ runningApplication: NSRunningApplication? = nil, _ wid: CGWindowID? = nil, _ attemptsCount: Int = 0) {
|
||||
DispatchQueue.global(qos: .userInteractive).async { [weak self] () -> () in
|
||||
guard let self = self else { return }
|
||||
|
||||
@@ -132,6 +132,8 @@ func CGSGetWindowLevel(_ cid: CGSConnectionID, _ wid: CGWindowID, _ level: inout
|
||||
@_silgen_name("SLSRequestScreenCaptureAccess") @discardableResult
|
||||
func SLSRequestScreenCaptureAccess() -> UInt8
|
||||
|
||||
// for some reason, this attribute is missing from AXAttributeConstants
|
||||
let kAXFullscreenAttribute = "AXFullScreen"
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,6 @@ import Foundation
|
||||
|
||||
// OS events should be observed on background threads
|
||||
class DispatchQueues {
|
||||
static let focusActions = DispatchQueue(label: "focusActions", qos: .userInteractive)
|
||||
static let accessibilityCommands = DispatchQueue(label: "accessibilityCommands", qos: .userInteractive)
|
||||
static let keyboardEvents = DispatchQueue(label: "keyboardEvents", qos: .userInteractive)
|
||||
}
|
||||
@@ -16,6 +16,7 @@ class Preferences {
|
||||
"nextWindowShortcut": "⇥",
|
||||
"previousWindowShortcut": "⇧⇥",
|
||||
"cancelShortcut": "⎋",
|
||||
"closeWindowShortcut": "W",
|
||||
"arrowKeysEnabled": true,
|
||||
"mouseHoverEnabled": true,
|
||||
"showMinimizedWindows": true,
|
||||
@@ -51,6 +52,7 @@ class Preferences {
|
||||
static var nextWindowShortcut: String { defaults.string(forKey: "nextWindowShortcut")! }
|
||||
static var previousWindowShortcut: String { defaults.string(forKey: "previousWindowShortcut")! }
|
||||
static var cancelShortcut: String { defaults.string(forKey: "cancelShortcut")! }
|
||||
static var closeWindowShortcut: String { defaults.string(forKey: "closeWindowShortcut")! }
|
||||
static var arrowKeysEnabled: Bool { defaults.bool(forKey: "arrowKeysEnabled") }
|
||||
static var mouseHoverEnabled: Bool { defaults.bool(forKey: "mouseHoverEnabled") }
|
||||
static var showMinimizedWindows: Bool { defaults.bool(forKey: "showMinimizedWindows") }
|
||||
|
||||
@@ -64,13 +64,17 @@ class Window {
|
||||
thumbnail = NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height))
|
||||
}
|
||||
|
||||
func close() {
|
||||
DispatchQueues.accessibilityCommands.async { [weak self] in
|
||||
self?.axUiElement.closeWindow()
|
||||
}
|
||||
}
|
||||
|
||||
func focus() {
|
||||
// implementation notes: the following sequence of actions repeats some calls. This is necessary for
|
||||
// minimized windows on other spaces, and focuses windows faster (e.g. the Security & Privacy window)
|
||||
// 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
|
||||
// You can reproduce this buggy behaviour by clicking on the dock icon, proving it's an OS bug
|
||||
DispatchQueues.focusActions.async { [weak self] in
|
||||
DispatchQueues.accessibilityCommands.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
var elementConnection = UInt32(0)
|
||||
CGSGetWindowOwner(cgsMainConnectionId, self.cgWindowId, &elementConnection)
|
||||
@@ -78,7 +82,7 @@ class Window {
|
||||
CGSGetConnectionPSN(elementConnection, &psn)
|
||||
_SLPSSetFrontProcessWithOptions(&psn, self.cgWindowId, .userGenerated)
|
||||
self.makeKeyWindow(psn)
|
||||
AXUIElementPerformAction(self.axUiElement, kAXRaiseAction as CFString)
|
||||
self.axUiElement.focusWindow()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class Windows {
|
||||
}
|
||||
|
||||
static func moveFocusedWindowIndexAfterWindowDestroyedInBackground(_ destroyedWindowIndex: Int) {
|
||||
if focusedWindowIndex >= destroyedWindowIndex {
|
||||
if focusedWindowIndex > destroyedWindowIndex {
|
||||
cycleFocusedWindowIndex(-1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,10 @@ class App: NSApplication, NSApplicationDelegate {
|
||||
isFirstSummon = true
|
||||
}
|
||||
|
||||
func closeSelectedWindow() {
|
||||
Windows.focusedWindow()?.close()
|
||||
}
|
||||
|
||||
func focusTarget() {
|
||||
debugPrint("focusTarget")
|
||||
focusSelectedWindow(Windows.focusedWindow())
|
||||
|
||||
@@ -17,6 +17,7 @@ class GeneralTab: NSViewController, PreferencePane {
|
||||
let nextWindowShortcut = LabelAndControl.makeLabelWithRecorder(NSLocalizedString("Select next window", comment: ""), "nextWindowShortcut", Preferences.nextWindowShortcut, labelPosition: .right)
|
||||
let previousWindowShortcut = LabelAndControl.makeLabelWithRecorder(NSLocalizedString("Select previous window", comment: ""), "previousWindowShortcut", Preferences.previousWindowShortcut, labelPosition: .right)
|
||||
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 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)
|
||||
@@ -27,7 +28,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].map { StackView($0) }, .vertical)
|
||||
let shortcuts = StackView([nextWindowShortcut, previousWindowShortcut, cancelShortcut, closeWindowShortcut].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: ""))
|
||||
@@ -43,7 +44,7 @@ class GeneralTab: NSViewController, PreferencePane {
|
||||
[1, 2, 3].forEach { grid.row(at: $0).topPadding = GridView.interPadding }
|
||||
grid.fit()
|
||||
|
||||
GeneralTab.shortcutsDependentOnHoldShortcut.append(contentsOf: [enableArrows[0] as! NSControl] + [nextWindowShortcut, previousWindowShortcut, cancelShortcut].map { $0[0] as! NSControl })
|
||||
GeneralTab.shortcutsDependentOnHoldShortcut.append(contentsOf: [enableArrows[0] as! NSControl] + [nextWindowShortcut, previousWindowShortcut, cancelShortcut, closeWindowShortcut].map { $0[0] as! NSControl })
|
||||
GeneralTab.arrowKeysEnabledCallback(enableArrows[0] as! NSControl)
|
||||
startAtLoginCallback(startAtLogin[1] as! NSControl)
|
||||
|
||||
@@ -58,9 +59,9 @@ class GeneralTab: NSViewController, PreferencePane {
|
||||
private static func addShortcut(_ fn: @escaping () -> Void, _ type: KeyEventType, _ shortcut: Shortcut, _ controlId: String) {
|
||||
removeShortcutIfExists(controlId, type) // remove the previous shortcut
|
||||
shortcutActions[controlId] = ShortcutAction(shortcut: shortcut, actionHandler: { _ in
|
||||
let shortcutThatInitiatesTheApp = controlId == "previousWindowShortcut" || controlId == "nextWindowShortcut"
|
||||
App.app.uiWorkShouldBeDone = shortcutThatInitiatesTheApp
|
||||
if shortcutThatInitiatesTheApp {
|
||||
let shortcutsWithUiShown = ["previousWindowShortcut", "nextWindowShortcut", "closeWindowShortcut"].contains(controlId)
|
||||
App.app.uiWorkShouldBeDone = shortcutsWithUiShown
|
||||
if shortcutsWithUiShown {
|
||||
App.app.appIsBeingUsed = true
|
||||
DispatchQueue.main.async { () -> () in fn() }
|
||||
} else if App.app.appIsBeingUsed {
|
||||
@@ -83,16 +84,24 @@ class GeneralTab: NSViewController, PreferencePane {
|
||||
removeShortcutIfExists(controlId, .down)
|
||||
return
|
||||
}
|
||||
if controlId == "nextWindowShortcut" {
|
||||
addShortcut({ App.app.showUiOrCycleSelection(1) }, .down, Shortcut(keyEquivalent: Preferences.holdShortcut + newValue)!, controlId)
|
||||
} else if controlId == "previousWindowShortcut" {
|
||||
addShortcut({ App.app.showUiOrCycleSelection(-1) }, .down, Shortcut(keyEquivalent: Preferences.holdShortcut + newValue)!, controlId)
|
||||
} else if controlId == "cancelShortcut" {
|
||||
addShortcut({ App.app.hideUi() }, .down, Shortcut(keyEquivalent: Preferences.holdShortcut + newValue)!, controlId)
|
||||
}
|
||||
let action = shortcutAction(controlId, newValue)
|
||||
addShortcut(action, .down, Shortcut(keyEquivalent: Preferences.holdShortcut + newValue)!, controlId)
|
||||
}
|
||||
}
|
||||
|
||||
private static func shortcutAction(_ controlId: String, _ newValue: String) -> () -> Void {
|
||||
if controlId == "nextWindowShortcut" {
|
||||
return { App.app.showUiOrCycleSelection(1) }
|
||||
} else if controlId == "previousWindowShortcut" {
|
||||
return { App.app.showUiOrCycleSelection(-1) }
|
||||
} else if controlId == "cancelShortcut" {
|
||||
return { App.app.hideUi() }
|
||||
} else if controlId == "closeWindowShortcut" {
|
||||
return { App.app.closeSelectedWindow() }
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
@objc static func arrowKeysEnabledCallback(_ sender: NSControl) {
|
||||
if (sender as! NSButton).state == .on {
|
||||
addShortcut({ App.app.cycleSelection(1) }, .down, Shortcut(keyEquivalent: Preferences.holdShortcut + "→")!, "→")
|
||||
|
||||
Reference in new issue
Block a user