Author SHA1 Message Date
gronod 2fefe98434 fix: UI test helpers for nested license window sheet
macOS CI / package (pull_request) Canceled after 0s
macOS CI / build-and-test (pull_request) Canceled after 21s
macOS CI / build-and-test (push) Successful in 37m17s
macOS CI / package (push) Successful in 3m29s
- Update element() helper in AboutHelpUITests to search all sheets (nested sheets)
- Add .accessibilityElement(children: .contain) to LicenseWindowView root and attribution section
- All 6 AboutHelpUITests now pass (2 existing + 4 new license window tests)
2026-09-15 01:21:54 +01:00
gronod 90d034baab fix: LicenseWindowView compatibility with macOS 12.0 (remove NavigationStack)
macOS CI / build-and-test (pull_request) Canceled after 0s
macOS CI / package (pull_request) Canceled after 0s
macOS CI / build-and-test (push) Failing after 41m0s
macOS CI / package (push) Skipped
2026-09-14 23:40:08 +01:00
gronod 8cb79e8ffa feat: add license window to About dialog (issue #31)
macOS CI / build-and-test (push) Skipped
- Add LicenseWindowView.swift with ICCery license, ArgyllCMS AGPLv3 license (from bundled License.txt), and attribution links to upstream and Gronod fork
- Add "View Licenses" button to AboutView.swift that opens license window as sheet
- Update fetch-argyll.sh to copy License.txt to Vendor/Argyll/ root
- Add UI tests for license window content, links, and AGPL isolation note

The license window includes:
- ICCery license text (embedded from LICENCE.md)
- ArgyllCMS AGPLv3 license (dynamically loaded from bundled Resources/Argyll/License.txt)
- Attribution to Graeme Gill with link to argyllcms.com
- Attribution to Gronod fork with link to git.i3omb.com/gronod/argyllcms
- AGPL subprocess isolation note
2026-09-14 23:35:43 +01:00
gronod c1c37bb7d7 test(m4): retry Start Read until the chartread session is running
macOS CI / package (pull_request) Canceled after 0s
macOS CI / build-and-test (pull_request) Canceled after 1m52s
macOS CI / build-and-test (push) Successful in 37m49s
macOS CI / package (push) Successful in 4m39s
testTwoPassAverageFailurePromotesFirstPass died on Gitea run 35443
waiting for btnCalibrate after a single Start click. Handheld in the
same job succeeded: AX reported enabled and synthesized the click, but
SwiftUI startRead() was still a no-op (same class as 35251).

Re-click until btnCancel/Calibrate/Trigger exist — Cancel is in the
tree for the whole session — then wait for Calibrate.
2026-09-14 18:40:27 +00:00
gronod d2466d9a66 Merge pull request 'test(m2): mock CUPS so Print All enables on GitHub macos-14' (!176) from fix/m2-print-all-mock-cups into develop
macOS CI / build-and-test (push) Failing after 37m56s
macOS CI / package (push) Skipped
Reviewed-on: #176
2026-09-14 17:35:39 +01:00
gronod 4af2d96294 test(m2): mock CUPS so Print All enables on GitHub macos-14
macOS CI / build-and-test (push) Skipped
macOS CI / build-and-test (pull_request) Canceled after 0s
macOS CI / package (pull_request) Canceled after 0s
testPrinttargFixtureGalleryAndStubbedPrint asserted btnPrintAll
immediately after the gallery appeared, without ICCERY_CUPS_BIN_DIR.
Gitea Monterey has system queues; GitHub macos-14-arm64 does not, so
selectedPrinter stays empty and the button stays disabled (run
34867767434). Point M2 at Fixtures/bin like M3, and wait for enable.
2026-09-14 16:32:57 +00:00
gronod d17aa962ce Merge pull request 'fix(ci): pin GitHub Actions to macos-14 and clamp the window on-screen' (#175) from fix/gh-ci-window-and-runner into develop
macOS CI / build-and-test (push) Canceled after 0s
macOS CI / package (push) Canceled after 0s
Reviewed-on: #175
2026-09-14 17:18:13 +01:00
gronod 93acd32b51 fix(ci): pin GitHub Actions to macos-14 and keep the window on-screen
macOS CI / build-and-test (push) Skipped
macOS CI / build-and-test (pull_request) Canceled after 0s
macOS CI / package (pull_request) Canceled after 0s
macos-latest is macos-26-arm64 (run 34864198118). A 1280x800 window
centered on that display hangs off the left edge, so XCTest sees
sidebar controls at x ≈ -116 and refuses the click. Pin the GitHub
workflow to macos-14 and clamp the main window to visibleFrame.
2026-09-14 16:17:20 +00:00
gronod 67cf452e59 Update .github/workflows/macos.yml
macOS CI / package (push) Canceled after 0s
macOS CI / build-and-test (push) Canceled after 27m49s
2026-09-14 16:44:50 +01:00
gronod 83ed53c7fc Merge pull request 'ci: GitHub Actions twin of the Gitea macOS workflow' (#174) from ci/github-workflows into develop
macOS CI / build-and-test (push) Successful in 40m30s
macOS CI / package (push) Successful in 3m27s
Reviewed-on: #174
2026-09-14 16:42:01 +01:00
gronod cba9476af7 Update .github/workflows/macos.yml
macOS CI / build-and-test (push) Skipped
macOS CI / build-and-test (pull_request) Failing after 3m40s
macOS CI / package (pull_request) Skipped
2026-09-14 16:41:41 +01:00
gronod 288b08e3d9 ci: add GitHub Actions twin of the Gitea macOS workflow
macOS CI / build-and-test (push) Skipped
macOS CI / build-and-test (pull_request) Failing after 42m38s
macOS CI / package (pull_request) Skipped
Copy .gitea/workflows/macos.yml to .github with the github.com deltas:
macos-14 (macos-12 is gone), upload-artifact@v4, no private-CA bundle,
and gh release upload instead of the Gitea asset script.
2026-09-14 15:33:13 +00:00
gronod 18f1d1ff33 Merge pull request 'test(m4): wait for Start Read enabled before chartread (CI 35251/35293)' (#173) from fix/m4-handheld-start-read into develop
macOS CI / build-and-test (push) Successful in 42m34s
macOS CI / package (push) Canceled after 21s
test(m4): wait for Start Read enabled before chartread

Runs 35251 and 35293 clicked a disabled Start Read.
2026-09-14 16:16:34 +01:00
gronod 9dfa79ebd0 test(m4): wait for Start Read to be enabled before clicking
macOS CI / build-and-test (push) Skipped
macOS CI / build-and-test (pull_request) Canceled after 0s
macOS CI / package (pull_request) Canceled after 0s
Runs 35251 and 35293 left Stage 3 idle because XCTest clicked a
disabled Start Read. driveOnePass already waits for isEnabled;
the handheld average test now uses it.
2026-09-14 15:16:25 +00:00
14 changed files with 621 additions and 378 deletions
+231
View File
@@ -0,0 +1,231 @@
# GitHub Actions twin of .gitea/workflows/macos.yml.
# Deltas from the Gitea file (everything else is the same jobs/steps):
# - runs-on macos-14: github.com retired macos-12. Do not use
# macos-latest — in 2026 that is macos-26-arm64, where a 1280×800
# window hangs off the virtual display and XCTest marks sidebar
# controls (x ≈ -116) as not hittable (run 34864198118).
# - actions/upload-artifact@v4: v3 is shut down on github.com. Gitea act_runner
# still uses v3.
# - No NODE_EXTRA_CA_CERTS / System keychain bundle: that is only for the
# private Gitea CA when the runner talks to git.i3omb.com.
# - Tag DMGs go to a GitHub Release via `gh` instead of
# scripts/attach-release-asset.sh (Gitea /api/v1).
name: macOS CI
on:
push:
branches:
- develop
tags:
- 'v*'
pull_request:
branches:
- develop
permissions:
contents: read
jobs:
build-and-test:
runs-on: macos-14
env:
DERIVED: build/DerivedData-test
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Assert Xcode 14+ toolchain
run: |
line="$(xcodebuild -version | head -1)"
major="$(printf '%s' "$line" | sed -n 's/^Xcode \([0-9][0-9]*\)\..*/\1/p')"
if [ -z "$major" ] || [ "$major" -lt 14 ]; then
echo "Unexpected Xcode version: $line" >&2; exit 1
fi
echo "$line"
# Tag pushes whose name contains "prerelease" skip the test build and both
# test legs: they exist to package a build already validated elsewhere.
# The job still succeeds quickly so `package`'s `needs:` stays satisfied.
# Homebrew's xcodegen formula requires Xcode 15.3, which cannot be
# installed on macOS 12 (#109). The script installs a pinned
# prebuilt release instead. dmgbuild is not installed here — the
# test job does not package (#95).
- name: Ensure host tools
if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
run: scripts/ensure-host-tools.sh
- name: Generate Xcode project
if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
run: xcodegen generate --spec project.yml
# Tests only ever run on the runner's own architecture; build
# just that slice. Packaging (scripts/package-release.sh) still
# produces the universal Release binary.
- name: Build for testing (host arch)
if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
run: |
xcodebuild build-for-testing \
-scheme ICCery \
-destination 'platform=macOS' \
-derivedDataPath "$DERIVED" \
-configuration Debug \
ARCHS="$(uname -m)" \
ONLY_ACTIVE_ARCH=NO \
CODE_SIGNING_ALLOWED=YES \
CODE_SIGN_IDENTITY='-'
# Xcode embeds the shared ICCeryCore package framework into the app
# and the test bundle without signing it. Ad-hoc hosts still require
# every loaded dylib to carry a cdhash — dyld killed the test host at
# launch (run 31992) — so sign every embedded copy once the build is
# done (embed steps run after any build script phase) (#119).
- name: Sign package product frameworks
if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
run: |
find "$DERIVED/Build/Products/Debug" -depth -name '*_PackageProduct.framework' -print0 \
| while IFS= read -r -d '' fw; do
echo "signing $fw"
codesign --force --sign - --timestamp=none "$fw"
done
- name: Test unit (ICCeryCoreTests)
if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
run: |
XCTESTRUN="$(find "$DERIVED" -name 'ICCery*.xctestrun' | head -n 1)"
if [ -z "$XCTESTRUN" ] || [ ! -f "$XCTESTRUN" ]; then
echo "error: no xctestrun produced by build-for-testing" >&2
exit 1
fi
echo "xctestrun: $XCTESTRUN"
xcodebuild test-without-building \
-xctestrun "$XCTESTRUN" \
-only-testing:ICCeryCoreTests \
-destination 'platform=macOS' \
-derivedDataPath "$DERIVED"
# UI tests need macOS Automation / Accessibility permission on the
# runner. GitHub-hosted macos-14 images enable this; a self-hosted
# Mac can still time out enabling that mode (run 29700) or launch
# the app into `.runningBackground` (run 29804). Kill any leftover
# unit-test host first; retry once; if the runner still cannot
# attach, do not fail the required gate so tag packaging can
# proceed. Real XCTest assertion failures still fail the job.
- name: Test UI (ICCeryUITests)
if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
run: |
set -o pipefail
XCTESTRUN="$(find "$DERIVED" -name 'ICCery*.xctestrun' | head -n 1)"
LOG="$DERIVED/ui-test.log"
pkill -x ICCery 2>/dev/null || true
sleep 1
run_ui() {
local label="$1"
shift
echo "::group::UI tests $label"
set +e
xcodebuild test-without-building \
-xctestrun "$XCTESTRUN" \
-destination 'platform=macOS' \
-derivedDataPath "$DERIVED" \
"$@" | tee "$LOG"
rc=${PIPESTATUS[0]}
set -e
echo "::endgroup::"
return "$rc"
}
is_runner_attach_failure() {
grep -Eq "Timed out while enabling automation mode|Failed to activate application|current state: Running Background" "$LOG"
}
attempt=1
while [ "$attempt" -le 2 ]; do
# Probe one case first. A background-activate failure costs
# ~65s here instead of ~25 minutes for the whole suite (29804).
if ! run_ui "probe attempt $attempt" \
-only-testing:ICCeryUITests/AboutHelpUITests/testAboutDialogShowsVersionAndBuildDate; then
if is_runner_attach_failure; then
echo "warning: UI runner could not attach/activate the app (attempt $attempt)"
pkill -x ICCery 2>/dev/null || true
attempt=$((attempt + 1))
sleep 8
continue
fi
echo "error: UI probe failed with a real test error" >&2
exit 1
fi
if run_ui "full suite attempt $attempt" -only-testing:ICCeryUITests \
-skip-testing:ICCeryUITests/AboutHelpUITests/testAboutDialogShowsVersionAndBuildDate; then
exit 0
fi
if is_runner_attach_failure; then
echo "warning: UI runner lost activation mid-suite (attempt $attempt)"
pkill -x ICCery 2>/dev/null || true
attempt=$((attempt + 1))
sleep 8
continue
fi
echo "error: UI tests failed with a real test error" >&2
exit 1
done
echo "warning: skipping UI tests after repeated runner attach/activate failures"
exit 0
# XCTest stores the a11y hierarchy snapshot and screenshots in the
# xcresult on failure — upload it so UI failures can be triaged
# without access to the runner (#126).
- name: Upload UI test xcresult
if: "failure() && !(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
uses: actions/upload-artifact@v4
with:
name: ui-test-xcresult
path: build/DerivedData-test/Logs/Test
package:
needs: build-and-test
runs-on: macos-14
if: github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
# scripts/package-release.sh runs `xcodegen generate` and dmgbuild;
# see build-and-test for why brew is not used on macOS 12 (#109).
# INSTALL_DMGBUILD isolates dmgbuild in build/.venv-dmgbuild so
# the test job never pip-installs it (#95).
- name: Ensure host tools
run: INSTALL_DMGBUILD=1 scripts/ensure-host-tools.sh
- name: Package release
run: scripts/package-release.sh
env:
CODESIGN_IDENTITY: ${{ secrets.CODESIGN_IDENTITY }}
DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }}
NOTARIZE_APPLE_ID: ${{ secrets.NOTARIZE_APPLE_ID }}
NOTARIZE_PASSWORD: ${{ secrets.NOTARIZE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- name: Upload DMG artifact
uses: actions/upload-artifact@v4
with:
name: iccery-dmg
path: ICCery-*.dmg
- name: Attach DMG to GitHub release
if: startsWith(github.ref, 'refs/tags/v')
env:
GH_TOKEN: ${{ github.token }}
run: |
set -eu
TAG="$GITHUB_REF_NAME"
case "$TAG" in
*prerelease*) PRE_FLAG=--prerelease ;;
*) PRE_FLAG= ;;
esac
if ! gh release view "$TAG" >/dev/null 2>&1; then
gh release create "$TAG" --title "$TAG" --target "$GITHUB_SHA" $PRE_FLAG
fi
gh release upload "$TAG" ICCery-*.dmg --clobber
+5 -1
View File
@@ -206,9 +206,13 @@ xcodebuild test -scheme ICCery -destination 'platform=macOS' \
-only-testing:ICCeryUITests/Milestone5UITests -only-testing:ICCeryUITests/Milestone5UITests
``` ```
CI (`.gitea/workflows/macos.yml`) runs `build-and-test` then `package` on CI (`.gitea/workflows/macos.yml` on Gitea, `.github/workflows/macos.yml` on
GitHub) runs `build-and-test` then `package` on
`develop` and on `v*` tags. Tags whose name contains `prerelease` skip the `develop` and on `v*` tags. Tags whose name contains `prerelease` skip the
test job and still package. `pull_request` is wired for **`develop` only**. test job and still package. `pull_request` is wired for **`develop` only**.
The GitHub file is the same pipeline on `macos-14` (github.com retired
`macos-12`), `actions/upload-artifact@v4`, and `gh release upload` for tag
DMGs.
UI tests need an unlocked console (`IOConsoleLocked=false`). Mock Argyll / UI tests need an unlocked console (`IOConsoleLocked=false`). Mock Argyll /
CUPS fixtures live under the test bundles; they must not be treated as proof CUPS fixtures live under the test bundles; they must not be treated as proof
+11
View File
@@ -6,6 +6,8 @@ import ICCeryCore
struct AboutView: View { struct AboutView: View {
let onClose: () -> Void let onClose: () -> Void
@State private var showingLicenses = false
private let info = ArtefactFiles.appInfo() private let info = ArtefactFiles.appInfo()
var body: some View { var body: some View {
@@ -54,6 +56,12 @@ struct AboutView: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
Button("View Licenses") {
showingLicenses = true
}
.controlSize(.large)
.accessibilityIdentifier("viewLicensesBtn")
Button("Close") { Button("Close") {
onClose() onClose()
} }
@@ -66,5 +74,8 @@ struct AboutView: View {
.background(Theme.panel) .background(Theme.panel)
.accessibilityElement(children: .contain) .accessibilityElement(children: .contain)
.accessibilityIdentifier("aboutDialog") .accessibilityIdentifier("aboutDialog")
.sheet(isPresented: $showingLicenses) {
LicenseWindowView()
}
} }
} }
+33 -2
View File
@@ -56,10 +56,41 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
} }
/// docs/21 §Shell: 1280×800 content, min 1100×700, centred. /// docs/21 §Shell: 1280×800 content, min 1100×700, centred.
/// GitHub-hosted Macs (and any display smaller than 1280×800) must
/// not get a window that hangs off-screen XCTest then reports
/// sidebar controls at negative x as not hittable (run 34864198118).
private func configureMainWindow(_ window: NSWindow) { private func configureMainWindow(_ window: NSWindow) {
window.setContentSize(NSSize(width: 1280, height: 800)) let desired = NSSize(width: 1280, height: 800)
window.contentMinSize = NSSize(width: 1100, height: 700) let minimum = NSSize(width: 1100, height: 700)
let visible = (window.screen ?? NSScreen.main)?.visibleFrame
?? NSRect(origin: .zero, size: desired)
window.contentMinSize = NSSize(
width: min(minimum.width, visible.width),
height: min(minimum.height, visible.height)
)
window.setContentSize(NSSize(
width: min(desired.width, visible.width),
height: min(desired.height, max(minimum.height, visible.height - 40))
))
window.center() window.center()
var frame = window.frame
if frame.width > visible.width {
frame.size.width = visible.width
}
if frame.height > visible.height {
frame.size.height = visible.height
}
frame.origin.x = min(
max(frame.origin.x, visible.minX),
visible.maxX - frame.width
)
frame.origin.y = min(
max(frame.origin.y, visible.minY),
visible.maxY - frame.height
)
window.setFrame(frame, display: true)
} }
/// Dock-click reopen: let the WindowGroup re-show or recreate the /// Dock-click reopen: let the WindowGroup re-show or recreate the
+148
View File
@@ -0,0 +1,148 @@
import AppKit
import SwiftUI
/// Detailed license and attribution window (issue #31, docs/21 §Modals).
struct LicenseWindowView: View {
@Environment(\.dismiss) private var dismiss
private let icceryLicense: String
private let argyllLicense: String
init() {
// ICCery license from LICENCE.md (embedded at compile time)
self.icceryLicense = """
# LICENCE
**Copyright (c) 2026 Gordon Bolton**
**All Rights Reserved.**
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), strictly to view the source code and execute the Software for the sole purpose of personal testing, evaluation, and providing feedback.
Under this licence, you may **not**:
* Modify, alter, or create derivative works of the Software.
* Distribute, publish, or sublicense the Software or any derivatives.
* Use the Software for any commercial or production purpose.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## Bundled ArgyllCMS sidecar binaries
This application bundles and invokes command-line binaries from the Gronod fork of ArgyllCMS. Those binaries are licensed separately under the **GNU Affero General Public License v3 (AGPLv3)**. They are executed strictly as independent subprocesses — they are never linked, loaded, or incorporated into this application — and a copy of `License.txt` is shipped beside the binaries in `Resources/Argyll/`. The terms above apply only to the ICCery application source code, not to the ArgyllCMS binaries.
"""
// ArgyllCMS license from bundled License.txt
if let url = Bundle.main.url(forResource: "License", withExtension: "txt", subdirectory: "Argyll"),
let content = try? String(contentsOf: url, encoding: .utf8) {
self.argyllLicense = content
} else {
self.argyllLicense = """
ArgyllCMS license not found — run `scripts/fetch-argyll.sh` to bundle binaries and license.
The ArgyllCMS binaries are licensed under the GNU Affero General Public License v3 (AGPLv3).
A copy of the license should be present at Resources/Argyll/License.txt.
See: https://git.i3omb.com/gronod/argyllcms/releases
"""
}
}
var body: some View {
VStack(spacing: 0) {
// Title bar
HStack {
Text("Licenses & Attribution")
.font(.headline)
.foregroundStyle(Theme.text)
Spacer()
Button("Close") {
dismiss()
}
.keyboardShortcut(.cancelAction)
.accessibilityIdentifier("closeLicenseBtn")
}
.padding(.horizontal, 20)
.padding(.vertical, 12)
.background(Theme.panel.opacity(0.9))
Divider()
ScrollView {
VStack(alignment: .leading, spacing: 24) {
// Section 1: ICCery License
licenseSection(
title: "ICCery License",
content: icceryLicense,
identifier: "icceryLicenseSection"
)
Divider()
// Section 2: ArgyllCMS License
licenseSection(
title: "ArgyllCMS License (AGPLv3)",
content: argyllLicense,
identifier: "argyllLicenseSection"
)
Divider()
// Section 3: Attribution & Links
VStack(alignment: .leading, spacing: 12) {
Text("Attribution & Links")
.font(.headline)
.foregroundStyle(Theme.text)
.accessibilityIdentifier("attributionHeader")
VStack(alignment: .leading, spacing: 8) {
Link("ArgyllCMS by Graeme Gill → https://www.argyllcms.com/",
destination: URL(string: "https://www.argyllcms.com/")!)
.font(.callout)
.foregroundStyle(Theme.accent)
.accessibilityIdentifier("argyllUpstreamLink")
Link("Gronod ArgyllCMS fork (v3.5.0-ICCery.1.x) → https://git.i3omb.com/gronod/argyllcms",
destination: URL(string: "https://git.i3omb.com/gronod/argyllcms")!)
.font(.callout)
.foregroundStyle(Theme.accent)
.accessibilityIdentifier("argyllForkLink")
}
Text("ICCery bundles and invokes ArgyllCMS binaries as isolated subprocesses per AGPLv3 isolation requirements. The ArgyllCMS binaries are never linked, loaded, or incorporated into the ICCery application binary.")
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
.accessibilityIdentifier("agplIsolationNote")
}
.padding(.horizontal, 4)
.accessibilityElement(children: .contain)
.accessibilityIdentifier("attributionSection")
}
.padding(24)
}
.frame(minWidth: 600, minHeight: 500)
.background(Theme.panel)
}
.accessibilityElement(children: .contain)
.accessibilityIdentifier("licenseWindow")
}
private func licenseSection(title: String, content: String, identifier: String) -> some View {
VStack(alignment: .leading, spacing: 8) {
Text(title)
.font(.headline)
.foregroundStyle(Theme.text)
.accessibilityIdentifier(identifier + "Header")
Text(content)
.font(.system(.body, design: .monospaced))
.foregroundStyle(Theme.text)
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
.accessibilityIdentifier(identifier + "Content")
}
.padding(.horizontal, 4)
}
}
+142
View File
@@ -20,6 +20,13 @@ final class AboutHelpUITests: XCTestCase {
private func element(_ id: String) -> XCUIElement { private func element(_ id: String) -> XCUIElement {
let inApp = app.descendants(matching: .any)[id].firstMatch let inApp = app.descendants(matching: .any)[id].firstMatch
if inApp.exists { return inApp } if inApp.exists { return inApp }
// Search all sheets (including nested sheets) for the element.
// The license window is a nested sheet (sheet presented from AboutView).
for sheet in app.sheets.allElementsBoundByIndex {
let inSheet = sheet.descendants(matching: .any)[id].firstMatch
if inSheet.exists { return inSheet }
}
// Fallback to original behavior
return app.sheets.firstMatch.descendants(matching: .any)[id].firstMatch return app.sheets.firstMatch.descendants(matching: .any)[id].firstMatch
} }
@@ -85,4 +92,139 @@ final class AboutHelpUITests: XCTestCase {
"Toggling global help must not reflow the sidebar.") "Toggling global help must not reflow the sidebar.")
XCTAssertTrue(app.descendants(matching: .any)["openSettingsBtn"].exists) XCTAssertTrue(app.descendants(matching: .any)["openSettingsBtn"].exists)
} }
func testAboutDialogShowsViewLicensesButton() throws {
launchApp()
let openAbout = app.buttons["openAboutBtn"]
XCTAssertTrue(openAbout.waitForExistence(timeout: 10))
openAbout.click()
_ = waitFor("aboutVersion", timeout: 10)
let viewLicensesBtn = waitFor("viewLicensesBtn", timeout: 10)
XCTAssertTrue(viewLicensesBtn.exists)
viewLicensesBtn.click()
// License window should open as a sheet
_ = waitFor("licenseWindow", timeout: 10)
XCTAssertTrue(element("licenseWindow").exists)
// Close license window
let closeLicenseBtn = waitFor("closeLicenseBtn", timeout: 5)
closeLicenseBtn.click()
// License window should be dismissed
XCTAssertFalse(element("licenseWindow").exists)
// Close about dialog
let closeAboutBtn = waitFor("closeAboutBtn", timeout: 5)
closeAboutBtn.click()
XCTAssertFalse(element("aboutDialog").exists)
}
func testLicenseWindowShowsICCeryLicense() throws {
launchApp()
let openAbout = app.buttons["openAboutBtn"]
XCTAssertTrue(openAbout.waitForExistence(timeout: 10))
openAbout.click()
let viewLicensesBtn = waitFor("viewLicensesBtn", timeout: 10)
viewLicensesBtn.click()
_ = waitFor("licenseWindow", timeout: 10)
// Verify ICCery license section exists
let icceryLicenseSection = waitFor("icceryLicenseSectionHeader", timeout: 5)
XCTAssertTrue(icceryLicenseSection.exists)
// Verify ICCery license content contains key phrases
let icceryLicenseContent = element("icceryLicenseSectionContent")
XCTAssertTrue(icceryLicenseContent.waitForExistence(timeout: 5))
let licenseText = icceryLicenseContent.value as? String ?? ""
XCTAssertTrue(licenseText.contains("Copyright (c) 2026 Gordon Bolton"))
XCTAssertTrue(licenseText.contains("All Rights Reserved"))
XCTAssertTrue(licenseText.contains("AGPLv3"))
// Close license window
let closeLicenseBtn = waitFor("closeLicenseBtn", timeout: 5)
closeLicenseBtn.click()
let closeAboutBtn = waitFor("closeAboutBtn", timeout: 5)
closeAboutBtn.click()
}
func testLicenseWindowShowsArgyllLicense() throws {
launchApp()
let openAbout = app.buttons["openAboutBtn"]
XCTAssertTrue(openAbout.waitForExistence(timeout: 10))
openAbout.click()
let viewLicensesBtn = waitFor("viewLicensesBtn", timeout: 10)
viewLicensesBtn.click()
_ = waitFor("licenseWindow", timeout: 10)
// Verify ArgyllCMS license section exists
let argyllLicenseSection = waitFor("argyllLicenseSectionHeader", timeout: 5)
XCTAssertTrue(argyllLicenseSection.exists)
// Verify Argyll license content exists (may be fallback if License.txt not bundled)
let argyllLicenseContent = element("argyllLicenseSectionContent")
XCTAssertTrue(argyllLicenseContent.waitForExistence(timeout: 5))
let licenseText = argyllLicenseContent.value as? String ?? ""
// Should contain either the actual AGPLv3 license or the fallback notice
XCTAssertTrue(licenseText.contains("AGPLv3") || licenseText.contains("GNU Affero General Public License") || licenseText.contains("fetch-argyll"))
// Close license window
let closeLicenseBtn = waitFor("closeLicenseBtn", timeout: 5)
closeLicenseBtn.click()
let closeAboutBtn = waitFor("closeAboutBtn", timeout: 5)
closeAboutBtn.click()
}
func testLicenseWindowShowsAttributionLinks() throws {
launchApp()
let openAbout = app.buttons["openAboutBtn"]
XCTAssertTrue(openAbout.waitForExistence(timeout: 10))
openAbout.click()
let viewLicensesBtn = waitFor("viewLicensesBtn", timeout: 10)
viewLicensesBtn.click()
_ = waitFor("licenseWindow", timeout: 10)
// Verify attribution section exists
let attributionHeader = waitFor("attributionHeader", timeout: 5)
XCTAssertTrue(attributionHeader.exists)
// Verify upstream link
let upstreamLink = element("argyllUpstreamLink")
XCTAssertTrue(upstreamLink.waitForExistence(timeout: 5))
let upstreamLabel = upstreamLink.label
XCTAssertTrue(upstreamLabel.contains("Graeme Gill") || upstreamLabel.contains("argyllcms.com"))
// Verify fork link
let forkLink = element("argyllForkLink")
XCTAssertTrue(forkLink.waitForExistence(timeout: 5))
let forkLabel = forkLink.label
XCTAssertTrue(forkLabel.contains("Gronod") || forkLabel.contains("git.i3omb.com"))
// Verify AGPL isolation note
let isolationNote = element("agplIsolationNote")
XCTAssertTrue(isolationNote.waitForExistence(timeout: 5))
let noteText = isolationNote.value as? String ?? ""
XCTAssertTrue(noteText.contains("isolated subprocesses") || noteText.contains("AGPLv3 isolation"))
// Close license window
let closeLicenseBtn = waitFor("closeLicenseBtn", timeout: 5)
closeLicenseBtn.click()
let closeAboutBtn = waitFor("closeAboutBtn", timeout: 5)
closeAboutBtn.click()
}
} }
+6 -4
View File
@@ -36,6 +36,7 @@ final class Milestone2UITests: XCTestCase {
"ICCERY_UI_TESTING": "1", "ICCERY_UI_TESTING": "1",
"ICCERY_TEST_ROOT": testRoot.path, "ICCERY_TEST_ROOT": testRoot.path,
"ICCERY_ARGYLL_BINARY_DIR": binDir.path, "ICCERY_ARGYLL_BINARY_DIR": binDir.path,
"ICCERY_CUPS_BIN_DIR": binDir.path,
"ICCERY_TEST_SAVE_TARGET": "ICCERY_TEST_SAVE_TARGET":
workDir.appendingPathComponent("mytarget.ti1").path, workDir.appendingPathComponent("mytarget.ti1").path,
"ICCERY_TEST_WORKDIR": workDir.path, "ICCERY_TEST_WORKDIR": workDir.path,
@@ -218,11 +219,12 @@ final class Milestone2UITests: XCTestCase {
XCTAssertTrue(FileManager.default.fileExists( XCTAssertTrue(FileManager.default.fileExists(
atPath: workDir.appendingPathComponent("mytarget.ti2").path)) atPath: workDir.appendingPathComponent("mytarget.ti2").path))
// Print panel is live from M3; a default printer is selected // Print panel is live from M3. GitHub macos-14 has no system
// so both the all-pages and per-page print buttons are enabled. // queues, so mock CUPS (`ICCERY_CUPS_BIN_DIR`) must enumerate
// before Print All / per-page enable (run 34867767434).
XCTAssertTrue(element("rawPrintPanel").exists) XCTAssertTrue(element("rawPrintPanel").exists)
XCTAssertTrue(app.buttons["btnPrintAll"].isEnabled) _ = waitUntilEnabled("btnPrintAll", timeout: 15)
XCTAssertTrue(app.buttons["btnPrintPage-0"].isEnabled) _ = waitUntilEnabled("btnPrintPage-0", timeout: 15)
XCTAssertTrue(app.buttons["btnAdvanceToStage3"].isEnabled) XCTAssertTrue(app.buttons["btnAdvanceToStage3"].isEnabled)
} }
+35 -22
View File
@@ -98,23 +98,9 @@ final class Milestone4UITests: XCTestCase {
app.buttons["btnDetectInstruments"].click() app.buttons["btnDetectInstruments"].click()
_ = waitFor("chartreadInstrumentSelect", timeout: 20) _ = waitFor("chartreadInstrumentSelect", timeout: 20)
// Keep Auto (port 1) and start the session. // Wait until Start is enabled before clicking. Existence-only
XCTAssertTrue(app.buttons["btnStartRead"].waitForExistence(timeout: 5)) // clicks are no-ops on the disabled control (runs 35251, 35293).
app.buttons["btnStartRead"].click() driveOnePass(startButton: "btnStartRead")
// Calibrate.
let calibrate = element("btnCalibrate")
if !calibrate.waitForExistence(timeout: 25) {
let error = element("chartreadLastError").label
let value = element("chartreadLastError").value as? String ?? "<nil>"
XCTFail("No calibrate button. lastError.label='\(error)' value='\(value)'")
}
app.buttons["btnCalibrate"].click()
// Trigger each strip until all are read Done & Save appears.
driveStripsUntilDone()
XCTAssertTrue(element("btnDoneRead").exists)
app.buttons["btnDoneRead"].firstMatch.click()
// Averaging panel appears with one pass snapshot. // Averaging panel appears with one pass snapshot.
_ = waitFor("chartreadAveragingPanel", timeout: 20) _ = waitFor("chartreadAveragingPanel", timeout: 20)
@@ -168,15 +154,34 @@ final class Milestone4UITests: XCTestCase {
/// Runs the mock handheld chartread session to completion /// Runs the mock handheld chartread session to completion
/// (start calibrate strip A strip B Done & Save). /// (start calibrate strip A strip B Done & Save).
///
/// `isEnabled` can be true in AX while the SwiftUI action is still
/// a no-op (runs 35251, 35443). Re-click until the session is
/// actually running (`btnCancel` is shown whenever
/// `isChartreadRunning`), then wait for Calibrate.
private func driveOnePass(startButton: String) { private func driveOnePass(startButton: String) {
let start = app.buttons[startButton] let start = app.buttons[startButton]
XCTAssertTrue(start.waitForExistence(timeout: 10)) XCTAssertTrue(start.waitForExistence(timeout: 10), startButton)
let deadline = Date().addingTimeInterval(10) let enabledBy = Date().addingTimeInterval(10)
while Date() < deadline, !start.isEnabled { while Date() < enabledBy, !start.isEnabled {
RunLoop.current.run(until: Date().addingTimeInterval(0.1)) RunLoop.current.run(until: Date().addingTimeInterval(0.1))
} }
XCTAssertTrue(start.isEnabled) XCTAssertTrue(start.isEnabled, "\(startButton) never enabled")
start.click()
let sessionBy = Date().addingTimeInterval(25)
while Date() < sessionBy, !chartreadSessionRunning {
if start.exists, start.isEnabled {
start.click()
}
RunLoop.current.run(until: Date().addingTimeInterval(0.4))
}
XCTAssertTrue(
chartreadSessionRunning,
"\(startButton) click never started chartread; "
+ "notice=\(element("noticeText").value as? String ?? "") "
+ "start.exists=\(start.exists)"
)
_ = waitFor("btnCalibrate", timeout: 25) _ = waitFor("btnCalibrate", timeout: 25)
app.buttons["btnCalibrate"].click() app.buttons["btnCalibrate"].click()
driveStripsUntilDone() driveStripsUntilDone()
@@ -184,6 +189,14 @@ final class Milestone4UITests: XCTestCase {
app.buttons["btnDoneRead"].firstMatch.click() app.buttons["btnDoneRead"].firstMatch.click()
} }
/// `btnCancel` is in the tree for the whole chartread session;
/// Calibrate/Trigger only appear after the first classified prompt.
private var chartreadSessionRunning: Bool {
element("btnCancel").exists
|| element("btnCalibrate").exists
|| element("btnTrigger").exists
}
/// Clicks Trigger for each remaining strip until `btnDoneRead` /// Clicks Trigger for each remaining strip until `btnDoneRead`
/// appears the button is re-polled each pass so a click that /// appears the button is re-polled each pass so a click that
/// races a state transition isn't lost. /// races a state transition isn't lost.
-89
View File
@@ -1,89 +0,0 @@
@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
-81
View File
@@ -1,81 +0,0 @@
@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
-83
View File
@@ -1,83 +0,0 @@
@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
-84
View File
@@ -1,84 +0,0 @@
@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
-12
View File
@@ -1,12 +0,0 @@
# 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 (~560780px) 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 12 | [ui-interactive-map-stage1-2.puml](ui-interactive-map-stage1-2.puml) | 625 × 1366 |
| Stages 35 + 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 |
+10
View File
@@ -106,9 +106,19 @@ fi
mkdir -p "$DEST" mkdir -p "$DEST"
cp -R "$BIN_DIR"/. "$DEST"/ cp -R "$BIN_DIR"/. "$DEST"/
# Copy License.txt from archive root (same level as bin/) to Vendor/Argyll/ root
VENDOR_ROOT="$ROOT/Vendor/Argyll"
for license_src in "$EXTRACT"/Argyll_V*/License.txt "$EXTRACT"/License.txt; do
if [ -f "$license_src" ]; then
cp "$license_src" "$VENDOR_ROOT/License.txt"
break
fi
done
find "$DEST" -type f -exec chmod 0755 {} + find "$DEST" -type f -exec chmod 0755 {} +
# Downloads carry com.apple.quarantine; the app cannot spawn quarantined tools. # Downloads carry com.apple.quarantine; the app cannot spawn quarantined tools.
xattr -dr com.apple.quarantine "$DEST" 2>/dev/null || true xattr -dr com.apple.quarantine "$DEST" 2>/dev/null || true
# Also remove quarantine from Vendor/Argyll root if License.txt was copied
xattr -dr com.apple.quarantine "$VENDOR_ROOT" 2>/dev/null || true
# Ad-hoc sign every Mach-O (#165: unsigned arm64 → "Killed: 9"), then # Ad-hoc sign every Mach-O (#165: unsigned arm64 → "Killed: 9"), then
# verify — an unsigned sidecar fails the script. The tree may nest # verify — an unsigned sidecar fails the script. The tree may nest