feat: camera session badge and collapsible legend
The camera overlay gains a clickable session badge (opens the session menu at the click position) and a baton gate: watching stays free, operating clicks are blocked when the baton is elsewhere. The legend collapses to a ? badge until clicked - the full box covered too much image. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,7 @@ class SampleCameraImageState(Enum):
|
||||
|
||||
class SampleCameraImageLabel(QGraphicsView):
|
||||
smargon = Signal(SmargonCoordinate)
|
||||
session_badge_clicked = Signal()
|
||||
|
||||
evaluate_grid = Signal()
|
||||
clear_grid = Signal()
|
||||
@@ -107,6 +108,9 @@ class SampleCameraImageLabel(QGraphicsView):
|
||||
self._sam_cam = SampleCameraSettings(exposure=0.1, gain=100.0)
|
||||
self._is_daq_busy = False
|
||||
self._camera_available = True
|
||||
# Baton gate: watching allowed, operating not (main_window drives it).
|
||||
self._operations_allowed = True
|
||||
self._session_badge_rect: QRect | None = None # viewport coords
|
||||
self._last_grid_update_ts = 0.0
|
||||
self._grid_update_min_interval_s = 1.0 / 25.0
|
||||
self._tell_state = None
|
||||
@@ -127,6 +131,10 @@ class SampleCameraImageLabel(QGraphicsView):
|
||||
self._show_target_coordinates = True
|
||||
self._show_overlay_legend = True
|
||||
self._compact_overlay_legend = False
|
||||
# Legend stays collapsed to a "?" badge until clicked — the full box
|
||||
# covers too much of the camera image to be always-on.
|
||||
self._legend_expanded = False
|
||||
self._legend_hit_rect: QRectF | None = None # viewport coords, set on paint
|
||||
self._target_point = None
|
||||
self._target_shape = None
|
||||
self._target_color_name = "Cyan"
|
||||
@@ -199,7 +207,14 @@ class SampleCameraImageLabel(QGraphicsView):
|
||||
)
|
||||
|
||||
def _camera_interaction_enabled(self) -> bool:
|
||||
return self._camera_available
|
||||
# Watching is free; clicking (targets, raster, smargon moves) needs
|
||||
# the camera AND the session baton.
|
||||
return self._camera_available and self._operations_allowed
|
||||
|
||||
@Slot(bool)
|
||||
def set_operations_enabled(self, enabled: bool):
|
||||
self._operations_allowed = enabled
|
||||
self.update()
|
||||
|
||||
@Slot(bool)
|
||||
def set_camera_available(self, available: bool):
|
||||
@@ -303,6 +318,7 @@ class SampleCameraImageLabel(QGraphicsView):
|
||||
painter.restore()
|
||||
|
||||
def _draw_session_overlay(self, painter: QPainter):
|
||||
self._session_badge_rect = None
|
||||
if self._busy_overlay_style is not None:
|
||||
return
|
||||
|
||||
@@ -343,6 +359,8 @@ class SampleCameraImageLabel(QGraphicsView):
|
||||
position_y = int((vh - bg_h) / 2)
|
||||
|
||||
bg_rect = QRect(position_x, position_y, bg_w, bg_h)
|
||||
# Clicking the badge opens the session (grab/request) menu.
|
||||
self._session_badge_rect = bg_rect
|
||||
|
||||
painter.setPen(QPen(qcolor(WHITE, 220)))
|
||||
painter.setBrush(bg_color)
|
||||
@@ -406,6 +424,31 @@ class SampleCameraImageLabel(QGraphicsView):
|
||||
self._scaling()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
# Legend badge first: pure UI affordance, must work even when camera
|
||||
# interaction is disabled (session overlay etc.).
|
||||
if (
|
||||
event.button() == Qt.MouseButton.LeftButton
|
||||
and self._legend_hit_rect is not None
|
||||
and self._legend_hit_rect.contains(
|
||||
QPointF(self.viewport().mapFrom(self, event.pos()))
|
||||
)
|
||||
):
|
||||
self._legend_expanded = not self._legend_expanded
|
||||
self.update()
|
||||
event.accept()
|
||||
return
|
||||
|
||||
# Session badge: the way IN when everything else is gated — must fire
|
||||
# before the interaction-enabled check below.
|
||||
if (
|
||||
event.button() == Qt.MouseButton.LeftButton
|
||||
and self._session_badge_rect is not None
|
||||
and self._session_badge_rect.contains(self.viewport().mapFrom(self, event.pos()))
|
||||
):
|
||||
self.session_badge_clicked.emit()
|
||||
event.accept()
|
||||
return
|
||||
|
||||
if not self._camera_interaction_enabled():
|
||||
if event.button() in (Qt.MouseButton.LeftButton, Qt.MouseButton.RightButton):
|
||||
self._show_camera_unavailable_tooltip(event)
|
||||
@@ -930,10 +973,38 @@ class SampleCameraImageLabel(QGraphicsView):
|
||||
|
||||
return lines
|
||||
|
||||
def _draw_legend_badge(self, painter: QPainter):
|
||||
# ponytail: painted circle, not a real QWidget button — the legend it
|
||||
# toggles is painter-drawn too, and a widget would need layout juggling.
|
||||
diameter = 22
|
||||
rect = QRectF(18, self.viewport().height() - diameter - 18, diameter, diameter)
|
||||
|
||||
painter.save()
|
||||
painter.resetTransform()
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
||||
painter.setPen(QPen(qcolor(WHITE, 60), 1))
|
||||
painter.setBrush(qcolor(LEGEND_BG, 170))
|
||||
painter.drawEllipse(rect)
|
||||
|
||||
font = QFont()
|
||||
font.setPointSize(10)
|
||||
font.setBold(True)
|
||||
painter.setFont(font)
|
||||
painter.setPen(QPen(qcolor(LEGEND_TEXT), 1))
|
||||
painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, "?")
|
||||
painter.restore()
|
||||
|
||||
self._legend_hit_rect = rect
|
||||
|
||||
def _draw_overlay_legend(self, painter: QPainter):
|
||||
self._legend_hit_rect = None
|
||||
if not self._legend_should_show():
|
||||
return
|
||||
|
||||
if not self._legend_expanded:
|
||||
self._draw_legend_badge(painter)
|
||||
return
|
||||
|
||||
painter.save()
|
||||
painter.resetTransform()
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
||||
@@ -959,6 +1030,7 @@ class SampleCameraImageLabel(QGraphicsView):
|
||||
height = len(lines) * line_height + 16
|
||||
|
||||
bg_rect = QRectF(left, max(18, top), width, height)
|
||||
self._legend_hit_rect = bg_rect # click anywhere on the box to collapse
|
||||
painter.setPen(QPen(qcolor(WHITE, 60), 1))
|
||||
painter.setBrush(qcolor(LEGEND_BG, 170))
|
||||
painter.drawRoundedRect(bg_rect, 8, 8)
|
||||
|
||||
@@ -334,7 +334,7 @@ class StatusBar(QStatusBar):
|
||||
|
||||
self.session_label.setText(text)
|
||||
|
||||
def show_session_menu(self):
|
||||
def show_session_menu(self, global_pos: QPoint | None = None):
|
||||
menu = QMenu(self)
|
||||
is_busy = self._status and self._status.busy
|
||||
session_state = self._status.session.session if self._status else SessionsStateEnum.Vacant
|
||||
@@ -413,10 +413,16 @@ class StatusBar(QStatusBar):
|
||||
action_force = menu.addAction("⚠️ Force Take Over")
|
||||
action_force.triggered.connect(self._on_force_session_clicked)
|
||||
|
||||
label_geometry = self.session_label.geometry()
|
||||
menu_width = max(label_geometry.width(), menu.sizeHint().width())
|
||||
menu.move(self.mapToGlobal(label_geometry.topLeft()) - QPoint(0, menu.sizeHint().height()))
|
||||
menu.setFixedWidth(menu_width)
|
||||
if global_pos is not None:
|
||||
# Invoked from the camera's session badge — open at the click.
|
||||
menu.move(global_pos)
|
||||
else:
|
||||
label_geometry = self.session_label.geometry()
|
||||
menu_width = max(label_geometry.width(), menu.sizeHint().width())
|
||||
menu.move(
|
||||
self.mapToGlobal(label_geometry.topLeft()) - QPoint(0, menu.sizeHint().height())
|
||||
)
|
||||
menu.setFixedWidth(menu_width)
|
||||
menu.exec()
|
||||
|
||||
def show_pgroup_menu(self):
|
||||
|
||||
Reference in New Issue
Block a user