SXXXXXXX_ControlPanel/ui.py
2025-04-09 15:47:53 +02:00

344 lines
18 KiB
Python

# --- 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("<<ComboboxSelected>>", 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("<<ComboboxSelected>>", 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("<<ComboboxSelected>>", 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 ---