540 lines
22 KiB
Python
540 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 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("<<ComboboxSelected>>", 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("<<ComboboxSelected>>", 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.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"
|
|
) # 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 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 ---
|