Fix multi-display window detection and click-to-capture; separate Redact tool
Window frames are now converted to AppKit space once at detection time, so
hover highlighting and picking work on every display. A click is only
treated as a drag after 4pt of movement, so clicking a detected window
captures it instead of passing a stray marquee. Redact is now a standalone
tool (Select/Draw/Redact) with its own toolbar hints instead of a toggle
inside the draw bar.
💘 Generated with Crush
Assisted-by: Crush:deepseek-v4-flash-0731
This commit is contained in:
@@ -30,11 +30,11 @@ final class OverlayView: NSView {
|
||||
private var startPoint: NSPoint?
|
||||
private(set) var selection: NSRect?
|
||||
private var isDragging = false
|
||||
private var dragDistance: CGFloat = 0
|
||||
|
||||
// Window detection
|
||||
// Window detection (frames stored in AppKit global space)
|
||||
private var trackedWindows: [(id: CGWindowID, frame: NSRect)] = []
|
||||
private var hoveredWindowFrame: NSRect?
|
||||
private var excludedIDs: [Int] = []
|
||||
|
||||
init(onComplete: @escaping (NSRect) -> Void, onCancel: @escaping () -> Void, onWindowPick: @escaping (NSRect) -> Void) {
|
||||
self.onComplete = onComplete
|
||||
@@ -79,14 +79,17 @@ final class OverlayView: NSView {
|
||||
else { continue }
|
||||
// Skip our own overlays
|
||||
if Int(cid) == ownID { continue }
|
||||
let rect = NSRect(
|
||||
let cgRect = NSRect(
|
||||
x: bounds["X"] ?? 0,
|
||||
y: bounds["Y"] ?? 0,
|
||||
width: bounds["Width"] ?? 0,
|
||||
height: bounds["Height"] ?? 0
|
||||
)
|
||||
guard rect.width >= 80, rect.height >= 60 else { continue }
|
||||
// Skip tiny/utility windows
|
||||
guard cgRect.width >= 80, cgRect.height >= 60 else { continue }
|
||||
// Convert to AppKit space FIRST — CG and AppKit Y axes disagree on
|
||||
// multi-desktop layouts, so intersect tests must use one space.
|
||||
let rect = Self.cgToAppKit(rect: cgRect)
|
||||
// Skip windows filling the whole screen (fullscreen apps)
|
||||
guard rect.width < screenFrame.width || rect.height < screenFrame.height else { continue }
|
||||
// Only windows intersecting this screen
|
||||
guard rect.intersects(screenFrame) else { continue }
|
||||
@@ -97,11 +100,11 @@ final class OverlayView: NSView {
|
||||
|
||||
private func windowFrame(at point: NSPoint) -> NSRect? {
|
||||
guard let window = window else { return nil }
|
||||
// convertToScreen gives AppKit-global coordinates; convert tracked
|
||||
// CG frames to the same space and pick the frontmost containing it.
|
||||
// convertToScreen gives AppKit-global coordinates; tracked frames are
|
||||
// already in that space.
|
||||
let screenPoint = window.convertToScreen(NSRect(origin: point, size: .zero)).origin
|
||||
for (_, frame) in trackedWindows {
|
||||
if Self.cgToAppKit(rect: frame).contains(screenPoint) {
|
||||
if frame.contains(screenPoint) {
|
||||
return frame
|
||||
}
|
||||
}
|
||||
@@ -132,15 +135,20 @@ final class OverlayView: NSView {
|
||||
override func mouseDown(with event: NSEvent) {
|
||||
let p = convert(event.locationInWindow, from: nil)
|
||||
startPoint = p
|
||||
dragDistance = 0
|
||||
selection = NSRect(origin: p, size: .zero)
|
||||
needsDisplay = true
|
||||
}
|
||||
|
||||
override func mouseDragged(with event: NSEvent) {
|
||||
guard let start = startPoint else { return }
|
||||
let p = convert(event.locationInWindow, from: nil)
|
||||
dragDistance = hypot(p.x - start.x, p.y - start.y)
|
||||
// Only become a marquee after real movement; micro-jitter from a
|
||||
// click must not cancel window-pick mode.
|
||||
guard dragDistance >= 4 else { return }
|
||||
isDragging = true
|
||||
hoveredWindowFrame = nil
|
||||
let p = convert(event.locationInWindow, from: nil)
|
||||
selection = NSRect(
|
||||
x: min(start.x, p.x),
|
||||
y: min(start.y, p.y),
|
||||
@@ -151,6 +159,10 @@ final class OverlayView: NSView {
|
||||
}
|
||||
|
||||
override func mouseUp(with event: NSEvent) {
|
||||
defer {
|
||||
isDragging = false
|
||||
dragDistance = 0
|
||||
}
|
||||
if isDragging {
|
||||
guard let sel = selection else { return }
|
||||
if sel.width >= 2, sel.height >= 2 {
|
||||
@@ -161,10 +173,10 @@ final class OverlayView: NSView {
|
||||
return
|
||||
}
|
||||
|
||||
// No drag: it was a click. If a window is hovered, capture it with
|
||||
// rounded corners; otherwise treat as an empty marquee (cancel).
|
||||
// It was a click. If a window is hovered, capture it with rounded
|
||||
// corners; otherwise treat as an empty marquee (cancel).
|
||||
if let frame = hoveredWindowFrame {
|
||||
onWindowPick(Self.cgToAppKit(rect: frame))
|
||||
onWindowPick(frame)
|
||||
} else {
|
||||
onCancel()
|
||||
}
|
||||
@@ -184,13 +196,12 @@ final class OverlayView: NSView {
|
||||
|
||||
// Highlight the hovered window (rounded, like the real window corners)
|
||||
if let frame = hoveredWindowFrame {
|
||||
// Convert the CG frame to AppKit-global, then to this view's space.
|
||||
// Tracked frames are AppKit-global; view space = global - window origin.
|
||||
guard let window = window else { return }
|
||||
let screenFrame = window.convertToScreen(NSRect(origin: .zero, size: bounds.size))
|
||||
let appKitFrame = Self.cgToAppKit(rect: frame)
|
||||
let viewFrame = NSRect(
|
||||
x: appKitFrame.origin.x - screenFrame.origin.x,
|
||||
y: appKitFrame.origin.y - screenFrame.origin.y,
|
||||
x: frame.origin.x - screenFrame.origin.x,
|
||||
y: frame.origin.y - screenFrame.origin.y,
|
||||
width: frame.width,
|
||||
height: frame.height
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user