208 lines
8.0 KiB
Swift
208 lines
8.0 KiB
Swift
import AppKit
|
|
|
|
/// SPEC §6.4 / §8 / §9 — document view used for both screen preview and printing.
|
|
/// Pagination via `knowsPageRange` / `rectForPage`. Each page is drawn in isolation.
|
|
|
|
final class TargetCanvasView: NSView {
|
|
var pages: [TargetPage] = [] {
|
|
didSet { needsDisplay = true }
|
|
}
|
|
var selectedIndex: Int = 0 {
|
|
didSet { needsDisplay = true }
|
|
}
|
|
var printInfo: NSPrintInfo = NSPrintInfo.shared
|
|
var centered: Bool = true {
|
|
didSet { needsDisplay = true }
|
|
}
|
|
var scaling: CGFloat = 1.0 {
|
|
didSet { needsDisplay = true }
|
|
}
|
|
var showImageable: Bool = false {
|
|
didSet { needsDisplay = true }
|
|
}
|
|
var airPrintWarning: String?
|
|
|
|
/// 1-based page index used by the print pipeline.
|
|
private var printingPage: Int = 1
|
|
|
|
override var isFlipped: Bool { true }
|
|
override var isOpaque: Bool { true }
|
|
override var acceptsFirstResponder: Bool { true }
|
|
|
|
var currentPage: TargetPage? {
|
|
guard !pages.isEmpty else { return nil }
|
|
let idx = min(max(selectedIndex, 0), pages.count - 1)
|
|
return pages[idx]
|
|
}
|
|
|
|
func currentPaper() -> Geometry.Paper {
|
|
let name = printInfo.paperName.map { $0.rawValue } ?? "A4"
|
|
var paper = Geometry.paper(named: name)
|
|
if printInfo.paperSize.width > 0 && printInfo.paperSize.height > 0 {
|
|
paper.widthPt = printInfo.paperSize.width
|
|
paper.heightPt = printInfo.paperSize.height
|
|
}
|
|
return paper
|
|
}
|
|
|
|
func imageableRect() -> CGRect {
|
|
let bounds = printInfo.imageablePageBounds
|
|
if bounds.width > 1 && bounds.height > 1 {
|
|
// `imageablePageBounds` is in the unflipped page coordinate system
|
|
// (origin bottom-left). Convert to our flipped view.
|
|
let paper = currentPaper()
|
|
return CGRect(
|
|
x: bounds.minX,
|
|
y: paper.heightPt - bounds.maxY,
|
|
width: bounds.width,
|
|
height: bounds.height
|
|
)
|
|
}
|
|
return Geometry.imageableBounds(paper: currentPaper())
|
|
}
|
|
|
|
func geometry(for page: TargetPage) -> Geometry.Report {
|
|
Geometry.report(
|
|
pixelWidth: CGFloat(page.pixelWidth),
|
|
pixelHeight: CGFloat(page.pixelHeight),
|
|
dpiX: page.dpiX,
|
|
dpiY: page.dpiY,
|
|
paper: currentPaper(),
|
|
imageable: imageableRect(),
|
|
centered: centered,
|
|
scaling: scaling
|
|
)
|
|
}
|
|
|
|
// MARK: - Pagination (SPEC §8)
|
|
|
|
override func knowsPageRange(_ range: NSRangePointer) -> Bool {
|
|
range.pointee = NSRange(location: 1, length: max(pages.count, 0))
|
|
return pages.count > 0
|
|
}
|
|
|
|
override func rectForPage(_ pageNumber: Int) -> NSRect {
|
|
printingPage = pageNumber
|
|
let size = printInfo.paperSize
|
|
return NSRect(origin: .zero, size: size)
|
|
}
|
|
|
|
override var pageHeader: NSAttributedString { NSAttributedString(string: "") }
|
|
override var pageFooter: NSAttributedString { NSAttributedString(string: "") }
|
|
|
|
// MARK: - Drawing
|
|
|
|
override func draw(_ dirtyRect: NSRect) {
|
|
guard let context = NSGraphicsContext.current?.cgContext else { return }
|
|
let printing = !(NSGraphicsContext.current?.isDrawingToScreen ?? true)
|
|
|
|
if printing {
|
|
drawPrintedPage(context: context)
|
|
} else {
|
|
drawPreview(context: context, dirty: dirtyRect)
|
|
}
|
|
}
|
|
|
|
private func drawPrintedPage(context: CGContext) {
|
|
// Reset so page N never inherits a transform from page N-1 (SPEC §8).
|
|
context.saveGState()
|
|
context.interpolationQuality = .none
|
|
context.setShouldAntialias(false)
|
|
context.setAllowsAntialiasing(false)
|
|
context.setShouldSmoothFonts(false)
|
|
|
|
let idx = printingPage - 1
|
|
guard idx >= 0, idx < pages.count else {
|
|
context.restoreGState()
|
|
return
|
|
}
|
|
let page = pages[idx]
|
|
let geo = geometry(for: page)
|
|
context.setFillColor(CGColor(gray: 1, alpha: 1))
|
|
context.fill(geo.paper)
|
|
context.draw(page.cgImage, in: geo.dest)
|
|
context.restoreGState()
|
|
}
|
|
|
|
private func drawPreview(context: CGContext, dirty: NSRect) {
|
|
context.setFillColor(NSColor(calibratedWhite: 0.07, alpha: 1).cgColor)
|
|
context.fill(bounds)
|
|
|
|
let paper = currentPaper()
|
|
let pad: CGFloat = 36
|
|
let fit = min(
|
|
max(1, (bounds.width - pad * 2) / max(paper.widthPt, 1)),
|
|
max(1, (bounds.height - pad * 2) / max(paper.heightPt, 1))
|
|
)
|
|
let paperCssW = paper.widthPt * fit
|
|
let paperCssH = paper.heightPt * fit
|
|
let ox = (bounds.width - paperCssW) / 2
|
|
let oy = (bounds.height - paperCssH) / 2
|
|
let paperRect = CGRect(x: ox, y: oy, width: paperCssW, height: paperCssH)
|
|
|
|
// Crop marks
|
|
context.setStrokeColor(NSColor(calibratedWhite: 0.83, alpha: 0.35).cgColor)
|
|
context.setLineWidth(1)
|
|
let mark: CGFloat = 10
|
|
context.beginPath()
|
|
context.move(to: CGPoint(x: ox - 12, y: oy)); context.addLine(to: CGPoint(x: ox - 12 + mark, y: oy))
|
|
context.move(to: CGPoint(x: ox, y: oy - 12)); context.addLine(to: CGPoint(x: ox, y: oy - 12 + mark))
|
|
context.move(to: CGPoint(x: ox + paperCssW + 12, y: oy)); context.addLine(to: CGPoint(x: ox + paperCssW + 12 - mark, y: oy))
|
|
context.move(to: CGPoint(x: ox + paperCssW, y: oy - 12)); context.addLine(to: CGPoint(x: ox + paperCssW, y: oy - 12 + mark))
|
|
context.move(to: CGPoint(x: ox - 12, y: oy + paperCssH)); context.addLine(to: CGPoint(x: ox - 12 + mark, y: oy + paperCssH))
|
|
context.move(to: CGPoint(x: ox, y: oy + paperCssH + 12)); context.addLine(to: CGPoint(x: ox, y: oy + paperCssH + 12 - mark))
|
|
context.move(to: CGPoint(x: ox + paperCssW + 12, y: oy + paperCssH)); context.addLine(to: CGPoint(x: ox + paperCssW + 12 - mark, y: oy + paperCssH))
|
|
context.move(to: CGPoint(x: ox + paperCssW, y: oy + paperCssH + 12)); context.addLine(to: CGPoint(x: ox + paperCssW, y: oy + paperCssH + 12 - mark))
|
|
context.strokePath()
|
|
|
|
context.setFillColor(CGColor(gray: 1, alpha: 1))
|
|
context.fill(paperRect)
|
|
|
|
if let page = currentPage {
|
|
let geo = geometry(for: page)
|
|
let dest = CGRect(
|
|
x: ox + geo.dest.minX * fit,
|
|
y: oy + geo.dest.minY * fit,
|
|
width: geo.dest.width * fit,
|
|
height: geo.dest.height * fit
|
|
)
|
|
context.interpolationQuality = .none
|
|
context.setShouldAntialias(false)
|
|
context.setAllowsAntialiasing(false)
|
|
context.draw(page.cgImage, in: dest)
|
|
|
|
if showImageable {
|
|
let img = geo.imageable
|
|
context.setStrokeColor(NSColor.systemBlue.withAlphaComponent(0.55).cgColor)
|
|
context.setLineDash(phase: 0, lengths: [4, 4])
|
|
context.stroke(CGRect(
|
|
x: ox + img.minX * fit,
|
|
y: oy + img.minY * fit,
|
|
width: img.width * fit,
|
|
height: img.height * fit
|
|
))
|
|
context.setLineDash(phase: 0, lengths: [])
|
|
context.stroke(dest)
|
|
}
|
|
}
|
|
|
|
if let warning = airPrintWarning, !warning.isEmpty {
|
|
drawWarningBanner(context: context, text: warning)
|
|
}
|
|
}
|
|
|
|
private func drawWarningBanner(context: CGContext, text: String) {
|
|
let banner = CGRect(x: 16, y: 16, width: bounds.width - 32, height: 52)
|
|
context.setFillColor(NSColor.systemOrange.withAlphaComponent(0.18).cgColor)
|
|
context.fill(banner)
|
|
context.setStrokeColor(NSColor.systemOrange.cgColor)
|
|
context.stroke(banner)
|
|
let attrs: [NSAttributedString.Key: Any] = [
|
|
.font: NSFont.systemFont(ofSize: 12, weight: .medium),
|
|
.foregroundColor: NSColor.labelColor,
|
|
]
|
|
let str = NSAttributedString(string: "Warning: " + text, attributes: attrs)
|
|
str.draw(in: banner.insetBy(dx: 10, dy: 8))
|
|
}
|
|
}
|