// 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, Keys.appearanceMode: AppearanceMode.system.rawValue ]) } 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 appearanceMode = "AppearanceMode" 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) } } var appearanceMode: AppearanceMode { get { AppearanceMode(rawValue: defaults.string(forKey: Keys.appearanceMode) ?? "system") ?? .system } set { defaults.set(newValue.rawValue, forKey: Keys.appearanceMode) } } // 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) } }