import tkinter as tk from tkinter import ttk, messagebox import json from typing import Dict, Any, Callable, Tuple, List class ConfigWindow: # UI Constants extracted from hardcoded values WINDOW_GEOMETRY = "800x600" BUTTON_COLORS = { "apply": "#4CAF50", "save": "#2196F3", "reset": "#f44336" } FONT_TITLE = ("Arial", 12, "bold") FONT_SECTION = ("Arial", 10, "bold") PADDING_STANDARD = 5 PADDING_LARGE = 10 ENTRY_WIDTH_STANDARD = 10 ENTRY_WIDTH_LONG = 40 BUTTON_PADDING = 20 def __init__(self, parent, config_path: str = "config.json", on_apply_callback: Callable = None): self.parent = parent self.config_path = config_path self.on_apply_callback = on_apply_callback self.window = None self.config_data = {} self.entry_widgets = {} # Load current configuration self.load_config() def load_config(self): """Load configuration from file""" try: with open(self.config_path, 'r', encoding='utf-8') as f: self.config_data = json.load(f) except Exception as e: # Follow fail-fast principle - raise exception instead of hiding it raise RuntimeError(f"Failed to load configuration from {self.config_path}: {e}") def save_config(self): """Save configuration to file""" try: with open(self.config_path, 'w', encoding='utf-8') as f: json.dump(self.config_data, f, indent=2, ensure_ascii=False) return True except Exception as e: # Log error but allow UI to handle it raise RuntimeError(f"Failed to save configuration to {self.config_path}: {e}") def show(self): """Show configuration window""" if self.window and self.window.winfo_exists(): self.window.lift() return self.window = tk.Toplevel(self.parent) self.window.title("Configuration Management") self.window.geometry(self.WINDOW_GEOMETRY) # Create notebook for tabs notebook = ttk.Notebook(self.window) notebook.pack(fill=tk.BOTH, expand=True, padx=self.PADDING_LARGE, pady=self.PADDING_LARGE) # Create tabs self.create_robot_tab(notebook) self.create_wall_tab(notebook) self.create_hole_tab(notebook) self.create_task_points_tab(notebook) self.create_transport_object_tab(notebook) self.create_simulation_tab(notebook) # Create buttons frame button_frame = tk.Frame(self.window) button_frame.pack(fill=tk.X, padx=self.PADDING_LARGE, pady=self.PADDING_STANDARD) # Apply button apply_btn = tk.Button(button_frame, text="Apply", command=self.apply_changes, bg=self.BUTTON_COLORS["apply"], fg="white", padx=self.BUTTON_PADDING) apply_btn.pack(side=tk.LEFT, padx=self.PADDING_STANDARD) # Save button save_btn = tk.Button(button_frame, text="Save to File", command=self.save_changes, bg=self.BUTTON_COLORS["save"], fg="white", padx=self.BUTTON_PADDING) save_btn.pack(side=tk.LEFT, padx=self.PADDING_STANDARD) # Reset button reset_btn = tk.Button(button_frame, text="Reset", command=self.reset_values, bg=self.BUTTON_COLORS["reset"], fg="white", padx=self.BUTTON_PADDING) reset_btn.pack(side=tk.LEFT, padx=self.PADDING_STANDARD) # Close button close_btn = tk.Button(button_frame, text="Close", command=self.window.destroy, padx=self.BUTTON_PADDING) close_btn.pack(side=tk.RIGHT, padx=self.PADDING_STANDARD) def _create_vector_input(self, parent: tk.Frame, row: int, label: str, key_prefix: str, values: List[float], labels: List[str] = None) -> None: """Create vector input fields (x, y, z or custom labels)""" if labels is None: labels = ["X:", "Y:", "Z:"] tk.Label(parent, text=label).grid(row=row, column=0, sticky="e", padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD) for i, (lbl, val) in enumerate(zip(labels, values)): tk.Label(parent, text=lbl).grid(row=row + i, column=1, sticky="e", padx=self.PADDING_STANDARD) entry_key = f"{key_prefix}.{lbl.lower().rstrip(':')}" self.entry_widgets[entry_key] = tk.Entry(parent, width=self.ENTRY_WIDTH_STANDARD) self.entry_widgets[entry_key].grid(row=row + i, column=2, padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD) self.entry_widgets[entry_key].insert(0, str(val)) def _create_title_label(self, parent: tk.Frame, text: str, row: int = 0, font_type: str = "title") -> None: """Create a title label""" font = self.FONT_TITLE if font_type == "title" else self.FONT_SECTION tk.Label(parent, text=text, font=font).grid( row=row, column=0, columnspan=6, pady=self.PADDING_LARGE) def _get_config_value(self, *keys, default=None): """Safely get nested configuration value""" value = self.config_data for key in keys: if isinstance(value, dict): value = value.get(key, {}) else: return default return value if value != {} else default def create_robot_tab(self, notebook): """Create robot configuration tab""" frame = ttk.Frame(notebook) notebook.add(frame, text="Robot") # Title self._create_title_label(frame, "Robot Configuration") # Model path tk.Label(frame, text="Model Path:").grid(row=1, column=0, sticky="e", padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD) self.entry_widgets['robot.model_path'] = tk.Entry(frame, width=self.ENTRY_WIDTH_LONG) self.entry_widgets['robot.model_path'].grid(row=1, column=1, columnspan=3, padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD) self.entry_widgets['robot.model_path'].insert(0, self._get_config_value('robot', 'model_path', default='')) # Base position base_pos = self._get_config_value('robot', 'base_position', default=[0, 0, 0]) self._create_vector_input(frame, 2, "Base Position:", "robot.base_position", base_pos) # Base orientation base_orient = self._get_config_value('robot', 'base_orientation', default=[0, 0, 0]) self._create_vector_input(frame, 5, "Base Orientation:", "robot.base_orientation", base_orient, labels=["Roll:", "Pitch:", "Yaw:"]) def _create_dimension_inputs(self, parent: tk.Frame, row: int, key_prefix: str, dimensions: Dict[str, float], dim_names: List[str]) -> None: """Create dimension input fields""" tk.Label(parent, text="Dimensions:").grid(row=row, column=0, sticky="e", padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD) for i, dim_name in enumerate(dim_names): tk.Label(parent, text=f"{dim_name.capitalize()}:").grid(row=row + i, column=1, sticky="e", padx=self.PADDING_STANDARD) entry_key = f"{key_prefix}.{dim_name}" self.entry_widgets[entry_key] = tk.Entry(parent, width=self.ENTRY_WIDTH_STANDARD) self.entry_widgets[entry_key].grid(row=row + i, column=2, padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD) # Get default value from config or use 0 default_val = dimensions.get(dim_name, 0.0) self.entry_widgets[entry_key].insert(0, str(default_val)) def create_wall_tab(self, notebook): """Create wall configuration tab""" frame = ttk.Frame(notebook) notebook.add(frame, text="Wall") # Title self._create_title_label(frame, "Wall Configuration") # Wall position wall_pos = self._get_config_value('wall', 'position', default=[2.0, 0.0, 1.0]) self._create_vector_input(frame, 1, "Position:", "wall.position", wall_pos) # Wall dimensions wall_dims = self._get_config_value('wall', 'dimensions', default={}) # Set defaults if not in config wall_dims.setdefault('width', 3.0) wall_dims.setdefault('height', 2.5) wall_dims.setdefault('thickness', 0.2) self._create_dimension_inputs(frame, 4, "wall.dimensions", wall_dims, ['width', 'height', 'thickness']) def create_hole_tab(self, notebook): """Create hole configuration tab""" frame = ttk.Frame(notebook) notebook.add(frame, text="Hole") # Title self._create_title_label(frame, "Hole Configuration") # Hole position relative to wall hole_pos = self._get_config_value('hole', 'position_relative_to_wall', default=[0, 0, 0]) self._create_vector_input(frame, 1, "Position (relative):", "hole.position", hole_pos) # Hole dimensions hole_dims = self._get_config_value('hole', 'dimensions', default={}) hole_dims.setdefault('width', 0.6) hole_dims.setdefault('height', 0.6) self._create_dimension_inputs(frame, 4, "hole.dimensions", hole_dims, ['width', 'height']) def _create_horizontal_vector_input(self, parent: tk.Frame, row: int, label: str, key_prefix: str, values: List[float]) -> None: """Create horizontal vector input fields (for compact layout)""" labels = ["X:", "Y:", "Z:"] for i, (lbl, val) in enumerate(zip(labels, values)): tk.Label(parent, text=lbl).grid(row=row, column=i*2, sticky="e", padx=self.PADDING_STANDARD) entry_key = f"{key_prefix}.{lbl.lower().rstrip(':')}" self.entry_widgets[entry_key] = tk.Entry(parent, width=self.ENTRY_WIDTH_STANDARD) self.entry_widgets[entry_key].grid(row=row, column=i*2+1, padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD) self.entry_widgets[entry_key].insert(0, str(val)) def create_task_points_tab(self, notebook): """Create task points configuration tab""" frame = ttk.Frame(notebook) notebook.add(frame, text="Task Points") # Title self._create_title_label(frame, "Task Points Configuration") # Point A self._create_title_label(frame, "Point A (Pick-up):", row=1, font_type="section") point_a = self._get_config_value('task_points', 'point_A', 'position', default=[1.0, 0.5, 0.8]) self._create_horizontal_vector_input(frame, 2, "Point A", "point_a", point_a) # Point B self._create_title_label(frame, "Point B (Drop-off):", row=3, font_type="section") point_b = self._get_config_value('task_points', 'point_B', 'position', default=[3.0, 0.5, 0.8]) self._create_horizontal_vector_input(frame, 4, "Point B", "point_b", point_b) def create_transport_object_tab(self, notebook): """Create transport object configuration tab""" frame = ttk.Frame(notebook) notebook.add(frame, text="Transport Object") # Title self._create_title_label(frame, "Transport Object Configuration") # Initial position obj_pos = self._get_config_value('transport_object', 'initial_position', default=[1.0, 0.5, 0.5]) self._create_vector_input(frame, 1, "Initial Position:", "object.position", obj_pos) # Dimensions obj_dims = self._get_config_value('transport_object', 'dimensions', default={}) obj_dims.setdefault('width', 0.1) obj_dims.setdefault('height', 0.1) obj_dims.setdefault('depth', 0.1) self._create_dimension_inputs(frame, 4, "object.dimensions", obj_dims, ['width', 'height', 'depth']) # Mass mass = self._get_config_value('transport_object', 'mass', default=0.5) tk.Label(frame, text="Mass (kg):").grid(row=7, column=0, sticky="e", padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD) self.entry_widgets['object.mass'] = tk.Entry(frame, width=self.ENTRY_WIDTH_STANDARD) self.entry_widgets['object.mass'].grid(row=7, column=2, padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD) self.entry_widgets['object.mass'].insert(0, str(mass)) def create_simulation_tab(self, notebook): """Create simulation configuration tab""" frame = ttk.Frame(notebook) notebook.add(frame, text="Simulation") # Title self._create_title_label(frame, "Simulation Configuration") # Timestep timestep = self._get_config_value('simulation', 'timestep', default=0.01) tk.Label(frame, text="Timestep:").grid(row=1, column=0, sticky="e", padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD) self.entry_widgets['simulation.timestep'] = tk.Entry(frame, width=self.ENTRY_WIDTH_STANDARD) self.entry_widgets['simulation.timestep'].grid(row=1, column=1, padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD) self.entry_widgets['simulation.timestep'].insert(0, str(timestep)) # Gravity gravity = self._get_config_value('simulation', 'gravity', default=[0, 0, -9.81]) self._create_vector_input(frame, 2, "Gravity:", "simulation.gravity", gravity) # Real-time mode real_time = self._get_config_value('simulation', 'real_time', default=False) tk.Label(frame, text="Real-time Mode:").grid(row=5, column=0, sticky="e", padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD) self.real_time_var = tk.BooleanVar() self.real_time_var.set(real_time) tk.Checkbutton(frame, variable=self.real_time_var).grid(row=5, column=1, padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD, sticky="w") def _get_vector_from_widgets(self, key_prefix: str) -> List[float]: """Get vector values from widgets""" return [ float(self.entry_widgets[f'{key_prefix}.x'].get()), float(self.entry_widgets[f'{key_prefix}.y'].get()), float(self.entry_widgets[f'{key_prefix}.z'].get()) ] def _get_vector_custom_from_widgets(self, key_prefix: str, labels: List[str]) -> List[float]: """Get vector values from widgets with custom labels""" return [float(self.entry_widgets[f'{key_prefix}.{label}'].get()) for label in labels] def _ensure_nested_dict(self, *keys) -> None: """Ensure nested dictionary structure exists""" current = self.config_data for key in keys: if key not in current: current[key] = {} current = current[key] def get_values_from_widgets(self) -> Dict[str, Any]: """Get values from all entry widgets and update config_data""" try: # Ensure all necessary nested structures exist self._ensure_nested_dict('robot') self._ensure_nested_dict('wall', 'dimensions') self._ensure_nested_dict('hole', 'dimensions') self._ensure_nested_dict('task_points', 'point_A') self._ensure_nested_dict('task_points', 'point_B') self._ensure_nested_dict('transport_object', 'dimensions') self._ensure_nested_dict('simulation') # Robot configuration self.config_data['robot']['model_path'] = self.entry_widgets['robot.model_path'].get() self.config_data['robot']['base_position'] = self._get_vector_from_widgets('robot.base_position') self.config_data['robot']['base_orientation'] = self._get_vector_custom_from_widgets( 'robot.base_orientation', ['roll', 'pitch', 'yaw']) # Wall configuration self.config_data['wall']['position'] = self._get_vector_from_widgets('wall.position') for dim in ['width', 'height', 'thickness']: self.config_data['wall']['dimensions'][dim] = float( self.entry_widgets[f'wall.dimensions.{dim}'].get()) # Hole configuration self.config_data['hole']['position_relative_to_wall'] = self._get_vector_from_widgets('hole.position') for dim in ['width', 'height']: self.config_data['hole']['dimensions'][dim] = float( self.entry_widgets[f'hole.dimensions.{dim}'].get()) # Task points self.config_data['task_points']['point_A']['position'] = self._get_vector_from_widgets('point_a') self.config_data['task_points']['point_B']['position'] = self._get_vector_from_widgets('point_b') # Transport object self.config_data['transport_object']['initial_position'] = self._get_vector_from_widgets('object.position') for dim in ['width', 'height', 'depth']: self.config_data['transport_object']['dimensions'][dim] = float( self.entry_widgets[f'object.dimensions.{dim}'].get()) self.config_data['transport_object']['mass'] = float(self.entry_widgets['object.mass'].get()) # Simulation self.config_data['simulation']['timestep'] = float(self.entry_widgets['simulation.timestep'].get()) self.config_data['simulation']['gravity'] = self._get_vector_from_widgets('simulation.gravity') self.config_data['simulation']['real_time'] = self.real_time_var.get() return self.config_data except (ValueError, KeyError) as e: # Allow UI to handle the error display raise ValueError(f"Invalid input or configuration structure: {e}") def apply_changes(self): """Apply configuration changes""" config = self.get_values_from_widgets() if self.on_apply_callback: self.on_apply_callback(config) def save_changes(self): """Save configuration changes to file""" self.config_data = self.get_values_from_widgets() self.save_config() def reset_values(self): """Reset values to original configuration""" self.load_config() # Refresh window if self.window: self.window.destroy() self.show()