# --- START OF FILE ui.py --- # ui.py """ THIS SOFTWARE IS PROVIDED “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. Defines the user interface components for the Control Panel application, including the main control panel area, status bar, map parameters, and helper functions for window creation. Uses standardized logging prefixes. """ # Standard library imports import logging from typing import TYPE_CHECKING, Dict, Tuple # Added Type Hinting imports # Third-party imports import tkinter as tk from tkinter import ttk from tkinter import colorchooser # Local application imports import config # Import config for defaults # Type hinting for App reference if TYPE_CHECKING: from app import App class ControlPanel(ttk.Frame): """ Represents the main control panel frame containing SAR, MFD, and Map parameter widgets, information displays, and statistics labels. Initializes UI elements and provides methods for updating specific labels. """ def __init__(self, parent: tk.Widget, app: 'App', *args, **kwargs): """ Initializes the ControlPanel frame. Args: parent (tk.Widget): The parent widget (usually the main window). app (App): Reference to the main application instance for callbacks. """ log_prefix = "[UI Setup]" logging.debug(f"{log_prefix} Initializing ControlPanel frame...") super().__init__(parent, *args, **kwargs) self.app = app # Store reference to the App instance # Type hint for the dictionary holding MFD color labels self.mfd_color_labels: Dict[str, tk.Label] = {} self.init_ui() logging.debug(f"{log_prefix} ControlPanel frame initialization complete.") def init_ui(self): """Initializes and arranges the user interface widgets within the frame. Order: SAR Params, MFD Params, Map Params, SAR Info. Compacted MFD Params and rearranged SAR Info.""" log_prefix = "[UI Setup]" logging.debug(f"{log_prefix} Starting init_ui widget creation...") # Configure main frame packing self.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5) # --- 1. SAR Parameters Frame --- logging.debug(f"{log_prefix} Creating SAR Parameters frame...") self.sar_params_frame = ttk.Labelframe(self, text="SAR Parameters", padding=5) self.sar_params_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=(5, 2)) # PACK ORDER 1 sar_row = 0 # Row counter for SAR frame grid # Test Image Checkbox self.test_image_var = tk.IntVar(value=1 if config.ENABLE_TEST_MODE else 0) self.test_image_check = ttk.Checkbutton( self.sar_params_frame, text="Test Image", variable=self.test_image_var, command=self.app.update_image_mode, # Callback in App ) self.test_image_check.grid(row=sar_row, column=0, columnspan=4, sticky=tk.W, padx=5, pady=2) sar_row += 1 # -> 1 # Row 1: Size and Palette self.sar_size_label = ttk.Label(self.sar_params_frame, text="Size:") self.sar_size_label.grid(row=sar_row, column=0, sticky=tk.W, padx=(5, 2), pady=1) self.sar_size_combo = ttk.Combobox( self.sar_params_frame, values=config.SAR_SIZE_FACTORS, state="readonly", width=6 ) try: # Set initial SAR size from AppState initial_factor = (config.SAR_WIDTH // self.app.state.sar_display_width if self.app.state.sar_display_width > 0 else 1) initial_size_str = f"1:{initial_factor}" if initial_size_str not in config.SAR_SIZE_FACTORS: initial_size_str = config.DEFAULT_SAR_SIZE self.sar_size_combo.set(initial_size_str) except Exception as e: logging.warning(f"{log_prefix} Error setting initial SAR size: {e}"); self.sar_size_combo.set(config.DEFAULT_SAR_SIZE) self.sar_size_combo.grid(row=sar_row, column=1, sticky=tk.EW, padx=(0, 10), pady=1) self.sar_size_combo.bind("<>", self.app.update_sar_size) # Callback in App self.palette_label = ttk.Label(self.sar_params_frame, text="Palette:") self.palette_label.grid(row=sar_row, column=2, sticky=tk.W, padx=(0, 2), pady=1) self.palette_combo = ttk.Combobox( self.sar_params_frame, values=config.COLOR_PALETTES, state="readonly", width=8 ) self.palette_combo.set(self.app.state.sar_palette) # Initial value from AppState self.palette_combo.grid(row=sar_row, column=3, sticky=tk.EW, padx=(0, 5), pady=1) self.palette_combo.bind("<>", self.app.update_sar_palette) # Callback in App sar_row += 1 # -> 2 # Row 2: Contrast and Brightness self.contrast_label = ttk.Label(self.sar_params_frame, text="Contrast:") self.contrast_label.grid(row=sar_row, column=0, sticky=tk.W, padx=(5, 2), pady=1) self.contrast_scale = ttk.Scale( self.sar_params_frame, orient=tk.HORIZONTAL, from_=0.1, to=3.0, value=self.app.state.sar_contrast, # Initial value from AppState command=self.app.update_contrast # Callback in App ) self.contrast_scale.grid(row=sar_row, column=1, sticky=tk.EW, padx=(0, 10), pady=1) self.brightness_label = ttk.Label(self.sar_params_frame, text="Brightness:") self.brightness_label.grid(row=sar_row, column=2, sticky=tk.W, padx=(0, 2), pady=1) self.brightness_scale = ttk.Scale( self.sar_params_frame, orient=tk.HORIZONTAL, from_=-100, to=100, value=self.app.state.sar_brightness, # Initial value from AppState command=self.app.update_brightness # Callback in App ) self.brightness_scale.grid(row=sar_row, column=3, sticky=tk.EW, padx=(0, 5), pady=1) # Configure column weights for expansion in SAR frame self.sar_params_frame.columnconfigure(1, weight=1) self.sar_params_frame.columnconfigure(3, weight=1) logging.debug(f"{log_prefix} SAR Parameters frame created and packed.") # --- 2. MFD Parameters Frame --- logging.debug(f"{log_prefix} Creating MFD Parameters frame...") self.mfd_params_frame = ttk.Labelframe(self, text="MFD Parameters", padding=5) self.mfd_params_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=2) # PACK ORDER 2 # Define categories in the desired display order for pairing mfd_categories_ordered = ["Occlusion", "Cat A", "Cat B", "Cat C", "Cat C1", "Cat C2", "Cat C3"] num_categories = len(mfd_categories_ordered) logging.debug(f"{log_prefix} Creating compacted MFD category controls...") for index, internal_name in enumerate(mfd_categories_ordered): current_row = index // 2 # Integer division determines the row (0, 0, 1, 1, 2, 2, 3) col_offset = 0 if (index % 2 == 0) else 4 # Start at column 0 for even index, 4 for odd # Label for category label_text = f"{internal_name}:" label = ttk.Label(self.mfd_params_frame, text=label_text) label.grid(row=current_row, column=0 + col_offset, sticky=tk.W, padx=(5, 1), pady=1) # Intensity Slider intensity_var_name = f"mfd_{internal_name.replace(' ', '_').lower()}_intensity_var" initial_intensity = config.DEFAULT_MFD_INTENSITY # Default try: # Get initial from AppState initial_intensity = self.app.state.mfd_params["categories"][internal_name]["intensity"] except Exception as e: logging.warning(f"{log_prefix} Error getting MFD intensity for {internal_name}: {e}") intensity_var = tk.IntVar(value=initial_intensity) setattr(self, intensity_var_name, intensity_var) # Store variable reference slider = ttk.Scale( self.mfd_params_frame, orient=tk.HORIZONTAL, length=100, from_=0, to=255, # Shorter default length variable=intensity_var, # Use lambda to pass current value and category name to App callback command=lambda v, name=internal_name, var=intensity_var: self.app.update_mfd_category_intensity(name, var.get()) ) slider.grid(row=current_row, column=1 + col_offset, sticky=tk.EW, padx=1, pady=1) # Color Chooser Button color_button = ttk.Button( self.mfd_params_frame, text="Color", width=5, # Use lambda to pass category name to App callback command=lambda name=internal_name: self.app.choose_mfd_category_color(name) ) color_button.grid(row=current_row, column=2 + col_offset, sticky=tk.W, padx=1, pady=1) # Color Preview Label color_label = tk.Label(self.mfd_params_frame, text="", width=3, relief=tk.SUNKEN, borderwidth=1) try: # Set initial color from AppState initial_bgr = self.app.state.mfd_params["categories"][internal_name]["color"] initial_hex = "#{:02x}{:02x}{:02x}".format(initial_bgr[2], initial_bgr[1], initial_bgr[0]) color_label.config(background=initial_hex) except Exception as e: logging.warning(f"{log_prefix} Error setting MFD color for {internal_name}: {e}"); color_label.config(background="grey") color_label.grid(row=current_row, column=3 + col_offset, sticky=tk.W, padx=(1, 5), pady=1) self.mfd_color_labels[internal_name] = color_label # Store label reference # Place Raw Map slider - it goes on the same row as the last odd-indexed category if one exists, or a new row last_cat_row = (num_categories - 1) // 2 raw_map_start_col = 4 if (num_categories % 2 != 0) else 0 # Start at col 4 if last category was on left, else col 0 raw_map_row = last_cat_row if (num_categories % 2 != 0) else last_cat_row + 1 # Use same row if possible raw_map_label = ttk.Label(self.mfd_params_frame, text="Raw Map:") raw_map_label.grid(row=raw_map_row, column=0 + raw_map_start_col, sticky=tk.W, padx=(5, 1), pady=1) initial_raw_intensity = config.DEFAULT_MFD_RAW_MAP_INTENSITY # Default try: initial_raw_intensity = self.app.state.mfd_params["raw_map_intensity"] # Get initial from AppState except Exception as e: logging.warning(f"{log_prefix} Error getting raw map intensity: {e}") self.mfd_raw_map_intensity_var = tk.IntVar(value=initial_raw_intensity) raw_map_slider = ttk.Scale( self.mfd_params_frame, orient=tk.HORIZONTAL, length=100, from_=0, to=255, # Shorter default length variable=self.mfd_raw_map_intensity_var, # Use lambda to pass current value to App callback command=lambda v: self.app.update_mfd_raw_map_intensity(self.mfd_raw_map_intensity_var.get()) ) # Span the slider across remaining columns in its pair (1, 2, 3 or 5, 6, 7) raw_map_slider.grid(row=raw_map_row, column=1 + raw_map_start_col, columnspan=3, sticky=tk.EW, padx=(1, 5), pady=1) # Configure column weights for sliders in MFD frame (Cols 1 and 5) self.mfd_params_frame.columnconfigure(1, weight=1) self.mfd_params_frame.columnconfigure(5, weight=1) # Corresponds to the second slider column logging.debug(f"{log_prefix} MFD Parameters frame created and packed.") # --- 3. Map Parameters Frame --- logging.debug(f"{log_prefix} Creating Map Parameters frame...") self.map_params_frame = ttk.Labelframe(self, text="Map Parameters", padding=5) self.map_params_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=2) # PACK ORDER 3 map_row = 0 # Row counter for Map frame grid # Map Size Combobox self.map_size_label = ttk.Label(self.map_params_frame, text="Map Display Size:") self.map_size_label.grid(row=map_row, column=0, sticky=tk.W, padx=(5, 2), pady=1) self.map_size_combo = ttk.Combobox( self.map_params_frame, values=config.MAP_SIZE_FACTORS, # Use factors defined in config state="readonly", width=6, # Consistent width ) # Set initial value from config default self.map_size_combo.set(config.DEFAULT_MAP_SIZE) self.map_size_combo.grid(row=map_row, column=1, sticky=tk.EW, padx=(2, 5), pady=1) # Bind to the App callback function self.map_size_combo.bind("<>", self.app.update_map_size) # Configure column weight for the combobox column to expand self.map_params_frame.columnconfigure(1, weight=1) logging.debug(f"{log_prefix} Map Parameters frame created and packed.") # --- 4. SAR Info Frame --- logging.debug(f"{log_prefix} Creating SAR Info frame...") self.sar_info_frame = ttk.Labelframe(self, text="SAR Info", padding=5) self.sar_info_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=2) # PACK ORDER 4 info_row = 0 # Row 1: Image Ref self.sar_center_label = ttk.Label(self.sar_info_frame, text="Image Ref: Lat=N/A, Lon=N/A") self.sar_center_label.grid(row=info_row, column=0, columnspan=4, sticky=tk.W, padx=5, pady=1) info_row += 1 # Row 2: Image Orient & Image Size self.sar_orientation_label = ttk.Label(self.sar_info_frame, text="Image Orient: N/A") self.sar_orientation_label.grid(row=info_row, column=0, columnspan=2, sticky=tk.W, padx=5, pady=1) # Span 2 cols self.sar_size_label = ttk.Label(self.sar_info_frame, text="Image Size: N/A") self.sar_size_label.grid(row=info_row, column=2, columnspan=2, sticky=tk.W, padx=5, pady=1) # Span 2 cols info_row += 1 # Row 3: Mouse Coordinates self.mouse_latlon_label = ttk.Label(self.sar_info_frame, text="Mouse : Lat=N/A, Lon=N/A") self.mouse_latlon_label.grid(row=info_row, column=0, columnspan=4, sticky=tk.W, padx=5, pady=1) info_row += 1 # Row 4: Drop & Incomplete self.dropped_label = ttk.Label(self.sar_info_frame, text="Drop (Q): S=0, M=0, Tk=0, Mo=0") # Shortened label self.dropped_label.grid(row=info_row, column=0, columnspan=2, sticky=tk.W, padx=5, pady=1) # Span 2 cols self.incomplete_label = ttk.Label(self.sar_info_frame, text="Incmpl (RX): S=0, M=0") # Shortened label self.incomplete_label.grid(row=info_row, column=2, columnspan=2, sticky=tk.W, padx=5, pady=1) # Span 2 cols # Configure column weights for SAR Info frame for better spacing if needed self.sar_info_frame.columnconfigure(1, weight=1) # Add weight between Orient/Size self.sar_info_frame.columnconfigure(3, weight=1) # Add weight after Size/Incmpl logging.debug(f"{log_prefix} SAR Info frame created and packed.") # --- End of init_ui --- logging.debug(f"{log_prefix} init_ui widget creation complete.") # --- Methods to update UI labels --- def set_mouse_coordinates(self, latitude_str: str, longitude_str: str): """Updates the mouse coordinates label with pre-formatted DMS strings.""" log_prefix = "[UI Update]" text_to_display = f"Mouse : Lat={latitude_str}, Lon={longitude_str}" try: if hasattr(self, "mouse_latlon_label") and self.mouse_latlon_label.winfo_exists(): self.mouse_latlon_label.config(text=text_to_display) except Exception as e: logging.warning(f"{log_prefix} Error updating mouse coords UI: {e}") def set_sar_orientation(self, orientation_deg_str: str): """Updates the SAR orientation label.""" log_prefix = "[UI Update]" text_to_display = f"Image Orient: {orientation_deg_str}" try: if hasattr(self, "sar_orientation_label") and self.sar_orientation_label.winfo_exists(): self.sar_orientation_label.config(text=text_to_display) except Exception as e: logging.warning(f"{log_prefix} Error updating SAR orientation UI: {e}") def set_sar_size_km(self, size_text: str): """Updates the SAR image size label.""" log_prefix = "[UI Update]" text_to_display = f"Image Size: {size_text}" try: if hasattr(self, "sar_size_label") and self.sar_size_label.winfo_exists(): self.sar_size_label.config(text=text_to_display) except Exception as e: logging.warning(f"{log_prefix} Error updating SAR size UI: {e}") # Type hint the color tuple argument def update_mfd_color_display(self, category_name: str, color_bgr_tuple: Tuple[int, int, int]): """Updates the background color of the specified MFD category's display label.""" log_prefix = "[UI Update]" if category_name in self.mfd_color_labels: target_label = self.mfd_color_labels[category_name] try: if target_label.winfo_exists(): hex_color = "#{:02x}{:02x}{:02x}".format(int(color_bgr_tuple[2]), int(color_bgr_tuple[1]), int(color_bgr_tuple[0])) target_label.config(background=hex_color) except Exception as e: logging.error(f"{log_prefix} Error updating color display for {category_name}: {e}", exc_info=True) else: logging.warning(f"{log_prefix} Unknown category key for MFD color display: '{category_name}'") class StatusBar(ttk.Label): """ Represents the status bar at the bottom of the main window. """ def __init__(self, parent: tk.Widget, *args, **kwargs): log_prefix = "[UI Setup]"; logging.debug(f"{log_prefix} Initializing StatusBar...") super().__init__(parent, text=config.INITIAL_STATUS_MESSAGE, relief=tk.SUNKEN, anchor=tk.W, *args, **kwargs) self.pack(side=tk.BOTTOM, fill=tk.X) logging.debug(f"{log_prefix} StatusBar initialization complete.") def set_status_text(self, text: str): """Sets the text displayed in the status bar.""" log_prefix = "[UI Status]" try: if self.winfo_exists(): self.config(text=text) except Exception as e: logging.warning(f"{log_prefix} Error setting status bar text: {e}") def create_main_window(title: str, min_width: int, min_height: int, x_pos: int, y_pos: int) -> tk.Tk: """Creates and configures the main Tkinter root window.""" log_prefix = "[UI Setup]"; logging.debug(f"{log_prefix} Creating main Tkinter root window...") try: root = tk.Tk(); root.title(title); root.minsize(min_width, min_height) logging.debug(f"{log_prefix} Main Tkinter root window created.") return root except Exception as e: logging.critical(f"{log_prefix} Failed to create main Tkinter window: {e}", exc_info=True); raise # --- END OF FILE ui.py ---