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]) if ConversionCatalog.outputs(for: urls.map { FileKind.category(of: $0) }).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 WheelBuilder.items(kind: .convert, urls: urls) { let entry = NSMenuItem(title: item.title, action: #selector(Choice.fire), keyEquivalent: "") entry.target = makeChoice { JobStore.shared.submit(urls: urls, target: .format(format(from: item))) } entry.isEnabled = item.isEnabled if let reason = item.availability.reason { entry.toolTip = reason } convert.addItem(entry) } 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 } private static func format(from item: WheelItem) -> Format { if case .format(let format) = item.payload { return format } return .png } }