Skip to content

Commit

Permalink
2/2 Detect popups more aggressively
Browse files Browse the repository at this point in the history
nikitabobko#47 (comment)

Motivation: for some reasons, AXCursorActionsWindow isn't set in some
machines

Anyway new aggressive method should also detect context menus (right
mouse click) in IntelliJ, Telegram and who knows what else
  • Loading branch information
nikitabobko committed May 19, 2024
1 parent b45f0cf commit 28c1821
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 33 deletions.
14 changes: 13 additions & 1 deletion Sources/AppBundle/normalizeLayoutReason.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
func normalizeLayoutReason() {
func normalizeLayoutReason(startup: Bool) {
for workspace in Workspace.all {
let windows: [Window] = workspace.allLeafWindowsRecursive
_normalizeLayoutReason(workspace: workspace, windows: windows)
}
_normalizeLayoutReason(workspace: Workspace.focused, windows: macosInvisibleWindowsContainer.children.filterIsInstance(of: Window.self))
validateStillPopups(startup: startup)
}

private func validateStillPopups(startup: Bool) {
for node in macosPopupWindowsContainer.children {
let popup = (node as! MacWindow)
if isWindow(popup.axWindow, popup.macApp) {
popup.unbindFromParent()
popup.relayoutWindow(on: Workspace.focused)
tryOnWindowDetected(popup, startup: startup)
}
}
}

private func _normalizeLayoutReason(workspace: Workspace, windows: [Window]) {
Expand Down
2 changes: 1 addition & 1 deletion Sources/AppBundle/refresh.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func refreshSession<T>(startup: Bool = false, forceFocus: Bool = false, body: ()
}

updateTrayText()
normalizeLayoutReason()
normalizeLayoutReason(startup: startup)
layoutWorkspaces()
}
return result
Expand Down
6 changes: 5 additions & 1 deletion Sources/AppBundle/tree/MacApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ final class MacApp: AbstractApp {
}

override func getFocusedWindow(startup: Bool) -> Window? {
axApp.get(Ax.focusedWindowAttr)?.lets { MacWindow.get(app: self, axWindow: $0, startup: startup) }
getFocusedAxWindow()?.lets { MacWindow.get(app: self, axWindow: $0, startup: startup) }
}

func getFocusedAxWindow() -> AXUIElement? {
axApp.get(Ax.focusedWindowAttr)
}

override func detectNewWindowsAndGetAll(startup: Bool) -> [Window] {
Expand Down
48 changes: 38 additions & 10 deletions Sources/AppBundle/tree/MacWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Common

final class MacWindow: Window, CustomStringConvertible {
let axWindow: AXUIElement
private let macApp: MacApp
let macApp: MacApp
// todo take into account monitor proportions
private var prevUnhiddenEmulationPositionRelativeToWorkspaceAssignedRect: CGPoint?
fileprivate var previousSize: CGSize?
Expand All @@ -19,7 +19,6 @@ final class MacWindow: Window, CustomStringConvertible {
static var allWindows: [MacWindow] { Array(allWindowsMap.values) }

static func get(app: MacApp, axWindow: AXUIElement, startup: Bool) -> MacWindow? {
if !isWindow(axWindow, app) { return nil }
guard let id = axWindow.containingWindowId() else { return nil }
if let existing = allWindowsMap[id] {
return existing
Expand All @@ -38,7 +37,8 @@ final class MacWindow: Window, CustomStringConvertible {
window.observe(resizedObs, kAXResizedNotification) {
debug("New window detected: \(window)")
allWindowsMap[id] = window
onWindowDetected(window, startup: startup)
debugWindowsIfRecording(window)
tryOnWindowDetected(window, startup: startup)
return window
} else {
window.garbageCollect()
Expand Down Expand Up @@ -170,10 +170,26 @@ final class MacWindow: Window, CustomStringConvertible {
}
}

private func isWindow(_ axWindow: AXUIElement, _ app: MacApp) -> Bool {
/// Alternative name: !isPopup
func isWindow(_ axWindow: AXUIElement, _ app: MacApp) -> Bool {
let subrole = axWindow.get(Ax.subroleAttr)
// Sonoma (macOS 14) keyboard layout switch
if axWindow.get(Ax.identifierAttr) == "AXCursorActionsWindow" && subrole == kAXDialogSubrole {

// Try to filter out incredibly weird popup like AXWindows without any buttons.
// E.g.
// - Sonoma (macOS 14) keyboard layout switch
// - IntelliJ context menu (right mouse click)
// - Telegram context menu (right mouse click)
if axWindow.get(Ax.closeButtonAttr) == nil &&
axWindow.get(Ax.fullscreenButtonAttr) == nil &&
axWindow.get(Ax.zoomButtonAttr) == nil &&
axWindow.get(Ax.minimizeButtonAttr) == nil &&

axWindow.get(Ax.isFocused) == false && // Three different ways to detect if the window is not focused
axWindow.get(Ax.isMainAttr) == false &&
app.getFocusedAxWindow()?.containingWindowId() != axWindow.containingWindowId() &&

subrole != kAXStandardWindowSubrole &&
(axWindow.get(Ax.titleAttr) ?? "").isEmpty {
return false
}
return subrole == kAXStandardWindowSubrole ||
Expand Down Expand Up @@ -229,9 +245,13 @@ private func isFullscreenable(_ axWindow: AXUIElement) -> Bool {
}

func getBindingDataForNewWindow(_ axWindow: AXUIElement, _ workspace: Workspace, _ app: MacApp) -> BindingData {
shouldFloat(axWindow, app)
? BindingData(parent: workspace as NonLeafTreeNodeObject, adaptiveWeight: WEIGHT_AUTO, index: INDEX_BIND_LAST)
: getBindingDataForNewTilingWindow(workspace)
if !isWindow(axWindow, app) {
return BindingData(parent: macosPopupWindowsContainer, adaptiveWeight: WEIGHT_AUTO, index: INDEX_BIND_LAST)
}
if shouldFloat(axWindow, app) {
return BindingData(parent: workspace, adaptiveWeight: WEIGHT_AUTO, index: INDEX_BIND_LAST)
}
return getBindingDataForNewTilingWindow(workspace)
}

func getBindingDataForNewTilingWindow(_ workspace: Workspace) -> BindingData {
Expand Down Expand Up @@ -260,9 +280,17 @@ private func destroyedObs(_ obs: AXObserver, ax: AXUIElement, notif: CFString, d
refreshAndLayout()
}

func tryOnWindowDetected(_ window: Window, startup: Bool) {
switch window.parent.cases {
case .tilingContainer, .workspace, .macosInvisibleWindowsContainer, .macosFullscreenWindowsContainer:
onWindowDetected(window, startup: startup)
case .macosPopupWindowsContainer:
break
}
}

private func onWindowDetected(_ window: Window, startup: Bool) {
check(Thread.current.isMainThread)
debugWindowsIfRecording(window)
for callback in config.onWindowDetected where callback.matches(window, startup: startup) {
_ = callback.run.run(CommandMutableState(.window(window)))
if !callback.checkFurtherCallbacks {
Expand Down
8 changes: 0 additions & 8 deletions Sources/AppBundle/tree/Window.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,6 @@ enum LayoutReason: Equatable {
case standard
/// Reason for the cur temp layout is macOS native fullscreen, minimize, or hide
case macos(prevParentKind: NonLeafTreeNodeKind)

var isMacos: Bool {
if case .macos = self {
return true
} else {
return false
}
}
}

extension Window {
Expand Down
28 changes: 16 additions & 12 deletions Sources/AppBundle/util/accessibility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,14 @@ enum Ax {
getter: { $0 as? Bool },
setter: { $0 as CFTypeRef }
)
//static let axMainAttr = ReadableAttrImpl<Bool>(
// key: kAXMainAttribute,
// getter: { $0 as? Bool }
//)
static let isFocused = ReadableAttrImpl<Bool>(
key: kAXFocusedAttribute,
getter: { $0 as? Bool }
)
static let isMainAttr = ReadableAttrImpl<Bool>(
key: kAXMainAttribute,
getter: { $0 as? Bool }
)
static let sizeAttr = WritableAttrImpl<CGSize>(
key: kAXSizeAttribute,
getter: {
Expand Down Expand Up @@ -273,14 +277,14 @@ enum Ax {
key: kAXFullScreenButtonAttribute,
getter: { ($0 as! AXUIElement) }
)
//static let zoomButtonAttr = ReadableAttrImpl<AXUIElement>(
// key: kAXZoomButtonAttribute,
// getter: { ($0 as! AXUIElement) }
//)
//static let minimizeButtonAttr = ReadableAttrImpl<AXUIElement>(
// key: kAXMinimizeButtonAttribute,
// getter: { ($0 as! AXUIElement) }
//)
static let zoomButtonAttr = ReadableAttrImpl<AXUIElement>(
key: kAXZoomButtonAttribute,
getter: { ($0 as! AXUIElement) }
)
static let minimizeButtonAttr = ReadableAttrImpl<AXUIElement>(
key: kAXMinimizeButtonAttribute,
getter: { ($0 as! AXUIElement) }
)
//static let growAreaAttr = ReadableAttrImpl<AXUIElement>(
// key: kAXGrowAreaAttribute,
// getter: { ($0 as! AXUIElement) }
Expand Down

0 comments on commit 28c1821

Please sign in to comment.