293 lines
12 KiB
Swift
293 lines
12 KiB
Swift
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?() }
|
||
}
|