Coverage for main/components/crosshair_graphic_view.py: 95%
66 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-03 15:51 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-03 15:51 +0000
1from PyQt6.QtCore import pyqtSignal, Qt
2from PyQt6.QtGui import QPainter, QPen, QColor, QMouseEvent, QWheelEvent
3from PyQt6.QtWidgets import QGraphicsView
6class CrosshairGraphicsView(QGraphicsView):
7 """Custom QGraphicsView subclass that supports:
8 - Live crosshair display following mouse movements
9 - Coordinate tracking and emission via Qt signals
10 - Integration with a parent viewer for synchronized slice updates
11 """
13 # Signal emitted whenever the mouse moves — sends (view_idx, x, y).
14 coordinate_changed = pyqtSignal(int, int, int)
15 """**Signal(int, int, int):** Emitted whenever the mouse moves within a view.
16 Parameters represent:
17 - `view_idx`: The index of the active view (e.g., axial, coronal, sagittal).
18 - `x`, `y`: The current mouse coordinates within that view.
19 """
21 def __init__(self, view_idx, parent=None):
22 """
23 Initialize the crosshair view.
25 Args:
26 view_idx (int): Index of the view (e.g. axial=0, coronal=1, sagittal=2)
27 parent (QWidget): Parent viewer widget (usually manages multiple CrosshairGraphicsViews)
28 """
29 super().__init__(parent)
30 self.view_idx = view_idx
31 self.parent_viewer = parent
33 # Enable continuous mouse tracking (even when no button pressed)
34 self.setMouseTracking(True)
36 # Disable dragging mode (only used for coordinate interaction)
37 self.setDragMode(QGraphicsView.DragMode.NoDrag)
39 # Crosshair graphics items (horizontal and vertical lines)
40 self.crosshair_h = None
41 self.crosshair_v = None
42 self.crosshair_visible = False
44 # Enable anti-aliasing and smooth scaling for better visual quality
45 self.setRenderHints(
46 QPainter.RenderHint.Antialiasing |
47 QPainter.RenderHint.SmoothPixmapTransform
48 )
50 # -------------------------------------------------------------------------
51 # Crosshair setup and management
52 # -------------------------------------------------------------------------
53 def setup_crosshairs(self):
54 """
55 Initialize the crosshair lines once the QGraphicsScene is available.
56 Adds two semi-transparent yellow lines to the scene (horizontal + vertical).
57 """
58 if self.scene():
59 # Yellow, semi-transparent pen for crosshair
60 pen = QPen(QColor(255, 255, 0, 180), 1)
62 # Create horizontal and vertical crosshair lines in the scene
63 self.crosshair_h = self.scene().addLine(0, 0, 0, 0, pen)
64 self.crosshair_v = self.scene().addLine(0, 0, 0, 0, pen)
66 # Initially invisible
67 self.crosshair_h.setVisible(False)
68 self.crosshair_v.setVisible(False)
70 # -------------------------------------------------------------------------
71 # Mouse movement: update coordinates and crosshair position
72 # -------------------------------------------------------------------------
73 def mouseMoveEvent(self, event: QMouseEvent):
74 """
75 Handle mouse movement:
76 - Maps cursor position to scene coordinates
77 - Checks bounds
78 - Updates crosshair lines
79 - Emits coordinate_changed signal
80 """
81 if self.scene() and self.parent_viewer and self.parent_viewer.img_data is not None:
82 # Map mouse position from view to scene coordinates
83 pos = self.mapToScene(event.pos())
84 x, y = int(pos.x()), int(pos.y())
86 # Get scene dimensions for bounds checking
87 scene_rect = self.scene().sceneRect()
89 # Only process if cursor is inside the image area
90 if (0 <= x < scene_rect.width() and 0 <= y < scene_rect.height()):
91 # Move crosshair to current position
92 self.update_crosshairs(x, y)
94 # Notify parent about the new coordinates
95 self.coordinate_changed.emit(self.view_idx, x, y)
97 # Pass event to base class for default behavior
98 super().mouseMoveEvent(event)
100 # -------------------------------------------------------------------------
101 # Mouse scroll: change slices
102 # -------------------------------------------------------------------------
104 def wheelEvent(self, event: QWheelEvent):
105 delta = int(event.angleDelta().y()/120)
106 self.parent_viewer.handle_scroll(self.view_idx, delta)
107 event.accept()
109 # -------------------------------------------------------------------------
110 # Mouse click: trigger coordinate selection or slice updates
111 # -------------------------------------------------------------------------
112 def mousePressEvent(self, event: QMouseEvent):
113 """
114 Handle mouse clicks:
115 - On left-click, compute image coordinates
116 - Notify parent viewer for cross-view synchronization or slice update
117 """
118 if (event.button() == Qt.MouseButton.LeftButton and
119 self.scene() and self.parent_viewer and
120 self.parent_viewer.img_data is not None):
122 # Convert click position to scene coordinates
123 pos = self.mapToScene(event.pos())
124 x, y = int(pos.x()), int(pos.y())
126 # Verify position is inside the scene
127 scene_rect = self.scene().sceneRect()
128 if 0 <= x < scene_rect.width() and 0 <= y < scene_rect.height():
129 # Delegate handling to parent viewer (e.g., update other views)
130 self.parent_viewer.handle_click_coordinates(self.view_idx, x, y)
132 super().mousePressEvent(event)
134 # -------------------------------------------------------------------------
135 # Update crosshair position visually
136 # -------------------------------------------------------------------------
137 def update_crosshairs(self, x, y):
138 """
139 Move crosshair lines to a new position (x, y).
140 Makes them visible if hidden.
141 """
142 if self.crosshair_h and self.crosshair_v and self.scene():
143 scene_rect = self.scene().sceneRect()
145 # Horizontal line across the scene
146 self.crosshair_h.setLine(0, y, scene_rect.width(), y)
148 # Vertical line across the scene
149 self.crosshair_v.setLine(x, 0, x, scene_rect.height())
151 # If not visible yet, make them appear
152 if not self.crosshair_visible:
153 self.crosshair_h.setVisible(True)
154 self.crosshair_v.setVisible(True)
155 self.crosshair_visible = True
157 # -------------------------------------------------------------------------
158 # External setter for crosshair position (used by parent viewer)
159 # -------------------------------------------------------------------------
160 def set_crosshair_position(self, x, y):
161 """
162 Allows external components (e.g. linked views) to set the crosshair.
163 Ensures it's within bounds before updating.
164 """
165 if self.crosshair_h and self.crosshair_v and self.scene():
166 scene_rect = self.scene().sceneRect()
167 if (0 <= x < scene_rect.width() and 0 <= y < scene_rect.height()):
168 self.crosshair_h.setLine(0, y, scene_rect.width(), y)
169 self.crosshair_v.setLine(x, 0, x, scene_rect.height())
170 self.crosshair_h.setVisible(True)
171 self.crosshair_v.setVisible(True)
172 self.crosshair_visible = True
174 # -------------------------------------------------------------------------
175 # Handle when the mouse leaves the widget area
176 # -------------------------------------------------------------------------
177 def leaveEvent(self, event):
178 """
179 Hide or update crosshairs when the mouse leaves the view area.
180 Currently, it calls the parent viewer’s update function to
181 synchronize crosshairs across multiple views.
182 """
183 if self.crosshair_h and self.crosshair_v:
184 # Previous behavior (hidden when leaving):
185 # self.crosshair_h.setVisible(False)
186 # self.crosshair_v.setVisible(False)
187 # self.crosshair_visible = False
189 # New behavior: ask parent to refresh all cross-view lines
190 self.parent_viewer.update_cross_view_lines()
192 super().leaveEvent(event)