Coverage for openhcs/textual_tui/widgets/different_values_wrapper.py: 0.0%

105 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-08-14 05:57 +0000

1"""Universal wrapper for handling 'DIFFERENT VALUES' state across all widget types.""" 

2 

3from typing import Any, Optional, Type 

4from textual.widget import Widget 

5from textual.events import Click 

6from textual import on 

7 

8 

9class DifferentValuesWrapper: 

10 """ 

11 Universal wrapper that adds 'DIFFERENT VALUES' functionality to ANY Textual widget. 

12  

13 Uses the common disabled interface that all widgets inherit from Widget base class. 

14 Provides consistent behavior: disabled + hover feedback + click-to-default. 

15 """ 

16 

17 def __init__( 

18 self, 

19 widget: Widget, 

20 default_value: Any, 

21 field_name: str = "", 

22 on_value_set_callback: Optional[callable] = None 

23 ): 

24 """Initialize the wrapper around any widget. 

25  

26 Args: 

27 widget: Any Textual widget (Input, Checkbox, RadioSet, etc.) 

28 default_value: The default value to use when clicked 

29 field_name: Name of the field (for debugging/logging) 

30 on_value_set_callback: Optional callback when value is set from different state 

31 """ 

32 self.widget = widget 

33 self.default_value = default_value 

34 self.field_name = field_name 

35 self.on_value_set_callback = on_value_set_callback 

36 self.is_different_state = True 

37 

38 # Set initial disabled state and styling 

39 self._apply_different_state() 

40 

41 # Hook into widget's click event 

42 self._setup_click_handler() 

43 

44 def _apply_different_state(self) -> None: 

45 """Apply 'DIFFERENT VALUES' visual state to the widget.""" 

46 # DON'T disable the widget - disabled widgets don't receive click events! 

47 # Instead, use CSS styling to make it appear disabled 

48 self.widget.disabled = False 

49 

50 # Add CSS class for styling (will make it look disabled) 

51 self.widget.add_class("different-values-widget") 

52 

53 # Set tooltip to explain the state 

54 self.widget.tooltip = f"DIFFERENT VALUES - Click to set to default: {self.default_value}" 

55 

56 def _setup_click_handler(self) -> None: 

57 """Set up click handler for the wrapped widget using Textual's event system.""" 

58 # Store original click handler if it exists 

59 self._original_on_click = getattr(self.widget, 'on_click', None) 

60 

61 # Use Textual's proper event handling approach 

62 # We'll add a message handler to the widget 

63 original_on_click = getattr(self.widget, 'on_click', None) 

64 

65 async def wrapped_on_click(event: Click) -> None: 

66 # Handle our different values logic first 

67 self._handle_click(event) 

68 # Then call original handler if it exists 

69 if original_on_click: 

70 if hasattr(original_on_click, '__call__'): 

71 if hasattr(original_on_click, '__code__') and original_on_click.__code__.co_flags & 0x80: 

72 # It's an async function 

73 await original_on_click(event) 

74 else: 

75 # It's a sync function 

76 original_on_click(event) 

77 

78 # Replace the widget's on_click method 

79 self.widget.on_click = wrapped_on_click 

80 

81 def _handle_click(self, event: Click) -> None: 

82 """Handle click event - set to default value if in different state.""" 

83 if self.is_different_state: 

84 # Set to default value using widget-specific method 

85 self._set_widget_value(self.default_value) 

86 

87 # Update state 

88 self.is_different_state = False 

89 

90 # Remove different state styling 

91 self._remove_different_state() 

92 

93 # Call callback if provided 

94 if self.on_value_set_callback: 

95 self.on_value_set_callback(self.field_name, self.default_value) 

96 else: 

97 # If not in different state, call original handler 

98 if self._original_on_click: 

99 self._original_on_click(event) 

100 

101 def _set_widget_value(self, value: Any) -> None: 

102 """Set value on the wrapped widget using appropriate method.""" 

103 widget_type = type(self.widget).__name__ 

104 

105 if hasattr(self.widget, 'value'): 

106 # Most widgets (Input, Checkbox, etc.) 

107 if widget_type == 'Checkbox': 

108 self.widget.value = bool(value) 

109 else: 

110 self.widget.value = str(value) if value is not None else "" 

111 elif hasattr(self.widget, 'pressed_button') or widget_type in ['RadioSet', 'EnumRadioSet']: 

112 # RadioSet widgets (use pressed_button, not pressed) 

113 target_value = value.value if hasattr(value, 'value') else str(value) 

114 

115 # Find the radio button with matching value and set it 

116 for button in self.widget.query('RadioButton'): 

117 if hasattr(button, 'label') and str(button.label).upper() == target_value.upper(): 

118 button.value = True 

119 break 

120 elif hasattr(button, 'id') and target_value.lower() in str(button.id).lower(): 

121 button.value = True 

122 break 

123 else: 

124 # Fallback for custom widgets 

125 if hasattr(self.widget, 'set_value'): 

126 self.widget.set_value(value) 

127 

128 def _remove_different_state(self) -> None: 

129 """Remove 'DIFFERENT VALUES' state and restore normal widget.""" 

130 # Re-enable the widget 

131 self.widget.disabled = False 

132 

133 # Remove styling 

134 self.widget.remove_class("different-values-widget") 

135 self.widget.add_class("modified-from-different") 

136 

137 # Update tooltip 

138 self.widget.tooltip = f"Set to default value: {self.default_value}" 

139 

140 def reset_to_different(self) -> None: 

141 """Reset the widget back to 'DIFFERENT VALUES' state.""" 

142 self.is_different_state = True 

143 self._apply_different_state() 

144 

145 # Clear widget value 

146 widget_type = type(self.widget).__name__ 

147 if hasattr(self.widget, 'value'): 

148 if widget_type == 'Checkbox': 

149 self.widget.value = False 

150 else: 

151 self.widget.value = "" 

152 elif hasattr(self.widget, 'pressed_button') or widget_type in ['RadioSet', 'EnumRadioSet']: 

153 # Clear RadioSet selection by setting all buttons to False 

154 for button in self.widget.query('RadioButton'): 

155 button.value = False 

156 

157 def set_value(self, value: Any) -> None: 

158 """Set a specific value (not from click-to-default).""" 

159 self._set_widget_value(value) 

160 self.is_different_state = False 

161 

162 # Update styling 

163 self.widget.disabled = False 

164 self.widget.remove_class("different-values-widget") 

165 self.widget.add_class("user-modified") 

166 self.widget.tooltip = f"User modified value: {value}" 

167 

168 @property 

169 def current_state(self) -> str: 

170 """Get the current state of the wrapper.""" 

171 if self.is_different_state: 

172 return "different" 

173 elif self._is_default_value(): 

174 return "default" 

175 else: 

176 return "modified" 

177 

178 def _is_default_value(self) -> bool: 

179 """Check if current widget value matches default.""" 

180 widget_type = type(self.widget).__name__ 

181 

182 if hasattr(self.widget, 'value'): 

183 current = self.widget.value 

184 if widget_type == 'Checkbox': 

185 return bool(current) == bool(self.default_value) 

186 else: 

187 return str(current) == str(self.default_value) 

188 elif hasattr(self.widget, 'pressed_button') or widget_type in ['RadioSet', 'EnumRadioSet']: 

189 # Check RadioSet selection 

190 pressed_button = self.widget.pressed_button 

191 if pressed_button is None: 

192 return False 

193 

194 current_label = str(pressed_button.label).upper() 

195 default_str = str(self.default_value.value if hasattr(self.default_value, 'value') else self.default_value).upper() 

196 return current_label == default_str 

197 return False 

198 

199 

200def create_different_values_widget( 

201 widget_type: Type[Widget], 

202 default_value: Any, 

203 field_name: str = "", 

204 widget_kwargs: Optional[dict] = None, 

205 on_value_set_callback: Optional[callable] = None 

206) -> Widget: 

207 """ 

208 Factory function to create any widget with 'DIFFERENT VALUES' functionality. 

209  

210 Args: 

211 widget_type: The widget class to create (Input, Checkbox, RadioSet, etc.) 

212 default_value: Default value for click-to-set behavior 

213 field_name: Field name for debugging 

214 widget_kwargs: Additional kwargs for widget creation 

215 on_value_set_callback: Callback when value is set from different state 

216  

217 Returns: 

218 Widget instance with DifferentValuesWrapper attached 

219 """ 

220 widget_kwargs = widget_kwargs or {} 

221 

222 # Create the widget 

223 widget = widget_type(**widget_kwargs) 

224 

225 # Wrap it with different values functionality 

226 wrapper = DifferentValuesWrapper( 

227 widget=widget, 

228 default_value=default_value, 

229 field_name=field_name, 

230 on_value_set_callback=on_value_set_callback 

231 ) 

232 

233 # Attach wrapper to widget for later access 

234 widget._different_values_wrapper = wrapper 

235 

236 return widget