SXXXXXXX_ControlPanel/ui.py

519 lines
22 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, 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 compact layout with Size/Palette and Contrast/Brightness on same rows."""
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=4, sticky=tk.W, padx=5, pady=2
)
logging.debug(f"{log_prefix} Test Image checkbox created.")
sar_row += 1 # Now sar_row is 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:
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 getting initial SAR size from state: {e}. Using default.")
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)
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)
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)
logging.debug(f"{log_prefix} Size and Palette controls created on row {sar_row}.")
sar_row += 1 # Now sar_row is 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 # Pad left of label
)
self.contrast_scale = ttk.Scale(
self.sar_params_frame,
orient=tk.HORIZONTAL,
from_=0.1,
to=3.0,
value=self.app.state.sar_contrast,
command=self.app.update_contrast,
)
# Grid in column 1, add right padding for spacing before next label
self.contrast_scale.grid(
row=sar_row, column=1, sticky=tk.EW, padx=(0, 10), pady=1
)
logging.debug(f"{log_prefix} Contrast scale created.")
self.brightness_label = ttk.Label(self.sar_params_frame, text="Brightness:")
# Grid in column 2, less left padding
self.brightness_label.grid(
row=sar_row, column=2, sticky=tk.W, padx=(0, 2), pady=1 # Space before label
)
self.brightness_scale = ttk.Scale(
self.sar_params_frame,
orient=tk.HORIZONTAL,
from_=-100,
to=100,
value=self.app.state.sar_brightness,
command=self.app.update_brightness,
)
# Grid in column 3, add right padding
self.brightness_scale.grid(
row=sar_row, column=3, sticky=tk.EW, padx=(0, 5), pady=1 # Space after scale
)
logging.debug(f"{log_prefix} Brightness scale created.")
logging.debug(f"{log_prefix} Contrast and Brightness controls created on row {sar_row}.")
# sar_row += 1 # No more SAR controls after this
# Configure column weights for expansion (already done previously, still correct)
self.sar_params_frame.columnconfigure(1, weight=1)
self.sar_params_frame.columnconfigure(3, weight=1)
# --- SAR Info Frame ---
# (No changes needed in this 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)
# ... (rest of the labels remain the same) ...
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): SAR=0, MFD=0, Tk=0, Mouse=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): 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 ---
# (No changes needed in this 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)
# ... (rest of the MFD setup remains the same) ...
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"
)
initial_intensity = config.DEFAULT_MFD_INTENSITY
try:
initial_intensity = self.app.state.mfd_params["categories"][
internal_name
]["intensity"]
except Exception as e:
logging.error(
f"{log_prefix} Could not get initial intensity for {internal_name} from AppState: {e}"
)
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:
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 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)
initial_raw_intensity = config.DEFAULT_MFD_RAW_MAP_INTENSITY
try:
initial_raw_intensity = self.app.state.mfd_params["raw_map_intensity"]
except Exception as e:
logging.error(
f"{log_prefix} Could not get initial raw map intensity from AppState: {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,
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."
)
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 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:
# Check widget exists
if hasattr(self, "sar_size_label") and self.sar_size_label.winfo_exists():
self.sar_size_label.config(text=text_to_display)
logging.debug(f"{log_prefix} SAR size label updated successfully.")
else:
logging.warning(
f"{log_prefix} SAR size label widget does not exist or is destroyed."
)
except tk.TclError as e:
logging.warning(
f"{log_prefix} Could not update SAR size label (TclError: {e})"
)
except Exception as e:
logging.exception(f"{log_prefix} Unexpected error updating SAR size 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 ---