Coverage for openhcs/textual_tui/windows/config_window.py: 0.0%
67 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-01 18:33 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-01 18:33 +0000
1"""Configuration window for OpenHCS Textual TUI."""
3from typing import Type, Any, Callable, Optional
4from textual.app import ComposeResult
5from textual.widgets import Button
6from textual.containers import Container, Horizontal
8from openhcs.textual_tui.windows.base_window import BaseOpenHCSWindow
9from openhcs.textual_tui.widgets.config_form import ConfigFormWidget
12class ConfigWindow(BaseOpenHCSWindow):
13 """Configuration window using textual-window system."""
15 DEFAULT_CSS = """
16 ConfigWindow {
17 width: 60; height: 20;
18 min-width: 60; min-height: 20;
19 }
20 ConfigWindow #dialog_container {
21 width: 80;
22 height: 30;
23 }
24 """
26 def __init__(self, config_class: Type, current_config: Any,
27 on_save_callback: Optional[Callable] = None,
28 is_global_config_editing: bool = False, **kwargs):
29 """
30 Initialize config window.
32 Args:
33 config_class: Configuration class type
34 current_config: Current configuration instance
35 on_save_callback: Function to call when config is saved
36 """
37 super().__init__(
38 window_id="config_dialog",
39 title="Configuration",
40 mode="temporary",
41 **kwargs
42 )
44 self.config_class = config_class
45 self.current_config = current_config
46 self.on_save_callback = on_save_callback
48 # Create the form widget using unified parameter analysis
49 self.config_form = ConfigFormWidget.from_dataclass(config_class, current_config, is_global_config_editing=is_global_config_editing)
51 def calculate_content_height(self) -> int:
52 """Calculate dialog height based on number of fields."""
53 # Base height for title, buttons, padding
54 base_height = 8
56 # Height per field (label + input + spacing)
57 field_height = 2
59 # Count total fields (including nested)
60 total_fields = len(self.field_specs)
62 # Add extra height for nested dataclasses
63 for spec in self.field_specs:
64 if hasattr(spec.actual_type, '__dataclass_fields__'):
65 # Nested dataclass adds extra height for collapsible
66 total_fields += len(spec.actual_type.__dataclass_fields__) + 1
68 calculated = base_height + (total_fields * field_height)
70 # Clamp between reasonable bounds
71 return min(max(calculated, 15), 40)
75 def compose(self) -> ComposeResult:
76 """Compose the config window content."""
77 with Container(classes="dialog-content"):
78 yield self.config_form
80 # Buttons
81 with Horizontal(classes="dialog-buttons"):
82 yield Button("Reset to Defaults", id="reset_to_defaults", compact=True)
83 yield Button("Save", id="save", compact=True)
84 yield Button("Cancel", id="cancel", compact=True)
86 def on_mount(self) -> None:
87 """Called when the window is mounted - prevent automatic scrolling on focus."""
88 # Override the default focus behavior to prevent automatic scrolling
89 # when the first widget in the form gets focus
90 self.call_after_refresh(self._set_initial_focus_without_scroll)
92 def _set_initial_focus_without_scroll(self) -> None:
93 """Set focus to the first input without causing scroll."""
94 try:
95 # Find the first focusable widget in the config form
96 first_input = self.config_form.query("Input, Checkbox, RadioSet").first()
97 if first_input:
98 # Focus without scrolling to prevent the window from jumping
99 first_input.focus(scroll_visible=False)
100 except Exception:
101 # If no focusable widgets found, that's fine - no focus needed
102 pass
106 def on_button_pressed(self, event: Button.Pressed) -> None:
107 """Handle button presses."""
108 if event.button.id == "save":
109 self._handle_save()
110 elif event.button.id == "cancel":
111 self.close_window()
112 elif event.button.id == "reset_to_defaults":
113 self._handle_reset_to_defaults()
115 def _handle_save(self):
116 """Handle save button - reuse existing logic from ConfigDialogScreen."""
117 # Get form values (same method as original)
118 form_values = self.config_form.get_config_values()
120 # CRITICAL FIX: For lazy dataclasses, create instance with raw values to preserve None vs concrete distinction
121 from openhcs.core.lazy_placeholder import LazyDefaultPlaceholderService
123 if LazyDefaultPlaceholderService.has_lazy_resolution(self.config_class):
124 # Create empty instance first (no constructor args to avoid resolution)
125 new_config = object.__new__(self.config_class)
127 # Set raw values directly using object.__setattr__ to avoid lazy resolution
128 for field_name, value in form_values.items():
129 object.__setattr__(new_config, field_name, value)
131 # Initialize any required lazy dataclass attributes
132 if hasattr(self.config_class, '_is_lazy_dataclass'):
133 object.__setattr__(new_config, '_is_lazy_dataclass', True)
134 else:
135 # For non-lazy dataclasses, use normal constructor
136 new_config = self.config_class(**form_values)
138 # Call the callback if provided
139 if self.on_save_callback:
140 self.on_save_callback(new_config)
142 self.close_window()
144 def _handle_reset_to_defaults(self):
145 """Reset all parameters using individual field reset logic for consistency."""
146 # Use the same logic as individual reset buttons to ensure consistency
147 # This delegates to the form manager's lazy-aware reset logic
148 if hasattr(self.config_form.form_manager, 'reset_all_parameters'):
149 # Use the form manager's lazy-aware reset_all_parameters method
150 self.config_form.form_manager.reset_all_parameters()
151 else:
152 # Fallback: reset each parameter individually
153 from openhcs.textual_tui.widgets.shared.signature_analyzer import SignatureAnalyzer
154 param_info = SignatureAnalyzer.analyze(self.config_class)
155 for param_name in param_info.keys():
156 if hasattr(self.config_form.form_manager, 'reset_parameter'):
157 self.config_form.form_manager.reset_parameter(param_name)