# --- 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 # Original before renaming main script from ControlPanel import ControlPanelApp # Use the new main app class name 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): # Original before renaming main script def __init__( self, parent: tk.Widget, app: "ControlPanelApp", *args, **kwargs ): # Use new main app class name """ Initializes the ControlPanel frame. Args: parent (tk.Widget): The parent widget (usually the main window). app (ControlPanelApp): 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 # --- >>> START OF MODIFIED SECTION IN SAR FRAME <<< --- # Row 0: Test Image and Record SAR Checkboxes # 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 ) # Place Test Image checkbox in column 0, spanning 2 columns initially self.test_image_check.grid( row=sar_row, column=0, columnspan=2, sticky=tk.W, padx=5, pady=2 ) # Record SAR Checkbox self.record_sar_var = tk.BooleanVar(value=config.DEFAULT_SAR_RECORDING_ENABLED) self.record_sar_check = ttk.Checkbutton( self.sar_params_frame, text="Record SAR", variable=self.record_sar_var, command=self.app.toggle_sar_recording, # New callback in App/ControlPanel ) # Place Record SAR checkbox in column 2, spanning 2 columns initially self.record_sar_check.grid( row=sar_row, column=2, columnspan=2, sticky=tk.W, padx=5, pady=2 ) sar_row += 1 # -> 1 # --- >>> END OF MODIFIED SECTION IN SAR FRAME <<< --- # Row 1 (Now 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 = 1 if self.app.state.sar_display_width > 0: initial_factor = config.SAR_WIDTH // self.app.state.sar_display_width 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 (Now 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 col_offset = 0 if (index % 2 == 0) else 4 # Column offset for right pair # 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, 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 = ( f"#{initial_bgr[2]:02x}{initial_bgr[1]:02x}{initial_bgr[0]:02x}" ) 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") # Fallback color 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 last_cat_row = (num_categories - 1) // 2 raw_map_start_col = 4 if (num_categories % 2 != 0) else 0 raw_map_row = last_cat_row if (num_categories % 2 != 0) else last_cat_row + 1 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: # Get initial from AppState initial_raw_intensity = self.app.state.mfd_params["raw_map_intensity"] 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, 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 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 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 # --- ROW 0: Map Display Size --- 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, state="readonly", width=6, ) self.map_size_combo.set(config.DEFAULT_MAP_SIZE) # Set initial value self.map_size_combo.grid( row=map_row, column=1, sticky=tk.EW, padx=(2, 10), pady=1 ) self.map_size_combo.bind("<>", self.app.update_map_size) map_row += 1 # -> 1 # --- ROW 1: SAR Overlay Checkbox --- self.sar_overlay_var = tk.BooleanVar( value=self.app.state.map_sar_overlay_enabled ) self.sar_overlay_check = ttk.Checkbutton( self.map_params_frame, text="Show SAR Overlay on Map", variable=self.sar_overlay_var, command=self.app.toggle_sar_overlay, # Callback in App/ControlPanel ) self.sar_overlay_check.grid( row=map_row, column=0, columnspan=2, sticky=tk.W, padx=5, pady=2 ) map_row += 1 # -> 2 # --- ROW 2: SAR Overlay Alpha Slider --- self.alpha_label = ttk.Label(self.map_params_frame, text="SAR Overlay Alpha:") self.alpha_label.grid(row=map_row, column=0, sticky=tk.W, padx=(5, 2), pady=1) self.sar_overlay_alpha_var = tk.DoubleVar( value=self.app.state.map_sar_overlay_alpha ) self.alpha_scale = ttk.Scale( self.map_params_frame, orient=tk.HORIZONTAL, from_=0.0, # Min alpha to=1.0, # Max alpha variable=self.sar_overlay_alpha_var, command=self.app.update_sar_overlay_alpha, # Callback in App/ControlPanel ) self.alpha_scale.grid( row=map_row, column=1, columnspan=3, sticky=tk.EW, padx=(0, 5), pady=1 ) map_row += 1 # -> 3 # Configure column weights for expansion self.map_params_frame.columnconfigure(1, weight=1) self.map_params_frame.columnconfigure(2, weight=1) self.map_params_frame.columnconfigure(3, 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 0: 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 1: 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 ) 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 ) info_row += 1 # Row 2: 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 3: Drop & Incomplete Stats self.dropped_label = ttk.Label( self.sar_info_frame, text="Drop (Q): S=0, M=0, Tk=0, Mo=0" ) self.dropped_label.grid( row=info_row, column=0, columnspan=2, sticky=tk.W, padx=5, pady=1 ) self.incomplete_label = ttk.Label( self.sar_info_frame, text="Incmpl (RX): S=0, M=0" ) self.incomplete_label.grid( row=info_row, column=2, columnspan=2, sticky=tk.W, padx=5, pady=1 ) # Configure column weights for SAR Info frame for better spacing self.sar_info_frame.columnconfigure( 1, weight=1 ) # Space between cols 0/1 and 2/3 self.sar_info_frame.columnconfigure(3, weight=1) # Space after col 2/3 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: # Check attribute exists and widget exists label = getattr(self, "mouse_latlon_label", None) if label and label.winfo_exists(): 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: label = getattr(self, "sar_orientation_label", None) if label and label.winfo_exists(): 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: label = getattr(self, "sar_size_label", None) if label and label.winfo_exists(): 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(): # Format BGR tuple to HEX color string hex_color = ( f"#{int(color_bgr_tuple[2]):02x}" f"{int(color_bgr_tuple[1]):02x}" f"{int(color_bgr_tuple[0]):02x}" ) 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: # Update config only if widget still exists 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) # Set initial position using geometry string root.geometry(f"+{x_pos}+{y_pos}") logging.debug( f"{log_prefix} Main Tkinter root window created and positioned " f"at ({x_pos},{y_pos})." ) return root except Exception as e: logging.critical( f"{log_prefix} Failed to create main Tkinter window: {e}", exc_info=True ) raise # Re-raise the exception after logging # --- END OF FILE ui.py ---