From d770daf03317ecc081fc3ff01e9826e111e29130 Mon Sep 17 00:00:00 2001 From: Sathya Date: Mon, 13 Jul 2026 10:10:51 +0530 Subject: [PATCH 1/5] Initial changes, to be tested --- README.md | 4 + Sources/ZoomItMacCore/App/AppController.swift | 4 + Sources/ZoomItMacCore/App/AppDelegate.swift | 2 + .../Capture/SnipController.swift | 91 +++++++++++++++++-- Sources/ZoomItMacCore/Core/AppCommand.swift | 1 + .../ZoomItMacCore/Core/ModeCoordinator.swift | 28 +++++- .../ZoomItMacCore/Hotkeys/HotkeyService.swift | 36 ++++++++ .../Overlay/OverlayWindowController.swift | 12 +++ .../Overlay/ZoomCanvasView.swift | 77 +++++++++------- .../Settings/SettingsStore.swift | 21 +++++ .../Settings/SettingsWindowController.swift | 49 +++++++++- 11 files changed, 284 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 5a50074..00a5223 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ The contributor build is named `ZoomIt (Dev).app` (bundle id `com.sysinternals.z | Snip region to clipboard | `Control+6` | | Snip region to file | `Control+Shift+6` | | OCR region to clipboard | `Control+Option+6` | +| Capture previous region to clipboard | `Control+9` | +| Capture previous region to file | `Control+Shift+9` | | Panorama to clipboard | `Control+8` | | Panorama to file | `Control+Shift+8` | @@ -75,6 +77,8 @@ Press the snip shortcut (`Control+6`) and drag a rectangle to copy that region o Press the OCR shortcut (`Control+Option+6`) and drag a rectangle to recognize the text inside it and copy that text to the clipboard. OCR uses Apple's on-device Vision text recognition, runs entirely on-device, and needs no extra permissions. Both shortcuts are configurable on the Snip tab in Settings. +ZoomItMac also remembers the last successful region snip. Use the menu command **Capture Previous Region** or press `Control+9` to repeat that capture without reopening the region selector. Hold `Shift` (`Control+Shift+9`) to save to a file instead of copying. + ## Recording Recording uses ScreenCaptureKit and AVAssetWriter. Static zoom and drawing overlays are captured even when ScreenCaptureKit omits ZoomIt's own windows; live zoom remains excluded from its own capture to avoid feedback. Webcam picture-in-picture stays fixed in the recorded viewport and is composited into overlay recordings instead of being zoomed into the source image. diff --git a/Sources/ZoomItMacCore/App/AppController.swift b/Sources/ZoomItMacCore/App/AppController.swift index 6387ead..1d94e32 100644 --- a/Sources/ZoomItMacCore/App/AppController.swift +++ b/Sources/ZoomItMacCore/App/AppController.swift @@ -48,6 +48,10 @@ final class AppController: NSObject { modeCoordinator.handle(.startPanorama(save: false)) } + @objc func capturePreviousRegion() { + modeCoordinator.handle(.snipPreviousRegion(save: false)) + } + @objc func toggleBreakTimer() { modeCoordinator.handle(.toggleBreakTimer) } diff --git a/Sources/ZoomItMacCore/App/AppDelegate.swift b/Sources/ZoomItMacCore/App/AppDelegate.swift index 0bcc24c..fae8adf 100644 --- a/Sources/ZoomItMacCore/App/AppDelegate.swift +++ b/Sources/ZoomItMacCore/App/AppDelegate.swift @@ -120,6 +120,8 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { menu.addItem(staticZoomItem) let liveZoomItem = NSMenuItem(title: "Live Zoom", action: #selector(AppController.activateLiveZoom), keyEquivalent: "") menu.addItem(liveZoomItem) + let previousRegionItem = NSMenuItem(title: "Capture Previous Region", action: #selector(AppController.capturePreviousRegion), keyEquivalent: "") + menu.addItem(previousRegionItem) let recordItem = NSMenuItem(title: "Record Screen", action: #selector(AppController.toggleRecording), keyEquivalent: "") menu.addItem(recordItem) let panoramaItem = NSMenuItem(title: "Panorama Capture", action: #selector(AppController.startPanorama), keyEquivalent: "") diff --git a/Sources/ZoomItMacCore/Capture/SnipController.swift b/Sources/ZoomItMacCore/Capture/SnipController.swift index 684ed5c..5d87014 100644 --- a/Sources/ZoomItMacCore/Capture/SnipController.swift +++ b/Sources/ZoomItMacCore/Capture/SnipController.swift @@ -184,6 +184,28 @@ enum SnipAction { /// overlay, and copies or saves the chosen region when the drag is released. @MainActor final class SnipController { + private struct PreviousSelection { + let displayID: CGDirectDisplayID + let displaySize: CGSize + let rect: CGRect + + func rectForCurrentDisplay(_ display: DisplayDescriptor) -> CGRect { + guard displaySize.width > 0, displaySize.height > 0 else { return rect } + if display.id == displayID { + return rect + } + + let widthScale = display.frame.width / displaySize.width + let heightScale = display.frame.height / displaySize.height + return CGRect( + x: rect.minX * widthScale, + y: rect.minY * heightScale, + width: rect.width * widthScale, + height: rect.height * heightScale + ) + } + } + private let captureService: ScreenCaptureService private let displayManager: DisplayManager private let permissionService: PermissionService @@ -194,6 +216,7 @@ final class SnipController { private var action: SnipAction = .copyImage private var onFinished: (() -> Void)? private var cursorLease: CrosshairCursorLease? + private var previousSelection: PreviousSelection? init( captureService: ScreenCaptureService, @@ -243,6 +266,40 @@ final class SnipController { } } + /// Repeats the last successful region snip with no selection UI. + /// Returns false if there is no previous region to reuse. + @discardableResult + func capturePrevious(action: SnipAction, onFinished: @escaping () -> Void) -> Bool { + guard let previousSelection else { + return false + } + + self.action = action + self.onFinished = onFinished + + guard ScreenRecordingPrompt.ensureGranted(permissionService) else { + finish() + return true + } + guard let display = displayManager.activeDisplay() else { + finish() + return true + } + + Task { @MainActor in + do { + let frame = try await captureService.captureDisplay(display) + let rect = previousSelection.rectForCurrentDisplay(frame.display) + _ = self.handleSelection(rect, in: frame) + self.finish() + } catch { + NSSound.beep() + self.finish() + } + } + return true + } + private func show(frame: CapturedFrame) { capturedFrame = frame @@ -285,19 +342,39 @@ final class SnipController { return } + _ = handleSelection(rect, in: frame) + finish() + } + + @discardableResult + private func handleSelection(_ rect: CGRect, in frame: CapturedFrame) -> Bool { + guard rect.width >= 3, rect.height >= 3 else { + return false + } + + let boundedRect = rect.intersection(CGRect(origin: .zero, size: frame.display.frame.size)) + guard boundedRect.width >= 3, boundedRect.height >= 3 else { + return false + } + let scale = frame.display.scaleFactor let pixelRect = CGRect( - x: rect.minX * scale, - y: rect.minY * scale, - width: rect.width * scale, - height: rect.height * scale + x: boundedRect.minX * scale, + y: boundedRect.minY * scale, + width: boundedRect.width * scale, + height: boundedRect.height * scale ).integral guard let cropped = frame.image.cropping(to: pixelRect) else { - finish() - return + return false } + previousSelection = PreviousSelection( + displayID: frame.display.id, + displaySize: frame.display.frame.size, + rect: boundedRect + ) + switch action { case .saveImage: ImageExporter.saveImage(cropped, settings: settingsStore.load()) @@ -306,7 +383,7 @@ final class SnipController { case .recognizeText: OcrService.recognizeAndCopy(cropped) } - finish() + return true } private func closeWindow() { diff --git a/Sources/ZoomItMacCore/Core/AppCommand.swift b/Sources/ZoomItMacCore/Core/AppCommand.swift index 57110ee..b997f69 100644 --- a/Sources/ZoomItMacCore/Core/AppCommand.swift +++ b/Sources/ZoomItMacCore/Core/AppCommand.swift @@ -15,6 +15,7 @@ enum AppCommand: Equatable { case undo case clear case snipRegion(save: Bool) + case snipPreviousRegion(save: Bool) case snipOcr case startPanorama(save: Bool) case toggleRecording(region: Bool) diff --git a/Sources/ZoomItMacCore/Core/ModeCoordinator.swift b/Sources/ZoomItMacCore/Core/ModeCoordinator.swift index 5f16673..4d81049 100644 --- a/Sources/ZoomItMacCore/Core/ModeCoordinator.swift +++ b/Sources/ZoomItMacCore/Core/ModeCoordinator.swift @@ -75,7 +75,7 @@ final class ModeCoordinator { // Ignore activation and snip hotkeys while a region selection is on // screen so a second trigger can't stack overlays. switch command { - case .activateStaticZoom, .activateLiveZoom, .activateDrawWithoutZoom, .snipRegion, .zoomIn, .zoomOutOrExit: + case .activateStaticZoom, .activateLiveZoom, .activateDrawWithoutZoom, .snipRegion, .snipPreviousRegion, .zoomIn, .zoomOutOrExit: return default: break @@ -115,6 +115,8 @@ final class ModeCoordinator { overlayController.requestRedraw() case .snipRegion(let save): startSnip(action: save ? .saveImage : .copyImage) + case .snipPreviousRegion(let save): + startSnip(action: save ? .saveImage : .copyImage, reusePreviousRegion: true) case .snipOcr: startSnip(action: .recognizeText) case .toggleRecording(let region): @@ -402,18 +404,40 @@ final class ModeCoordinator { /// Starts a region snip. From idle it captures the screen; while zoomed it /// selects within the current viewport so ZoomIt's own overlay is reused /// rather than captured. - private func startSnip(action: SnipAction) { + private func startSnip(action: SnipAction, reusePreviousRegion: Bool = false) { guard !isSnipping else { NSSound.beep() return } switch mode { case .idle: + if reusePreviousRegion { + isSnipping = true + let started = snipController.capturePrevious(action: action) { [weak self] in + self?.isSnipping = false + } + if !started { + isSnipping = false + NSSound.beep() + } + return + } isSnipping = true snipController.begin(action: action) { [weak self] in self?.isSnipping = false } case .staticZoom, .liveZoom, .drawOnly, .typing: + if reusePreviousRegion { + isSnipping = true + let started = overlayController.capturePreviousRegionSnip(action: action) { [weak self] in + self?.isSnipping = false + } + if !started { + isSnipping = false + NSSound.beep() + } + return + } isSnipping = true overlayController.beginRegionSnip(action: action) { [weak self] in self?.isSnipping = false diff --git a/Sources/ZoomItMacCore/Hotkeys/HotkeyService.swift b/Sources/ZoomItMacCore/Hotkeys/HotkeyService.swift index f78e4ff..293e4f0 100644 --- a/Sources/ZoomItMacCore/Hotkeys/HotkeyService.swift +++ b/Sources/ZoomItMacCore/Hotkeys/HotkeyService.swift @@ -11,6 +11,8 @@ final class HotkeyService { private var snipCopyHotKeyRef: EventHotKeyRef? private var snipSaveHotKeyRef: EventHotKeyRef? private var snipOcrHotKeyRef: EventHotKeyRef? + private var snipPreviousHotKeyRef: EventHotKeyRef? + private var snipPreviousSaveHotKeyRef: EventHotKeyRef? private var recordHotKeyRef: EventHotKeyRef? private var recordRegionHotKeyRef: EventHotKeyRef? private var demoTypeHotKeyRef: EventHotKeyRef? @@ -131,6 +133,8 @@ final class HotkeyService { case 13: command = .snipOcr case 14: command = .startDemoType case 15: command = .resetDemoType + case 16: command = .snipPreviousRegion(save: false) + case 17: command = .snipPreviousRegion(save: true) default: return noErr } @@ -220,6 +224,30 @@ final class HotkeyService { ) } + // Capture Previous Region: reuses the last successful region snip. + // The base shortcut copies; the same shortcut with Shift toggled saves. + if settings.snipPreviousHotKeyCode != 0 { + let snipPreviousModifiers = NSEvent.ModifierFlags(rawValue: settings.snipPreviousHotKeyModifiers) + RegisterEventHotKey( + UInt32(settings.snipPreviousHotKeyCode), + carbonModifiers(from: snipPreviousModifiers), + EventHotKeyID(signature: signature, id: 16), + target, + 0, + &snipPreviousHotKeyRef + ) + + let snipPreviousSaveModifiers = NSEvent.ModifierFlags(rawValue: settings.snipPreviousHotKeyModifiers ^ NSEvent.ModifierFlags.shift.rawValue) + RegisterEventHotKey( + UInt32(settings.snipPreviousHotKeyCode), + carbonModifiers(from: snipPreviousSaveModifiers), + EventHotKeyID(signature: signature, id: 17), + target, + 0, + &snipPreviousSaveHotKeyRef + ) + } + // Recording: the base shortcut records the whole screen; the same // shortcut with Shift toggled records a selected region. let recordModifiers = NSEvent.ModifierFlags(rawValue: settings.recordHotKeyModifiers) @@ -322,6 +350,14 @@ final class HotkeyService { UnregisterEventHotKey(snipOcrHotKeyRef) } snipOcrHotKeyRef = nil + if let snipPreviousHotKeyRef { + UnregisterEventHotKey(snipPreviousHotKeyRef) + } + snipPreviousHotKeyRef = nil + if let snipPreviousSaveHotKeyRef { + UnregisterEventHotKey(snipPreviousSaveHotKeyRef) + } + snipPreviousSaveHotKeyRef = nil if let recordHotKeyRef { UnregisterEventHotKey(recordHotKeyRef) } diff --git a/Sources/ZoomItMacCore/Overlay/OverlayWindowController.swift b/Sources/ZoomItMacCore/Overlay/OverlayWindowController.swift index 7900414..ecc21bd 100644 --- a/Sources/ZoomItMacCore/Overlay/OverlayWindowController.swift +++ b/Sources/ZoomItMacCore/Overlay/OverlayWindowController.swift @@ -136,6 +136,18 @@ final class OverlayWindowController { } canvasView.beginRegionSnip(action: action, onFinished: onFinished) } + + /// Captures using the last successful viewport-region snip, without showing + /// the drag selection UI. Returns false if no previous region exists. + func capturePreviousRegionSnip(action: SnipAction, onFinished: @escaping () -> Void) -> Bool { + guard let canvasView else { + onFinished() + return false + } + let captured = canvasView.capturePreviousRegionSnip(action: action) + onFinished() + return captured + } /// The overlay's window number, used to exclude it from live screen capture /// so the magnified overlay is never captured back into itself. var overlayWindowNumber: Int? { window?.windowNumber } diff --git a/Sources/ZoomItMacCore/Overlay/ZoomCanvasView.swift b/Sources/ZoomItMacCore/Overlay/ZoomCanvasView.swift index 2250349..727259e 100644 --- a/Sources/ZoomItMacCore/Overlay/ZoomCanvasView.swift +++ b/Sources/ZoomItMacCore/Overlay/ZoomCanvasView.swift @@ -29,6 +29,7 @@ final class ZoomCanvasView: NSView { private var regionRect: CGRect = .zero private var regionCursorLease: CrosshairCursorLease? private var onRegionSnipFinished: (() -> Void)? + private var previousRegionRect: CGRect? private var scrollZoomAccumulator: CGFloat = 0 private let smoothImage: Bool @@ -790,20 +791,17 @@ final class ZoomCanvasView: NSView { /// directly to the configured directory instead of showing a dialog. private func presentSavePanelOverOverlay(_ image: CGImage) { let settings = UserDefaultsSettingsStore().load() - if settings.copySnipToClipboardOnSave { - ImageExporter.copyToPasteboard(image) - } - if settings.saveSnipToDirectory { - ImageExporter.writeToDirectory(image, directoryPath: settings.snipSaveDirectory) - return - } let savedLevel = window?.level let wasCursorHidden = cursorHidden - window?.level = NSWindow.Level(rawValue: NSWindow.Level.normal.rawValue - 1) - if wasCursorHidden { showSystemCursor() } - ImageExporter.presentSavePanel(for: image) - if let savedLevel { window?.level = savedLevel } - if wasCursorHidden { hideSystemCursor() } + ImageExporter.saveImage(image, settings: settings) { [weak self] in + guard let self else { return } + self.window?.level = NSWindow.Level(rawValue: NSWindow.Level.normal.rawValue - 1) + if wasCursorHidden { self.showSystemCursor() } + } + if !settings.saveSnipToDirectory { + if let savedLevel { window?.level = savedLevel } + if wasCursorHidden { hideSystemCursor() } + } } /// Snapshots exactly what the overlay is displaying (magnified image plus @@ -849,6 +847,12 @@ final class ZoomCanvasView: NSView { needsDisplay = true } + /// Captures with the previously successful region snip, if any. + func capturePreviousRegionSnip(action: SnipAction) -> Bool { + guard let previousRegionRect else { return false } + return performRegionSnip(rect: previousRegionRect, action: action) + } + private func updateRegionRect(to point: CGPoint) { guard let anchor = regionAnchor else { return } regionRect = CGRect( @@ -867,28 +871,39 @@ final class ZoomCanvasView: NSView { regionAnchor = nil popRegionCursor() - if rect.width >= 3, rect.height >= 3, let full = captureViewportImage() { - let scale = window?.backingScaleFactor ?? capturedFrame.display.scaleFactor - let pixelRect = CGRect( - x: rect.minX * scale, - y: rect.minY * scale, - width: rect.width * scale, - height: rect.height * scale - ).integral - if let cropped = full.cropping(to: pixelRect) { - switch action { - case .saveImage: - presentSavePanelOverOverlay(cropped) - case .copyImage: - ImageExporter.copyToPasteboard(cropped) - case .recognizeText: - OcrService.recognizeAndCopy(cropped) - } - } - } + _ = performRegionSnip(rect: rect, action: action) endRegionSnip() } + private func performRegionSnip(rect: CGRect, action: SnipAction) -> Bool { + guard rect.width >= 3, rect.height >= 3, let full = captureViewportImage() else { + return false + } + + let scale = window?.backingScaleFactor ?? capturedFrame.display.scaleFactor + let pixelRect = CGRect( + x: rect.minX * scale, + y: rect.minY * scale, + width: rect.width * scale, + height: rect.height * scale + ).integral + + guard let cropped = full.cropping(to: pixelRect) else { + return false + } + + previousRegionRect = rect + switch action { + case .saveImage: + presentSavePanelOverOverlay(cropped) + case .copyImage: + ImageExporter.copyToPasteboard(cropped) + case .recognizeText: + OcrService.recognizeAndCopy(cropped) + } + return true + } + private func cancelRegionSnip() { isSelectingRegion = false regionRect = .zero diff --git a/Sources/ZoomItMacCore/Settings/SettingsStore.swift b/Sources/ZoomItMacCore/Settings/SettingsStore.swift index fd759bc..0bc44a3 100644 --- a/Sources/ZoomItMacCore/Settings/SettingsStore.swift +++ b/Sources/ZoomItMacCore/Settings/SettingsStore.swift @@ -36,6 +36,11 @@ struct AppSettings: Equatable { /// SnipOcrToggleKey behavior. var snipOcrHotKeyCode: Int var snipOcrHotKeyModifiers: UInt + /// Virtual key code and modifier-flag raw value for re-running the last + /// successful region snip without opening the selector UI. A key code of 0 + /// disables the hotkey. + var snipPreviousHotKeyCode: Int + var snipPreviousHotKeyModifiers: UInt var recordHotKeyCode: Int var recordHotKeyModifiers: UInt /// Virtual key code and modifier-flag raw value for DemoType. A key code of @@ -134,6 +139,10 @@ struct AppSettings: Equatable { // copies it to the clipboard, matching ZoomIt's Ctrl+Alt+6 default. snipOcrHotKeyCode: 22, snipOcrHotKeyModifiers: (1 << 18) | (1 << 19), + // Control+9 (kVK_ANSI_9 = 25) repeats the last region snip; + // Control+Shift+9 repeats it and saves to a file. + snipPreviousHotKeyCode: 25, + snipPreviousHotKeyModifiers: 1 << 18, // Control+5 (kVK_ANSI_5 = 23) records the screen; // Control+Shift+5 records a selected region. recordHotKeyCode: 23, @@ -204,6 +213,8 @@ final class UserDefaultsSettingsStore: SettingsStore { static let snipHotKeyModifiers = "snipHotKeyModifiers" static let snipOcrHotKeyCode = "snipOcrHotKeyCode" static let snipOcrHotKeyModifiers = "snipOcrHotKeyModifiers" + static let snipPreviousHotKeyCode = "snipPreviousHotKeyCode" + static let snipPreviousHotKeyModifiers = "snipPreviousHotKeyModifiers" static let recordHotKeyCode = "recordHotKeyCode" static let recordHotKeyModifiers = "recordHotKeyModifiers" static let demoTypeHotKeyCode = "demoTypeHotKeyCode" @@ -329,6 +340,14 @@ final class UserDefaultsSettingsStore: SettingsStore { settings.snipOcrHotKeyModifiers = UInt(bitPattern: defaults.integer(forKey: Key.snipOcrHotKeyModifiers)) } + if defaults.object(forKey: Key.snipPreviousHotKeyCode) != nil { + settings.snipPreviousHotKeyCode = defaults.integer(forKey: Key.snipPreviousHotKeyCode) + } + + if defaults.object(forKey: Key.snipPreviousHotKeyModifiers) != nil { + settings.snipPreviousHotKeyModifiers = UInt(bitPattern: defaults.integer(forKey: Key.snipPreviousHotKeyModifiers)) + } + if defaults.object(forKey: Key.recordHotKeyCode) != nil { settings.recordHotKeyCode = defaults.integer(forKey: Key.recordHotKeyCode) } @@ -488,6 +507,8 @@ final class UserDefaultsSettingsStore: SettingsStore { defaults.set(Int(bitPattern: settings.snipHotKeyModifiers), forKey: Key.snipHotKeyModifiers) defaults.set(settings.snipOcrHotKeyCode, forKey: Key.snipOcrHotKeyCode) defaults.set(Int(bitPattern: settings.snipOcrHotKeyModifiers), forKey: Key.snipOcrHotKeyModifiers) + defaults.set(settings.snipPreviousHotKeyCode, forKey: Key.snipPreviousHotKeyCode) + defaults.set(Int(bitPattern: settings.snipPreviousHotKeyModifiers), forKey: Key.snipPreviousHotKeyModifiers) defaults.set(settings.recordHotKeyCode, forKey: Key.recordHotKeyCode) defaults.set(Int(bitPattern: settings.recordHotKeyModifiers), forKey: Key.recordHotKeyModifiers) defaults.set(settings.demoTypeHotKeyCode, forKey: Key.demoTypeHotKeyCode) diff --git a/Sources/ZoomItMacCore/Settings/SettingsWindowController.swift b/Sources/ZoomItMacCore/Settings/SettingsWindowController.swift index 2aecb70..373cc4c 100644 --- a/Sources/ZoomItMacCore/Settings/SettingsWindowController.swift +++ b/Sources/ZoomItMacCore/Settings/SettingsWindowController.swift @@ -79,6 +79,7 @@ final class SettingsWindowController: NSObject, NSWindowDelegate { case breakTimer case snip case snipOcr + case snipPrevious case record case demoType case panorama @@ -89,6 +90,7 @@ final class SettingsWindowController: NSObject, NSWindowDelegate { private weak var breakHotKeyButton: NSButton? private weak var snipHotKeyButton: NSButton? private weak var snipOcrHotKeyButton: NSButton? + private weak var snipPreviousHotKeyButton: NSButton? private weak var recordHotKeyButton: NSButton? private weak var demoTypeHotKeyButton: NSButton? private weak var panoramaHotKeyButton: NSButton? @@ -130,6 +132,7 @@ final class SettingsWindowController: NSObject, NSWindowDelegate { breakHotKeyButton?.title = breakHotKeyDisplayString() snipHotKeyButton?.title = snipHotKeyDisplayString() snipOcrHotKeyButton?.title = snipOcrHotKeyDisplayString() + snipPreviousHotKeyButton?.title = snipPreviousHotKeyDisplayString() recordHotKeyButton?.title = recordHotKeyDisplayString() demoTypeHotKeyButton?.title = demoTypeHotKeyDisplayString() panoramaHotKeyButton?.title = panoramaHotKeyDisplayString() @@ -472,6 +475,10 @@ final class SettingsWindowController: NSObject, NSWindowDelegate { beginRecording(target: .snipOcr, sender: sender) } + @objc private func toggleSnipPreviousHotKeyRecording(_ sender: NSButton) { + beginRecording(target: .snipPrevious, sender: sender) + } + @objc private func toggleRecordHotKeyRecording(_ sender: NSButton) { beginRecording(target: .record, sender: sender) } @@ -564,6 +571,7 @@ final class SettingsWindowController: NSObject, NSWindowDelegate { conflictsWithDraw(code: newCode, modifiers: newModifiers) || conflictsWithLive(code: newCode, modifiers: newModifiers) || conflictsWithBreak(code: newCode, modifiers: newModifiers) || + conflictsWithSnipPrevious(code: newCode, modifiers: newModifiers) || conflictsWithDemoType(code: newCode, modifiers: newModifiers) { NSSound.beep() return nil @@ -576,17 +584,34 @@ final class SettingsWindowController: NSObject, NSWindowDelegate { conflictsWithLive(code: newCode, modifiers: newModifiers) || conflictsWithBreak(code: newCode, modifiers: newModifiers) || conflictsWithSnip(code: newCode, modifiers: newModifiers) || + conflictsWithSnipPrevious(code: newCode, modifiers: newModifiers) || conflictsWithDemoType(code: newCode, modifiers: newModifiers) { NSSound.beep() return nil } settings.snipOcrHotKeyCode = newCode settings.snipOcrHotKeyModifiers = newModifiers + case .snipPrevious: + if conflictsWithZoom(code: newCode, modifiers: newModifiers) || + conflictsWithDraw(code: newCode, modifiers: newModifiers) || + conflictsWithLive(code: newCode, modifiers: newModifiers) || + conflictsWithBreak(code: newCode, modifiers: newModifiers) || + conflictsWithSnip(code: newCode, modifiers: newModifiers) || + conflictsWithSnipOcr(code: newCode, modifiers: newModifiers) || + conflictsWithRecord(code: newCode, modifiers: newModifiers) || + conflictsWithDemoType(code: newCode, modifiers: newModifiers) || + conflictsWithPanorama(code: newCode, modifiers: newModifiers) { + NSSound.beep() + return nil + } + settings.snipPreviousHotKeyCode = newCode + settings.snipPreviousHotKeyModifiers = newModifiers case .record: if conflictsWithZoom(code: newCode, modifiers: newModifiers) || conflictsWithDraw(code: newCode, modifiers: newModifiers) || conflictsWithLive(code: newCode, modifiers: newModifiers) || conflictsWithBreak(code: newCode, modifiers: newModifiers) || + conflictsWithSnipPrevious(code: newCode, modifiers: newModifiers) || conflictsWithDemoType(code: newCode, modifiers: newModifiers) { NSSound.beep() return nil @@ -600,6 +625,7 @@ final class SettingsWindowController: NSObject, NSWindowDelegate { conflictsWithBreak(code: newCode, modifiers: newModifiers) || conflictsWithSnip(code: newCode, modifiers: newModifiers) || conflictsWithSnipOcr(code: newCode, modifiers: newModifiers) || + conflictsWithSnipPrevious(code: newCode, modifiers: newModifiers) || conflictsWithRecord(code: newCode, modifiers: newModifiers) || conflictsWithPanorama(code: newCode, modifiers: newModifiers) { NSSound.beep() @@ -612,6 +638,7 @@ final class SettingsWindowController: NSObject, NSWindowDelegate { conflictsWithDraw(code: newCode, modifiers: newModifiers) || conflictsWithLive(code: newCode, modifiers: newModifiers) || conflictsWithBreak(code: newCode, modifiers: newModifiers) || + conflictsWithSnipPrevious(code: newCode, modifiers: newModifiers) || conflictsWithDemoType(code: newCode, modifiers: newModifiers) { NSSound.beep() return nil @@ -652,6 +679,11 @@ final class SettingsWindowController: NSObject, NSWindowDelegate { code == settings.snipOcrHotKeyCode && modifiers == settings.snipOcrHotKeyModifiers } + private func conflictsWithSnipPrevious(code: Int, modifiers: UInt) -> Bool { + settings.snipPreviousHotKeyCode != 0 && + code == settings.snipPreviousHotKeyCode && modifiers == settings.snipPreviousHotKeyModifiers + } + private func conflictsWithRecord(code: Int, modifiers: UInt) -> Bool { code == settings.recordHotKeyCode && modifiers == settings.recordHotKeyModifiers } @@ -677,6 +709,7 @@ final class SettingsWindowController: NSObject, NSWindowDelegate { breakHotKeyButton?.title = breakHotKeyDisplayString() snipHotKeyButton?.title = snipHotKeyDisplayString() snipOcrHotKeyButton?.title = snipOcrHotKeyDisplayString() + snipPreviousHotKeyButton?.title = snipPreviousHotKeyDisplayString() recordHotKeyButton?.title = recordHotKeyDisplayString() demoTypeHotKeyButton?.title = demoTypeHotKeyDisplayString() panoramaHotKeyButton?.title = panoramaHotKeyDisplayString() @@ -707,6 +740,11 @@ final class SettingsWindowController: NSObject, NSWindowDelegate { return Self.describe(keyCode: settings.snipOcrHotKeyCode, modifiers: NSEvent.ModifierFlags(rawValue: settings.snipOcrHotKeyModifiers)) } + private func snipPreviousHotKeyDisplayString() -> String { + guard settings.snipPreviousHotKeyCode != 0 else { return "None" } + return Self.describe(keyCode: settings.snipPreviousHotKeyCode, modifiers: NSEvent.ModifierFlags(rawValue: settings.snipPreviousHotKeyModifiers)) + } + private func recordHotKeyDisplayString() -> String { Self.describe(keyCode: settings.recordHotKeyCode, modifiers: NSEvent.ModifierFlags(rawValue: settings.recordHotKeyModifiers)) } @@ -1037,6 +1075,8 @@ final class SettingsWindowController: NSObject, NSWindowDelegate { The OCR shortcut works the same way, but recognizes the text in the selected region and copies that text to the clipboard. + Capture Previous Region repeats the last successful region snip without showing the drag selector. Hold Shift with that shortcut to save to a file. + Saved images are PNG files named with the current date and time. """, wraps: true @@ -1056,6 +1096,13 @@ final class SettingsWindowController: NSObject, NSWindowDelegate { self.snipOcrHotKeyButton = snipOcrHotKeyButton let snipOcrHotKeyRow = makeRow([makeLabel("OCR region to text:"), snipOcrHotKeyButton]) + let snipPreviousHotKeyButton = NSButton(title: snipPreviousHotKeyDisplayString(), target: self, action: #selector(toggleSnipPreviousHotKeyRecording(_:))) + snipPreviousHotKeyButton.bezelStyle = .rounded + snipPreviousHotKeyButton.setButtonType(.momentaryPushIn) + snipPreviousHotKeyButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 140).isActive = true + self.snipPreviousHotKeyButton = snipPreviousHotKeyButton + let snipPreviousHotKeyRow = makeRow([makeLabel("Capture previous region:"), snipPreviousHotKeyButton]) + let copyOnSaveCheck = NSButton( checkboxWithTitle: "Also copy to clipboard when saving to a file", target: self, @@ -1082,7 +1129,7 @@ final class SettingsWindowController: NSObject, NSWindowDelegate { updateSnipSaveDirectoryControlsEnabled() - return makeColumn([help, snipHotKeyRow, snipOcrHotKeyRow, copyOnSaveCheck, saveToDirectoryCheck, directoryRow]) + return makeColumn([help, snipHotKeyRow, snipOcrHotKeyRow, copyOnSaveCheck, saveToDirectoryCheck, directoryRow, snipPreviousHotKeyRow]) } private func snipSaveDirectoryDisplayPath() -> String { From ef388649a08e207fe9d7205f3a9ddcde2e145d41 Mon Sep 17 00:00:00 2001 From: Sathya Date: Mon, 13 Jul 2026 10:11:09 +0530 Subject: [PATCH 2/5] updated read me --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 00a5223..ac51680 100644 --- a/README.md +++ b/README.md @@ -45,21 +45,21 @@ The contributor build is named `ZoomIt (Dev).app` (bundle id `com.sysinternals.z ## Default Hotkeys -| Action | Shortcut | -| --- | --- | -| Static zoom | `Control+1` | -| Draw without zoom | `Control+2` | -| Break timer | `Control+3` | -| Live zoom | `Control+4` | -| Record screen | `Control+5` | -| Record region | `Control+Shift+5` | -| Snip region to clipboard | `Control+6` | -| Snip region to file | `Control+Shift+6` | -| OCR region to clipboard | `Control+Option+6` | -| Capture previous region to clipboard | `Control+9` | -| Capture previous region to file | `Control+Shift+9` | -| Panorama to clipboard | `Control+8` | -| Panorama to file | `Control+Shift+8` | +| Action | Shortcut | +| ------------------------------------ | ------------------ | +| Static zoom | `Control+1` | +| Draw without zoom | `Control+2` | +| Break timer | `Control+3` | +| Live zoom | `Control+4` | +| Record screen | `Control+5` | +| Record region | `Control+Shift+5` | +| Snip region to clipboard | `Control+6` | +| Snip region to file | `Control+Shift+6` | +| OCR region to clipboard | `Control+Option+6` | +| Capture previous region to clipboard | `Control+9` | +| Capture previous region to file | `Control+Shift+9` | +| Panorama to clipboard | `Control+8` | +| Panorama to file | `Control+Shift+8` | All global hotkeys are configurable in Settings. While zoomed, use `Option+Up` and `Option+Down` to change zoom level, mouse wheel to zoom or resize tools depending on mode, `Command+S` / `Command+C` to save or copy the viewport, and `Esc` or right click to exit the active overlay mode. From ebe0b92a5c9bff57a86d8d8e6497771f9eb6681e Mon Sep 17 00:00:00 2001 From: Sathya Date: Mon, 13 Jul 2026 10:16:54 +0530 Subject: [PATCH 3/5] Updated Read me --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ac51680..4b7c8fc 100644 --- a/README.md +++ b/README.md @@ -56,10 +56,10 @@ The contributor build is named `ZoomIt (Dev).app` (bundle id `com.sysinternals.z | Snip region to clipboard | `Control+6` | | Snip region to file | `Control+Shift+6` | | OCR region to clipboard | `Control+Option+6` | -| Capture previous region to clipboard | `Control+9` | -| Capture previous region to file | `Control+Shift+9` | | Panorama to clipboard | `Control+8` | | Panorama to file | `Control+Shift+8` | +| Capture previous region to clipboard | `Control+9` | +| Capture previous region to file | `Control+Shift+9` | All global hotkeys are configurable in Settings. While zoomed, use `Option+Up` and `Option+Down` to change zoom level, mouse wheel to zoom or resize tools depending on mode, `Command+S` / `Command+C` to save or copy the viewport, and `Esc` or right click to exit the active overlay mode. From f51359f85fd0c2e4cf67c127b14be2704c6cbcf5 Mon Sep 17 00:00:00 2001 From: nsathya2010 <83582267+nsathya2010@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:20:49 +0530 Subject: [PATCH 4/5] Potential fix for inactive previous region selection Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Sources/ZoomItMacCore/Capture/SnipController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ZoomItMacCore/Capture/SnipController.swift b/Sources/ZoomItMacCore/Capture/SnipController.swift index 5d87014..7f03824 100644 --- a/Sources/ZoomItMacCore/Capture/SnipController.swift +++ b/Sources/ZoomItMacCore/Capture/SnipController.swift @@ -282,6 +282,7 @@ final class SnipController { return true } guard let display = displayManager.activeDisplay() else { + NSSound.beep() finish() return true } From a03f61b36df0f3245b5318da975e71a6c7432fdc Mon Sep 17 00:00:00 2001 From: nsathya2010 <83582267+nsathya2010@users.noreply.github.com> Date: Fri, 24 Jul 2026 07:43:24 +0530 Subject: [PATCH 5/5] Potential fix for pull request finding Fix for rectangle Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Capture/SnipController.swift | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/Sources/ZoomItMacCore/Capture/SnipController.swift b/Sources/ZoomItMacCore/Capture/SnipController.swift index 7f03824..e4e7b9d 100644 --- a/Sources/ZoomItMacCore/Capture/SnipController.swift +++ b/Sources/ZoomItMacCore/Capture/SnipController.swift @@ -189,21 +189,18 @@ final class SnipController { let displaySize: CGSize let rect: CGRect - func rectForCurrentDisplay(_ display: DisplayDescriptor) -> CGRect { - guard displaySize.width > 0, displaySize.height > 0 else { return rect } - if display.id == displayID { - return rect - } - - let widthScale = display.frame.width / displaySize.width - let heightScale = display.frame.height / displaySize.height - return CGRect( - x: rect.minX * widthScale, - y: rect.minY * heightScale, - width: rect.width * widthScale, - height: rect.height * heightScale - ) - } +func rectForCurrentDisplay(_ display: DisplayDescriptor) -> CGRect { + guard displaySize.width > 0, displaySize.height > 0 else { return rect } + + let widthScale = display.frame.width / displaySize.width + let heightScale = display.frame.height / displaySize.height + return CGRect( + x: rect.minX * widthScale, + y: rect.minY * heightScale, + width: rect.width * widthScale, + height: rect.height * heightScale + ) +} } private let captureService: ScreenCaptureService