diff --git a/Sources/ICCery/SettingsView.swift b/Sources/ICCery/SettingsView.swift index 6625f68..6f5295c 100644 --- a/Sources/ICCery/SettingsView.swift +++ b/Sources/ICCery/SettingsView.swift @@ -69,22 +69,24 @@ struct SettingsView: View { } Section("Verification") { - HStack { - Text("Good ΔE ≤") - TextField( - "2.0", - value: $model.settings.deltaEGoodMax, - format: .number - ) - .frame(width: 60) - Text("Warning ΔE ≤") - TextField( - "5.0", - value: $model.settings.deltaEWarningMax, - format: .number - ) - .frame(width: 60) - } + TextField( + "Good ΔE ≤", + value: $model.settings.deltaEGoodMax, + format: .number + ) + .accessibilityIdentifier("settingsDeltaEGood") + + TextField( + "Warning ΔE ≤", + value: $model.settings.deltaEWarningMax, + format: .number + ) + .accessibilityIdentifier("settingsDeltaEWarning") + + Text("Swatch and verify status use these as the green / amber cutoffs. Fail is anything above Warning.") + .font(.caption) + .foregroundStyle(.secondary) + ForEach(model.validationErrors, id: \.self) { error in Text(error) .font(.caption) @@ -93,16 +95,12 @@ struct SettingsView: View { } Section("Calibration") { - HStack { - Text("Stale after") - TextField( - "30", - value: $model.settings.calibrationStaleDays, - format: .number - ) - .frame(width: 60) - Text("days") - } + TextField( + "Stale after (days)", + value: $model.settings.calibrationStaleDays, + format: .number + ) + .accessibilityIdentifier("settingsCalStaleDays") } Section("Profile install") { @@ -143,6 +141,7 @@ struct SettingsView: View { } } } + .padding(.leading, 45) Divider() diff --git a/Tests/ICCeryUITests/SettingsUITests.swift b/Tests/ICCeryUITests/SettingsUITests.swift new file mode 100644 index 0000000..3aa918f --- /dev/null +++ b/Tests/ICCeryUITests/SettingsUITests.swift @@ -0,0 +1,192 @@ +import Foundation +import XCTest + +/// Settings sheet UI tests (issue #165). +/// +/// The Verification thresholds once shared one non-wrapping `HStack` and +/// drew past the sheet's right clip on the macOS grouped `Form`. AX +/// existence cannot see clipping (#163), so containment is asserted on +/// real frame geometry against the sheet's bounds. +/// +/// All three numeric fields also passed their default value as the +/// `TextField` label; inside an `HStack` row that label renders inline — +/// it is not a placeholder — producing "Stale after 30 [30] days". The +/// fields are now direct `Form` children, so the descriptive label +/// renders once in the label column and the box fills the control +/// column; `testNumericFieldsCarryLabelsNotDuplicatedValues` pins it. +@MainActor +final class SettingsUITests: XCTestCase { + + private var app: XCUIApplication! + + override func setUp() async throws { + continueAfterFailure = false + app = XCUIApplication() + app.launchEnvironment = ["ICCERY_UI_TESTING": "1"] + app.launch() + app.activate() + } + + override func tearDown() async throws { + app?.terminate() + app = nil + } + + /// Sheet content lives under `app.sheets`, outside the main window's + /// a11y tree (Milestone2 pattern). + private var sheet: XCUIElement { + app.sheets.firstMatch + } + + private func openSettings() { + let gear = app.buttons["openSettingsBtn"] + XCTAssertTrue(gear.waitForExistence(timeout: 10)) + gear.click() + XCTAssertTrue(sheet.waitForExistence(timeout: 10)) + } + + private func thresholdField(_ fieldID: String) -> XCUIElement { + let field = sheet.textFields[fieldID] + XCTAssertTrue(field.waitForExistence(timeout: 10), "missing \(fieldID)") + return field + } + + private func replaceFieldValue(_ field: XCUIElement, with text: String) { + field.click() + app.typeKey("a", modifierFlags: .command) + field.typeText(text) + } + + private func waitForSheetDismiss(timeout: TimeInterval = 10) { + let deadline = Date().addingTimeInterval(timeout) + while Date() < deadline { + if !sheet.exists { return } + RunLoop.current.run(until: Date().addingTimeInterval(0.1)) + } + XCTAssertFalse(sheet.exists, "Expected sheet to disappear") + } + + /// Both ΔE rows must render fully inside the 560×620 sheet: the + /// label-column `StaticText`s and the control-column fields all sit + /// within the sheet bounds, the two fields share the Form's control + /// column margin, and the Warning row sits below the Good row so the + /// two cannot overlap on one clipped line. + /// + /// The numeric fields are direct `Form` children, so macOS lifts + /// each `TextField` label into the right-aligned label column and + /// the editable box fills the control column — the same layout the + /// Pickers use. The control column ends only ~3.5 pt inside the + /// sheet (PopUpButtons reach it too), so the right-edge assertion is + /// "inside the sheet", not the older 12 pt compact-field inset. + func testVerificationRowsStayInsideSheet() throws { + openSettings() + + let goodField = thresholdField("settingsDeltaEGood") + let warningField = thresholdField("settingsDeltaEWarning") + + // Labels render as sibling staticTexts in the label column. + let goodLabel = sheet.staticTexts["Good ΔE ≤"] + let warningLabel = sheet.staticTexts["Warning ΔE ≤"] + XCTAssertTrue(goodLabel.waitForExistence(timeout: 5)) + XCTAssertTrue(warningLabel.waitForExistence(timeout: 5)) + + // Left boundary: labels must be inside the sheet with >=12 pt inset + XCTAssertTrue(goodLabel.frame.minX >= sheet.frame.minX + 12.0) + XCTAssertTrue(warningLabel.frame.minX >= sheet.frame.minX + 12.0) + + // Right boundary: fields must not draw past the sheet's clip + XCTAssertTrue( + goodField.frame.maxX <= sheet.frame.maxX, + "Good ΔE field clips the sheet's right edge") + XCTAssertTrue( + warningField.frame.maxX <= sheet.frame.maxX, + "Warning ΔE field clips the sheet's right edge") + + // Vertical separation + XCTAssertTrue( + warningField.frame.minY > goodField.frame.minY, + "thresholds must be two separate rows") + + // Both threshold fields align at the same control column margin + XCTAssertTrue( + abs(goodField.frame.minX - warningField.frame.minX) <= 1.0, + "Good and Warning ΔE fields should align at the same column margin") + + // Other labels must not overflow the left boundary + let defaultInstLabel = sheet.staticTexts["Default instrument"] + XCTAssertTrue(defaultInstLabel.waitForExistence(timeout: 5)) + XCTAssertTrue( + defaultInstLabel.frame.minX >= sheet.frame.minX + 12.0, + "Default instrument label must not overflow left edge") + + let bundledSidecarsLabel = sheet.staticTexts["Bundled sidecars"] + XCTAssertTrue(bundledSidecarsLabel.waitForExistence(timeout: 5)) + XCTAssertTrue( + bundledSidecarsLabel.frame.minX >= sheet.frame.minX + 12.0, + "Bundled sidecars label must not overflow left edge") + + sheet.buttons["Cancel"].click() + waitForSheetDismiss(timeout: 5) + } + + /// `warning <= good` fails `AppSettings.validate()` and keeps the + /// sheet open with the contract error text; restoring valid values + /// lets Save dismiss (issue #165 acceptance, strings are the #5 + /// contract). + func testDeltaEValidationBlocksSaveThenValidSaveDismisses() throws { + openSettings() + + replaceFieldValue(thresholdField("settingsDeltaEWarning"), with: "1") + sheet.buttons["Save"].click() + + let error = sheet.staticTexts[ + "Good ΔE threshold must be strictly less than the warning threshold." + ] + XCTAssertTrue(error.waitForExistence(timeout: 10)) + XCTAssertTrue(sheet.exists, "invalid ΔE must not dismiss the sheet") + + replaceFieldValue(thresholdField("settingsDeltaEWarning"), with: "5") + sheet.buttons["Save"].click() + waitForSheetDismiss(timeout: 10) + } + + /// macOS renders a `TextField`'s first argument as a label, not a + /// placeholder — inside the old `HStack` rows it drew inline, so the + /// sheet read "Stale after 30 [30] days" / "Good ΔE ≤ 2.0 [2.0]". + /// As direct `Form` children each label now renders exactly once, in + /// the label column; no `staticText` may echo the field's value. + func testNumericFieldsCarryLabelsNotDuplicatedValues() throws { + openSettings() + + // (identifier, label-column text, rendered default value, the + // literal that used to double-render as the field's label) + // `value:` shows the formatted number — 2.0 renders as "2". + let rows: [(id: String, label: String, value: String, dup: String)] = [ + ("settingsDeltaEGood", "Good ΔE ≤", "2", "2.0"), + ("settingsDeltaEWarning", "Warning ΔE ≤", "5", "5.0"), + ("settingsCalStaleDays", "Stale after (days)", "30", "30"), + ] + + for spec in rows { + let field = sheet.textFields[spec.id] + XCTAssertTrue(field.waitForExistence(timeout: 10), "missing \(spec.id)") + XCTAssertEqual( + field.value as? String, spec.value, + "\(spec.id) default value changed unexpectedly") + XCTAssertTrue( + sheet.staticTexts[spec.label].waitForExistence(timeout: 5), + "\(spec.id) must render \"\(spec.label)\" once in the label column") + for ghost in Set([spec.value, spec.dup]) { + XCTAssertFalse( + sheet.staticTexts[ghost].exists, + "\(spec.id) must not render \"\(ghost)\" as a second label") + } + XCTAssertTrue( + field.frame.maxX <= sheet.frame.maxX, + "\(spec.id) field clips the sheet's right edge") + } + + sheet.buttons["Cancel"].click() + waitForSheetDismiss(timeout: 5) + } +} diff --git a/docs/21-ui-reference.md b/docs/21-ui-reference.md index 0ea7777..72ff8ce 100644 --- a/docs/21-ui-reference.md +++ b/docs/21-ui-reference.md @@ -68,7 +68,7 @@ Keyboard: **R** resets gamut camera when Stage 5 is visible. Bind to a focusable | Dialog | Root id | Controls | |--------|---------|----------| -| Settings | `settingsDialog` | listed in [22](22-settings-presets.md) | +| Settings | `settingsDialog` | `settingsDeltaEGood`, `settingsDeltaEWarning`, `settingsCalStaleDays`; other controls listed in [22](22-settings-presets.md) | | About | `aboutDialog` | `aboutVersion`, `aboutBuildDate` from `get_app_info`, `closeAboutBtn` | | Save preset | `savePresetDialog` | `savePresetName`, `savePresetDesc`, `btnConfirmSavePreset`, `btnCloseSavePresetDialog` | | Manage presets | `managePresetsDialog` | `managePresetsList`, `btnExportActivePreset`, `btnImportPreset`, `btnCloseManagePresetsDialog` | @@ -84,6 +84,6 @@ Keyboard: **R** resets gamut camera when Stage 5 is visible. Bind to a focusable Tauri v2 has **no** `window.__TAURI__.dialog`. Use invoke wrappers (`select_*`). Bugs #103, #210, #211 were exactly this. -## Complete `id=` roster (355) +## Complete `id=` roster (358) -`openSettingsBtn`, `openAboutBtn`, `btnSavePresetModal`, `btnOpenPresetsDialog`, `presetSelect`, `btnCalibratePrinter`, `calStatusChip`, `wizardNotification`, `wizardNotificationIcon`, `wizardNotificationText`, `wizardNotificationClose`, `stage-cal`, `calApplyToggleDash`, `calRgbHint`, `calSteps`, `calInkExplore`, `calNeutralEmphasis`, `btnCalGenerate`, `btnCalLayout`, `btnCalMeasure`, `calCurrentFile`, `btnCalLoad`, `btnCalLibrary`, `btnCalClear`, `calSavedSelect`, `btnCalCompute`, `calCurveSvg`, `calCurveLegend`, `calTacValue`, `calTacOverride`, `calInkLimitControls`, `calRecommendedPower`, `btnCalReturn`, `calLogContainer`, `calLog`, `stage-1`, `btnToggleAllHelp`, `calStage1Recommend`, `btnCalRecalibrate`, `stage1FormContainer`, `patchCountPreset`, `patchCountCustom`, `whitePatches`, `blackPatches`, `btn-import-dataset`, `btnOpenExisting`, `targetBasename`, `btnBrowse`, `selectedPathDisplay`, `targenAdvancedDetails`, `targenPrecondProfile`, `btnBrowsePrecondProfile`, `targenNeutralSteps`, `targenNeutralConcentration`, `targenNeutralConcVal`, `targenGreySteps`, `targenSingleChannelSteps`, `targenAdaptation`, `targenAdaptationVal`, `targenDarkEmphasis`, `targenDarkEmphasisVal`, `targenDevicePower`, `targenInkLimitGroup`, `targenInkLimit`, `targenAlgorithm`, `targenHighQuality`, `btnGenerate`, `targenLogContainer`, `targenLog`, `stage-2`, `cmWarningBanner`, `instrumentSelect`, `pageSizeSelect`, `customPageSizeRow`, `customPageW`, `customPageH`, `tiffDpi`, `printtargLayoutOrder`, `printtargCustomSeedGroup`, `printtargCustomSeed`, `btnToggleLabelEdit`, `targetMetadataPrinter`, `targetMetadataInkSet`, `targetMetadataDriverPaper`, `targetMetadataActualPaper`, `targetLabelPreview`, `btnCreateLayout`, `printtargLogContainer`, `printtargLog`, `tiffGallery`, `galleryInfo`, `galleryGrid`, `rawPrintPanel`, `printNotification`, `printNotificationIcon`, `printNotificationText`, `printerSelect`, `btnRefreshPrinters`, `btnPrinterProperties`, `printerStatusBadge`, `cupsOptionsGroup`, `chkPpdFallback`, `printerTraySelect`, `mediaTypeGroup`, `printerMediaTypeSelect`, `btnOrientPortrait`, `btnOrientLandscape`, `btnPrintAll`, `btnAdvanceToStage3`, `stage-3`, `stage3LoadedTargetBanner`, `stage3TargetBasename`, `stage3TargetMeta`, `stage3TargetBadge`, `chartreadInstrumentSelect`, `btnDetectInstruments`, `xyTableHint`, `xyTablePanel`, `xyTableActiveStepBadge`, `xyStepPlace`, `xyStepAlign`, `xyStepScan`, `xyStepRemove`, `chartreadState`, `chartreadPrompt`, `btnStartRead`, `btnCalibrate`, `btnDoneRead`, `btnAccept`, `btnRetry`, `btnUndo`, `btnSkip`, `btnCancel`, `readProgressContainer`, `readProgress`, `readProgressText`, `readStats`, `swatchGrid`, `chartreadAveragingPanel`, `passCounterBadge`, `passesList`, `btnMeasureAnotherSheet`, `btnFinishAndAverage`, `chartreadLogContainer`, `chartreadLog`, `stage-4`, `colprofQuality`, `colprofDescription`, `colprofCopyright`, `colprofAlgorithm`, `colprofFwa`, `colprofCustomSpRow`, `colprofCustomSpPath`, `btnBrowseCustomSp`, `colprofIlluminant`, `colprofObserver`, `colprofInputViewCond`, `colprofOutputViewCond`, `btnCreateProfile`, `colprofSpinnerContainer`, `colprofStageLabel`, `colprofSuccessCard`, `colprofSuccessInfo`, `btnGoToVerify`, `colprofLogContainer`, `colprofLog`, `stage-5`, `btnVerify`, `btnInstallProfile`, `profcheckReportCard`, `profcheckBadge`, `profcheckAvgDe`, `profcheckMaxDe`, `profcheckRmsDe`, `driftHistorySection`, `driftAlertCard`, `driftAlertIcon`, `driftAlertText`, `btnDriftRecalibrate`, `driftFilterRow`, `driftPrinterFilter`, `driftChartWrap`, `driftTrendChart`, `driftEmptyState`, `verificationHistoryTable`, `verificationHistoryTbody`, `btnExportHistoryCsv`, `btnClearHistory`, `gamutViewerWrap`, `gamutViewerContainer`, `gamutControlsPanel`, `chkProfileGamut`, `rngProfileOpacity`, `chkSrgbReference`, `rngSrgbOpacity`, `chkLabAxes`, `rngAxisOpacity`, `btnGamutResetCamera`, `profcheckLogContainer`, `profcheckLog`, `settingsDialog`, `argyll_binary_dir`, `default_instrument`, `enable_i1pro2_leds`, `deltaEGoodMax`, `deltaEWarningMax`, `deltaEThresholdError`, `calibrationStaleDays`, `defaultInstallLocation`, `askBeforeOverwriteProfile`, `openColorPanelAfterInstall`, `logLevelSelect`, `btnOpenLogFolder`, `btnCopyLogPath`, `btnCopyLogExcerpt`, `logPathDisplay`, `saveSettingsBtn`, `closeSettingsBtn`, `calCollisionDialog`, `calCollisionMessage`, `calOverwriteBtn`, `calRenameBtn`, `calCancelCollisionBtn`, `profileInstallCollisionDialog`, `profileInstallCollisionMessage`, `profileOverwriteBtn`, `profileRenameBtn`, `profileCancelCollisionBtn`, `aboutDialog`, `aboutVersion`, `aboutBuildDate`, `closeAboutBtn`, `savePresetDialog`, `savePresetName`, `savePresetDesc`, `btnConfirmSavePreset`, `btnCloseSavePresetDialog`, `managePresetsDialog`, `managePresetsList`, `btnExportActivePreset`, `btnImportPreset`, `btnCloseManagePresetsDialog`, `mediaSelect`, `mediaRecipeStale`, `btnMediaLibraryCapture`, `btnMediaLibraryManage`, `saveMediaRecipeDialog`, `saveMediaName`, `saveMediaNotes`, `saveMediaPaper`, `saveMediaInk`, `saveMediaPrinter`, `saveMediaPreset`, `saveMediaColourSpace`, `saveMediaCal`, `saveMediaApplyCal`, `btnConfirmSaveMedia`, `btnCloseSaveMediaDialog`, `manageMediaDialog`, `mediaLibraryList`, `mediaLibraryEmpty`, `mediaRow-{id}`, `btnMediaLibraryApply-{id}`, `btnMediaLibraryDelete-{id}`, `btnMediaLibraryApply`, `btnMediaLibraryCaptureFromManage`, `btnCloseManageMediaDialog`, `btnSpotRead`, `spotReadView`, `btnCloseSpotRead`, `spotSidecarMissing`, `btnSpotDetectInstruments`, `spotDetectError`, `spotInstrumentSelect`, `spotDefaultMissing`, `spotSetDefault`, `spotXYHint`, `spotPrompt`, `spotLastError`, `spotLogContainer`, `spotLog`, `btnSpotStart`, `btnSpotCalibrate`, `btnSpotTrigger`, `btnSpotStop`, `spotLastSample`, `spotLastEmpty`, `spotLabL`, `spotLabA`, `spotLabB`, `spotXYZ`, `spotSwatch`, `spotDeltaE`, `spotLastInstrument`, `spotLabImplausible`, `spotHistoryTable`, `spotHistoryEmpty`, `spotHistoryRow-{uuid}`, `btnSpotCopyLab`, `btnSpotExportCsv`, `btnViewGamut`, `gamutView`, `gamutStatusText`, `gamutNoticeText`, `btnResetGamutCamera`, `gamutLayer-sRGB`, `gamutLayer-profile`, `gamutLayer-compare`, `btnGamutAddCompare`, `btnGamutOpenGam`, `btnGamutOpenProfile`, `btnGamutRemoveCompare`, `btnGamutSampleTiff`, `gamutInspectPanel`, `gamutInspectIdle`, `gamutInspectL`, `gamutInspectA`, `gamutInspectB`, `gamutInspect-sRGB`, `gamutInspect-profile`, `gamutInspect-compare`, `gamutInspectSwatch`, `gamutInspectApprox`, `gamutLabEntryL`, `gamutLabEntryA`, `gamutLabEntryB`, `btnGamutInspectLab`, `gamutTiffPreview`, `btnCloseGamutTiffPreview`, `gamutViewerUnavailable`, `menuProjectNew`, `menuProjectOpen`, `menuProjectRecents`, `projectRecent-{id}`, `menuProjectRecentsClear`, `menuProjectSave`, `menuProjectSaveAs`, `menuProjectReport`, `menuProjectClose`, `projectChip`, `projectChipName`, `projectChipPath`, `projectChipStale`, `btnProjectOpen`, `btnProjectSave`, `btnProjectReveal`, `projectNewAlert`, `btnProjectNewCancel`, `btnProjectNewConfirm`, `btnProjectDirtySave`, `btnProjectDirtyDiscard`, `btnProjectDirtyCancel`, `projectRelocateSheet`, `btnProjectRelocate`, `btnProjectRelocateCancel`. +`openSettingsBtn`, `openAboutBtn`, `btnSavePresetModal`, `btnOpenPresetsDialog`, `presetSelect`, `btnCalibratePrinter`, `calStatusChip`, `wizardNotification`, `wizardNotificationIcon`, `wizardNotificationText`, `wizardNotificationClose`, `stage-cal`, `calApplyToggleDash`, `calRgbHint`, `calSteps`, `calInkExplore`, `calNeutralEmphasis`, `btnCalGenerate`, `btnCalLayout`, `btnCalMeasure`, `calCurrentFile`, `btnCalLoad`, `btnCalLibrary`, `btnCalClear`, `calSavedSelect`, `btnCalCompute`, `calCurveSvg`, `calCurveLegend`, `calTacValue`, `calTacOverride`, `calInkLimitControls`, `calRecommendedPower`, `btnCalReturn`, `calLogContainer`, `calLog`, `stage-1`, `btnToggleAllHelp`, `calStage1Recommend`, `btnCalRecalibrate`, `stage1FormContainer`, `patchCountPreset`, `patchCountCustom`, `whitePatches`, `blackPatches`, `btn-import-dataset`, `btnOpenExisting`, `targetBasename`, `btnBrowse`, `selectedPathDisplay`, `targenAdvancedDetails`, `targenPrecondProfile`, `btnBrowsePrecondProfile`, `targenNeutralSteps`, `targenNeutralConcentration`, `targenNeutralConcVal`, `targenGreySteps`, `targenSingleChannelSteps`, `targenAdaptation`, `targenAdaptationVal`, `targenDarkEmphasis`, `targenDarkEmphasisVal`, `targenDevicePower`, `targenInkLimitGroup`, `targenInkLimit`, `targenAlgorithm`, `targenHighQuality`, `btnGenerate`, `targenLogContainer`, `targenLog`, `stage-2`, `cmWarningBanner`, `instrumentSelect`, `pageSizeSelect`, `customPageSizeRow`, `customPageW`, `customPageH`, `tiffDpi`, `printtargLayoutOrder`, `printtargCustomSeedGroup`, `printtargCustomSeed`, `btnToggleLabelEdit`, `targetMetadataPrinter`, `targetMetadataInkSet`, `targetMetadataDriverPaper`, `targetMetadataActualPaper`, `targetLabelPreview`, `btnCreateLayout`, `printtargLogContainer`, `printtargLog`, `tiffGallery`, `galleryInfo`, `galleryGrid`, `rawPrintPanel`, `printNotification`, `printNotificationIcon`, `printNotificationText`, `printerSelect`, `btnRefreshPrinters`, `btnPrinterProperties`, `printerStatusBadge`, `cupsOptionsGroup`, `chkPpdFallback`, `printerTraySelect`, `mediaTypeGroup`, `printerMediaTypeSelect`, `btnOrientPortrait`, `btnOrientLandscape`, `btnPrintAll`, `btnAdvanceToStage3`, `stage-3`, `stage3LoadedTargetBanner`, `stage3TargetBasename`, `stage3TargetMeta`, `stage3TargetBadge`, `chartreadInstrumentSelect`, `btnDetectInstruments`, `xyTableHint`, `xyTablePanel`, `xyTableActiveStepBadge`, `xyStepPlace`, `xyStepAlign`, `xyStepScan`, `xyStepRemove`, `chartreadState`, `chartreadPrompt`, `btnStartRead`, `btnCalibrate`, `btnDoneRead`, `btnAccept`, `btnRetry`, `btnUndo`, `btnSkip`, `btnCancel`, `readProgressContainer`, `readProgress`, `readProgressText`, `readStats`, `swatchGrid`, `chartreadAveragingPanel`, `passCounterBadge`, `passesList`, `btnMeasureAnotherSheet`, `btnFinishAndAverage`, `chartreadLogContainer`, `chartreadLog`, `stage-4`, `colprofQuality`, `colprofDescription`, `colprofCopyright`, `colprofAlgorithm`, `colprofFwa`, `colprofCustomSpRow`, `colprofCustomSpPath`, `btnBrowseCustomSp`, `colprofIlluminant`, `colprofObserver`, `colprofInputViewCond`, `colprofOutputViewCond`, `btnCreateProfile`, `colprofSpinnerContainer`, `colprofStageLabel`, `colprofSuccessCard`, `colprofSuccessInfo`, `btnGoToVerify`, `colprofLogContainer`, `colprofLog`, `stage-5`, `btnVerify`, `btnInstallProfile`, `profcheckReportCard`, `profcheckBadge`, `profcheckAvgDe`, `profcheckMaxDe`, `profcheckRmsDe`, `driftHistorySection`, `driftAlertCard`, `driftAlertIcon`, `driftAlertText`, `btnDriftRecalibrate`, `driftFilterRow`, `driftPrinterFilter`, `driftChartWrap`, `driftTrendChart`, `driftEmptyState`, `verificationHistoryTable`, `verificationHistoryTbody`, `btnExportHistoryCsv`, `btnClearHistory`, `gamutViewerWrap`, `gamutViewerContainer`, `gamutControlsPanel`, `chkProfileGamut`, `rngProfileOpacity`, `chkSrgbReference`, `rngSrgbOpacity`, `chkLabAxes`, `rngAxisOpacity`, `btnGamutResetCamera`, `profcheckLogContainer`, `profcheckLog`, `settingsDialog`, `argyll_binary_dir`, `default_instrument`, `enable_i1pro2_leds`, `deltaEGoodMax`, `deltaEWarningMax`, `settingsDeltaEGood`, `settingsDeltaEWarning`, `settingsCalStaleDays`, `deltaEThresholdError`, `calibrationStaleDays`, `defaultInstallLocation`, `askBeforeOverwriteProfile`, `openColorPanelAfterInstall`, `logLevelSelect`, `btnOpenLogFolder`, `btnCopyLogPath`, `btnCopyLogExcerpt`, `logPathDisplay`, `saveSettingsBtn`, `closeSettingsBtn`, `calCollisionDialog`, `calCollisionMessage`, `calOverwriteBtn`, `calRenameBtn`, `calCancelCollisionBtn`, `profileInstallCollisionDialog`, `profileInstallCollisionMessage`, `profileOverwriteBtn`, `profileRenameBtn`, `profileCancelCollisionBtn`, `aboutDialog`, `aboutVersion`, `aboutBuildDate`, `closeAboutBtn`, `savePresetDialog`, `savePresetName`, `savePresetDesc`, `btnConfirmSavePreset`, `btnCloseSavePresetDialog`, `managePresetsDialog`, `managePresetsList`, `btnExportActivePreset`, `btnImportPreset`, `btnCloseManagePresetsDialog`, `mediaSelect`, `mediaRecipeStale`, `btnMediaLibraryCapture`, `btnMediaLibraryManage`, `saveMediaRecipeDialog`, `saveMediaName`, `saveMediaNotes`, `saveMediaPaper`, `saveMediaInk`, `saveMediaPrinter`, `saveMediaPreset`, `saveMediaColourSpace`, `saveMediaCal`, `saveMediaApplyCal`, `btnConfirmSaveMedia`, `btnCloseSaveMediaDialog`, `manageMediaDialog`, `mediaLibraryList`, `mediaLibraryEmpty`, `mediaRow-{id}`, `btnMediaLibraryApply-{id}`, `btnMediaLibraryDelete-{id}`, `btnMediaLibraryApply`, `btnMediaLibraryCaptureFromManage`, `btnCloseManageMediaDialog`, `btnSpotRead`, `spotReadView`, `btnCloseSpotRead`, `spotSidecarMissing`, `btnSpotDetectInstruments`, `spotDetectError`, `spotInstrumentSelect`, `spotDefaultMissing`, `spotSetDefault`, `spotXYHint`, `spotPrompt`, `spotLastError`, `spotLogContainer`, `spotLog`, `btnSpotStart`, `btnSpotCalibrate`, `btnSpotTrigger`, `btnSpotStop`, `spotLastSample`, `spotLastEmpty`, `spotLabL`, `spotLabA`, `spotLabB`, `spotXYZ`, `spotSwatch`, `spotDeltaE`, `spotLastInstrument`, `spotLabImplausible`, `spotHistoryTable`, `spotHistoryEmpty`, `spotHistoryRow-{uuid}`, `btnSpotCopyLab`, `btnSpotExportCsv`, `btnViewGamut`, `gamutView`, `gamutStatusText`, `gamutNoticeText`, `btnResetGamutCamera`, `gamutLayer-sRGB`, `gamutLayer-profile`, `gamutLayer-compare`, `btnGamutAddCompare`, `btnGamutOpenGam`, `btnGamutOpenProfile`, `btnGamutRemoveCompare`, `btnGamutSampleTiff`, `gamutInspectPanel`, `gamutInspectIdle`, `gamutInspectL`, `gamutInspectA`, `gamutInspectB`, `gamutInspect-sRGB`, `gamutInspect-profile`, `gamutInspect-compare`, `gamutInspectSwatch`, `gamutInspectApprox`, `gamutLabEntryL`, `gamutLabEntryA`, `gamutLabEntryB`, `btnGamutInspectLab`, `gamutTiffPreview`, `btnCloseGamutTiffPreview`, `gamutViewerUnavailable`, `menuProjectNew`, `menuProjectOpen`, `menuProjectRecents`, `projectRecent-{id}`, `menuProjectRecentsClear`, `menuProjectSave`, `menuProjectSaveAs`, `menuProjectReport`, `menuProjectClose`, `projectChip`, `projectChipName`, `projectChipPath`, `projectChipStale`, `btnProjectOpen`, `btnProjectSave`, `btnProjectReveal`, `projectNewAlert`, `btnProjectNewCancel`, `btnProjectNewConfirm`, `btnProjectDirtySave`, `btnProjectDirtyDiscard`, `btnProjectDirtyCancel`, `projectRelocateSheet`, `btnProjectRelocate`, `btnProjectRelocateCancel`.