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 */; };
|
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 */; };
|
D04BAD1BE9DC22C48C53D195 /* AboutTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA4A26987F67DD94C827F /* AboutTab.swift */; };
|
||||||
D04BAD2A7F2E8BF64EE982E9 /* TextArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA7C836A8CE8C0B8D128B /* TextArea.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 */; };
|
D04BAD451966B43720120D2E /* Menubar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD905546AA93E5117B0A /* Menubar.swift */; };
|
||||||
D04BADCDA9F9A6C3D6499877 /* SystemPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA7C6F2519091717F4B4E /* SystemPermissions.swift */; };
|
D04BADCDA9F9A6C3D6499877 /* SystemPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA7C6F2519091717F4B4E /* SystemPermissions.swift */; };
|
||||||
D04BAE8B16A06A10E2FA94DE /* AccessibilityEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA8FDCF137D892114F5F3 /* AccessibilityEvents.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D04BAF9052D852CD38B03EF6 /* icon_512x512@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_512x512@2x.png"; sourceTree = "<group>"; };
|
||||||
@@ -820,6 +822,7 @@
|
|||||||
D04BA4A621A8A15660F973C1 /* KeyboardEvents.swift */,
|
D04BA4A621A8A15660F973C1 /* KeyboardEvents.swift */,
|
||||||
D04BA76AEE37D6656EB80126 /* WorkspaceEvents.swift */,
|
D04BA76AEE37D6656EB80126 /* WorkspaceEvents.swift */,
|
||||||
D04BA5D9B4EBEB9E4333AE39 /* UserDefaultsEvents.swift */,
|
D04BA5D9B4EBEB9E4333AE39 /* UserDefaultsEvents.swift */,
|
||||||
|
D04BAF320F4D43F0DDFE063E /* MouseEvents.swift */,
|
||||||
);
|
);
|
||||||
path = events;
|
path = events;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1081,6 +1084,7 @@
|
|||||||
D04BAFB90E00AA5D662EAF24 /* PermissionsWindow.swift in Sources */,
|
D04BAFB90E00AA5D662EAF24 /* PermissionsWindow.swift in Sources */,
|
||||||
D04BA7E39FA539DD8316447A /* PermissionView.swift in Sources */,
|
D04BA7E39FA539DD8316447A /* PermissionView.swift in Sources */,
|
||||||
D04BA446D702C5E252AF2319 /* TitleLabel.swift in Sources */,
|
D04BA446D702C5E252AF2319 /* TitleLabel.swift in Sources */,
|
||||||
|
D04BAD43C122A1C7E88B0362 /* MouseEvents.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,19 +2,24 @@ import Foundation
|
|||||||
|
|
||||||
// queues and dedicated threads to observe background events such as keyboard inputs, or accessibility events
|
// queues and dedicated threads to observe background events such as keyboard inputs, or accessibility events
|
||||||
class BackgroundWork {
|
class BackgroundWork {
|
||||||
static let mainQueueConcurrentWorkQueue = DispatchQueue.globalConcurrent("mainQueueConcurrentWorkQueue", .userInteractive)
|
static var mainQueueConcurrentWorkQueue: DispatchQueue!
|
||||||
static let accessibilityCommandsQueue = DispatchQueue.globalConcurrent("accessibilityCommandsQueue", .userInteractive)
|
static var accessibilityCommandsQueue: DispatchQueue!
|
||||||
static let axCallsQueue = DispatchQueue.globalConcurrent("axCallsQueue", .userInteractive)
|
static var axCallsQueue: DispatchQueue!
|
||||||
static let accessibilityEventsThread = BackgroundThreadWithRunLoop("accessibilityEventsThread")
|
static var accessibilityEventsThread: BackgroundThreadWithRunLoop!
|
||||||
static let keyboardEventsThread = BackgroundThreadWithRunLoop("keyboardEventsThread")
|
static var keyboardEventsThread: BackgroundThreadWithRunLoop!
|
||||||
|
static var mouseEventsThread: BackgroundThreadWithRunLoop!
|
||||||
|
|
||||||
// we cap concurrent tasks to .processorCount to avoid thread explosion on the .global queue
|
// we cap concurrent tasks to .processorCount to avoid thread explosion on the .global queue
|
||||||
static let globalSemaphore = DispatchSemaphore(value: ProcessInfo.processInfo.processorCount)
|
static let globalSemaphore = DispatchSemaphore(value: ProcessInfo.processInfo.processorCount)
|
||||||
|
|
||||||
// swift static variables are lazy; we artificially force the threads to init
|
// swift static variables are lazy; we artificially force the threads to init
|
||||||
static func start() {
|
static func start() {
|
||||||
_ = accessibilityEventsThread
|
mainQueueConcurrentWorkQueue = DispatchQueue.globalConcurrent("mainQueueConcurrentWorkQueue", .userInteractive)
|
||||||
_ = keyboardEventsThread
|
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 Cocoa
|
||||||
import Carbon.HIToolbox.Events
|
import Carbon.HIToolbox.Events
|
||||||
|
|
||||||
|
fileprivate var eventTap: CFMachPort?
|
||||||
|
|
||||||
class KeyboardEvents {
|
class KeyboardEvents {
|
||||||
static func observe() {
|
static func observe() {
|
||||||
observe_()
|
observe_()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var eventTap: CFMachPort?
|
|
||||||
|
|
||||||
private func observe_() {
|
private func observe_() {
|
||||||
let eventMask = [CGEventType.keyDown, CGEventType.keyUp, CGEventType.flagsChanged].reduce(CGEventMask(0), { $0 | (1 << $1.rawValue) })
|
let eventMask = [CGEventType.keyDown, CGEventType.keyUp, CGEventType.flagsChanged].reduce(CGEventMask(0), { $0 | (1 << $1.rawValue) })
|
||||||
// CGEvent.tapCreate returns null if ensureAccessibilityCheckboxIsChecked() didn't pass
|
// 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()
|
Applications.initialDiscovery()
|
||||||
self.loadPreferencesWindow()
|
self.loadPreferencesWindow()
|
||||||
KeyboardEvents.observe()
|
KeyboardEvents.observe()
|
||||||
|
MouseEvents.observe()
|
||||||
// TODO: undeterministic; events in the queue may still be processing; good enough for now
|
// TODO: undeterministic; events in the queue may still be processing; good enough for now
|
||||||
DispatchQueue.main.async { () -> () in Windows.sortByLevel() }
|
DispatchQueue.main.async { () -> () in Windows.sortByLevel() }
|
||||||
self.preloadWindows()
|
self.preloadWindows()
|
||||||
@@ -116,6 +117,7 @@ class App: NSApplication, NSApplicationDelegate {
|
|||||||
debugPrint("hideUi")
|
debugPrint("hideUi")
|
||||||
appIsBeingUsed = false
|
appIsBeingUsed = false
|
||||||
isFirstSummon = true
|
isFirstSummon = true
|
||||||
|
MouseEvents.disable()
|
||||||
thumbnailsPanel.orderOut(nil)
|
thumbnailsPanel.orderOut(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class ThumbnailView: NSStackView {
|
|||||||
var hiddenIcon = ThumbnailFontIconView(ThumbnailFontIconView.sfSymbolCircledSlashSign, Preferences.fontIconSize, .white)
|
var hiddenIcon = ThumbnailFontIconView(ThumbnailFontIconView.sfSymbolCircledSlashSign, Preferences.fontIconSize, .white)
|
||||||
var spaceIcon = ThumbnailFontIconView(ThumbnailFontIconView.sfSymbolCircledNumber0, Preferences.fontIconSize, .white)
|
var spaceIcon = ThumbnailFontIconView(ThumbnailFontIconView.sfSymbolCircledNumber0, Preferences.fontIconSize, .white)
|
||||||
var hStackView: NSStackView!
|
var hStackView: NSStackView!
|
||||||
var mouseDownCallback: (() -> Void)!
|
var mouseUpCallback: (() -> Void)!
|
||||||
var mouseMovedCallback: (() -> Void)!
|
var mouseMovedCallback: (() -> Void)!
|
||||||
var dragAndDropTimer: Timer?
|
var dragAndDropTimer: Timer?
|
||||||
var isHighlighted = false
|
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)
|
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(&label.textContainer!.size.width, frame.width - Preferences.iconSize - Preferences.intraCellPadding * 3 - fontIconWidth)
|
||||||
assignIfDifferent(&subviews.first!.frame.size, frame.size)
|
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) }
|
self.mouseMovedCallback = { () -> Void in Windows.updateFocusedWindowIndex(index) }
|
||||||
// force a display to avoid flickering; see https://github.com/lwouis/alt-tab-macos/issues/197
|
// 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
|
// 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 {
|
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
|
||||||
dragAndDropTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false, block: { _ in
|
dragAndDropTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false, block: { _ in
|
||||||
self.mouseDownCallback()
|
self.mouseUpCallback()
|
||||||
})
|
})
|
||||||
return .link
|
return .link
|
||||||
}
|
}
|
||||||
@@ -144,8 +144,10 @@ class ThumbnailView: NSStackView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func mouseDown(with theEvent: NSEvent) {
|
override func mouseUp(with theEvent: NSEvent) {
|
||||||
mouseDownCallback()
|
if theEvent.clickCount >= 1 {
|
||||||
|
mouseUpCallback()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func makeShadow(_ color: NSColor) -> NSShadow {
|
static func makeShadow(_ color: NSColor) -> NSShadow {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class ThumbnailsPanel: NSPanel {
|
|||||||
hasShadow = false
|
hasShadow = false
|
||||||
titleVisibility = .hidden
|
titleVisibility = .hidden
|
||||||
styleMask.remove(.titled)
|
styleMask.remove(.titled)
|
||||||
|
styleMask.update(with: .nonactivatingPanel)
|
||||||
backgroundColor = .clear
|
backgroundColor = .clear
|
||||||
contentView!.addSubview(thumbnailsView)
|
contentView!.addSubview(thumbnailsView)
|
||||||
// 2nd highest level possible; this allows the app to go on top of context menus
|
// 2nd highest level possible; this allows the app to go on top of context menus
|
||||||
@@ -25,7 +26,8 @@ class ThumbnailsPanel: NSPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func show() {
|
func show() {
|
||||||
makeKeyAndOrderFront(nil)
|
orderFrontRegardless()
|
||||||
|
MouseEvents.enable()
|
||||||
thumbnailsView.scrollView.flashScrollers()
|
thumbnailsView.scrollView.flashScrollers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user