// 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…" } } }