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
« 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."""
3from typing import Any, Optional, Type
4from textual.widget import Widget
5from textual.events import Click
8class DifferentValuesWrapper:
9 """
10 Universal wrapper that adds 'DIFFERENT VALUES' functionality to ANY Textual widget.
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 """
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.
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
37 # Set initial disabled state and styling
38 self._apply_different_state()
40 # Hook into widget's click event
41 self._setup_click_handler()
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
49 # Add CSS class for styling (will make it look disabled)
50 self.widget.add_class("different-values-widget")
52 # Set tooltip to explain the state
53 self.widget.tooltip = f"DIFFERENT VALUES - Click to set to default: {self.default_value}"
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)
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)
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)
77 # Replace the widget's on_click method
78 self.widget.on_click = wrapped_on_click
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)
86 # Update state
87 self.is_different_state = False
89 # Remove different state styling
90 self._remove_different_state()
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)
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__
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)
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)
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
132 # Remove styling
133 self.widget.remove_class("different-values-widget")
134 self.widget.add_class("modified-from-different")
136 # Update tooltip
137 self.widget.tooltip = f"Set to default value: {self.default_value}"
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()
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
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
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}"
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"
177 def _is_default_value(self) -> bool:
178 """Check if current widget value matches default."""
179 widget_type = type(self.widget).__name__
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
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
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.
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
216 Returns:
217 Widget instance with DifferentValuesWrapper attached
218 """
219 widget_kwargs = widget_kwargs or {}
221 # Create the widget
222 widget = widget_type(**widget_kwargs)
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 )
232 # Attach wrapper to widget for later access
233 widget._different_values_wrapper = wrapper
235 return widget