Skip to content

Commit

Permalink
feat(quick-actions): adds quick-actions for the keys q and w
Browse files Browse the repository at this point in the history
  • Loading branch information
rbnis committed Jan 3, 2020
1 parent c69123b commit b8476e8
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 9 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ From macOS 10.12 to 10.15

* You hover and click with the `🖱️ mouse`.
* You cycle with `⇦ left arrow` and `⇨ right arrow`.
* You can close a window with `w` or quit the whole application with `q`
* You can cancel with `⎋ escape`.

## Configuration
Expand Down
4 changes: 4 additions & 0 deletions alt-tab-macos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
05F4FB3C23BA5A890001427A /* Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05F4FB3B23BA5A890001427A /* Observer.swift */; };
4807A6C623A9CD190052A53E /* SkyLight.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4807A6C523A9CD190052A53E /* SkyLight.framework */; };
D04BA02DD4152997C32CF50B /* StatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA0AF7C5DCF367FBB663C /* StatusItem.swift */; };
D04BA0496ACF1427B6E9D369 /* CGWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA78E3B4E73B40DB77174 /* CGWindow.swift */; };
Expand Down Expand Up @@ -35,6 +36,7 @@
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
05F4FB3B23BA5A890001427A /* Observer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observer.swift; sourceTree = "<group>"; };
4807A6C523A9CD190052A53E /* SkyLight.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SkyLight.framework; path = ../../../../System/Library/PrivateFrameworks/SkyLight.framework; sourceTree = "<group>"; };
D04BA02F476DE30C4647886C /* PreferencesPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesPanel.swift; sourceTree = "<group>"; };
D04BA0AF7C5DCF367FBB663C /* StatusItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusItem.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -225,6 +227,7 @@
D04BA2D2AD6B1CCA3F3A4DD7 /* SystemPermissions.swift */,
D04BA5EB5ED248C8C22CC672 /* Spaces.swift */,
D04BAE80772D25834E440975 /* TrackedWindow.swift */,
05F4FB3B23BA5A890001427A /* Observer.swift */,
);
path = logic;
sourceTree = "<group>";
Expand Down Expand Up @@ -348,6 +351,7 @@
buildActionMask = 2147483647;
files = (
D04BA960DDD1D32A3019C835 /* CollectionViewCenterFlowLayout.swift in Sources */,
05F4FB3C23BA5A890001427A /* Observer.swift in Sources */,
D04BAEF78503D7A2CEFB9E9E /* main.swift in Sources */,
D04BA20D4A240843293B3B52 /* Cell.swift in Sources */,
D04BA57A871B7269BEBAFF84 /* Keyboard.swift in Sources */,
Expand Down
16 changes: 16 additions & 0 deletions alt-tab-macos/api-wrappers/AXUIElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import Foundation
enum AXAttributeKey: String {
case windows = "AXWindows"
case minimized = "AXMinimized"
case fullScreen = "AXFullScreen"
case focusedWindow = "AXFocusedWindow"
case closeButton = "AXCloseButton"
}

extension AXUIElement {
Expand Down Expand Up @@ -47,6 +49,10 @@ extension AXUIElement {
return attribute(.minimized, Bool.self) == true
}

func isFullScreen() -> Bool {
return attribute(.fullScreen, Bool.self) == true
}

func focus(_ id: CGWindowID) {
var elementConnection = UInt32(0)
CGSGetWindowOwner(cgsMainConnectionId, id, &elementConnection)
Expand Down Expand Up @@ -80,4 +86,14 @@ extension AXUIElement {
SLPSPostEventRecordTo(&psn_, &(UnsafeMutablePointer(mutating: UnsafePointer<UInt8>(bytes1)).pointee))
SLPSPostEventRecordTo(&psn_, &(UnsafeMutablePointer(mutating: UnsafePointer<UInt8>(bytes2)).pointee))
}

func close() {
if isFullScreen() {
AXUIElementSetAttributeValue(self, AXAttributeKey.fullScreen.rawValue as CFString, 0 as CFTypeRef)
return
}
if let closeButtonRef = attribute(.closeButton, AXUIElement.self) {
AXUIElementPerformAction(closeButtonRef, kAXPressAction as CFString)
}
}
}
6 changes: 6 additions & 0 deletions alt-tab-macos/logic/Keyboard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ func keyboardHandler(proxy: CGEventTapProxy, type: CGEventType, event_: CGEvent,
let isMetaDown = event.modifierFlags.contains(Preferences.metaModifierFlag!)
let isRightArrow = event.keyCode == kVK_RightArrow
let isLeftArrow = event.keyCode == kVK_LeftArrow
let isQ = event.keyCode == kVK_ANSI_Q
let isW = event.keyCode == kVK_ANSI_W
let isEscape = event.keyCode == kVK_Escape
if isMetaDown && type == .keyDown {
if isTab && event.modifierFlags.contains(.shift) {
Expand All @@ -57,6 +59,10 @@ func keyboardHandler(proxy: CGEventTapProxy, type: CGEventType, event_: CGEvent,
return dispatchWork(application, true, { application.cycleSelection(1) })
} else if isLeftArrow && application.appIsBeingUsed {
return dispatchWork(application, true, { application.cycleSelection(-1) })
} else if isQ && application.appIsBeingUsed {
return dispatchWork(application, true, { application.quitTargetApp() })
} else if isW && application.appIsBeingUsed {
return dispatchWork(application, true, { application.closeTarget() })
} else if type == .keyDown && isEscape {
return dispatchWork(application, false, { application.hideUi() })
}
Expand Down
70 changes: 70 additions & 0 deletions alt-tab-macos/logic/Observer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Cocoa
import Foundation

enum AXNotification: String {
case destroyed = "AXUIElementDestroyed"
case rezized = "AXWindowResized"
}

enum ObserverMode {
case refreshUiOnClose
case refreshUiOnQuit
}

class Observer: NSObject {
var axObserver: AXObserver?

func createObserver(_ window: TrackedWindow, _ delegate: Application, _ mode: ObserverMode) {
let application = UnsafeMutableRawPointer(Unmanaged.passUnretained(delegate).toOpaque())

func refreshUiOnCloseCallback(observer: AXObserver, element: AXUIElement, notificationName: CFString, delegate_: UnsafeMutableRawPointer?) -> Void {
debugPrint("refreshUiOnCloseCallback")
let application = Unmanaged<Application>.fromOpaque(delegate_!).takeUnretainedValue()

if notificationName == AXNotification.rezized.rawValue as CFString {
element.close()
AXObserverRemoveNotification(observer, element, notificationName)
}
if notificationName == AXNotification.destroyed.rawValue as CFString {
if application.appIsBeingUsed {
// give the system a ms to clean up
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .milliseconds(1), execute: {
application.isOutdated = true
application.showUiOrCycleSelection(TrackedWindows.focusedWindowIndex)
})
}
AXObserverRemoveNotification(observer, element, notificationName)
}
}

if mode == .refreshUiOnClose {
if AXObserverCreate(window.ownerPid, refreshUiOnCloseCallback, &axObserver) == .success {
if (window.axWindow?.isFullScreen())! {
if AXObserverAddNotification(axObserver!, window.axWindow!, AXNotification.rezized.rawValue as CFString, application) != .success {
AXObserverRemoveNotification(axObserver!, window.axWindow!, AXNotification.rezized.rawValue as CFString)
}
}
if AXObserverAddNotification(axObserver!, window.axWindow!, AXNotification.destroyed.rawValue as CFString, application) != .success {
AXObserverRemoveNotification(axObserver!, window.axWindow!, AXNotification.destroyed.rawValue as CFString)
}
}
CFRunLoopAddSource(CFRunLoopGetCurrent(), AXObserverGetRunLoopSource(axObserver!), .defaultMode)
} else if mode == .refreshUiOnQuit {
window.app?.addObserver(self, forKeyPath: #keyPath(NSRunningApplication.isTerminated), options: .new, context: application)
}
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
debugPrint("observeValue")
if keyPath == #keyPath(NSRunningApplication.isTerminated) {
let application = Unmanaged<Application>.fromOpaque(context!).takeUnretainedValue()
if application.appIsBeingUsed {
DispatchQueue.main.async(execute: {
application.isOutdated = true
application.showUiOrCycleSelection(TrackedWindows.focusedWindowIndex)
})
}
(object as! NSRunningApplication).removeObserver(self, forKeyPath: keyPath!)
}
}
}
28 changes: 23 additions & 5 deletions alt-tab-macos/logic/TrackedWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,18 @@ class TrackedWindow {
var thumbnail: NSImage?
var icon: NSImage?
var app: NSRunningApplication?
var axWindow: AXUIElement?
private var _axWindow: AXUIElement? = nil
var axWindow: AXUIElement? {
set {
_axWindow = newValue
}
get {
if _axWindow == nil {
_axWindow = id.AXUIElementOfOtherSpaceWindow(ownerPid)
}
return _axWindow
}
}
var isMinimized: Bool
var spaceId: CGSSpaceID?
var spaceIndex: SpaceIndex?
Expand All @@ -28,7 +39,7 @@ class TrackedWindow {
if let cgImage = cgId.screenshot() {
self.thumbnail = NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height))
}
self.axWindow = axWindow
self._axWindow = axWindow
self.isMinimized = isMinimized
self.spaceId = spaceId
// System Preferences windows appear on all spaces, so we make them the current space
Expand All @@ -37,9 +48,16 @@ class TrackedWindow {
}

func focus() {
if axWindow == nil {
axWindow = id.AXUIElementOfOtherSpaceWindow(ownerPid)
}
axWindow?.focus(id)
}

func close() {
axWindow?.close()
}

func quitApp() {
if app != nil {
app?.terminate()
}
}
}
34 changes: 30 additions & 4 deletions alt-tab-macos/ui/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ let cgsMainConnectionId = CGSMainConnectionID()

class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
static let name = "AltTab"
let observer = Observer()
var statusItem: NSStatusItem?
var thumbnailsPanel: ThumbnailsPanel?
var preferencesPanel: PreferencesPanel?
var uiWorkShouldBeDone = true
var isFirstSummon = true
var isOutdated = false
var appIsBeingUsed = false

override init() {
Expand Down Expand Up @@ -49,13 +51,26 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
}

func focusTarget() {
debugPrint("focusTarget")
if appIsBeingUsed {
debugPrint("focusTarget: appIsBeingUsed")
debugPrint("focusTarget")
focusSelectedWindow(TrackedWindows.focusedWindow())
}
}

func closeTarget() {
if appIsBeingUsed {
debugPrint("closeTarget")
closeSelectedWindow(TrackedWindows.focusedWindow())
}
}

func quitTargetApp() {
if appIsBeingUsed {
debugPrint("quitTargetApp")
quitApplicationOfSelectedWindow(TrackedWindows.focusedWindow())
}
}

@objc
func showPreferencesPanel() {
if preferencesPanel == nil {
Expand All @@ -72,9 +87,10 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
func showUiOrCycleSelection(_ step: Int) {
debugPrint("showUiOrCycleSelection", step)
appIsBeingUsed = true
if isFirstSummon {
debugPrint("showUiOrCycleSelection: isFirstSummon")
if isFirstSummon || isOutdated {
debugPrint("showUiOrCycleSelection: isFirstSummon", isFirstSummon, "isOutdated", isOutdated)
isFirstSummon = false
isOutdated = false
TrackedWindows.refreshList(step)
if TrackedWindows.list.count == 0 {
appIsBeingUsed = false
Expand All @@ -96,4 +112,14 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
hideUi()
DispatchQueue.global(qos: .userInteractive).async { window?.focus() }
}

func closeSelectedWindow(_ window: TrackedWindow?) {
observer.createObserver(window!, self, .refreshUiOnClose)
DispatchQueue.global(qos: .userInteractive).async { window?.close() }
}

func quitApplicationOfSelectedWindow(_ window: TrackedWindow?) {
observer.createObserver(window!, self, .refreshUiOnQuit)
DispatchQueue.global(qos: .userInteractive).async { window?.quitApp() }
}
}

0 comments on commit b8476e8

Please sign in to comment.