Add frozen-screen capture on Ctrl+Shift+Cmd+S
A second hotkey snapshots every display the moment it is pressed and
shows that still image as the overlay, so the pointer and hover states
(tooltips, open menus) are captured. Selection is marquee-only — no
window detection — and the result is cropped from the snapshot rather
than recaptured, keeping the frame exactly as it was.
💘 Generated with Crush
Assisted-by: Crush:deepseek-v4-flash-0731
This commit is contained in:
@@ -76,13 +76,19 @@ final class CaptureController: NSObject {
|
||||
|
||||
// MARK: - Session lifecycle
|
||||
|
||||
func begin() {
|
||||
/// Frozen mode: the screen is snapshotted at hotkey press, so the pointer
|
||||
/// and any hover state (tooltips, open menus) end up in the capture.
|
||||
private(set) var isFrozenMode = false
|
||||
private var frozenSnapshots: [ScreenCapture.DisplaySnapshot] = []
|
||||
|
||||
func begin(frozen: Bool = false) {
|
||||
guard !isSelecting else { return }
|
||||
guard PermissionManager.isGranted else {
|
||||
PermissionManager.request()
|
||||
return
|
||||
}
|
||||
isSelecting = true
|
||||
isFrozenMode = frozen
|
||||
previousApp = NSWorkspace.shared.frontmostApplication
|
||||
// With the event tap driving input we must NOT activate: stealing
|
||||
// focus turns the target app into background mode (dimmed chrome),
|
||||
@@ -92,12 +98,31 @@ final class CaptureController: NSObject {
|
||||
if !usesTap {
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
CaptureController.log("begin: usesTap=\(usesTap) active=\(NSApp.isActive)")
|
||||
CaptureController.log("begin: frozen=\(frozen) usesTap=\(usesTap) active=\(NSApp.isActive)")
|
||||
|
||||
if frozen {
|
||||
ScreenCapture.shared.snapshotAllDisplays(includeCursor: true) { [weak self] snapshots in
|
||||
DispatchQueue.main.async {
|
||||
guard let self, self.isSelecting else { return }
|
||||
self.frozenSnapshots = snapshots
|
||||
CaptureController.log("frozen: captured \(snapshots.count) displays")
|
||||
self.showOverlays()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showOverlays()
|
||||
}
|
||||
}
|
||||
|
||||
private func showOverlays() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
|
||||
guard let self, self.isSelecting else { return }
|
||||
self.overlays = NSScreen.screens.map { screen in
|
||||
let panel = OverlayWindow(screen: screen, controller: self)
|
||||
(panel.contentView as? OverlayView)?.controller = self
|
||||
if let view = panel.contentView as? OverlayView {
|
||||
view.controller = self
|
||||
view.frozenImage = self.frozenSnapshots.first(where: { $0.frame == screen.frame })?.image
|
||||
}
|
||||
return panel
|
||||
}
|
||||
// orderFrontRegardless: show without taking key/focus.
|
||||
@@ -126,11 +151,30 @@ final class CaptureController: NSObject {
|
||||
|
||||
private func finishSelection(_ rect: NSRect) {
|
||||
let ids = overlayIDs
|
||||
let frozen = frozenSnapshots
|
||||
let wasFrozen = isFrozenMode
|
||||
hideOverlays()
|
||||
guard rect.width >= 2, rect.height >= 2 else {
|
||||
cancelSelection()
|
||||
return
|
||||
}
|
||||
|
||||
if wasFrozen {
|
||||
let midpoint = NSPoint(x: rect.midX, y: rect.midY)
|
||||
let snapshot = frozen.first(where: { $0.frame.contains(midpoint) }) ?? frozen.first
|
||||
isSelecting = false
|
||||
restorePreviousApp()
|
||||
DispatchQueue.main.async { [self] in
|
||||
if let snapshot, let image = ScreenCapture.cropFrozen(snapshot, to: rect) {
|
||||
CaptureController.log("frozen: cropped \(Int(rect.width))x\(Int(rect.height)) -> \(image.width)x\(image.height)")
|
||||
ResultWindow.shared.show(image: image)
|
||||
} else {
|
||||
ResultWindow.shared.show(image: ResultWindow.failureImage("Could not crop the frozen screen."))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async { [self] in
|
||||
capture(rect, excludingWindowIDs: ids)
|
||||
}
|
||||
@@ -149,6 +193,8 @@ final class CaptureController: NSObject {
|
||||
marqueeRect = nil
|
||||
hoverWindow = nil
|
||||
marqueeStart = nil
|
||||
frozenSnapshots = []
|
||||
isFrozenMode = false
|
||||
}
|
||||
|
||||
private func restorePreviousApp() {
|
||||
@@ -306,6 +352,8 @@ final class CaptureController: NSObject {
|
||||
}
|
||||
let appKitPoint = cgPointToAppKit(NSPoint(x: event.location.x, y: event.location.y))
|
||||
if type == .mouseMoved {
|
||||
// Frozen mode is marquee-only: no window hover detection.
|
||||
if isFrozenMode { return event }
|
||||
let window = topmostWindowContaining(appKitPoint)
|
||||
if window?.frame != hoverFrame {
|
||||
hoverWindow = window
|
||||
@@ -345,6 +393,9 @@ final class CaptureController: NSObject {
|
||||
if let rect = marqueeRect, dragDistance >= 4, rect.width >= 2, rect.height >= 2 {
|
||||
CaptureController.log("tap: marquee complete \(rect)")
|
||||
finishSelection(rect)
|
||||
} else if isFrozenMode {
|
||||
CaptureController.log("tap: frozen click without drag — cancel")
|
||||
cancelSelection()
|
||||
} else if let hover = hoverWindow {
|
||||
// An overlapping window from the same app means this is an
|
||||
// owned popup — desktopIndependentWindow would composite the
|
||||
@@ -506,6 +557,74 @@ final class ScreenCapture {
|
||||
return result
|
||||
}
|
||||
|
||||
/// Screen frame (AppKit global points) + full-resolution image per display.
|
||||
struct DisplaySnapshot {
|
||||
let frame: NSRect
|
||||
let image: CGImage
|
||||
}
|
||||
|
||||
/// Snapshot every display right now. Used by frozen mode: the images are
|
||||
/// taken before the overlay appears, so the pointer and hover states
|
||||
/// (tooltips, menus) are baked in.
|
||||
func snapshotAllDisplays(includeCursor: Bool, completion: @escaping ([DisplaySnapshot]) -> Void) {
|
||||
if #available(macOS 14.0, *) {
|
||||
Task {
|
||||
guard let content = try? await SCShareableContent.current else {
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
var out: [DisplaySnapshot] = []
|
||||
for display in content.displays {
|
||||
guard let screen = NSScreen.screens.first(where: {
|
||||
(($0.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber)?.uint32Value ?? 0) == display.displayID
|
||||
}) else { continue }
|
||||
let config = SCStreamConfiguration()
|
||||
let scale = screen.backingScaleFactor
|
||||
config.width = Int(CGFloat(display.width) * scale)
|
||||
config.height = Int(CGFloat(display.height) * scale)
|
||||
config.showsCursor = includeCursor
|
||||
let filter = SCContentFilter(display: display, excludingWindows: [])
|
||||
if let image = try? await SCScreenshotManager.captureImage(contentFilter: filter, configuration: config) {
|
||||
out.append(DisplaySnapshot(frame: screen.frame, image: image))
|
||||
}
|
||||
}
|
||||
completion(out)
|
||||
}
|
||||
} else {
|
||||
var out: [DisplaySnapshot] = []
|
||||
for screen in NSScreen.screens {
|
||||
let cgRect = CGRect(
|
||||
x: screen.frame.minX,
|
||||
y: (NSScreen.screens.first(where: { $0.frame.origin == .zero })?.frame.height ?? screen.frame.height) - screen.frame.maxY,
|
||||
width: screen.frame.width,
|
||||
height: screen.frame.height
|
||||
)
|
||||
if let image = CGWindowListCreateImage(cgRect, .optionOnScreenOnly, kCGNullWindowID, [.bestResolution, .boundsIgnoreFraming]) {
|
||||
out.append(DisplaySnapshot(frame: screen.frame, image: image))
|
||||
}
|
||||
}
|
||||
completion(out)
|
||||
}
|
||||
}
|
||||
|
||||
/// Crop a selection out of a frozen snapshot (top-left pixel space).
|
||||
static func cropFrozen(_ snapshot: DisplaySnapshot, to rect: NSRect) -> CGImage? {
|
||||
let scaleX = CGFloat(snapshot.image.width) / snapshot.frame.width
|
||||
let scaleY = CGFloat(snapshot.image.height) / snapshot.frame.height
|
||||
guard scaleX > 0, scaleY > 0 else { return nil }
|
||||
let localX = rect.minX - snapshot.frame.minX
|
||||
let localTopY = snapshot.frame.maxY - rect.maxY
|
||||
let px = CGRect(
|
||||
x: localX * scaleX,
|
||||
y: localTopY * scaleY,
|
||||
width: rect.width * scaleX,
|
||||
height: rect.height * scaleY
|
||||
)
|
||||
let safe = px.intersection(CGRect(x: 0, y: 0, width: CGFloat(snapshot.image.width), height: CGFloat(snapshot.image.height))).integral
|
||||
guard safe.width >= 1, safe.height >= 1 else { return nil }
|
||||
return snapshot.image.cropping(to: safe)
|
||||
}
|
||||
|
||||
@available(macOS 14.0, *)
|
||||
private func displayContaining(_ rect: NSRect, in content: SCShareableContent) -> SCDisplay? {
|
||||
let mid = NSPoint(x: rect.midX, y: rect.midY)
|
||||
|
||||
Reference in New Issue
Block a user