Coverage for openhcs/textual_tui/windows/config_window.py: 0.0%

60 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-08-14 05:57 +0000

1"""Configuration window for OpenHCS Textual TUI.""" 

2 

3from typing import Type, Any, Callable, Optional 

4from textual.app import ComposeResult 

5from textual.widgets import Button 

6from textual.containers import Container, Horizontal 

7 

8from openhcs.textual_tui.windows.base_window import BaseOpenHCSWindow 

9from openhcs.textual_tui.widgets.config_form import ConfigFormWidget 

10 

11 

12class ConfigWindow(BaseOpenHCSWindow): 

13 """Configuration window using textual-window system.""" 

14 

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

25 

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. 

31 

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 ) 

43 

44 self.config_class = config_class 

45 self.current_config = current_config 

46 self.on_save_callback = on_save_callback 

47 

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) 

50 

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 

55 

56 # Height per field (label + input + spacing) 

57 field_height = 2 

58 

59 # Count total fields (including nested) 

60 total_fields = len(self.field_specs) 

61 

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 

67 

68 calculated = base_height + (total_fields * field_height) 

69 

70 # Clamp between reasonable bounds 

71 return min(max(calculated, 15), 40) 

72 

73 

74 

75 def compose(self) -> ComposeResult: 

76 """Compose the config window content.""" 

77 with Container(classes="dialog-content"): 

78 yield self.config_form 

79 

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) 

85 

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) 

91 

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 

103 

104 

105 

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() 

114 

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() 

119 

120 # Create new config instance (same as original) 

121 new_config = self.config_class(**form_values) 

122 

123 # Call the callback if provided 

124 if self.on_save_callback: 

125 self.on_save_callback(new_config) 

126 

127 self.close_window() 

128 

129 def _handle_reset_to_defaults(self): 

130 """Reset all parameters using individual field reset logic for consistency.""" 

131 # Use the same logic as individual reset buttons to ensure consistency 

132 # This delegates to the form manager's lazy-aware reset logic 

133 if hasattr(self.config_form.form_manager, 'reset_all_parameters'): 

134 # Use the form manager's lazy-aware reset_all_parameters method 

135 self.config_form.form_manager.reset_all_parameters() 

136 else: 

137 # Fallback: reset each parameter individually 

138 from openhcs.textual_tui.widgets.shared.signature_analyzer import SignatureAnalyzer 

139 param_info = SignatureAnalyzer.analyze(self.config_class) 

140 for param_name in param_info.keys(): 

141 if hasattr(self.config_form.form_manager, 'reset_parameter'): 

142 self.config_form.form_manager.reset_parameter(param_name) 

143