feat: click outside alt-tab main window to cancel (closes #341)
* also fix issue where clicking a thumbnail would give alt-tab focus, removing it from the app currently focused * also use click on a thumbnail to focus it instead of only mouse-down
This commit is contained in:
1 parent
95d75d3c85
commit
1fc620d405
7 files changed
+83
-15
No files matched your search
@@ -87,6 +87,7 @@
|
||||
D04BACE5702CEB1819866875 /* menubar-icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BA399F1DF2C61FC2C9599 /* menubar-icon@2x.png */; };
|
||||
D04BAD1BE9DC22C48C53D195 /* AboutTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA4A26987F67DD94C827F /* AboutTab.swift */; };
|
||||
D04BAD2A7F2E8BF64EE982E9 /* TextArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA7C836A8CE8C0B8D128B /* TextArea.swift */; };
|
||||
D04BAD43C122A1C7E88B0362 /* MouseEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAF320F4D43F0DDFE063E /* MouseEvents.swift */; };
|
||||
D04BAD451966B43720120D2E /* Menubar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD905546AA93E5117B0A /* Menubar.swift */; };
|
||||
D04BADCDA9F9A6C3D6499877 /* SystemPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA7C6F2519091717F4B4E /* SystemPermissions.swift */; };
|
||||
D04BAE8B16A06A10E2FA94DE /* AccessibilityEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA8FDCF137D892114F5F3 /* AccessibilityEvents.swift */; };
|
||||
@@ -272,6 +273,7 @@
|
||||
D04BAEF1D194C2379C9970F1 /* setup_ci_master.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = setup_ci_master.sh; sourceTree = "<group>"; };
|
||||
D04BAF076A30A1BAFEDBEA66 /* 5 windows - 2 lines.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "5 windows - 2 lines.jpg"; sourceTree = "<group>"; };
|
||||
D04BAF1058D2599E6E8ABBA6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.info; path = Info.plist; sourceTree = "<group>"; };
|
||||
D04BAF320F4D43F0DDFE063E /* MouseEvents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MouseEvents.swift; sourceTree = "<group>"; };
|
||||
D04BAF6F617FCA44D1F75B60 /* alt_tab_macos.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = alt_tab_macos.entitlements; sourceTree = "<group>"; };
|
||||
D04BAF77F9D94C397EB646AE /* 1-row.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "1-row.jpg"; sourceTree = "<group>"; };
|
||||
D04BAF9052D852CD38B03EF6 /* icon_512x512@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_512x512@2x.png"; sourceTree = "<group>"; };
|
||||
@@ -820,6 +822,7 @@
|
||||
D04BA4A621A8A15660F973C1 /* KeyboardEvents.swift */,
|
||||
D04BA76AEE37D6656EB80126 /* WorkspaceEvents.swift */,
|
||||
D04BA5D9B4EBEB9E4333AE39 /* UserDefaultsEvents.swift */,
|
||||
D04BAF320F4D43F0DDFE063E /* MouseEvents.swift */,
|
||||
);
|
||||
path = events;
|
||||
sourceTree = "<group>";
|
||||
@@ -1081,6 +1084,7 @@
|
||||
D04BAFB90E00AA5D662EAF24 /* PermissionsWindow.swift in Sources */,
|
||||
D04BA7E39FA539DD8316447A /* PermissionView.swift in Sources */,
|
||||
D04BA446D702C5E252AF2319 /* TitleLabel.swift in Sources */,
|
||||
D04BAD43C122A1C7E88B0362 /* MouseEvents.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -2,19 +2,24 @@ import Foundation
|
||||
|
||||
// queues and dedicated threads to observe background events such as keyboard inputs, or accessibility events
|
||||
class BackgroundWork {
|
||||
static let mainQueueConcurrentWorkQueue = DispatchQueue.globalConcurrent("mainQueueConcurrentWorkQueue", .userInteractive)
|
||||
static let accessibilityCommandsQueue = DispatchQueue.globalConcurrent("accessibilityCommandsQueue", .userInteractive)
|
||||
static let axCallsQueue = DispatchQueue.globalConcurrent("axCallsQueue", .userInteractive)
|
||||
static let accessibilityEventsThread = BackgroundThreadWithRunLoop("accessibilityEventsThread")
|
||||
static let keyboardEventsThread = BackgroundThreadWithRunLoop("keyboardEventsThread")
|
||||
static var mainQueueConcurrentWorkQueue: DispatchQueue!
|
||||
static var accessibilityCommandsQueue: DispatchQueue!
|
||||
static var axCallsQueue: DispatchQueue!
|
||||
static var accessibilityEventsThread: BackgroundThreadWithRunLoop!
|
||||
static var keyboardEventsThread: BackgroundThreadWithRunLoop!
|
||||
static var mouseEventsThread: BackgroundThreadWithRunLoop!
|
||||
|
||||
// we cap concurrent tasks to .processorCount to avoid thread explosion on the .global queue
|
||||
static let globalSemaphore = DispatchSemaphore(value: ProcessInfo.processInfo.processorCount)
|
||||
|
||||
// swift static variables are lazy; we artificially force the threads to init
|
||||
static func start() {
|
||||
_ = accessibilityEventsThread
|
||||
_ = keyboardEventsThread
|
||||
mainQueueConcurrentWorkQueue = DispatchQueue.globalConcurrent("mainQueueConcurrentWorkQueue", .userInteractive)
|
||||
accessibilityCommandsQueue = DispatchQueue.globalConcurrent("accessibilityCommandsQueue", .userInteractive)
|
||||
axCallsQueue = DispatchQueue.globalConcurrent("axCallsQueue", .userInteractive)
|
||||
accessibilityEventsThread = BackgroundThreadWithRunLoop("accessibilityEventsThread")
|
||||
keyboardEventsThread = BackgroundThreadWithRunLoop("keyboardEventsThread")
|
||||
mouseEventsThread = BackgroundThreadWithRunLoop("mouseEventsThread")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import Cocoa
|
||||
import Carbon.HIToolbox.Events
|
||||
|
||||
fileprivate var eventTap: CFMachPort?
|
||||
|
||||
class KeyboardEvents {
|
||||
static func observe() {
|
||||
observe_()
|
||||
}
|
||||
}
|
||||
|
||||
private var eventTap: CFMachPort?
|
||||
|
||||
private func observe_() {
|
||||
let eventMask = [CGEventType.keyDown, CGEventType.keyUp, CGEventType.flagsChanged].reduce(CGEventMask(0), { $0 | (1 << $1.rawValue) })
|
||||
// CGEvent.tapCreate returns null if ensureAccessibilityCheckboxIsChecked() didn't pass
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import Cocoa
|
||||
import Carbon.HIToolbox.Events
|
||||
|
||||
fileprivate var eventTap: CFMachPort!
|
||||
fileprivate var shouldBeEnabled: Bool!
|
||||
|
||||
class MouseEvents {
|
||||
static func observe() {
|
||||
observe_()
|
||||
}
|
||||
|
||||
static func disable() {
|
||||
shouldBeEnabled = false
|
||||
CGEvent.tapEnable(tap: eventTap, enable: false)
|
||||
}
|
||||
|
||||
static func enable() {
|
||||
shouldBeEnabled = true
|
||||
CGEvent.tapEnable(tap: eventTap, enable: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func observe_() {
|
||||
let eventMask = [CGEventType.leftMouseDown, CGEventType.leftMouseUp].reduce(CGEventMask(0), { $0 | (1 << $1.rawValue) })
|
||||
// CGEvent.tapCreate returns null if ensureAccessibilityCheckboxIsChecked() didn't pass
|
||||
eventTap = CGEvent.tapCreate(
|
||||
tap: .cgSessionEventTap,
|
||||
place: .headInsertEventTap,
|
||||
options: .defaultTap,
|
||||
eventsOfInterest: eventMask,
|
||||
callback: mouseHandler,
|
||||
userInfo: nil)
|
||||
MouseEvents.disable()
|
||||
let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
|
||||
CFRunLoopAddSource(BackgroundWork.mouseEventsThread.runLoop, runLoopSource, .commonModes)
|
||||
}
|
||||
|
||||
private func mouseHandler(proxy: CGEventTapProxy, type: CGEventType, cgEvent: CGEvent, userInfo: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
|
||||
if type == .leftMouseDown && !isPointerInsideUi() {
|
||||
return nil // focused app won't receive the event
|
||||
} else if type == .leftMouseUp && cgEvent.getIntegerValueField(.mouseEventClickState) >= 1 && !isPointerInsideUi() {
|
||||
App.app.hideUi()
|
||||
return nil // focused app won't receive the event
|
||||
} else if (type == .tapDisabledByUserInput || type == .tapDisabledByTimeout) && shouldBeEnabled {
|
||||
CGEvent.tapEnable(tap: eventTap!, enable: true)
|
||||
}
|
||||
return Unmanaged.passRetained(cgEvent) // focused app will receive the event
|
||||
}
|
||||
|
||||
private func isPointerInsideUi() -> Bool {
|
||||
App.app.thumbnailsPanel.contentLayoutRect.contains(App.app.thumbnailsPanel.mouseLocationOutsideOfEventStream)
|
||||
}
|
||||
@@ -47,6 +47,7 @@ class App: NSApplication, NSApplicationDelegate {
|
||||
Applications.initialDiscovery()
|
||||
self.loadPreferencesWindow()
|
||||
KeyboardEvents.observe()
|
||||
MouseEvents.observe()
|
||||
// TODO: undeterministic; events in the queue may still be processing; good enough for now
|
||||
DispatchQueue.main.async { () -> () in Windows.sortByLevel() }
|
||||
self.preloadWindows()
|
||||
@@ -116,6 +117,7 @@ class App: NSApplication, NSApplicationDelegate {
|
||||
debugPrint("hideUi")
|
||||
appIsBeingUsed = false
|
||||
isFirstSummon = true
|
||||
MouseEvents.disable()
|
||||
thumbnailsPanel.orderOut(nil)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ class ThumbnailView: NSStackView {
|
||||
var hiddenIcon = ThumbnailFontIconView(ThumbnailFontIconView.sfSymbolCircledSlashSign, Preferences.fontIconSize, .white)
|
||||
var spaceIcon = ThumbnailFontIconView(ThumbnailFontIconView.sfSymbolCircledNumber0, Preferences.fontIconSize, .white)
|
||||
var hStackView: NSStackView!
|
||||
var mouseDownCallback: (() -> Void)!
|
||||
var mouseUpCallback: (() -> Void)!
|
||||
var mouseMovedCallback: (() -> Void)!
|
||||
var dragAndDropTimer: Timer?
|
||||
var isHighlighted = false
|
||||
@@ -96,7 +96,7 @@ class ThumbnailView: NSStackView {
|
||||
let fontIconWidth = CGFloat([fullscreenIcon, minimizedIcon, hiddenIcon, spaceIcon].filter { !$0.isHidden }.count) * (Preferences.fontIconSize + Preferences.intraCellPadding)
|
||||
assignIfDifferent(&label.textContainer!.size.width, frame.width - Preferences.iconSize - Preferences.intraCellPadding * 3 - fontIconWidth)
|
||||
assignIfDifferent(&subviews.first!.frame.size, frame.size)
|
||||
self.mouseDownCallback = { () -> Void in App.app.focusSelectedWindow(element) }
|
||||
self.mouseUpCallback = { () -> Void in App.app.focusSelectedWindow(element) }
|
||||
self.mouseMovedCallback = { () -> Void in Windows.updateFocusedWindowIndex(index) }
|
||||
// force a display to avoid flickering; see https://github.com/lwouis/alt-tab-macos/issues/197
|
||||
// quirk: display() should be called last as it resets thumbnail.frame.size somehow
|
||||
@@ -120,7 +120,7 @@ class ThumbnailView: NSStackView {
|
||||
|
||||
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
|
||||
dragAndDropTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false, block: { _ in
|
||||
self.mouseDownCallback()
|
||||
self.mouseUpCallback()
|
||||
})
|
||||
return .link
|
||||
}
|
||||
@@ -144,8 +144,10 @@ class ThumbnailView: NSStackView {
|
||||
}
|
||||
}
|
||||
|
||||
override func mouseDown(with theEvent: NSEvent) {
|
||||
mouseDownCallback()
|
||||
override func mouseUp(with theEvent: NSEvent) {
|
||||
if theEvent.clickCount >= 1 {
|
||||
mouseUpCallback()
|
||||
}
|
||||
}
|
||||
|
||||
static func makeShadow(_ color: NSColor) -> NSShadow {
|
||||
|
||||
@@ -11,6 +11,7 @@ class ThumbnailsPanel: NSPanel {
|
||||
hasShadow = false
|
||||
titleVisibility = .hidden
|
||||
styleMask.remove(.titled)
|
||||
styleMask.update(with: .nonactivatingPanel)
|
||||
backgroundColor = .clear
|
||||
contentView!.addSubview(thumbnailsView)
|
||||
// 2nd highest level possible; this allows the app to go on top of context menus
|
||||
@@ -25,7 +26,8 @@ class ThumbnailsPanel: NSPanel {
|
||||
}
|
||||
|
||||
func show() {
|
||||
makeKeyAndOrderFront(nil)
|
||||
orderFrontRegardless()
|
||||
MouseEvents.enable()
|
||||
thumbnailsView.scrollView.flashScrollers()
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user