Skip to content

Commit 8f1e948

Browse files
committed
fix(ring_progress_bar): added hover mouse effect
1 parent 4bb8e86 commit 8f1e948

File tree

2 files changed

+148
-4
lines changed

2 files changed

+148
-4
lines changed

bec_widgets/widgets/progress/ring_progress_bar/ring.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@ def __init__(self, parent: RingProgressContainerWidget | None = None, client=Non
8282
self.registered_slot: tuple[Callable, str | EndpointInfo] | None = None
8383
self.RID = None
8484
self._gap = 5
85+
self._hovered = False
86+
self._hover_progress = 0.0
87+
self._hover_animation = QtCore.QPropertyAnimation(self, b"hover_progress")
88+
self._hover_animation.setDuration(180)
89+
easing_curve = (
90+
QtCore.QEasingCurve.Type.OutCubic
91+
if hasattr(QtCore.QEasingCurve, "Type")
92+
else QtCore.QEasingCurve.OutCubic
93+
)
94+
self._hover_animation.setEasingCurve(easing_curve)
8595
self.set_start_angle(self.config.start_position)
8696

8797
def set_value(self, value: int | float):
@@ -424,22 +434,37 @@ def paintEvent(self, event):
424434
rect.adjust(max_ring_size, max_ring_size, -max_ring_size, -max_ring_size)
425435

426436
# Background arc
437+
base_line_width = float(self.config.line_width)
438+
hover_line_delta = min(3.0, round(base_line_width * 0.6, 1))
439+
current_line_width = base_line_width + (hover_line_delta * self._hover_progress)
427440
painter.setPen(
428-
QtGui.QPen(self._background_color, self.config.line_width, QtCore.Qt.PenStyle.SolidLine)
441+
QtGui.QPen(self._background_color, current_line_width, QtCore.Qt.PenStyle.SolidLine)
429442
)
430443

431444
gap: int = self.gap # type: ignore
432445

433446
# Important: Qt uses a 16th of a degree for angles. start_position is therefore multiplied by 16.
434447
start_position: float = self.config.start_position * 16 # type: ignore
435448

436-
adjusted_rect = QtCore.QRect(
449+
adjusted_rect = QtCore.QRectF(
437450
rect.left() + gap, rect.top() + gap, rect.width() - 2 * gap, rect.height() - 2 * gap
438451
)
452+
if self._hover_progress > 0.0:
453+
hover_radius_delta = 4.0
454+
base_radius = adjusted_rect.width() / 2
455+
if base_radius > 0:
456+
target_radius = base_radius + (hover_radius_delta * self._hover_progress)
457+
scale = target_radius / base_radius
458+
center = adjusted_rect.center()
459+
new_width = adjusted_rect.width() * scale
460+
new_height = adjusted_rect.height() * scale
461+
adjusted_rect = QtCore.QRectF(
462+
center.x() - new_width / 2, center.y() - new_height / 2, new_width, new_height
463+
)
439464
painter.drawArc(adjusted_rect, start_position, 360 * 16)
440465

441466
# Foreground arc
442-
pen = QtGui.QPen(self.color, self.config.line_width, QtCore.Qt.PenStyle.SolidLine)
467+
pen = QtGui.QPen(self.color, current_line_width, QtCore.Qt.PenStyle.SolidLine)
443468
pen.setCapStyle(QtCore.Qt.PenCapStyle.RoundCap)
444469
painter.setPen(pen)
445470
proportion = (self.config.value - self.config.min_value) / (
@@ -449,6 +474,15 @@ def paintEvent(self, event):
449474
painter.drawArc(adjusted_rect, start_position, angle)
450475
painter.end()
451476

477+
def set_hovered(self, hovered: bool):
478+
if hovered == self._hovered:
479+
return
480+
self._hovered = hovered
481+
self._hover_animation.stop()
482+
self._hover_animation.setStartValue(self._hover_progress)
483+
self._hover_animation.setEndValue(1.0 if hovered else 0.0)
484+
self._hover_animation.start()
485+
452486
def convert_color(self, color: str | tuple | QColor) -> QColor:
453487
"""
454488
Convert the color to QColor
@@ -522,6 +556,8 @@ def value(self, value: float):
522556
float(max(self.config.min_value, min(self.config.max_value, value))),
523557
self.config.precision,
524558
)
559+
if self.progress_container and self.progress_container.is_ring_hovered(self):
560+
self.progress_container._update_hover_tooltip(self)
525561
self.update()
526562

527563
@SafeProperty(float)
@@ -602,6 +638,15 @@ def direction(self, value: int):
602638
self.config.direction = value
603639
self.update()
604640

641+
@SafeProperty(float)
642+
def hover_progress(self) -> float:
643+
return self._hover_progress
644+
645+
@hover_progress.setter
646+
def hover_progress(self, value: float):
647+
self._hover_progress = value
648+
self.update()
649+
605650

606651
if __name__ == "__main__": # pragma: no cover
607652
import sys

bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33

44
import pyqtgraph as pg
55
from bec_lib.logger import bec_logger
6-
from qtpy.QtCore import QSize, Qt
6+
from qtpy.QtCore import QPointF, QSize, Qt
7+
from qtpy.QtWidgets import QToolTip
78
from qtpy.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget
89

910
from bec_widgets.utils import Colors
@@ -29,7 +30,9 @@ def __init__(self, parent: QWidget | None = None, **kwargs):
2930
self.rings: list[Ring] = []
3031
self.gap = 20 # Gap between rings
3132
self.color_map: str = "turbo"
33+
self._hovered_ring: Ring | None = None
3234
self.setLayout(QHBoxLayout())
35+
self.setMouseTracking(True)
3336
self.initialize_bars()
3437
self.initialize_center_label()
3538

@@ -59,6 +62,7 @@ def add_ring(self, config: dict | None = None) -> Ring:
5962
"""
6063
ring = Ring(parent=self)
6164
ring.setGeometry(self.rect())
65+
ring.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
6266
ring.gap = self.gap * len(self.rings)
6367
ring.set_value(0)
6468
self.rings.append(ring)
@@ -106,6 +110,7 @@ def initialize_center_label(self):
106110

107111
self.center_label = QLabel("", parent=self)
108112
self.center_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
113+
self.center_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
109114
layout.addWidget(self.center_label)
110115

111116
def _calculate_minimum_size(self):
@@ -150,6 +155,100 @@ def resizeEvent(self, event):
150155
for ring in self.rings:
151156
ring.setGeometry(self.rect())
152157

158+
def mouseMoveEvent(self, event):
159+
pos = event.position() if hasattr(event, "position") else QPointF(event.pos())
160+
ring = self._ring_at_pos(pos)
161+
self._set_hovered_ring(ring, event)
162+
super().mouseMoveEvent(event)
163+
164+
def leaveEvent(self, event):
165+
self._set_hovered_ring(None, event)
166+
super().leaveEvent(event)
167+
168+
def _set_hovered_ring(self, ring: Ring | None, event=None):
169+
if ring is self._hovered_ring:
170+
if ring is not None:
171+
self._update_hover_tooltip(ring, event)
172+
return
173+
if self._hovered_ring is not None:
174+
self._hovered_ring.set_hovered(False)
175+
self._hovered_ring = ring
176+
if self._hovered_ring is not None:
177+
self._hovered_ring.set_hovered(True)
178+
self._update_hover_tooltip(self._hovered_ring, event)
179+
else:
180+
QToolTip.hideText()
181+
182+
def _ring_at_pos(self, pos: QPointF) -> Ring | None:
183+
if not self.rings:
184+
return None
185+
size = min(self.width(), self.height())
186+
if size <= 0:
187+
return None
188+
x_offset = (self.width() - size) / 2
189+
y_offset = (self.height() - size) / 2
190+
center_x = x_offset + size / 2
191+
center_y = y_offset + size / 2
192+
dx = pos.x() - center_x
193+
dy = pos.y() - center_y
194+
distance = (dx * dx + dy * dy) ** 0.5
195+
196+
max_ring_size = self.get_max_ring_size()
197+
base_radius = (size - 2 * max_ring_size) / 2
198+
if base_radius <= 0:
199+
return None
200+
201+
best_ring: Ring | None = None
202+
best_delta: float | None = None
203+
for ring in self.rings:
204+
radius = base_radius - ring.gap
205+
if radius <= 0:
206+
continue
207+
half_width = ring.config.line_width / 2
208+
inner = radius - half_width
209+
outer = radius + half_width
210+
if inner <= distance <= outer:
211+
delta = abs(distance - radius)
212+
if best_delta is None or delta < best_delta:
213+
best_delta = delta
214+
best_ring = ring
215+
216+
return best_ring
217+
218+
def is_ring_hovered(self, ring: Ring) -> bool:
219+
return ring is self._hovered_ring
220+
221+
def _update_hover_tooltip(self, ring: Ring, event=None):
222+
text = self._build_tooltip_text(ring)
223+
if event is not None:
224+
global_pos = (
225+
event.globalPosition().toPoint()
226+
if hasattr(event, "globalPosition")
227+
else event.globalPos()
228+
)
229+
QToolTip.showText(global_pos, text, self)
230+
else:
231+
self.setToolTip(text)
232+
233+
def _build_tooltip_text(self, ring: Ring) -> str:
234+
mode = ring.config.mode
235+
mode_label = {"manual": "Manual", "scan": "Scan progress", "device": "Device"}.get(
236+
mode, mode
237+
)
238+
239+
precision = int(ring.config.precision)
240+
value = f"{ring.config.value:.{precision}f}"
241+
max_value = f"{ring.config.max_value:.{precision}f}"
242+
243+
lines = [f"Mode: {mode_label}", f"Value: {value} / {max_value}"]
244+
if mode == "device" and ring.config.device:
245+
if ring.config.signal:
246+
lines.append(f"Device: {ring.config.device}:{ring.config.signal}")
247+
else:
248+
lines.append(f"Device: {ring.config.device}")
249+
250+
return "\n".join(lines)
251+
153252
def set_colors_from_map(self, colormap, color_format: Literal["RGB", "HEX"] = "RGB"):
154253
"""
155254
Set the colors for the progress bars from a colormap.

0 commit comments

Comments
 (0)