Initial commit
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
// 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…"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user