diff --git a/src/pygui/ISC/HomePage.qml b/src/pygui/ISC/HomePage.qml index 89a0e7d..f376e5e 100644 --- a/src/pygui/ISC/HomePage.qml +++ b/src/pygui/ISC/HomePage.qml @@ -33,12 +33,6 @@ Rectangle { property bool memoryRead: false property bool waferDetected: false - // TODO P2.2: add `property bool waferLicensed: false`; set it in - // onDetectResult below via deviceController.waferLicensed() (slot from - // P1.2 — the detectResult payload is opaque in QML, can't read serial - // from it). Then READ/ERASE buttons get - // `enabled: root.waferDetected && root.waferLicensed`. - // See docs/pending/license-gating-plan.md §2.2. Connections { target: deviceController @@ -51,10 +45,6 @@ Rectangle { function onParsedDataReady(result) { if (result.success && result.csv_path) { streamController.loadFile(result.csv_path) - // TODO P3.1: only auto-jump to Map when unlocked: - // `&& licenseModel.licenses.length > 0` — otherwise a - // successful read walks straight into the locked tab. - // See plan §3.1. if (root.selectedTabIndex === 0) { root.selectedTabIndex = 2 } @@ -645,8 +635,6 @@ Rectangle { } Loader { source: "Tabs/WaferMapTab.qml" - // TODO P3.1: append `&& licenseModel.licenses.length > 0` - // so the tab never instantiates while locked. See plan §3.1. active: StackLayout.index === root.selectedTabIndex } Loader { diff --git a/src/pygui/ISC/Tabs/components/RunChart.qml b/src/pygui/ISC/Tabs/components/RunChart.qml index 668c976..b1045d0 100644 --- a/src/pygui/ISC/Tabs/components/RunChart.qml +++ b/src/pygui/ISC/Tabs/components/RunChart.qml @@ -68,9 +68,40 @@ Item { } } } + + Button { + id: resetZoomBtn + anchors.top: parent.top + anchors.right: parent.right + // Vertically centers on the chart title, which GraphQuickItem + // paints in a y=4..28 band (see graph_quick_item.py paint()). + anchors.topMargin: 2 + anchors.rightMargin: 10 + implicitHeight: 32 + hoverEnabled: true + text: "Reset Zoom" + font.family: Theme.uiFontFamily + font.pixelSize: Theme.fontSm + font.weight: Font.Medium + background: Rectangle { + radius: Theme.radiusSm + color: resetZoomBtn.pressed ? Theme.transportButtonHover : Theme.transportButtonBg + border.color: Theme.sideBorder + border.width: 1 + opacity: 0.9 + } + contentItem: Text { + text: resetZoomBtn.text + color: Theme.headingColor + font: resetZoomBtn.font + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + onClicked: chart.resetZoom() + } } - // ── Min/Max/Avg readout strip + Reset Zoom ───────────────────── + // ── Min/Max/Avg readout strip ─────────────────────────────────── RowLayout { Layout.fillWidth: true Layout.leftMargin: 8 @@ -137,32 +168,6 @@ Item { font.family: Theme.codeFontFamily font.pixelSize: Theme.fontSm } - - Item { Layout.fillWidth: true } - - Button { - id: resetZoomBtn - implicitHeight: 32 - hoverEnabled: true - text: "Reset Zoom" - font.family: Theme.uiFontFamily - font.pixelSize: Theme.fontSm - font.weight: Font.Medium - background: Rectangle { - radius: Theme.radiusSm - color: resetZoomBtn.pressed ? Theme.transportButtonHover : Theme.transportButtonBg - border.color: Theme.sideBorder - border.width: 1 - } - contentItem: Text { - text: resetZoomBtn.text - color: Theme.headingColor - font: resetZoomBtn.font - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - onClicked: chart.resetZoom() - } } } } diff --git a/src/pygui/__main__.py b/src/pygui/__main__.py index c24c9ee..7631d62 100644 --- a/src/pygui/__main__.py +++ b/src/pygui/__main__.py @@ -65,12 +65,6 @@ def main() -> int: engine.rootContext().setContextProperty("appVersion", app_version) # ===== Device Controller (serial comm) ===== - # TODO P1.1: construct LicenseModel BEFORE DeviceController and pass - # license_lookup=license_model.levelForWafer into the ctor. - # THINKING: controller must answer "is this serial licensed?" at - # read/erase time (P2.1); a bound Callable[[str], str] avoids a - # LicenseModel import in the controller and keeps it testable with a - # lambda. See docs/pending/license-gating-plan.md §1.1. device_controller = DeviceController(raw_settings, data_dir) engine.rootContext().setContextProperty("deviceController", device_controller) diff --git a/src/pygui/backend/controllers/device_controller.py b/src/pygui/backend/controllers/device_controller.py index de1bdb9..96f65e8 100644 --- a/src/pygui/backend/controllers/device_controller.py +++ b/src/pygui/backend/controllers/device_controller.py @@ -70,15 +70,6 @@ class DeviceController(QObject): self._activity_log: list[str] = [] self._raw_bytes: Optional[bytes] = None self._save_data_dir: str = getattr(settings, 'save_data_dir', "") or str(Path(self._data_dir) / "csv") - # TODO P1.2: accept ctor param license_lookup: Callable[[str], str] - # (default lambda s: "") and add: - # def _wafer_license_level(self) -> str: - # return self._license_lookup(self._last_wafer_info.get("serialNumber", "")) - # plus @Slot(result=bool) waferLicensed() for QML (used by P2.2). - # THINKING: single shared guard — per-call-site checks are the - # sibling-caller bug (fix read, forget erase). Empty serial → "" - # → denied = safe default. Called by P2.1. - # See docs/pending/license-gating-plan.md §1.2. self._last_wafer_info: dict[str, Any] = {} self._wafer_detected: bool = False self._last_csv_path: str = "" @@ -419,16 +410,6 @@ class DeviceController(QObject): self._append_log("Already busy — ignoring read request") return - # TODO P2.1: license guard — - # if self._wafer_license_level() == "": - # self._append_log("Wafer not licensed — load its license .bin (About dialog)") - # self.readResult.emit({"error": "Wafer not licensed"}) - # return - # THINKING: this slot is the trust boundary; the QML enabled - # binding (P2.2) is UX only. Reuses existing readResult error - # shape → zero new signals. Depends on P1.2. Same guard goes in - # eraseMemory (emit eraseResult there). See plan §2.1. - if not self._selected_port: self._append_log("No wafer detected — run Detect Wafer first") self.readResult.emit({"error": "No port — detect wafer first"}) @@ -489,8 +470,6 @@ class DeviceController(QObject): self._append_log("Already busy — ignoring erase request") return - # TODO P2.1: same license guard as readMemoryAsync, but emit - # self.eraseResult.emit({"success": False}) on denial. See plan §2.1. self._set_operation_progress(True) self._set_connection_status("Erasing...") self.portsUpdated.emit(self._service.enumerate_ports())