Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 20 additions & 11 deletions native2/Sources/DesktopMac/Elements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -299,11 +299,14 @@ private struct Walk {
return (result, false)
}

mutating func process(_ element: AXUIElement, depth: Int) {
guard ContinuousClock.now < deadline else { tree.deadlineReached = true; return }
guard depth < budget.maxDepth else { tree.maxDepthReached = true; return }
guard tree.elements.count < budget.maxElements else { tree.maxElementCountReached = true; return }
guard !visited.contains(where: { CFEqual($0, element) }) else { return }
/// Reads one element and its subtree. Returns false when it lay off screen and was skipped, so it doesn't count
/// toward its parent's child cap.
@discardableResult
mutating func process(_ element: AXUIElement, depth: Int) -> Bool {
guard ContinuousClock.now < deadline else { tree.deadlineReached = true; return true }
guard depth < budget.maxDepth else { tree.maxDepthReached = true; return true }
guard tree.elements.count < budget.maxElements else { tree.maxElementCountReached = true; return true }
guard !visited.contains(where: { CFEqual($0, element) }) else { return true }
visited.append(element)
AXUIElementSetMessagingTimeout(element, timeout() ?? 0.01)

Expand All @@ -313,13 +316,14 @@ private struct Walk {
descriptor.isFocused != true
{
tree.offscreenSkipped += 1
return
return false
}
if let descriptor = read, !incomplete { emit(descriptor, element, depth: depth) }
let outer = clip
if let descriptor = read, !incomplete { clip = VisibleClip.narrowed(clip, role: descriptor.role, frame: descriptor.frame) }
processChildren(element, depth: depth)
clip = outer
return true
}

mutating func emit(_ descriptor: Descriptor, _ element: AXUIElement, depth: Int) {
Expand Down Expand Up @@ -385,14 +389,19 @@ private struct Walk {
mutating func processChildren(_ element: AXUIElement, depth: Int) {
let read = children(element)
if read.incomplete { tree.incompleteAccessibilityRead = true }
if read.elements.count > budget.maxChildren { tree.maxChildrenPerNodeReached = true }
let limited = read.elements.prefix(budget.maxChildren)
for (index, child) in limited.enumerated() {
process(child, depth: depth + 1)
guard index < limited.count - 1 else { break }
// A visible-only walk doesn't count children it skipped as off screen toward the cap, so a long list scrolled to
// its middle still lists the rows on screen (#249). It reads at most ten times the cap, since each skipped
// child still costs a read.
let scanned = read.elements.prefix(clip == nil ? budget.maxChildren : budget.maxChildren * 10)
var counted = 0
for (index, child) in scanned.enumerated() {
if counted == budget.maxChildren { tree.maxChildrenPerNodeReached = true; return }
if process(child, depth: depth + 1) { counted += 1 }
guard index < scanned.count - 1 else { break }
if tree.elements.count >= budget.maxElements { tree.maxElementCountReached = true; return }
if ContinuousClock.now >= deadline { tree.deadlineReached = true; return }
}
if read.elements.count > scanned.count { tree.maxChildrenPerNodeReached = true }
}
}

Expand Down
60 changes: 60 additions & 0 deletions native2/fixture/longlist.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import AppKit
// The long list fixture (#249): a scroll area whose content is a stack of 400 rows, every one of them an
// Accessibility child of the stack whether or not it is scrolled into view, scrolled at launch past its first 190
// rows (the stack's spacing puts about row 209 at the top). An inspection must list the rows on screen though
// hundreds of off-screen siblings come before them. Build it with `swiftc -O native2/fixture/longlist.swift -o native2/.build/longlist`; pass a row number to scroll elsewhere.

let app = NSApplication.shared
app.setActivationPolicy(.regular)
let menu = NSMenu()
let appItem = NSMenuItem()
appItem.submenu = NSMenu(title: "longlist")
appItem.submenu!.addItem(withTitle: "Quit longlist", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
menu.addItem(appItem)
app.mainMenu = menu

let window = NSWindow(contentRect: NSRect(x: 240, y: 240, width: 360, height: 420), styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: false)
window.title = "Long List"
let stack = NSStackView()
stack.orientation = .vertical
stack.alignment = .leading
stack.spacing = 0
stack.edgeInsets = NSEdgeInsets(top: 0, left: 12, bottom: 0, right: 12)
let rowHeight: CGFloat = 28
for index in 0..<400 {
let row = NSButton(title: "Row \(index)", target: nil, action: nil)
row.bezelStyle = .inline
row.translatesAutoresizingMaskIntoConstraints = false
row.heightAnchor.constraint(equalToConstant: rowHeight - 4).isActive = true
stack.addArrangedSubview(row)
stack.setCustomSpacing(4, after: row)
}
let flipped = FlippedView()
flipped.addSubview(stack)
stack.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: flipped.topAnchor), stack.leadingAnchor.constraint(equalTo: flipped.leadingAnchor),
stack.trailingAnchor.constraint(equalTo: flipped.trailingAnchor), stack.bottomAnchor.constraint(equalTo: flipped.bottomAnchor),
])
let scroll = NSScrollView(frame: window.contentView!.bounds)
scroll.autoresizingMask = [.width, .height]
scroll.hasVerticalScroller = true
scroll.documentView = flipped
flipped.translatesAutoresizingMaskIntoConstraints = false
flipped.widthAnchor.constraint(equalTo: scroll.contentView.widthAnchor).isActive = true
window.contentView!.addSubview(scroll)
window.makeKeyAndOrderFront(nil)
window.layoutIfNeeded()

let top = CommandLine.arguments.dropFirst().first.flatMap(Int.init) ?? 190
if let target = stack.arrangedSubviews.dropFirst(top).first {
scroll.contentView.scroll(to: NSPoint(x: 0, y: target.frame.minY))
scroll.reflectScrolledClipView(scroll.contentView)
}
print("ready, row \(top) at the top")
fflush(stdout)
app.run()

final class FlippedView: NSView {
override var isFlipped: Bool { true }
}
Loading