# 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 import filedialog from tkinter import Menu from tkinter import font as tkFont from typing import List, Dict, Optional, Tuple, Any import logging import os from datetime import datetime, timezone import webbrowser from ..data import config as app_config from ..utils.logger import ( get_logger, add_tkinter_handler, shutdown_logging_system, ) from ..data.logging_config import LOGGING_CONFIG from ..data.common_models import CanonicalFlightState from ..utils.gui_utils import ( GUI_STATUS_OK, GUI_STATUS_WARNING, GUI_STATUS_ERROR, GUI_STATUS_FETCHING, GUI_STATUS_UNKNOWN, GUI_STATUS_PERMANENT_FAILURE, ) from .panels.log_status_panel import LogStatusPanel from .panels.map_tools_panel import MapToolsPanel from .panels.map_info_panel import MapInfoPanel from .panels.selected_flight_details_panel import SelectedFlightDetailsPanel from .panels.function_notebook_panel import FunctionNotebookPanel from .panels.views_notebook_panel import ViewsNotebookPanel try: from ..map.map_canvas_manager import MapCanvasManager from ..map.map_utils import _is_valid_bbox_dict MAP_CANVAS_MANAGER_AVAILABLE = True except ImportError as e_map_import: MapCanvasManager = None _is_valid_bbox_dict = lambda x: False MAP_CANVAS_MANAGER_AVAILABLE = False print( f"CRITICAL ERROR in MainWindow import: Failed to import MapCanvasManager or map_utils: {e_map_import}. Map functionality will be disabled.", flush=True, ) try: from .dialogs.import_progress_dialog import ImportProgressDialog IMPORT_DIALOG_AVAILABLE = True except ImportError as e_dialog_import: ImportProgressDialog = None IMPORT_DIALOG_AVAILABLE = False print( f"ERROR in MainWindow import: Failed to import ImportProgressDialog: {e_dialog_import}. Import progress UI will be basic.", flush=True, ) module_logger = get_logger(__name__) BBOX_COLOR_INSIDE = "green4" BBOX_COLOR_OUTSIDE = "red2" BBOX_COLOR_PARTIAL = "darkorange" BBOX_COLOR_NA = "gray50" class MainWindow: def __init__(self, root: tk.Tk, controller: Any): self.root = root self.controller = controller self.root.title("Flight Monitor") self.progress_dialog: Optional[ImportProgressDialog] = None self.full_flight_details_window: Optional[tk.Toplevel] = None if app_config.LAYOUT_START_MAXIMIZED: try: self.root.state("zoomed") except tk.TclError: try: self.root.geometry( f"{self.root.winfo_screenwidth()}x{self.root.winfo_screenheight()}+0+0" ) except tk.TclError: pass min_win_w = getattr(app_config, "LAYOUT_WINDOW_MIN_WIDTH", 900) min_win_h = getattr(app_config, "LAYOUT_WINDOW_MIN_HEIGHT", 650) self.root.minsize(min_win_h, min_win_h) # Corrected min_win_h used twice, should be min_win_w and min_win_h self.menubar = Menu(self.root) self.file_menu = Menu(self.menubar, tearoff=0) self.file_menu.add_command( label="Import Aircraft Database (CSV)...", command=self._import_aircraft_db_csv, ) self.file_menu.add_separator() self.file_menu.add_command(label="Exit", command=self._on_closing) self.menubar.add_cascade(label="File", menu=self.file_menu) self.root.config(menu=self.menubar) self.main_horizontal_paned_window = ttk.PanedWindow( self.root, orient=tk.HORIZONTAL ) self.main_horizontal_paned_window.pack( fill=tk.BOTH, expand=True, padx=5, pady=5 ) self.left_column_frame = ttk.Frame(self.main_horizontal_paned_window) self.main_horizontal_paned_window.add( self.left_column_frame, weight=app_config.LAYOUT_MAIN_HORIZONTAL_WEIGHTS.get("left_column", 25), ) self.left_vertical_paned_window = ttk.PanedWindow( self.left_column_frame, orient=tk.VERTICAL ) self.left_vertical_paned_window.pack(fill=tk.BOTH, expand=True) self.function_notebook_frame = ttk.Frame(self.left_vertical_paned_window) self.left_vertical_paned_window.add( self.function_notebook_frame, weight=app_config.LAYOUT_LEFT_VERTICAL_WEIGHTS.get("function_notebook", 65), ) self.function_notebook_panel = FunctionNotebookPanel( self.function_notebook_frame, self.controller ) self.log_status_area_frame_container = ttk.Frame(self.left_vertical_paned_window) self.left_vertical_paned_window.add( self.log_status_area_frame_container, weight=app_config.LAYOUT_LEFT_VERTICAL_WEIGHTS.get("log_status_area", 35), ) self.log_status_panel = LogStatusPanel(self.log_status_area_frame_container, self.root) self.right_column_frame = ttk.Frame(self.main_horizontal_paned_window) self.main_horizontal_paned_window.add( self.right_column_frame, weight=app_config.LAYOUT_MAIN_HORIZONTAL_WEIGHTS.get("right_column", 75), ) self.right_vertical_paned_window = ttk.PanedWindow( self.right_column_frame, orient=tk.VERTICAL ) self.right_vertical_paned_window.pack(fill=tk.BOTH, expand=True) self.views_notebook_outer_frame = ttk.Frame(self.right_vertical_paned_window) self.right_vertical_paned_window.add( self.views_notebook_outer_frame, weight=app_config.LAYOUT_RIGHT_VERTICAL_WEIGHTS.get("views_notebook", 80), ) self.views_notebook_panel = ViewsNotebookPanel(self.views_notebook_outer_frame) self.flight_canvas = self.views_notebook_panel.get_map_canvas() self.map_manager_instance: Optional[MapCanvasManager] = None self.map_tools_info_area_frame = ttk.Frame( self.right_vertical_paned_window, padding=(0, 5, 0, 0) ) self.right_vertical_paned_window.add( self.map_tools_info_area_frame, weight=app_config.LAYOUT_RIGHT_VERTICAL_WEIGHTS.get("map_tools_info", 20), ) self.bottom_panel_container = ttk.Frame(self.map_tools_info_area_frame) self.bottom_panel_container.pack(fill=tk.BOTH, expand=True) bottom_panel_weights = getattr( app_config, "LAYOUT_BOTTOM_PANELS_HORIZONTAL_WEIGHTS", {"map_tools": 1, "map_info": 2, "flight_details": 2}, ) self.bottom_panel_container.columnconfigure( 0, weight=bottom_panel_weights.get("map_tools", 1) ) self.bottom_panel_container.columnconfigure( 1, weight=bottom_panel_weights.get("map_info", 2) ) self.bottom_panel_container.columnconfigure( 2, weight=bottom_panel_weights.get("flight_details", 2) ) self.map_tool_frame = ttk.LabelFrame( self.bottom_panel_container, text="Map Tools", padding=5 ) self.map_tool_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 2)) self.map_tools_panel = MapToolsPanel(self.map_tool_frame, self.controller) self.map_info_panel_frame = ttk.LabelFrame( self.bottom_panel_container, text="Map Information", padding=10 ) self.map_info_panel_frame.grid(row=0, column=1, sticky="nsew", padx=2) self.map_info_panel = MapInfoPanel(self.map_info_panel_frame) self.selected_flight_details_frame = ttk.LabelFrame( self.bottom_panel_container, text="Selected Flight Details", padding=10 ) self.selected_flight_details_frame.grid( row=0, column=2, sticky="nsew", padx=(2, 0) ) self.selected_flight_details_panel = SelectedFlightDetailsPanel( self.selected_flight_details_frame, self.controller ) if self.log_status_panel.get_log_widget() and self.root: add_tkinter_handler( gui_log_widget=self.log_status_panel.get_log_widget(), root_tk_instance_for_gui_handler=self.root, logging_config_dict=LOGGING_CONFIG, ) else: module_logger.error( "LogStatusPanel or root not available in MainWindow init for logger setup." ) self.root.protocol("WM_DELETE_WINDOW", self._on_closing) self.root.after(100, self._delayed_initialization) module_logger.info( "MainWindow basic structure initialized. Delayed init pending." ) def _on_closing(self): module_logger.info("Main window closing event triggered.") user_confirmed_quit = ( messagebox.askokcancel( "Quit", "Do you want to quit Flight Monitor?", parent=self.root ) if self.root.winfo_exists() else True ) if user_confirmed_quit: if self.controller and hasattr(self.controller, "on_application_exit"): try: self.controller.on_application_exit() except Exception as e: module_logger.error( f"Error during controller.on_application_exit: {e}", exc_info=True, ) try: shutdown_logging_system() except Exception as e: module_logger.error( f"Error during shutdown_logging_system: {e}", exc_info=True ) if hasattr(self, "root") and self.root.winfo_exists(): try: self.root.destroy() except Exception: pass else: module_logger.info("User cancelled quit.") def _import_aircraft_db_csv(self): if not self.controller: self.show_error_message( "Controller Error", "Application controller not available." ) module_logger.error("Controller not available for aircraft DB import.") return if not hasattr( self.controller, "import_aircraft_database_from_file_with_progress" ): self.show_error_message( "Function Error", "Progress import function not available in controller.", ) module_logger.error( "Method 'import_aircraft_database_from_file_with_progress' missing in controller." ) return filepath = filedialog.askopenfilename( master=self.root, title="Select Aircraft Database CSV File", filetypes=(("CSV files", "*.csv"), ("All files", "*.*")), ) if filepath: module_logger.info(f"GUI: Selected CSV file for import: {filepath}") if IMPORT_DIALOG_AVAILABLE and ImportProgressDialog is not None: if self.progress_dialog and self.progress_dialog.winfo_exists(): try: self.progress_dialog.destroy() except tk.TclError: module_logger.warning( "Could not destroy previous progress dialog cleanly." ) self.progress_dialog = ImportProgressDialog( self.root, file_name=filepath ) self.controller.import_aircraft_database_from_file_with_progress( filepath, self.progress_dialog ) else: module_logger.warning( "ImportProgressDialog class not available. Using basic status update for import." ) if hasattr( self.controller, "import_aircraft_database_from_file" ): self.show_info_message( "Import Started", f"Starting import of {os.path.basename(filepath)}...\nThis might take a while. Check logs for completion.", ) self.controller.import_aircraft_database_from_file(filepath) else: self.show_error_message( "Import Error", "Import progress UI is not available and no fallback import method found.", ) else: module_logger.info("GUI: CSV import cancelled by user.") def _delayed_initialization(self): if not self.root.winfo_exists(): module_logger.warning( "Root window destroyed before delayed initialization." ) return if MAP_CANVAS_MANAGER_AVAILABLE and MapCanvasManager is not None: default_map_bbox = { "lat_min": app_config.DEFAULT_BBOX_LAT_MIN, "lon_min": app_config.DEFAULT_BBOX_LON_MIN, "lat_max": app_config.DEFAULT_BBOX_LAT_MAX, "lon_max": app_config.DEFAULT_BBOX_LON_MAX, } initial_track_len = self.function_notebook_panel.get_track_length_input() self.root.after( 200, self._initialize_map_manager, default_map_bbox, initial_track_len ) else: module_logger.error("MapCanvasManager class not available post-init.") fallback_canvas_w = self.views_notebook_panel.canvas_width fallback_canvas_h = self.views_notebook_panel.canvas_height self.root.after( 50, lambda w=fallback_canvas_w, h=fallback_canvas_h: self._update_map_placeholder( "Map functionality disabled (Import Error)." ), ) self.root.after(10, self.function_notebook_panel._on_mode_change) module_logger.info( "MainWindow fully initialized. Delegating initial mode setup." ) def _initialize_map_manager( self, initial_bbox_for_map: Dict[str, float], initial_track_length: int ): if not MAP_CANVAS_MANAGER_AVAILABLE or MapCanvasManager is None: self._update_map_placeholder("Map Error: Manager class missing.") if self.controller and hasattr(self.controller, "update_general_map_info"): self.controller.update_general_map_info() return current_flight_canvas = self.views_notebook_panel.get_map_canvas() if not current_flight_canvas or not current_flight_canvas.winfo_exists(): module_logger.warning( "Flight canvas does not exist. Cannot initialize MapCanvasManager." ) return canvas_w, canvas_h = ( current_flight_canvas.winfo_width(), current_flight_canvas.winfo_height(), ) if canvas_w <= 1: canvas_w = self.views_notebook_panel.canvas_width if canvas_h <= 1: canvas_h = self.views_notebook_panel.canvas_height if canvas_w > 1 and canvas_h > 1: try: self.map_manager_instance = MapCanvasManager( app_controller=self.controller, tk_canvas=current_flight_canvas, initial_bbox_dict=initial_bbox_for_map, ) if self.controller and hasattr(self.controller, "set_map_track_length"): try: self.controller.set_map_track_length(initial_track_length) module_logger.info( f"Initial map track length set to {initial_track_length} via controller." ) except Exception as e_trk: module_logger.error( f"Error setting initial track length for map manager: {e_trk}" ) else: module_logger.warning( "Controller or set_map_track_length not available for initial setup." ) if self.controller and hasattr( self.controller, "update_general_map_info" ): self.controller.update_general_map_info() except Exception as e_init: module_logger.critical( f"Failed to initialize MapCanvasManager: {e_init}", exc_info=True ) self.show_error_message( "Map Error", f"Could not initialize map: {e_init}" ) self._update_map_placeholder(f"Map Error: Init failed.\n{e_init}") if self.controller and hasattr( self.controller, "update_general_map_info" ): self.controller.update_general_map_info() else: if self.root.winfo_exists(): module_logger.info( "Canvas not sized yet, retrying map manager initialization." ) self.root.after( 300, self._initialize_map_manager, initial_bbox_for_map, initial_track_length, ) def _recreate_map_tools_content(self, parent_frame: ttk.Frame): pass def _recreate_map_info_content(self, parent_frame: ttk.Frame): pass def _create_selected_flight_details_content(self, parent_frame: ttk.LabelFrame): pass def update_semaphore_and_status(self, status_level: str, message: str): if hasattr(self, "log_status_panel") and self.log_status_panel: self.log_status_panel.update_status_display(status_level, message) else: print(f"Status update (LogStatusPanel not ready): {status_level}: {message}", flush=True) log_level_to_use = logging.INFO if status_level == GUI_STATUS_ERROR: log_level_to_use = logging.ERROR elif status_level == GUI_STATUS_PERMANENT_FAILURE: log_level_to_use = logging.CRITICAL elif status_level == GUI_STATUS_WARNING: log_level_to_use = logging.WARNING elif status_level in [GUI_STATUS_FETCHING, GUI_STATUS_OK, GUI_STATUS_UNKNOWN]: log_level_to_use = logging.DEBUG module_logger.log( log_level_to_use, f"GUI Status Update: Level='{status_level}', Message='{message}'", ) def _reset_gui_to_stopped_state( self, status_message: Optional[str] = "Monitoring stopped." ): if hasattr(self, "function_notebook_panel") and self.function_notebook_panel: self.function_notebook_panel.set_monitoring_button_states(False) else: if hasattr(self, "start_button") and self.start_button.winfo_exists(): self.start_button.config(state=tk.NORMAL) if hasattr(self, "stop_button") and self.stop_button.winfo_exists(): self.stop_button.config(state=tk.DISABLED) if hasattr(self, "live_radio") and self.live_radio.winfo_exists(): self.live_radio.config(state=tk.NORMAL) if hasattr(self, "history_radio") and self.history_radio.winfo_exists(): self.history_radio.config(state=tk.NORMAL) if hasattr(self, "function_notebook_panel") and self.function_notebook_panel: self.function_notebook_panel._update_internal_controls_state() status_level = GUI_STATUS_OK if status_message and ( "failed" in status_message.lower() or "error" in status_message.lower() ): status_level = GUI_STATUS_ERROR elif status_message and "warning" in status_message.lower(): status_level = GUI_STATUS_WARNING if hasattr(self, "root") and self.root.winfo_exists(): self.update_semaphore_and_status( status_level, status_message if status_message else "System Ready." ) def _should_show_main_placeholder(self) -> bool: current_flight_canvas = self.views_notebook_panel.get_map_canvas() if hasattr(self, "views_notebook_panel") else None return not ( current_flight_canvas and hasattr(self, "map_manager_instance") and self.map_manager_instance is not None and MAP_CANVAS_MANAGER_AVAILABLE ) def _update_map_placeholder(self, text_to_display: str): current_flight_canvas = self.views_notebook_panel.get_map_canvas() if hasattr(self, "views_notebook_panel") else None if not ( current_flight_canvas and current_flight_canvas.winfo_exists() ): return if ( not self._should_show_main_placeholder() ): try: current_flight_canvas.delete("placeholder_text") except: pass return try: current_flight_canvas.delete("placeholder_text") canvas_w, canvas_h = ( current_flight_canvas.winfo_width(), current_flight_canvas.winfo_height(), ) if canvas_w <= 1: canvas_w = self.views_notebook_panel.canvas_width if canvas_h <= 1: canvas_h = self.views_notebook_panel.canvas_height if canvas_w > 1 and canvas_h > 1: current_flight_canvas.create_text( canvas_w / 2, canvas_h / 2, text=text_to_display, tags="placeholder_text", fill="gray50", font=("Arial", 12, "italic"), justify=tk.CENTER, width=canvas_w - 40, ) except tk.TclError: pass except Exception as e_placeholder_draw: module_logger.error(f"Error drawing placeholder: {e_placeholder_draw}") def _on_mode_change(self): pass def _on_function_tab_change(self, event: Optional[tk.Event] = None): pass def _update_controls_state_based_on_mode_and_tab(self): pass def _set_bbox_entries_state(self, state: str): pass def _start_monitoring(self): pass def _stop_monitoring(self): pass def get_bounding_box_from_gui(self) -> Optional[Dict[str, float]]: if hasattr(self, "function_notebook_panel") and self.function_notebook_panel: return self.function_notebook_panel.get_bounding_box_input() else: module_logger.error("FunctionNotebookPanel not initialized for get_bounding_box_from_gui.") return None def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]): if hasattr(self, "function_notebook_panel") and self.function_notebook_panel: self.function_notebook_panel.update_bbox_gui_fields(bbox_dict) else: module_logger.warning("FunctionNotebookPanel not initialized for update_bbox_gui_fields.") def display_flights_on_canvas( self, flight_states: List[CanonicalFlightState], _active_bbox_context: Optional[ Dict[str, float] ], ): current_flight_canvas = self.views_notebook_panel.get_map_canvas() if hasattr(self, "views_notebook_panel") else None if not ( current_flight_canvas and hasattr(self, "map_manager_instance") and self.map_manager_instance is not None and MAP_CANVAS_MANAGER_AVAILABLE ): if hasattr(self, "root") and self.root.winfo_exists(): self._update_map_placeholder("Map N/A to display flights.") return try: self.map_manager_instance.update_flights_on_map(flight_states) if ( not flight_states and not self._should_show_main_placeholder() ): pass elif ( not flight_states and self._should_show_main_placeholder() ): self._update_map_placeholder("No flights in the selected area.") except Exception as e: module_logger.error( f"Error in display_flights_on_canvas: {e}", exc_info=True ) self.show_error_message( "Map Display Error", "Could not update flights on map." ) def clear_all_views_data(self): module_logger.info("GUI: Clearing all views data.") if ( hasattr(self, "map_manager_instance") and self.map_manager_instance and MAP_CANVAS_MANAGER_AVAILABLE ): try: self.map_manager_instance.clear_map_display() except Exception as e: module_logger.warning(f"Error clearing map via map manager: {e}") elif ( self._should_show_main_placeholder() ): mode = self.function_notebook_panel.get_selected_mode() if hasattr(self, "function_notebook_panel") else "Unknown" active_func_tab_text = self.function_notebook_panel.get_active_function_tab_text() if hasattr(self, "function_notebook_panel") else "" text = f"Map - {mode}. Data cleared." if mode == "Live" and "Live: Area Monitor" in active_func_tab_text: text = "Map - Live. Define area and Start." elif mode == "History" and "History" in active_func_tab_text: text = "Map - History. (Coming Soon)" elif mode == "Live" and "Live: Airport" in active_func_tab_text: text = "Map - Live Airport. (Coming Soon)" self._update_map_placeholder(text) if hasattr(self, "selected_flight_details_panel"): self.selected_flight_details_panel.update_details(None) else: module_logger.warning("SelectedFlightDetailsPanel not initialized for clearing.") # MODIFIED: Corrected parameter passing # WHY: The 'flight_data' parameter received by this method was not being passed # to the delegated call on `selected_flight_details_panel.update_details`. # HOW: Changed `self.selected_flight_details_panel.update_details(None)` to # `self.selected_flight_details_panel.update_details(flight_data)`. def update_selected_flight_details(self, flight_data: Optional[Dict[str, Any]]): """ Delegates the update of selected flight details to the SelectedFlightDetailsPanel. Args: flight_data: A dictionary containing flight information. """ if hasattr(self, "selected_flight_details_panel") and self.selected_flight_details_panel: self.selected_flight_details_panel.update_details(flight_data) # FIX APPLIED HERE else: module_logger.warning("SelectedFlightDetailsPanel not initialized for updating.") def show_error_message(self, title: str, message: str): status_msg = f"Error: {message[:70]}{'...' if len(message)>70 else ''}" if hasattr(self, "root") and self.root.winfo_exists(): self.update_semaphore_and_status(GUI_STATUS_ERROR, status_msg) try: messagebox.showerror(title, message, parent=self.root) except tk.TclError: pass else: print(f"ERROR (No GUI messagebox): {title} - {message}", flush=True) def show_info_message(self, title: str, message: str): if hasattr(self, "root") and self.root.winfo_exists(): try: messagebox.showinfo(title, message, parent=self.root) except tk.TclError: pass else: print(f"INFO (No GUI messagebox): {title} - {message}", flush=True) def show_map_context_menu( self, latitude: float, longitude: float, screen_x: int, screen_y: int ): if ( hasattr(self, "map_manager_instance") and self.map_manager_instance and hasattr(self.map_manager_instance, "show_map_context_menu_from_gui") and MAP_CANVAS_MANAGER_AVAILABLE ): try: self.map_manager_instance.show_map_context_menu_from_gui( latitude, longitude, screen_x, screen_y ) except Exception as e: module_logger.error( f"Error delegating context menu to MapCanvasManager: {e}", exc_info=True, ) else: module_logger.warning( "Map manager or context menu method not available for GUI." ) def update_clicked_map_info( self, lat_deg: Optional[float], lon_deg: Optional[float], lat_dms: str, lon_dms: str, ): if hasattr(self, "map_info_panel") and self.map_info_panel: self.map_info_panel.update_clicked_map_info(lat_deg, lon_deg, lat_dms, lon_dms) else: module_logger.warning("MapInfoPanel not initialized for update_clicked_map_info.") def update_general_map_info_display( self, zoom: Optional[int], map_size_str: str, map_geo_bounds: Optional[Tuple[float, float, float, float]], target_bbox_input: Optional[Dict[str, float]], flight_count: Optional[int], ): if hasattr(self, "map_info_panel") and self.map_info_panel: self.map_info_panel.update_general_map_info_display( zoom, map_size_str, map_geo_bounds, target_bbox_input, flight_count ) else: module_logger.warning("MapInfoPanel not initialized for update_general_map_info_display.") def _on_track_length_change(self): pass