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

1"""Parameter tracker for managing UI variable bindings to EditorController. 

2 

3This module provides a clean interface for tracking parameter changes in the UI 

4and synchronizing them with the EditorController. 

5""" 

6 

7import tkinter as tk 

8from typing import Any, Callable, Dict, Optional 

9 

10import pytribeam.GUI.config_ui.lookup as lut 

11from pytribeam.GUI.config_ui.editor_controller import EditorController 

12 

13 

14class ParameterTracker: 

15 """Manages tkinter variables and their connection to EditorController. 

16 

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 

22 

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 """ 

29 

30 def __init__(self, controller: EditorController, master: Optional[tk.Misc] = None): 

31 """Initialize parameter tracker. 

32 

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 

42 

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. 

51 

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 

57 

58 Returns: 

59 Tkinter variable with trace attached 

60 """ 

61 # Create appropriate variable type based on dtype 

62 var = self._create_typed_variable(dtype) 

63 

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) 

73 

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 ) 

78 

79 # Store variable and trace info 

80 self.variables[param_path] = var 

81 self.trace_ids[param_path] = trace_id 

82 

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) 

89 

90 return var 

91 

92 def _create_typed_variable(self, dtype: type) -> tk.Variable: 

93 """Create appropriate tkinter variable based on dtype. 

94 

95 Args: 

96 dtype: Python type (int, float, bool, str, etc.) 

97 

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) 

108 

109 def _create_default_validator(self, dtype: type) -> Callable: 

110 """Create default validator function based on dtype. 

111 

112 Args: 

113 dtype: Python type 

114 

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 "" 

126 

127 def _validate_int(self, value: Any) -> str: 

128 """Validate integer input. 

129 

130 Args: 

131 value: Value to validate 

132 

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}") 

147 

148 def _validate_float(self, value: Any) -> str: 

149 """Validate float input. 

150 

151 Args: 

152 value: Value to validate 

153 

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}") 

168 

169 def _validate_bool(self, value: Any) -> bool: 

170 """Validate boolean input. 

171 

172 Args: 

173 value: Value to validate 

174 

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) 

186 

187 def _on_variable_changed(self, param_path: str, var: tk.Variable, dtype: type): 

188 """Handle variable change from UI. 

189 

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 

198 

199 try: 

200 value = var.get() 

201 

202 # Apply validation 

203 if param_path in self.validators: 

204 validated_value = self.validators[param_path](value) 

205 else: 

206 validated_value = value 

207 

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 

213 

214 # Update controller 

215 self.controller.update_parameter(param_path, validated_value) 

216 

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 

227 

228 def load_from_controller(self): 

229 """Update all UI variables from current controller state. 

230 

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 

244 

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 

256 

257 # Clear all storage 

258 self.variables.clear() 

259 self.trace_ids.clear() 

260 self.validators.clear() 

261 

262 def get_variable(self, param_path: str) -> Optional[tk.Variable]: 

263 """Get tracked variable for parameter. 

264 

265 Args: 

266 param_path: Parameter path 

267 

268 Returns: 

269 Tkinter variable or None if not found 

270 """ 

271 return self.variables.get(param_path) 

272 

273 def add_custom_validator(self, param_path: str, validator: Callable): 

274 """Add or replace custom validator for parameter. 

275 

276 Args: 

277 param_path: Parameter path 

278 validator: Validator function 

279 """ 

280 self.validators[param_path] = validator 

281 

282 def __repr__(self) -> str: 

283 return f"ParameterTracker(variables={len(self.variables)})"