diff --git a/flightmonitor/assets/icons/play_icon.png b/flightmonitor/assets/icons/play_icon.png new file mode 100644 index 0000000..8c0c499 Binary files /dev/null and b/flightmonitor/assets/icons/play_icon.png differ diff --git a/flightmonitor/assets/icons/stop_icon.png b/flightmonitor/assets/icons/stop_icon.png new file mode 100644 index 0000000..a0c58da Binary files /dev/null and b/flightmonitor/assets/icons/stop_icon.png differ diff --git a/flightmonitor/gui/main_window.py b/flightmonitor/gui/main_window.py index cc19989..ff5c116 100644 --- a/flightmonitor/gui/main_window.py +++ b/flightmonitor/gui/main_window.py @@ -18,7 +18,7 @@ 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 -from .panels.function_notebook_panel import FunctionNotebookPanel +from .panels.function_notebook_panel import FunctionNotebookPanel, DEFAULT_PROFILE_NAME try: from ..map.map_canvas_manager import MapCanvasManager @@ -211,6 +211,10 @@ class MainWindow: 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.") diff --git a/flightmonitor/gui/panels/function_notebook_panel.py b/flightmonitor/gui/panels/function_notebook_panel.py index 5c37631..a2f714d 100644 --- a/flightmonitor/gui/panels/function_notebook_panel.py +++ b/flightmonitor/gui/panels/function_notebook_panel.py @@ -3,8 +3,15 @@ Panel managing the area profiles, BBox input, and the function notebooks. """ import tkinter as tk -from tkinter import ttk, messagebox +from tkinter import ttk, messagebox, font as tkFont from typing import Dict, Any, Optional +import os + +try: + from PIL import Image, ImageTk + PIL_AVAILABLE = True +except ImportError: + PIL_AVAILABLE = False from ...utils.logger import get_logger from ...data import config as app_config @@ -13,6 +20,19 @@ from ...data.area_profile_manager import DEFAULT_PROFILE_NAME module_logger = get_logger(__name__) +ICON_PATH = os.path.join(os.path.dirname(__file__), '..', '..', 'assets', 'icons') +PLAY_ICON_PATH = os.path.join(ICON_PATH, 'play_icon.png') +STOP_ICON_PATH = os.path.join(ICON_PATH, 'stop_icon.png') + +# Definiamo i colori per i bottoni +COLOR_START_BG = "#28a745" # Verde +COLOR_START_ACTIVE = "#218838" +COLOR_STOP_BG = "#dc3545" # Rosso +COLOR_STOP_ACTIVE = "#c82333" +COLOR_DISABLED_BG = "#e0e0e0" # Grigio chiaro +COLOR_DISABLED_FG = "#a0a0a0" # Grigio scuro per il testo +COLOR_TEXT = "black" + class FunctionNotebookPanel: """ Manages the combined Area Profiles, BBox, and Function Notebooks panel. @@ -21,7 +41,10 @@ class FunctionNotebookPanel: self.parent_frame = parent_frame self.controller = controller - # --- Tkinter Variables --- + self.play_icon: Optional[tk.PhotoImage] = None + self.stop_icon: Optional[tk.PhotoImage] = None + + # ... (il resto del costruttore rimane uguale) ... self.lat_min_var = tk.StringVar() self.lon_min_var = tk.StringVar() self.lat_max_var = tk.StringVar() @@ -30,51 +53,62 @@ class FunctionNotebookPanel: self._build_ui() self.update_profile_list() + + # Seleziona il valore nella combobox, ma non carica ancora i dati self.set_selected_profile(DEFAULT_PROFILE_NAME) + + # RIMOSSA: La chiamata a _on_profile_selected() viene ora gestita da MainWindow module_logger.debug("FunctionNotebookPanel (unified) initialized.") + def _load_icons(self): + if not PIL_AVAILABLE: return + try: + icon_size = (32, 32) # Icone più grandi + play_img = Image.open(PLAY_ICON_PATH).resize(icon_size, Image.Resampling.LANCZOS) + self.play_icon = ImageTk.PhotoImage(play_img) + + stop_img = Image.open(STOP_ICON_PATH).resize(icon_size, Image.Resampling.LANCZOS) + self.stop_icon = ImageTk.PhotoImage(stop_img) + except Exception as e: + module_logger.warning(f"Could not load icons: {e}. Buttons will be text-only.") + def _build_ui(self): + self._load_icons() + button_font = tkFont.Font(family="Helvetica", size=10, weight="bold") + # --- Main Container for Area Management --- area_frame = ttk.LabelFrame(self.parent_frame, text="Area Profiles & BBox", padding=10) area_frame.pack(side=tk.TOP, fill=tk.X, padx=2, pady=(0, 5)) # Configure grid for the area frame area_frame.columnconfigure(1, weight=1) + # MODIFICA: Assegna lo stesso peso anche alla quarta colonna + area_frame.columnconfigure(3, weight=1) # --- Row 0: Profile Selection and Buttons --- profile_controls_frame = ttk.Frame(area_frame) profile_controls_frame.grid(row=0, column=0, columnspan=4, sticky="ew", pady=(0, 10)) - profile_controls_frame.columnconfigure(0, weight=1) # Let combobox expand - - self.profile_combobox = ttk.Combobox( - profile_controls_frame, textvariable=self.selected_profile_var, state="readonly" - ) + profile_controls_frame.columnconfigure(0, weight=1) + + self.profile_combobox = ttk.Combobox(profile_controls_frame, textvariable=self.selected_profile_var, state="readonly") self.profile_combobox.grid(row=0, column=0, sticky="ew", padx=(0, 5)) self.profile_combobox.bind("<>", self._on_profile_selected) - new_button = ttk.Button(profile_controls_frame, text="New", command=self._on_new_profile, width=5) new_button.grid(row=0, column=1, padx=(0, 2)) - save_button = ttk.Button(profile_controls_frame, text="Save", command=self._on_save_profile, width=5) save_button.grid(row=0, column=2, padx=(0, 2)) - delete_button = ttk.Button(profile_controls_frame, text="Delete", command=self._on_delete_profile, width=7) delete_button.grid(row=0, column=3) - - # --- Row 1 & 2: Bounding Box Entries --- ttk.Label(area_frame, text="Lat Min:").grid(row=1, column=0, padx=(0, 2), pady=2, sticky=tk.W) self.lat_min_entry = ttk.Entry(area_frame, textvariable=self.lat_min_var) self.lat_min_entry.grid(row=1, column=1, padx=(0, 5), pady=2, sticky=tk.EW) - ttk.Label(area_frame, text="Lon Min:").grid(row=1, column=2, padx=(5, 2), pady=2, sticky=tk.W) self.lon_min_entry = ttk.Entry(area_frame, textvariable=self.lon_min_var) self.lon_min_entry.grid(row=1, column=3, padx=(0, 0), pady=2, sticky=tk.EW) - ttk.Label(area_frame, text="Lat Max:").grid(row=2, column=0, padx=(0, 2), pady=2, sticky=tk.W) self.lat_max_entry = ttk.Entry(area_frame, textvariable=self.lat_max_var) self.lat_max_entry.grid(row=2, column=1, padx=(0, 5), pady=2, sticky=tk.EW) - ttk.Label(area_frame, text="Lon Max:").grid(row=2, column=2, padx=(5, 2), pady=2, sticky=tk.W) self.lon_max_entry = ttk.Entry(area_frame, textvariable=self.lon_max_var) self.lon_max_entry.grid(row=2, column=3, padx=(0, 0), pady=2, sticky=tk.EW) @@ -84,44 +118,81 @@ class FunctionNotebookPanel: self.function_notebook.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=2, pady=2) # --- Live Tab --- - live_tab_frame = ttk.Frame(self.function_notebook, padding=10) + live_tab_frame = ttk.Frame(self.function_notebook, padding=(0, 10, 0, 0)) # Aggiunto padding self.function_notebook.add(live_tab_frame, text="Live Monitor") - self.start_live_button = ttk.Button(live_tab_frame, text="Start Live Monitoring", command=self._on_start_live_monitoring) - self.start_live_button.pack(side=tk.LEFT, padx=5, pady=5) - self.stop_live_button = ttk.Button(live_tab_frame, text="Stop Live Monitoring", command=self._on_stop_live_monitoring, state=tk.DISABLED) - self.stop_live_button.pack(side=tk.LEFT, padx=5, pady=5) - # --- Historical Download Tab --- + # Usiamo pack per un controllo migliore sull'allineamento verticale + button_container = ttk.Frame(live_tab_frame) + button_container.pack(side=tk.TOP, fill=tk.X, expand=False, anchor='n') + + # Frame interni per centrare i bottoni orizzontalmente + left_spacer = ttk.Frame(button_container) + left_spacer.pack(side=tk.LEFT, expand=True) + + self.start_live_button = tk.Button( + button_container, + text="Start Live", + image=self.play_icon, + compound=tk.TOP, + command=self._on_start_live_monitoring, + font=button_font, + bg=COLOR_START_BG, + fg=COLOR_TEXT, + activebackground=COLOR_START_ACTIVE, + activeforeground=COLOR_TEXT, + relief=tk.RAISED, + borderwidth=2, + padx=10, + pady=5 + ) + self.start_live_button.pack(side=tk.LEFT, padx=15, pady=5) + + self.stop_live_button = tk.Button( + button_container, + text="Stop Live", + image=self.stop_icon, + compound=tk.TOP, + command=self._on_stop_live_monitoring, + font=button_font, + bg=COLOR_DISABLED_BG, + fg=COLOR_DISABLED_FG, + activebackground=COLOR_DISABLED_BG, + activeforeground=COLOR_DISABLED_FG, + state=tk.DISABLED, + relief=tk.RAISED, + borderwidth=2, + padx=10, + pady=5 + ) + self.stop_live_button.pack(side=tk.LEFT, padx=15, pady=5) + + right_spacer = ttk.Frame(button_container) + right_spacer.pack(side=tk.LEFT, expand=True) + + # --- Altri Tab (invariati) --- download_tab_frame = ttk.Frame(self.function_notebook, padding=10) self.function_notebook.add(download_tab_frame, text="Historical Download") ttk.Label(download_tab_frame, text="Historical download controls...").pack() - # --- Playback Tab --- playback_tab_frame = ttk.Frame(self.function_notebook, padding=10) self.function_notebook.add(playback_tab_frame, text="Playback") ttk.Label(playback_tab_frame, text="Playback controls...").pack() self.function_notebook.bind("<>", self._on_tab_change) - # --- Event Handlers --- + # --- Event Handlers (invariati) --- def _on_start_live_monitoring(self): if self.controller: self.controller.start_live_monitoring() - def _on_stop_live_monitoring(self): if self.controller: self.controller.stop_live_monitoring() - def _on_profile_selected(self, event=None): selected_name = self.selected_profile_var.get() - if selected_name and self.controller: - self.controller.load_area_profile(selected_name) - + if selected_name and self.controller: self.controller.load_area_profile(selected_name) def _on_new_profile(self): self.selected_profile_var.set("") self.update_bbox_gui_fields({}) - def _on_save_profile(self): if self.controller: self.controller.save_current_area_as_profile() - def _on_delete_profile(self): profile_to_delete = self.selected_profile_var.get() if not profile_to_delete or profile_to_delete == DEFAULT_PROFILE_NAME: @@ -129,14 +200,40 @@ class FunctionNotebookPanel: return if messagebox.askyesno("Confirm Delete", f"Delete profile '{profile_to_delete}'?", parent=self.parent_frame): if self.controller: self.controller.delete_area_profile(profile_to_delete) - def _on_tab_change(self, event=None): - if not self.function_notebook: return - tab_text = self.function_notebook.tab(self.function_notebook.index("current"), "text") - if self.controller and hasattr(self.controller, "on_function_tab_changed"): - self.controller.on_function_tab_changed(tab_text) + if self.function_notebook: + tab_text = self.function_notebook.tab(self.function_notebook.index("current"), "text") + if self.controller: self.controller.on_function_tab_changed(tab_text) - # --- Public Methods --- + # --- Public Methods (modificata set_monitoring_button_states) --- + def set_monitoring_button_states(self, is_monitoring_active: bool): + """Sets the state and style of the Start/Stop buttons.""" + if is_monitoring_active: + # Monitoring ATTIVO + if self.start_live_button: + self.start_live_button.config( + state=tk.DISABLED, bg=COLOR_DISABLED_BG, fg=COLOR_DISABLED_FG, + activebackground=COLOR_DISABLED_BG, activeforeground=COLOR_DISABLED_FG + ) + if self.stop_live_button: + self.stop_live_button.config( + state=tk.NORMAL, bg=COLOR_STOP_BG, fg=COLOR_TEXT, + activebackground=COLOR_STOP_ACTIVE, activeforeground=COLOR_TEXT + ) + else: + # Monitoring FERMO + if self.start_live_button: + self.start_live_button.config( + state=tk.NORMAL, bg=COLOR_START_BG, fg=COLOR_TEXT, + activebackground=COLOR_START_ACTIVE, activeforeground=COLOR_TEXT + ) + if self.stop_live_button: + self.stop_live_button.config( + state=tk.DISABLED, bg=COLOR_DISABLED_BG, fg=COLOR_DISABLED_FG, + activebackground=COLOR_DISABLED_BG, activeforeground=COLOR_DISABLED_FG + ) + + # --- Altri metodi pubblici (invariati) --- def update_profile_list(self): if self.controller: profile_names = self.controller.get_profile_names() @@ -144,24 +241,21 @@ class FunctionNotebookPanel: self.set_selected_profile(self.selected_profile_var.get() or DEFAULT_PROFILE_NAME) def set_selected_profile(self, profile_name: str): - if profile_name in self.profile_combobox["values"]: + if self.profile_combobox["values"] and profile_name in self.profile_combobox["values"]: self.selected_profile_var.set(profile_name) elif self.profile_combobox["values"]: self.selected_profile_var.set(self.profile_combobox["values"][0]) - + def get_bounding_box_input(self): - # ... (metodo rimane invariato) try: bbox_candidate = { "lat_min": float(self.lat_min_var.get()), "lon_min": float(self.lon_min_var.get()), "lat_max": float(self.lat_max_var.get()), "lon_max": float(self.lon_max_var.get()), } return bbox_candidate if _is_valid_bbox_dict(bbox_candidate) else None - except (ValueError, TypeError): - return None + except (ValueError, TypeError): return None def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]): - # ... (metodo rimane invariato) if bbox_dict and _is_valid_bbox_dict(bbox_dict): decimals = getattr(app_config, "COORDINATE_DECIMAL_PLACES", 5) self.lat_min_var.set(f"{bbox_dict['lat_min']:.{decimals}f}") @@ -169,27 +263,14 @@ class FunctionNotebookPanel: self.lat_max_var.set(f"{bbox_dict['lat_max']:.{decimals}f}") self.lon_max_var.set(f"{bbox_dict['lon_max']:.{decimals}f}") else: - self.lat_min_var.set("") - self.lon_min_var.set("") - self.lat_max_var.set("") - self.lon_max_var.set("") + self.lat_min_var.set(""), self.lon_min_var.set(""), self.lat_max_var.set(""), self.lon_max_var.set("") - def set_monitoring_button_states(self, is_monitoring_active: bool): - # ... (metodo rimane invariato) - if self.start_live_button and self.start_live_button.winfo_exists(): - self.start_live_button.config(state=tk.DISABLED if is_monitoring_active else tk.NORMAL) - if self.stop_live_button and self.stop_live_button.winfo_exists(): - self.stop_live_button.config(state=tk.NORMAL if is_monitoring_active else tk.DISABLED) - def set_controls_state(self, enabled: bool): state = tk.NORMAL if enabled else tk.DISABLED readonly_state = "readonly" if enabled else tk.DISABLED - self.profile_combobox.config(state=readonly_state) for entry in [self.lat_min_entry, self.lon_min_entry, self.lat_max_entry, self.lon_max_entry]: if entry.winfo_exists(): entry.config(state=state) - - # Abilita/Disabilita i bottoni dei profili for child in self.profile_combobox.master.winfo_children(): if isinstance(child, ttk.Button): child.config(state=state) \ No newline at end of file