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

210 lines
7.6 KiB
Swift

// Destinations.swift
// GreenshotMac
//
// Port of Greenshot's IDestination implementations for macOS.
import AppKit
import UniformTypeIdentifiers
@MainActor
enum Destinations {
/// Sends a capture to a destination. `anchor` is used to position pickers and share sheets.
@discardableResult
static func export(_ capture: Capture,
to destination: DestinationID,
anchor: NSView? = nil) -> Bool {
switch destination {
case .picker:
DestinationPickerWindowController.present(for: capture)
return true
case .editor:
EditorWindowController.open(with: capture)
return true
case .clipboard:
copyToClipboard(capture)
Notifier.show(title: "Copied to clipboard", body: nil)
return true
case .file:
return saveToFile(capture)
case .fileWithDialog:
return saveWithDialog(capture)
case .printer:
printCapture(capture)
return true
case .share:
share(capture, anchor: anchor)
return true
case .email:
sendAsEmail(capture, anchor: anchor)
return true
case .openWith:
openWith(capture)
return true
}
}
// MARK: - Clipboard
static func copyToClipboard(_ capture: Capture) {
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
let image = capture.nsImage
pasteboard.writeObjects([image])
if let data = ImageExporter.data(from: capture.image, format: .png, jpegQuality: 100) {
pasteboard.setData(data, forType: .png)
}
}
// MARK: - Files
@discardableResult
static func saveToFile(_ capture: Capture) -> Bool {
let url = ImageExporter.suggestedURL(for: capture.details)
do {
try ImageExporter.write(capture.image, to: url, format: Configuration.shared.outputFileFormat)
capture.details.filePath = url.path
if Configuration.shared.copyPathToClipboard {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(url.path, forType: .string)
}
Notifier.show(title: "Screenshot saved", body: url.lastPathComponent, revealing: url)
return true
} catch {
presentError(error)
return false
}
}
@discardableResult
static func saveWithDialog(_ capture: Capture) -> Bool {
let panel = NSSavePanel()
let configuration = Configuration.shared
panel.nameFieldStringValue = FilenamePattern.filename(pattern: configuration.outputFileFilenamePattern,
details: capture.details,
format: configuration.outputFileFormat)
panel.directoryURL = URL(fileURLWithPath: configuration.outputFilePath, isDirectory: true)
panel.allowedContentTypes = [.png, .jpeg, .tiff, .bmp, .gif]
panel.canCreateDirectories = true
panel.isExtensionHidden = false
NSApp.activate(ignoringOtherApps: true)
guard panel.runModal() == .OK, let url = panel.url else { return false }
let format = ImageExporter.format(forPathExtension: url.pathExtension)
do {
try ImageExporter.write(capture.image, to: url, format: format)
capture.details.filePath = url.path
return true
} catch {
presentError(error)
return false
}
}
// MARK: - Print
static func printCapture(_ capture: Capture) {
let image = capture.nsImage
let info = NSPrintInfo.shared
info.horizontalPagination = .fit
info.verticalPagination = .fit
info.isHorizontallyCentered = true
info.isVerticallyCentered = true
info.orientation = image.size.width > image.size.height ? .landscape : .portrait
let imageView = NSImageView(frame: CGRect(origin: .zero, size: image.size))
imageView.image = image
imageView.imageScaling = .scaleProportionallyUpOrDown
let operation = NSPrintOperation(view: imageView, printInfo: info)
operation.showsPrintPanel = true
operation.showsProgressPanel = true
NSApp.activate(ignoringOtherApps: true)
operation.run()
}
// MARK: - Share
static func share(_ capture: Capture, anchor: NSView?) {
guard let url = writeTemporaryFile(capture) else { return }
let picker = NSSharingServicePicker(items: [url])
if let anchor {
picker.show(relativeTo: anchor.bounds, of: anchor, preferredEdge: .minY)
} else if let window = anchorWindow() {
let view = window.contentView ?? NSView()
picker.show(relativeTo: view.bounds, of: view, preferredEdge: .minY)
}
}
// MARK: - Open with
static func openWith(_ capture: Capture) {
guard let url = writeTemporaryFile(capture) else { return }
let panel = NSOpenPanel()
panel.message = "Choose an application to open the screenshot with"
panel.directoryURL = URL(fileURLWithPath: "/Applications")
panel.allowedContentTypes = [.application]
panel.allowsMultipleSelection = false
panel.canChooseDirectories = false
NSApp.activate(ignoringOtherApps: true)
guard panel.runModal() == .OK, let application = panel.url else { return }
NSWorkspace.shared.open([url],
withApplicationAt: application,
configuration: NSWorkspace.OpenConfiguration()) { _, error in
if let error {
DispatchQueue.main.async { presentError(error) }
}
}
}
// MARK: - Email
static func sendAsEmail(_ capture: Capture, anchor: NSView?) {
guard let url = writeTemporaryFile(capture) else { return }
let subject = capture.details.title.isEmpty ? "Screenshot" : "Screenshot: \(capture.details.title)"
if let service = NSSharingService(named: .composeEmail) {
service.subject = subject
service.perform(withItems: [url])
} else {
// No mail client is configured for the share service; fall back to the share sheet.
share(capture, anchor: anchor)
}
}
// MARK: - Helpers
static func writeTemporaryFile(_ capture: Capture) -> URL? {
let configuration = Configuration.shared
let name = FilenamePattern.filename(pattern: configuration.outputFileFilenamePattern,
details: capture.details,
format: configuration.outputFileFormat)
let directory = FileManager.default.temporaryDirectory
.appendingPathComponent("GreenshotMac", isDirectory: true)
let url = ImageExporter.uniqueURL(directory.appendingPathComponent(name))
do {
try ImageExporter.write(capture.image, to: url, format: configuration.outputFileFormat)
return url
} catch {
presentError(error)
return nil
}
}
private static func anchorWindow() -> NSWindow? {
NSApp.keyWindow ?? NSApp.windows.first { $0.isVisible }
}
static func presentError(_ error: Error) {
let alert = NSAlert()
alert.messageText = "GreenshotMac"
alert.informativeText = error.localizedDescription
alert.alertStyle = .warning
alert.addButton(withTitle: "OK")
NSApp.activate(ignoringOtherApps: true)
alert.runModal()
}
}