314 lines
15 KiB
Python
314 lines
15 KiB
Python
# 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, font as tkFont
|
|
from typing import List, Dict, Optional, Any, Tuple, TYPE_CHECKING
|
|
|
|
import os
|
|
|
|
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 ..utils.gui_utils import GUI_STATUS_OK, GUI_STATUS_WARNING, GUI_STATUS_ERROR, GUI_STATUS_FETCHING
|
|
|
|
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.views_notebook_panel import ViewsNotebookPanel
|
|
# New and updated imports
|
|
from .panels.function_notebook_panel import FunctionNotebookPanel
|
|
from .panels.area_management_panel import AreaManagementPanel # New panel
|
|
|
|
try:
|
|
from ..map.map_canvas_manager import MapCanvasManager
|
|
MAP_CANVAS_MANAGER_AVAILABLE = True
|
|
except ImportError:
|
|
MapCanvasManager = None
|
|
MAP_CANVAS_MANAGER_AVAILABLE = False
|
|
print("CRITICAL ERROR in MainWindow: Failed to import MapCanvasManager.", flush=True)
|
|
|
|
try:
|
|
from .dialogs.import_progress_dialog import ImportProgressDialog
|
|
IMPORT_DIALOG_AVAILABLE = True
|
|
except ImportError:
|
|
ImportProgressDialog = None
|
|
IMPORT_DIALOG_AVAILABLE = False
|
|
print("ERROR in MainWindow: Failed to import ImportProgressDialog.", 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(
|
|
getattr(app_config, "LAYOUT_WINDOW_MIN_WIDTH", 900),
|
|
getattr(app_config, "LAYOUT_WINDOW_MIN_HEIGHT", 650)
|
|
)
|
|
|
|
self._create_menu()
|
|
self._create_main_layout()
|
|
self._create_panels()
|
|
|
|
# Setup logging to the GUI widget
|
|
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 horizontal division
|
|
main_hpane = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
|
|
main_hpane.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
|
|
# Left column
|
|
left_column_frame = ttk.Frame(main_hpane)
|
|
main_hpane.add(left_column_frame, weight=app_config.LAYOUT_MAIN_HORIZONTAL_WEIGHTS.get("left_column", 25))
|
|
|
|
# Right column
|
|
right_column_frame = ttk.Frame(main_hpane)
|
|
main_hpane.add(right_column_frame, weight=app_config.LAYOUT_MAIN_HORIZONTAL_WEIGHTS.get("right_column", 75))
|
|
|
|
# --- Configure Left Column ---
|
|
left_column_frame.rowconfigure(0, weight=3) # Area for BBox and Profiles
|
|
left_column_frame.rowconfigure(1, weight=3) # Area for Function Notebook
|
|
left_column_frame.rowconfigure(2, weight=4) # Area for Log/Status
|
|
left_column_frame.columnconfigure(0, weight=1)
|
|
|
|
self.area_management_frame = ttk.Frame(left_column_frame)
|
|
self.area_management_frame.grid(row=0, column=0, sticky="nsew", pady=(0, 5))
|
|
|
|
self.function_notebook_frame = ttk.Frame(left_column_frame)
|
|
self.function_notebook_frame.grid(row=1, column=0, sticky="nsew")
|
|
|
|
self.log_status_area_frame_container = ttk.Frame(left_column_frame, padding=(0,5,0,0))
|
|
self.log_status_area_frame_container.grid(row=2, column=0, sticky="nsew")
|
|
|
|
# --- Configure Right Column ---
|
|
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.get("views_notebook", 80))
|
|
|
|
self.map_tools_info_area_frame = ttk.Frame(right_vpane, padding=(0, 5, 0, 0))
|
|
right_vpane.add(self.map_tools_info_area_frame, weight=app_config.LAYOUT_RIGHT_VERTICAL_WEIGHTS.get("map_tools_info", 20))
|
|
|
|
def _create_panels(self):
|
|
# Left column panels
|
|
self.area_management_panel = AreaManagementPanel(self.area_management_frame, self.controller)
|
|
self.function_notebook_panel = FunctionNotebookPanel(self.function_notebook_frame, self.controller)
|
|
self.log_status_panel = LogStatusPanel(self.log_status_area_frame_container, self.root)
|
|
|
|
# Right column panels
|
|
self.views_notebook_panel = ViewsNotebookPanel(self.views_notebook_outer_frame)
|
|
|
|
# Bottom panels container
|
|
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)
|
|
|
|
# --- Public Methods and Callbacks ---
|
|
|
|
def get_bounding_box_from_gui(self) -> Optional[Dict[str, float]]:
|
|
"""Retrieves the bounding box from the dedicated AreaManagementPanel."""
|
|
return self.area_management_panel.get_bounding_box_input()
|
|
|
|
def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]):
|
|
"""Delegates updating BBox GUI fields to the AreaManagementPanel."""
|
|
self.area_management_panel.update_bbox_gui_fields(bbox_dict)
|
|
|
|
def set_monitoring_button_states(self, is_monitoring_active: bool):
|
|
"""Delegates setting the state of monitoring buttons."""
|
|
self.function_notebook_panel.set_monitoring_button_states(is_monitoring_active)
|
|
self.area_management_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."):
|
|
"""Resets the GUI to its initial, non-monitoring state."""
|
|
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 and hasattr(self.controller, "on_application_exit"):
|
|
self.controller.on_application_exit()
|
|
shutdown_logging_system()
|
|
if self.root and self.root.winfo_exists():
|
|
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:
|
|
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.")
|
|
# Fallback to a non-progress version if you have one
|
|
# self.controller.import_aircraft_database_from_file(filepath)
|
|
|
|
# Other methods (show_error_message, update_selected_flight_details, etc.) remain mostly the same
|
|
# But now they delegate to the appropriate sub-panel if necessary.
|
|
|
|
def show_error_message(self, title: str, message: str):
|
|
if self.root.winfo_exists():
|
|
messagebox.showerror(title, message, parent=self.root)
|
|
self.update_semaphore_and_status(GUI_STATUS_ERROR, message)
|
|
|
|
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 and MapCanvasManager:
|
|
default_map_bbox = self.get_bounding_box_from_gui()
|
|
# Initial track length is now handled by map_canvas_manager default
|
|
self.root.after(200, self._initialize_map_manager, default_map_bbox)
|
|
else:
|
|
self._update_map_placeholder("Map functionality disabled (Import Error).")
|
|
|
|
self.function_notebook_panel._on_tab_change() # Trigger initial state
|
|
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()):
|
|
module_logger.warning("Flight canvas not available for map manager init.")
|
|
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
|
|
|
|
# Check if map manager is active. If so, don't show placeholder.
|
|
if self.map_manager_instance:
|
|
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
|
|
|
|
# Pass-through methods for map info updates
|
|
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: Optional[int],
|
|
map_size_str: str,
|
|
map_geo_bounds: Optional[Tuple[float, float, float, float]],
|
|
target_bbox_input: Optional[Dict[str, float]], # Aggiunto il parametro mancante
|
|
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 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) |