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

315 lines
12 KiB
Swift

// Configuration.swift
// GreenshotMac
//
// Port of Greenshot's ICoreConfiguration / IEditorConfiguration onto UserDefaults.
import AppKit
enum OutputFormat: String, CaseIterable, Codable {
case png, jpg, tiff, bmp, gif
var displayName: String { rawValue.uppercased() }
var fileType: NSBitmapImageRep.FileType {
switch self {
case .png: return .png
case .jpg: return .jpeg
case .tiff: return .tiff
case .bmp: return .bmp
case .gif: return .gif
}
}
}
enum WindowCaptureMode: String, CaseIterable, Codable {
case screen // crop the window rectangle out of a full screen grab
case window // ask ScreenCaptureKit for the window itself (no shadow)
case windowWithShadow
}
/// Identifiers of the destinations a capture can be sent to.
enum DestinationID: String, CaseIterable, Codable {
case picker
case editor
case clipboard
case file
case fileWithDialog
case printer
case share
case email
case openWith
var title: String {
switch self {
case .picker: return "Show destination picker"
case .editor: return "Open in image editor"
case .clipboard: return "Copy to clipboard"
case .file: return "Save (auto-named)"
case .fileWithDialog: return "Save as…"
case .printer: return "Print"
case .share: return "Share…"
case .email: return "Send as email"
case .openWith: return "Open with…"
}
}
var symbolName: String {
switch self {
case .picker: return "list.bullet"
case .editor: return "pencil.tip.crop.circle"
case .clipboard: return "doc.on.clipboard"
case .file: return "square.and.arrow.down"
case .fileWithDialog: return "folder"
case .printer: return "printer"
case .share: return "square.and.arrow.up"
case .email: return "envelope"
case .openWith: return "arrow.up.forward.app"
}
}
}
/// A modifier + key combination used for a global hotkey.
struct HotkeySpec: Codable, Equatable {
var keyCode: UInt32
var modifiers: UInt32 // Carbon modifier mask
static let none = HotkeySpec(keyCode: 0, modifiers: 0)
var isEmpty: Bool { keyCode == 0 && modifiers == 0 }
}
final class Configuration {
static let shared = Configuration()
private let defaults = UserDefaults.standard
private init() {
defaults.register(defaults: [
Keys.outputFileFilenamePattern: "${capturetime:d\"yyyy-MM-dd HH_mm_ss\"}-${title}",
Keys.outputFileFormat: OutputFormat.png.rawValue,
Keys.outputFileJpegQuality: 80,
Keys.outputFilePath: FileManager.default.urls(for: .picturesDirectory, in: .userDomainMask).first?.path
?? NSHomeDirectory(),
Keys.captureMousepointer: false,
Keys.captureDelayMillis: 0,
Keys.playCameraSound: true,
Keys.showFlashAnimation: true,
Keys.windowCaptureMode: WindowCaptureMode.window.rawValue,
Keys.defaultDestination: DestinationID.picker.rawValue,
Keys.pickerDestinations: [DestinationID.editor.rawValue,
DestinationID.file.rawValue,
DestinationID.clipboard.rawValue,
DestinationID.email.rawValue],
Keys.copyPathToClipboard: false,
Keys.editorLineThickness: 2,
Keys.editorLineColor: "#FF0000",
Keys.editorFillColor: "#00000000",
Keys.editorFontFamily: "Helvetica",
Keys.editorFontSize: 14,
Keys.editorBlurRadius: 12,
Keys.editorPixelSize: 8,
Keys.editorHighlightColor: "#FFFF00",
Keys.editorDropShadow: false,
Keys.showMagnifier: true,
Keys.showSelectionSize: true
])
}
enum Keys {
static let outputFileFilenamePattern = "OutputFileFilenamePattern"
static let outputFileFormat = "OutputFileFormat"
static let outputFileJpegQuality = "OutputFileJpegQuality"
static let outputFilePath = "OutputFilePath"
static let captureMousepointer = "CaptureMousepointer"
static let captureDelayMillis = "CaptureDelayMillis"
static let playCameraSound = "PlayCameraSound"
static let showFlashAnimation = "ShowFlashAnimation"
static let windowCaptureMode = "WindowCaptureMode"
static let defaultDestination = "DefaultDestination"
static let pickerDestinations = "PickerDestinations"
static let copyPathToClipboard = "CopyPathToClipboard"
static let editorLineThickness = "EditorLineThickness"
static let editorLineColor = "EditorLineColor"
static let editorFillColor = "EditorFillColor"
static let editorFontFamily = "EditorFontFamily"
static let editorFontSize = "EditorFontSize"
static let editorBlurRadius = "EditorBlurRadius"
static let editorPixelSize = "EditorPixelSize"
static let editorHighlightColor = "EditorHighlightColor"
static let editorDropShadow = "EditorDropShadow"
static let showMagnifier = "ShowMagnifier"
static let showSelectionSize = "ShowSelectionSize"
static let hotkeyRegion = "HotkeyRegion"
static let hotkeyFullscreen = "HotkeyFullscreen"
static let hotkeyWindow = "HotkeyWindow"
static let hotkeyLastRegion = "HotkeyLastRegion"
}
// MARK: - Output
var outputFileFilenamePattern: String {
get { defaults.string(forKey: Keys.outputFileFilenamePattern) ?? "" }
set { defaults.set(newValue, forKey: Keys.outputFileFilenamePattern) }
}
var outputFileFormat: OutputFormat {
get { OutputFormat(rawValue: defaults.string(forKey: Keys.outputFileFormat) ?? "png") ?? .png }
set { defaults.set(newValue.rawValue, forKey: Keys.outputFileFormat) }
}
var outputFileJpegQuality: Int {
get { defaults.integer(forKey: Keys.outputFileJpegQuality) }
set { defaults.set(newValue, forKey: Keys.outputFileJpegQuality) }
}
var outputFilePath: String {
get { defaults.string(forKey: Keys.outputFilePath) ?? NSHomeDirectory() }
set { defaults.set(newValue, forKey: Keys.outputFilePath) }
}
var copyPathToClipboard: Bool {
get { defaults.bool(forKey: Keys.copyPathToClipboard) }
set { defaults.set(newValue, forKey: Keys.copyPathToClipboard) }
}
// MARK: - Capture
var captureMousepointer: Bool {
get { defaults.bool(forKey: Keys.captureMousepointer) }
set { defaults.set(newValue, forKey: Keys.captureMousepointer) }
}
var captureDelayMillis: Int {
get { defaults.integer(forKey: Keys.captureDelayMillis) }
set { defaults.set(newValue, forKey: Keys.captureDelayMillis) }
}
var playCameraSound: Bool {
get { defaults.bool(forKey: Keys.playCameraSound) }
set { defaults.set(newValue, forKey: Keys.playCameraSound) }
}
var showFlashAnimation: Bool {
get { defaults.bool(forKey: Keys.showFlashAnimation) }
set { defaults.set(newValue, forKey: Keys.showFlashAnimation) }
}
var windowCaptureMode: WindowCaptureMode {
get { WindowCaptureMode(rawValue: defaults.string(forKey: Keys.windowCaptureMode) ?? "window") ?? .window }
set { defaults.set(newValue.rawValue, forKey: Keys.windowCaptureMode) }
}
var showMagnifier: Bool {
get { defaults.bool(forKey: Keys.showMagnifier) }
set { defaults.set(newValue, forKey: Keys.showMagnifier) }
}
var showSelectionSize: Bool {
get { defaults.bool(forKey: Keys.showSelectionSize) }
set { defaults.set(newValue, forKey: Keys.showSelectionSize) }
}
// MARK: - Destinations
var defaultDestination: DestinationID {
get { DestinationID(rawValue: defaults.string(forKey: Keys.defaultDestination) ?? "editor") ?? .editor }
set { defaults.set(newValue.rawValue, forKey: Keys.defaultDestination) }
}
var pickerDestinations: [DestinationID] {
get { (defaults.stringArray(forKey: Keys.pickerDestinations) ?? []).compactMap(DestinationID.init(rawValue:)) }
set { defaults.set(newValue.map(\.rawValue), forKey: Keys.pickerDestinations) }
}
// MARK: - Editor defaults
var editorLineThickness: Int {
get { defaults.integer(forKey: Keys.editorLineThickness) }
set { defaults.set(newValue, forKey: Keys.editorLineThickness) }
}
var editorLineColor: NSColor {
get { NSColor(hexString: defaults.string(forKey: Keys.editorLineColor) ?? "#FF0000") ?? .systemRed }
set { defaults.set(newValue.hexString, forKey: Keys.editorLineColor) }
}
var editorFillColor: NSColor {
get { NSColor(hexString: defaults.string(forKey: Keys.editorFillColor) ?? "#00000000") ?? .clear }
set { defaults.set(newValue.hexString, forKey: Keys.editorFillColor) }
}
var editorHighlightColor: NSColor {
get { NSColor(hexString: defaults.string(forKey: Keys.editorHighlightColor) ?? "#FFFF00") ?? .systemYellow }
set { defaults.set(newValue.hexString, forKey: Keys.editorHighlightColor) }
}
var editorFontFamily: String {
get { defaults.string(forKey: Keys.editorFontFamily) ?? "Helvetica" }
set { defaults.set(newValue, forKey: Keys.editorFontFamily) }
}
var editorFontSize: Int {
get { defaults.integer(forKey: Keys.editorFontSize) }
set { defaults.set(newValue, forKey: Keys.editorFontSize) }
}
var editorBlurRadius: Int {
get { defaults.integer(forKey: Keys.editorBlurRadius) }
set { defaults.set(newValue, forKey: Keys.editorBlurRadius) }
}
var editorPixelSize: Int {
get { defaults.integer(forKey: Keys.editorPixelSize) }
set { defaults.set(newValue, forKey: Keys.editorPixelSize) }
}
var editorDropShadow: Bool {
get { defaults.bool(forKey: Keys.editorDropShadow) }
set { defaults.set(newValue, forKey: Keys.editorDropShadow) }
}
// MARK: - Hotkeys
func hotkey(forKey key: String, default fallback: HotkeySpec) -> HotkeySpec {
guard let data = defaults.data(forKey: key),
let spec = try? JSONDecoder().decode(HotkeySpec.self, from: data) else { return fallback }
return spec
}
func setHotkey(_ spec: HotkeySpec, forKey key: String) {
if let data = try? JSONEncoder().encode(spec) {
defaults.set(data, forKey: key)
}
}
}
extension NSColor {
/// #RRGGBB or #RRGGBBAA
convenience init?(hexString: String) {
var hex = hexString.trimmingCharacters(in: .whitespacesAndNewlines)
if hex.hasPrefix("#") { hex.removeFirst() }
guard hex.count == 6 || hex.count == 8, let value = UInt64(hex, radix: 16) else { return nil }
let r, g, b, a: CGFloat
if hex.count == 6 {
r = CGFloat((value >> 16) & 0xFF) / 255
g = CGFloat((value >> 8) & 0xFF) / 255
b = CGFloat(value & 0xFF) / 255
a = 1
} else {
r = CGFloat((value >> 24) & 0xFF) / 255
g = CGFloat((value >> 16) & 0xFF) / 255
b = CGFloat((value >> 8) & 0xFF) / 255
a = CGFloat(value & 0xFF) / 255
}
self.init(srgbRed: r, green: g, blue: b, alpha: a)
}
var hexString: String {
guard let rgb = usingColorSpace(.sRGB) else { return "#000000FF" }
let r = Int((rgb.redComponent * 255).rounded())
let g = Int((rgb.greenComponent * 255).rounded())
let b = Int((rgb.blueComponent * 255).rounded())
let a = Int((rgb.alphaComponent * 255).rounded())
return String(format: "#%02X%02X%02X%02X", r, g, b, a)
}
}