181 lines
7.5 KiB
Swift
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…"
|
|
}
|
|
}
|
|
}
|