diff --git a/.gitignore b/.gitignore index df65550..3ae3eb0 100644 --- a/.gitignore +++ b/.gitignore @@ -37,7 +37,6 @@ moc_*.cpp # Build / packaging artifacts build/ dist/ -*.spec *.icns *.ico diff --git a/src/pygui/ISC/AboutDialog.qml b/src/pygui/ISC/AboutDialog.qml index 386efab..f569721 100644 --- a/src/pygui/ISC/AboutDialog.qml +++ b/src/pygui/ISC/AboutDialog.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Dialogs import QtQuick.Layouts import ISC +import ISC.Tabs.components Popup { id: root @@ -133,13 +134,28 @@ Popup { Label { text: modelData.mfgDate; font.pixelSize: Theme.fontMd; color: Theme.bodyColor; Layout.preferredWidth: 120 } Label { text: modelData.level; font.pixelSize: Theme.fontMd; color: Theme.bodyColor; Layout.preferredWidth: 80 } Label { text: modelData.licDate; font.pixelSize: Theme.fontMd; color: Theme.bodyColor; Layout.fillWidth: true } - ToolButton { + Button { + id: removeLicenseBtn Layout.preferredWidth: 28 - text: "✕" - font.pixelSize: Theme.fontSm - ToolTip.visible: hovered - ToolTip.text: "Remove license" + implicitHeight: 28 + hoverEnabled: true + background: Rectangle { + color: removeLicenseBtn.pressed ? Theme.buttonPressed : removeLicenseBtn.hovered ? Theme.buttonNeutralHover : "transparent" + radius: Theme.radiusSm + } + contentItem: Text { + text: "✕" + font.pixelSize: Theme.fontSm + color: Theme.bodyColor + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } onClicked: licenseModel.removeLicense(modelData.serial, modelData.level) + + AppToolTip { + visible: removeLicenseBtn.hovered + text: "Remove license" + } } } } diff --git a/src/pygui/ISC/HomePage.qml b/src/pygui/ISC/HomePage.qml index 0570a72..21e5432 100644 --- a/src/pygui/ISC/HomePage.qml +++ b/src/pygui/ISC/HomePage.qml @@ -124,16 +124,22 @@ Rectangle { licenseModel.startReplayTrial() } } - function onParsedDataReady(result) { - if (result.success && result.csv_path) { - streamController.loadFile(result.csv_path) - // Fires while still on Status(0), before the tab switch below — - // the generic per-tab tracker only attributes picks made while - // already on Map/Graph, so record this one explicitly. - root.mapReviewFile = result.csv_path - if (root.selectedTabIndex === 0 && licenseModel.licenses.length > 0) { + } + Connections { + // Deferred until the post-read metadata dialog (StatusTab.qml) closes — + // jumping tabs from onParsedDataReady directly would deactivate the + // Status Loader (StackLayout.index !== selectedTabIndex) and tear down + // the dialog before the technician could see or use it. + target: statusTabLoader.item + function onCsvMetadataDialogClosed(csvPath) { + if (!csvPath) return + streamController.loadFile(csvPath) + // Fires while still on Status(0), before the tab switch below — + // the generic per-tab tracker only attributes picks made while + // already on Map/Graph, so record this one explicitly. + root.mapReviewFile = csvPath + if (root.selectedTabIndex === 0 && licenseModel.licenses.length > 0) { root.selectedTabIndex = 2 - } } } } @@ -764,6 +770,7 @@ Rectangle { currentIndex: root.selectedTabIndex Loader { + id: statusTabLoader source: "Tabs/StatusTab.qml" active: StackLayout.index === root.selectedTabIndex } diff --git a/src/pygui/ISC/Tabs/StatusTab.qml b/src/pygui/ISC/Tabs/StatusTab.qml index e680178..ac4882c 100644 --- a/src/pygui/ISC/Tabs/StatusTab.qml +++ b/src/pygui/ISC/Tabs/StatusTab.qml @@ -15,6 +15,12 @@ ColumnLayout { anchors.margins: Theme.panelPadding spacing: 8 + // Fired once the post-read metadata dialog is dismissed (Save or Cancel), + // so HomePage can defer its auto-jump-to-Map until after the technician + // has had a chance to edit metadata — jumping tabs immediately would + // deactivate this Loader's item and kill the dialog mid-display. + signal csvMetadataDialogClosed(string csvPath) + // ── Reusable Inline Components ── component StatCard : Rectangle { property alias value: valueText.text @@ -147,31 +153,127 @@ ColumnLayout { id: editCsvDialog parent: Overlay.overlay modal: true - title: "Edit CSV Metadata" width: 460 anchors.centerIn: parent - standardButtons: Dialog.Save | Dialog.Cancel + standardButtons: Dialog.NoButton + padding: 20 onAccepted: file_browser.saveMetadata(root.csvPath, metaWafer.text, metaDate.text, metaChamber.text, metaNotes.text, false, "") + onClosed: root.csvMetadataDialogClosed(root.csvPath) + + background: Rectangle { + radius: Theme.radiusLg + color: Theme.cardBackground + border.color: Theme.cardBorder + border.width: Theme.borderThin + } + + component MetaField: TextField { + id: metaField + Layout.fillWidth: true + color: Theme.fieldText + placeholderTextColor: Theme.fieldPlaceholder + selectedTextColor: Theme.fieldBackground + selectionColor: Theme.fieldText + font.pixelSize: Theme.fontMd + background: Rectangle { + radius: Theme.radiusXs + color: Theme.fieldBackground + border.width: metaField.activeFocus ? Theme.borderStrong : Theme.borderThin + border.color: metaField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder + Behavior on border.color { + ColorAnimation { duration: Theme.durationFast } + } + } + } ColumnLayout { anchors.fill: parent spacing: 6 + // ── header ─────────────────────────────────────────────── + Label { + text: "EDIT CSV METADATA" + color: Theme.headingColor + font.pixelSize: Theme.fontLg + font.bold: true + font.letterSpacing: 1 + Layout.fillWidth: true + } + Text { text: root.csvPath.split("/").pop() color: Theme.sideMutedText font.pixelSize: Theme.fontXs elide: Text.ElideMiddle Layout.fillWidth: true + Layout.bottomMargin: 4 + } + + Rectangle { + Layout.fillWidth: true + Layout.bottomMargin: 8 + height: 1 + color: Theme.cardBorder + } + + Label { text: "Wafer"; color: Theme.bodyColor; font.pixelSize: Theme.fontXs } + MetaField { id: metaWafer } + Label { text: "Date"; color: Theme.bodyColor; font.pixelSize: Theme.fontXs; Layout.topMargin: 6 } + MetaField { id: metaDate } + Label { text: "Chamber"; color: Theme.bodyColor; font.pixelSize: Theme.fontXs; Layout.topMargin: 6 } + MetaField { id: metaChamber } + Label { text: "Notes"; color: Theme.bodyColor; font.pixelSize: Theme.fontXs; Layout.topMargin: 6 } + MetaField { id: metaNotes } + + // ── buttons ─────────────────────────────────────────────── + RowLayout { + Layout.fillWidth: true + Layout.topMargin: 14 + spacing: 8 + + Button { + id: cancelCsvBtn + text: "Cancel" + Layout.fillWidth: true + hoverEnabled: true + background: Rectangle { + radius: Theme.radiusSm + color: cancelCsvBtn.down ? Theme.buttonNeutralPressed : cancelCsvBtn.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground + border.width: Theme.borderThin + border.color: Theme.fieldBorder + } + contentItem: Text { + text: cancelCsvBtn.text + color: Theme.buttonNeutralText + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.bold: true + font.pixelSize: Theme.fontSm + } + onClicked: editCsvDialog.reject() + } + Button { + id: saveCsvBtn + text: "Save" + Layout.fillWidth: true + hoverEnabled: true + background: Rectangle { + radius: Theme.radiusSm + color: saveCsvBtn.hovered ? Qt.lighter(Theme.primaryAccent, 1.1) : Theme.primaryAccent + border.width: Theme.borderThin + border.color: Theme.primaryAccent + } + contentItem: Text { + text: saveCsvBtn.text + color: Theme.tone100 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.bold: true + font.pixelSize: Theme.fontSm + } + onClicked: editCsvDialog.accept() + } } - Label { text: "Wafer" } - TextField { id: metaWafer; Layout.fillWidth: true } - Label { text: "Date" } - TextField { id: metaDate; Layout.fillWidth: true } - Label { text: "Chamber" } - TextField { id: metaChamber; Layout.fillWidth: true } - Label { text: "Notes" } - TextField { id: metaNotes; Layout.fillWidth: true } } } @@ -214,74 +316,74 @@ ColumnLayout { Item { Layout.fillHeight: true } - RowLayout { + // Badge cluster centers on the full card width; port name overlays + // the right edge independently so a long port string can't drag + // the centered badge off-center (was sharing width via RowLayout). + Item { Layout.fillWidth: true - spacing: 8 + Layout.preferredHeight: 22 - Item { - Layout.fillWidth: true - implicitHeight: 22 - - Row { - anchors.centerIn: parent - spacing: 5 - - Repeater { - model: 4 - Rectangle { - width: 4; height: 4; radius: 2 - anchors.verticalCenter: parent.verticalCenter - color: root.isConnected ? Theme.statusSuccessColor : (root.isBusy ? Theme.statusWarningColor : Theme.sideMutedText) - opacity: (root.isConnected || root.isBusy) ? 1.0 : 0.3 - - SequentialAnimation on opacity { - running: root.isConnected || root.isBusy - loops: Animation.Infinite - NumberAnimation { to: 0.3; duration: 1400 } - NumberAnimation { to: 1.0; duration: 1400 } - } - } - } + Row { + anchors.centerIn: parent + spacing: 5 + Repeater { + model: 4 Rectangle { + width: 4; height: 4; radius: 2 anchors.verticalCenter: parent.verticalCenter - width: statusPillText.implicitWidth + 16 - height: 20 - radius: 10 - color: root.isConnected ? Theme.statusSuccessColor : (root.isBusy ? Theme.statusWarningColor : Theme.statusErrorColor) + color: root.isConnected ? Theme.statusSuccessColor : (root.isBusy ? Theme.statusWarningColor : Theme.sideMutedText) + opacity: (root.isConnected || root.isBusy) ? 1.0 : 0.3 - Text { - id: statusPillText - anchors.centerIn: parent - text: root.isConnected ? "ACTIVE" : (root.isBusy ? deviceController.connectionStatus.replace("...", "").toUpperCase() : "INACTIVE") - color: Theme.statusBadgeText - font.pixelSize: Theme.fontXs - font.weight: Font.Bold - font.family: Theme.uiFontFamily - font.letterSpacing: 0.6 + SequentialAnimation on opacity { + running: root.isConnected || root.isBusy + loops: Animation.Infinite + NumberAnimation { to: 0.3; duration: 1400 } + NumberAnimation { to: 1.0; duration: 1400 } } } + } - Repeater { - model: 4 - Rectangle { - width: 4; height: 4; radius: 2 - anchors.verticalCenter: parent.verticalCenter - color: root.isConnected ? Theme.statusSuccessColor : (root.isBusy ? Theme.statusWarningColor : Theme.sideMutedText) - opacity: (root.isConnected || root.isBusy) ? 1.0 : 0.3 + Rectangle { + anchors.verticalCenter: parent.verticalCenter + width: statusPillText.implicitWidth + 16 + height: 20 + radius: 10 + color: root.isConnected ? Theme.statusSuccessColor : (root.isBusy ? Theme.statusWarningColor : Theme.statusErrorColor) - SequentialAnimation on opacity { - running: root.isConnected || root.isBusy - loops: Animation.Infinite - NumberAnimation { to: 0.3; duration: 1400 } - NumberAnimation { to: 1.0; duration: 1400 } - } + Text { + id: statusPillText + anchors.centerIn: parent + text: root.isConnected ? "ACTIVE" : (root.isBusy ? deviceController.connectionStatus.replace("...", "").toUpperCase() : "INACTIVE") + color: Theme.statusBadgeText + font.pixelSize: Theme.fontXs + font.weight: Font.Bold + font.family: Theme.uiFontFamily + font.letterSpacing: 0.6 + } + } + + Repeater { + model: 4 + Rectangle { + width: 4; height: 4; radius: 2 + anchors.verticalCenter: parent.verticalCenter + color: root.isConnected ? Theme.statusSuccessColor : (root.isBusy ? Theme.statusWarningColor : Theme.sideMutedText) + opacity: (root.isConnected || root.isBusy) ? 1.0 : 0.3 + + SequentialAnimation on opacity { + running: root.isConnected || root.isBusy + loops: Animation.Infinite + NumberAnimation { to: 0.3; duration: 1400 } + NumberAnimation { to: 1.0; duration: 1400 } } } } } Text { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter text: root.portName color: root.isConnected ? Theme.statusSuccessColor : Theme.headingColor font.pixelSize: Theme.fontMd @@ -612,7 +714,7 @@ ColumnLayout { root.dataCols = 0; root.csvPath = ""; if (result && result.success === true) - saveDirDialog.open(); + root.parseAndSavePendingRead(); } } diff --git a/src/pygui/ISC/Tabs/components/OverlapMapCard.qml b/src/pygui/ISC/Tabs/components/OverlapMapCard.qml index a27ef34..f1e46e9 100644 --- a/src/pygui/ISC/Tabs/components/OverlapMapCard.qml +++ b/src/pygui/ISC/Tabs/components/OverlapMapCard.qml @@ -53,6 +53,7 @@ PanelBox { margin: 1.0 blend: card.blendAmount / 100 showLabels: true + showExtremes: false ringColor: Theme.waferRingColor axisColor: Theme.waferAxisColor lowColor: Theme.sensorLow