Files
Snaggle/GreenshotMac/App/CaptureCoordinator.swift
T
2026-09-17 15:15:45 -05:00

171 lines
5.7 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// CaptureCoordinator.swift
// GreenshotMac
//
// Drives the capture flows, the macOS counterpart of Greenshot's CaptureHelper.
import AppKit
import UniformTypeIdentifiers
@MainActor
final class CaptureCoordinator {
static let shared = CaptureCoordinator()
private var overlay: CaptureOverlayController?
private var isCapturing = false
private var lastRegion: CGRect?
private init() {}
var hasLastRegion: Bool { lastRegion != nil }
// MARK: - Entry points
func captureRegion() {
beginOverlayCapture(mode: .region)
}
func captureWindow() {
beginOverlayCapture(mode: .window)
}
func captureFullscreen() {
guard !isCapturing, CaptureEngine.ensurePermission() else { return requestPermission() }
isCapturing = true
Task {
defer { isCapturing = false }
do {
try await delayIfNeeded()
let capture = try await CaptureEngine.captureFullscreen()
finish(capture)
} catch {
report(error)
}
}
}
func captureLastRegion() {
guard let lastRegion else { return captureRegion() }
guard !isCapturing, CaptureEngine.ensurePermission() else { return requestPermission() }
isCapturing = true
Task {
defer { isCapturing = false }
do {
try await delayIfNeeded()
let snapshots = try await CaptureEngine.snapshotAllDisplays()
guard let snapshot = snapshots.first(where: { $0.cgFrame.intersects(lastRegion) }) else {
throw CaptureError.noContent
}
let capture = try CaptureEngine.crop(snapshot: snapshot, cgRect: lastRegion, mode: .lastRegion)
finish(capture)
} catch {
report(error)
}
}
}
/// Opens an image file in the editor, the equivalent of Greenshot's "Open image from file".
func openImageFile() {
let panel = NSOpenPanel()
panel.allowedContentTypes = [.image]
panel.allowsMultipleSelection = false
NSApp.activate(ignoringOtherApps: true)
guard panel.runModal() == .OK,
let url = panel.url,
let image = NSImage(contentsOf: url),
let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return }
var details = CaptureDetails()
details.captureMode = .importedFile
details.title = url.deletingPathExtension().lastPathComponent
details.scaleFactor = CGFloat(cgImage.width) / max(1, image.size.width)
EditorWindowController.open(with: Capture(image: cgImage, details: details))
}
// MARK: - Overlay flow
private func beginOverlayCapture(mode: OverlayMode) {
guard !isCapturing else { return }
guard CaptureEngine.ensurePermission() else { return requestPermission() }
isCapturing = true
Task {
do {
try await delayIfNeeded()
let content = try await CaptureEngine.shareableContent()
let snapshots = try await CaptureEngine.snapshotAllDisplays()
let windows = CaptureEngine.selectableWindows(from: content)
let controller = CaptureOverlayController(snapshots: snapshots, windows: windows, mode: mode)
overlay = controller
controller.present { result in
self.overlay = nil
self.isCapturing = false
self.handle(result)
}
} catch {
isCapturing = false
report(error)
}
}
}
private func handle(_ result: OverlayResult) {
switch result {
case .cancelled:
return
case .region(let snapshot, let rect):
lastRegion = rect
do {
let capture = try CaptureEngine.crop(snapshot: snapshot, cgRect: rect, mode: .region)
finish(capture)
} catch {
report(error)
}
case .window(let info):
Task {
do {
let capture = try await CaptureEngine.captureWindow(info)
finish(capture)
} catch {
report(error)
}
}
}
}
// MARK: - Shared
private func finish(_ capture: Capture) {
CaptureEngine.playFeedback(on: capture.details.screenBounds)
Destinations.export(capture, to: Configuration.shared.defaultDestination)
}
private func delayIfNeeded() async throws {
let millis = Configuration.shared.captureDelayMillis
guard millis > 0 else { return }
try await Task.sleep(nanoseconds: UInt64(millis) * 1_000_000)
}
private func report(_ error: Error) {
if let captureError = error as? CaptureError, case .cancelled = captureError { return }
Destinations.presentError(error)
}
private func requestPermission() {
let alert = NSAlert()
alert.messageText = "Screen Recording permission needed"
alert.informativeText = """
macOS requires explicit permission before an app can capture the screen.
Open System Settings › Privacy & Security › Screen Recording, enable GreenshotMac, then try again.
"""
alert.addButton(withTitle: "Open System Settings")
alert.addButton(withTitle: "Cancel")
NSApp.activate(ignoringOtherApps: true)
if alert.runModal() == .alertFirstButtonReturn,
let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture") {
NSWorkspace.shared.open(url)
}
}
}