diff --git a/.github/workflows/ied-simulator-qt.yml b/.github/workflows/ied-simulator-qt.yml index d1b9a7caf..9b7206f85 100644 --- a/.github/workflows/ied-simulator-qt.yml +++ b/.github/workflows/ied-simulator-qt.yml @@ -145,6 +145,9 @@ jobs: apps/ied_simulator/qml/FileWorkspace.qml apps/ied_simulator/qml/Main.qml + - name: Browser ribbon legibility contract + run: python3 apps/ied_simulator/test_browser_ribbon_legibility.py + - name: Browser convergence contract run: >- python3 apps/ied_simulator/test_browser_convergence.py diff --git a/apps/ied_simulator/qml/IedBrowserWorkspace.qml b/apps/ied_simulator/qml/IedBrowserWorkspace.qml index 49f376d3e..b30fafd64 100644 --- a/apps/ied_simulator/qml/IedBrowserWorkspace.qml +++ b/apps/ied_simulator/qml/IedBrowserWorkspace.qml @@ -31,20 +31,20 @@ Item { required property string badge property bool emphasized: false - implicitWidth: 66 - implicitHeight: 64 + implicitWidth: 80 + implicitHeight: 86 hoverEnabled: true - leftPadding: 4 - rightPadding: 4 + leftPadding: 3 + rightPadding: 3 contentItem: Column { - spacing: 3 + spacing: 5 anchors.centerIn: parent Rectangle { anchors.horizontalCenter: parent.horizontalCenter - width: 27 - height: 25 - radius: 4 + width: 37 + height: 35 + radius: 6 color: action.enabled ? (action.emphasized ? root.theme.accent : root.theme.surfaceSoft) : root.theme.surfaceRaised @@ -58,18 +58,22 @@ Item { color: action.enabled ? (action.emphasized ? "#ffffff" : root.theme.textSoft) : root.theme.muted - font.pixelSize: action.badge.length > 2 ? 7 : 9 + font.pixelSize: action.badge.length > 2 ? 12 : 16 font.weight: Font.DemiBold } } Label { anchors.horizontalCenter: parent.horizontalCenter - width: 62 + width: 77 + height: 33 text: action.text color: action.enabled ? root.theme.text : root.theme.muted - font.pixelSize: 8 - font.weight: Font.Medium + font.pixelSize: 11 + font.weight: Font.DemiBold horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + wrapMode: Text.WordWrap + maximumLineCount: 2 elide: Text.ElideRight } } @@ -190,7 +194,7 @@ Item { Rectangle { Layout.fillWidth: true - Layout.preferredHeight: 94 + Layout.preferredHeight: 112 color: root.theme.chrome border.width: 1 border.color: root.theme.lineSoft @@ -210,7 +214,7 @@ Item { spacing: 7 Column { - width: 136 + width: 168 height: parent.height Row { RibbonAction { @@ -234,15 +238,15 @@ Item { anchors.horizontalCenter: parent.horizontalCenter text: "APPLICATION" color: root.theme.muted - font.pixelSize: 7 + font.pixelSize: 10 font.weight: Font.DemiBold } } - Rectangle { width: 1; height: 78; y: 5; color: root.theme.lineSoft } + Rectangle { width: 1; height: 96; y: 8; color: root.theme.lineSoft } Column { - width: 262 + width: 282 height: parent.height spacing: 4 Row { @@ -255,7 +259,7 @@ Item { placeholderText: "IED IP / hostname" selectByMouse: true enabled: !session.configurationLocked - font.pixelSize: 9 + font.pixelSize: 12 onEditingFinished: session.host = text } SpinBox { @@ -284,7 +288,7 @@ Item { width: 244 text: session.stateText + " · " + session.endpoint color: session.lastError.length ? root.theme.red : root.theme.textSoft - font.pixelSize: 8 + font.pixelSize: 11 font.weight: Font.Medium elide: Text.ElideRight } @@ -293,15 +297,15 @@ Item { anchors.horizontalCenter: parent.horizontalCenter text: "ENDPOINT" color: root.theme.muted - font.pixelSize: 7 + font.pixelSize: 10 font.weight: Font.DemiBold } } - Rectangle { width: 1; height: 78; y: 5; color: root.theme.lineSoft } + Rectangle { width: 1; height: 96; y: 8; color: root.theme.lineSoft } Column { - width: 200 + width: 246 height: parent.height Row { RibbonAction { @@ -336,15 +340,15 @@ Item { anchors.horizontalCenter: parent.horizontalCenter text: "IED SESSION" color: root.theme.muted - font.pixelSize: 7 + font.pixelSize: 10 font.weight: Font.DemiBold } } - Rectangle { width: 1; height: 78; y: 5; color: root.theme.lineSoft } + Rectangle { width: 1; height: 96; y: 8; color: root.theme.lineSoft } Column { - width: 200 + width: 246 height: parent.height Row { RibbonAction { @@ -375,15 +379,15 @@ Item { anchors.horizontalCenter: parent.horizontalCenter text: "DATA" color: root.theme.muted - font.pixelSize: 7 + font.pixelSize: 10 font.weight: Font.DemiBold } } - Rectangle { width: 1; height: 78; y: 5; color: root.theme.lineSoft } + Rectangle { width: 1; height: 96; y: 8; color: root.theme.lineSoft } Column { - width: 266 + width: 332 height: parent.height Row { RibbonAction { @@ -425,52 +429,12 @@ Item { anchors.horizontalCenter: parent.horizontalCenter text: "SERVICES" color: root.theme.muted - font.pixelSize: 7 + font.pixelSize: 10 font.weight: Font.DemiBold } } - Rectangle { width: 1; height: 78; y: 5; color: root.theme.lineSoft } - Column { - width: 176 - height: parent.height - y: 6 - spacing: 2 - Label { - width: parent.width - text: "MODEL" - color: root.theme.muted - font.pixelSize: 7 - font.weight: Font.DemiBold - } - Label { - width: parent.width - text: context.iedName.length ? context.iedName - : context.selectionRequired ? "Select active IED" : "IED model" - color: root.theme.text - font.pixelSize: 10 - font.weight: Font.DemiBold - elide: Text.ElideRight - } - Label { - width: parent.width - text: context.loaded - ? context.logicalDeviceCount + " LD · " + context.logicalNodeCount + " LN · " - + context.dataAttributeCount + " DA" - : "Open SCL or discover one endpoint" - color: root.theme.muted - font.pixelSize: 8 - elide: Text.ElideRight - } - Label { - width: parent.width - text: context.online ? "ONLINE" : "OFFLINE" - color: context.online ? root.theme.green : root.theme.muted - font.pixelSize: 8 - font.weight: Font.DemiBold - } - } } } } diff --git a/apps/ied_simulator/test_browser_convergence.py b/apps/ied_simulator/test_browser_convergence.py index 9b76d3ac6..0da5781e7 100644 --- a/apps/ied_simulator/test_browser_convergence.py +++ b/apps/ied_simulator/test_browser_convergence.py @@ -20,8 +20,8 @@ workspace_required = [ 'component RibbonAction: Button', - 'Layout.preferredHeight: 94', - 'text: "MODEL"', + 'Layout.preferredHeight: 112', + 'width: 332', 'text: "COMMANDS"', 'text: "Online"', '"Discover IED"', diff --git a/apps/ied_simulator/test_browser_ribbon_legibility.py b/apps/ied_simulator/test_browser_ribbon_legibility.py new file mode 100644 index 000000000..f388bd221 --- /dev/null +++ b/apps/ied_simulator/test_browser_ribbon_legibility.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""Browser ribbon legibility and ownership contract (no Qt dependencies). + +Guards against regressing to the 7px/8px pseudo-icons seen on field screenshots. +Native Qt CI still renders and starts QML on the exact source commit. +""" +from pathlib import Path + +source = (Path(__file__).resolve().parent / "qml/IedBrowserWorkspace.qml").read_text(encoding="utf-8") +ribbon = source.split("component RibbonAction: Button {", 1)[-1].split( + "function watchedRows()", 1)[0] +section = source.split("id: ribbonRow", 1)[-1].split( + "visible: context.authorityKey", 1)[0] +required = ( + "implicitWidth: 80", + "implicitHeight: 86", + "width: 37", + "height: 35", + "font.pixelSize: action.badge.length > 2 ? 12 : 16", + "font.pixelSize: 11", + "maximumLineCount: 2", + "wrapMode: Text.WordWrap", + "action.hovered ? root.theme.accentSoft", + "color: action.enabled ? root.theme.text : root.theme.muted", +) +if any(item not in ribbon for item in required): + raise SystemExit("BROWSER_RIBBON_FAIL tiny or unreadable operator action") +if "Layout.preferredHeight: 112" not in source: + raise SystemExit("BROWSER_RIBBON_FAIL toolbar clips 86px actions") +for label in ("APPLICATION", "ENDPOINT", "IED SESSION", "DATA", "SERVICES"): + if f'text: "{label}"' not in section: + raise SystemExit("BROWSER_RIBBON_FAIL missing service group " + label) +if 'text: "MODEL"' in section: + raise SystemExit("BROWSER_RIBBON_FAIL duplicate identity chrome") +for item in ( + "onClicked: reports.enableSelected(true)", + "onClicked: reports.disableSelected()", + "session.discoverAndConnect()", + "onClicked: session.disconnectFromIed()", + "onClicked: client.readEngineeringSelected()", +): + if item not in section: + raise SystemExit("BROWSER_RIBBON_FAIL lost live operation " + item) +if "implicitWidth: 66" in ribbon or "font.pixelSize: 8" in ribbon: + raise SystemExit("BROWSER_RIBBON_FAIL retained miniature action") +# Explicit negative regression: an unapproved tiny font must break the gate. +shrunk = ribbon.replace("font.pixelSize: 11", "font.pixelSize: 8", 1) +if "font.pixelSize: 8" not in shrunk: + raise SystemExit("BROWSER_RIBBON_FAIL ineffective tiny-caption regression") +print("BROWSER_RIBBON_PASS legibility=12px_or_larger badges=37px captions=11px actions=80x86")