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

181 lines
7.5 KiB
Swift

// DestinationPicker.swift
// GreenshotMac
//
// Port of Greenshot's destination picker dialog: a preview of the capture,
// one large button per destination, and "always use this destination".
import AppKit
@MainActor
final class DestinationPickerWindowController: NSWindowController, NSWindowDelegate {
private static var openPickers: [DestinationPickerWindowController] = []
private let capture: Capture
private let destinations: [DestinationID]
private let rememberCheckbox = NSButton(checkboxWithTitle: "Always use this destination (you can change it in Preferences)",
target: nil,
action: nil)
static func present(for capture: Capture) {
let controller = DestinationPickerWindowController(capture: capture)
openPickers.append(controller)
controller.showWindow(nil)
controller.window?.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
}
private init(capture: Capture) {
self.capture = capture
let configured = Configuration.shared.pickerDestinations.filter { $0 != .picker }
self.destinations = configured.isEmpty ? [.editor, .file, .clipboard, .email] : configured
let window = NSWindow(contentRect: CGRect(x: 0, y: 0, width: 560, height: 560),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false)
window.title = "Choose Destination for Screenshot"
window.isReleasedWhenClosed = false
window.level = .floating
window.center()
super.init(window: window)
window.delegate = self
buildInterface()
}
required init?(coder: NSCoder) { fatalError("init(coder:) is not used") }
func windowWillClose(_ notification: Notification) {
DestinationPickerWindowController.openPickers.removeAll { $0 === self }
}
// MARK: - Interface
private func buildInterface() {
guard let window, let contentView = window.contentView else { return }
let preview = NSImageView()
preview.image = capture.nsImage
preview.imageScaling = .scaleProportionallyDown
preview.imageAlignment = .alignCenter
preview.wantsLayer = true
preview.layer?.backgroundColor = NSColor.underPageBackgroundColor.cgColor
preview.layer?.borderColor = NSColor.separatorColor.cgColor
preview.layer?.borderWidth = 1
preview.layer?.cornerRadius = 4
preview.translatesAutoresizingMaskIntoConstraints = false
preview.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
preview.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
let buttonRow = NSStackView()
buttonRow.orientation = .horizontal
buttonRow.alignment = .top
buttonRow.distribution = .fillEqually
buttonRow.spacing = 8
buttonRow.translatesAutoresizingMaskIntoConstraints = false
for (index, destination) in destinations.enumerated() {
buttonRow.addArrangedSubview(makeButton(for: destination, index: index))
}
rememberCheckbox.target = self
rememberCheckbox.action = #selector(rememberToggled)
rememberCheckbox.translatesAutoresizingMaskIntoConstraints = false
let separator = NSBox()
separator.boxType = .separator
separator.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(preview)
contentView.addSubview(separator)
contentView.addSubview(buttonRow)
contentView.addSubview(rememberCheckbox)
NSLayoutConstraint.activate([
preview.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16),
preview.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
preview.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
separator.topAnchor.constraint(equalTo: preview.bottomAnchor, constant: 14),
separator.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
separator.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
buttonRow.topAnchor.constraint(equalTo: separator.bottomAnchor, constant: 14),
buttonRow.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
buttonRow.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
rememberCheckbox.topAnchor.constraint(equalTo: buttonRow.bottomAnchor, constant: 14),
rememberCheckbox.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
rememberCheckbox.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16)
])
// Size the window so the preview keeps the capture's aspect ratio, within sensible bounds.
let aspect = capture.pointSize.height / max(1, capture.pointSize.width)
let previewWidth: CGFloat = 528
let previewHeight = min(max(180, previewWidth * aspect), 380)
preview.heightAnchor.constraint(equalToConstant: previewHeight).isActive = true
window.setContentSize(NSSize(width: 560, height: previewHeight + 190))
window.center()
}
private func makeButton(for destination: DestinationID, index: Int) -> NSView {
let button = NSButton()
button.bezelStyle = .shadowlessSquare
button.isBordered = false
button.imagePosition = .imageAbove
button.title = destination.pickerTitle
button.font = NSFont.systemFont(ofSize: 11)
button.image = NSImage(systemSymbolName: destination.symbolName, accessibilityDescription: destination.title)?
.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 34, weight: .regular))
button.imageScaling = .scaleNone
button.target = self
button.action = #selector(destinationClicked(_:))
button.tag = index
button.toolTip = destination.title
// ⌘1…⌘9 select a destination from the keyboard.
if index < 9 {
button.keyEquivalent = String(index + 1)
button.keyEquivalentModifierMask = [.command]
}
button.setContentHuggingPriority(.defaultLow, for: .horizontal)
button.heightAnchor.constraint(equalToConstant: 76).isActive = true
return button
}
// MARK: - Actions
@objc private func destinationClicked(_ sender: NSButton) {
guard sender.tag >= 0, sender.tag < destinations.count else { return }
let destination = destinations[sender.tag]
if rememberCheckbox.state == .on {
Configuration.shared.defaultDestination = destination
}
close()
Destinations.export(capture, to: destination)
}
@objc private func rememberToggled() {}
override func cancelOperation(_ sender: Any?) {
close()
}
}
extension DestinationID {
/// Shorter labels for the picker buttons, matching Greenshot's wording.
var pickerTitle: String {
switch self {
case .editor: return "Edit in Greenshot"
case .file: return "Save to Disk"
case .fileWithDialog: return "Save As…"
case .clipboard: return "Copy to Clipboard"
case .email: return "Send as Mail"
case .printer: return "Print"
case .share: return "Share…"
case .openWith: return "Open With…"
case .picker: return "Choose…"
}
}
}