# --- 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, and helper functions for window creation. Uses standardized logging prefixes for UI events. """ # Standard library imports import logging # Third-party imports import tkinter as tk from tkinter import ttk from tkinter import ( colorchooser, ) # Keep even if only used in App callbacks triggered from here # Local application imports import config # Import config for defaults class ControlPanel(ttk.Frame): """ Represents the main control panel frame containing SAR and MFD parameter widgets, information displays, and statistics labels. Initializes UI elements and provides methods for updating specific labels. """ def __init__(self, parent, 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. """ # Define log prefix for UI setup 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 self.mfd_color_labels = {} # To store MFD color preview Labels by category name self.init_ui() logging.debug(f"{log_prefix} ControlPanel frame initialization complete.") # Method within ControlPanel class in ui.py def init_ui(self): """Initializes and arranges the user interface widgets within the frame. Uses a more compact layout.""" log_prefix = "[UI Setup]" logging.debug( f"{log_prefix} Starting init_ui widget creation (compact layout)..." ) self.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5) # --- 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)) sar_row = 0 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, ) self.test_image_check.grid( row=sar_row, column=0, columnspan=2, sticky=tk.W, padx=5, pady=2 ) logging.debug(f"{log_prefix} Test Image checkbox created.") sar_row += 1 # SAR Size self.sar_size_label = ttk.Label(self.sar_params_frame, text="SAR 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=5, ) # Set initial value based on AppState (via app reference) try: 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 ) # Fallback if calculated factor is not in list self.sar_size_combo.set(initial_size_str) except Exception: # Catch potential errors accessing state during init self.sar_size_combo.set(config.DEFAULT_SAR_SIZE) self.sar_size_combo.grid( row=sar_row, column=1, sticky=tk.EW, padx=(2, 5), pady=1 ) self.sar_size_combo.bind("<>", self.app.update_sar_size) logging.debug(f"{log_prefix} SAR Size combobox created.") sar_row += 1 # Contrast Scale 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, length=150, from_=0.1, to=3.0, # Set initial value from AppState value=self.app.state.sar_contrast, command=self.app.update_contrast, ) self.contrast_scale.grid( row=sar_row, column=1, sticky=tk.EW, padx=(2, 5), pady=1 ) logging.debug(f"{log_prefix} Contrast scale created.") sar_row += 1 # Brightness Scale self.brightness_label = ttk.Label(self.sar_params_frame, text="Brightness:") self.brightness_label.grid( row=sar_row, column=0, sticky=tk.W, padx=(5, 2), pady=1 ) self.brightness_scale = ttk.Scale( self.sar_params_frame, orient=tk.HORIZONTAL, length=150, from_=-100, to=100, # Set initial value from AppState value=self.app.state.sar_brightness, command=self.app.update_brightness, ) self.brightness_scale.grid( row=sar_row, column=1, sticky=tk.EW, padx=(2, 5), pady=1 ) logging.debug(f"{log_prefix} Brightness scale created.") sar_row += 1 # Palette Combobox self.palette_label = ttk.Label(self.sar_params_frame, text="Palette:") self.palette_label.grid(row=sar_row, column=0, sticky=tk.W, padx=(5, 2), pady=1) self.palette_combo = ttk.Combobox( self.sar_params_frame, values=config.COLOR_PALETTES, state="readonly", width=8, ) # Set initial value from AppState self.palette_combo.set(self.app.state.sar_palette) self.palette_combo.grid( row=sar_row, column=1, sticky=tk.EW, padx=(2, 5), pady=1 ) self.palette_combo.bind("<>", self.app.update_sar_palette) logging.debug(f"{log_prefix} Palette combobox created.") self.sar_params_frame.columnconfigure(1, weight=1) # --- 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) # SAR Info Labels (Initial text set here, updated by App.update_status) self.sar_center_label = ttk.Label( self.sar_info_frame, text="Image Ref: Lat=N/A, Lon=N/A" ) self.sar_center_label.pack(side=tk.TOP, anchor=tk.W, padx=5, pady=1) self.sar_orientation_label = ttk.Label( self.sar_info_frame, text="Image Orient: N/A" ) self.sar_orientation_label.pack(side=tk.TOP, anchor=tk.W, padx=5, pady=1) self.mouse_latlon_label = ttk.Label( self.sar_info_frame, text="Mouse : Lat=N/A, Lon=N/A" ) self.mouse_latlon_label.pack(side=tk.TOP, anchor=tk.W, padx=5, pady=1) self.dropped_label = ttk.Label( self.sar_info_frame, text="Dropped (Q): SAR=0, MFD=0, Tk=0, Mouse=0" ) # Initial text self.dropped_label.pack(side=tk.TOP, anchor=tk.W, padx=5, pady=1) self.incomplete_label = ttk.Label( self.sar_info_frame, text="Incomplete (RX): SAR=0, MFD=0" ) self.incomplete_label.pack(side=tk.TOP, anchor=tk.W, padx=5, pady=1) logging.debug(f"{log_prefix} SAR Info labels created.") # --- 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) mfd_row = 0 mfd_categories_ui_setup = { "Occlusion": {"label_text": "Occlusion:"}, "Cat A": {"label_text": "Cat A:"}, "Cat B": {"label_text": "Cat B:"}, "Cat C": {"label_text": "Cat C:"}, "Cat C1": {"label_text": "Cat C1:"}, "Cat C2": {"label_text": "Cat C2:"}, "Cat C3": {"label_text": "Cat C3:"}, } logging.debug(f"{log_prefix} Creating MFD category controls...") for internal_name, params in mfd_categories_ui_setup.items(): label = ttk.Label(self.mfd_params_frame, text=params["label_text"]) label.grid(row=mfd_row, column=0, sticky=tk.W, padx=(5, 1), pady=1) intensity_var_name = ( f"mfd_{internal_name.replace(' ', '_').lower()}_intensity_var" ) # Read initial intensity from AppState initial_intensity = config.DEFAULT_MFD_INTENSITY # Default fallback try: initial_intensity = self.app.state.mfd_params["categories"][ internal_name ]["intensity"] except Exception: logging.error( f"{log_prefix} Could not get initial intensity for {internal_name} from AppState." ) intensity_var = tk.IntVar(value=initial_intensity) setattr(self, intensity_var_name, intensity_var) slider = ttk.Scale( self.mfd_params_frame, orient=tk.HORIZONTAL, length=150, from_=0, to=255, variable=intensity_var, command=lambda v, name=internal_name, var=intensity_var: self.app.update_mfd_category_intensity( name, var.get() ), ) slider.grid(row=mfd_row, column=1, sticky=tk.EW, padx=1, pady=1) color_button = ttk.Button( self.mfd_params_frame, text="Color", width=5, command=lambda name=internal_name: self.app.choose_mfd_category_color( name ), ) color_button.grid(row=mfd_row, column=2, sticky=tk.W, padx=1, pady=1) color_label = tk.Label( self.mfd_params_frame, text="", width=3, relief=tk.SUNKEN, borderwidth=1 ) try: # --- CORRECTION HERE --- # Access mfd_params through self.app.state initial_bgr = self.app.state.mfd_params["categories"][internal_name][ "color" ] # --- END CORRECTION --- initial_hex = "#{:02x}{:02x}{:02x}".format( initial_bgr[2], initial_bgr[1], initial_bgr[0] ) color_label.config(background=initial_hex) except KeyError: logging.error( f"{log_prefix} MFD category '{internal_name}' not found in app state for initial color. Using grey." ) color_label.config(background="grey") except Exception as e: logging.error( f"{log_prefix} Failed to set initial color for {internal_name} from AppState: {e}. Using grey." ) color_label.config(background="grey") color_label.grid(row=mfd_row, column=3, sticky=tk.W, padx=(1, 5), pady=1) self.mfd_color_labels[internal_name] = color_label mfd_row += 1 logging.debug(f"{log_prefix} MFD category controls created.") # Raw Map Intensity Slider logging.debug(f"{log_prefix} Creating Raw Map intensity slider...") raw_map_label = ttk.Label(self.mfd_params_frame, text="Raw Map:") raw_map_label.grid(row=mfd_row, column=0, sticky=tk.W, padx=(5, 1), pady=1) # Read initial value from AppState initial_raw_intensity = config.DEFAULT_MFD_RAW_MAP_INTENSITY # Default fallback try: initial_raw_intensity = self.app.state.mfd_params["raw_map_intensity"] except Exception: logging.error( f"{log_prefix} Could not get initial raw map intensity from AppState." ) 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=150, from_=0, to=255, variable=self.mfd_raw_map_intensity_var, command=lambda v: self.app.update_mfd_raw_map_intensity( self.mfd_raw_map_intensity_var.get() ), ) raw_map_slider.grid(row=mfd_row, column=1, sticky=tk.EW, padx=1, pady=1) logging.debug(f"{log_prefix} Raw Map intensity slider created.") self.mfd_params_frame.columnconfigure(1, weight=1) logging.debug( f"{log_prefix} init_ui widget creation complete (compact layout)." ) def set_mouse_coordinates(self, latitude_str, longitude_str): """Updates the mouse coordinates label with pre-formatted DMS strings.""" log_prefix = "[UI Update]" # Use update prefix text_to_display = f"Mouse : Lat={latitude_str}, Lon={longitude_str}" # DEBUG log for the requested update logging.debug( f"{log_prefix} Setting mouse coordinates label to: '{text_to_display}'" ) try: # Check widget exists before configuring if ( hasattr(self, "mouse_latlon_label") and self.mouse_latlon_label.winfo_exists() ): self.mouse_latlon_label.config(text=text_to_display) logging.debug( f"{log_prefix} Mouse coordinates label updated successfully." ) else: logging.warning( f"{log_prefix} Mouse coordinates label widget does not exist or is destroyed." ) except tk.TclError as e: # WARNING for Tkinter errors (usually means window closed) logging.warning( f"{log_prefix} Could not update mouse coordinates label (TclError: {e})" ) except Exception as e: # Keep EXCEPTION for unexpected errors logging.exception( f"{log_prefix} Unexpected error updating mouse coordinates label:" ) def set_sar_orientation(self, orientation_deg_str): """Updates the SAR orientation label.""" log_prefix = "[UI Update]" # Use update prefix text_to_display = f"Image Orient: {orientation_deg_str}" # DEBUG log for the requested update logging.debug( f"{log_prefix} Setting SAR orientation label to: '{text_to_display}'" ) try: # Check widget exists if ( hasattr(self, "sar_orientation_label") and self.sar_orientation_label.winfo_exists() ): self.sar_orientation_label.config(text=text_to_display) logging.debug( f"{log_prefix} SAR orientation label updated successfully." ) else: logging.warning( f"{log_prefix} SAR orientation label widget does not exist or is destroyed." ) except tk.TclError as e: # WARNING for Tkinter errors logging.warning( f"{log_prefix} Could not update SAR orientation label (TclError: {e})" ) except Exception as e: # Keep EXCEPTION for unexpected errors logging.exception( f"{log_prefix} Unexpected error updating SAR orientation label:" ) def update_mfd_color_display(self, category_name, color_bgr_tuple): """Updates the background color of the specified MFD category's display label.""" log_prefix = "[UI Update]" # Use update prefix # DEBUG log for requested update logging.debug( f"{log_prefix} Updating MFD color display for '{category_name}' to BGR={color_bgr_tuple}" ) if category_name in self.mfd_color_labels: target_label = self.mfd_color_labels[category_name] try: # Check widget exists if target_label.winfo_exists(): # Convert BGR tuple (0-255) to #RRGGBB hex string 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) logging.debug( f"{log_prefix} Updated color display for '{category_name}' to hex {hex_color}" ) else: logging.warning( f"{log_prefix} MFD color label for '{category_name}' does not exist or is destroyed." ) except tk.TclError as e: # WARNING for Tkinter errors logging.warning( f"{log_prefix} Could not update color display for {category_name} (TclError: {e})" ) except Exception as e: # Keep ERROR for formatting/update errors specific to this widget logging.error( f"{log_prefix} Error updating color display for {category_name} (Color: {color_bgr_tuple}): {e}", exc_info=True, ) else: # WARNING for unknown category key logging.warning( f"{log_prefix} Attempted to update color display for unknown category key: '{category_name}'" ) class StatusBar(ttk.Label): """ Represents the status bar at the bottom of the main window. Provides a method to set its text content. """ def __init__(self, parent, *args, **kwargs): """Initializes the StatusBar label.""" log_prefix = "[UI Setup]" # Use setup prefix 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): """Sets the text displayed in the status bar.""" log_prefix = "[UI Status]" # Specific prefix for status updates # DEBUG log for the update action logging.debug(f"{log_prefix} Setting status bar text to: '{text}'") try: # Check widget exists if self.winfo_exists(): self.config(text=text) logging.debug(f"{log_prefix} Status bar text updated successfully.") else: logging.warning( f"{log_prefix} Status bar widget does not exist or is destroyed." ) except tk.TclError as e: # WARNING for Tkinter errors logging.warning( f"{log_prefix} Could not update status bar text (TclError: {e})" ) except Exception as e: # Keep EXCEPTION for unexpected errors logging.exception(f"{log_prefix} Unexpected error setting status bar text:") def create_main_window(title, min_width, min_height, x_pos, y_pos): """Creates and configures the main Tkinter root window.""" log_prefix = "[UI Setup]" # Use setup prefix logging.debug(f"{log_prefix} Creating main Tkinter root window...") try: root = tk.Tk() root.title(title) root.minsize(min_width, min_height) # Note: Initial position is now set within App.__init__ after calculations # root.geometry(f"+{x_pos}+{y_pos}") # This line can be removed if App sets it logging.debug(f"{log_prefix} Main Tkinter root window created successfully.") return root except Exception as e: # CRITICAL if main window creation fails logging.critical( f"{log_prefix} Failed to create main Tkinter window: {e}", exc_info=True ) # Depending on the error, might need to exit or raise raise # Re-raise the exception after logging # --- END OF FILE ui.py ---