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

1""" 

2No-scroll spinbox widgets for PyQt6. 

3 

4Prevents accidental value changes from mouse wheel events. 

5""" 

6 

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 

10 

11 

12class NoScrollSpinBox(QSpinBox): 

13 """SpinBox that ignores wheel events to prevent accidental value changes.""" 

14 

15 def wheelEvent(self, event: QWheelEvent): 

16 """Ignore wheel events to prevent accidental value changes.""" 

17 event.ignore() 

18 

19 

20class NoScrollDoubleSpinBox(QDoubleSpinBox): 

21 """DoubleSpinBox that ignores wheel events to prevent accidental value changes.""" 

22 

23 def wheelEvent(self, event: QWheelEvent): 

24 """Ignore wheel events to prevent accidental value changes.""" 

25 event.ignore() 

26 

27 

28class NoScrollComboBox(QComboBox): 

29 """ComboBox that ignores wheel events to prevent accidental value changes. 

30 

31 Supports placeholder text when currentIndex == -1 (for None values). 

32 """ 

33 

34 def __init__(self, parent=None, placeholder=""): 

35 super().__init__(parent) 

36 self._placeholder = placeholder 

37 self._placeholder_active = True 

38 

39 def wheelEvent(self, event: QWheelEvent): 

40 """Ignore wheel events to prevent accidental value changes.""" 

41 event.ignore() 

42 

43 def setPlaceholder(self, text: str): 

44 """Set the placeholder text shown when currentIndex == -1.""" 

45 self._placeholder = text 

46 self.update() 

47 

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() 

53 

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) 

60 

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) 

66 

67 # Now manually draw the placeholder text with our styling 

68 placeholder_color = QColor("#888888") 

69 font = QFont(self.font()) 

70 font.setItalic(True) 

71 

72 painter.setPen(placeholder_color) 

73 painter.setFont(font) 

74 

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 ) 

82 

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) 

88 

89 

90class NoneAwareCheckBox(QCheckBox): 

91 """ 

92 QCheckBox that supports None state for lazy dataclass contexts. 

93 

94 Shows inherited value as grayed placeholder when value is None. 

95 Clicking converts placeholder to explicit value. 

96 """ 

97 

98 def __init__(self, parent=None): 

99 super().__init__(parent) 

100 self._is_placeholder = False 

101 

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() 

107 

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)) 

116 

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) 

124 

125 def paintEvent(self, event): 

126 """Draw with distinct placeholder styling based on inherited value. 

127 

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 

136 

137 # Placeholder styling 

138 from PyQt6.QtWidgets import QStyle, QStyleOptionButton 

139 

140 painter = QPainter(self) 

141 painter.setRenderHint(QPainter.RenderHint.Antialiasing) 

142 

143 # Get the checkbox style option 

144 option = QStyleOptionButton() 

145 self.initStyleOption(option) 

146 

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) 

154 

155 # Get the indicator rectangle 

156 indicator_rect = self.style().subElementRect( 

157 QStyle.SubElement.SE_CheckBoxIndicator, 

158 option, 

159 self 

160 ) 

161 

162 # Draw a darker semi-transparent overlay on the indicator background 

163 painter.setOpacity(0.6) 

164 painter.fillRect(indicator_rect, QColor("#222222")) 

165 

166 painter.end() 

167 

168