Compare commits

..
3 Commits
Author SHA1 Message Date
TZGyn 265571d7e2 Reject window captures that composite extra windows
ScreenCaptureKit's desktopIndependentWindow filter can include owned
child windows (e.g. an app dialog plus its parent), producing a capture
much larger than the picked window. Detect the overflow by comparing the
returned image size against the window's frame and fall back to the
screen crop with rounded mask. Hover changes are logged with the picked
window id and tracked count for diagnosis.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-09-09 14:55:40 +08:00
TZGyn 7633e81c52 Capture picked windows natively with real rounded corners
Window picks now capture via ScreenCaptureKit's desktopIndependentWindow
filter, so the image carries the app's actual rounded-corner alpha
instead of a fixed 10pt mask that let the desktop show through on apps
with larger radii. Falls back to crop+mask when the window is not in the
shareable content.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-09-09 14:10:03 +08:00
TZGyn 83b127cf44 Detect maximized windows on secondary displays
A leftover heuristic skipped any window matching its screen's dimensions,
which made window detection silently fail on secondary monitors where the
working window is typically maximized.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-09-09 14:03:52 +08:00
2 changed files with 59 additions and 26 deletions
+57 -24
View File
@@ -14,7 +14,8 @@ final class CaptureController: NSObject {
// Selection state (driven by the event tap; AppKit global coordinates) // Selection state (driven by the event tap; AppKit global coordinates)
private var marqueeStart: NSPoint? private var marqueeStart: NSPoint?
private(set) var marqueeRect: NSRect? private(set) var marqueeRect: NSRect?
private(set) var hoverFrame: NSRect? private(set) var hoverWindow: (id: CGWindowID, frame: NSRect)?
var hoverFrame: NSRect? { hoverWindow?.frame }
private var trackedWindows: [(id: CGWindowID, frame: NSRect)] = [] private var trackedWindows: [(id: CGWindowID, frame: NSRect)] = []
private var lastWindowScan = Date.distantPast private var lastWindowScan = Date.distantPast
private var eventTap: CFMachPort? private var eventTap: CFMachPort?
@@ -102,14 +103,15 @@ final class CaptureController: NSObject {
} }
} }
/// A window was picked from hover — capture it with rounded corners. /// A window was picked from hover — capture it directly (real rounded
private func finishWindowPick(_ rect: NSRect) { /// corners come from the window's own alpha).
private func finishWindowPick(_ windowID: CGWindowID, _ rect: NSRect) {
let ids = overlayIDs let ids = overlayIDs
hideOverlays() hideOverlays()
isSelecting = false isSelecting = false
restorePreviousApp() restorePreviousApp()
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
capture(rect, excludingWindowIDs: ids, roundedCorners: true) capture(rect, excludingWindowIDs: ids, windowID: windowID)
} }
} }
@@ -136,7 +138,7 @@ final class CaptureController: NSObject {
overlays.forEach { $0.orderOut(nil) } overlays.forEach { $0.orderOut(nil) }
overlays = [] overlays = []
marqueeRect = nil marqueeRect = nil
hoverFrame = nil hoverWindow = nil
marqueeStart = nil marqueeStart = nil
} }
@@ -233,11 +235,6 @@ final class CaptureController: NSObject {
) )
guard cgRect.width >= 80, cgRect.height >= 60 else { continue } guard cgRect.width >= 80, cgRect.height >= 60 else { continue }
let rect = Self.cgToAppKit(rect: cgRect) let rect = Self.cgToAppKit(rect: cgRect)
// Skip windows filling an entire screen (fullscreen apps)
let coversWholeScreen = NSScreen.screens.contains { screen in
abs(screen.frame.width - rect.width) < 2 && abs(screen.frame.height - rect.height) < 2
}
guard !coversWholeScreen else { continue }
found.append((cid, rect)) found.append((cid, rect))
} }
trackedWindows = found trackedWindows = found
@@ -251,10 +248,10 @@ final class CaptureController: NSObject {
return NSRect(x: rect.origin.x, y: y, width: rect.width, height: rect.height) return NSRect(x: rect.origin.x, y: y, width: rect.width, height: rect.height)
} }
private func topmostWindowContaining(_ appKitPoint: NSPoint) -> NSRect? { private func topmostWindowContaining(_ appKitPoint: NSPoint) -> (id: CGWindowID, frame: NSRect)? {
for (_, frame) in trackedWindows { for entry in trackedWindows {
if frame.contains(appKitPoint) { if entry.frame.contains(appKitPoint) {
return frame return entry
} }
} }
return nil return nil
@@ -287,9 +284,14 @@ final class CaptureController: NSObject {
} }
let appKitPoint = cgPointToAppKit(NSPoint(x: event.location.x, y: event.location.y)) let appKitPoint = cgPointToAppKit(NSPoint(x: event.location.x, y: event.location.y))
if type == .mouseMoved { if type == .mouseMoved {
let frame = topmostWindowContaining(appKitPoint) let window = topmostWindowContaining(appKitPoint)
if frame != hoverFrame { if window?.frame != hoverFrame {
hoverFrame = frame hoverWindow = window
if let w = window {
CaptureController.log("hover: picked id=\(w.id) frame=\(w.frame) (tracked=\(trackedWindows.count))")
} else {
CaptureController.log("hover: none (tracked=\(trackedWindows.count))")
}
renderOverlays() renderOverlays()
} }
return event return event
@@ -303,7 +305,7 @@ final class CaptureController: NSObject {
height: abs(appKitPoint.y - start.y) height: abs(appKitPoint.y - start.y)
) )
marqueeRect = rect marqueeRect = rect
hoverFrame = nil hoverWindow = nil
renderOverlays() renderOverlays()
return nil return nil
case .leftMouseDown: case .leftMouseDown:
@@ -318,9 +320,9 @@ final class CaptureController: NSObject {
if let rect = marqueeRect, dragDistance >= 4, rect.width >= 2, rect.height >= 2 { if let rect = marqueeRect, dragDistance >= 4, rect.width >= 2, rect.height >= 2 {
CaptureController.log("tap: marquee complete \(rect)") CaptureController.log("tap: marquee complete \(rect)")
finishSelection(rect) finishSelection(rect)
} else if let hover = hoverFrame { } else if let hover = hoverWindow {
CaptureController.log("tap: window pick \(hover)") CaptureController.log("tap: window pick id=\(hover.id) \(hover.frame)")
finishWindowPick(hover) finishWindowPick(hover.id, hover.frame)
} else { } else {
CaptureController.log("tap: click with no hover/marquee — cancel") CaptureController.log("tap: click with no hover/marquee — cancel")
cancelSelection() cancelSelection()
@@ -333,8 +335,8 @@ final class CaptureController: NSObject {
// MARK: - Capture // MARK: - Capture
private func capture(_ rect: NSRect, excludingWindowIDs ids: [Int], roundedCorners: Bool = false) { private func capture(_ rect: NSRect, excludingWindowIDs ids: [Int], roundedCorners: Bool = false, windowID: CGWindowID? = nil) {
ScreenCapture.shared.capture(rect, excludingWindowIDs: ids, roundedCorners: roundedCorners) { [weak self] image in ScreenCapture.shared.capture(rect, excludingWindowIDs: ids, roundedCorners: roundedCorners, windowID: windowID) { [weak self] image in
DispatchQueue.main.async { DispatchQueue.main.async {
self?.isSelecting = false self?.isSelecting = false
self?.restorePreviousApp() self?.restorePreviousApp()
@@ -419,7 +421,7 @@ final class ScreenCapture {
return content.displays.first(where: { $0.displayID == cgID }) ?? content.displays.first return content.displays.first(where: { $0.displayID == cgID }) ?? content.displays.first
} }
func capture(_ rect: NSRect, excludingWindowIDs excludedIDs: [Int], roundedCorners: Bool = false, completion: @escaping (CGImage?) -> Void) { func capture(_ rect: NSRect, excludingWindowIDs excludedIDs: [Int], roundedCorners: Bool = false, windowID: CGWindowID? = nil, completion: @escaping (CGImage?) -> Void) {
if #available(macOS 14.0, *) { if #available(macOS 14.0, *) {
Task { Task {
guard let content = try? await SCShareableContent.current, guard let content = try? await SCShareableContent.current,
@@ -454,6 +456,37 @@ final class ScreenCapture {
height: rect.height height: rect.height
) )
// Window pick: capture the window DIRECTLY — the image comes
// with the app's real rounded-corner alpha (no mask guessing).
if let windowID,
let scWindow = content.windows.first(where: { $0.windowID == windowID }) {
var windowConfig = SCStreamConfiguration()
windowConfig.width = Int(scWindow.frame.width * backingScale)
windowConfig.height = Int(scWindow.frame.height * backingScale)
windowConfig.showsCursor = false
if let windowImage = try? await SCScreenshotManager.captureImage(
contentFilter: SCContentFilter(desktopIndependentWindow: scWindow),
configuration: windowConfig
) {
let expectedW = CGFloat(scWindow.frame.width) * backingScale
let expectedH = CGFloat(scWindow.frame.height) * backingScale
let overflow = max(
CGFloat(windowImage.width) / max(expectedW, 1),
CGFloat(windowImage.height) / max(expectedH, 1)
)
if overflow > 1.35 {
// SCKit composited extra windows (owned popups etc.)
// — reject and use the screen crop instead.
CaptureController.log("capture: WINDOW id=\(windowID) overflow=\(overflow) (\(windowImage.width)x\(windowImage.height) vs \(Int(expectedW))x\(Int(expectedH))) — rejecting, using crop+mask")
} else {
CaptureController.log("capture: WINDOW id=\(windowID) image=\(windowImage.width)x\(windowImage.height) (native alpha corners)")
completion(windowImage)
return
}
}
CaptureController.log("capture: window filter failed for id=\(windowID) — falling back to crop+mask")
}
guard let image = try? await SCScreenshotManager.captureImage(contentFilter: filter, configuration: config) else { guard let image = try? await SCScreenshotManager.captureImage(contentFilter: filter, configuration: config) else {
completion(nil) completion(nil)
return return
+2 -2
View File
@@ -108,9 +108,9 @@ cat > "$APP/Contents/Info.plist" <<'PLIST'
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>Icon</string> <string>Icon</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.7.3</string> <string>1.8.1</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>73</string> <string>81</string>
<key>LSUIElement</key> <key>LSUIElement</key>
<true/> <true/>
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>