From 23232b60391a58593db2ec01c3e84f3a433cdc3a Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Thu, 15 May 2025 16:39:47 +0200 Subject: [PATCH] fix graphics --- flightmonitor/gui/main_window.py | 531 +++++++++++++++++++++---------- 1 file changed, 368 insertions(+), 163 deletions(-) diff --git a/flightmonitor/gui/main_window.py b/flightmonitor/gui/main_window.py index 497aa0e..47e211a 100644 --- a/flightmonitor/gui/main_window.py +++ b/flightmonitor/gui/main_window.py @@ -1,21 +1,26 @@ # FlightMonitor/gui/main_window.py +""" +Main window of the Flight Monitor application. +Handles the layout with multiple notebooks for functions and views, +user interactions, status display including a semaphore, and logging. +""" import tkinter as tk from tkinter import ttk from tkinter import messagebox from tkinter.scrolledtext import ScrolledText from tkinter import font as tkFont -from typing import List, Dict, Optional, Tuple +from typing import List, Dict, Optional, Tuple, Any # Relative imports from ..data import config -from ..utils.logger import get_logger # Solo get_logger è usato direttamente qui +from ..utils.logger import get_logger from ..data.common_models import CanonicalFlightState module_logger = get_logger(__name__) # flightmonitor.gui.main_window # --- Constants for Semaphore --- SEMAPHORE_SIZE = 12 -SEMAPHORE_PAD = 3 # Aumentato leggermente per più spazio visivo +SEMAPHORE_PAD = 3 SEMAPHORE_BORDER_WIDTH = 1 SEMAPHORE_TOTAL_SIZE = SEMAPHORE_SIZE + 2 * (SEMAPHORE_PAD + SEMAPHORE_BORDER_WIDTH) @@ -23,129 +28,182 @@ SEMAPHORE_COLOR_OK = "green3" SEMAPHORE_COLOR_WARNING = "gold" SEMAPHORE_COLOR_ERROR = "red2" SEMAPHORE_COLOR_UNKNOWN = "gray70" -SEMAPHORE_COLOR_FETCHING = "sky blue" # Nuovo colore per quando sta attivamente facendo qualcosa +SEMAPHORE_COLOR_FETCHING = "sky blue" class MainWindow: """ Main window of the Flight Monitor application. - Handles the layout, user interactions, and status display including a semaphore. + Handles layout with function and view notebooks, interactions, status, and logging. """ def __init__(self, root: tk.Tk, controller): + """ + Initializes the main window. + + Args: + root (tk.Tk): The root Tkinter object. + controller: The application controller instance. + """ self.root = root self.controller = controller self.root.title("Flight Monitor") - # Stime delle altezze dei componenti - controls_bbox_estimated_height = 120 - log_area_min_height = 80 # Altezza minima desiderata per il log quando si ridimensiona + # Estimate heights for layout + controls_estimated_height = 50 # Height of just the control buttons/radios + bbox_estimated_height = 70 # Height of just the bbox input fields + log_area_min_height = 80 map_min_height = 200 status_bar_actual_height = 30 window_padding_buffer = 20 - min_total_height = (controls_bbox_estimated_height + - map_min_height + - log_area_min_height + - status_bar_actual_height + - window_padding_buffer) + # Calculate minimum acceptable window size + min_function_notebook_height = controls_estimated_height + bbox_estimated_height + 20 # Buffer for padding/tabs + min_views_notebook_height = map_min_height + 20 # Buffer for tabs + min_paned_top_panel_height = min_function_notebook_height + min_views_notebook_height + min_paned_bottom_panel_height = log_area_min_height + status_bar_actual_height + 10 # Buffer for spacing + + min_total_height = min_paned_top_panel_height + min_paned_bottom_panel_height + 10 # Buffer for panedwindow separator min_total_width = config.DEFAULT_CANVAS_WIDTH // 2 + 100 if min_total_width < 500: min_total_width = 500 - self.root.minsize(min_total_width, min_total_height) # Questo è corretto per la finestra principale + self.root.minsize(min_total_width, min_total_height) module_logger.debug(f"Minimum window size set to: {min_total_width}x{min_total_height}") - initial_width = config.DEFAULT_CANVAS_WIDTH + 250 - initial_height = (config.DEFAULT_CANVAS_HEIGHT + - controls_bbox_estimated_height + - 100 + # Altezza iniziale log - status_bar_actual_height + - window_padding_buffer + 20) - - if initial_height < min_total_height: - initial_height = min_total_height - - self.root.geometry(f"{initial_width}x{initial_height}") - module_logger.debug(f"Initial window size set to: {initial_width}x{initial_height}") - # --- Main Layout: PanedWindow --- + # Calculate initial window size + initial_width = config.DEFAULT_CANVAS_WIDTH + 250 + initial_function_notebook_height = controls_estimated_height + bbox_estimated_height + 20 + initial_views_notebook_height = config.DEFAULT_CANVAS_HEIGHT + 20 + initial_log_height = 100 # pixels + + initial_total_height = (initial_function_notebook_height + initial_views_notebook_height + + initial_log_height + status_bar_actual_height + 10 + 10) + + if initial_total_height < min_total_height: initial_total_height = min_total_height + + self.root.geometry(f"{initial_width}x{initial_total_height}") + module_logger.debug(f"Initial window size set to: {initial_width}x{initial_total_height}") + + + # --- Main Layout: PanedWindow (Controls+Views area | Log+Status area) --- self.main_paned_window = ttk.PanedWindow(self.root, orient=tk.VERTICAL) self.main_paned_window.pack(fill=tk.BOTH, expand=True, padx=10, pady=(5,0)) - # --- Top Frame (Controls + Map) --- - self.top_frame = ttk.Frame(self.main_paned_window) - # MODIFIED: Rimosso minsize da .add() - self.main_paned_window.add(self.top_frame, weight=3) + # --- Top Panel of Main PanedWindow (to hold Function Notebook and Views Notebook) --- + self.paned_top_panel = ttk.Frame(self.main_paned_window) # This frame holds the two notebooks + self.main_paned_window.add(self.paned_top_panel, weight=3) # Gets more space - self.controls_and_map_frame = ttk.Frame(self.top_frame) - self.controls_and_map_frame.pack(fill=tk.BOTH, expand=True) + # --- Function Notebook (Top Notebook) --- + self.function_notebook = ttk.Notebook(self.paned_top_panel) + self.function_notebook.pack(side=tk.TOP, fill=tk.X, expand=False, padx=0, pady=(0,5)) # Fill X, but don't expand vertically - # ... (Control Frame, BBox Frame, Output Frame (Canvas) come prima) ... - self.control_frame = ttk.LabelFrame(self.controls_and_map_frame, text="Controls", padding=(10, 5)) - self.control_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=(5,5)) - self.mode_var = tk.StringVar(value="Live") + # --- Tab: Live from BBox --- + self.live_bbox_tab_frame = ttk.Frame(self.function_notebook, padding=5) + self.function_notebook.add(self.live_bbox_tab_frame, text="Live: Area Monitor") + + # --- Frame for Controls (inside live_bbox_tab_frame) --- + # Move controls and bbox input into this specific tab frame + self.controls_frame = ttk.Frame(self.live_bbox_tab_frame) + self.controls_frame.pack(side=tk.TOP, fill=tk.X, pady=(0,5)) + + # Control Frame (inside controls_frame) + self.control_frame = ttk.LabelFrame(self.controls_frame, text="Controls", padding=(10, 5)) + self.control_frame.pack(side=tk.TOP, fill=tk.X) + + # Radio buttons (Mode selection applies globally, keep reference) + self.mode_var = tk.StringVar(value="Live") # Default mode self.live_radio = ttk.Radiobutton(self.control_frame, text="Live", variable=self.mode_var, value="Live", command=self._on_mode_change) self.live_radio.pack(side=tk.LEFT, padx=(0,5)) self.history_radio = ttk.Radiobutton(self.control_frame, text="History", variable=self.mode_var, value="History", command=self._on_mode_change) self.history_radio.pack(side=tk.LEFT, padx=5) + + # Start/Stop buttons (apply globally, keep reference) self.start_button = ttk.Button(self.control_frame, text="Start Monitoring", command=self._start_monitoring) self.start_button.pack(side=tk.LEFT, padx=5) self.stop_button = ttk.Button(self.control_frame, text="Stop Monitoring", command=self._stop_monitoring, state=tk.DISABLED) self.stop_button.pack(side=tk.LEFT, padx=5) - self.bbox_frame = ttk.LabelFrame(self.controls_and_map_frame, text="Geographic Area (Bounding Box)", padding=(10,5)) - self.bbox_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5) + # Bounding Box Input Frame (inside controls_frame) + self.bbox_frame = ttk.LabelFrame(self.controls_frame, text="Geographic Area (Bounding Box)", padding=(10,5)) + self.bbox_frame.pack(side=tk.TOP, fill=tk.X, pady=5) # pack below control_frame + self.bbox_frame.columnconfigure(1, weight=1) self.bbox_frame.columnconfigure(3, weight=1) + ttk.Label(self.bbox_frame, text="Lat Min:").grid(row=0, column=0, padx=(0,2), pady=2, sticky=tk.W) self.lat_min_var = tk.StringVar(value=str(config.DEFAULT_BBOX_LAT_MIN)) self.lat_min_entry = ttk.Entry(self.bbox_frame, textvariable=self.lat_min_var, width=10) self.lat_min_entry.grid(row=0, column=1, padx=(0,5), pady=2, sticky=tk.EW) + ttk.Label(self.bbox_frame, text="Lon Min:").grid(row=0, column=2, padx=(5,2), pady=2, sticky=tk.W) self.lon_min_var = tk.StringVar(value=str(config.DEFAULT_BBOX_LON_MIN)) self.lon_min_entry = ttk.Entry(self.bbox_frame, textvariable=self.lon_min_var, width=10) self.lon_min_entry.grid(row=0, column=3, padx=(0,0), pady=2, sticky=tk.EW) + ttk.Label(self.bbox_frame, text="Lat Max:").grid(row=1, column=0, padx=(0,2), pady=2, sticky=tk.W) self.lat_max_var = tk.StringVar(value=str(config.DEFAULT_BBOX_LAT_MAX)) self.lat_max_entry = ttk.Entry(self.bbox_frame, textvariable=self.lat_max_var, width=10) self.lat_max_entry.grid(row=1, column=1, padx=(0,5), pady=2, sticky=tk.EW) + ttk.Label(self.bbox_frame, text="Lon Max:").grid(row=1, column=2, padx=(5,2), pady=2, sticky=tk.W) self.lon_max_var = tk.StringVar(value=str(config.DEFAULT_BBOX_LON_MAX)) self.lon_max_entry = ttk.Entry(self.bbox_frame, textvariable=self.lon_max_var, width=10) self.lon_max_entry.grid(row=1, column=3, padx=(0,0), pady=2, sticky=tk.EW) - self.output_frame = ttk.LabelFrame(self.controls_and_map_frame, text="Flight Map", padding=5) - self.output_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5) + # --- Placeholder Tabs for other Functions --- + self.live_airport_tab_frame = ttk.Frame(self.function_notebook, padding=5) + self.function_notebook.add(self.live_airport_tab_frame, text="Live: Airport") + ttk.Label(self.live_airport_tab_frame, text="Live from Airport - Coming Soon", font=("Arial", 10)).pack(expand=True) + + self.history_tab_frame = ttk.Frame(self.function_notebook, padding=5) + self.function_notebook.add(self.history_tab_frame, text="History") + ttk.Label(self.history_tab_frame, text="History Analysis - Coming Soon", font=("Arial", 10)).pack(expand=True) + + # Bind event to handle function tab changes (less frequent than view changes) + self.function_notebook.bind("<>", self._on_function_tab_change) + + + # --- Views Notebook (Middle Notebook, fills remaining space in paned_top_panel) --- + self.views_notebook = ttk.Notebook(self.paned_top_panel) + self.views_notebook.pack(fill=tk.BOTH, expand=True, padx=0, pady=0) # Fills remaining space in paned_top_panel + + # --- Tab: Map View --- + self.map_view_frame = ttk.Frame(self.views_notebook, padding=5) + self.views_notebook.add(self.map_view_frame, text="Map View") + + # Canvas for Flight Map (inside map_view_frame) self.flight_canvas = tk.Canvas( - self.output_frame, bg="gray90", + self.map_view_frame, bg="gray90", width=config.DEFAULT_CANVAS_WIDTH, height=config.DEFAULT_CANVAS_HEIGHT, highlightthickness=0 ) self.flight_canvas.pack(fill=tk.BOTH, expand=True) - # --- Log Area --- - self.log_frame = ttk.LabelFrame(self.main_paned_window, text="Application Log", padding=5) - # MODIFIED: Rimosso minsize da .add() - self.main_paned_window.add(self.log_frame, weight=1) - - log_font_family = "Consolas" if "Consolas" in tkFont.families() else "Courier New" - self.log_text_widget = ScrolledText( - self.log_frame, state=tk.DISABLED, height=8, wrap=tk.WORD, - font=(log_font_family, 9), - relief=tk.SUNKEN, borderwidth=1 - ) - self.log_text_widget.pack(fill=tk.BOTH, expand=True, padx=2, pady=2) - - from ..utils.logger import setup_logging as setup_app_logging - setup_app_logging(gui_log_widget=self.log_text_widget) - - # --- Status Bar --- - self.status_bar_frame = ttk.Frame(self.root, padding=(5, 3)) - self.status_bar_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=(5,3)) + # --- Tab: Table View (Placeholder) --- + self.table_view_frame = ttk.Frame(self.views_notebook, padding=5) + self.views_notebook.add(self.table_view_frame, text="Table View") + ttk.Label(self.table_view_frame, text="Table View - Coming Soon", font=("Arial", 12)).pack(expand=True) + + # Bind event to handle view tab changes + self.views_notebook.bind("<>", self._on_view_tab_change) + + + # --- Bottom Panel of Main PanedWindow (to hold Status Bar and Log Area) --- + self.paned_bottom_panel = ttk.Frame(self.main_paned_window) + self.main_paned_window.add(self.paned_bottom_panel, weight=1) # Gets less space + + # --- Status Bar (with Semaphore - inside paned_bottom_panel) --- + self.status_bar_frame = ttk.Frame(self.paned_bottom_panel, padding=(5, 3)) + # Pack at the top of the bottom panel + self.status_bar_frame.pack(side=tk.TOP, fill=tk.X, pady=(0,5)) # pady bottom to separate from log self.semaphore_canvas = tk.Canvas( - self.status_bar_frame, width=SEMAPHORE_TOTAL_SIZE, height=SEMAPHORE_TOTAL_SIZE, - bg="SystemButtonFace", highlightthickness=0 + self.status_bar_frame, + width=SEMAPHORE_TOTAL_SIZE, + height=SEMAPHORE_TOTAL_SIZE, + bg="SystemButtonFace", + highlightthickness=0 ) self.semaphore_canvas.pack(side=tk.LEFT, padx=(0, 5)) @@ -158,51 +216,55 @@ class MainWindow: self.status_label = ttk.Label(self.status_bar_frame, text="Status: Initializing...") self.status_label.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0,2)) - self.root.protocol("WM_DELETE_WINDOW", self._on_closing) - self._on_mode_change() - self.update_semaphore_and_status("OK", "System Initialized. Ready.") - module_logger.info("MainWindow fully initialized and displayed.") - # print("MW: __init__ - Fine, status iniziale impostato.") # Rimuovi o commenta i print di debug estremo + # --- Log Area (ScrolledText Widget - inside paned_bottom_panel) --- + # This frame now directly contains the log widget + # self.log_frame = ttk.LabelFrame(self.paned_bottom_panel, text="Application Log", padding=5) # No longer a LabelFrame + self.log_frame = ttk.Frame(self.paned_bottom_panel, padding=(5,0,5,5)) # Just a frame, pady bottom + # Pack log_frame below status_bar_frame + self.log_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=0) # Fills remaining space in bottom panel + + log_font_family = "Consolas" if "Consolas" in tkFont.families() else "Courier New" + self.log_text_widget = ScrolledText( + self.log_frame, state=tk.DISABLED, height=8, wrap=tk.WORD, + font=(log_font_family, 9), + relief=tk.SUNKEN, borderwidth=1 + ) + self.log_text_widget.pack(fill=tk.BOTH, expand=True, padx=0, pady=0) # No internal padding, handled by log_frame padding + + from ..utils.logger import setup_logging as setup_app_logging + setup_app_logging(gui_log_widget=self.log_text_widget) + + # Finalize initialization + self.root.protocol("WM_DELETE_WINDOW", self._on_closing) + + # Initial calls after all widgets are packed + self._on_mode_change() # This will call _update_map_placeholder based on default mode (Live) + self.update_semaphore_and_status("OK", "System Initialized. Ready.") # Initial status display + + module_logger.info("MainWindow fully initialized and displayed.") def update_semaphore_and_status(self, status_level: str, message: str): - """ - Updates the semaphore color and the status bar message. + color_to_set = SEMAPHORE_COLOR_UNKNOWN + if status_level == "OK": color_to_set = SEMAPHORE_COLOR_OK + elif status_level == "WARNING": color_to_set = SEMAPHORE_COLOR_WARNING + elif status_level == "ERROR": color_to_set = SEMAPHORE_COLOR_ERROR + elif status_level == "FETCHING": color_to_set = SEMAPHORE_COLOR_FETCHING - Args: - status_level (str): The level of the status. Expected values: - "OK", "WARNING", "ERROR", "UNKNOWN", "FETCHING". - message (str): The message to display in the status bar. - """ - color_to_set = SEMAPHORE_COLOR_UNKNOWN # Default - if status_level == "OK": - color_to_set = SEMAPHORE_COLOR_OK - elif status_level == "WARNING": - color_to_set = SEMAPHORE_COLOR_WARNING - elif status_level == "ERROR": - color_to_set = SEMAPHORE_COLOR_ERROR - elif status_level == "FETCHING": # New visual state for active fetching - color_to_set = SEMAPHORE_COLOR_FETCHING - if hasattr(self, 'semaphore_canvas') and self.semaphore_canvas.winfo_exists(): - try: - self.semaphore_canvas.itemconfig(self._semaphore_oval_id, fill=color_to_set) - except tk.TclError: - module_logger.warning("TclError updating semaphore color, canvas widget might be gone.") + try: self.semaphore_canvas.itemconfig(self._semaphore_oval_id, fill=color_to_set) + except tk.TclError: module_logger.warning("TclError updating semaphore color.") current_status_text = f"Status: {message}" if hasattr(self, 'status_label') and self.status_label.winfo_exists(): - try: - self.status_label.config(text=current_status_text) - except tk.TclError: - module_logger.warning("TclError updating status label, widget might be gone.") + try: self.status_label.config(text=current_status_text) + except tk.TclError: module_logger.warning("TclError updating status label text.") module_logger.debug(f"GUI Status Update: Level='{status_level}', Message='{message}'") - def _on_closing(self): module_logger.info("Main window closing event triggered.") if messagebox.askokcancel("Quit", "Do you want to quit Flight Monitor?", parent=self.root): @@ -215,8 +277,8 @@ class MainWindow: else: module_logger.info("User cancelled quit.") - def _reset_gui_to_stopped_state(self, status_message: Optional[str] = "Monitoring stopped."): + # Reset controls state (buttons, radios, bbox entries) if hasattr(self, 'start_button'): self.start_button.config(state=tk.NORMAL) if hasattr(self, 'stop_button'): self.stop_button.config(state=tk.DISABLED) if hasattr(self, 'live_radio'): self.live_radio.config(state=tk.NORMAL) @@ -225,10 +287,11 @@ class MainWindow: current_mode_is_live = hasattr(self, 'mode_var') and self.mode_var.get() == "Live" self._set_bbox_entries_state(tk.NORMAL if current_mode_is_live else tk.DISABLED) - status_level_for_semaphore = "OK" # Default for a clean stop by user + # Determine semaphore level based on the message content + status_level_for_semaphore = "OK" if status_message and ("failed" in status_message.lower() or "error" in status_message.lower()): - status_level_for_semaphore = "ERROR" # If stop was due to an error state - elif status_message and "warning" in status_message.lower(): # For less severe issues + status_level_for_semaphore = "ERROR" + elif status_message and "warning" in status_message.lower(): status_level_for_semaphore = "WARNING" self.update_semaphore_and_status(status_level_for_semaphore, status_message) @@ -236,31 +299,136 @@ class MainWindow: def _on_mode_change(self): + """Handles UI changes when the global monitoring mode (Live/History) changes.""" selected_mode = self.mode_var.get() status_message = f"Mode: {selected_mode}. Ready." self.update_semaphore_and_status("OK", status_message) - placeholder_text_to_set = "Map Area - Select mode and configure." + # Control visibility/state of functionality tabs based on global mode + # For now, Live BBox tab is only for Live mode. History tab is only for History mode. + if hasattr(self, 'function_notebook') and self.function_notebook.winfo_exists(): + try: + live_bbox_tab_index = self.function_notebook.index(self.live_bbox_tab_frame) + history_tab_index = self.function_notebook.index(self.history_tab_frame) + # live_airport_tab_index = self.function_notebook.index(self.live_airport_tab_frame) # Future + + if selected_mode == "Live": + # Enable Live tabs, disable History tabs + self.function_notebook.tab(live_bbox_tab_index, state='normal') + # self.function_notebook.tab(live_airport_tab_index, state='normal') # Future + self.function_notebook.tab(history_tab_index, state='disabled') + + # Switch to a Live-appropriate tab if on a disabled one + current_func_tab_index = self.function_notebook.index("current") + if current_func_tab_index == history_tab_index: + self.function_notebook.select(live_bbox_tab_index) # Select the Live BBox tab + + elif selected_mode == "History": + # Enable History tabs, disable Live tabs + self.function_notebook.tab(live_bbox_tab_index, state='disabled') + # self.function_notebook.tab(live_airport_tab_index, state='disabled') # Future + self.function_notebook.tab(history_tab_index, state='normal') + + # Switch to History tab if on a disabled one + current_func_tab_index = self.function_notebook.index("current") + if current_func_tab_index != history_tab_index: + self.function_notebook.select(history_tab_index) # Select the History tab + # Update views notebook visibility/state if needed (e.g., disable Map view in History) + # But views notebook is flexible, let's keep it enabled for now. + + except tk.TclError as e: + module_logger.warning(f"TclError updating function notebook tabs state: {e}") + except ValueError as e: # Tab index not found + module_logger.warning(f"ValueError finding function notebook tab index: {e}") + + + # Clear existing data from views and update appropriate placeholder + self.clear_all_views_data() # Clear data from ALL views + + # Update placeholder text based on the *current function tab* (driven by mode change) + # We might need to get the text from the currently selected tab frame's controls/concept + # For now, let's just update the map placeholder based on mode as before. + placeholder_text_to_set = "Data Area." # Default if selected_mode == "Live": placeholder_text_to_set = "Map Area - Ready for live data. Define area and press Start." - self._set_bbox_entries_state(tk.NORMAL) + # The bbox_frame is enabled/disabled by _set_bbox_entries_state below + # based on the *global* mode. + self._set_bbox_entries_state(tk.NORMAL) # Enable bbox entry for Live mode + elif selected_mode == "History": placeholder_text_to_set = "Map Area - Ready for historical data. (Functionality TBD)" - self._set_bbox_entries_state(tk.DISABLED) + self._set_bbox_entries_state(tk.DISABLED) # Disable bbox entry for History mode + + # Update map placeholder regardless of the current view tab + self._update_map_placeholder(placeholder_text_to_set) + + + def _on_function_tab_change(self, event: Any): + """Handles when the user switches between function tabs (e.g., Live BBox, History).""" + if not (hasattr(self, 'function_notebook') and self.function_notebook.winfo_exists()): + return - self.clear_canvas_data() # Clear old flight data - self._update_map_placeholder(placeholder_text_to_set) # Set new placeholder + selected_tab_index = self.function_notebook.index("current") + # tab_widget = self.function_notebook.winfo_children()[selected_tab_index] # Get the frame widget + tab_text = self.function_notebook.tab(selected_tab_index, "text") + + module_logger.info(f"GUI: Switched function tab to: {tab_text}") + + # Logic to handle UI changes or state based on which function tab is active + # For example, enable/disable certain global controls like Start/Stop based on tab capability. + # The global mode (Live/History) in _on_mode_change already sets basic control states. + # We might need to synchronize the mode_var with the selected tab if tabs imply mode. + # For now, let _on_mode_change manage control states based on mode_var. + + # Update the UI related to the specific tab content if needed. + # The bbox_frame is in the Live BBox tab. Its state is managed by _on_mode_change. + # If the user manually switches function tabs *without* changing mode, + # the bbox entries should reflect the current *global* mode. + + # Update placeholder text when function tab changes (as it implies a different task) + placeholder_text = f"Selected tab: {tab_text} - Ready." + if "Live: Area Monitor" in tab_text: + placeholder_text = "Map Area - Ready for live data. Define area and press Start." + elif "Live: Airport" in tab_text: + placeholder_text = "Map Area - Live Airport Monitor. (Functionality TBD)" + elif "History" in tab_text: + placeholder_text = "Map Area - History Analysis. (Functionality TBD)" + # In history mode, we might show historical data placeholder here. + # The actual placeholder text depends on whether data is loaded. + + self._update_map_placeholder(placeholder_text) + # You might also want to clear views data when switching function tabs, + # especially if the new tab implies loading new/different data. + # self.clear_all_views_data() # Consider clearing data on function tab switch + + + def _on_view_tab_change(self, event: Any): + """Handles when the user switches between view tabs (Map/Table).""" + if not (hasattr(self, 'views_notebook') and self.views_notebook.winfo_exists()): + return + + selected_tab_index = self.views_notebook.index("current") + # tab_widget = self.views_notebook.winfo_children()[selected_tab_index] + tab_text = self.views_notebook.tab(selected_tab_index, "text") + + module_logger.info(f"GUI: Switched view tab to: {tab_text}") + + # Logic specific to view tab change (e.g., ensuring table is populated with current data) + # For now, simply logging the change is sufficient. def _update_map_placeholder(self, text_to_display: str): + # Ensure the map canvas exists if hasattr(self, 'flight_canvas') and self.flight_canvas.winfo_exists(): try: - self.flight_canvas.delete("placeholder_text") # Remove any old one first + # Remove any old placeholder first + self.flight_canvas.delete("placeholder_text") + # Create new placeholder text canvas_w = self.flight_canvas.winfo_width() if self.flight_canvas.winfo_width() > 1 else config.DEFAULT_CANVAS_WIDTH canvas_h = self.flight_canvas.winfo_height() if self.flight_canvas.winfo_height() > 1 else config.DEFAULT_CANVAS_HEIGHT self.flight_canvas.create_text( canvas_w / 2, canvas_h / 2, - text=text_to_display, tags="placeholder_text", fill="gray50" # Darker gray + text=text_to_display, tags="placeholder_text", fill="gray50" ) except tk.TclError: module_logger.warning("TclError updating map placeholder, canvas might be gone.") @@ -268,7 +436,6 @@ class MainWindow: def _set_bbox_entries_state(self, state: str): if hasattr(self, 'lat_min_entry'): self.lat_min_entry.config(state=state) - # ... (checks for other entries) if hasattr(self, 'lon_min_entry'): self.lon_min_entry.config(state=state) if hasattr(self, 'lat_max_entry'): self.lat_max_entry.config(state=state) if hasattr(self, 'lon_max_entry'): self.lon_max_entry.config(state=state) @@ -278,52 +445,76 @@ class MainWindow: def _start_monitoring(self): selected_mode = self.mode_var.get() module_logger.info(f"GUI: User requested to start {selected_mode} monitoring.") - self.update_semaphore_and_status("WARNING", f"Attempting to start {selected_mode} monitoring...") + + # Determine which function tab is currently active + active_func_tab_index = -1 + active_func_tab_text = "Unknown Tab" + if hasattr(self, 'function_notebook') and self.function_notebook.winfo_exists(): + active_func_tab_index = self.function_notebook.index("current") + active_func_tab_text = self.function_notebook.tab(active_func_tab_index, "text") + module_logger.debug(f"GUI: Active function tab is: {active_func_tab_text}") + # Set controls state immediately self.start_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.NORMAL) + # Keep radios disabled while monitoring is starting/active self.live_radio.config(state=tk.DISABLED) self.history_radio.config(state=tk.DISABLED) - self._set_bbox_entries_state(tk.DISABLED) + # Bbox entries state is managed by _set_bbox_entries_state called below or in _on_mode_change + + # Logic to start monitoring based on the GLOBAL mode and the ACTIVE FUNCTION TAB + # For now, only "Live from BBox" tab is implemented for Live mode if selected_mode == "Live": - bounding_box = self.get_bounding_box() - if bounding_box: - if self.controller: - self.controller.start_live_monitoring(bounding_box) - else: # Should not happen if app structure is correct - module_logger.critical("Controller not available when attempting to start live monitoring.") - self._reset_gui_to_stopped_state("Critical Error: Controller unavailable.") - self.show_error_message("Internal Error", "Application controller is missing.") - else: # Invalid bounding box, get_bounding_box() already showed error and set semaphore - self._reset_gui_to_stopped_state("Failed to start: Invalid bounding box.") + # Only start Live monitoring if the "Live: Area Monitor" tab is active + if "Live: Area Monitor" in active_func_tab_text: # Check text for simplicity now + bounding_box = self.get_bounding_box() # This shows GUI error if input invalid and sets semaphore + if bounding_box: + module_logger.debug(f"Valid bounding box for live monitoring: {bounding_box}") + if self.controller: + self.controller.start_live_monitoring(bounding_box) # Controller updates status + else: + module_logger.critical("Controller not available.") + self._reset_gui_to_stopped_state("Critical Error: Controller unavailable.") + self.show_error_message("Internal Error", "Application controller is missing.") + else: # Invalid bounding box, error shown by get_bounding_box() + self._reset_gui_to_stopped_state("Failed to start: Invalid bounding box details.") + else: + # User pressed Start in Live mode, but not on the BBox tab + module_logger.warning(f"GUI: Start pressed in Live mode, but active tab '{active_func_tab_text}' is not the Live Area Monitor tab.") + self.update_semaphore_and_status("WARNING", f"Start not supported on '{active_func_tab_text}' tab in Live mode.") + self._reset_gui_to_stopped_state(f"Start not supported on {active_func_tab_text} tab.") + + elif selected_mode == "History": - if self.controller: self.controller.start_history_monitoring() - self.clear_canvas_data() - self._update_map_placeholder("Displaying historical data (placeholder)...") - # ... (placeholder drawing for history) - if hasattr(self, 'flight_canvas') and self.flight_canvas.winfo_exists(): - try: - self.flight_canvas.create_oval(90,90,110,110,fill="blue",outline="black",tags="flight_dot") - self.flight_canvas.create_text(100,125,text="HIST_FLT",tags="flight_label") - except tk.TclError: pass - module_logger.info("GUI: Switched to History mode (placeholder display).") + # Future: Start History monitoring based on active History tab parameters + if "History" in active_func_tab_text: # Check text for simplicity now + if self.controller: self.controller.start_history_monitoring() # Placeholder call + # ... (update GUI for history monitoring started state) ... + module_logger.info("GUI: History monitoring started (placeholder).") + else: + module_logger.warning(f"GUI: Start pressed in History mode, but active tab '{active_func_tab_text}' is not the History tab.") + self.update_semaphore_and_status("WARNING", f"Start not supported on '{active_func_tab_text}' tab in History mode.") + self._reset_gui_to_stopped_state(f"Start not supported on {active_func_tab_text} tab.") + + # Ensure bbox entries are disabled after pressing start, regardless of tab, + # as controls are locked while monitoring is starting/active. + self._set_bbox_entries_state(tk.DISABLED) def _stop_monitoring(self): module_logger.info("GUI: User requested to stop monitoring.") selected_mode = self.mode_var.get() + # Signal controller to stop background processes first if self.controller: if selected_mode == "Live": self.controller.stop_live_monitoring() # Controller will manage final status update elif selected_mode == "History": self.controller.stop_history_monitoring() - # Reset GUI controls. The final status message (OK, STOPPED) will come from the controller - # via a status update message, or _reset_gui_to_stopped_state provides a generic one. + # Reset GUI controls to stopped state self._reset_gui_to_stopped_state(f"{selected_mode} monitoring stopped by user.") - module_logger.debug("GUI controls reset after user initiated stop.") def get_bounding_box(self) -> Optional[Dict[str, float]]: @@ -336,8 +527,8 @@ class MainWindow: lon_max = float(self.lon_max_var.get()) except ValueError: msg = "Bounding box coordinates must be valid numbers." - module_logger.error(f"Input Error (ValueError): {msg}", exc_info=False) # No need for full stack trace here - self.show_error_message("Input Error", msg) # This sets semaphore to ERROR + module_logger.error(f"Input Error (ValueError): {msg}", exc_info=False) + self.show_error_message("Input Error", msg) # Sets semaphore to ERROR return None module_logger.debug(f"Raw BBox input: lat_min={lat_min}, lon_min={lon_min}, lat_max={lat_max}, lon_max={lon_max}") @@ -348,38 +539,41 @@ class MainWindow: self.show_error_message("Input Error", msg) return None if lat_min >= lat_max: - msg = "Latitude Min must be less than Latitude Max." + msg = "Latitude Min must be strictly less than Latitude Max." module_logger.error(f"Validation Error: {msg}") self.show_error_message("Input Error", msg) return None if lon_min >= lon_max: - msg = "Longitude Min must be less than Longitude Max." + msg = "Longitude Min must be strictly less than Longitude Max." module_logger.error(f"Validation Error: {msg}") self.show_error_message("Input Error", msg) return None - valid_bbox = {"lat_min": lat_min, "lon_min": lon_min, "lat_max": lat_max, "lon_max": lon_max} - module_logger.info(f"Valid bounding box obtained: {valid_bbox}") - return valid_bbox + return {"lat_min": lat_min, "lon_min": lon_min, "lat_max": lat_max, "lon_max": lon_max} def display_flights_on_canvas(self, flight_states: List[CanonicalFlightState], bounding_box: Dict[str, float]): - """Displays flight data from a list of CanonicalFlightState objects on the canvas.""" + """ + Displays flight data from a list of CanonicalFlightState objects on the map canvas. + This method specifically updates the Map View tab's canvas. + """ if not (hasattr(self, 'flight_canvas') and self.flight_canvas.winfo_exists()): module_logger.warning("Flight canvas widget does not exist, cannot display flights.") return - self.clear_canvas_data() # Clear previous flight dots and labels + self.clear_canvas_data() # Clear previous flight dots and labels from map canvas if not flight_states: self._update_map_placeholder("No flights currently in the selected area.") - module_logger.info("display_flights_on_canvas: No flight states to display.") + module_logger.info("display_flights_on_canvas: No flight states to display for this update.") return - else: # Flights are present, ensure placeholder is removed + else: # Flights are present, ensure placeholder is removed from map canvas if hasattr(self, 'flight_canvas') and self.flight_canvas.winfo_exists(): - self.flight_canvas.delete("placeholder_text") + # Check if placeholder exists on THIS canvas before deleting + if self.flight_canvas.find_withtag("placeholder_text"): + self.flight_canvas.delete("placeholder_text") canvas_width = self.flight_canvas.winfo_width() canvas_height = self.flight_canvas.winfo_height() @@ -394,60 +588,71 @@ class MainWindow: displayed_count = 0 for state in flight_states: + # Ensure it's a CanonicalFlightState with coordinates if not isinstance(state, CanonicalFlightState) or state.latitude is None or state.longitude is None: + if not isinstance(state, CanonicalFlightState): + module_logger.warning(f"Skipping non-CanonicalFlightState item during map display: {type(state)}") + else: + module_logger.debug(f"Skipping flight ICAO {state.icao24} due to missing coordinates for map display.") continue + + # Check if within the display bounding box if not (lon_min_bbox <= state.longitude <= lon_max_bbox and lat_min_bbox <= state.latitude <= lat_max_bbox): - continue + continue # Skip if outside current bbox + # Calculate pixel coordinates x_pixel = ((state.longitude - lon_min_bbox) / delta_lon_bbox) * canvas_width y_pixel = canvas_height - (((state.latitude - lat_min_bbox) / delta_lat_bbox) * canvas_height) + radius = 3 display_label = state.callsign if state.callsign else state.icao24 try: + # Draw the aircraft dot self.flight_canvas.create_oval( x_pixel - radius, y_pixel - radius, x_pixel + radius, y_pixel + radius, - fill="red", outline="black", tags="flight_dot" + fill="red", outline="black", tags=("flight_dot", f"icao_{state.icao24}") # Added ICAO tag for potential updates ) + # Draw the label self.flight_canvas.create_text( - x_pixel, y_pixel - (radius + 7), text=display_label, # Increased y-offset for label - font=("Arial", 7), tags="flight_label", anchor=tk.S + x_pixel, y_pixel - (radius + 7), text=display_label, + font=("Arial", 7), tags=("flight_label", f"icao_label_{state.icao24}"), anchor=tk.S # Added ICAO label tag ) displayed_count += 1 except tk.TclError: module_logger.warning("TclError drawing flight on canvas, widget might be gone.") - break + break # Stop trying if canvas is invalid if displayed_count > 0: - module_logger.info(f"Displayed {displayed_count} flight states on map.") - # Status message like "X flights displayed" will be set by AppController via update_semaphore_and_status + module_logger.info(f"Displayed {displayed_count} flight states on map canvas.") + + + def clear_all_views_data(self): + """Clears all displayed flight data from all view tabs.""" + self.clear_canvas_data() # Clear data from the map canvas + # Future: Call a method to clear data from other views (e.g., table) + # if hasattr(self, 'flight_table_widget') and self.flight_table_widget.winfo_exists(): + # self.flight_table_widget.clear_data() # Example for a table widget + module_logger.debug("Cleared data from all views.") def clear_canvas_data(self): - """Clears only flight-specific items (dots and labels) from the canvas.""" + """Clears only flight-specific items (dots and labels) from the map canvas.""" if hasattr(self, 'flight_canvas') and self.flight_canvas.winfo_exists(): try: self.flight_canvas.delete("flight_dot") self.flight_canvas.delete("flight_label") - module_logger.debug("Canvas flight items (dots and labels) cleared.") + # Placeholder text is managed separately by _update_map_placeholder or display_flights_on_canvas + module_logger.debug("Map canvas flight items (dots and labels) cleared.") except tk.TclError: - module_logger.warning("TclError clearing canvas flight items, widget might be gone.") + module_logger.warning("TclError clearing map canvas flight items, widget might be gone.") - # This method becomes the primary way to update status text from within MainWindow if needed, - # but AppController should be the main driver for status updates via update_semaphore_and_status. - def _set_status_text(self, message: str): - """Updates only the text part of the status bar. Internal use or simple messages.""" - if hasattr(self, 'status_label') and self.status_label.winfo_exists(): - try: - self.status_label.config(text=f"Status: {message}") - except tk.TclError: - module_logger.warning("TclError updating status label text directly, widget might be gone.") def show_error_message(self, title: str, message: str): """Displays an error message box and updates the status bar to reflect the error.""" module_logger.error(f"Displaying error message to user: Title='{title}', Message='{message}'") status_bar_msg = f"Error: {message[:70]}" if len(message) > 70: status_bar_msg += "..." - self.update_semaphore_and_status("ERROR", status_bar_msg) # Set semaphore and status + self.update_semaphore_and_status("ERROR", status_bar_msg) if hasattr(self, 'root') and self.root.winfo_exists(): messagebox.showerror(title, message, parent=self.root) \ No newline at end of file