Files
ICCery-CPU/Sources/Views/InspectorView.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

293 lines
12 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import AppKit
/// SPEC §9 — inspector: printer, media, tray, scale, colour-management mode.
/// When a job is loaded with locks, locked fields are read-only.
final class InspectorView: NSView {
var onChange: (() -> Void)?
var onPrint: (() -> Void)?
private(set) var queues: [PrinterQueue] = []
var selectedQueue: PrinterQueue? {
let i = printerPop.indexOfSelectedItem
guard i >= 0, i < queues.count else { return nil }
return queues[i]
}
var lockColorManagement = false
var allowBasicDriverChanges = true
var jobLocked = false
let printerPop = NSPopUpButton()
let mediaPop = NSPopUpButton()
let typePop = NSPopUpButton()
let trayPop = NSPopUpButton()
let qualityPop = NSPopUpButton()
let resolutionPop = NSPopUpButton()
let scaleSlider = NSSlider()
let scaleLabel = NSTextField(labelWithString: "100%")
let centeredCheck = NSButton(checkboxWithTitle: "Centre on page", target: nil, action: nil)
let landscapeCheck = NSButton(checkboxWithTitle: "Landscape", target: nil, action: nil)
let colorSeg = NSSegmentedControl(
labels: ["No Colour Management", "ColorSync", "Driver Managed"],
trackingMode: .selectOne,
target: nil,
action: nil
)
let airPrintBox = NSTextField(wrappingLabelWithString: "")
let printButton = NSButton(title: "Print…", target: nil, action: nil)
let geoLabel = NSTextField(wrappingLabelWithString: "")
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
wantsLayer = true
layer?.backgroundColor = NSColor.controlBackgroundColor.cgColor
printerPop.target = self; printerPop.action = #selector(changed)
mediaPop.target = self; mediaPop.action = #selector(changed)
typePop.target = self; typePop.action = #selector(changed)
trayPop.target = self; trayPop.action = #selector(changed)
qualityPop.target = self; qualityPop.action = #selector(changed)
resolutionPop.target = self; resolutionPop.action = #selector(changed)
scaleSlider.minValue = 0.5
scaleSlider.maxValue = 1.5
scaleSlider.doubleValue = 1.0
scaleSlider.target = self
scaleSlider.action = #selector(scaleChanged)
centeredCheck.target = self; centeredCheck.action = #selector(changed)
landscapeCheck.target = self; landscapeCheck.action = #selector(changed)
centeredCheck.state = .on
colorSeg.segmentStyle = .texturedRounded
colorSeg.target = self
colorSeg.action = #selector(changed)
colorSeg.selectedSegment = 0
airPrintBox.textColor = NSColor.systemOrange
airPrintBox.font = NSFont.systemFont(ofSize: 11)
airPrintBox.isHidden = true
printButton.bezelStyle = .rounded
printButton.keyEquivalent = "p"
printButton.keyEquivalentModifierMask = .command
printButton.target = self
printButton.action = #selector(printClicked)
geoLabel.font = NSFont.monospacedDigitSystemFont(ofSize: 11, weight: .regular)
geoLabel.textColor = NSColor.secondaryLabelColor
let stack = NSStackView(views: [
section("Printer", printerPop),
section("Media size", mediaPop),
section("Media type", typePop),
section("Tray", trayPop),
section("Print quality", qualityPop),
section("Resolution", resolutionPop),
section("Scale", scaleRow()),
centeredCheck,
landscapeCheck,
section("Colour mode", colorSeg),
airPrintBox,
geoLabel,
printButton,
])
stack.orientation = .vertical
stack.alignment = .leading
stack.spacing = 12
stack.translatesAutoresizingMaskIntoConstraints = false
addSubview(stack)
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 14),
stack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -14),
stack.topAnchor.constraint(equalTo: topAnchor, constant: 14),
printerPop.widthAnchor.constraint(equalTo: stack.widthAnchor),
mediaPop.widthAnchor.constraint(equalTo: stack.widthAnchor),
typePop.widthAnchor.constraint(equalTo: stack.widthAnchor),
trayPop.widthAnchor.constraint(equalTo: stack.widthAnchor),
qualityPop.widthAnchor.constraint(equalTo: stack.widthAnchor),
resolutionPop.widthAnchor.constraint(equalTo: stack.widthAnchor),
colorSeg.widthAnchor.constraint(equalTo: stack.widthAnchor),
])
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func loadQueues(_ queues: [PrinterQueue], select name: String?) {
self.queues = queues
printerPop.removeAllItems()
for q in queues { printerPop.addItem(withTitle: q.displayName) }
if let name = name, let idx = queues.firstIndex(where: { $0.name == name }) {
printerPop.selectItem(at: idx)
}
refreshDependent()
}
func apply(engine: PrintEngine) {
if let idx = queues.firstIndex(where: { $0.name == engine.printerName }) {
printerPop.selectItem(at: idx)
}
refreshDependent()
select(mediaPop, engine.mediaSize)
if let t = engine.mediaType { selectChoice(typePop, t) }
if let t = engine.paperSource { selectChoice(trayPop, t) }
if let q = engine.printQuality { selectChoice(qualityPop, q) }
if let r = engine.resolution { select(resolutionPop, r) }
scaleSlider.doubleValue = engine.scaling
scaleLabel.stringValue = String(format: "%.0f%%", engine.scaling * 100)
centeredCheck.state = engine.centered ? .on : .off
landscapeCheck.state = engine.landscape ? .on : .off
switch engine.colorMode {
case .unmanaged: colorSeg.selectedSegment = 0
case .colorsync: colorSeg.selectedSegment = 1
case .driver: colorSeg.selectedSegment = 2
}
applyLocks()
updateAirPrint()
}
func push(into engine: PrintEngine) {
if let q = selectedQueue { engine.printerName = q.name }
engine.mediaSize = mediaPop.titleOfSelectedItem ?? engine.mediaSize
engine.mediaType = (typePop.selectedItem?.representedObject as? String) ?? emptyToNil(typePop.titleOfSelectedItem)
engine.paperSource = (trayPop.selectedItem?.representedObject as? String) ?? emptyToNil(trayPop.titleOfSelectedItem)
engine.printQuality = (qualityPop.selectedItem?.representedObject as? String) ?? emptyToNil(qualityPop.titleOfSelectedItem)
engine.resolution = emptyToNil(resolutionPop.titleOfSelectedItem)
engine.scaling = scaleSlider.doubleValue
engine.centered = centeredCheck.state == .on
engine.landscape = landscapeCheck.state == .on
switch colorSeg.selectedSegment {
case 1: engine.colorMode = .colorsync
case 2: engine.colorMode = .driver
default: engine.colorMode = .unmanaged
}
}
func applyLocks() {
let mediaEnabled = allowBasicDriverChanges && !jobLocked
mediaPop.isEnabled = mediaEnabled
typePop.isEnabled = mediaEnabled
trayPop.isEnabled = mediaEnabled
qualityPop.isEnabled = mediaEnabled && (selectedQueue?.qualityChoices.isEmpty == false)
resolutionPop.isEnabled = !jobLocked
landscapeCheck.isEnabled = mediaEnabled
scaleSlider.isEnabled = !jobLocked
centeredCheck.isEnabled = !jobLocked
printerPop.isEnabled = !jobLocked
colorSeg.isEnabled = !lockColorManagement
}
func showGeometry(_ report: Geometry.Report?) {
guard let r = report else {
geoLabel.stringValue = "Drop TIFF targets or load a job."
return
}
let overflow = r.overflow ? " OVERFLOW" : ""
geoLabel.stringValue = String(
format: "Image %.3f × %.3f in\nDest %.1f × %.1f pt%@",
r.physicalWidthIn, r.physicalHeightIn, r.pointWidth, r.pointHeight, overflow
)
}
private func refreshDependent() {
let q = selectedQueue
refill(mediaPop, q?.mediaSizes.isEmpty == false ? q!.mediaSizes : Geometry.papers.map { $0.name })
let mediaChoices = (q?.mediaTypeChoices.isEmpty == false) ? q!.mediaTypeChoices : [PPDChoice(name: "Plain", title: "Plain")]
refillChoices(typePop, mediaChoices)
let trayChoices = (q?.trayChoices.isEmpty == false) ? q!.trayChoices : [PPDChoice(name: "Auto", title: "Auto")]
refillChoices(trayPop, trayChoices)
let qualityChoices = (q?.qualityChoices.isEmpty == false) ? q!.qualityChoices : [PPDChoice(name: "Auto", title: "Auto")]
refillChoices(qualityPop, qualityChoices)
qualityPop.isEnabled = allowBasicDriverChanges && !jobLocked && (q?.qualityChoices.isEmpty == false)
refill(resolutionPop, q?.resolutions ?? [])
updateAirPrint()
}
private func updateAirPrint() {
if selectedQueue?.isAirPrint == true {
airPrintBox.stringValue = CUPSManager.airPrintWarning
airPrintBox.isHidden = false
} else {
airPrintBox.isHidden = true
}
}
private func refill(_ pop: NSPopUpButton, _ items: [String]) {
let current = pop.titleOfSelectedItem
pop.removeAllItems()
items.forEach { pop.addItem(withTitle: $0) }
if let current = current { pop.selectItem(withTitle: current) }
}
private func refillChoices(_ pop: NSPopUpButton, _ choices: [PPDChoice]) {
let currentChoice = (pop.selectedItem?.representedObject as? String) ?? pop.titleOfSelectedItem
pop.removeAllItems()
for choice in choices {
pop.addItem(withTitle: choice.title)
pop.lastItem?.representedObject = choice.name
}
if let current = currentChoice,
choices.contains(where: { $0.name == current || $0.title == current }) {
selectChoice(pop, current)
}
}
private func selectChoice(_ pop: NSPopUpButton, _ value: String) {
if let item = pop.itemArray.first(where: { ($0.representedObject as? String) == value }) {
pop.select(item)
return
}
if let item = pop.itemArray.first(where: { $0.title == value }) {
pop.select(item)
return
}
pop.addItem(withTitle: value)
pop.lastItem?.representedObject = value
pop.selectItem(withTitle: value)
}
private func select(_ pop: NSPopUpButton, _ title: String) {
pop.selectItem(withTitle: title)
if pop.indexOfSelectedItem < 0 {
pop.addItem(withTitle: title)
pop.selectItem(withTitle: title)
}
}
private func emptyToNil(_ s: String?) -> String? {
guard let s = s, !s.isEmpty else { return nil }
return s
}
private func section(_ title: String, _ view: NSView) -> NSView {
let label = NSTextField(labelWithString: title)
label.font = NSFont.systemFont(ofSize: 11, weight: .medium)
label.textColor = NSColor.secondaryLabelColor
let stack = NSStackView(views: [label, view])
stack.orientation = .vertical
stack.alignment = .leading
stack.spacing = 4
return stack
}
private func scaleRow() -> NSView {
let row = NSStackView(views: [scaleSlider, scaleLabel])
row.orientation = .horizontal
row.spacing = 8
scaleSlider.translatesAutoresizingMaskIntoConstraints = false
scaleSlider.widthAnchor.constraint(greaterThanOrEqualToConstant: 120).isActive = true
return row
}
@objc private func scaleChanged() {
scaleLabel.stringValue = String(format: "%.0f%%", scaleSlider.doubleValue * 100)
changed()
}
@objc private func changed() {
refreshDependent()
onChange?()
}
@objc private func printClicked() { onPrint?() }
}