Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c65d1e60f8 |
No files matched your search
@@ -76,13 +76,11 @@
|
||||
76D02BB22BFE7C9E0056008D /* Pods_alt_tab_macos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0712B3BEA2B3780398C0999 /* Pods_alt_tab_macos.framework */; };
|
||||
AA974BA929B7D84C0099A29E /* PreviewPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA974BA829B7D84C0099A29E /* PreviewPanel.swift */; };
|
||||
BF0C803A13C409E7F01FBCAB /* SkyLight.framework.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8B94D7994AB9007F6391 /* SkyLight.framework.swift */; };
|
||||
BF0C803A5A3148C3E5F5B064 /* MetalThumbnailsView.metal in Sources */ = {isa = PBXBuildFile; fileRef = BF0C80E288D7EBC42B5D165F /* MetalThumbnailsView.metal */; };
|
||||
BF0C80484AA63306D0DB4DB2 /* SpacesEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8DF008F1EB5F23291A13 /* SpacesEvents.swift */; };
|
||||
BF0C8052AE41B1B10E42BFCE /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C875C983226CB16DBD90F /* InfoPlist.strings */; };
|
||||
BF0C8053A58DFB383A3E3351 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C8E237569FA2A8FE20707 /* InfoPlist.strings */; };
|
||||
BF0C807D26E465F467B066F7 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C849EBA5A547912BD8BB9 /* Localizable.strings */; };
|
||||
BF0C80C69EFEF93C18539925 /* PreferencesMigrations.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8F58B38F29B2662E6C2D /* PreferencesMigrations.swift */; };
|
||||
BF0C80CA549018A0099A6B42 /* MetalThumbnailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8E476869D8097FE4A9F1 /* MetalThumbnailsView.swift */; };
|
||||
BF0C80FCECC1386EB73B66E5 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C8E9B9EF1485A14AE3316 /* Localizable.strings */; };
|
||||
BF0C812C068CD751CE9BFB67 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C8D5993267A74D4875C1A /* Localizable.strings */; };
|
||||
BF0C81480366BAD71BC9A158 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C8ABA423E72218605CA4F /* InfoPlist.strings */; };
|
||||
@@ -376,7 +374,6 @@
|
||||
BF0C809C9DE36EA1317816FB /* nn */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = nn; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
BF0C80A05C11C8ADC366EC4A /* lb */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = lb; path = InfoPlist.strings; sourceTree = "<group>"; };
|
||||
BF0C80CD087C6384E0340F4E /* convert_font_to_png.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = convert_font_to_png.sh; sourceTree = "<group>"; };
|
||||
BF0C80E288D7EBC42B5D165F /* MetalThumbnailsView.metal */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.metal; path = MetalThumbnailsView.metal; sourceTree = "<group>"; };
|
||||
BF0C80F8FAA00FF3D9682F26 /* DebugMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugMenu.swift; sourceTree = "<group>"; };
|
||||
BF0C810FA0F6BC70E5886E11 /* HelperExtensionsTestable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HelperExtensionsTestable.swift; sourceTree = "<group>"; };
|
||||
BF0C8113492C0FFCB4454BC9 /* sl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = sl; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
@@ -520,7 +517,6 @@
|
||||
BF0C8E1C49845D813673D7A2 /* 20MB-photo.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "20MB-photo.jpg"; sourceTree = "<group>"; };
|
||||
BF0C8E1FBCBC7B64C69D933C /* titles-style-full.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "titles-style-full.jpg"; sourceTree = "<group>"; };
|
||||
BF0C8E25373671B528AD448D /* screen-recording@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "screen-recording@2x.png"; sourceTree = "<group>"; };
|
||||
BF0C8E476869D8097FE4A9F1 /* MetalThumbnailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetalThumbnailsView.swift; sourceTree = "<group>"; };
|
||||
BF0C8E478D68ED85DAE6BF43 /* ga */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = ga; path = InfoPlist.strings; sourceTree = "<group>"; };
|
||||
BF0C8E54658B4DAEA0528200 /* menubar-2@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menubar-2@2x.png"; sourceTree = "<group>"; };
|
||||
BF0C8E5D2BD5F7F93DF77DF5 /* sk */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = sk; path = InfoPlist.strings; sourceTree = "<group>"; };
|
||||
@@ -1599,8 +1595,6 @@
|
||||
D04BA258B56193958D60978A /* ThumbnailView.swift */,
|
||||
D04BAA998119CAA8B70A2B67 /* ThumbnailsView.swift */,
|
||||
AA974BA829B7D84C0099A29E /* PreviewPanel.swift */,
|
||||
BF0C8E476869D8097FE4A9F1 /* MetalThumbnailsView.swift */,
|
||||
BF0C80E288D7EBC42B5D165F /* MetalThumbnailsView.metal */,
|
||||
);
|
||||
path = "main-window";
|
||||
sourceTree = "<group>";
|
||||
@@ -2482,8 +2476,6 @@
|
||||
BF0C8E5F4E4D5F7DAEE07C20 /* ThumbnailsPanelBackgroundView.swift in Sources */,
|
||||
BF0C8AD478399D2E97AE3FB4 /* CustomRecorderControlTestable.swift in Sources */,
|
||||
BF0C8A5FA46C5E74A071FF4C /* ScrollwheelEvents.swift in Sources */,
|
||||
BF0C80CA549018A0099A6B42 /* MetalThumbnailsView.swift in Sources */,
|
||||
BF0C803A5A3148C3E5F5B064 /* MetalThumbnailsView.metal in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -83,7 +83,7 @@ extension AXUIElement {
|
||||
// should we give up?
|
||||
let timePassedInSeconds = Float(DispatchTime.now().uptimeNanoseconds - startTimeInNanoseconds) / 1_000_000_000
|
||||
if timePassedInSeconds >= axCallsRetriesQueueTimeoutInSeconds {
|
||||
Logger.info { "AX call failed for more than \(Int(axCallsRetriesQueueTimeoutInSeconds))s. Giving up on it. \(logFromContext(file, function, line, context, callType))" }
|
||||
Logger.info("AX call failed for more than \(Int(axCallsRetriesQueueTimeoutInSeconds))s. Giving up on it", logFromContext(file, function, line, context, callType))
|
||||
if let pid, callType == .updateWindow || callType == .updateAppWindows {
|
||||
let unresponsiveAppsMapKey = "\(callType.rawValue)\(pid)"
|
||||
axCallsRetriesQueueUnresponsiveAppsMap.withLock { $0.removeValue(forKey: unresponsiveAppsMapKey) }
|
||||
@@ -91,7 +91,7 @@ extension AXUIElement {
|
||||
return
|
||||
}
|
||||
// retry
|
||||
Logger.info { "(pid:\(pid) wid:\(wid)) \(logFromContext(file, function, line, context, callType))" }
|
||||
Logger.info(logFromContext(file, function, line, context, callType))
|
||||
retryAxCallUntilTimeout(file: file, function: function, line: line, context: context, after: .now() + humanPerceptionDelay, debounceType: debounceType, pid: pid, wid: wid, retriesQueue: true, startTimeInNanoseconds: startTimeInNanoseconds, callType: callType, block: block)
|
||||
}
|
||||
|
||||
@@ -290,7 +290,8 @@ extension AXUIElement {
|
||||
crossoverWindow(app, role, subrole, level) ||
|
||||
isAlwaysOnTopScrcpy(app, level, role, subrole)
|
||||
) || (
|
||||
(
|
||||
// CGWindowLevel == .normalWindow helps filter out iStats Pro and other top-level pop-overs, and floating windows
|
||||
level == CGWindow.normalLevel && (
|
||||
[kAXStandardWindowSubrole, kAXDialogSubrole].contains(subrole) ||
|
||||
openBoard(app) ||
|
||||
adobeAudition(app, subrole) ||
|
||||
|
||||
@@ -328,7 +328,7 @@ extension DispatchTimeInterval {
|
||||
}
|
||||
|
||||
extension NSRunningApplication {
|
||||
func debugId() -> String { "(pid:\(processIdentifier) \(bundleIdentifier ?? bundleURL?.absoluteString ?? executableURL?.absoluteString ?? localizedName))" }
|
||||
var id: String { "pid:\(String(describing: processIdentifier)) app:\(bundleIdentifier ?? bundleURL?.absoluteString ?? executableURL?.absoluteString ?? localizedName ?? "nil")" }
|
||||
}
|
||||
|
||||
// 250ms is similar to human delay in processing changes on screen
|
||||
@@ -347,16 +347,3 @@ extension NSTouch.Phase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// this changes the behavior of interpolating optional values (e.g. "\(optionalValue)")
|
||||
/// default is to return a compiler warning "string interpolation produces a debug description for an optional value; did you mean to make this explicit?"
|
||||
/// instead, we either print the value, or print "nil"
|
||||
extension String.StringInterpolation {
|
||||
mutating func appendInterpolation<T>(_ value: T?) {
|
||||
if let value {
|
||||
appendInterpolation(value)
|
||||
} else {
|
||||
appendLiteral("nil")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,27 +34,27 @@ class Logger {
|
||||
return .error
|
||||
}
|
||||
|
||||
static func debug(_ message: @escaping () -> Any?, file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
custom(level: .debug, file: file, function: function, line: line, context: context, message)
|
||||
static func debug(_ items: Any?..., file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
custom(.debug, items, file: file, function: function, line: line, context: context)
|
||||
}
|
||||
|
||||
static func info(_ message: @escaping () -> Any?, file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
custom(level: .info, file: file, function: function, line: line, context: context, message)
|
||||
static func info(_ items: Any?..., file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
custom(.info, items, file: file, function: function, line: line, context: context)
|
||||
}
|
||||
|
||||
static func warning(_ message: @escaping () -> Any?, file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
custom(level: .warning, file: file, function: function, line: line, context: context, message)
|
||||
static func warning(_ items: Any?..., file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
custom(.warning, items, file: file, function: function, line: line, context: context)
|
||||
}
|
||||
|
||||
static func error(_ message: @escaping () -> Any?, file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
custom(level: .error, file: file, function: function, line: line, context: context, message)
|
||||
static func error(_ items: Any?..., file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
custom(.error, items, file: file, function: function, line: line, context: context)
|
||||
}
|
||||
|
||||
private static func custom(level: SwiftyBeaver.Level, file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil, _ message: @escaping () -> Any?) {
|
||||
logger.custom(level: level, message: { "[\(threadName())] \(message())" }(), file: file, function: function, line: line, context: context)
|
||||
private static func custom(_ level: SwiftyBeaver.Level, _ items: [Any?], file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
let message = items.map { "\($0 ?? "nil")" }.joined(separator: " ")
|
||||
logger.custom(level: level, message: "[\(threadName())] \(message)", file: file, function: function, line: line, context: context)
|
||||
}
|
||||
|
||||
|
||||
private static func threadName() -> String {
|
||||
if Thread.isMainThread {
|
||||
return "main"
|
||||
|
||||
@@ -18,7 +18,7 @@ class MissionControl {
|
||||
stateLock.lock()
|
||||
defer { stateLock.unlock() }
|
||||
state_ = state
|
||||
Logger.info { state }
|
||||
Logger.info(state)
|
||||
}
|
||||
|
||||
// on macOS < 12, this is the way we used to guess if Mission Control is active
|
||||
|
||||
@@ -12,7 +12,7 @@ class IOKitPrototype {
|
||||
kIOHIDDeviceUsageKey: kHIDUsage_GD_Keyboard
|
||||
] as CFDictionary)
|
||||
IOHIDManagerRegisterInputValueCallback(manager, keyboardEventHandler, nil)
|
||||
IOHIDManagerScheduleWithRunLoop(manager, BackgroundWork.keyboardAndMouseAndTrackpadEventsThread.runLoop!, CFRunLoopMode.commonModes.rawValue)
|
||||
IOHIDManagerScheduleWithRunLoop(manager, BackgroundWork.keyboardAndTrackpadEventsThread.runLoop!, CFRunLoopMode.commonModes.rawValue)
|
||||
IOHIDManagerOpen(manager, IOOptionBits(kIOHIDOptionsTypeNone))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ if #available(macOS 13.1, *) {
|
||||
}
|
||||
}
|
||||
let windows = CGWindowListCopyWindowInfo([.excludeDesktopElements, .optionOnScreenOnly], kCGNullWindowID) as! [CGWindow]
|
||||
Logger.error("CGWindowListCopyWindowInfo extra", windows.sorted { $0.id()! < $1.id()! }.map { ($0.id()!, $0.title()) })
|
||||
Logger.error("CGWindowListCopyWindowInfo extra", windows.sorted { $0.id()! < $1.id()! }.map { ($0.id()!, $0.title() ?? "nil") })
|
||||
Logger.error("CGWindowListCopyWindowInfo", windows.sorted { $0.id()! < $1.id()! }.map { $0.id()! })
|
||||
Spaces.refresh()
|
||||
let spaceIdsAndIndexes = Spaces.idsAndIndexes.map { $0.0 }
|
||||
|
||||
@@ -70,7 +70,7 @@ class ATShortcut {
|
||||
}
|
||||
|
||||
func executeAction(_ isARepeat: Bool) {
|
||||
Logger.info { self.id }
|
||||
Logger.info("executeAction", id)
|
||||
ATShortcut.lastEventIsARepeat = isARepeat
|
||||
ControlsTab.executeAction(id)
|
||||
}
|
||||
@@ -95,7 +95,7 @@ class ATShortcut {
|
||||
}
|
||||
if state == .up {
|
||||
// ensure timers don't keep running if their shortcut is UP
|
||||
KeyRepeatTimer.stopTimerForRepeatingKey(id)
|
||||
KeyRepeatTimer.deactivateTimerForRepeatingKey(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class Appearance {
|
||||
static var highlightBorderWidth = CGFloat(3)
|
||||
|
||||
// theme: constants
|
||||
static var enablePanelShadow = true
|
||||
static let enablePanelShadow = true
|
||||
static var highlightFocusedBackgroundColor: NSColor { get { NSColor.systemAccentColor.withAlphaComponent(0.2) } }
|
||||
static var highlightHoveredBackgroundColor: NSColor { get { NSColor.systemAccentColor.withAlphaComponent(0.1) } }
|
||||
static var highlightFocusedBorderColor: NSColor { get { NSColor.systemAccentColor } }
|
||||
@@ -75,12 +75,10 @@ class Appearance {
|
||||
} else {
|
||||
lightTheme()
|
||||
}
|
||||
// for Liquid Glass, we don't want a shadow around the panel
|
||||
if #available(macOS 26.0, *), currentStyle == .appIcons && LiquidGlassEffectView.canUsePrivateLiquidGlassLook() {
|
||||
enablePanelShadow = false
|
||||
} else {
|
||||
enablePanelShadow = true
|
||||
}
|
||||
// // for Liquid Glass, we don't want a shadow around the panel
|
||||
// if #available(macOS 26.0, *), currentStyle == .appIcons && LiquidGlassEffectView.canUsePrivateLiquidGlassLook() {
|
||||
// enablePanelShadow = false
|
||||
// }
|
||||
}
|
||||
|
||||
private static func thumbnailsSize(_ isHorizontalScreen: Bool) {
|
||||
|
||||
@@ -19,10 +19,7 @@ class Application: NSObject {
|
||||
var dockLabel: String?
|
||||
var focusedWindow: Window? = nil
|
||||
var alreadyRequestedToQuit = false
|
||||
|
||||
func debugId() -> String {
|
||||
return "(pid:\(pid) \(bundleIdentifier ?? bundleURL?.absoluteString ?? executableURL?.absoluteString ?? localizedName))"
|
||||
}
|
||||
var debugId: String { "(pid:\(String(describing: pid)) \(bundleIdentifier ?? bundleURL?.absoluteString ?? executableURL?.absoluteString ?? localizedName ?? "nil"))" }
|
||||
|
||||
static let notifications = [
|
||||
kAXApplicationActivatedNotification,
|
||||
@@ -68,7 +65,7 @@ class Application: NSObject {
|
||||
bundleURL = runningApplication.bundleURL
|
||||
executableURL = runningApplication.executableURL
|
||||
super.init()
|
||||
Logger.debug { self.debugId() }
|
||||
Logger.debug(debugId)
|
||||
observeEventsIfEligible()
|
||||
kvObservers = [
|
||||
runningApplication.observe(\.isFinishedLaunching, options: [.new]) { [weak self] _, _ in
|
||||
@@ -86,7 +83,7 @@ class Application: NSObject {
|
||||
}
|
||||
|
||||
deinit {
|
||||
Logger.debug { self.debugId() }
|
||||
Logger.debug("Deinit app", debugId)
|
||||
}
|
||||
|
||||
func removeWindowslessAppWindow() {
|
||||
@@ -109,7 +106,7 @@ class Application: NSObject {
|
||||
}
|
||||
|
||||
func manuallyUpdateWindows() {
|
||||
AXUIElement.retryAxCallUntilTimeout(context: debugId(), pid: pid, callType: .updateAppWindows) { [weak self] in
|
||||
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: pid, callType: .updateAppWindows) { [weak self] in
|
||||
guard let self, let axUiElement = self.axUiElement else { return }
|
||||
var atLeastOneActualWindow = false
|
||||
let axWindows = try axUiElement.allWindows(self.pid)
|
||||
@@ -203,17 +200,17 @@ class Application: NSObject {
|
||||
|
||||
private func observeEvents() {
|
||||
guard let axObserver else { return }
|
||||
AXUIElement.retryAxCallUntilTimeout(context: debugId(), pid: pid, callType: .subscribeToAppNotification) { [weak self] in
|
||||
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: pid, callType: .subscribeToAppNotification) { [weak self] in
|
||||
guard let self, !self.isReallyFinishedLaunching else { return }
|
||||
if try self.axUiElement!.subscribeToNotification(axObserver, Application.notifications.first!) {
|
||||
Logger.debug { "Subscribed to app: \(self.debugId())" }
|
||||
Logger.debug("Subscribed to app", self.debugId)
|
||||
if !self.isReallyFinishedLaunching {
|
||||
// some apps have `isFinishedLaunching == true` but are actually not finished, and will return .cannotComplete
|
||||
// we consider them ready when the first subscription succeeds
|
||||
// windows opened before that point won't send a notification, so check those windows manually here
|
||||
self.isReallyFinishedLaunching = true
|
||||
for notification in Application.notifications.dropFirst() {
|
||||
AXUIElement.retryAxCallUntilTimeout(context: self.debugId(), pid: self.pid, callType: .subscribeToAppNotification) { [weak self] in
|
||||
AXUIElement.retryAxCallUntilTimeout(context: self.debugId, pid: self.pid, callType: .subscribeToAppNotification) { [weak self] in
|
||||
try self?.axUiElement!.subscribeToNotification(axObserver, notification)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class Applications {
|
||||
let zombies = believedAlive.subtracting(confirmedAlive)
|
||||
for (index, window) in Windows.list.enumerated().reversed() {
|
||||
if let wid = window.cgWindowId, zombies.contains(wid) {
|
||||
Logger.debug { window.debugId() }
|
||||
Logger.error("----", window.title, window.application.bundleIdentifier)
|
||||
Windows.removeWindow(index, window.application.pid)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,69 +2,64 @@ import Foundation
|
||||
|
||||
// queues and dedicated threads to observe background events such as keyboard inputs, or accessibility events
|
||||
class BackgroundWork {
|
||||
// we use Threads when the APIs we observe require a RunLoop (e.g. CGEvent.tapCreate, AXObserverGetRunLoopSource, CFMessagePortCreateRunLoopSource)
|
||||
// when possible, we prefer OperationQueues
|
||||
static var accessibilityEventsThread: BackgroundThreadWithRunLoop!
|
||||
static var keyboardAndMouseAndTrackpadEventsThread: BackgroundThreadWithRunLoop!
|
||||
static var missionControlThread: BackgroundThreadWithRunLoop!
|
||||
static var cliEventsThread: BackgroundThreadWithRunLoop!
|
||||
|
||||
// we use an OperationQueue for most tasks, especially when we need to call blocking APIs in parallel
|
||||
static var repeatingKeyQueue: LabeledOperationQueue!
|
||||
// we use an OperationQueue tied to the global DispatchQueue to call blocking APIs in parallel
|
||||
static var screenshotsQueue: LabeledOperationQueue!
|
||||
static var accessibilityCommandsQueue: LabeledOperationQueue!
|
||||
static var axCallsFirstAttemptQueue: LabeledOperationQueue!
|
||||
static var axCallsRetriesQueue: LabeledOperationQueue!
|
||||
static var axCallsManualDiscoveryQueue: LabeledOperationQueue!
|
||||
static var crashReportsQueue: LabeledOperationQueue!
|
||||
static var permissionsCheckOnTimerQueue: LabeledOperationQueue!
|
||||
static var permissionsSystemCallsQueue: LabeledOperationQueue!
|
||||
// we use Threads to observe events in sequence
|
||||
static var accessibilityEventsThread: BackgroundThreadWithRunLoop!
|
||||
static var keyboardAndTrackpadEventsThread: BackgroundThreadWithRunLoop!
|
||||
static var systemPermissionsThread: BackgroundThreadWithRunLoop!
|
||||
static var repeatingKeyThread: BackgroundThreadWithRunLoop!
|
||||
static var missionControlThread: BackgroundThreadWithRunLoop!
|
||||
static var cliEventsThread: BackgroundThreadWithRunLoop!
|
||||
static var debugMenu: DebugMenu!
|
||||
|
||||
private static var debugMenu: DebugMenu!
|
||||
private static var totalPotentialThreadCount = 0
|
||||
|
||||
static func preStart() {
|
||||
// we make calls to the system permissions API to know if permissions are granted. We do this on a timer
|
||||
permissionsCheckOnTimerQueue = LabeledOperationQueue("permissionsCheckOnTimer", .userInteractive, 1)
|
||||
// if macOS is overwhelmed, let's reduce the pressure on it by calling permission APIs one at a time
|
||||
permissionsSystemCallsQueue = LabeledOperationQueue("permissionsSystemCalls", .userInteractive, 1)
|
||||
}
|
||||
|
||||
static func start() {
|
||||
screenshotsQueue = LabeledOperationQueue("screenshots", .userInteractive, 8)
|
||||
screenshotsQueue = LabeledOperationQueue("screenshotsQueue", .userInteractive, 8)
|
||||
// calls to focus/close/minimize/etc windows
|
||||
// They are tried once and if they timeout we don't retry. The OS seems to still execute them even if the call timed out
|
||||
accessibilityCommandsQueue = LabeledOperationQueue("axCommands", .userInteractive, 4)
|
||||
accessibilityCommandsQueue = LabeledOperationQueue("accessibilityCommandsQueue", .userInteractive, 4)
|
||||
// calls to the AX APIs can block for a long time (e.g. if an app is unresponsive)
|
||||
// We first try the AX calls on axCallsFirstAttemptQueue. If we get a timeout, we move to axCallsRetriesQueue and retry there for a while
|
||||
axCallsFirstAttemptQueue = LabeledOperationQueue("axCallsFirst", .userInteractive, 8)
|
||||
axCallsRetriesQueue = LabeledOperationQueue("axCallsRetry", .userInteractive, 8)
|
||||
axCallsFirstAttemptQueue = LabeledOperationQueue("axCallsFirstAttemptQueue", .userInteractive, 8)
|
||||
axCallsRetriesQueue = LabeledOperationQueue("axCallsRetriesQueue", .userInteractive, 8)
|
||||
// we separate calls to manuallyUpdateWindows since those can be very heavy and numerous
|
||||
axCallsManualDiscoveryQueue = LabeledOperationQueue("axCallsManual", .userInteractive, 8)
|
||||
// we time key repeat on a background queue. We handle their consequence on the main-thread
|
||||
repeatingKeyQueue = LabeledOperationQueue("repeatingKey", .userInteractive, 1)
|
||||
axCallsManualDiscoveryQueue = LabeledOperationQueue("axCallsManualDiscoveryQueue", .userInteractive, 8)
|
||||
// we observe app and windows notifications. They arrive on this thread, and are handled off the main thread initially
|
||||
accessibilityEventsThread = BackgroundThreadWithRunLoop("axEvents", .userInteractive)
|
||||
accessibilityEventsThread = BackgroundThreadWithRunLoop("accessibilityEventsThread", .userInteractive)
|
||||
// we listen to as any keyboard events as possible on a background thread, as it's more available/reliable than the main thread
|
||||
keyboardAndMouseAndTrackpadEventsThread = BackgroundThreadWithRunLoop("inputDevices", .userInteractive)
|
||||
keyboardAndTrackpadEventsThread = BackgroundThreadWithRunLoop("keyboardAndTrackpadEventsThread", .userInteractive)
|
||||
// we time key repeat on a background thread for precision. We handle their consequence on the main-thread
|
||||
repeatingKeyThread = BackgroundThreadWithRunLoop("repeatingKeyThread", .userInteractive)
|
||||
// we main Mission Control state on a background thread. We protect reads from main-thread with an NSLock
|
||||
missionControlThread = BackgroundThreadWithRunLoop("missionControl", .userInteractive)
|
||||
missionControlThread = BackgroundThreadWithRunLoop("missionControlThread", .userInteractive)
|
||||
// we listen to CLI commands (CFMessagePort events)
|
||||
cliEventsThread = BackgroundThreadWithRunLoop("cliMessages", .userInteractive)
|
||||
cliEventsThread = BackgroundThreadWithRunLoop("cliEventsThread", .userInteractive)
|
||||
// logThreadsAndQueuesOnRepeat()
|
||||
}
|
||||
|
||||
static func startCrashReportsQueue() {
|
||||
if crashReportsQueue == nil {
|
||||
// crash reports can be sent off the main thread
|
||||
crashReportsQueue = LabeledOperationQueue("crashReports", .utility, 1)
|
||||
crashReportsQueue = LabeledOperationQueue("crashReportsQueue", .utility, 1)
|
||||
}
|
||||
}
|
||||
|
||||
static func startSystemPermissionThread() {
|
||||
// not 100% sure this shouldn't be on the main-thread; it doesn't do anything except dispatch to main.async
|
||||
systemPermissionsThread = BackgroundThreadWithRunLoop("systemPermissionsThread", .utility)
|
||||
}
|
||||
|
||||
static func addPotentialThreadCount(_ additionalCount: Int) {
|
||||
totalPotentialThreadCount += additionalCount
|
||||
// a macos process has a soft limit of 64 threads. We need to be careful to don't spawn too many threads through DispatchQueues
|
||||
assert(totalPotentialThreadCount <= 45)
|
||||
assert(totalPotentialThreadCount <= 43)
|
||||
}
|
||||
|
||||
// useful during development to inspect how many threads are used by AltTab
|
||||
@@ -87,7 +82,7 @@ class BackgroundWork {
|
||||
map[queue.underlyingQueue!.label] = queue.operations.reduce(0) { $1.isExecuting ? $0 + 1 : $0 }
|
||||
}
|
||||
let prettyPrintMap = map.sorted { $0.key < $1.key }.map { "\($0.key): \($0.value)" }.joined(separator: ", ")
|
||||
Logger.debug { prettyPrintMap }
|
||||
Logger.debug(prettyPrintMap)
|
||||
}
|
||||
|
||||
private static func logThreads() -> Void {
|
||||
@@ -114,8 +109,8 @@ class BackgroundWork {
|
||||
vm_deallocate(mach_task_self_,
|
||||
vm_address_t(bitPattern: threads),
|
||||
vm_size_t(count) * vm_size_t(MemoryLayout<thread_t>.size))
|
||||
Logger.info { "\(namedThreads.count) named threads:\(namedThreads.sorted())" }
|
||||
Logger.info { "\(unnamedThreadsCount) unnamed threads (e.g. from GCD queues)" }
|
||||
Logger.info(namedThreads.count, "Named threads:", namedThreads.sorted())
|
||||
Logger.info(unnamedThreadsCount, "Unnamed threads (e.g. from GCD queues)")
|
||||
}
|
||||
|
||||
class BackgroundThreadWithRunLoop: Thread {
|
||||
@@ -133,8 +128,8 @@ class BackgroundWork {
|
||||
}
|
||||
|
||||
override func main() {
|
||||
Logger.debug { "Thread ready" }
|
||||
// the RunLoop is lazy; calling this initializes it
|
||||
Logger.debug(name)
|
||||
// the RunLoop is lazy; calling this initialize it
|
||||
runLoop = CFRunLoopGetCurrent()
|
||||
addDummySourceToPreventRunLoopTermination()
|
||||
threadStartSemaphore.signal()
|
||||
|
||||
@@ -2,54 +2,52 @@ import Cocoa
|
||||
import ShortcutRecorder
|
||||
|
||||
class KeyRepeatTimer {
|
||||
static var timer = DispatchSource.makeTimerSource(queue: BackgroundWork.repeatingKeyQueue.strongUnderlyingQueue)
|
||||
static var timerIsSuspended = true
|
||||
static var timer: Timer?
|
||||
static var currentTimerShortcutName: String?
|
||||
|
||||
static func startRepeatingKeyPreviousWindow() {
|
||||
static func toggleRepeatingKeyPreviousWindow() {
|
||||
if let shortcut = ControlsTab.shortcuts["previousWindowShortcut"],
|
||||
// events already repeat when using a shortcut with a keycode; no need for artificial repeat
|
||||
shortcut.shortcut.keyCode == .none {
|
||||
startTimerForRepeatingKey(shortcut) {
|
||||
activateTimerForRepeatingKey(shortcut) {
|
||||
App.app.previousWindowShortcutWithRepeatingKey()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func startRepeatingKeyNextWindow() {
|
||||
static func toggleRepeatingKeyNextWindow() {
|
||||
if let shortcut = ControlsTab.shortcuts[Preferences.indexToName("nextWindowShortcut", App.app.shortcutIndex)] {
|
||||
startTimerForRepeatingKey(shortcut) {
|
||||
activateTimerForRepeatingKey(shortcut) {
|
||||
ControlsTab.executeAction(Preferences.indexToName("nextWindowShortcut", App.app.shortcutIndex))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func stopTimerForRepeatingKey(_ shortcutName: String) {
|
||||
static func deactivateTimerForRepeatingKey(_ shortcutName: String) {
|
||||
if shortcutName == currentTimerShortcutName {
|
||||
Logger.debug { shortcutName }
|
||||
Logger.debug(shortcutName)
|
||||
currentTimerShortcutName = nil
|
||||
timer.suspend()
|
||||
timerIsSuspended = true
|
||||
timer?.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
private static func startTimerForRepeatingKey(_ atShortcut: ATShortcut, _ block: @escaping () -> Void) {
|
||||
guard timerIsSuspended && atShortcut.state != .up else { return }
|
||||
currentTimerShortcutName = atShortcut.id
|
||||
// reading these user defaults every time guarantees we have the latest value, if the user has updated those
|
||||
let repeatRate = ticksToSeconds(CachedUserDefaults.globalString("KeyRepeat") ?? "6")
|
||||
let initialDelay = ticksToSeconds(CachedUserDefaults.globalString("InitialKeyRepeat") ?? "25")
|
||||
Logger.debug { "\(currentTimerShortcutName) repeatRate:\(repeatRate)s initialDelay:\(initialDelay)s" }
|
||||
timer.schedule(deadline: .now() + initialDelay, repeating: repeatRate, leeway: .milliseconds(Int(repeatRate * 1000 / 10)))
|
||||
timer.setEventHandler { handleEvent(atShortcut, block) }
|
||||
timer.resume()
|
||||
timerIsSuspended = false
|
||||
private static func activateTimerForRepeatingKey(_ atShortcut: ATShortcut, _ block: @escaping () -> Void) {
|
||||
if ((timer == nil || !timer!.isValid) && atShortcut.state != .up) {
|
||||
let repeatRate = ticksToSeconds(CachedUserDefaults.globalString("KeyRepeat") ?? "6")
|
||||
let initialDelay = ticksToSeconds(CachedUserDefaults.globalString("InitialKeyRepeat") ?? "25")
|
||||
timer = Timer(fire: Date(timeIntervalSinceNow: initialDelay), interval: repeatRate, repeats: true) { _ in
|
||||
handleEvent(atShortcut, block)
|
||||
}
|
||||
timer!.tolerance = repeatRate * 0.1
|
||||
CFRunLoopAddTimer(BackgroundWork.repeatingKeyThread.runLoop, timer!, .commonModes)
|
||||
currentTimerShortcutName = atShortcut.id
|
||||
}
|
||||
}
|
||||
|
||||
private static func handleEvent(_ atShortcut: ATShortcut, _ block: @escaping () -> Void) {
|
||||
DispatchQueue.main.async {
|
||||
if atShortcut.state == .up {
|
||||
stopTimerForRepeatingKey(atShortcut.id)
|
||||
deactivateTimerForRepeatingKey(atShortcut.id)
|
||||
} else {
|
||||
block()
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ class PreferencesMigrations {
|
||||
}
|
||||
|
||||
private static func updateToNewPreferences(_ versionInPlist: String) {
|
||||
Logger.debug { "App-version:\(App.version), Plist-version:\(versionInPlist)" }
|
||||
// x.compare(y) is .orderedDescending if x > y
|
||||
if versionInPlist.compare("7.27.0", options: .numeric) != .orderedDescending {
|
||||
migrateCursorFollowFocus()
|
||||
|
||||
@@ -4,56 +4,58 @@ import ScreenCaptureKit.SCShareableContent
|
||||
// macOS has some privacy restrictions. The user needs to grant certain permissions, app by app, in System Preferences > Security & Privacy
|
||||
class SystemPermissions {
|
||||
static var preStartupPermissionsPassed = false
|
||||
private static var timer: DispatchSourceTimer!
|
||||
static var timerPermissionsToUpdatePermissionsWindow: Timer?
|
||||
static var timerPermissionsRemovedWhileAltTabIsRunning: Timer?
|
||||
|
||||
static func ensurePermissionsAreGranted() {
|
||||
timer = DispatchSource.makeTimerSource(queue: BackgroundWork.permissionsCheckOnTimerQueue.strongUnderlyingQueue)
|
||||
timer.setEventHandler(handler: checkPermissionsOnTimer)
|
||||
setImmediateTimer()
|
||||
timer.resume()
|
||||
static func ensurePermissionsAreGranted(_ continueAppStartup: @escaping () -> Void) {
|
||||
let startupBlock = {
|
||||
pollPermissionsRemovedWhileAltTabIsRunning()
|
||||
continueAppStartup()
|
||||
}
|
||||
if AccessibilityPermission.update() != .notGranted && ScreenRecordingPermission.update() != .notGranted {
|
||||
preStartupPermissionsPassed = true
|
||||
startupBlock()
|
||||
} else {
|
||||
App.app.permissionsWindow.show(startupBlock)
|
||||
}
|
||||
}
|
||||
|
||||
private static func checkPermissionsOnTimer() {
|
||||
static func pollPermissionsToUpdatePermissionsWindow(_ startupBlock: @escaping () -> Void) {
|
||||
timerPermissionsToUpdatePermissionsWindow = Timer(timeInterval: 0.1, repeats: true) { _ in
|
||||
checkPermissionsToUpdatePermissionsWindow(startupBlock)
|
||||
}
|
||||
timerPermissionsToUpdatePermissionsWindow!.tolerance = 0.1
|
||||
CFRunLoopAddTimer(BackgroundWork.systemPermissionsThread.runLoop, timerPermissionsToUpdatePermissionsWindow!, .commonModes)
|
||||
}
|
||||
|
||||
private static func pollPermissionsRemovedWhileAltTabIsRunning() {
|
||||
timerPermissionsRemovedWhileAltTabIsRunning = Timer(timeInterval: 5, repeats: true) { _ in
|
||||
DispatchQueue.main.async {
|
||||
checkPermissionsWhileAltTabIsRunning()
|
||||
}
|
||||
}
|
||||
timerPermissionsRemovedWhileAltTabIsRunning!.tolerance = 1
|
||||
CFRunLoopAddTimer(BackgroundWork.systemPermissionsThread.runLoop, timerPermissionsRemovedWhileAltTabIsRunning!, .commonModes)
|
||||
}
|
||||
|
||||
private static func checkPermissionsWhileAltTabIsRunning() {
|
||||
Logger.debug(AccessibilityPermission.status, ScreenRecordingPermission.status, preStartupPermissionsPassed, ScreenRecordingPermission.status, Appearance.hideThumbnails, Preferences.previewFocusedWindow)
|
||||
AccessibilityPermission.update()
|
||||
ScreenRecordingPermission.update()
|
||||
if !preStartupPermissionsPassed {
|
||||
checkPermissionsPreStartup()
|
||||
} else {
|
||||
checkPermissionsPostStartup()
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
Menubar.togglePermissionCallout(ScreenRecordingPermission.status == .skipped)
|
||||
App.app.permissionsWindow.updatePermissionViews()
|
||||
}
|
||||
}
|
||||
|
||||
private static func checkPermissionsPreStartup() {
|
||||
Logger.info { "accessibility:\(AccessibilityPermission.status) screenRecording:\(ScreenRecordingPermission.status)" }
|
||||
if AccessibilityPermission.status != .notGranted && ScreenRecordingPermission.status != .notGranted {
|
||||
DispatchQueue.main.async {
|
||||
preStartupPermissionsPassed = true
|
||||
App.app.permissionsWindow?.close()
|
||||
setInfrequentTimer()
|
||||
App.app.continueAppLaunchAfterPermissionsAreGranted()
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
App.app.permissionsWindow.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func checkPermissionsPostStartup() {
|
||||
Logger.debug { "accessibility:\(AccessibilityPermission.status) screenRecording:\(ScreenRecordingPermission.status)" }
|
||||
Logger.debug(AccessibilityPermission.status)
|
||||
if AccessibilityPermission.status == .notGranted {
|
||||
Logger.error { "Accessibility permission revoked while AltTab was running; restarting" }
|
||||
DispatchQueue.main.async { App.app.restart() }
|
||||
Logger.info("accessibilityPermission not granted; restarting")
|
||||
App.app.restart()
|
||||
}
|
||||
if ScreenRecordingPermission.status == .skipped || (Appearance.hideThumbnails && !Preferences.previewFocusedWindow) { return }
|
||||
ScreenRecordingPermission.update()
|
||||
Logger.debug(ScreenRecordingPermission.status)
|
||||
Menubar.togglePermissionCallout(ScreenRecordingPermission.status == .skipped)
|
||||
if ScreenRecordingPermission.status == .notGranted {
|
||||
// permission check may yield a false negative during wake-up; we restart after 2 negative checks
|
||||
// permission check may yield a false negative during wake-up
|
||||
// we restart after 2 negative checks
|
||||
if ScreenRecordingPermission.flakyCounter >= 2 {
|
||||
Logger.error { "Screen-recording permission revoked while AltTab was running (checked 2 times); restarting" }
|
||||
DispatchQueue.main.async { App.app.restart() }
|
||||
Logger.info("screenRecordingPermission not granted 3 times; restarting")
|
||||
App.app.restart()
|
||||
} else {
|
||||
ScreenRecordingPermission.flakyCounter += 1
|
||||
}
|
||||
@@ -62,16 +64,30 @@ class SystemPermissions {
|
||||
}
|
||||
}
|
||||
|
||||
static func setInfrequentTimer() {
|
||||
timer.schedule(deadline: .now() + 5, repeating: 5, leeway: .seconds(1))
|
||||
}
|
||||
|
||||
static func setFrequentTimer() {
|
||||
timer.schedule(deadline: .now() + 0.5, repeating: 0.5, leeway: .milliseconds(500))
|
||||
}
|
||||
|
||||
private static func setImmediateTimer() {
|
||||
timer.schedule(deadline: .now(), repeating: .never, leeway: .never)
|
||||
private static func checkPermissionsToUpdatePermissionsWindow(_ startupBlock: @escaping () -> Void) {
|
||||
AccessibilityPermission.update()
|
||||
ScreenRecordingPermission.update()
|
||||
Logger.debug(AccessibilityPermission.status, ScreenRecordingPermission.status, preStartupPermissionsPassed)
|
||||
DispatchQueue.main.async {
|
||||
Menubar.togglePermissionCallout(ScreenRecordingPermission.status == .skipped)
|
||||
if AccessibilityPermission.status != App.app.permissionsWindow?.accessibilityView?.permissionStatus {
|
||||
App.app.permissionsWindow?.accessibilityView.updatePermissionStatus(AccessibilityPermission.status)
|
||||
}
|
||||
if #available(macOS 10.15, *), ScreenRecordingPermission.status != App.app.permissionsWindow?.screenRecordingView?.permissionStatus {
|
||||
App.app.permissionsWindow?.screenRecordingView?.updatePermissionStatus(ScreenRecordingPermission.status)
|
||||
}
|
||||
if !preStartupPermissionsPassed {
|
||||
if AccessibilityPermission.status != .notGranted && ScreenRecordingPermission.status != .notGranted {
|
||||
preStartupPermissionsPassed = true
|
||||
App.app.permissionsWindow?.close()
|
||||
startupBlock()
|
||||
}
|
||||
} else {
|
||||
if AccessibilityPermission.status == .notGranted || ScreenRecordingPermission.status == .notGranted {
|
||||
App.app.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,12 +116,6 @@ class ScreenRecordingPermission {
|
||||
}
|
||||
|
||||
private static func detect() -> PermissionStatus {
|
||||
if #available(macOS 15.0, *) {
|
||||
// since macOS 15.0, CGPreflightScreenCaptureAccess now returns the live-value of the permission
|
||||
// before macOS 15.0, it would return the frozen-value at the time the app was launch
|
||||
return CGPreflightScreenCaptureAccess() ? .granted :
|
||||
(Preferences.screenRecordingPermissionSkipped ? .skipped : .notGranted)
|
||||
}
|
||||
if #available(macOS 10.15, *) {
|
||||
return isGrantedOnSomeDisplay() ? .granted :
|
||||
(Preferences.screenRecordingPermissionSkipped ? .skipped : .notGranted)
|
||||
@@ -166,15 +176,12 @@ class ScreenRecordingPermission {
|
||||
private static func runWithTimeout(_ block: @escaping (@escaping (Bool) -> Void) -> Void) -> Bool {
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var result = false
|
||||
BackgroundWork.permissionsSystemCallsQueue.addOperation {
|
||||
block { r in
|
||||
result = r
|
||||
semaphore.signal()
|
||||
}
|
||||
block { r in
|
||||
result = r
|
||||
semaphore.signal()
|
||||
}
|
||||
let timeoutResult = semaphore.wait(timeout: .now() + 6)
|
||||
let timeoutResult = semaphore.wait(timeout: .now() + 1)
|
||||
if timeoutResult == .timedOut {
|
||||
Logger.error { "Screen-recording permission call timed out after 6s" }
|
||||
return false
|
||||
}
|
||||
return result
|
||||
|
||||
+8
-11
@@ -33,10 +33,7 @@ class Window {
|
||||
var application: Application
|
||||
var axObserver: AXObserver?
|
||||
var rowIndex: Int?
|
||||
|
||||
func debugId() -> String {
|
||||
return "\(application.debugId()) (wid:\(cgWindowId) title:\(title))"
|
||||
}
|
||||
var debugId: String { "(wid:\(cgWindowId.map { String(describing: $0) } ?? "nil")) \(title ?? "nil")) \(application.debugId)" }
|
||||
|
||||
init(_ axUiElement: AXUIElement, _ application: Application, _ wid: CGWindowID, _ axTitle: String?, _ isFullscreen: Bool, _ isMinimized: Bool, _ position: CGPoint?, _ size: CGSize?) {
|
||||
self.axUiElement = axUiElement
|
||||
@@ -55,7 +52,7 @@ class Window {
|
||||
// It may be responsive now since it has a window; we attempt again
|
||||
application.observeEventsIfEligible()
|
||||
checkIfFocused(application, wid)
|
||||
Logger.debug { self.debugId() }
|
||||
Logger.debug(debugId)
|
||||
observeEvents()
|
||||
}
|
||||
|
||||
@@ -64,11 +61,11 @@ class Window {
|
||||
title = bestEffortTitle(nil)
|
||||
Window.globalCreationCounter += 1
|
||||
creationOrder = Window.globalCreationCounter
|
||||
Logger.debug { self.debugId() }
|
||||
Logger.debug(debugId)
|
||||
}
|
||||
|
||||
deinit {
|
||||
Logger.debug { self.debugId() }
|
||||
Logger.debug(debugId)
|
||||
}
|
||||
|
||||
func isEqualRobust(_ otherWindowAxUiElement: AXUIElement, _ otherWindowWid: CGWindowID?) -> Bool {
|
||||
@@ -80,12 +77,12 @@ class Window {
|
||||
private func observeEvents() {
|
||||
AXObserverCreate(application.pid, AccessibilityEvents.axObserverCallback, &axObserver)
|
||||
guard let axObserver else { return }
|
||||
AXUIElement.retryAxCallUntilTimeout(context: debugId(), pid: application.pid, callType: .subscribeToWindowNotification) { [weak self] in
|
||||
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: application.pid, callType: .subscribeToWindowNotification) { [weak self] in
|
||||
guard let self else { return }
|
||||
if try self.axUiElement!.subscribeToNotification(axObserver, Window.notifications.first!) {
|
||||
Logger.debug { "Subscribed to window: \(self.debugId())" }
|
||||
Logger.debug("Subscribed to window", self.debugId)
|
||||
for notification in Window.notifications.dropFirst() {
|
||||
AXUIElement.retryAxCallUntilTimeout(context: self.debugId(), pid: self.application.pid, callType: .subscribeToWindowNotification) { [weak self] in
|
||||
AXUIElement.retryAxCallUntilTimeout(context: self.debugId, pid: self.application.pid, callType: .subscribeToWindowNotification) { [weak self] in
|
||||
try self?.axUiElement!.subscribeToNotification(axObserver, notification)
|
||||
}
|
||||
}
|
||||
@@ -295,7 +292,7 @@ class Window {
|
||||
/// some apps will not trigger AXApplicationActivated, where we usually update application.focusedWindow
|
||||
/// workaround: we check and possibly do it here
|
||||
private func checkIfFocused(_ application: Application, _ wid: CGWindowID) {
|
||||
AXUIElement.retryAxCallUntilTimeout(context: debugId(), pid: application.pid, callType: .updateWindow) {
|
||||
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: application.pid, callType: .updateWindow) {
|
||||
let focusedWid = try application.axUiElement?.focusedWindow()?.cgWindowId()
|
||||
if wid == focusedWid {
|
||||
application.focusedWindow = self
|
||||
|
||||
+10
-12
@@ -74,7 +74,7 @@ class Windows {
|
||||
static func updateIsFullscreenOnCurrentSpace() {
|
||||
let windowsOnCurrentSpace = Windows.list.filter { !$0.isWindowlessApp }
|
||||
for window in windowsOnCurrentSpace {
|
||||
AXUIElement.retryAxCallUntilTimeout(context: window.debugId(), after: .now() + humanPerceptionDelay, callType: .updateWindow) { [weak window] in
|
||||
AXUIElement.retryAxCallUntilTimeout(context: window.debugId, after: .now() + humanPerceptionDelay, callType: .updateWindow) { [weak window] in
|
||||
guard let window else { return }
|
||||
try AccessibilityEvents.updateWindowSizeAndPositionAndFullscreen(window.axUiElement!, window.cgWindowId!, window)
|
||||
}
|
||||
@@ -239,7 +239,7 @@ class Windows {
|
||||
let nextIndex = windowIndexAfterCycling(step)
|
||||
// don't wrap-around at the end, if key-repeat
|
||||
if (((step > 0 && nextIndex < focusedWindowIndex) || (step < 0 && nextIndex > focusedWindowIndex)) &&
|
||||
(!allowWrap || ATShortcut.lastEventIsARepeat || !KeyRepeatTimer.timerIsSuspended))
|
||||
(!allowWrap || ATShortcut.lastEventIsARepeat || KeyRepeatTimer.timer?.isValid ?? false))
|
||||
// don't cycle to another row, if !allowWrap
|
||||
|| (!allowWrap && list[nextIndex].rowIndex != list[focusedWindowIndex].rowIndex) {
|
||||
return
|
||||
@@ -339,17 +339,15 @@ class Windows {
|
||||
}
|
||||
}
|
||||
guard (!eligibleWindows.isEmpty || windowRemoved) else { return }
|
||||
if Preferences.videoThumbnailsAndPreview {
|
||||
if App.app.appIsBeingUsed, #available(macOS 12.3, *) {
|
||||
WindowCaptureEvents.toggleOn(source != .refreshOnlyThumbnailsAfterShowUi ? eligibleWindows : [])
|
||||
}
|
||||
} else {
|
||||
if #available(macOS 14.0, *) {
|
||||
WindowCapture.oneTimeScreenshots(eligibleWindows, source)
|
||||
} else {
|
||||
WindowCapture.oneTimeScreenshotsPrivateApi(eligibleWindows, source)
|
||||
}
|
||||
if Preferences.videoThumbnailsAndPreview && App.app.appIsBeingUsed, #available(macOS 12.3, *) {
|
||||
WindowCaptureEvents.toggleOn(source != .refreshOnlyThumbnailsAfterShowUi ? eligibleWindows : [])
|
||||
return
|
||||
}
|
||||
if !Preferences.videoThumbnailsAndPreview, #available(macOS 14.0, *) {
|
||||
WindowCapture.oneTimeScreenshots(eligibleWindows, source)
|
||||
return
|
||||
}
|
||||
WindowCapture.oneTimeScreenshotsPrivateApi(eligibleWindows, source)
|
||||
}
|
||||
|
||||
static func refreshWhichWindowsToShowTheUser() {
|
||||
|
||||
@@ -5,8 +5,8 @@ import ApplicationServices.HIServices.AXNotificationConstants
|
||||
class AccessibilityEvents {
|
||||
static let axObserverCallback: AXObserverCallback = { _, element, notificationName, _ in
|
||||
let type = notificationName as String
|
||||
Logger.debug { type }
|
||||
AXUIElement.retryAxCallUntilTimeout(context: "(type:\(type)", callType: .axEventEntrypoint) { try handleEvent(type, element) }
|
||||
Logger.debug(type)
|
||||
AXUIElement.retryAxCallUntilTimeout(callType: .axEventEntrypoint) { try handleEvent(type, element) }
|
||||
}
|
||||
|
||||
static func updateWindowSizeAndPositionAndFullscreen(_ element: AXUIElement, _ wid: CGWindowID, _ window: Window?) throws {
|
||||
@@ -33,7 +33,7 @@ class AccessibilityEvents {
|
||||
// events are handled concurrently, thus we check that the app is still running
|
||||
let pid = try element.pid()
|
||||
if try pid != ProcessInfo.processInfo.processIdentifier || (element.subrole() != kAXUnknownSubrole) {
|
||||
Logger.info { "\(type) (pid:\(pid) title:\(try? element.title()))" }
|
||||
Logger.info(type, pid, try element.title() ?? "nil")
|
||||
switch type {
|
||||
case kAXApplicationActivatedNotification: try applicationActivated(element, pid)
|
||||
case kAXApplicationHiddenNotification,
|
||||
@@ -121,7 +121,7 @@ class AccessibilityEvents {
|
||||
if let windows = Windows.updateLastFocus(element, wid) {
|
||||
App.app.refreshOpenUi(windows, .refreshUiAfterExternalEvent)
|
||||
} else {
|
||||
AXUIElement.retryAxCallUntilTimeout(context: "(wid:\(wid) pid:\(pid))", pid: pid, callType: .updateWindow) {
|
||||
AXUIElement.retryAxCallUntilTimeout(context: "wid:\(wid) pid:\(pid)", pid: pid, callType: .updateWindow) {
|
||||
if let (title, role, subrole, isMinimized, isFullscreen) = try element.windowAttributes() {
|
||||
let position = try element.position()
|
||||
let size = try element.size()
|
||||
@@ -167,7 +167,7 @@ class AccessibilityEvents {
|
||||
|
||||
private static func windowTitleChanged(_ element: AXUIElement, _ pid: pid_t) throws {
|
||||
let wid = try element.cgWindowId()
|
||||
AXUIElement.retryAxCallUntilTimeout(context: "(wid:\(wid) pid:\(pid))", debounceType: .windowTitleChanged, pid: pid, wid: wid, callType: .updateWindow) {
|
||||
AXUIElement.retryAxCallUntilTimeout(context: "\(wid)", debounceType: .windowTitleChanged, pid: pid, wid: wid, callType: .updateWindow) {
|
||||
if let (title, _, _, isMinimized, isFullscreen) = try element.windowAttributes() {
|
||||
DispatchQueue.main.async {
|
||||
if let window = (Windows.list.first { $0.isEqualRobust(element, wid) }), title != window.title {
|
||||
@@ -183,7 +183,7 @@ class AccessibilityEvents {
|
||||
|
||||
private static func windowResizedOrMoved(_ element: AXUIElement, _ pid: pid_t) throws {
|
||||
let wid = try element.cgWindowId()
|
||||
AXUIElement.retryAxCallUntilTimeout(context: "(wid:\(wid) pid:\(pid))", debounceType: .windowResizedOrMoved, pid: pid, wid: wid, callType: .updateWindow) {
|
||||
AXUIElement.retryAxCallUntilTimeout(context: "\(wid)", debounceType: .windowResizedOrMoved, pid: pid, wid: wid, callType: .updateWindow) {
|
||||
try updateWindowSizeAndPositionAndFullscreen(element, wid, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ class CliEvents {
|
||||
let source = CFMessagePortCreateRunLoopSource(nil, messagePort, 0) {
|
||||
CFRunLoopAddSource(BackgroundWork.cliEventsThread.runLoop, source, .commonModes)
|
||||
} else {
|
||||
Logger.error { "Can't listen on message port. Is another AltTab already running?" }
|
||||
Logger.error("Can't listen on message port. Is another AltTab already running?")
|
||||
// TODO: should we quit or restart here?
|
||||
// It's complex since AltTab can be restarted sometimes,
|
||||
// and the new instance may coexist with the old for some duration
|
||||
@@ -16,16 +16,16 @@ class CliEvents {
|
||||
}
|
||||
|
||||
private static let handleEvent: CFMessagePortCallBack = { (_: CFMessagePort?, _: Int32, _ data: CFData?, _: UnsafeMutableRawPointer?) in
|
||||
Logger.debug { "" }
|
||||
Logger.debug()
|
||||
if let data,
|
||||
let message = String(data: data as Data, encoding: .utf8) {
|
||||
Logger.info { message }
|
||||
Logger.info(message)
|
||||
let output = CliServer.executeCommandAndSendReponse(message)
|
||||
if let responseData = try? CliServer.jsonEncoder.encode(output) as CFData {
|
||||
return Unmanaged.passRetained(responseData)
|
||||
}
|
||||
}
|
||||
Logger.error { "Failed to decode message" }
|
||||
Logger.error("Failed to decode message")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ class DockEvents {
|
||||
AXUIElement.retryAxCallUntilTimeout(callType: .subscribeToDockNotification) {
|
||||
if try axUiElement!.subscribeToNotification(axObserver!, notification.rawValue, nil) {
|
||||
if notification == MissionControlState.showDesktop {
|
||||
Logger.debug { "Subscribed to Dock" }
|
||||
Logger.debug("Subscribed to Dock")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ class DockEvents {
|
||||
}
|
||||
|
||||
private static let handleEvent: AXObserverCallback = { _, _, notificationName, _ in
|
||||
Logger.debug { notificationName }
|
||||
Logger.debug(notificationName)
|
||||
MissionControl.setState(MissionControlState(rawValue: notificationName as String)!)
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ class KeyboardEvents {
|
||||
fn(shortcutId, shortcut)
|
||||
}
|
||||
}
|
||||
Logger.info { "disabled:\(shouldDisable)" }
|
||||
Logger.info("toggleGlobalShortcuts", shouldDisable)
|
||||
globalShortcutsAreDisabled = shouldDisable
|
||||
}
|
||||
}
|
||||
@@ -97,7 +97,7 @@ class KeyboardEvents {
|
||||
userInfo: nil)
|
||||
if let eventTap {
|
||||
let runLoopSource = CFMachPortCreateRunLoopSource(nil, eventTap, 0)
|
||||
CFRunLoopAddSource(BackgroundWork.keyboardAndMouseAndTrackpadEventsThread.runLoop, runLoopSource, .commonModes)
|
||||
CFRunLoopAddSource(BackgroundWork.keyboardAndTrackpadEventsThread.runLoop, runLoopSource, .commonModes)
|
||||
} else {
|
||||
App.app.restart()
|
||||
}
|
||||
|
||||
@@ -13,19 +13,7 @@ class KeyboardEventsTestable {
|
||||
|
||||
@discardableResult
|
||||
func handleKeyboardEvent(_ globalId: Int?, _ shortcutState: ShortcutState?, _ keyCode: UInt32?, _ modifiers: NSEvent.ModifierFlags?, _ isARepeat: Bool) -> Bool {
|
||||
if let globalId, let shortcutState {
|
||||
Logger.debug {
|
||||
let shortcut = KeyboardEventsTestable.globalShortcutsIds.first { $0.value == globalId }
|
||||
return "globalShortcut:\(shortcut?.key ?? "") state:\(shortcutState)"
|
||||
}
|
||||
} else {
|
||||
// TODO: use proper pattern from SwiftBeaver to not compute SymbolicModifierFlagsTransformer when logs are off
|
||||
Logger.debug {
|
||||
let modifiersAsString = modifiers.flatMap { SymbolicModifierFlagsTransformer.shared.transformedValue(NSNumber(value: $0.rawValue)) }
|
||||
let keyCodeAsString = keyCode.flatMap { SymbolicKeyCodeTransformer.shared.transformedValue(NSNumber(value: $0)) }
|
||||
return "keys:\(modifiersAsString ?? "")\(keyCodeAsString ?? "") isARepeat:\(isARepeat)"
|
||||
}
|
||||
}
|
||||
Logger.debug(globalId, shortcutState, keyCode, modifiers, isARepeat, NSEvent.modifierFlags)
|
||||
var someShortcutTriggered = false
|
||||
for shortcut in ControlsTab.shortcuts.values {
|
||||
if shortcut.matches(globalId, shortcutState, keyCode, modifiers) && shortcut.shouldTrigger() {
|
||||
|
||||
@@ -30,7 +30,6 @@ class MouseEvents {
|
||||
if let eventTap {
|
||||
MouseEvents.toggle(false)
|
||||
let runLoopSource = CFMachPortCreateRunLoopSource(nil, eventTap, 0)
|
||||
// we run on main-thread directly since all we do is check UI coordinates, which we must do on main-thread
|
||||
CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSource, .commonModes)
|
||||
} else {
|
||||
App.app.restart()
|
||||
|
||||
@@ -2,23 +2,24 @@ import Cocoa
|
||||
|
||||
class RunningApplicationsEvents {
|
||||
private static var appsObserver: NSKeyValueObservation!
|
||||
private static var previousValueOfRunningApps: Set<NSRunningApplication>!
|
||||
|
||||
static func observe() {
|
||||
// we can't observe NSWorkspace.didLaunchApplicationNotification or NSWorkspace.didTerminateApplicationNotification
|
||||
// these only trigger for some apps, mostly GUI app. We need to track all processes as any could spawn a window
|
||||
appsObserver = NSWorkspace.shared.observe(\.runningApplications, options: [.old, .new], changeHandler: { (_, change) in handleEvent(change) })
|
||||
previousValueOfRunningApps = Set(NSWorkspace.shared.runningApplications)
|
||||
appsObserver = NSWorkspace.shared.observe(\.runningApplications, options: [.old, .new], changeHandler: { (_, _) in handleEvent() })
|
||||
}
|
||||
|
||||
private static func handleEvent(_ change: NSKeyValueObservedChange<[NSRunningApplication]>) {
|
||||
let launched = change.newValue
|
||||
let quit = change.oldValue
|
||||
if let launched {
|
||||
Logger.debug { "launched:\(launched.map { $0.debugId() })" }
|
||||
Applications.addRunningApplications(launched)
|
||||
private static func handleEvent() {
|
||||
let workspaceApps = Set(NSWorkspace.shared.runningApplications)
|
||||
let added = workspaceApps.subtracting(previousValueOfRunningApps)
|
||||
let removed = previousValueOfRunningApps.subtracting(workspaceApps)
|
||||
Logger.info("added:", added.map { $0.id }, "removed:", removed.map { $0.id })
|
||||
if !added.isEmpty {
|
||||
Applications.addRunningApplications(Array(added))
|
||||
}
|
||||
if let quit {
|
||||
Logger.debug { "quit:\(quit.map { $0.debugId() })" }
|
||||
Applications.removeRunningApplications(quit)
|
||||
if !removed.isEmpty {
|
||||
Applications.removeRunningApplications(Array(removed))
|
||||
}
|
||||
previousValueOfRunningApps = workspaceApps
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,14 @@ class ScreensEvents {
|
||||
}
|
||||
|
||||
@objc private static func handleEvent(_ notification: Notification) {
|
||||
Logger.debug { notification.name.rawValue }
|
||||
Logger.debug(notification.name.rawValue)
|
||||
Spaces.refresh()
|
||||
// a screen added or removed, or screen resolution change can mess up layout; we reset components
|
||||
App.app.resetPreferencesDependentComponents()
|
||||
// a screen added or removed can shuffle windows around Spaces; we refresh them
|
||||
App.app.refreshOpenUi(Windows.list, .refreshUiAfterExternalEvent)
|
||||
Logger.info { "screens:\(NSScreen.screens.map { ($0.uuid() ?? "nil" as CFString, $0.frame) })" }
|
||||
Logger.info { "spaces:\(Spaces.screenSpacesMap)" }
|
||||
Logger.info { "current space:\(Spaces.currentSpaceIndex) (id:\(Spaces.currentSpaceId))" }
|
||||
Logger.info("screens", NSScreen.screens.map { ($0.uuid() ?? "nil" as CFString, $0.frame) })
|
||||
Logger.info("spaces", Spaces.screenSpacesMap)
|
||||
Logger.info("current space", Spaces.currentSpaceIndex, Spaces.currentSpaceId)
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ class ScrollwheelEvents {
|
||||
userInfo: nil)
|
||||
if let eventTap {
|
||||
let runLoopSource = CFMachPortCreateRunLoopSource(nil, eventTap, 0)
|
||||
CFRunLoopAddSource(BackgroundWork.keyboardAndMouseAndTrackpadEventsThread.runLoop, runLoopSource, .commonModes)
|
||||
CFRunLoopAddSource(BackgroundWork.keyboardAndTrackpadEventsThread.runLoop, runLoopSource, .commonModes)
|
||||
} else {
|
||||
App.app.restart()
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ class SpacesEvents {
|
||||
}
|
||||
|
||||
@objc private static func handleEvent(_ notification: Notification) {
|
||||
Logger.debug { notification.name.rawValue }
|
||||
Logger.debug(notification.name.rawValue)
|
||||
// if UI was kept open during Space transition, the Spaces may be obsolete; we refresh them
|
||||
App.app.refreshOpenUi(Windows.list, .refreshUiAfterExternalEvent)
|
||||
Logger.info { "current space:\(Spaces.currentSpaceIndex) (id:\(Spaces.currentSpaceId))" }
|
||||
Logger.info("current space", Spaces.currentSpaceIndex, Spaces.currentSpaceId)
|
||||
// Workaround for Safari full-screen videos
|
||||
// when full-screening a video, Safari spawns a second full-screen window called "Safari"
|
||||
// this window doesn't emit resize/move events. It doesn't pass isActualWindow on creation. It's added on focusedWindowChanged
|
||||
|
||||
@@ -8,8 +8,8 @@ class SystemAppearanceEvents {
|
||||
}
|
||||
|
||||
@objc private static func handleEvent(_ notification: Notification) {
|
||||
Logger.debug { notification.name.rawValue }
|
||||
Logger.info { UserDefaults.standard.string(forKey: "AppleInterfaceStyle") ?? "Light" }
|
||||
Logger.debug(notification.name.rawValue)
|
||||
Logger.info(UserDefaults.standard.string(forKey: "AppleInterfaceStyle") ?? "Light")
|
||||
// fix layout issues by resetting components
|
||||
App.app.resetPreferencesDependentComponents()
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ class SystemScrollerStyleEvents {
|
||||
}
|
||||
|
||||
@objc private static func handleEvent(_ notification: Notification) {
|
||||
Logger.debug { notification.name.rawValue }
|
||||
Logger.info { App.app.thumbnailsPanel.thumbnailsView.scrollView.scrollerStyle == .overlay ? "overlay" : "legacy" }
|
||||
Logger.debug(notification.name.rawValue)
|
||||
Logger.info(App.app.thumbnailsPanel.thumbnailsView.scrollView.scrollerStyle == .overlay ? "overlay" : "legacy")
|
||||
// force overlay style after a change in System Preference > General > Show scroll bars
|
||||
App.app.thumbnailsPanel?.thumbnailsView.scrollView.scrollerStyle = .overlay
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ class TrackpadEvents {
|
||||
userInfo: nil)
|
||||
if let eventTap {
|
||||
let runLoopSource = CFMachPortCreateRunLoopSource(nil, eventTap, 0)
|
||||
CFRunLoopAddSource(BackgroundWork.keyboardAndMouseAndTrackpadEventsThread.runLoop, runLoopSource, .commonModes)
|
||||
CFRunLoopAddSource(BackgroundWork.keyboardAndTrackpadEventsThread.runLoop, runLoopSource, .commonModes)
|
||||
} else {
|
||||
App.app.restart()
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class UserDefaultsEvents: NSObject {
|
||||
}
|
||||
|
||||
private func handleEvent(_ keyPath: String?) {
|
||||
Logger.debug { "\(keyPath ?? "keyPath:nil") updatePolicy:\(Preferences.updatePolicy) policyLock:\(PoliciesTab.policyLock)" }
|
||||
Logger.debug(keyPath, PoliciesTab.policyLock, Preferences.updatePolicy)
|
||||
guard !PoliciesTab.policyLock else { return }
|
||||
let buttons = PoliciesTab.updatesPolicyDropdown!
|
||||
let id = buttonIdToUpdate()
|
||||
|
||||
@@ -108,16 +108,12 @@ class WindowCaptureEvents {
|
||||
BackgroundWork.screenshotsQueue.trackCallbacks {
|
||||
if sampleBuffer.isValid,
|
||||
let pixelBuffer = sampleBuffer.pixelBuffer() {
|
||||
let pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
|
||||
App.app.thumbnailsPanel.thumbnailsView.metalView.enqueue(pixelBuffer, pts, for: self.wid)
|
||||
|
||||
// TODO: if static thumbnails then we need to draw the metal view on-event, instead of 60hz
|
||||
|
||||
// DispatchQueue.main.async { [wid = self.wid] in
|
||||
// if let window = (Windows.list.first { $0.cgWindowId == wid }) {
|
||||
// window.refreshThumbnail(.pixelBuffer(pixelBuffer))
|
||||
// }
|
||||
// }
|
||||
let wid_ = wid
|
||||
DispatchQueue.main.async {
|
||||
if let window = (Windows.list.first { $0.cgWindowId == wid_ }) {
|
||||
window.refreshThumbnail(.pixelBuffer(pixelBuffer))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,19 +127,12 @@ class WindowCaptureEvents {
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate let scStreamBackgroundColorDark = NSColor.init(white: 0.23, alpha: 1).cgColor
|
||||
fileprivate let scStreamBackgroundColorLight = NSColor.white.cgColor
|
||||
|
||||
@available(macOS 12.3, *)
|
||||
extension SCStreamConfiguration {
|
||||
static func forWindow(_ window: SCWindow, _ video: Bool) -> SCStreamConfiguration {
|
||||
let config = SCStreamConfiguration()
|
||||
config.setWindowSize(window)
|
||||
// ~60% memory reduction compared to kCVPixelFormatType_32BGRA
|
||||
config.pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
|
||||
// kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange doesn't have transparency, so we end up with an opaque background color around the window corners
|
||||
// we use a background color that try and hide these corners as much as possible
|
||||
config.backgroundColor = Appearance.currentTheme == .dark ? scStreamBackgroundColorDark : scStreamBackgroundColorLight
|
||||
config.pixelFormat = kCVPixelFormatType_32BGRA
|
||||
config.showsCursor = false
|
||||
if video {
|
||||
config.minimumFrameInterval = CMTime(value: 1, timescale: CMTimeScale(60))
|
||||
@@ -172,19 +161,6 @@ extension CMSampleBuffer {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@available(macOS 12.3, *)
|
||||
func metalTexture(_ device: MTLDevice) -> MTLTexture? {
|
||||
guard let pixelBuffer = pixelBuffer(),
|
||||
let surface = CVPixelBufferGetIOSurface(pixelBuffer)?.takeUnretainedValue() else { return nil }
|
||||
let desc = MTLTextureDescriptor.texture2DDescriptor(
|
||||
pixelFormat: .bgra8Unorm,
|
||||
width: CVPixelBufferGetWidth(pixelBuffer),
|
||||
height: CVPixelBufferGetHeight(pixelBuffer),
|
||||
mipmapped: false
|
||||
)
|
||||
return device.makeTexture(descriptor: desc, iosurface: surface, plane: 0)
|
||||
}
|
||||
}
|
||||
|
||||
class WindowCapture {
|
||||
|
||||
+36
-38
@@ -56,7 +56,7 @@ class App: AppCenterApplication {
|
||||
}
|
||||
|
||||
func hideUi(_ keepPreview: Bool = false) {
|
||||
Logger.info { "appIsBeingUsed:\(self.appIsBeingUsed)" }
|
||||
Logger.info(appIsBeingUsed)
|
||||
guard appIsBeingUsed else { return } // already hidden
|
||||
appIsBeingUsed = false
|
||||
isFirstSummon = true
|
||||
@@ -118,7 +118,7 @@ class App: AppCenterApplication {
|
||||
func focusTarget() {
|
||||
guard appIsBeingUsed else { return } // already hidden
|
||||
let focusedWindow = Windows.focusedWindow()
|
||||
Logger.info { focusedWindow?.debugId() }
|
||||
Logger.info(focusedWindow?.debugId)
|
||||
focusSelectedWindow(focusedWindow)
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ class App: AppCenterApplication {
|
||||
}
|
||||
|
||||
@objc func checkPermissions(_ sender: NSMenuItem) {
|
||||
permissionsWindow.show()
|
||||
permissionsWindow.show({})
|
||||
}
|
||||
|
||||
@objc func supportProject() {
|
||||
@@ -176,7 +176,7 @@ class App: AppCenterApplication {
|
||||
|
||||
func previousWindowShortcutWithRepeatingKey() {
|
||||
cycleSelection(.trailing)
|
||||
KeyRepeatTimer.startRepeatingKeyPreviousWindow()
|
||||
KeyRepeatTimer.toggleRepeatingKeyPreviousWindow()
|
||||
}
|
||||
|
||||
func focusSelectedWindow(_ selectedWindow: Window?) {
|
||||
@@ -220,9 +220,10 @@ class App: AppCenterApplication {
|
||||
|
||||
func showUiOrCycleSelection(_ shortcutIndex: Int, _ forceDoNothingOnRelease_: Bool) {
|
||||
forceDoNothingOnRelease = forceDoNothingOnRelease_
|
||||
Logger.debug { "isFirstSummon:\(self.isFirstSummon) shortcutIndex:\(shortcutIndex)" }
|
||||
Logger.debug(shortcutIndex, self.shortcutIndex, isFirstSummon)
|
||||
App.app.appIsBeingUsed = true
|
||||
if isFirstSummon || shortcutIndex != self.shortcutIndex {
|
||||
MainMenu.toggle(enabled: false)
|
||||
NSScreen.updatePreferred()
|
||||
if isVeryFirstSummon {
|
||||
Windows.sortByLevel()
|
||||
@@ -245,7 +246,7 @@ class App: AppCenterApplication {
|
||||
}
|
||||
} else {
|
||||
cycleSelection(.leading)
|
||||
KeyRepeatTimer.startRepeatingKeyNextWindow()
|
||||
KeyRepeatTimer.toggleRepeatingKeyNextWindow()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +257,7 @@ class App: AppCenterApplication {
|
||||
refreshOpenUi([], .showUi)
|
||||
guard appIsBeingUsed else { return }
|
||||
thumbnailsPanel.show()
|
||||
KeyRepeatTimer.startRepeatingKeyNextWindow()
|
||||
KeyRepeatTimer.toggleRepeatingKeyNextWindow()
|
||||
Windows.refreshThumbnailsAsync(Windows.list, .refreshOnlyThumbnailsAfterShowUi)
|
||||
}
|
||||
|
||||
@@ -281,7 +282,7 @@ extension App: NSApplicationDelegate {
|
||||
appCenterDelegate = AppCenterCrash()
|
||||
App.shared.disableRelaunchOnLogin()
|
||||
Logger.initialize()
|
||||
Logger.info { "Launching AltTab \(App.version)" }
|
||||
Logger.info("Launching AltTab \(App.version)")
|
||||
#if DEBUG
|
||||
UserDefaults.standard.set(true, forKey: "NSConstraintBasedLayoutVisualizeMutuallyExclusiveConstraints")
|
||||
#endif
|
||||
@@ -290,38 +291,35 @@ extension App: NSApplicationDelegate {
|
||||
#endif
|
||||
AXUIElement.setGlobalTimeout()
|
||||
Preferences.initialize()
|
||||
BackgroundWork.startSystemPermissionThread()
|
||||
permissionsWindow = PermissionsWindow()
|
||||
BackgroundWork.preStart()
|
||||
SystemPermissions.ensurePermissionsAreGranted()
|
||||
}
|
||||
|
||||
func continueAppLaunchAfterPermissionsAreGranted() {
|
||||
Logger.info { "System permissions are granted; continuing launch" }
|
||||
BackgroundWork.start()
|
||||
NSScreen.updatePreferred()
|
||||
Appearance.update()
|
||||
Menubar.initialize()
|
||||
MainMenu.loadFromXib()
|
||||
self.thumbnailsPanel = ThumbnailsPanel()
|
||||
self.previewPanel = PreviewPanel()
|
||||
Spaces.refresh()
|
||||
SpacesEvents.observe()
|
||||
ScreensEvents.observe()
|
||||
SystemAppearanceEvents.observe()
|
||||
SystemScrollerStyleEvents.observe()
|
||||
Applications.initialDiscovery()
|
||||
self.preferencesWindow = PreferencesWindow()
|
||||
self.feedbackWindow = FeedbackWindow()
|
||||
KeyboardEvents.addEventHandlers()
|
||||
MouseEvents.observe()
|
||||
TrackpadEvents.observe()
|
||||
CliEvents.observe()
|
||||
// login item and plist updates can be done a bit later, to accelerate launch
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { GeneralTab.startAtLoginCallback() }
|
||||
Logger.info { "Finished launching AltTab" }
|
||||
#if DEBUG
|
||||
SystemPermissions.ensurePermissionsAreGranted { [weak self] in
|
||||
guard let self else { return }
|
||||
Logger.info("System Permissions are granted")
|
||||
BackgroundWork.start()
|
||||
NSScreen.updatePreferred()
|
||||
Appearance.update()
|
||||
Menubar.initialize()
|
||||
MainMenu.loadFromXib()
|
||||
self.thumbnailsPanel = ThumbnailsPanel()
|
||||
self.previewPanel = PreviewPanel()
|
||||
Spaces.refresh()
|
||||
SpacesEvents.observe()
|
||||
ScreensEvents.observe()
|
||||
SystemAppearanceEvents.observe()
|
||||
SystemScrollerStyleEvents.observe()
|
||||
Applications.initialDiscovery()
|
||||
self.preferencesWindow = PreferencesWindow()
|
||||
self.feedbackWindow = FeedbackWindow()
|
||||
KeyboardEvents.addEventHandlers()
|
||||
MouseEvents.observe()
|
||||
TrackpadEvents.observe()
|
||||
CliEvents.observe()
|
||||
Logger.info("AltTab finished launching")
|
||||
#if DEBUG
|
||||
// self.showPreferencesWindow()
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
|
||||
|
||||
@@ -88,7 +88,7 @@ class FeedbackWindow: NSWindow {
|
||||
func openTicket() {
|
||||
URLSession.shared.dataTask(with: prepareRequest(), completionHandler: { data, response, error in
|
||||
if error != nil || response == nil || (response as! HTTPURLResponse).statusCode != 201 {
|
||||
Logger.error { "HTTP call failed. response:\(response) error:\(error) data:\(data.flatMap { String(data: $0, encoding: .utf8) })" }
|
||||
Logger.error("HTTP call failed:", response, error, data.flatMap { String(data: $0, encoding: .utf8) })
|
||||
}
|
||||
}).resume()
|
||||
issueTitle.stringValue = ""
|
||||
|
||||
+6
-17
@@ -1,6 +1,5 @@
|
||||
class MainMenu {
|
||||
private static var mainMenu: NSMenu!
|
||||
private static var menuItemsWithShortcut = [NSMenuItem: String]()
|
||||
|
||||
/// classic keyboard shortcuts like copy-and-paste are missing without a MainMenu. We generated the default menu from XCode and load it
|
||||
/// see https://stackoverflow.com/a/3746058/2249756
|
||||
@@ -9,26 +8,16 @@ class MainMenu {
|
||||
Bundle.main.loadNibNamed("MainMenu", owner: self, topLevelObjects: &menuObjects)
|
||||
mainMenu = menuObjects?.first { $0 is NSMenu } as? NSMenu
|
||||
App.shared.mainMenu = mainMenu
|
||||
rememberMenuItemsWithShortcut()
|
||||
}
|
||||
|
||||
/// we toggle the mainMenu off when showing the main window, and on when showing secondary windows
|
||||
static func toggle(enabled: Bool) {
|
||||
for (item, keyEquivalent) in menuItemsWithShortcut {
|
||||
item.keyEquivalent = enabled ? keyEquivalent : ""
|
||||
}
|
||||
}
|
||||
|
||||
private static func rememberMenuItemsWithShortcut() {
|
||||
var stack: [NSMenu] = [mainMenu]
|
||||
while let menu = stack.popLast() {
|
||||
for item in menu.items {
|
||||
if !item.keyEquivalent.isEmpty {
|
||||
menuItemsWithShortcut[item] = item.keyEquivalent
|
||||
}
|
||||
if let submenu = item.submenu {
|
||||
stack.append(submenu)
|
||||
}
|
||||
if enabled {
|
||||
if App.shared.mainMenu == nil {
|
||||
App.shared.mainMenu = mainMenu
|
||||
}
|
||||
} else {
|
||||
App.shared.mainMenu = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ class Menubar {
|
||||
static var statusItem: NSStatusItem!
|
||||
static var menu: NSMenu!
|
||||
static var permissionCalloutMenuItems: [NSMenuItem]?
|
||||
static var isShowingPermissionCallout = false
|
||||
|
||||
static func addMenuItem(_ title: String, _ action: Selector, _ keyEquivalent: String, _ symbolName: String?, _ color: NSColor? = nil) {
|
||||
let item = menu.addItem(withTitle: title, action: action, keyEquivalent: keyEquivalent)
|
||||
@@ -42,8 +41,6 @@ class Menubar {
|
||||
|
||||
// NSMenuItem.isHidden isn't reliable with custom views. We add/remove to hide/show these items
|
||||
static func togglePermissionCallout(_ show: Bool) {
|
||||
guard isShowingPermissionCallout != show else { return }
|
||||
isShowingPermissionCallout = show
|
||||
permissionCalloutMenuItems?.enumerated().forEach { offset, element in
|
||||
if show && !menu.items.contains(element) {
|
||||
menu.insertItem(element, at: offset)
|
||||
|
||||
@@ -24,7 +24,6 @@ class LightImageView: NSView {
|
||||
"position": NSNull(),
|
||||
"contentsScale": NSNull()
|
||||
]
|
||||
layerContentsRedrawPolicy = .never
|
||||
}
|
||||
|
||||
func updateWithResizedCopy(_ caLayerContents: CALayerContents, _ size: NSSize) {
|
||||
@@ -41,9 +40,7 @@ class LightImageView: NSView {
|
||||
default: break
|
||||
}
|
||||
layer!.contentsScale = scaleFactor
|
||||
if frame.size != size {
|
||||
frame.size = size
|
||||
}
|
||||
frame.size = size
|
||||
}
|
||||
|
||||
func releaseImage() {
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct VertexOut {
|
||||
float4 position [[position]];
|
||||
float2 uv;
|
||||
};
|
||||
|
||||
vertex VertexOut thumbnailVertex(
|
||||
uint vid [[vertex_id]],
|
||||
const device float4 *verts [[buffer(0)]]
|
||||
) {
|
||||
VertexOut out;
|
||||
float4 v = verts[vid];
|
||||
out.position = float4(v.xy, 0.0, 1.0);
|
||||
out.uv = v.zw;
|
||||
return out;
|
||||
}
|
||||
|
||||
fragment float4 thumbnailFragment(
|
||||
VertexOut in [[stage_in]],
|
||||
texture2d<float> yTex [[texture(0)]],
|
||||
texture2d<float> cbcrTex [[texture(1)]]
|
||||
) {
|
||||
constexpr sampler s(address::clamp_to_edge, filter::linear);
|
||||
|
||||
float y = yTex.sample(s, in.uv).r;
|
||||
float2 cbcr = cbcrTex.sample(s, in.uv).rg - float2(0.5, 0.5);
|
||||
|
||||
float r = y + 1.402 * cbcr.y;
|
||||
float g = y - 0.344136 * cbcr.x - 0.714136 * cbcr.y;
|
||||
float b = y + 1.772 * cbcr.x;
|
||||
|
||||
return float4(r, g, b, 1.0);
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
import AppKit
|
||||
import Metal
|
||||
import QuartzCore
|
||||
import ScreenCaptureKit
|
||||
|
||||
final class MetalThumbnailsView: NSView {
|
||||
override var wantsUpdateLayer: Bool { true }
|
||||
override func makeBackingLayer() -> CALayer { metalLayer }
|
||||
|
||||
private let device: MTLDevice
|
||||
private let commandQueue: MTLCommandQueue
|
||||
private let metalLayer: CAMetalLayer
|
||||
private let pipelineState: MTLRenderPipelineState
|
||||
private var thumbnails = ConcurrentMap<CGWindowID, ThumbnailFrame>()
|
||||
private var thumbnailsToPaint = ConcurrentMap<CGWindowID, Bool>()
|
||||
var thumbnailRects = [CGWindowID: NSRect]()
|
||||
|
||||
// we use AnyObject to avoid compiler warning about CADisplayLink being only for macOS >= 14.0
|
||||
private var displayLink: AnyObject?
|
||||
|
||||
private var isDrawing = false
|
||||
|
||||
init() {
|
||||
device = MTLCreateSystemDefaultDevice()!
|
||||
commandQueue = device.makeCommandQueue()!
|
||||
metalLayer = CAMetalLayer()
|
||||
pipelineState = MetalThumbnailsView.makePipelineState(device, metalLayer)
|
||||
super.init(frame: .zero)
|
||||
metalLayer.device = device
|
||||
metalLayer.pixelFormat = .bgra8Unorm
|
||||
metalLayer.isOpaque = false
|
||||
metalLayer.framebufferOnly = true
|
||||
metalLayer.contentsScale = NSScreen.main?.backingScaleFactor ?? 2.0
|
||||
metalLayer.actions = [
|
||||
"contents": NSNull(),
|
||||
"bounds": NSNull(),
|
||||
"position": NSNull()
|
||||
]
|
||||
if #available(macOS 14.0, *) {
|
||||
setupDisplayLink()
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 14.0, *)
|
||||
func setupDisplayLink() {
|
||||
displayLink = displayLink(target: self, selector: #selector(frameTick))
|
||||
displayLink!.add(to: .main, forMode: .common)
|
||||
}
|
||||
|
||||
@objc private func frameTick() {
|
||||
guard !isDrawing else { return }
|
||||
isDrawing = true
|
||||
let start = nowNs()
|
||||
drawFrame()
|
||||
let end = nowNs()
|
||||
let durationNs = toNs(end - start)
|
||||
Logger.error { Double(durationNs) / 1_000_000.0 }
|
||||
isDrawing = false
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("Class only supports programmatic initialization")
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
metalLayer.frame = bounds
|
||||
metalLayer.drawableSize = CGSize(
|
||||
width: bounds.width * metalLayer.contentsScale,
|
||||
height: bounds.height * metalLayer.contentsScale
|
||||
)
|
||||
}
|
||||
|
||||
private static func makePipelineState(_ device: MTLDevice, _ metalLayer: CAMetalLayer) -> MTLRenderPipelineState {
|
||||
let library = device.makeDefaultLibrary()!
|
||||
let descriptor = MTLRenderPipelineDescriptor()
|
||||
descriptor.vertexFunction = library.makeFunction(name: "thumbnailVertex")
|
||||
descriptor.fragmentFunction = library.makeFunction(name: "thumbnailFragment")
|
||||
descriptor.colorAttachments[0].pixelFormat = metalLayer.pixelFormat
|
||||
let attachment = descriptor.colorAttachments[0]!
|
||||
attachment.isBlendingEnabled = true
|
||||
attachment.rgbBlendOperation = .add
|
||||
attachment.alphaBlendOperation = .add
|
||||
attachment.sourceRGBBlendFactor = .sourceAlpha
|
||||
attachment.destinationRGBBlendFactor = .oneMinusSourceAlpha
|
||||
attachment.sourceAlphaBlendFactor = .one
|
||||
attachment.destinationAlphaBlendFactor = .oneMinusSourceAlpha
|
||||
do {
|
||||
return try device.makeRenderPipelineState(descriptor: descriptor)
|
||||
} catch {
|
||||
Logger.error { error }
|
||||
fatalError("\(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func makePlaneTexture(_ pixelBuffer: CVPixelBuffer, plane: Int, pixelFormat: MTLPixelFormat) -> MTLTexture? {
|
||||
guard let surface = CVPixelBufferGetIOSurface(pixelBuffer)?.takeUnretainedValue() else { return nil }
|
||||
let width = CVPixelBufferGetWidthOfPlane(pixelBuffer, plane)
|
||||
let height = CVPixelBufferGetHeightOfPlane(pixelBuffer, plane)
|
||||
let desc = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: pixelFormat, width: width, height: height, mipmapped: false)
|
||||
desc.storageMode = .shared
|
||||
desc.usage = [.shaderRead]
|
||||
return device.makeTexture(descriptor: desc, iosurface: surface, plane: plane)
|
||||
}
|
||||
|
||||
func enqueue(_ pixelBuffer: CVPixelBuffer, _ pts: CMTime, for id: CGWindowID) {
|
||||
guard let yTex = makePlaneTexture(pixelBuffer, plane: 0, pixelFormat: .r8Unorm),
|
||||
let cbcrTex = makePlaneTexture(pixelBuffer, plane: 1, pixelFormat: .rg8Unorm)
|
||||
else { return }
|
||||
let frame = ThumbnailFrame(yTex: yTex, cbcrTex: cbcrTex, pts: pts)
|
||||
thumbnails.withLock { t in
|
||||
t[id] = frame
|
||||
}
|
||||
thumbnailsToPaint.withLock { t in
|
||||
t[id] = true
|
||||
let x = t.filter { $0.value }.count
|
||||
Logger.error { x }
|
||||
}
|
||||
Logger.error { (Windows.list.first {$0.cgWindowId == id }?.title) }
|
||||
}
|
||||
|
||||
func drawFrame() {
|
||||
let t0 = nowNs()
|
||||
let drawable = metalLayer.nextDrawable()
|
||||
let t1 = nowNs()
|
||||
Logger.error { "nextDrawable() took \((t1 - t0)/1_000_000) ms" }
|
||||
guard
|
||||
let drawable,
|
||||
let commandBuffer = commandQueue.makeCommandBuffer()
|
||||
else { return }
|
||||
let renderPass = MTLRenderPassDescriptor()
|
||||
renderPass.colorAttachments[0].texture = drawable.texture
|
||||
renderPass.colorAttachments[0].loadAction = .clear
|
||||
renderPass.colorAttachments[0].storeAction = .store
|
||||
renderPass.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
|
||||
guard let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPass) else { return }
|
||||
encoder.setRenderPipelineState(pipelineState)
|
||||
let thumbnailsToPaintIds = thumbnailsToPaint.withLock { ($0.filter { t in t.value }).keys }
|
||||
Logger.error { thumbnailsToPaintIds.count }
|
||||
for id in thumbnailsToPaintIds {
|
||||
guard let rect = thumbnailRects[id] else { continue }
|
||||
guard let frame = (thumbnails.withLock { $0[id] }) else { continue }
|
||||
drawThumbnail(frame: frame, in: rect, encoder: encoder)
|
||||
thumbnailsToPaint.withLock { $0[id] = false }
|
||||
}
|
||||
encoder.endEncoding()
|
||||
commandBuffer.present(drawable)
|
||||
commandBuffer.commit()
|
||||
// BackgroundWork.screenshotsQueue.strongUnderlyingQueue.async(flags: .barrier) {
|
||||
// self?.thumbnails = nil
|
||||
// self?.thumbnailRects = nil
|
||||
// }
|
||||
}
|
||||
|
||||
private func drawThumbnail(frame: ThumbnailFrame, in rect: CGRect, encoder: MTLRenderCommandEncoder) {
|
||||
let viewSize = bounds.size
|
||||
// Convert AppKit rect → NDC
|
||||
let x0 = Float((rect.minX / viewSize.width) * 2 - 1)
|
||||
let x1 = Float((rect.maxX / viewSize.width) * 2 - 1)
|
||||
let y0 = Float((rect.minY / viewSize.height) * 2 - 1)
|
||||
let y1 = Float((rect.maxY / viewSize.height) * 2 - 1)
|
||||
let vertices: [Float] = [x0, y0, 0, 1, x1, y0, 1, 1, x0, y1, 0, 0, x1, y1, 1, 0]
|
||||
encoder.setVertexBytes(vertices, length: vertices.count * MemoryLayout<Float>.size, index: 0)
|
||||
encoder.setFragmentTexture(frame.yTex, index: 0)
|
||||
encoder.setFragmentTexture(frame.cbcrTex, index: 1)
|
||||
encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
|
||||
}
|
||||
}
|
||||
|
||||
struct ThumbnailFrame {
|
||||
let yTex: MTLTexture
|
||||
let cbcrTex: MTLTexture
|
||||
let pts: CMTime
|
||||
}
|
||||
|
||||
import MachO
|
||||
|
||||
@inline(__always)
|
||||
func nowNs() -> UInt64 {
|
||||
mach_continuous_time()
|
||||
}
|
||||
|
||||
let timebase: mach_timebase_info = {
|
||||
var info = mach_timebase_info()
|
||||
mach_timebase_info(&info)
|
||||
return info
|
||||
}()
|
||||
|
||||
@inline(__always)
|
||||
func toNs(_ t: UInt64) -> UInt64 {
|
||||
t * UInt64(timebase.numer) / UInt64(timebase.denom)
|
||||
}
|
||||
@@ -143,27 +143,21 @@ class ThumbnailView: FlippedView {
|
||||
}
|
||||
|
||||
func showOrHideWindowControls(_ shouldShowWindowControls: Bool) {
|
||||
guard Preferences.appearanceStyle == .thumbnails else { return }
|
||||
let shouldShow = shouldShowWindowControls && !Preferences.hideColoredCircles && !Appearance.hideThumbnails
|
||||
guard isShowingWindowControls != shouldShow else { return }
|
||||
isShowingWindowControls = shouldShow
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
for icon in windowControlIcons {
|
||||
let shouldHide = !shouldShow
|
||||
|| (icon.type == .quit && !(window_?.application.canBeQuit() ?? true))
|
||||
|| (icon.type == .close && !(window_?.canBeClosed() ?? true))
|
||||
|| ((icon.type == .miniaturize || icon.type == .fullscreen) && !(window_?.canBeMinDeminOrFullscreened() ?? true))
|
||||
|
||||
if icon.isHidden != shouldHide {
|
||||
icon.isHidden = shouldHide
|
||||
icon.needsDisplay = true
|
||||
if Preferences.appearanceStyle == .thumbnails {
|
||||
isShowingWindowControls = shouldShow
|
||||
for icon in windowControlIcons {
|
||||
icon.isHidden = !shouldShow
|
||||
|| (icon.type == .quit && !(window_?.application.canBeQuit() ?? true))
|
||||
|| (icon.type == .close && !(window_?.canBeClosed() ?? true))
|
||||
|| ((icon.type == .miniaturize || icon.type == .fullscreen) && !(window_?.canBeMinDeminOrFullscreened() ?? true))
|
||||
// Force the icons to redraw; otherwise after clicking the fullscreen button,
|
||||
// it will still appear to be in fullscreen mode.
|
||||
icon.display()
|
||||
}
|
||||
}
|
||||
CATransaction.commit()
|
||||
}
|
||||
|
||||
|
||||
func updateDockLabelIcon(_ dockLabel: String?) {
|
||||
assignIfDifferent(&dockLabelIcon.isHidden, dockLabel == nil || Preferences.hideAppBadges || Appearance.iconSize == 0)
|
||||
if !dockLabelIcon.isHidden, let dockLabel {
|
||||
@@ -191,7 +185,6 @@ class ThumbnailView: FlippedView {
|
||||
let shadow = ThumbnailView.makeShadow(Appearance.imagesShadowColor)
|
||||
thumbnailContainer.wantsLayer = true
|
||||
thumbnailContainer.layer!.masksToBounds = false // let thumbnail shadows show
|
||||
thumbnail.layer!.opacity = 0
|
||||
thumbnail.shadow = shadow
|
||||
windowlessIcon.shadow = shadow
|
||||
appIcon.shadow = shadow
|
||||
|
||||
@@ -99,15 +99,6 @@ extension ThumbnailsPanel: NSWindowDelegate {
|
||||
if App.app.appIsBeingUsed {
|
||||
App.app.thumbnailsPanel.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
MainMenu.toggle(enabled: true)
|
||||
}
|
||||
}
|
||||
|
||||
func windowDidBecomeKey(_ notification: Notification) {
|
||||
// we toggle the mainMenu off when showing the main window
|
||||
// this avoids command+q from quitting AltTab itself, or command+p from printing
|
||||
DispatchQueue.main.async {
|
||||
MainMenu.toggle(enabled: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,18 +77,16 @@ func makeAppropriateEffectView() -> EffectView {
|
||||
if #available(macOS 26.0, *) {
|
||||
if Preferences.appearanceStyle == .appIcons {
|
||||
if LiquidGlassEffectView.canUsePrivateLiquidGlassLook() {
|
||||
Logger.debug { "Using LiquidGlassEffectView(true)" }
|
||||
Logger.debug("Using LiquidGlassEffectView(true)")
|
||||
return LiquidGlassEffectView(true)
|
||||
}
|
||||
Logger.error {
|
||||
let os = ProcessInfo.processInfo.operatingSystemVersion
|
||||
let version = "\(os.majorVersion).\(os.minorVersion).\(os.patchVersion)"
|
||||
return "Private API set_variant is no longer available. macOS version: \(version))"
|
||||
}
|
||||
let os = ProcessInfo.processInfo.operatingSystemVersion
|
||||
let version = "\(os.majorVersion).\(os.minorVersion).\(os.patchVersion)"
|
||||
Logger.error("Private API set_variant is no longer available. macOS version: \(version))")
|
||||
}
|
||||
Logger.debug { "Using LiquidGlassEffectView(false)" }
|
||||
Logger.debug("Using LiquidGlassEffectView(false)")
|
||||
return LiquidGlassEffectView(false)
|
||||
}
|
||||
Logger.debug { "Using FrostedGlassEffectView(nil)" }
|
||||
Logger.debug("Using FrostedGlassEffectView(nil)")
|
||||
return FrostedGlassEffectView(nil)
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import Cocoa
|
||||
class ThumbnailsView {
|
||||
var scrollView: ScrollView!
|
||||
var contentView: EffectView!
|
||||
var metalView: MetalThumbnailsView!
|
||||
var rows = [[ThumbnailView]]()
|
||||
static var recycledViews = [ThumbnailView]()
|
||||
static var thumbnailsWidth = CGFloat(0.0)
|
||||
@@ -11,7 +10,6 @@ class ThumbnailsView {
|
||||
|
||||
init() {
|
||||
updateBackgroundView()
|
||||
metalView = MetalThumbnailsView()
|
||||
// TODO: think about this optimization more
|
||||
(1...20).forEach { _ in ThumbnailsView.recycledViews.append(ThumbnailView()) }
|
||||
}
|
||||
@@ -60,7 +58,7 @@ class ThumbnailsView {
|
||||
}
|
||||
}
|
||||
if ((step > 0 && nextRow < currentRow) || (step < 0 && nextRow > currentRow)) &&
|
||||
(ATShortcut.lastEventIsARepeat || !KeyRepeatTimer.timerIsSuspended) {
|
||||
(ATShortcut.lastEventIsARepeat || KeyRepeatTimer.timer?.isValid ?? false) {
|
||||
return nil
|
||||
}
|
||||
return rows[nextRow]
|
||||
@@ -143,11 +141,6 @@ class ThumbnailsView {
|
||||
rows[rows.count - 1].append(view)
|
||||
newViews.append(view)
|
||||
window.rowIndex = rows.count - 1
|
||||
if let wid = window.cgWindowId {
|
||||
App.app.thumbnailsPanel.thumbnailsView.metalView.thumbnailRects[wid] = NSRect.init(
|
||||
origin: view.frame.origin,
|
||||
size: view.thumbnail.frame.size)
|
||||
}
|
||||
} else {
|
||||
// release images from unused recycledViews; they take lots of RAM
|
||||
view.thumbnail.releaseImage()
|
||||
@@ -204,8 +197,6 @@ class ThumbnailsView {
|
||||
}
|
||||
scrollView.addTrackingArea(NSTrackingArea(rect: scrollView.bounds,
|
||||
options: [.mouseMoved, .mouseEnteredAndExited, .activeAlways], owner: scrollView, userInfo: nil))
|
||||
scrollView.documentView!.addSubview(metalView, positioned: .below, relativeTo: nil)
|
||||
metalView.frame.size = scrollView.frame.size
|
||||
}
|
||||
|
||||
func centerRows(_ maxX: CGFloat) {
|
||||
@@ -286,38 +277,33 @@ class ScrollView: NSScrollView {
|
||||
}
|
||||
}
|
||||
|
||||
override func mouseExited(with event: NSEvent) {
|
||||
previousTarget = nil
|
||||
resetHoveredWindow()
|
||||
}
|
||||
|
||||
override func mouseMoved(with event: NSEvent) {
|
||||
guard let documentView, !isCurrentlyScrolling && !ScrollwheelEvents.shouldBeEnabled else { return }
|
||||
let location = documentView.convert(App.app.thumbnailsPanel.mouseLocationOutsideOfEventStream, from: nil)
|
||||
let newTarget = findTarget(location)
|
||||
guard newTarget !== previousTarget else { return }
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
if let newTarget {
|
||||
previousTarget?.showOrHideWindowControls(false)
|
||||
newTarget.mouseMoved()
|
||||
// disable mouse hover during scrolling as it creates jank during elastic bounces at the start/end of the scrollview
|
||||
// also disable it when ScrollwheelEvents is enabled, as it indicates that a multi-finder gesture is in progress
|
||||
if isCurrentlyScrolling || ScrollwheelEvents.shouldBeEnabled { return }
|
||||
if let hit = hitTest(App.app.thumbnailsPanel.mouseLocationOutsideOfEventStream) {
|
||||
var target: NSView? = hit
|
||||
while !(target is ThumbnailView) && target != nil {
|
||||
target = target!.superview
|
||||
}
|
||||
if let target = target as? ThumbnailView {
|
||||
if previousTarget != target {
|
||||
previousTarget?.showOrHideWindowControls(false)
|
||||
previousTarget = target
|
||||
}
|
||||
target.mouseMoved()
|
||||
} else {
|
||||
if !checkIfWithinInterPadding() {
|
||||
resetHoveredWindow()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resetHoveredWindow()
|
||||
}
|
||||
previousTarget = newTarget
|
||||
CATransaction.commit()
|
||||
}
|
||||
|
||||
private func findTarget(_ location: NSPoint) -> ThumbnailView? {
|
||||
for i in 0..<Windows.list.count {
|
||||
let view = ThumbnailsView.recycledViews[i]
|
||||
let frame = view.frame
|
||||
let expandedFrame = CGRect(x: frame.minX - (App.shared.userInterfaceLayoutDirection == .leftToRight ? 0 : 1), y: frame.minY, width: frame.width + 1, height: frame.height + 1)
|
||||
if expandedFrame.contains(location) {
|
||||
return view
|
||||
}
|
||||
}
|
||||
return nil
|
||||
override func mouseExited(with event: NSEvent) {
|
||||
resetHoveredWindow()
|
||||
}
|
||||
|
||||
/// Checks whether the mouse pointer is within the padding area around a thumbnail.
|
||||
|
||||
@@ -38,12 +38,9 @@ class PermissionView: StackView {
|
||||
self.status = status
|
||||
wantsLayer = true
|
||||
layer!.cornerRadius = GridView.interPadding / 2
|
||||
setContentHuggingPriority(.required, for: .horizontal)
|
||||
setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
}
|
||||
|
||||
func updatePermissionStatus(_ permissionStatus: PermissionStatus) {
|
||||
guard status.stringValue.isEmpty || permissionStatus != self.permissionStatus else { return }
|
||||
self.permissionStatus = permissionStatus
|
||||
var color: NSColor
|
||||
var label: String
|
||||
|
||||
@@ -13,22 +13,19 @@ class PermissionsWindow: NSWindow {
|
||||
setupView()
|
||||
}
|
||||
|
||||
func show() {
|
||||
guard !isVisible else { return }
|
||||
Logger.debug { "" }
|
||||
SystemPermissions.setFrequentTimer()
|
||||
updatePermissionViews()
|
||||
func show(_ startupBlock: @escaping () -> Void) {
|
||||
accessibilityView.updatePermissionStatus(AccessibilityPermission.update())
|
||||
if #available(macOS 10.15, *) {
|
||||
screenRecordingView.updatePermissionStatus(ScreenRecordingPermission.update())
|
||||
}
|
||||
center()
|
||||
App.shared.activate(ignoringOtherApps: true)
|
||||
makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
func updatePermissionViews() {
|
||||
guard isVisible else { return }
|
||||
accessibilityView.updatePermissionStatus(AccessibilityPermission.status)
|
||||
if #available(macOS 10.15, *) {
|
||||
screenRecordingView.updatePermissionStatus(ScreenRecordingPermission.status)
|
||||
if #available(macOS 10.15, *), !SystemPermissions.preStartupPermissionsPassed {
|
||||
// this call triggers the permission prompt, however it's the only way to force the app to be listed with a checkbox
|
||||
SLSRequestScreenCaptureAccess()
|
||||
}
|
||||
SystemPermissions.pollPermissionsToUpdatePermissionsWindow(startupBlock)
|
||||
}
|
||||
|
||||
private func setupWindow() {
|
||||
@@ -72,37 +69,25 @@ class PermissionsWindow: NSWindow {
|
||||
)
|
||||
rows.append([screenRecordingView])
|
||||
}
|
||||
let widestRowWidth = rows.reduce(0) { max($0, $1[0]!.fittingSize.width) }
|
||||
rows.forEach { $0[0]!.fit(widestRowWidth, $0[0]!.fittingSize.height) }
|
||||
let view = GridView(rows as! [[NSView]])
|
||||
view.fit()
|
||||
setContentSize(view.fittingSize)
|
||||
contentView = view
|
||||
}
|
||||
|
||||
override func close() {
|
||||
hideAppIfLastWindowIsClosed()
|
||||
super.close()
|
||||
}
|
||||
}
|
||||
|
||||
extension PermissionsWindow: NSWindowDelegate {
|
||||
func windowShouldClose(_ sender: NSWindow) -> Bool {
|
||||
Logger.debug { "preStartupPermissionsPassed:\(SystemPermissions.preStartupPermissionsPassed), accessibility:\(AccessibilityPermission.status), screenRecording:\(ScreenRecordingPermission.status)" }
|
||||
func windowWillClose(_ notification: Notification) {
|
||||
Logger.debug(SystemPermissions.preStartupPermissionsPassed)
|
||||
if !SystemPermissions.preStartupPermissionsPassed {
|
||||
if AccessibilityPermission.status == .notGranted || ScreenRecordingPermission.status == .notGranted {
|
||||
Logger.error {
|
||||
"""
|
||||
Before using this app, you need to give permission in System Settings > Privacy & Security > Accessibility.
|
||||
Please authorize and re-launch.
|
||||
See https://help.rescuetime.com/article/59-how-do-i-enable-accessibility-permissions-on-mac-osx
|
||||
"""
|
||||
}
|
||||
if AccessibilityPermission.update() == .notGranted || ScreenRecordingPermission.update() == .notGranted {
|
||||
Logger.error("Before using this app, you need to give permission in System Settings > Privacy & Security > Accessibility.",
|
||||
"Please authorize and re-launch.",
|
||||
"See https://help.rescuetime.com/article/59-how-do-i-enable-accessibility-permissions-on-mac-osx")
|
||||
App.shared.terminate(self)
|
||||
return false // prevent the close; termination will close everything once
|
||||
}
|
||||
} else {
|
||||
SystemPermissions.timerPermissionsToUpdatePermissionsWindow?.invalidate()
|
||||
}
|
||||
SystemPermissions.setInfrequentTimer()
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ class GeneralTab {
|
||||
]
|
||||
static var menubarIconDropdown: NSPopUpButton?
|
||||
private static var menubarIsVisibleObserver: NSKeyValueObservation?
|
||||
private static var startAtLoginToggle: NSControl?
|
||||
|
||||
static func initTab() -> NSView {
|
||||
let startAtLogin = TableGroupView.Row(leftTitle: NSLocalizedString("Start at login", comment: ""),
|
||||
@@ -45,7 +44,7 @@ class GeneralTab {
|
||||
cell.bezelStyle = .regularSquare
|
||||
cell.arrowPosition = .arrowAtBottom
|
||||
cell.imagePosition = .imageOverlaps
|
||||
startAtLoginToggle = startAtLogin.rightViews[0] as? NSControl
|
||||
startAtLoginCallback(startAtLogin.rightViews[0] as! NSControl)
|
||||
Menubar.menubarIconCallback(nil)
|
||||
enableDraggingOffMenubarIcon(menuIconShownToggle)
|
||||
let table = TableGroupView(width: PreferencesWindow.width)
|
||||
@@ -84,8 +83,8 @@ class GeneralTab {
|
||||
}
|
||||
|
||||
/// add/remove plist file in ~/Library/LaunchAgents/ depending on the checkbox state
|
||||
static func startAtLoginCallback(_: NSControl? = nil) {
|
||||
let sender = startAtLoginToggle as! Switch
|
||||
static func startAtLoginCallback(_ sender: NSControl) {
|
||||
let sender = sender as! Switch
|
||||
// if the user has added AltTab manually as a LoginItem, we remove it, and add AltTab as a LaunchAgent
|
||||
// LaunchAgent are the recommended method for open-at-login in recent versions of macos
|
||||
if (GeneralTab.self as AvoidDeprecationWarnings.Type).removeLoginItemIfPresent() && sender.state == .off {
|
||||
@@ -95,7 +94,7 @@ class GeneralTab {
|
||||
do {
|
||||
try writePlistToDisk(sender)
|
||||
} catch let error {
|
||||
Logger.error { "Failed to write plist file to disk. error:\(error)" }
|
||||
Logger.error("Failed to write plist file to disk", error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,17 +103,17 @@ class GeneralTab {
|
||||
launchAgentsPath.appendPathComponent("LaunchAgents", isDirectory: true)
|
||||
if !FileManager.default.fileExists(atPath: launchAgentsPath.path) {
|
||||
try FileManager.default.createDirectory(at: launchAgentsPath, withIntermediateDirectories: false)
|
||||
Logger.debug { launchAgentsPath.absoluteString + " created" }
|
||||
Logger.debug(launchAgentsPath.absoluteString + " created")
|
||||
}
|
||||
launchAgentsPath.appendPathComponent("com.lwouis.alt-tab-macos.plist", isDirectory: false)
|
||||
if sender.state == .on {
|
||||
let data = try PropertyListSerialization.data(fromPropertyList: launchAgentPlist, format: .xml, options: 0)
|
||||
try data.write(to: launchAgentsPath, options: [.atomic])
|
||||
Logger.debug { launchAgentsPath.absoluteString + " written" }
|
||||
Logger.debug(launchAgentsPath.absoluteString + " written")
|
||||
} else {
|
||||
if FileManager.default.fileExists(atPath: launchAgentsPath.path) {
|
||||
try FileManager.default.removeItem(at: launchAgentsPath)
|
||||
Logger.debug { launchAgentsPath.absoluteString + " removed" }
|
||||
Logger.debug(launchAgentsPath.absoluteString + " removed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ class ControlsTab {
|
||||
}
|
||||
|
||||
class KeyRepeatTimer {
|
||||
static func stopTimerForRepeatingKey(_ shortcutName: String) {
|
||||
static func deactivateTimerForRepeatingKey(_ shortcutName: String) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user