Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d056c26bf | ||
|
|
c4f037535a | ||
|
|
191bcb0280 | ||
|
|
9c625ee21f | ||
|
|
40cc4ad701 | ||
|
|
658d9813bd | ||
|
|
368ae56cf3 | ||
|
|
bd539df009 | ||
|
|
1b91b0a94e | ||
|
|
890e7281eb |
@@ -220,6 +220,15 @@ struct ManageMediaDialog: View {
|
||||
.accessibilityIdentifier("mediaLibraryList")
|
||||
.frame(minHeight: 260)
|
||||
|
||||
if let notice = media.manageApplyNotice {
|
||||
Text(notice)
|
||||
.font(.callout)
|
||||
.foregroundStyle(.orange)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.accessibilityIdentifier("manageMediaNotice")
|
||||
.accessibilityValue(notice)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Button("Apply selected") {
|
||||
if let id = selection,
|
||||
|
||||
@@ -41,6 +41,9 @@ final class MediaLibraryViewModel: ObservableObject {
|
||||
@Published var saveMediaApplyCal = false
|
||||
/// Inline caption inside the capture sheet (no a11y id — roster complete).
|
||||
@Published var saveMediaError: String?
|
||||
/// Last failed Apply while Manage is open. The window banner sits
|
||||
/// behind the sheet on Monterey, so the dialog shows this too (#170).
|
||||
@Published var manageApplyNotice: String?
|
||||
|
||||
/// Pure flow flag — the manage sheet's "Capture current…" asks the
|
||||
/// sheet's `onDismiss` to open the capture sheet, avoiding a
|
||||
@@ -120,24 +123,21 @@ final class MediaLibraryViewModel: ObservableObject {
|
||||
/// with warning; the refusal is permanent so re-clicking can't help).
|
||||
@discardableResult
|
||||
func apply(_ recipe: MediaRecipe) async -> Bool {
|
||||
manageApplyNotice = nil
|
||||
guard let r = try? recipe.validated() else {
|
||||
workflow.wizard.showNotice(
|
||||
"Media recipe is invalid — not applied.", kind: .error)
|
||||
return false
|
||||
return failApply("Media recipe is invalid — not applied.", kind: .error)
|
||||
}
|
||||
guard let preset = environment.presetStore.all()
|
||||
.first(where: { $0.id == r.presetID })
|
||||
else {
|
||||
workflow.wizard.showNotice(
|
||||
return failApply(
|
||||
"Preset \(r.presetID) no longer exists — recipe not applied.",
|
||||
kind: .error)
|
||||
return false
|
||||
}
|
||||
guard preset.colourSpace.lowercased() == r.colourSpace.lowercased() else {
|
||||
workflow.wizard.showNotice(
|
||||
return failApply(
|
||||
"Recipe colour space does not match its preset — not applied.",
|
||||
kind: .error)
|
||||
return false
|
||||
}
|
||||
|
||||
// Existing #82 mapping: presetSelect jumps, Stage 1/2/4 fields.
|
||||
@@ -145,8 +145,6 @@ final class MediaLibraryViewModel: ObservableObject {
|
||||
// Literal per issue: displayName, not the queue id.
|
||||
workflow.wizard.printerName = r.printerDisplayName
|
||||
|
||||
var succeeded = true
|
||||
|
||||
// Queue: enumerate fresh via the session's serialized path —
|
||||
// listPrinters uses fixed process ids, so an overlapping
|
||||
// enumeration would throw duplicateID. An empty result is a
|
||||
@@ -156,16 +154,14 @@ final class MediaLibraryViewModel: ObservableObject {
|
||||
workflow.print.selectedPrinter = r.printerID
|
||||
await workflow.print.reloadSelectedCapabilities()
|
||||
} else {
|
||||
workflow.wizard.showNotice(
|
||||
return failApply(
|
||||
"Printer \(r.printerDisplayName) is not installed.",
|
||||
kind: .warning)
|
||||
succeeded = false
|
||||
}
|
||||
} else {
|
||||
workflow.wizard.showNotice(
|
||||
return failApply(
|
||||
"Could not enumerate printers — queue left unchanged.",
|
||||
kind: .warning)
|
||||
succeeded = false
|
||||
}
|
||||
|
||||
// Calibration — the recipe is authoritative and runs after
|
||||
@@ -195,10 +191,8 @@ final class MediaLibraryViewModel: ObservableObject {
|
||||
guard FileManager.default.fileExists(atPath: calPath) else {
|
||||
workflow.profile.applyCalibration = false
|
||||
workflow.profile.calibrationFile = calPath
|
||||
workflow.wizard.showNotice(
|
||||
return failApply(
|
||||
"Calibration file is missing: \(calPath)", kind: .error)
|
||||
refreshStaleness()
|
||||
return false
|
||||
}
|
||||
do {
|
||||
let staleDays = environment.settingsStore.load().calibrationStaleDays
|
||||
@@ -215,23 +209,26 @@ final class MediaLibraryViewModel: ObservableObject {
|
||||
}
|
||||
} catch {
|
||||
workflow.profile.applyCalibration = false
|
||||
workflow.wizard.showNotice(
|
||||
return failApply(
|
||||
"Could not load calibration: \(error.localizedDescription)",
|
||||
kind: .error)
|
||||
refreshStaleness()
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
workflow.profile.applyCalibration = false
|
||||
workflow.profile.calibrationFile = calPath
|
||||
}
|
||||
|
||||
if succeeded {
|
||||
selectedRecipeID = r.id
|
||||
workflow.wizard.showNotice("Applied \(r.name)")
|
||||
}
|
||||
selectedRecipeID = r.id
|
||||
workflow.wizard.showNotice("Applied \(r.name)")
|
||||
refreshStaleness()
|
||||
return succeeded
|
||||
return true
|
||||
}
|
||||
|
||||
private func failApply(_ text: String, kind: Notice.Kind) -> Bool {
|
||||
manageApplyNotice = text
|
||||
workflow.wizard.showNotice(text, kind: kind)
|
||||
refreshStaleness()
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: - Capture
|
||||
|
||||
@@ -148,7 +148,9 @@ final class Milestone10MediaLibraryUITests: XCTestCase {
|
||||
XCTAssertTrue(apply.waitForExistence(timeout: 10))
|
||||
apply.click()
|
||||
|
||||
let notice = waitFor("noticeText")
|
||||
// The window banner (`noticeText`) sits behind this sheet on
|
||||
// Monterey (#170). Assert the in-sheet copy instead.
|
||||
let notice = waitFor("manageMediaNotice", timeout: 15)
|
||||
let text = (notice.value as? String) ?? notice.label
|
||||
XCTAssertTrue(
|
||||
text.contains("is not installed"),
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
@startuml ICCery-UI-sheets
|
||||
title Sheets Settings, presets, media, Spot Read, Gamut, project
|
||||
!theme plain
|
||||
skinparam backgroundColor white
|
||||
skinparam defaultFontColor black
|
||||
skinparam defaultFontSize 13
|
||||
skinparam mapBackgroundColor white
|
||||
skinparam mapBorderColor #222222
|
||||
skinparam mapFontColor black
|
||||
skinparam arrowColor #222222
|
||||
skinparam shadowing false
|
||||
hide circle
|
||||
top to bottom direction
|
||||
|
||||
map "SettingsView.swift" as set {
|
||||
Argyll dir => TextField+Browse : empty = bundled
|
||||
Default instrument => Picker : seeds Stage 3 and Spot Read
|
||||
Enable i1Pro 2 LEDs => Toggle
|
||||
settingsDeltaEGood => TextField
|
||||
settingsDeltaEWarning => TextField : must be greater than Good
|
||||
settingsCalStaleDays => TextField
|
||||
Install location => Picker User or System
|
||||
Ask before overwriting => Toggle
|
||||
Open ColorSync after install => Toggle
|
||||
Log level => Picker
|
||||
Open log folder => Button
|
||||
Cancel / Save => Save stays if validation fails
|
||||
}
|
||||
|
||||
map "Presets PresetDialogs.swift" as pre {
|
||||
savePresetName => TextField
|
||||
savePresetDesc => TextField
|
||||
btnCloseSavePresetDialog => Button
|
||||
btnConfirmSavePreset => Button : disabled if name empty
|
||||
presetRow-id => Row : built-in cannot delete
|
||||
btnImportPreset => Button
|
||||
btnExportActivePreset => Button : custom selected
|
||||
btnCloseManagePresetsDialog => Button
|
||||
}
|
||||
|
||||
map "Media MediaLibraryDialogs.swift" as media {
|
||||
saveMediaName/Paper/Ink => TextField
|
||||
saveMediaApplyCal => Toggle : disabled if CAL_ or missing file
|
||||
btnConfirmSaveMedia => Button : name+paper+ink required
|
||||
mediaLibraryList => List
|
||||
btnMediaLibraryApply-id => Button per row
|
||||
manageMediaNotice => Caption : failed Apply in-sheet
|
||||
btnMediaLibraryApply => Button : disabled if no selection
|
||||
btnCloseManageMediaDialog => Button
|
||||
}
|
||||
|
||||
map "spotReadView SpotReadView.swift" as spot {
|
||||
btnSpotDetectInstruments => Button
|
||||
spotInstrumentSelect => Picker : disabled while running
|
||||
spotSetDefault => Toggle
|
||||
btnSpotStart => Button : needs sidecar, cwd, not chartread
|
||||
btnSpotCalibrate => Button : calibrating
|
||||
btnSpotTrigger => Button : Read, awaitingStrip
|
||||
btnSpotStop => Button : while running
|
||||
btnSpotCopyLab => Button : disabled if no sample
|
||||
btnSpotExportCsv => Button : disabled if no history
|
||||
btnCloseSpotRead => Button Esc : dismiss = Stop
|
||||
}
|
||||
|
||||
map "gamutView GamutView.swift" as gam {
|
||||
gamutLayer-srgb => Toggle : can hide, cannot remove
|
||||
gamutLayer-profile => Toggle : disabled if no .gam
|
||||
gamutLayer-compare => Toggle : disabled if no compare
|
||||
btnGamutAddCompare => Menu
|
||||
btnGamutRemoveCompare => Button
|
||||
btnGamutSampleTiff => Button
|
||||
btnResetGamutCamera => Button R
|
||||
gamutLabEntryL/A/B => TextField
|
||||
btnGamutInspectLab => Button
|
||||
btnCloseGamut => Button Esc
|
||||
}
|
||||
|
||||
map "Project alerts" as proj {
|
||||
projectNewAlert => Alert : New Cancel / Confirm
|
||||
Dirty save => Alert : Save / Don't Save / Cancel
|
||||
projectRelocateSheet => Sheet : cwd missing on Open
|
||||
}
|
||||
|
||||
set -[hidden]down- pre
|
||||
pre -[hidden]down- media
|
||||
media -[hidden]down- spot
|
||||
spot -[hidden]down- gam
|
||||
gam -[hidden]down- proj
|
||||
@enduml
|
||||
@@ -0,0 +1,81 @@
|
||||
@startuml ICCery-UI-shell
|
||||
title Shell, sidebar, stepper, File menu
|
||||
!theme plain
|
||||
skinparam backgroundColor white
|
||||
skinparam defaultFontColor black
|
||||
skinparam defaultFontSize 13
|
||||
skinparam mapBackgroundColor white
|
||||
skinparam mapBorderColor #222222
|
||||
skinparam mapFontColor black
|
||||
skinparam arrowColor #222222
|
||||
skinparam shadowing false
|
||||
hide circle
|
||||
top to bottom direction
|
||||
|
||||
map "Window" as win {
|
||||
WindowGroup => ICCeryApp.swift : 1280x800 min 1100x700
|
||||
RootView => RootView.swift : hosts every sheet
|
||||
noticeText => NoticeBanner : shown if wizard.notice set; auto-hide 6s
|
||||
}
|
||||
|
||||
map "Sidebar header" as hdr {
|
||||
openSettingsBtn => Button : always / Settings sheet
|
||||
openAboutBtn => Button : always / About
|
||||
btnToggleAllHelp => Button : toggles yellow help dots
|
||||
}
|
||||
|
||||
map "Presets" as pre {
|
||||
presetSelect => Picker.menu : none + preset id; none is not factory reset
|
||||
btnSavePresetModal => Button : always
|
||||
btnOpenPresetsDialog => Button : always
|
||||
}
|
||||
|
||||
map "Media library" as med {
|
||||
mediaSelect => Picker.menu : never reuses presetSelect
|
||||
mediaRecipeStale => Caption : shown if staleReasons nonempty
|
||||
btnMediaLibraryCapture => Button : disabled if no Stage 2 printer
|
||||
btnMediaLibraryManage => Button : always
|
||||
}
|
||||
|
||||
map "Studio (not stepper)" as stu {
|
||||
btnCalibratePrinter => Button : always; Stage 0
|
||||
btnViewGamut => Button : sidebar always on; same id as Stage 5
|
||||
btnSpotRead => Button : disabled if no cwd or chartread running
|
||||
}
|
||||
|
||||
map "Stepper 1-5 disk is truth" as stp {
|
||||
Stage1 Generate => always
|
||||
Stage2 Lay out => .ti1
|
||||
Stage3 Measure => .ti1 AND .ti2
|
||||
Stage4 Build => .ti3 (not .ti2 alone)
|
||||
Stage5 Verify => .ti3 AND .icc/.icm
|
||||
Calibrate => not a stepper row; always available
|
||||
}
|
||||
|
||||
map "Project chip" as chip {
|
||||
projectChipName => Text : bound name or No project
|
||||
projectChipPath => Text : shown if bound
|
||||
projectChipStale => Caption : diskBehindNotes
|
||||
btnProjectReveal => Button : shown if bound
|
||||
btnProjectSave => Button : canSave = bound + basename + cwd
|
||||
btnProjectOpen => Button : shown if not bound
|
||||
}
|
||||
|
||||
map "File menu" as menu {
|
||||
menuProjectNew => Button Cmd-N
|
||||
menuProjectOpen => Button Cmd-O
|
||||
menuProjectRecents => Menu : disabled if recents empty
|
||||
menuProjectSave => Button Cmd-S : !canSave
|
||||
menuProjectSaveAs => Button Shift-Cmd-S : basename + cwd
|
||||
menuProjectReport => Button : !canReport
|
||||
menuProjectClose => Button : !isBound
|
||||
}
|
||||
|
||||
win -[hidden]down- hdr
|
||||
hdr -[hidden]down- pre
|
||||
pre -[hidden]down- med
|
||||
med -[hidden]down- stu
|
||||
stu -[hidden]down- stp
|
||||
stp -[hidden]down- chip
|
||||
chip -[hidden]down- menu
|
||||
@enduml
|
||||
@@ -0,0 +1,83 @@
|
||||
@startuml ICCery-UI-stage1-2
|
||||
title Stages 1-2 Generate Target / Lay Out and Print
|
||||
!theme plain
|
||||
skinparam backgroundColor white
|
||||
skinparam defaultFontColor black
|
||||
skinparam defaultFontSize 13
|
||||
skinparam mapBackgroundColor white
|
||||
skinparam mapBorderColor #222222
|
||||
skinparam mapFontColor black
|
||||
skinparam arrowColor #222222
|
||||
skinparam shadowing false
|
||||
hide circle
|
||||
top to bottom direction
|
||||
|
||||
map "stage-1 Stage1View.swift" as s1 {
|
||||
colourSpace => Picker.segmented : RGB or CMYK
|
||||
patchCountPreset => Picker
|
||||
patchCountCustom => TextField : shown if preset is custom
|
||||
whitePatches => Stepper 0-50
|
||||
blackPatches => Stepper 0-50
|
||||
targetBasename => TextField
|
||||
btnBrowse => Button
|
||||
btnSelectWorkDir => Button
|
||||
btnOpenExisting => Button : .ti1 or .ti2
|
||||
btn-import-dataset => Button : unlocks stage 4
|
||||
selectedPathDisplay => Text
|
||||
targenAdvancedDetails => DisclosureGroup : UI tests pre-expand
|
||||
btnGenerate => Button : needs basename AND directory
|
||||
targenLog => ProcessLogView
|
||||
}
|
||||
|
||||
map "Stage 1 Advanced (inside disclosure)" as adv {
|
||||
targenGreySteps => Toggle+field : field iff on
|
||||
targenSingleChannelSteps => Toggle+field
|
||||
targenNeutralSteps => Toggle+field
|
||||
targenNeutralConcentration => Toggle+Slider 0-1
|
||||
targenAdaptation => Toggle+Slider 0-1
|
||||
targenPrecondProfile => TextField
|
||||
btnBrowsePrecondProfile => Button
|
||||
targenHighQuality => Toggle
|
||||
targenAlgorithm => Picker
|
||||
targenInkLimitGroup => Toggle+field : CMYK only
|
||||
targenDarkEmphasis => Toggle+Slider 0-3
|
||||
targenDevicePower => Toggle+Slider 0-3
|
||||
}
|
||||
|
||||
map "stage-2 Stage2View.swift" as s2 {
|
||||
cmWarningBanner => Banner : always on stage 2
|
||||
instrumentSelect => Picker : chart code, not USB port
|
||||
pageSizeSelect => Picker
|
||||
customPageW / customPageH => TextField : pageSize custom
|
||||
tiffDpi => Stepper 72-600
|
||||
printtargLayoutOrder => Picker
|
||||
printtargCustomSeed => TextField : custom seed order
|
||||
btnToggleLabelEdit => Button
|
||||
targetMetadataPrinter => TextField
|
||||
targetMetadataInkSet => TextField
|
||||
targetMetadataDriverPaper => TextField
|
||||
targetMetadataActualPaper => TextField
|
||||
targetLabelPreview => TextField or Text
|
||||
btnCreateLayout => Button : disabled if no basename
|
||||
tiffGallery => Grid : after printtarg
|
||||
btnPrintPage-N => Button : disabled if no printer
|
||||
}
|
||||
|
||||
map "rawPrintPanel" as prn {
|
||||
printerSelect => Picker CUPS
|
||||
printerStatusBadge => Caption
|
||||
btnRefreshPrinters => Button
|
||||
btnPrinterProperties => Button : disabled if no printer
|
||||
printerTraySelect => Picker : if trays exist
|
||||
printerMediaTypeSelect => Picker : if media types exist
|
||||
btnOrientPortrait => Button
|
||||
btnOrientLandscape => Button
|
||||
btnPrintAll => Button : needs layout AND printer
|
||||
btnAdvanceToStage3 => Button : needs .ti2 unlock
|
||||
printNotificationText => Caption : if printNotice set
|
||||
}
|
||||
|
||||
s1 -[hidden]down- adv
|
||||
adv -[hidden]down- s2
|
||||
s2 -[hidden]down- prn
|
||||
@enduml
|
||||
@@ -0,0 +1,84 @@
|
||||
@startuml ICCery-UI-stage3-5-cal
|
||||
title Stages 3-5 and Calibrate Printer
|
||||
!theme plain
|
||||
skinparam backgroundColor white
|
||||
skinparam defaultFontColor black
|
||||
skinparam defaultFontSize 13
|
||||
skinparam mapBackgroundColor white
|
||||
skinparam mapBorderColor #222222
|
||||
skinparam mapFontColor black
|
||||
skinparam arrowColor #222222
|
||||
skinparam shadowing false
|
||||
hide circle
|
||||
top to bottom direction
|
||||
|
||||
map "stage-3 Stage3View.swift" as s3 {
|
||||
btnDetectInstruments => Button : disabled while detecting
|
||||
chartreadInstrumentSelect => Picker.menu
|
||||
xyTableHint => Caption : if XY instrument
|
||||
xyTablePanel => Place/Align/Scan/Remove
|
||||
chartreadPrompt => Text
|
||||
chartreadLastError => Caption : if notice set
|
||||
btnStartRead => Button : shown if not running; needs basename+cwd
|
||||
btnCalibrate => Button : running and calibrating
|
||||
btnTrigger => Button : running and awaitingStrip
|
||||
btnDoneReadEarly => Button : awaitingStrip
|
||||
btnAccept => Button : place / align / continue / warning
|
||||
btnRetry => Button : error state
|
||||
btnDoneRead => Button : allStripsRead
|
||||
btnCancel => Button : while running
|
||||
swatchGrid => swatch-rowId-loc
|
||||
btnMeasureAnotherSheet => Button : after a finished pass
|
||||
btnFinishAndAverage => Button : finished AND at least one pass
|
||||
}
|
||||
|
||||
map "stage-4 Stage4View.swift" as s4 {
|
||||
colprofAlgorithm => Picker
|
||||
colprofQuality => Picker
|
||||
colprofFwa => Picker
|
||||
colprofFwaCustomPath => TextField : custom spectrum
|
||||
btnBrowseFwaSp => Button
|
||||
colprofIlluminant => TextField
|
||||
colprofObserver => TextField
|
||||
colprofInputViewCond => TextField
|
||||
colprofOutputViewCond => TextField
|
||||
colprofDescription => TextField
|
||||
colprofCopyright => TextField
|
||||
colprofApplyCalibration => Toggle
|
||||
colprofCalibrationFile => TextField : if applyCalibration
|
||||
btnBrowseCalibrationFile => Button : if applyCalibration
|
||||
btnCreateProfile => Button : basename + cwd, not running
|
||||
}
|
||||
|
||||
map "stage-5 Stage5View.swift" as s5 {
|
||||
btnVerifyProfile => Button : needs created profile
|
||||
btnViewGamut => Button : disabled if no .gam (same id as sidebar)
|
||||
btnInstallProfile => Button : needs created profile
|
||||
driftPrinterFilter => Picker
|
||||
btnExportHistory => Button
|
||||
btnClearHistory => Button
|
||||
verificationHistoryTable => Table
|
||||
driftChart => not interactive
|
||||
profileOverwriteBtn => Alert : install collision
|
||||
profileRenameBtn => Alert
|
||||
profileCancelCollisionBtn => Alert
|
||||
}
|
||||
|
||||
map "stage-cal CalibrationView.swift" as cal {
|
||||
Colour space => Picker.segmented : no a11y id
|
||||
calSteps => TextField
|
||||
White patches => TextField : no a11y id
|
||||
calInkExplore => TextField : CMYK only
|
||||
calNeutralEmphasis => Toggle
|
||||
btnCalGenerate => Button : needs basename + cwd
|
||||
btnCalLayout => Button : same
|
||||
btnCalMeasure => Button : needs .ti3
|
||||
btnCalCompute => Button : .ti3 and not computing
|
||||
calApplyToggle => Toggle : after curves exist
|
||||
btnCalReturn => Button Esc : restores original basename
|
||||
}
|
||||
|
||||
s3 -[hidden]down- s4
|
||||
s4 -[hidden]down- s5
|
||||
s5 -[hidden]down- cal
|
||||
@enduml
|
||||
@@ -0,0 +1,12 @@
|
||||
# Interactive UI map
|
||||
|
||||
PlantUML **maps** (not class diagrams). Class diagrams laid out 6500px wide, so Gitea on a phone showed a sliver of a black arrow and the UML “C” icon.
|
||||
|
||||
Each file is one stacked column (~560–780px) that Kroki actually fits in the page.
|
||||
|
||||
| Diagram | File | Kroki size |
|
||||
| --- | --- | --- |
|
||||
| Shell, sidebar, stepper, File menu | [ui-interactive-map-shell.puml](ui-interactive-map-shell.puml) | 563 × 1367 |
|
||||
| Stages 1–2 | [ui-interactive-map-stage1-2.puml](ui-interactive-map-stage1-2.puml) | 625 × 1366 |
|
||||
| Stages 3–5 + Calibrate | [ui-interactive-map-stage3-5-cal.puml](ui-interactive-map-stage3-5-cal.puml) | 568 × 1386 |
|
||||
| Sheets | [ui-interactive-map-sheets.puml](ui-interactive-map-sheets.puml) | 782 × 1502 |
|
||||
Reference in New Issue
Block a user