diff --git a/apps/arstack_studio/CMakeLists.txt b/apps/arstack_studio/CMakeLists.txt index 0d660fb43..4a9fa7112 100644 --- a/apps/arstack_studio/CMakeLists.txt +++ b/apps/arstack_studio/CMakeLists.txt @@ -98,6 +98,7 @@ qt_add_qml_module(arstack_studio qml/ConfigurationHub.qml qml/PtpExpertPanel.qml qml/StudioDarkComboBox.qml + qml/StudioDarkCheckBox.qml qml/DeviceExpertPanel.qml qml/FirmwarePanel.qml qml/WaveformExpertPanel.qml @@ -171,21 +172,23 @@ target_compile_definitions(arstack_session_harness PRIVATE ARSTACK_STUDIO_VERSION="${PROJECT_VERSION}" ) +# Use the existing Studio-specific artwork for every branding surface. +# This is intentionally separate from root assets/ used by Workbench. +set(ARSTACK_STUDIO_BRAND_DIR "${CMAKE_CURRENT_LIST_DIR}/../../assets/studio_icon") +qt_add_resources(arstack_studio "studio_branding" + PREFIX "/studio" + BASE "${ARSTACK_STUDIO_BRAND_DIR}" + FILES + "${ARSTACK_STUDIO_BRAND_DIR}/android-chrome-192x192.png" + "${ARSTACK_STUDIO_BRAND_DIR}/android-chrome-512x512.png" +) + if(WIN32) - # One deterministic icon shared by executable, installer and shortcuts. - # Python is already available on supported Windows CI hosts. The icon is - # generated at configure time so RC.exe sees its file before compilation. - find_package(Python3 COMPONENTS Interpreter REQUIRED) + # The exact same embedded icon is copied to stage/ for Inno Setup. + # No Python icon generation or alternative branding paths. set(ARSTACK_STUDIO_ICON "${CMAKE_CURRENT_BINARY_DIR}/ARStackStudio.ico") - execute_process( - COMMAND "${Python3_EXECUTABLE}" - "${CMAKE_CURRENT_LIST_DIR}/assets/make_studio_icon.py" - "${ARSTACK_STUDIO_ICON}" - RESULT_VARIABLE icon_result - ) - if(NOT icon_result EQUAL 0) - message(FATAL_ERROR "Could not generate native ARStack Studio icon") - endif() + configure_file("${ARSTACK_STUDIO_BRAND_DIR}/ARStackStudio.ico" + "${ARSTACK_STUDIO_ICON}" COPYONLY) file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/ARStackStudio.rc" "IDI_ARSTACK ICON \"${ARSTACK_STUDIO_ICON}\"\n") target_sources(arstack_studio PRIVATE diff --git a/apps/arstack_studio/qml/DeviceExpertPanel.qml b/apps/arstack_studio/qml/DeviceExpertPanel.qml index 0470ddf30..84631a638 100644 --- a/apps/arstack_studio/qml/DeviceExpertPanel.qml +++ b/apps/arstack_studio/qml/DeviceExpertPanel.qml @@ -53,7 +53,9 @@ SurfacePanel { RowLayout { Layout.fillWidth: true - ComboBox { + StudioDarkComboBox { + theme: panel.theme + uiFont: panel.uiFont id: recoveryPort Layout.preferredWidth: 170 model: panel.device.ports diff --git a/apps/arstack_studio/qml/ProfileInspector.qml b/apps/arstack_studio/qml/ProfileInspector.qml index cff19bbce..61889cd37 100644 --- a/apps/arstack_studio/qml/ProfileInspector.qml +++ b/apps/arstack_studio/qml/ProfileInspector.qml @@ -117,7 +117,9 @@ SurfacePanel { font.letterSpacing: 1.0 } - ComboBox { + StudioDarkComboBox { + theme: panel.theme + uiFont: panel.uiFont Layout.fillWidth: true model: panel.profiles textRole: "control" @@ -308,7 +310,9 @@ SurfacePanel { wrapMode: Text.WordWrap } - ComboBox { + StudioDarkComboBox { + theme: panel.theme + uiFont: panel.uiFont id: profileFamilySelector Layout.fillWidth: true enabled: panel.session && panel.session.engineeringEditable diff --git a/apps/arstack_studio/qml/PtpExpertPanel.qml b/apps/arstack_studio/qml/PtpExpertPanel.qml index 93870c7db..7cb838094 100644 --- a/apps/arstack_studio/qml/PtpExpertPanel.qml +++ b/apps/arstack_studio/qml/PtpExpertPanel.qml @@ -109,6 +109,53 @@ SurfacePanel { } } Item { Layout.fillWidth: true } + Switch { + id: ptpPower + text: "PTP" + checked: Boolean(panel.device.ptpRunning) + enabled: panel.session && panel.device.ptpAvailable + && !panel.startPending && !panel.session.updatingFirmware + font.family: panel.uiFont + font.pixelSize: 11 + ToolTip.visible: hovered + ToolTip.text: "Start the selected lab PTP role, or stop PTP. State follows verified board telemetry." + onClicked: { + // A Switch flips locally before asynchronous firmware ACK. + // Restore its binding to the sole device-owned state first; + // rejected START/STOP must never advertise the wrong status. + const wantsRunning = checked + checked = Qt.binding(function() { return Boolean(panel.device.ptpRunning) }) + if (wantsRunning) panel.startSelectedMode() + else if (panel.session && !panel.session.requestStopPtp() && panel.controller) + panel.controller.showMessage("PTP stop was not accepted. Copy diagnostics.", true) + } + indicator: Rectangle { + implicitWidth: 46 + implicitHeight: 24 + x: ptpPower.leftPadding + y: (ptpPower.height - height) / 2 + radius: 12 + color: ptpPower.checked ? "#195b44" : panel.theme.surface2 + border.width: 1 + border.color: ptpPower.checked ? panel.theme.green : panel.theme.line + Rectangle { + width: 16 + height: 16 + y: 4 + x: ptpPower.checked ? parent.width - width - 4 : 4 + radius: 8 + color: ptpPower.checked ? panel.theme.green : panel.theme.textSoft + Behavior on x { NumberAnimation { duration: 120 } } + } + } + contentItem: Text { + text: ptpPower.text + color: ptpPower.enabled ? panel.theme.text : panel.theme.muted + font: ptpPower.font + leftPadding: ptpPower.indicator.width + ptpPower.spacing + verticalAlignment: Text.AlignVCenter + } + } StateBadge { theme: panel.theme monoFont: panel.uiFont @@ -279,7 +326,9 @@ SurfacePanel { } } Item { Layout.fillWidth: true } - CheckBox { + StudioDarkCheckBox { + theme: panel.theme + uiFont: panel.uiFont text: "Advanced settings" checked: panel.advancedVisible onToggled: panel.advancedVisible = checked @@ -376,11 +425,11 @@ SurfacePanel { NumericField { id: announceField; theme: panel.theme; monoFont: panel.uiFont; text: "1000"; validator: IntValidator { bottom: 100; top: 10000 } } Label { text: "Sync interval (ms)"; color: panel.theme.muted; font.family: panel.uiFont; font.pixelSize: 9 } NumericField { id: syncField; theme: panel.theme; monoFont: panel.uiFont; text: "1000"; validator: IntValidator { bottom: 20; top: 5000 } } - CheckBox { id: vlanCheck; text: "VLAN"; font.family: panel.uiFont; font.pixelSize: 10; palette.windowText: panel.theme.text } + StudioDarkCheckBox { id: vlanCheck; theme: panel.theme; uiFont: panel.uiFont; text: "VLAN" } NumericField { id: vlanField; enabled: vlanCheck.checked; theme: panel.theme; monoFont: panel.uiFont; text: "100"; validator: IntValidator { bottom: 1; top: 4094 } } Label { text: "VLAN PCP"; color: panel.theme.muted; font.family: panel.uiFont; font.pixelSize: 9 } NumericField { id: pcpField; enabled: vlanCheck.checked; theme: panel.theme; monoFont: panel.uiFont; text: "4"; validator: IntValidator { bottom: 0; top: 7 } } - CheckBox { id: pdelayCheck; text: "Peer-delay response"; checked: true; font.family: panel.uiFont; font.pixelSize: 10; palette.windowText: panel.theme.text } + StudioDarkCheckBox { id: pdelayCheck; theme: panel.theme; uiFont: panel.uiFont; text: "Peer-delay response"; checked: true } CalmButton { theme: panel.theme uiFont: panel.uiFont diff --git a/apps/arstack_studio/qml/StudioDarkCheckBox.qml b/apps/arstack_studio/qml/StudioDarkCheckBox.qml new file mode 100644 index 000000000..eedfce586 --- /dev/null +++ b/apps/arstack_studio/qml/StudioDarkCheckBox.qml @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// One dark, legible checkbox shared across all Studio engineering panels. +import QtQuick +import QtQuick.Controls + +CheckBox { + id: control + property var theme + property string uiFont: "Inter" + implicitHeight: 30 + spacing: 9 + font.family: uiFont + font.pixelSize: 11 + indicator: Rectangle { + implicitWidth: 18 + implicitHeight: 18 + x: control.leftPadding + y: (control.height - height) / 2 + radius: 5 + color: control.checked ? control.theme.accent : control.theme.surface2 + border.width: 1 + border.color: control.checked ? control.theme.accent + : control.activeFocus ? control.theme.textSoft : control.theme.line + Text { + anchors.centerIn: parent + visible: control.checked + text: "\u2713" + color: control.theme.bg + font.pixelSize: 13 + font.weight: Font.Bold + } + } + contentItem: Text { + leftPadding: control.indicator.width + control.spacing + text: control.text + font: control.font + color: control.enabled ? control.theme.textSoft : control.theme.muted + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } +} diff --git a/apps/arstack_studio/qml/StudioDarkComboBox.qml b/apps/arstack_studio/qml/StudioDarkComboBox.qml index 4becf2c49..60f7afb00 100644 --- a/apps/arstack_studio/qml/StudioDarkComboBox.qml +++ b/apps/arstack_studio/qml/StudioDarkComboBox.qml @@ -47,11 +47,13 @@ ComboBox { } delegate: ItemDelegate { + id: choice + required property int index width: combo.width - 8 implicitHeight: 36 highlighted: combo.highlightedIndex === index contentItem: Text { - text: modelData + text: combo.textAt(choice.index) color: combo.theme.text font: combo.font verticalAlignment: Text.AlignVCenter diff --git a/apps/arstack_studio/qml/WaveformExpertPanel.qml b/apps/arstack_studio/qml/WaveformExpertPanel.qml index fdc888d96..cce81ce14 100644 --- a/apps/arstack_studio/qml/WaveformExpertPanel.qml +++ b/apps/arstack_studio/qml/WaveformExpertPanel.qml @@ -35,7 +35,7 @@ SurfacePanel { RowLayout { anchors.fill: parent anchors.margins: 12 - CheckBox { id: enabledCheck; checked: panel.controller.ctSaturationEnabled; text: "Enable CT saturation stress"; font.family: panel.uiFont; font.pixelSize: 10 } + StudioDarkCheckBox { id: enabledCheck; theme: panel.theme; uiFont: panel.uiFont; checked: panel.controller.ctSaturationEnabled; text: "Enable CT saturation stress" } Item { Layout.fillWidth: true } Label { text: panel.controller.ctSaturationEnabled ? "ACTIVE" : "CLEAN SINE"; color: panel.controller.ctSaturationEnabled ? panel.theme.amber : panel.theme.muted; font.family: panel.uiFont; font.pixelSize: 8; font.weight: Font.Bold } } diff --git a/apps/arstack_studio/src/main.cpp b/apps/arstack_studio/src/main.cpp index f208cd705..b74c177f0 100644 --- a/apps/arstack_studio/src/main.cpp +++ b/apps/arstack_studio/src/main.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -438,6 +439,10 @@ int main(int argc, char* argv[]) { QGuiApplication app(argc, argv); app.setQuitOnLastWindowClosed(true); + QIcon studioIcon; + studioIcon.addFile(QStringLiteral(":/studio/android-chrome-192x192.png")); + studioIcon.addFile(QStringLiteral(":/studio/android-chrome-512x512.png")); + app.setWindowIcon(studioIcon); QCoreApplication::setOrganizationName(QStringLiteral("ARStack61850")); #ifdef ARSTACK_STUDIO_ENGINEERING_PREVIEW QCoreApplication::setApplicationName(QStringLiteral("ARStack Studio Preview")); diff --git a/assets/studio_icon/ARStackStudio.ico b/assets/studio_icon/ARStackStudio.ico new file mode 100644 index 000000000..b8289491c Binary files /dev/null and b/assets/studio_icon/ARStackStudio.ico differ