SXXXXXXX_FlightMonitor/flightmonitor/gui/panels/function_notebook_panel.py

272 lines
14 KiB
Python

# FlightMonitor/gui/panels/function_notebook_panel.py
"""
Panel managing the area profiles, data logging controls, BBox input,
and the function notebooks (Live, Historical, etc.).
"""
import tkinter as tk
from tkinter import ttk, messagebox, font as tkFont
from typing import Dict, Any, Optional, Tuple
import os
try:
from PIL import Image, ImageTk
PIL_AVAILABLE = True
except ImportError:
PIL_AVAILABLE = False
from flightmonitor.utils.logger import get_logger
from flightmonitor.data import config as app_config
from flightmonitor.map.map_utils import _is_valid_bbox_dict
from flightmonitor.data.area_profile_manager import DEFAULT_PROFILE_NAME
from flightmonitor.gui.panels.historical_download_panel import HistoricalDownloadPanel
from flightmonitor.gui.panels.data_logging_panel import DataLoggingPanel # MODIFICATO: Import del nuovo pannello
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"
COLOR_START_ACTIVE = "#218838"
COLOR_STOP_BG = "#dc3545"
COLOR_STOP_ACTIVE = "#c82333"
COLOR_DISABLED_BG = "#e0e0e0"
COLOR_DISABLED_FG = "#a0a0a0"
COLOR_TEXT = "white" # Testo bianco per migliore contrasto su bottoni colorati
class FunctionNotebookPanel:
"""
Manages the combined Area Profiles, Data Logging, BBox,
and Function Notebooks panel.
"""
def __init__(self, parent_frame: ttk.Frame, controller: Any):
self.parent_frame = parent_frame
self.controller = controller
# --- Riferimenti ai pannelli figli ---
self.historical_panel: Optional[HistoricalDownloadPanel] = None
self.data_logging_panel: Optional[DataLoggingPanel] = None # MODIFICATO: Aggiunto riferimento
self.play_icon: Optional[ImageTk.PhotoImage] = None
self.stop_icon: Optional[ImageTk.PhotoImage] = None
self.lat_min_var = tk.StringVar()
self.lon_min_var = tk.StringVar()
self.lat_max_var = tk.StringVar()
self.lon_max_var = tk.StringVar()
self.selected_profile_var = tk.StringVar()
self._build_ui()
self.update_profile_list()
self.set_selected_profile(DEFAULT_PROFILE_NAME)
module_logger.debug("FunctionNotebookPanel (container) initialized.")
def _load_icons(self):
if not PIL_AVAILABLE: return
try:
icon_size = (24, 24) # Dimensione icone
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):
"""
Builds the UI by creating and packing the Area, Logging, and Function Notebook panels.
"""
self._load_icons()
button_font = tkFont.Font(family="Helvetica", size=10, weight="bold")
# --- 1. Area Profiles & BBox Panel ---
area_frame = ttk.LabelFrame(self.parent_frame, text="Area Profiles & BBox", padding=10)
area_frame.pack(side=tk.TOP, fill=tk.X, expand=False, padx=2, pady=(0, 0))
area_frame.columnconfigure(1, weight=1)
area_frame.columnconfigure(3, weight=1)
# Widgets for Area Profiles & BBox (codice invariato)
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)
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("<<ComboboxSelected>>", 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)
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)
# --- 2. Data Logging Panel (NUOVO) ---
# Questo pannello è ora un componente separato e condiviso.
self.data_logging_panel = DataLoggingPanel(self.parent_frame, self.controller)
# Il .pack() del suo frame contenitore è gestito all'interno della sua classe.
# --- 3. Function Notebook ---
self.function_notebook = ttk.Notebook(self.parent_frame)
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, 10, 10, 10))
self.function_notebook.add(live_tab_frame, text="Live Monitor")
button_container = ttk.Frame(live_tab_frame)
button_container.pack(side=tk.TOP, fill=tk.X, expand=False, anchor='n')
button_container.columnconfigure(0, weight=1)
button_container.columnconfigure(3, weight=1)
self.start_live_button = tk.Button(
button_container, text="Start Live", image=self.play_icon, compound=tk.LEFT,
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.grid(row=0, column=1, padx=15, pady=5)
self.stop_live_button = tk.Button(
button_container, text="Stop Live", image=self.stop_icon, compound=tk.LEFT,
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.grid(row=0, column=2, padx=15, pady=5)
# --- Historical Download Tab ---
download_tab_frame = ttk.Frame(self.function_notebook)
self.function_notebook.add(download_tab_frame, text="Historical Download")
self.historical_panel = HistoricalDownloadPanel(download_tab_frame, self.controller)
# --- 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("<<NotebookTabChanged>>", self._on_tab_change)
# --- Public Methods ---
def set_monitoring_button_states(self, is_monitoring_active: bool):
"""Sets the state and style of the Start/Stop buttons for the active mode."""
# Per ora, assume che influenzi solo i bottoni "Live"
# Potrebbe essere esteso per gestire i bottoni di altre tab
if is_monitoring_active:
self.start_live_button.config(state=tk.DISABLED, bg=COLOR_DISABLED_BG, fg=COLOR_DISABLED_FG)
self.stop_live_button.config(state=tk.NORMAL, bg=COLOR_STOP_BG, fg=COLOR_TEXT)
else:
self.start_live_button.config(state=tk.NORMAL, bg=COLOR_START_BG, fg=COLOR_TEXT)
self.stop_live_button.config(state=tk.DISABLED, bg=COLOR_DISABLED_BG, fg=COLOR_DISABLED_FG)
def set_controls_state(self, enabled: bool):
"""Enable/disable area profile and BBox controls."""
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)
for child in self.profile_combobox.master.winfo_children():
if isinstance(child, ttk.Button):
child.config(state=state)
def set_logging_controls_state(self, enabled: bool):
"""Enable/disable the entire data logging panel."""
if self.data_logging_panel:
state = tk.NORMAL if enabled else tk.DISABLED
# Potremmo voler creare un metodo nel DataLoggingPanel per gestire questo in modo più granulare
for child in self.data_logging_panel.parent_frame.winfo_children():
if hasattr(child, 'configure'):
child.configure(state=state)
def get_logging_panel_settings(self) -> Optional[Dict[str, Any]]:
"""Retrieves settings from the data logging panel."""
if self.data_logging_panel:
return {
"enabled": self.data_logging_panel.is_logging_enabled(),
"directory": self.data_logging_panel.get_log_directory()
}
return None
def clear_all_panel_data(self):
"""Clears data from all relevant panels, like the logging summary table."""
if self.data_logging_panel:
self.data_logging_panel.clear_summary_table()
def update_logging_panel_settings(self, settings: Dict[str, Any]):
"""Updates the data logging panel with given settings."""
if self.data_logging_panel:
self.data_logging_panel.update_settings(
enabled=settings.get("enabled", False),
directory=settings.get("directory", "")
)
# --- Event Handlers and other methods (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)
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:
messagebox.showerror("Delete Error", "Cannot delete the default profile or no profile selected.", parent=self.parent_frame)
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 self.function_notebook:
try:
tab_text = self.function_notebook.tab(self.function_notebook.index("current"), "text")
if self.controller: self.controller.on_function_tab_changed(tab_text)
except tk.TclError: # Può accadere se il notebook viene distrutto
pass
def update_profile_list(self):
if self.controller:
profile_names = self.controller.get_profile_names()
self.profile_combobox["values"] = profile_names
self.set_selected_profile(self.selected_profile_var.get() or DEFAULT_PROFILE_NAME)
def set_selected_profile(self, profile_name: str):
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):
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
def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]):
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}")
self.lon_min_var.set(f"{bbox_dict['lon_min']:.{decimals}f}")
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("")