From ecac7885ddcbb9302aa9fc51976ab23c6830885d Mon Sep 17 00:00:00 2001 From: TZGyn Date: Sun, 20 Sep 2026 14:46:25 +0800 Subject: [PATCH] Keep the captured app focused during selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The overlay used to activate Kapture, pushing the target app into background mode so its chrome (title bar, toolbar, tabs) was captured dimmed. Input no longer needs activation — the event tap handles it — so the overlay is a non-activating panel shown without taking focus, and the app keeps its active appearance in the capture. 💘 Generated with Crush Assisted-by: Crush:deepseek-v4-flash-0731 --- Sources/Kapture/CaptureController.swift | 19 ++++++++++++------- Sources/Kapture/OverlayView.swift | 5 ++++- build-app.sh | 4 ++-- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/Sources/Kapture/CaptureController.swift b/Sources/Kapture/CaptureController.swift index e2cae29..df653d2 100644 --- a/Sources/Kapture/CaptureController.swift +++ b/Sources/Kapture/CaptureController.swift @@ -84,21 +84,26 @@ final class CaptureController: NSObject { } isSelecting = true previousApp = NSWorkspace.shared.frontmostApplication - // Activation is async on macOS 26; wait a tick so the app is active - // before the overlays appear (otherwise clicks pass through). - NSApp.activate(ignoringOtherApps: true) - CaptureController.log("begin: activate issued, active=\(NSApp.isActive)") + // With the event tap driving input we must NOT activate: stealing + // focus turns the target app into background mode (dimmed chrome), + // and that is what would end up in the screenshot. Activate only on + // the fallback path, where panel events are the only input. + let usesTap = AXIsProcessTrusted() + if !usesTap { + NSApp.activate(ignoringOtherApps: true) + } + CaptureController.log("begin: usesTap=\(usesTap) active=\(NSApp.isActive)") DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in guard let self, self.isSelecting else { return } - CaptureController.log("begin: active=\(NSApp.isActive) — showing overlays") self.overlays = NSScreen.screens.map { screen in let panel = OverlayWindow(screen: screen, controller: self) (panel.contentView as? OverlayView)?.controller = self return panel } - self.overlays.forEach { $0.makeKeyAndOrderFront(nil) } + // orderFrontRegardless: show without taking key/focus. + self.overlays.forEach { $0.orderFrontRegardless() } self.overlayIDs = self.overlays.map { $0.windowNumber } - CaptureController.log("begin: \(self.overlays.count) overlays, ids=\(self.overlayIDs), active=\(NSApp.isActive)") + CaptureController.log("begin: \(self.overlays.count) overlays, ids=\(self.overlayIDs) — prior app stays focused") self.installEventTap() } } diff --git a/Sources/Kapture/OverlayView.swift b/Sources/Kapture/OverlayView.swift index 8a59387..1e9192c 100644 --- a/Sources/Kapture/OverlayView.swift +++ b/Sources/Kapture/OverlayView.swift @@ -6,8 +6,11 @@ final class OverlayWindow: NSPanel { init(screen: NSScreen, controller: CaptureController) { let content = OverlayView() super.init( + // Non-activating: the overlay must NOT steal focus, so the app + // being captured keeps its active appearance (undimmed chrome). + // Input comes from the CGEventTap, not panel events. contentRect: screen.frame, - styleMask: [.borderless], + styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: false ) diff --git a/build-app.sh b/build-app.sh index 9b3f421..a7510d9 100755 --- a/build-app.sh +++ b/build-app.sh @@ -108,9 +108,9 @@ cat > "$APP/Contents/Info.plist" <<'PLIST' CFBundleIconFile Icon CFBundleShortVersionString - 1.8.7 + 1.9.0 CFBundleVersion - 87 + 90 LSUIElement NSHighResolutionCapable