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

104 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-04 02:09 +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 

6 

7 

8class DifferentValuesWrapper: 

9 """ 

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

11  

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

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

14 """ 

15 

16 def __init__( 

17 self, 

18 widget: Widget, 

19 default_value: Any, 

20 field_name: str = "", 

21 on_value_set_callback: Optional[callable] = None 

22 ): 

23 """Initialize the wrapper around any widget. 

24  

25 Args: 

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

27 default_value: The default value to use when clicked 

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

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

30 """ 

31 self.widget = widget 

32 self.default_value = default_value 

33 self.field_name = field_name 

34 self.on_value_set_callback = on_value_set_callback 

35 self.is_different_state = True 

36 

37 # Set initial disabled state and styling 

38 self._apply_different_state() 

39 

40 # Hook into widget's click event 

41 self._setup_click_handler() 

42 

43 def _apply_different_state(self) -> None: 

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

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

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

47 self.widget.disabled = False 

48 

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

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

51 

52 # Set tooltip to explain the state 

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

54 

55 def _setup_click_handler(self) -> None: 

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

57 # Store original click handler if it exists 

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

59 

60 # Use Textual's proper event handling approach 

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

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

63 

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

65 # Handle our different values logic first 

66 self._handle_click(event) 

67 # Then call original handler if it exists 

68 if original_on_click: 

69 if hasattr(original_on_click, '__call__'): 

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

71 # It's an async function 

72 await original_on_click(event) 

73 else: 

74 # It's a sync function 

75 original_on_click(event) 

76 

77 # Replace the widget's on_click method 

78 self.widget.on_click = wrapped_on_click 

79 

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

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

82 if self.is_different_state: 

83 # Set to default value using widget-specific method 

84 self._set_widget_value(self.default_value) 

85 

86 # Update state 

87 self.is_different_state = False 

88 

89 # Remove different state styling 

90 self._remove_different_state() 

91 

92 # Call callback if provided 

93 if self.on_value_set_callback: 

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

95 else: 

96 # If not in different state, call original handler 

97 if self._original_on_click: 

98 self._original_on_click(event) 

99 

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

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

102 widget_type = type(self.widget).__name__ 

103 

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

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

106 if widget_type == 'Checkbox': 

107 self.widget.value = bool(value) 

108 else: 

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

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

111 # RadioSet widgets (use pressed_button, not pressed) 

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

113 

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

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

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

117 button.value = True 

118 break 

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

120 button.value = True 

121 break 

122 else: 

123 # Fallback for custom widgets 

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

125 self.widget.set_value(value) 

126 

127 def _remove_different_state(self) -> None: 

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

129 # Re-enable the widget 

130 self.widget.disabled = False 

131 

132 # Remove styling 

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

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

135 

136 # Update tooltip 

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

138 

139 def reset_to_different(self) -> None: 

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

141 self.is_different_state = True 

142 self._apply_different_state() 

143 

144 # Clear widget value 

145 widget_type = type(self.widget).__name__ 

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

147 if widget_type == 'Checkbox': 

148 self.widget.value = False 

149 else: 

150 self.widget.value = "" 

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

152 # Clear RadioSet selection by setting all buttons to False 

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

154 button.value = False 

155 

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

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

158 self._set_widget_value(value) 

159 self.is_different_state = False 

160 

161 # Update styling 

162 self.widget.disabled = False 

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

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

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

166 

167 @property 

168 def current_state(self) -> str: 

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

170 if self.is_different_state: 

171 return "different" 

172 elif self._is_default_value(): 

173 return "default" 

174 else: 

175 return "modified" 

176 

177 def _is_default_value(self) -> bool: 

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

179 widget_type = type(self.widget).__name__ 

180 

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

182 current = self.widget.value 

183 if widget_type == 'Checkbox': 

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

185 else: 

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

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

188 # Check RadioSet selection 

189 pressed_button = self.widget.pressed_button 

190 if pressed_button is None: 

191 return False 

192 

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

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

195 return current_label == default_str 

196 return False 

197 

198 

199def create_different_values_widget( 

200 widget_type: Type[Widget], 

201 default_value: Any, 

202 field_name: str = "", 

203 widget_kwargs: Optional[dict] = None, 

204 on_value_set_callback: Optional[callable] = None 

205) -> Widget: 

206 """ 

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

208  

209 Args: 

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

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

212 field_name: Field name for debugging 

213 widget_kwargs: Additional kwargs for widget creation 

214 on_value_set_callback: Callback when value is set from different state 

215  

216 Returns: 

217 Widget instance with DifferentValuesWrapper attached 

218 """ 

219 widget_kwargs = widget_kwargs or {} 

220 

221 # Create the widget 

222 widget = widget_type(**widget_kwargs) 

223 

224 # Wrap it with different values functionality 

225 wrapper = DifferentValuesWrapper( 

226 widget=widget, 

227 default_value=default_value, 

228 field_name=field_name, 

229 on_value_set_callback=on_value_set_callback 

230 ) 

231 

232 # Attach wrapper to widget for later access 

233 widget._different_values_wrapper = wrapper 

234 

235 return widget