From 92713a1f7ab9fd082f9da283fc1bd6d939a27a23 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Wed, 28 Jan 2026 10:00:12 +0100 Subject: [PATCH] fix(ring_progress_bar): added hover mouse effect --- .../progress/ring_progress_bar/ring.py | 51 ++++++++- .../ring_progress_bar/ring_progress_bar.py | 102 +++++++++++++++++- 2 files changed, 148 insertions(+), 5 deletions(-) diff --git a/bec_widgets/widgets/progress/ring_progress_bar/ring.py b/bec_widgets/widgets/progress/ring_progress_bar/ring.py index a29641544..d8898318e 100644 --- a/bec_widgets/widgets/progress/ring_progress_bar/ring.py +++ b/bec_widgets/widgets/progress/ring_progress_bar/ring.py @@ -82,6 +82,16 @@ def __init__(self, parent: RingProgressContainerWidget | None = None, client=Non self.registered_slot: tuple[Callable, str | EndpointInfo] | None = None self.RID = None self._gap = 5 + self._hovered = False + self._hover_progress = 0.0 + self._hover_animation = QtCore.QPropertyAnimation(self, b"hover_progress") + self._hover_animation.setDuration(180) + easing_curve = ( + QtCore.QEasingCurve.Type.OutCubic + if hasattr(QtCore.QEasingCurve, "Type") + else QtCore.QEasingCurve.OutCubic + ) + self._hover_animation.setEasingCurve(easing_curve) self.set_start_angle(self.config.start_position) def set_value(self, value: int | float): @@ -424,8 +434,11 @@ def paintEvent(self, event): rect.adjust(max_ring_size, max_ring_size, -max_ring_size, -max_ring_size) # Background arc + base_line_width = float(self.config.line_width) + hover_line_delta = min(3.0, round(base_line_width * 0.6, 1)) + current_line_width = base_line_width + (hover_line_delta * self._hover_progress) painter.setPen( - QtGui.QPen(self._background_color, self.config.line_width, QtCore.Qt.PenStyle.SolidLine) + QtGui.QPen(self._background_color, current_line_width, QtCore.Qt.PenStyle.SolidLine) ) gap: int = self.gap # type: ignore @@ -433,13 +446,25 @@ def paintEvent(self, event): # Important: Qt uses a 16th of a degree for angles. start_position is therefore multiplied by 16. start_position: float = self.config.start_position * 16 # type: ignore - adjusted_rect = QtCore.QRect( + adjusted_rect = QtCore.QRectF( rect.left() + gap, rect.top() + gap, rect.width() - 2 * gap, rect.height() - 2 * gap ) + if self._hover_progress > 0.0: + hover_radius_delta = 4.0 + base_radius = adjusted_rect.width() / 2 + if base_radius > 0: + target_radius = base_radius + (hover_radius_delta * self._hover_progress) + scale = target_radius / base_radius + center = adjusted_rect.center() + new_width = adjusted_rect.width() * scale + new_height = adjusted_rect.height() * scale + adjusted_rect = QtCore.QRectF( + center.x() - new_width / 2, center.y() - new_height / 2, new_width, new_height + ) painter.drawArc(adjusted_rect, start_position, 360 * 16) # Foreground arc - pen = QtGui.QPen(self.color, self.config.line_width, QtCore.Qt.PenStyle.SolidLine) + pen = QtGui.QPen(self.color, current_line_width, QtCore.Qt.PenStyle.SolidLine) pen.setCapStyle(QtCore.Qt.PenCapStyle.RoundCap) painter.setPen(pen) proportion = (self.config.value - self.config.min_value) / ( @@ -449,6 +474,15 @@ def paintEvent(self, event): painter.drawArc(adjusted_rect, start_position, angle) painter.end() + def set_hovered(self, hovered: bool): + if hovered == self._hovered: + return + self._hovered = hovered + self._hover_animation.stop() + self._hover_animation.setStartValue(self._hover_progress) + self._hover_animation.setEndValue(1.0 if hovered else 0.0) + self._hover_animation.start() + def convert_color(self, color: str | tuple | QColor) -> QColor: """ Convert the color to QColor @@ -522,6 +556,8 @@ def value(self, value: float): float(max(self.config.min_value, min(self.config.max_value, value))), self.config.precision, ) + if self.progress_container and self.progress_container.is_ring_hovered(self): + self.progress_container._update_hover_tooltip(self) self.update() @SafeProperty(float) @@ -602,6 +638,15 @@ def direction(self, value: int): self.config.direction = value self.update() + @SafeProperty(float) + def hover_progress(self) -> float: + return self._hover_progress + + @hover_progress.setter + def hover_progress(self, value: float): + self._hover_progress = value + self.update() + if __name__ == "__main__": # pragma: no cover import sys diff --git a/bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py b/bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py index 0a6c8dd8b..4bdb45eb6 100644 --- a/bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py +++ b/bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py @@ -3,8 +3,8 @@ import pyqtgraph as pg from bec_lib.logger import bec_logger -from qtpy.QtCore import QSize, Qt -from qtpy.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget +from qtpy.QtCore import QPointF, QSize, Qt +from qtpy.QtWidgets import QHBoxLayout, QLabel, QToolTip, QVBoxLayout, QWidget from bec_widgets.utils import Colors from bec_widgets.utils.bec_widget import BECWidget @@ -29,7 +29,9 @@ def __init__(self, parent: QWidget | None = None, **kwargs): self.rings: list[Ring] = [] self.gap = 20 # Gap between rings self.color_map: str = "turbo" + self._hovered_ring: Ring | None = None self.setLayout(QHBoxLayout()) + self.setMouseTracking(True) self.initialize_bars() self.initialize_center_label() @@ -59,6 +61,7 @@ def add_ring(self, config: dict | None = None) -> Ring: """ ring = Ring(parent=self) ring.setGeometry(self.rect()) + ring.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True) ring.gap = self.gap * len(self.rings) ring.set_value(0) self.rings.append(ring) @@ -106,6 +109,7 @@ def initialize_center_label(self): self.center_label = QLabel("", parent=self) self.center_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.center_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True) layout.addWidget(self.center_label) def _calculate_minimum_size(self): @@ -150,6 +154,100 @@ def resizeEvent(self, event): for ring in self.rings: ring.setGeometry(self.rect()) + def mouseMoveEvent(self, event): + pos = event.position() if hasattr(event, "position") else QPointF(event.pos()) + ring = self._ring_at_pos(pos) + self._set_hovered_ring(ring, event) + super().mouseMoveEvent(event) + + def leaveEvent(self, event): + self._set_hovered_ring(None, event) + super().leaveEvent(event) + + def _set_hovered_ring(self, ring: Ring | None, event=None): + if ring is self._hovered_ring: + if ring is not None: + self._update_hover_tooltip(ring, event) + return + if self._hovered_ring is not None: + self._hovered_ring.set_hovered(False) + self._hovered_ring = ring + if self._hovered_ring is not None: + self._hovered_ring.set_hovered(True) + self._update_hover_tooltip(self._hovered_ring, event) + else: + QToolTip.hideText() + + def _ring_at_pos(self, pos: QPointF) -> Ring | None: + if not self.rings: + return None + size = min(self.width(), self.height()) + if size <= 0: + return None + x_offset = (self.width() - size) / 2 + y_offset = (self.height() - size) / 2 + center_x = x_offset + size / 2 + center_y = y_offset + size / 2 + dx = pos.x() - center_x + dy = pos.y() - center_y + distance = (dx * dx + dy * dy) ** 0.5 + + max_ring_size = self.get_max_ring_size() + base_radius = (size - 2 * max_ring_size) / 2 + if base_radius <= 0: + return None + + best_ring: Ring | None = None + best_delta: float | None = None + for ring in self.rings: + radius = base_radius - ring.gap + if radius <= 0: + continue + half_width = ring.config.line_width / 2 + inner = radius - half_width + outer = radius + half_width + if inner <= distance <= outer: + delta = abs(distance - radius) + if best_delta is None or delta < best_delta: + best_delta = delta + best_ring = ring + + return best_ring + + def is_ring_hovered(self, ring: Ring) -> bool: + return ring is self._hovered_ring + + def _update_hover_tooltip(self, ring: Ring, event=None): + text = self._build_tooltip_text(ring) + if event is not None: + global_pos = ( + event.globalPosition().toPoint() + if hasattr(event, "globalPosition") + else event.globalPos() + ) + QToolTip.showText(global_pos, text, self) + else: + self.setToolTip(text) + + def _build_tooltip_text(self, ring: Ring) -> str: + mode = ring.config.mode + mode_label = {"manual": "Manual", "scan": "Scan progress", "device": "Device"}.get( + mode, mode + ) + + precision = int(ring.config.precision) + value = f"{ring.config.value:.{precision}f}" + max_value = f"{ring.config.max_value:.{precision}f}" + + lines = [f"Mode: {mode_label}", f"Value: {value} / {max_value}"] + if mode == "device" and ring.config.device: + if ring.config.signal: + lines.append(f"Device: {ring.config.device}:{ring.config.signal}") + else: + lines.append(f"Device: {ring.config.device}") + + return "\n".join(lines) + def set_colors_from_map(self, colormap, color_format: Literal["RGB", "HEX"] = "RGB"): """ Set the colors for the progress bars from a colormap.