SXXXXXXX_ControlPanel/ui.py
2025-04-09 13:37:43 +02:00

482 lines
20 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.
Includes SAR, SAR Info, MFD, and Map Parameters frames."""
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)
# --- 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 # 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.")
# --- 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)
# Labels within SAR Info frame (Text updated by App)
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.sar_size_label = ttk.Label(self.sar_info_frame, text="Image Size: N/A")
self.sar_size_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): S=0, M=0, Tk=0, Mo=0"
)
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): S=0, M=0"
)
self.incomplete_label.pack(side=tk.TOP, anchor=tk.W, padx=5, pady=1)
logging.debug(f"{log_prefix} SAR Info frame 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 # Row counter for MFD frame grid
# Define categories and labels concisely
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:"},
}
for internal_name, params in mfd_categories_ui_setup.items():
# Label for category
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 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 initial 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=150,
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=mfd_row, column=1, 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=mfd_row, column=2, 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 initial MFD color for {internal_name}: {e}"
)
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 # Store label reference
mfd_row += 1 # Increment row for next category
# 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)
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 initial 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=150,
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()
),
)
raw_map_slider.grid(row=mfd_row, column=1, sticky=tk.EW, padx=1, pady=1)
# Configure column weight for expansion in MFD frame
self.mfd_params_frame.columnconfigure(1, weight=1)
logging.debug(f"{log_prefix} MFD Parameters frame created.")
# --- NEW: Map Parameters Frame ---
logging.debug(f"{log_prefix} Creating Map Parameters frame...")
self.map_params_frame = ttk.Labelframe(self, text="Map Parameters", padding=5)
# Pack below MFD frame
self.map_params_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=2)
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 new App callback function (needs to be created in App class)
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.")
# --- 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}"
logging.debug(
f"{log_prefix} Setting mouse coordinates label to: '{text_to_display}'"
)
try:
if (
hasattr(self, "mouse_latlon_label")
and self.mouse_latlon_label.winfo_exists()
):
self.mouse_latlon_label.config(text=text_to_display)
# else: logging.warning(f"{log_prefix} Mouse coordinates label widget does not exist.")
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}"
logging.debug(
f"{log_prefix} Setting SAR orientation label to: '{text_to_display}'"
)
try:
if (
hasattr(self, "sar_orientation_label")
and self.sar_orientation_label.winfo_exists()
):
self.sar_orientation_label.config(text=text_to_display)
# else: logging.warning(f"{log_prefix} SAR orientation label widget does not exist.")
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}"
logging.debug(f"{log_prefix} Setting SAR size label to: '{text_to_display}'")
try:
if hasattr(self, "sar_size_label") and self.sar_size_label.winfo_exists():
self.sar_size_label.config(text=text_to_display)
# else: logging.warning(f"{log_prefix} SAR size label widget does not exist.")
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]"
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:
if target_label.winfo_exists():
# Ensure values are ints and format hex color 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)
# else: logging.warning(f"{log_prefix} MFD color label for '{category_name}' does not exist.")
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.
Provides a method to set its text content.
"""
def __init__(self, parent: tk.Widget, *args, **kwargs):
"""Initializes the StatusBar label."""
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]"
logging.debug(f"{log_prefix} Setting status bar text to: '{text}'")
try:
if self.winfo_exists():
self.config(text=text)
# else: logging.warning(f"{log_prefix} Status bar widget does not exist.")
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)
# Initial position is set by App class after calculation
logging.debug(f"{log_prefix} Main Tkinter root window created successfully.")
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 ---