Coverage for src/pytribeam/GUI/config_ui/parameter_tracker.py: 0%
120 statements
« prev ^ index » next coverage.py v7.6.1, created at 2026-06-16 18:30 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2026-06-16 18:30 +0000
1"""Parameter tracker for managing UI variable bindings to EditorController.
3This module provides a clean interface for tracking parameter changes in the UI
4and synchronizing them with the EditorController.
5"""
7import tkinter as tk
8from typing import Any, Callable, Dict, Optional
10import pytribeam.GUI.config_ui.lookup as lut
11from pytribeam.GUI.config_ui.editor_controller import EditorController
14class ParameterTracker:
15 """Manages tkinter variables and their connection to EditorController.
17 This class provides:
18 - Automatic variable creation based on parameter type
19 - Input validation based on LUT dtype
20 - Direct updates to controller when UI changes
21 - Automatic UI updates when controller state changes
23 Attributes:
24 controller: EditorController instance
25 variables: Dict mapping parameter paths to tkinter variables
26 trace_ids: Dict mapping parameter paths to trace IDs
27 validators: Dict mapping parameter paths to validation functions
28 """
30 def __init__(self, controller: EditorController, master: Optional[tk.Misc] = None):
31 """Initialize parameter tracker.
33 Args:
34 controller: EditorController instance to bind to
35 """
36 self.controller = controller
37 self.master = master
38 self.variables: Dict[str, tk.Variable] = {}
39 self.trace_ids: Dict[str, str] = {}
40 self.validators: Dict[str, Callable] = {}
41 self._updating_from_controller = False
43 def create_variable(
44 self,
45 param_path: str,
46 dtype: type,
47 default: Any = None,
48 validator: Optional[Callable] = None,
49 ) -> tk.Variable:
50 """Create a traced variable for a parameter.
52 Args:
53 param_path: Parameter path (e.g., 'beam/voltage_kv')
54 dtype: Data type from LUT
55 default: Default value if parameter not set
56 validator: Optional custom validator function
58 Returns:
59 Tkinter variable with trace attached
60 """
61 # Create appropriate variable type based on dtype
62 var = self._create_typed_variable(dtype)
64 # Set initial value from controller
65 value = self.controller.get_parameter(param_path, default)
66 if value is not None:
67 try:
68 var.set(value)
69 except tk.TclError:
70 # If value is incompatible with var type, use default
71 if default is not None:
72 var.set(default)
74 # Add trace to update controller when variable changes
75 trace_id = var.trace_add(
76 "write", lambda *args: self._on_variable_changed(param_path, var, dtype)
77 )
79 # Store variable and trace info
80 self.variables[param_path] = var
81 self.trace_ids[param_path] = trace_id
83 # Store custom validator if provided
84 if validator:
85 self.validators[param_path] = validator
86 else:
87 # Create default validator based on dtype
88 self.validators[param_path] = self._create_default_validator(dtype)
90 return var
92 def _create_typed_variable(self, dtype: type) -> tk.Variable:
93 """Create appropriate tkinter variable based on dtype.
95 Args:
96 dtype: Python type (int, float, bool, str, etc.)
98 Returns:
99 Appropriate tkinter variable
100 """
101 # Note: We use StringVar for most types and do validation in callbacks
102 # This allows better control over input validation and error handling
103 if dtype == bool:
104 return tk.BooleanVar(master=self.master)
105 else:
106 # Use StringVar for int, float, str - gives us more control
107 return tk.StringVar(master=self.master)
109 def _create_default_validator(self, dtype: type) -> Callable:
110 """Create default validator function based on dtype.
112 Args:
113 dtype: Python type
115 Returns:
116 Validator function
117 """
118 if dtype == int:
119 return lambda value: self._validate_int(value)
120 elif dtype == float:
121 return lambda value: self._validate_float(value)
122 elif dtype == bool:
123 return lambda value: self._validate_bool(value)
124 else:
125 return lambda value: str(value) if value is not None else ""
127 def _validate_int(self, value: Any) -> str:
128 """Validate integer input.
130 Args:
131 value: Value to validate
133 Returns:
134 Validated string representation
135 """
136 if value == "" or value is None:
137 return ""
138 try:
139 # Allow negative sign and digits
140 str_val = str(value).strip()
141 if str_val in ["-", "+"]:
142 return str_val
143 int(str_val) # Test if valid
144 return str_val
145 except ValueError:
146 raise ValueError(f"Invalid integer: {value}")
148 def _validate_float(self, value: Any) -> str:
149 """Validate float input.
151 Args:
152 value: Value to validate
154 Returns:
155 Validated string representation
156 """
157 if value == "" or value is None:
158 return ""
159 try:
160 # Allow negative sign, digits, and decimal point
161 str_val = str(value).strip()
162 if str_val in ["-", "+", ".", "-.", "+."]:
163 return str_val
164 float(str_val) # Test if valid
165 return str_val
166 except ValueError:
167 raise ValueError(f"Invalid float: {value}")
169 def _validate_bool(self, value: Any) -> bool:
170 """Validate boolean input.
172 Args:
173 value: Value to validate
175 Returns:
176 Boolean value
177 """
178 if isinstance(value, bool):
179 return value
180 if isinstance(value, str):
181 if value.lower() in ["true", "1", "yes"]:
182 return True
183 elif value.lower() in ["false", "0", "no", ""]:
184 return False
185 return bool(value)
187 def _on_variable_changed(self, param_path: str, var: tk.Variable, dtype: type):
188 """Handle variable change from UI.
190 Args:
191 param_path: Parameter path
192 var: Tkinter variable that changed
193 dtype: Expected data type
194 """
195 # Avoid recursive updates
196 if self._updating_from_controller:
197 return
199 try:
200 value = var.get()
202 # Apply validation
203 if param_path in self.validators:
204 validated_value = self.validators[param_path](value)
205 else:
206 validated_value = value
208 # Update variable with validated value if it changed
209 if value != validated_value:
210 self._updating_from_controller = True
211 var.set(validated_value)
212 self._updating_from_controller = False
214 # Update controller
215 self.controller.update_parameter(param_path, validated_value)
217 except (ValueError, tk.TclError) as e:
218 # Validation failed - revert to previous value from controller
219 old_value = self.controller.get_parameter(param_path)
220 if old_value is not None:
221 self._updating_from_controller = True
222 try:
223 var.set(old_value)
224 except tk.TclError:
225 pass # If revert fails, just leave it
226 self._updating_from_controller = False
228 def load_from_controller(self):
229 """Update all UI variables from current controller state.
231 This is called when a new step is selected to sync the UI.
232 """
233 self._updating_from_controller = True
234 try:
235 for param_path, var in self.variables.items():
236 value = self.controller.get_parameter(param_path)
237 if value is not None:
238 try:
239 var.set(value)
240 except tk.TclError:
241 pass # Skip if incompatible
242 finally:
243 self._updating_from_controller = False
245 def clear(self):
246 """Clear all traced variables and cleanup."""
247 # Remove all traces
248 for param_path in list(self.trace_ids.keys()):
249 if param_path in self.variables:
250 try:
251 self.variables[param_path].trace_remove(
252 "write", self.trace_ids[param_path]
253 )
254 except (KeyError, tk.TclError):
255 pass # Already removed or invalid
257 # Clear all storage
258 self.variables.clear()
259 self.trace_ids.clear()
260 self.validators.clear()
262 def get_variable(self, param_path: str) -> Optional[tk.Variable]:
263 """Get tracked variable for parameter.
265 Args:
266 param_path: Parameter path
268 Returns:
269 Tkinter variable or None if not found
270 """
271 return self.variables.get(param_path)
273 def add_custom_validator(self, param_path: str, validator: Callable):
274 """Add or replace custom validator for parameter.
276 Args:
277 param_path: Parameter path
278 validator: Validator function
279 """
280 self.validators[param_path] = validator
282 def __repr__(self) -> str:
283 return f"ParameterTracker(variables={len(self.variables)})"