Initial commit

This commit is contained in:
Alex E. Tafoya
2026-09-17 15:15:45 -05:00
commit 87fe7927c4
36 changed files with 5113 additions and 0 deletions
@@ -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)
}
}