Initial commit
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
// HotkeyRecorder.swift
|
||||
// GreenshotMac
|
||||
//
|
||||
// A small AppKit control that records the next key combination, wrapped for SwiftUI.
|
||||
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
import Carbon.HIToolbox
|
||||
|
||||
final class HotkeyRecorderControl: NSButton {
|
||||
|
||||
var onRecord: ((HotkeySpec) -> Void)?
|
||||
var spec: HotkeySpec = .none {
|
||||
didSet { refreshTitle() }
|
||||
}
|
||||
|
||||
private var isRecording = false {
|
||||
didSet { refreshTitle() }
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
bezelStyle = .rounded
|
||||
setButtonType(.momentaryPushIn)
|
||||
target = self
|
||||
action = #selector(startRecording)
|
||||
refreshTitle()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not used") }
|
||||
|
||||
private func refreshTitle() {
|
||||
title = isRecording ? "Press a key…" : HotkeyManager.description(of: spec)
|
||||
}
|
||||
|
||||
@objc private func startRecording() {
|
||||
isRecording = true
|
||||
window?.makeFirstResponder(self)
|
||||
}
|
||||
|
||||
override var acceptsFirstResponder: Bool { true }
|
||||
|
||||
override func keyDown(with event: NSEvent) {
|
||||
guard isRecording else { return super.keyDown(with: event) }
|
||||
|
||||
if event.keyCode == UInt16(kVK_Escape) {
|
||||
isRecording = false
|
||||
return
|
||||
}
|
||||
if event.keyCode == UInt16(kVK_Delete) {
|
||||
isRecording = false
|
||||
spec = .none
|
||||
onRecord?(.none)
|
||||
return
|
||||
}
|
||||
|
||||
let modifiers = HotkeyManager.carbonModifiers(from: event.modifierFlags)
|
||||
guard modifiers != 0 else {
|
||||
NSSound.beep() // a global hotkey without modifiers would swallow normal typing
|
||||
return
|
||||
}
|
||||
let recorded = HotkeySpec(keyCode: UInt32(event.keyCode), modifiers: modifiers)
|
||||
isRecording = false
|
||||
spec = recorded
|
||||
onRecord?(recorded)
|
||||
}
|
||||
|
||||
override func resignFirstResponder() -> Bool {
|
||||
isRecording = false
|
||||
return super.resignFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
struct HotkeyRecorder: NSViewRepresentable {
|
||||
let action: HotkeyAction
|
||||
|
||||
func makeNSView(context: Context) -> HotkeyRecorderControl {
|
||||
let control = HotkeyRecorderControl()
|
||||
control.spec = HotkeyBindings.shared.spec(for: action)
|
||||
control.onRecord = { spec in
|
||||
HotkeyBindings.shared.set(spec, for: action)
|
||||
}
|
||||
return control
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: HotkeyRecorderControl, context: Context) {
|
||||
nsView.spec = HotkeyBindings.shared.spec(for: action)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
// 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
|
||||
@State private var launchAtLogin = LaunchAtLogin.isEnabled
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// PreferencesWindowController.swift
|
||||
// GreenshotMac
|
||||
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
final class PreferencesWindowController: NSWindowController {
|
||||
|
||||
convenience init() {
|
||||
let hosting = NSHostingController(rootView: PreferencesView())
|
||||
let window = NSWindow(contentViewController: hosting)
|
||||
window.title = "GreenshotMac Preferences"
|
||||
window.styleMask = [.titled, .closable]
|
||||
window.setContentSize(NSSize(width: 552, height: 412))
|
||||
window.center()
|
||||
window.isReleasedWhenClosed = false
|
||||
self.init(window: window)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user