diff --git a/Sources/ICCery/MediaLibraryDialogs.swift b/Sources/ICCery/MediaLibraryDialogs.swift index 988effe..4d3932e 100644 --- a/Sources/ICCery/MediaLibraryDialogs.swift +++ b/Sources/ICCery/MediaLibraryDialogs.swift @@ -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, diff --git a/Sources/ICCery/MediaLibraryViewModel.swift b/Sources/ICCery/MediaLibraryViewModel.swift index 1b11532..930edb3 100644 --- a/Sources/ICCery/MediaLibraryViewModel.swift +++ b/Sources/ICCery/MediaLibraryViewModel.swift @@ -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 diff --git a/Tests/ICCeryUITests/Milestone10MediaLibraryUITests.swift b/Tests/ICCeryUITests/Milestone10MediaLibraryUITests.swift index bb4b518..0daf084 100644 --- a/Tests/ICCeryUITests/Milestone10MediaLibraryUITests.swift +++ b/Tests/ICCeryUITests/Milestone10MediaLibraryUITests.swift @@ -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"),