Initial commit
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user