272 lines
14 KiB
Python
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 ...utils.logger import get_logger
|
|
from ...data import config as app_config
|
|
from ...map.map_utils import _is_valid_bbox_dict
|
|
from ...data.area_profile_manager import DEFAULT_PROFILE_NAME
|
|
from .historical_download_panel import HistoricalDownloadPanel
|
|
from .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("") |