33
44import pyqtgraph as pg
55from 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
78from qtpy .QtWidgets import QHBoxLayout , QLabel , QVBoxLayout , QWidget
89
910from 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