Coverage for openhcs/pyqt_gui/widgets/shared/no_scroll_spinbox.py: 0.0%
75 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-01 18:33 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-01 18:33 +0000
1"""
2No-scroll spinbox widgets for PyQt6.
4Prevents accidental value changes from mouse wheel events.
5"""
7from PyQt6.QtWidgets import QSpinBox, QDoubleSpinBox, QComboBox, QCheckBox, QStylePainter, QStyleOptionComboBox, QStyle
8from PyQt6.QtGui import QWheelEvent, QPalette, QFont, QColor, QPainter
9from PyQt6.QtCore import Qt, QRect
12class NoScrollSpinBox(QSpinBox):
13 """SpinBox that ignores wheel events to prevent accidental value changes."""
15 def wheelEvent(self, event: QWheelEvent):
16 """Ignore wheel events to prevent accidental value changes."""
17 event.ignore()
20class NoScrollDoubleSpinBox(QDoubleSpinBox):
21 """DoubleSpinBox that ignores wheel events to prevent accidental value changes."""
23 def wheelEvent(self, event: QWheelEvent):
24 """Ignore wheel events to prevent accidental value changes."""
25 event.ignore()
28class NoScrollComboBox(QComboBox):
29 """ComboBox that ignores wheel events to prevent accidental value changes.
31 Supports placeholder text when currentIndex == -1 (for None values).
32 """
34 def __init__(self, parent=None, placeholder=""):
35 super().__init__(parent)
36 self._placeholder = placeholder
37 self._placeholder_active = True
39 def wheelEvent(self, event: QWheelEvent):
40 """Ignore wheel events to prevent accidental value changes."""
41 event.ignore()
43 def setPlaceholder(self, text: str):
44 """Set the placeholder text shown when currentIndex == -1."""
45 self._placeholder = text
46 self.update()
48 def setCurrentIndex(self, index: int):
49 """Override to track when placeholder should be active."""
50 super().setCurrentIndex(index)
51 self._placeholder_active = (index == -1)
52 self.update()
54 def paintEvent(self, event):
55 """Override to draw placeholder text when currentIndex == -1."""
56 if self._placeholder_active and self.currentIndex() == -1 and self._placeholder:
57 # Use regular QPainter to have full control over text rendering
58 painter = QPainter(self)
59 painter.setRenderHint(QPainter.RenderHint.Antialiasing)
61 # Draw the combobox frame using style
62 option = QStyleOptionComboBox()
63 self.initStyleOption(option)
64 option.currentText = "" # Don't let style draw the text
65 self.style().drawComplexControl(QStyle.ComplexControl.CC_ComboBox, option, painter, self)
67 # Now manually draw the placeholder text with our styling
68 placeholder_color = QColor("#888888")
69 font = QFont(self.font())
70 font.setItalic(True)
72 painter.setPen(placeholder_color)
73 painter.setFont(font)
75 # Get the text rect from the style
76 text_rect = self.style().subControlRect(
77 QStyle.ComplexControl.CC_ComboBox,
78 option,
79 QStyle.SubControl.SC_ComboBoxEditField,
80 self
81 )
83 # Draw the placeholder text
84 painter.drawText(text_rect, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, self._placeholder)
85 painter.end()
86 else:
87 super().paintEvent(event)
90class NoneAwareCheckBox(QCheckBox):
91 """
92 QCheckBox that supports None state for lazy dataclass contexts.
94 Shows inherited value as grayed placeholder when value is None.
95 Clicking converts placeholder to explicit value.
96 """
98 def __init__(self, parent=None):
99 super().__init__(parent)
100 self._is_placeholder = False
102 def get_value(self):
103 """Get value, returning None if in placeholder state."""
104 if self._is_placeholder:
105 return None
106 return self.isChecked()
108 def set_value(self, value):
109 """Set value, handling None by leaving in placeholder state."""
110 if value is None:
111 # Don't change state - placeholder system will set the preview value
112 self._is_placeholder = True
113 else:
114 self._is_placeholder = False
115 self.setChecked(bool(value))
117 def mousePressEvent(self, event):
118 """On click, switch from placeholder to explicit value."""
119 if self._is_placeholder:
120 self._is_placeholder = False
121 # Clear placeholder property so get_value returns actual boolean
122 self.setProperty("is_placeholder_state", False)
123 super().mousePressEvent(event)
125 def paintEvent(self, event):
126 """Draw with distinct placeholder styling based on inherited value.
128 - Placeholder True: Dimmer/semi-transparent checkmark
129 - Placeholder False: Darker background on checkbox indicator
130 - Concrete True/False: Normal styling unchanged
131 """
132 if not self._is_placeholder:
133 # Concrete value: Normal styling
134 super().paintEvent(event)
135 return
137 # Placeholder styling
138 from PyQt6.QtWidgets import QStyle, QStyleOptionButton
140 painter = QPainter(self)
141 painter.setRenderHint(QPainter.RenderHint.Antialiasing)
143 # Get the checkbox style option
144 option = QStyleOptionButton()
145 self.initStyleOption(option)
147 if self.isChecked():
148 # Placeholder True: Draw with dimmer checkmark
149 painter.setOpacity(0.4)
150 self.style().drawControl(QStyle.ControlElement.CE_CheckBox, option, painter, self)
151 else:
152 # Placeholder False: Draw normal checkbox first, then add dark overlay
153 self.style().drawControl(QStyle.ControlElement.CE_CheckBox, option, painter, self)
155 # Get the indicator rectangle
156 indicator_rect = self.style().subElementRect(
157 QStyle.SubElement.SE_CheckBoxIndicator,
158 option,
159 self
160 )
162 # Draw a darker semi-transparent overlay on the indicator background
163 painter.setOpacity(0.6)
164 painter.fillRect(indicator_rect, QColor("#222222"))
166 painter.end()