diff --git a/src/pygui/ISC/Tabs/components/ReplaceSensorDialog.qml b/src/pygui/ISC/Tabs/components/ReplaceSensorDialog.qml index cf43116..5c1ea37 100644 --- a/src/pygui/ISC/Tabs/components/ReplaceSensorDialog.qml +++ b/src/pygui/ISC/Tabs/components/ReplaceSensorDialog.qml @@ -10,6 +10,10 @@ Dialog { standardButtons: Dialog.NoButton width: 300 + // Anchor to the top-left of the window instead of the default centered position. + x: 24 + y: 24 + // called by WaferMapView's TapHandler property int sensorIndex: -1 property string sensorLabel: "" diff --git a/src/pygui/ISC/Tabs/components/TransportBar.qml b/src/pygui/ISC/Tabs/components/TransportBar.qml index 51b7516..1f03d9d 100644 --- a/src/pygui/ISC/Tabs/components/TransportBar.qml +++ b/src/pygui/ISC/Tabs/components/TransportBar.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Controls +import QtQuick.Controls.impl import QtQuick.Layouts import ISC @@ -177,8 +178,6 @@ Item { implicitWidth: 56 implicitHeight: 56 hoverEnabled: true - text: "⏮" - font.pixelSize: 24 background: Rectangle { radius: 8 color: skipStartBtn.pressed ? Theme.transportButtonHover @@ -188,12 +187,13 @@ Item { ColorAnimation { duration: Theme.durationFast } } } - contentItem: Text { - text: parent.text + contentItem: IconImage { + source: "../icons/prev.svg" + width: 22; height: 22 + sourceSize.width: 22 + sourceSize.height: 22 color: Theme.headingColor - font: parent.font - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter + anchors.centerIn: parent } onClicked: streamController.step(-1) } @@ -205,8 +205,6 @@ Item { implicitWidth: 56 implicitHeight: 56 hoverEnabled: true - text: "■" - font.pixelSize: 22 background: Rectangle { radius: 8 color: reviewStopBtn.pressed ? Theme.transportButtonHover @@ -216,12 +214,13 @@ Item { ColorAnimation { duration: Theme.durationFast } } } - contentItem: Text { - text: parent.text + contentItem: IconImage { + source: "../icons/stop.svg" + width: 20; height: 20 + sourceSize.width: 20 + sourceSize.height: 20 color: Theme.headingColor - font: parent.font - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter + anchors.centerIn: parent } onClicked: streamController.stop() } @@ -233,8 +232,6 @@ Item { implicitWidth: 64 implicitHeight: 56 hoverEnabled: true - text: streamController.playing ? "⏸" : "▶" - font.pixelSize: 24 background: Rectangle { radius: 8 color: playPauseBtn.pressed ? Theme.transportButtonHover @@ -244,13 +241,13 @@ Item { ColorAnimation { duration: Theme.durationFast } } } - contentItem: Text { - text: parent.text + contentItem: IconImage { + source: streamController.playing ? "../icons/pause.svg" : "../icons/play.svg" + width: 22; height: 22 + sourceSize.width: 22 + sourceSize.height: 22 color: Theme.headingColor - font: parent.font - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - leftPadding: parent.text === "▶" ? 4 : 0 + anchors.centerIn: parent } onClicked: { if (streamController.playing) { @@ -268,8 +265,6 @@ Item { implicitWidth: 56 implicitHeight: 56 hoverEnabled: true - text: "⏭" - font.pixelSize: 24 background: Rectangle { radius: 8 color: skipEndBtn.pressed ? Theme.transportButtonHover @@ -279,12 +274,13 @@ Item { ColorAnimation { duration: Theme.durationFast } } } - contentItem: Text { - text: parent.text + contentItem: IconImage { + source: "../icons/next.svg" + width: 22; height: 22 + sourceSize.width: 22 + sourceSize.height: 22 color: Theme.headingColor - font: parent.font - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter + anchors.centerIn: parent } onClicked: streamController.step(1) } diff --git a/src/pygui/ISC/Tabs/components/WaferMapView.qml b/src/pygui/ISC/Tabs/components/WaferMapView.qml index ba4bce9..f65258d 100644 --- a/src/pygui/ISC/Tabs/components/WaferMapView.qml +++ b/src/pygui/ISC/Tabs/components/WaferMapView.qml @@ -29,6 +29,22 @@ Item { highColor: Theme.sensorHigh textColor: Theme.headingColor + // Grows the hovered marker smoothly; hoverScale setter triggers a repaint. + hoverScale: 1.0 + 0.35 * hoverPulse + property real hoverPulse: map.hoveredIndex >= 0 ? 1.0 : 0.0 + Behavior on hoverPulse { + NumberAnimation { duration: 150; easing.type: Easing.OutQuad } + } + + HoverHandler { + id: hoverHandler + onPointChanged: { + map.hoveredIndex = map.which_marker(hoverHandler.point.position.x, hoverHandler.point.position.y); + } + onHoveredChanged: if (!hoverHandler.hovered) map.hoveredIndex = -1 + cursorShape: map.hoveredIndex >= 0 ? Qt.PointingHandCursor : Qt.ArrowCursor + } + TapHandler { onTapped: ev => { var idx = map.which_marker(ev.position.x, ev.position.y); diff --git a/src/pygui/ISC/Tabs/icons/back.svg b/src/pygui/ISC/Tabs/icons/back.svg new file mode 100644 index 0000000..b40bff8 --- /dev/null +++ b/src/pygui/ISC/Tabs/icons/back.svg @@ -0,0 +1,14 @@ + + + + diff --git a/src/pygui/ISC/Tabs/icons/next.svg b/src/pygui/ISC/Tabs/icons/next.svg new file mode 100644 index 0000000..db62a0c --- /dev/null +++ b/src/pygui/ISC/Tabs/icons/next.svg @@ -0,0 +1,13 @@ + + + diff --git a/src/pygui/ISC/Tabs/icons/pause.svg b/src/pygui/ISC/Tabs/icons/pause.svg new file mode 100644 index 0000000..47b0c5c --- /dev/null +++ b/src/pygui/ISC/Tabs/icons/pause.svg @@ -0,0 +1,14 @@ + + + + diff --git a/src/pygui/ISC/Tabs/icons/play.svg b/src/pygui/ISC/Tabs/icons/play.svg new file mode 100644 index 0000000..5bd511c --- /dev/null +++ b/src/pygui/ISC/Tabs/icons/play.svg @@ -0,0 +1,13 @@ + + + diff --git a/src/pygui/ISC/Tabs/icons/prev.svg b/src/pygui/ISC/Tabs/icons/prev.svg new file mode 100644 index 0000000..056d5ab --- /dev/null +++ b/src/pygui/ISC/Tabs/icons/prev.svg @@ -0,0 +1,13 @@ + + + diff --git a/src/pygui/ISC/Tabs/icons/stop.svg b/src/pygui/ISC/Tabs/icons/stop.svg new file mode 100644 index 0000000..7862733 --- /dev/null +++ b/src/pygui/ISC/Tabs/icons/stop.svg @@ -0,0 +1,13 @@ + + + diff --git a/src/pygui/backend/visualization/wafer_map_item.py b/src/pygui/backend/visualization/wafer_map_item.py index 96310cc..a74d086 100644 --- a/src/pygui/backend/visualization/wafer_map_item.py +++ b/src/pygui/backend/visualization/wafer_map_item.py @@ -52,6 +52,8 @@ class WaferMapItem(QQuickPaintedItem): sizeChanged = Signal() thicknessChanged = Signal() showThicknessChanged = Signal() + hoveredIndexChanged = Signal() + hoverScaleChanged = Signal() def __init__(self, parent=None): @@ -68,6 +70,10 @@ class WaferMapItem(QQuickPaintedItem): self._thickness_data: list[float] = [] self._show_thickness: bool = False self._thickness_heatmap:QImage | None = None + # Hover highlight: index of the marker under the pointer (-1 = none) + # and a 1.0->~1.5 grow factor driven by a QML Behavior for a smooth animation. + self._hovered_index: int = -1 + self._hover_scale: float = 1.0 # Dark-theme color defaults (match Theme.qml tokens) self._ring_color = QColor("#2A3441") # waferRingColor (toneBorder) @@ -81,6 +87,12 @@ class WaferMapItem(QQuickPaintedItem): self._markers: dict[int, tuple[int, int]] = {} # sensor index → (px, py) self._marker_r: int = 4 self._heatmap: QImage | None = None + # Size the markers/heatmap were last computed against. On first activation + # (e.g. a freshly-created Loader item) width()/height() can still be 0/stale + # when `sensors` is first set, computing degenerate marker positions that + # never get retried. paint() re-checks this every frame so geometry that + # settles after property assignment still gets picked up. + self._last_draw_size: int = -1 self.widthChanged.connect(self._on_resize) self.heightChanged.connect(self._on_resize) @@ -213,6 +225,29 @@ class WaferMapItem(QQuickPaintedItem): self.showThicknessChanged.emit() self.update() + @Property(int, notify=hoveredIndexChanged) + def hoveredIndex(self) -> int: + return self._hovered_index + + @hoveredIndex.setter + def hoveredIndex(self, val: int) -> None: + val = int(val) + if val == self._hovered_index: + return + self._hovered_index = val + self.hoveredIndexChanged.emit() + self.update() + + @Property(float, notify=hoverScaleChanged) + def hoverScale(self) -> float: + return self._hover_scale + + @hoverScale.setter + def hoverScale(self, val: float) -> None: + self._hover_scale = float(val) + self.hoverScaleChanged.emit() + self.update() + # Colour properties — QML can bind these to Theme tokens @Property(QColor, notify=colorsChanged) def ringColor(self) -> QColor: return self._ring_color @@ -440,7 +475,15 @@ class WaferMapItem(QQuickPaintedItem): # ── paint ───────────────────────────────────────────────────────────── def paint(self, painter: QPainter) -> None: - ds = self._draw_size() + ds = self._draw_size() + if ds != self._last_draw_size: + # Geometry settled (or changed) since markers/heatmap were last built + # against a stale size — recompute now, at actual paint time. + self._last_draw_size = ds + self._compute_markers() + self._rebuild_heatmap() + self._rebuild_thickness() + r_px = int(ds / 2 - 4) cx, cy = self._center() @@ -544,10 +587,22 @@ class WaferMapItem(QQuickPaintedItem): self._bands[i] if i < len(self._bands) else "in_range", self._in_range_color, ) + is_hovered = i == self._hovered_index + marker_r = int(round(r * self._hover_scale)) if is_hovered else r + + if is_hovered: + # Soft glow ring behind the marker, growing with hoverScale. + glow_r = int(round(marker_r * 1.9)) + glow_color = QColor(color) + glow_color.setAlpha(70) + painter.setPen(Qt.PenStyle.NoPen) + painter.setBrush(QBrush(glow_color)) + painter.drawEllipse(px - glow_r, py - glow_r, 2 * glow_r, 2 * glow_r) + # Filled circle with thin dark outline for contrast over heatmap painter.setPen(QPen(QColor(0, 0, 0, 100), 1)) painter.setBrush(QBrush(color)) - painter.drawEllipse(px - r, py - r, 2 * r, 2 * r) + painter.drawEllipse(px - marker_r, py - marker_r, 2 * marker_r, 2 * marker_r) if self._show_labels: has_temp = i < len(self._values)