import AppKit import SwiftUI /// The form a tool needs before it can run (quality, trim points, password …). /// Values are remembered per tool, so the second time is still one drop. final class OptionsWindowController: NSObject { static let shared = OptionsWindowController() private var window: NSWindow? private var tool: ToolID? private var urls: [URL] = [] private var origin: CGPoint? private var params = ToolParams() private override init() { super.init() } func present(tool: ToolID, urls: [URL], origin: CGPoint?) { self.tool = tool self.urls = urls self.origin = origin params = Settings.shared.params(for: tool) params.fillMissing(from: tool) let view = OptionsView( tool: tool, params: params, fileCount: urls.count, onApply: { [weak self] values in self?.apply(values) }, onCancel: { [weak self] in self?.close() } ) close() let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 340, height: 120), styleMask: [.titled, .closable, .fullSizeContentView], backing: .buffered, defer: false ) window.title = tool.title window.titlebarAppearsTransparent = true window.isMovableByWindowBackground = true window.level = .floating window.contentView = NSHostingView(rootView: view) window.center() if let origin, let screen = NSScreen.screens.first(where: { $0.frame.contains(origin) }) ?? NSScreen.main { let frame = screen.visibleFrame window.setFrameOrigin(CGPoint( x: min(max(frame.minX + 20, origin.x - 170), frame.maxX - 360), y: min(max(frame.minY + 20, origin.y - 180), frame.maxY - 220) )) } NSApp.activate(ignoringOtherApps: true) window.makeKeyAndOrderFront(nil) self.window = window } private func apply(_ values: ToolParams) { guard let tool else { return } Settings.shared.store(params: values, for: tool) let captured = urls let point = origin close() JobStore.shared.submit(urls: captured, target: .tool(tool), params: values, origin: point) } private func close() { window?.orderOut(nil) window = nil } } struct OptionsView: View { let tool: ToolID let fileCount: Int let onApply: (ToolParams) -> Void let onCancel: () -> Void @State private var values: [String: String] = [:] init( tool: ToolID, params: ToolParams, fileCount: Int, onApply: @escaping (ToolParams) -> Void, onCancel: @escaping () -> Void ) { self.tool = tool self.fileCount = fileCount self.onApply = onApply self.onCancel = onCancel var initial: [String: String] = [:] for field in tool.fields { initial[field.id] = params[field.id] ?? field.defaultValue } _values = State(initialValue: initial) } var body: some View { VStack(alignment: .leading, spacing: 12) { HStack(spacing: 8) { Image(systemName: tool.symbol) .font(.system(size: 15, weight: .semibold)) .foregroundStyle(Color.accentColor) VStack(alignment: .leading, spacing: 1) { Text(tool.title) .font(.system(size: 13, weight: .semibold)) Text(fileCount == 1 ? "1 file" : "\(fileCount) files") .font(.system(size: 10.5)) .foregroundStyle(.secondary) } } ForEach(tool.fields) { field in fieldView(field) } HStack { Spacer() Button("Cancel", action: onCancel) .keyboardShortcut(.cancelAction) Button("Apply") { var params = ToolParams() for (key, value) in values { params[key] = value } onApply(params) } .keyboardShortcut(.defaultAction) } } .padding(16) .frame(width: 340) } @ViewBuilder private func fieldView(_ field: ToolField) -> some View { VStack(alignment: .leading, spacing: 3) { HStack { Text(field.label) .font(.system(size: 11, weight: .medium)) Spacer() if let unit = field.unit { Text(unit) .font(.system(size: 10)) .foregroundStyle(.secondary) } } control(for: field) if let help = field.help { Text(help) .font(.system(size: 9.5)) .foregroundStyle(.secondary) } } } @ViewBuilder private func control(for field: ToolField) -> some View { let binding = Binding( get: { values[field.id] ?? field.defaultValue }, set: { values[field.id] = $0 } ) switch field.kind { case .text: TextField("", text: binding) .textFieldStyle(.roundedBorder) case .secret: SecureField("", text: binding) .textFieldStyle(.roundedBorder) case .toggle: Toggle("", isOn: Binding( get: { (values[field.id] ?? field.defaultValue) == "true" }, set: { values[field.id] = $0 ? "true" : "false" } )) .labelsHidden() .toggleStyle(.switch) case .choice(let options): Picker("", selection: binding) { ForEach(options, id: \.self) { option in Text(option).tag(option) } } .labelsHidden() .pickerStyle(.menu) case .integer, .decimal, .timecode: TextField("", text: binding) .textFieldStyle(.roundedBorder) .font(.system(size: 12, design: .monospaced)) } } }