SXXXXXXX_ControlPanel/ui.py

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