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