// PreferencesView.swift // GreenshotMac // // Port of Greenshot's SettingsForm. import SwiftUI import AppKit struct PreferencesView: View { var body: some View { TabView { GeneralPreferences() .tabItem { Label("General", systemImage: "gearshape") } OutputPreferences() .tabItem { Label("Output", systemImage: "square.and.arrow.down") } HotkeyPreferences() .tabItem { Label("Hotkeys", systemImage: "keyboard") } EditorPreferences() .tabItem { Label("Editor", systemImage: "pencil.tip.crop.circle") } } .padding(16) .frame(width: 520, height: 380) } } private struct GeneralPreferences: View { @AppStorage(Configuration.Keys.defaultDestination) private var destination = DestinationID.editor.rawValue @AppStorage(Configuration.Keys.captureMousepointer) private var captureMousePointer = false @AppStorage(Configuration.Keys.captureDelayMillis) private var captureDelay = 0 @AppStorage(Configuration.Keys.playCameraSound) private var cameraSound = true @AppStorage(Configuration.Keys.showFlashAnimation) private var flash = true @AppStorage(Configuration.Keys.showMagnifier) private var magnifier = true @AppStorage(Configuration.Keys.showSelectionSize) private var selectionSize = true @AppStorage(Configuration.Keys.windowCaptureMode) private var windowMode = WindowCaptureMode.window.rawValue @AppStorage(Configuration.Keys.appearanceMode) private var appearance = AppearanceMode.system.rawValue @State private var launchAtLogin = LaunchAtLogin.isEnabled var body: some View { Form { Picker("Appearance:", selection: $appearance) { ForEach(AppearanceMode.allCases, id: \.rawValue) { mode in Text(mode.title).tag(mode.rawValue) } } .onChange(of: appearance) { _, _ in NotificationCenter.default.post(name: .appearanceChanged, object: nil) } Picker("After capture:", selection: $destination) { ForEach(DestinationID.allCases.filter { $0 != .openWith }, id: \.rawValue) { item in Text(item.title).tag(item.rawValue) } } Picker("Window capture:", selection: $windowMode) { Text("Window only").tag(WindowCaptureMode.window.rawValue) Text("Window with shadow").tag(WindowCaptureMode.windowWithShadow.rawValue) Text("Crop from screen").tag(WindowCaptureMode.screen.rawValue) } Toggle("Capture the mouse pointer", isOn: $captureMousePointer) Toggle("Play a shutter sound", isOn: $cameraSound) Toggle("Flash the captured area", isOn: $flash) Toggle("Show magnifier while selecting", isOn: $magnifier) Toggle("Show selection size while selecting", isOn: $selectionSize) Toggle("Launch at login", isOn: $launchAtLogin) .onChange(of: launchAtLogin) { _, newValue in LaunchAtLogin.isEnabled = newValue } HStack { Text("Capture delay:") Slider(value: Binding(get: { Double(captureDelay) }, set: { captureDelay = Int($0) }), in: 0...5000, step: 250) Text("\(captureDelay) ms").frame(width: 70, alignment: .trailing) } } .formStyle(.grouped) } } private struct OutputPreferences: View { @AppStorage(Configuration.Keys.outputFilePath) private var outputPath = "" @AppStorage(Configuration.Keys.outputFileFilenamePattern) private var pattern = "" @AppStorage(Configuration.Keys.outputFileFormat) private var format = OutputFormat.png.rawValue @AppStorage(Configuration.Keys.outputFileJpegQuality) private var jpegQuality = 80 @AppStorage(Configuration.Keys.copyPathToClipboard) private var copyPath = false var body: some View { Form { HStack { Text("Save to:") Text(outputPath).lineLimit(1).truncationMode(.middle) .foregroundStyle(.secondary) Spacer() Button("Choose…", action: chooseFolder) } TextField("Filename pattern:", text: $pattern) Text("Variables: ${capturetime}, ${title}, ${appname}, ${user}, ${hostname}, ${mode}, ${NUM}. " + "Example: ${capturetime:d\"yyyy-MM-dd HH_mm_ss\"}-${title}") .font(.caption) .foregroundStyle(.secondary) Picker("Format:", selection: $format) { ForEach(OutputFormat.allCases, id: \.rawValue) { item in Text(item.displayName).tag(item.rawValue) } } if format == OutputFormat.jpg.rawValue { HStack { Text("JPEG quality:") Slider(value: Binding(get: { Double(jpegQuality) }, set: { jpegQuality = Int($0) }), in: 10...100, step: 5) Text("\(jpegQuality)%").frame(width: 50, alignment: .trailing) } } Toggle("Copy the file path to the clipboard after saving", isOn: $copyPath) } .formStyle(.grouped) } private func chooseFolder() { let panel = NSOpenPanel() panel.canChooseDirectories = true panel.canChooseFiles = false panel.allowsMultipleSelection = false panel.directoryURL = URL(fileURLWithPath: outputPath, isDirectory: true) if panel.runModal() == .OK, let url = panel.url { outputPath = url.path } } } private struct HotkeyPreferences: View { var body: some View { Form { ForEach(HotkeyAction.allCases, id: \.rawValue) { action in HStack { Text(action.title) Spacer() HotkeyRecorder(action: action) .frame(width: 150, height: 24) } } Text("Click a shortcut, then press the new combination. Backspace clears it, Esc cancels. " + "A modifier key is required.") .font(.caption) .foregroundStyle(.secondary) } .formStyle(.grouped) } } private struct EditorPreferences: View { @AppStorage(Configuration.Keys.editorLineThickness) private var lineThickness = 2 @AppStorage(Configuration.Keys.editorFontFamily) private var fontFamily = "Helvetica" @AppStorage(Configuration.Keys.editorFontSize) private var fontSize = 14 @AppStorage(Configuration.Keys.editorPixelSize) private var pixelSize = 8 @AppStorage(Configuration.Keys.editorBlurRadius) private var blurRadius = 12 @AppStorage(Configuration.Keys.editorDropShadow) private var dropShadow = false var body: some View { Form { Stepper("Default line thickness: \(lineThickness) px", value: $lineThickness, in: 0...20) Picker("Default font:", selection: $fontFamily) { ForEach(["Helvetica", "Helvetica Neue", "Menlo", "Times New Roman", "Arial", "Georgia"], id: \.self) { Text($0).tag($0) } } Stepper("Default font size: \(fontSize) pt", value: $fontSize, in: 6...96) Stepper("Pixelation size: \(pixelSize) px", value: $pixelSize, in: 2...40) Stepper("Blur radius: \(blurRadius) px", value: $blurRadius, in: 1...40) Toggle("Drop shadow on new elements", isOn: $dropShadow) } .formStyle(.grouped) } }