# 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, messagebox, filedialog, Menu from typing import List, Dict, Optional, Any, Tuple from flightmonitor.data import config as app_config from flightmonitor.utils.logger import ( get_logger, add_tkinter_handler, shutdown_logging_system, ) from flightmonitor.data.logging_config import LOGGING_CONFIG from flightmonitor.utils.gui_utils import ( GUI_STATUS_OK, GUI_STATUS_WARNING, GUI_STATUS_ERROR, GUI_STATUS_FETCHING, ) from flightmonitor.gui.panels.log_status_panel import LogStatusPanel from flightmonitor.gui.panels.map_tools_panel import MapToolsPanel from flightmonitor.gui.panels.map_info_panel import MapInfoPanel from flightmonitor.gui.panels.selected_flight_details_panel import ( SelectedFlightDetailsPanel, ) from flightmonitor.gui.panels.views_notebook_panel import ViewsNotebookPanel from flightmonitor.gui.panels.function_notebook_panel import ( FunctionNotebookPanel, DEFAULT_PROFILE_NAME, ) try: from flightmonitor.map.map_canvas_manager import MapCanvasManager MAP_CANVAS_MANAGER_AVAILABLE = True except ImportError as e_map_import: MapCanvasManager = None MAP_CANVAS_MANAGER_AVAILABLE = False print( f"CRITICAL ERROR in MainWindow: Failed to import MapCanvasManager: {e_map_import}.", flush=True, ) try: from flightmonitor.gui.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: Failed to import ImportProgressDialog: {e_dialog_import}.", flush=True, ) module_logger = get_logger(__name__) class MainWindow: def __init__(self, root: tk.Tk, controller: Any): self.root = root self.controller = controller self.root.title("Flight Monitor") # --- Instance variables --- self.progress_dialog: Optional[ImportProgressDialog] = None self.full_flight_details_window: Optional[tk.Toplevel] = None self.map_manager_instance: Optional[MapCanvasManager] = None if app_config.LAYOUT_START_MAXIMIZED: try: self.root.state("zoomed") except tk.TclError: pass self.root.minsize( app_config.LAYOUT_WINDOW_MIN_WIDTH, app_config.LAYOUT_WINDOW_MIN_HEIGHT ) self._create_menu() self._create_main_layout() self._create_panels() 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 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.") def _create_menu(self): menubar = Menu(self.root) file_menu = Menu(menubar, tearoff=0) file_menu.add_command( label="Import Aircraft Database (CSV)...", command=self._import_aircraft_db_csv, ) file_menu.add_separator() file_menu.add_command(label="Exit", command=self._on_closing) menubar.add_cascade(label="File", menu=file_menu) self.root.config(menu=menubar) def _create_main_layout(self): main_hpane = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL) main_hpane.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) left_column_frame = ttk.Frame(main_hpane) main_hpane.add( left_column_frame, weight=app_config.LAYOUT_MAIN_HORIZONTAL_WEIGHTS["left_column"], ) right_column_frame = ttk.Frame(main_hpane) main_hpane.add( right_column_frame, weight=app_config.LAYOUT_MAIN_HORIZONTAL_WEIGHTS["right_column"], ) left_vpane = ttk.PanedWindow(left_column_frame, orient=tk.VERTICAL) left_vpane.pack(fill=tk.BOTH, expand=True) self.function_and_area_frame = ttk.Frame(left_vpane) left_vpane.add( self.function_and_area_frame, weight=app_config.LAYOUT_LEFT_VERTICAL_WEIGHTS["function_notebook"], ) self.log_status_area_frame_container = ttk.Frame(left_vpane) left_vpane.add( self.log_status_area_frame_container, weight=app_config.LAYOUT_LEFT_VERTICAL_WEIGHTS["log_status_area"], ) right_vpane = ttk.PanedWindow(right_column_frame, orient=tk.VERTICAL) right_vpane.pack(fill=tk.BOTH, expand=True) self.views_notebook_outer_frame = ttk.Frame(right_vpane) right_vpane.add( self.views_notebook_outer_frame, weight=app_config.LAYOUT_RIGHT_VERTICAL_WEIGHTS["views_notebook"], ) self.map_tools_info_area_frame = ttk.Frame(right_vpane) right_vpane.add( self.map_tools_info_area_frame, weight=app_config.LAYOUT_RIGHT_VERTICAL_WEIGHTS["map_tools_info"], ) def _create_panels(self): self.function_notebook_panel = FunctionNotebookPanel( self.function_and_area_frame, self.controller ) self.log_status_panel = LogStatusPanel( self.log_status_area_frame_container, self.root ) self.views_notebook_panel = ViewsNotebookPanel(self.views_notebook_outer_frame) bottom_panel_container = ttk.Frame(self.map_tools_info_area_frame) bottom_panel_container.pack(fill=tk.BOTH, expand=True) bottom_weights = app_config.LAYOUT_BOTTOM_PANELS_HORIZONTAL_WEIGHTS bottom_panel_container.columnconfigure( 0, weight=bottom_weights.get("map_tools", 1) ) bottom_panel_container.columnconfigure( 1, weight=bottom_weights.get("map_info", 2) ) bottom_panel_container.columnconfigure( 2, weight=bottom_weights.get("flight_details", 2) ) map_tool_frame = ttk.LabelFrame( bottom_panel_container, text="Map Tools", padding=5 ) map_tool_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 2)) self.map_tools_panel = MapToolsPanel(map_tool_frame, self.controller) map_info_panel_frame = ttk.LabelFrame( bottom_panel_container, text="Map Information", padding=10 ) map_info_panel_frame.grid(row=0, column=1, sticky="nsew", padx=2) self.map_info_panel = MapInfoPanel(map_info_panel_frame) selected_flight_details_frame = ttk.LabelFrame( bottom_panel_container, text="Selected Flight Details", padding=10 ) selected_flight_details_frame.grid(row=0, column=2, sticky="nsew", padx=(2, 0)) self.selected_flight_details_panel = SelectedFlightDetailsPanel( selected_flight_details_frame, self.controller ) def get_bounding_box_from_gui(self) -> Optional[Dict[str, float]]: return self.function_notebook_panel.get_bounding_box_input() def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]): self.function_notebook_panel.update_bbox_gui_fields(bbox_dict) def set_monitoring_button_states(self, is_monitoring_active: bool): self.function_notebook_panel.set_monitoring_button_states(is_monitoring_active) self.function_notebook_panel.set_controls_state(not is_monitoring_active) def update_semaphore_and_status(self, status_level: str, message: str): self.log_status_panel.update_status_display(status_level, message) def _reset_gui_to_stopped_state( self, status_message: Optional[str] = "Monitoring stopped." ): self.set_monitoring_button_states(False) status_level = ( GUI_STATUS_ERROR if "error" in status_message.lower() else GUI_STATUS_OK ) self.update_semaphore_and_status(status_level, status_message) def _on_closing(self): if messagebox.askokcancel( "Quit", "Do you want to quit Flight Monitor?", parent=self.root ): if self.controller: self.controller.on_application_exit() shutdown_logging_system() if self.root: self.root.destroy() def _import_aircraft_db_csv(self): if not ( self.controller and hasattr( self.controller, "import_aircraft_database_from_file_with_progress" ) ): self.show_error_message( "Controller Error", "Import function not available." ) return filepath = filedialog.askopenfilename( master=self.root, title="Select Aircraft Database CSV File", filetypes=(("CSV files", "*.csv"), ("All files", "*.*")), ) if filepath: if IMPORT_DIALOG_AVAILABLE and ImportProgressDialog: if self.progress_dialog and self.progress_dialog.winfo_exists(): self.progress_dialog.destroy() self.progress_dialog = ImportProgressDialog( self.root, file_name=filepath ) self.controller.import_aircraft_database_from_file_with_progress( filepath, self.progress_dialog ) else: self.show_info_message( "Import Started", "Importing in background. Check logs for completion.", ) def show_error_message(self, title: str, message: str): if self.root.winfo_exists(): messagebox.showerror(title, message, parent=self.root) def show_info_message(self, title: str, message: str): if self.root.winfo_exists(): messagebox.showinfo(title, message, parent=self.root) def update_selected_flight_details(self, flight_data: Optional[Dict[str, Any]]): self.selected_flight_details_panel.update_details(flight_data) def clear_all_views_data(self): module_logger.info("GUI: Clearing all views data.") if self.map_manager_instance: self.map_manager_instance.clear_map_display() else: self._update_map_placeholder("Map Cleared. Ready.") self.selected_flight_details_panel.update_details(None) def _delayed_initialization(self): if not self.root.winfo_exists(): return if MAP_CANVAS_MANAGER_AVAILABLE: initial_bbox = self.get_bounding_box_from_gui() self.root.after(200, self._initialize_map_manager, initial_bbox) else: self._update_map_placeholder("Map functionality disabled (Import Error).") # MODIFICA: Invochiamo il caricamento del profilo di default da qui, # dopo che la mappa รจ stata programmata per l'inizializzazione. self.root.after( 250, lambda: self.controller.load_area_profile(DEFAULT_PROFILE_NAME) ) self.function_notebook_panel._on_tab_change() module_logger.info("MainWindow fully initialized.") def _initialize_map_manager(self, initial_bbox_for_map: Optional[Dict[str, float]]): flight_canvas = self.views_notebook_panel.get_map_canvas() if not (flight_canvas and flight_canvas.winfo_exists()): return canvas_w, canvas_h = flight_canvas.winfo_width(), flight_canvas.winfo_height() if canvas_w > 1 and canvas_h > 1: try: self.map_manager_instance = MapCanvasManager( app_controller=self.controller, tk_canvas=flight_canvas, initial_bbox_dict=initial_bbox_for_map, ) if 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:\n{e_init}") else: self.root.after(300, self._initialize_map_manager, initial_bbox_for_map) def _update_map_placeholder(self, text_to_display: str): flight_canvas = self.views_notebook_panel.get_map_canvas() if not (flight_canvas and flight_canvas.winfo_exists()): return if self.map_manager_instance and not self.map_manager_instance.is_detail_map: flight_canvas.delete("placeholder_text") return try: flight_canvas.delete("placeholder_text") w, h = flight_canvas.winfo_width(), flight_canvas.winfo_height() flight_canvas.create_text( w / 2, h / 2, text=text_to_display, tags="placeholder_text", fill="gray50", font=("Arial", 12, "italic"), justify=tk.CENTER, ) except tk.TclError: pass def update_clicked_map_info(self, lat_deg, lon_deg, lat_dms, lon_dms): self.map_info_panel.update_clicked_map_info(lat_deg, lon_deg, lat_dms, lon_dms) def update_general_map_info_display( self, zoom, map_size_str, map_geo_bounds, target_bbox_input, flight_count ): self.map_info_panel.update_general_map_info_display( zoom, map_size_str, map_geo_bounds, target_bbox_input, flight_count ) def show_map_context_menu(self, latitude, longitude, screen_x, screen_y): if self.map_manager_instance: self.map_manager_instance.show_map_context_menu_from_gui( latitude, longitude, screen_x, screen_y )