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:
Louis Pontoise authored and lwouis committed 2020-07-16 02:17:27 +09:00
1 parent 95d75d3c85
commit 1fc620d405
7 files changed
+83 -15

No files matched your search

+4
View File
@@ -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;
};
+12 -7
View File
@@ -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")
}
}
+2 -2
View File
@@ -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
+53
View File
@@ -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)
}
+2
View File
@@ -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)
}
+7 -5
View File
@@ -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 {
+3 -1
View File
@@ -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()
}