Files
TZGyn a01d5def89 Give mixed selections something to do
A Drag of different kinds showed an empty wheel, because the catalog only
answers for a single category. Filee's answer is a mixed profile, so add
Zip (one archive, flat names) and Merge (non-PDFs are converted to PDF
first); a multi-file drag of one kind gets Zip too.
2026-10-20 01:30:42 +08:00

112 lines
4.2 KiB
Swift

import AppKit
import Foundation
/// Native menu fallback for the paths where there is no drag to follow: the
/// menu bar's "Konvert Files…" item and the Finder Quick Action.
enum QuickMenu {
private final class Choice: NSObject {
let handler: () -> Void
init(_ handler: @escaping () -> Void) {
self.handler = handler
}
@objc func fire() {
handler()
}
}
/// The menu items hold their target weakly, so the choices are retained
/// here for the duration of the (synchronous) popup.
private static var retained: [Choice] = []
static func show(urls: [URL], at point: CGPoint) {
guard !urls.isEmpty else { return }
retained.removeAll()
let menu = NSMenu()
menu.autoenablesItems = false
let header = NSMenuItem(
title: WheelBuilder.summary(for: urls),
action: nil,
keyEquivalent: ""
)
header.isEnabled = false
menu.addItem(header)
menu.addItem(.separator())
let category = FileKind.category(of: urls[0])
// The convert wheel also carries the combine tools, so it is empty only
// when there is genuinely nothing to offer.
let conversions = WheelBuilder.items(kind: .convert, urls: urls)
if conversions.isEmpty {
let item = NSMenuItem(title: "No conversions for this selection", action: nil, keyEquivalent: "")
item.isEnabled = false
menu.addItem(item)
} else {
let convert = NSMenu()
for item in conversions {
guard case .format(let target) = item.payload else { continue }
let entry = NSMenuItem(title: item.title, action: #selector(Choice.fire), keyEquivalent: "")
entry.target = makeChoice {
JobStore.shared.submit(urls: urls, target: .format(target))
}
entry.isEnabled = item.isEnabled
if let reason = item.availability.reason { entry.toolTip = reason }
convert.addItem(entry)
}
if !convert.items.isEmpty {
let submenu = NSMenuItem(title: "Convert", action: nil, keyEquivalent: "")
submenu.submenu = convert
menu.addItem(submenu)
}
}
let tools = WheelBuilder.items(kind: .tools, urls: urls)
if category != .unknown, !tools.isEmpty {
let submenu = NSMenu()
for item in tools {
guard case .tool(let tool) = item.payload else { continue }
let entry = NSMenuItem(title: tool.title, action: #selector(Choice.fire), keyEquivalent: "")
entry.target = makeChoice {
if tool.fields.isEmpty {
JobStore.shared.submit(urls: urls, target: .tool(tool))
} else {
OptionsWindowController.shared.present(tool: tool, urls: urls, origin: point)
}
}
entry.isEnabled = item.isEnabled
if let reason = item.availability.reason { entry.toolTip = reason }
submenu.addItem(entry)
}
let wrapper = NSMenuItem(title: "Tools", action: nil, keyEquivalent: "")
wrapper.submenu = submenu
menu.addItem(wrapper)
}
let actions = NSMenu()
for item in WheelBuilder.items(kind: .actions, urls: urls) {
guard case .action(let action) = item.payload else { continue }
let entry = NSMenuItem(title: action.title, action: #selector(Choice.fire), keyEquivalent: "")
entry.target = makeChoice {
JobStore.shared.submit(urls: urls, target: .action(action))
}
actions.addItem(entry)
}
let actionsWrapper = NSMenuItem(title: "Actions", action: nil, keyEquivalent: "")
actionsWrapper.submenu = actions
menu.addItem(actionsWrapper)
menu.popUp(positioning: nil, at: point, in: nil)
retained.removeAll()
}
private static func makeChoice(_ handler: @escaping () -> Void) -> Choice {
let choice = Choice(handler)
retained.append(choice)
return choice
}
}