Files
ICCery-CPU/Sources/Controllers/PreviewWindowController.swift
gronod 474888541b
Build macOS Packages / Build macOS (Intel) (pull_request) Successful in 32s
Build macOS Packages / Build macOS (Apple Silicon) (pull_request) Successful in 37s
Build macOS Packages / Build macOS (Universal) (pull_request) Failing after 10s
feat: add hardware print quality option supporting Canon 5-notch Custom slider and Epson modes (Closes #5)
2026-09-07 23:37:01 +01:00

262 lines
9.1 KiB
Swift

import AppKit
import UniformTypeIdentifiers
/// SPEC §9 — three-pane window: thumbnails | canvas | inspector.
final class PreviewWindowController: NSWindowController {
let canvas = TargetCanvasView()
let sidebar = ThumbnailSidebarView()
let inspector = InspectorView()
let dropZone = DropZoneView()
let engine = PrintEngine()
private(set) var job: TargetJob?
private(set) var modeIsJob = false
var onPagesChanged: (() -> Void)?
convenience init(standalone: Bool) {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 1280, height: 800),
styleMask: [.titled, .closable, .miniaturizable, .resizable],
backing: .buffered,
defer: false
)
window.minSize = NSSize(width: 900, height: 560)
window.title = "TargetPrint — Untitled"
window.center()
self.init(window: window)
buildUI()
refreshPrinters()
}
private func buildUI() {
guard let window = window else { return }
let split = NSSplitView()
split.isVertical = true
split.dividerStyle = .thin
split.autosaveName = "TargetPrint.Split"
sidebar.frame = NSRect(x: 0, y: 0, width: 200, height: 800)
inspector.frame = NSRect(x: 0, y: 0, width: 280, height: 800)
let canvasHost = NSView(frame: NSRect(x: 0, y: 0, width: 800, height: 800))
canvas.frame = canvasHost.bounds
canvas.autoresizingMask = [.width, .height]
dropZone.frame = canvasHost.bounds
dropZone.autoresizingMask = [.width, .height]
canvasHost.addSubview(canvas)
canvasHost.addSubview(dropZone)
dropZone.isHidden = true
split.addSubview(sidebar)
split.addSubview(canvasHost)
split.addSubview(inspector)
split.setHoldingPriority(NSLayoutConstraint.Priority(250), forSubviewAt: 1)
window.contentView = split
window.setFrameAutosaveName("TargetPrint.Main")
DispatchQueue.main.async {
let w = split.bounds.width
if w > 500 {
split.setPosition(200, ofDividerAt: 0)
split.setPosition(w - 280, ofDividerAt: 1)
}
}
sidebar.onSelect = { [weak self] idx in
self?.canvas.selectedIndex = idx
self?.syncGeometry()
}
inspector.onChange = { [weak self] in
self?.applyInspectorToEngine()
self?.syncGeometry()
}
inspector.onPrint = { [weak self] in self?.presentPrint() }
dropZone.onFiles = { [weak self] urls in
self?.openURLs(urls)
}
canvas.registerForDraggedTypes([.fileURL])
updateDropZone()
}
func refreshPrinters() {
do {
let queues = try CUPSManager.listQueues()
inspector.loadQueues(queues, select: engine.printerName.isEmpty ? queues.first?.name : engine.printerName)
if engine.printerName.isEmpty, let first = queues.first {
engine.printerName = first.name
}
inspector.apply(engine: engine)
syncGeometry()
} catch {
TPLog.error("Failed to list CUPS queues: \(error)")
}
}
func loadJob(from url: URL) throws {
let parsed = try TargetJobParser.parse(url: url)
TPLog.info("Loaded job \(parsed.jobTitle) from \(url.path)")
// SPEC §13: do not delete the job file.
job = parsed
modeIsJob = true
engine.jobTitle = parsed.jobTitle
engine.printerName = parsed.printSettings.printerName
engine.mediaSize = parsed.printSettings.mediaSize
engine.mediaType = parsed.printSettings.mediaType
engine.paperSource = parsed.printSettings.paperSource
engine.printQuality = parsed.printSettings.printQuality
engine.resolution = parsed.printSettings.resolution
engine.scaling = parsed.printSettings.scaling
engine.centered = parsed.printSettings.centered
engine.colorMode = ColorMatchingMode.from(forceUnmanagedColor: parsed.printSettings.forceUnmanagedColor)
engine.lockColorManagement = parsed.uiPolicy.lockColorManagement
engine.allowBasicDriverChanges = parsed.uiPolicy.allowBasicDriverChanges
inspector.jobLocked = true
inspector.lockColorManagement = parsed.uiPolicy.lockColorManagement
inspector.allowBasicDriverChanges = parsed.uiPolicy.allowBasicDriverChanges
var pages: [TargetPage] = []
for path in parsed.files {
let fileURL = URL(fileURLWithPath: path)
do {
pages.append(contentsOf: try TargetPage.load(url: fileURL))
} catch {
throw AppError.unreadableTarget(path)
}
}
setPages(pages)
window?.title = "TargetPrint — \(parsed.jobTitle)"
refreshPrinters()
inspector.apply(engine: engine)
applyInspectorToEngine()
syncGeometry()
}
func openURLs(_ urls: [URL]) {
var jobURLs: [URL] = []
var tiffs: [URL] = []
for url in urls {
let ext = url.pathExtension.lowercased()
if ext == "targetjob" || ext == "json" { jobURLs.append(url) }
else { tiffs.append(url) }
}
if let jobURL = jobURLs.first {
do {
try loadJob(from: jobURL)
} catch {
PreviewWindowController.presentError(error, on: window)
}
return
}
var pages: [TargetPage] = []
for url in tiffs {
do {
pages.append(contentsOf: try TargetPage.load(url: url))
} catch {
PreviewWindowController.presentError(error, on: window)
return
}
}
if pages.isEmpty { return }
modeIsJob = false
job = nil
inspector.jobLocked = false
engine.jobTitle = pages.first.map { $0.name } ?? "Untitled"
window?.title = "TargetPrint — \(engine.jobTitle)"
setPages(canvas.pages + pages)
syncGeometry()
}
func setPages(_ pages: [TargetPage]) {
canvas.pages = pages
canvas.selectedIndex = 0
sidebar.pages = pages
sidebar.selectedIndex = 0
updateDropZone()
onPagesChanged?()
syncGeometry()
}
func presentPrint() {
applyInspectorToEngine()
guard !canvas.pages.isEmpty else { return }
guard let window = window else { return }
engine.run(view: canvas, window: window, presentPanel: true)
}
private func applyInspectorToEngine() {
inspector.push(into: engine)
canvas.centered = engine.centered
canvas.scaling = CGFloat(engine.scaling)
if let info = try? engine.makePrintInfo() {
canvas.printInfo = info
}
canvas.airPrintWarning = inspector.selectedQueue?.isAirPrint == true
? CUPSManager.airPrintWarning
: nil
inspector.applyLocks()
}
private func syncGeometry() {
if let page = canvas.currentPage {
inspector.showGeometry(canvas.geometry(for: page))
} else {
inspector.showGeometry(nil)
}
canvas.needsDisplay = true
}
private func updateDropZone() {
dropZone.isHidden = !canvas.pages.isEmpty
}
static func presentOpenPanel(completion: @escaping ([URL]) -> Void) {
let panel = NSOpenPanel()
panel.allowsMultipleSelection = true
panel.canChooseDirectories = false
if #available(macOS 12.0, *) {
var types: [UTType] = [.tiff, .json]
if let jobType = UTType(filenameExtension: "targetjob") {
types.append(jobType)
}
panel.allowedContentTypes = types
} else {
panel.allowedFileTypes = ["tif", "tiff", "targetjob", "json"]
}
panel.begin { result in
if result == .OK { completion(panel.urls) }
}
}
static func presentError(_ error: Error, on window: NSWindow?) {
let alert = NSAlert()
alert.alertStyle = .critical
alert.messageText = "TargetPrint"
alert.informativeText = (error as? AppError).map { $0.description } ?? error.localizedDescription
alert.addButton(withTitle: "OK")
TPLog.error(alert.informativeText)
if let window = window {
alert.beginSheetModal(for: window)
} else {
alert.runModal()
}
}
}
// Drag onto the canvas itself (not only the empty drop zone).
extension TargetCanvasView {
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
DropZoneView.containsSupportedFiles(sender) ? .copy : []
}
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
let urls = DropZoneView.urls(from: sender)
guard !urls.isEmpty else { return false }
NotificationCenter.default.post(name: .targetPrintOpenURLs, object: urls)
return true
}
}
extension Notification.Name {
static let targetPrintOpenURLs = Notification.Name("com.iccery.TargetPrint.openURLs")
}