# Módulo: vista/panel_central.py import tkinter as tk from tkinter import ttk from tkinter import messagebox from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.figure import Figure import psutil # <--- IMPORTACIÓN REQUERIDA PARA CPU/RAM # --- LÓGICA DE CONTROL UNIVERSAL --- from logica.T1.trafficMeter import iniciar_monitor_red from logica.T2.alarm import AlarmManager # --- MÓDULOS DE PESTAÑAS (Importación de las Vistas Modulares) --- from vista.central_panel.view_recursos import RecursosPanel from vista.central_panel.view_carrera import CarreraPanel from vista.central_panel.view_radio import RadioPanel from vista.central_panel.view_alarmas import AlarmaPanel from vista.central_panel.view_notas import NotasPanel from vista.central_panel.view_scrapping import NavegadorPanel from vista.central_panel.view_chat import ChatPanel from vista.central_panel.view_correos import CorreosPanel from vista.central_panel.view_enlaces import EnlacesPanel # --- IMPORTACIÓN UNIVERSAL DE CONSTANTES --- from vista.config import * class PanelCentral(ttk.Frame): """ Controlador central del Panel. Gestiona el layout principal, inicializa la lógica universal y coordina las instancias de las clases modulares de cada pestaña (view_*). """ INTERVALO_ACTUALIZACION_MS = INTERVALO_RECURSOS_MS # 🔑 CORRECCIÓN CRUCIAL: Añadir 'panel_lateral' al constructor para acceder al controlador de música. def __init__(self, parent, root, panel_lateral, *args, **kwargs): super().__init__(parent, *args, **kwargs) self.root = root # 🔑 Guardamos la referencia para poder acceder al ReproductorController self.panel_lateral = panel_lateral # --- Variables de Estado y Lógica Central --- self.after_id = None self.net_monitor = None # Lógica de Matplotlib (T1) self.figure = Figure(figsize=(5, 4), dpi=100) self.canvas = None self.canvas_widget = None self.grafico_frame = None # Lógica de Alarmas self.alarm_manager = AlarmManager(self.root, self.show_alarm_popup) # Contenedores para módulos y pestañas self.tabs = {} self.modulos = {} # Configuración de Layout Principal self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure(0, weight=1) self.crear_area_principal() # 📦 ESTRUCTURA PRINCIPAL Y CREACIÓN DE PESTAÑAS # ------------------------------------------------------------- def crear_area_principal(self): """Crea el contenedor de las subpestañas (Notebook), columna izquierda (0).""" frame_izquierdo = ttk.Frame(self, style='TFrame') frame_izquierdo.grid(row=0, column=0, sticky="nsew") frame_izquierdo.grid_rowconfigure(0, weight=1) frame_izquierdo.grid_columnconfigure(0, weight=1) self.crear_notebook_pestanas(frame_izquierdo) def crear_notebook_pestanas(self, parent_frame): """Crea las pestañas e inicializa los módulos modulares en cada Frame.""" sub_notebook = ttk.Notebook(parent_frame) sub_notebook.grid(row=0, column=0, sticky="nsew") # Lista completa de pestañas sub_tabs = ["Recursos", "Carrera", "Radios", "Navegador", "Correos", "Tareas", "Alarmas", "Enlaces", "Chat"] # Mapeo de la pestaña a la clase modular CLASES_MODULARES = { "Recursos": RecursosPanel, "Carrera": CarreraPanel, "Radios": RadioPanel, "Navegador": NavegadorPanel, "Correos": CorreosPanel, "Tareas": NotasPanel, "Alarmas": AlarmaPanel, "Enlaces": EnlacesPanel, "Chat": ChatPanel, } for sub_tab_text in sub_tabs: frame = ttk.Frame(sub_notebook, style='TFrame') sub_notebook.add(frame, text=sub_tab_text) self.tabs[sub_tab_text] = frame # Asegurar que el Frame de la pestaña se expande frame.grid_rowconfigure(0, weight=1) frame.grid_columnconfigure(0, weight=1) ClasePanel = CLASES_MODULARES.get(sub_tab_text) vista_instancia = None if ClasePanel: # --- Lógica de Inicialización específica --- if sub_tab_text == "Recursos": vista_instancia = RecursosPanel(frame, self.figure, self.canvas) self.modulos[sub_tab_text] = vista_instancia self.canvas = vista_instancia.canvas self.canvas_widget = vista_instancia.canvas_widget self.grafico_frame = vista_instancia.grafico_frame elif sub_tab_text == "Alarmas": vista_instancia = AlarmaPanel(frame, self.root, self.alarm_manager) self.modulos[sub_tab_text] = vista_instancia # 🔑 CORRECCIÓN CLAVE: Inyectar la dependencia del controlador de música en RadioPanel elif sub_tab_text == "Radios": # Accedemos al controlador de música a través de la referencia al PanelLateral reproductor_controller = getattr(self.panel_lateral, 'controles_musica', None) vista_instancia = RadioPanel(frame, self.root, reproductor_controller_instance=reproductor_controller) self.modulos[sub_tab_text] = vista_instancia else: vista_instancia = ClasePanel(frame, self.root) self.modulos[sub_tab_text] = vista_instancia # Ubicar la instancia de la vista modular dentro de su Frame padre (CRÍTICO) vista_instancia.grid(row=0, column=0, sticky="nsew") # ------------------------------------------------------------- # 📞 MÉTODOS DE CONEXIÓN (Llamados desde panel_lateral.py) # ------------------------------------------------------------- def manejar_inicio_carrera(self): """Inicia la carrera de camellos llamando al módulo de Carrera (T2).""" if 'Carrera' in self.modulos: self.modulos['Carrera'].manejar_inicio_carrera() else: messagebox.showerror("Error", "Módulo de Carrera no inicializado.") def cargar_contenido_web(self, titulo, contenido): """Muestra el resultado del scraping llamando al módulo Navegador.""" if 'Navegador' in self.modulos: self.modulos['Navegador'].cargar_contenido_web(titulo, contenido) else: messagebox.showerror("Error", "El módulo Navegador (Scrapping) no está inicializado.") # ------------------------------------------------------------- # 🔔 POPUP DE ALARMA Y LÓGICA T1 # ------------------------------------------------------------- def show_alarm_popup(self, alarm_name, alarm_id): """Muestra una ventana emergente cuando una alarma salta.""" popup = tk.Toplevel(self.root) popup.title("🚨 ¡ALARMA!") # --- Código de configuración del popup omitido por brevedad --- popup.geometry("350x150") popup.resizable(False, False) popup.overrideredirect(True) popup.transient(self.root) popup.grab_set() self.root.update_idletasks() width = popup.winfo_width() height = popup.winfo_height() x = (self.root.winfo_screenwidth() // 2) - (width // 2) y = (self.root.winfo_screenheight() // 2) - (height // 2) popup.geometry(f'{width}x{height}+{x}+{y}') def close_and_stop(event=None): self.alarm_manager.stop_alarm_sound() self.alarm_manager.cancel_alarm(alarm_id) popup.destroy() if 'Alarmas' in self.modulos: self.modulos['Alarmas'].actualizar_lista_alarmas() frame = ttk.Frame(popup, padding=20, relief='solid', borderwidth=2) frame.pack(expand=True, fill="both") ttk.Label(frame, text="¡El Temporizador ha Terminado!", font=FUENTE_TITULO, foreground=COLOR_ACCION).pack( pady=5) ttk.Label(frame, text=f"Hora de Disparo: {alarm_name}", font=FUENTE_NEGOCIOS).pack(pady=5) ttk.Label(frame, text="Haz clic para cerrar.", font=('Arial', 9, 'italic'), foreground=COLOR_TEXTO).pack(pady=0) popup.bind("", close_and_stop) frame.bind("", close_and_stop) self.root.wait_window(popup) # ------------------------------------------------------------- # 📈 LÓGICA DE T1 (MONITOR DE RECURSOS) # ------------------------------------------------------------- def actualizar_recursos(self): """Obtiene los datos del sistema y delega el dibujo al módulo RecursosPanel.""" try: if 'Recursos' not in self.modulos or self.net_monitor is None: # Si el módulo Recursos o el monitor no están listos, reprogramamos. self.after_id = self.after(self.INTERVALO_ACTUALIZACION_MS, self.actualizar_recursos) return # 1. OBTENER DATOS (Red, CPU y RAM) # 🔑 CORRECCIÓN CLAVE: Desempaquetar los 4 valores devueltos por TrafficMeter. net_in, net_out, cpu_percent, ram_percent = self.net_monitor.get_io_data_kb() # 2. ACTUALIZAR Y DIBUJAR recursos_panel = self.modulos['Recursos'] # Llama a actualizar_datos en la lógica de graficos.py recursos_panel.actualizar_datos(net_in, net_out, cpu_percent, ram_percent) # Llama a dibujar_grafico en la vista (que ahora incluye self.canvas.draw()) recursos_panel.dibujar_grafico() except Exception as e: # Captura y muestra el error, pero no detiene la tarea print(f"Error en la actualización de recursos T1: {e}") # 3. REPROGRAMAR TAREA (Crucial para el bucle) self.after_id = self.after(self.INTERVALO_ACTUALIZACION_MS, self.actualizar_recursos) def iniciar_actualizacion_automatica(self): """ Inicia el ciclo de actualización del gráfico de recursos. Inicializa TrafficMeter si no se hizo en __init__. """ if self.net_monitor is None: print("Inicializando TrafficMeter antes de actualizar recursos.") try: self.net_monitor = iniciar_monitor_red() except Exception as e: messagebox.showerror("Error de Hilo", f"No se pudo iniciar TrafficMeter. ¿Falta 'psutil'? Detalle: {e}") print("No se pudo iniciar TrafficMeter. La actualización automática no comenzará.") return print("Iniciando actualización automática de recursos.") # 🔑 CORRECCIÓN: Realizar una llamada inicial a psutil.cpu_percent() # para establecer el punto de partida del intervalo de medición en el hilo principal. psutil.cpu_percent(interval=None) # Iniciar el ciclo de actualización. self.after_id = self.after(0, self.actualizar_recursos) def detener_actualizacion_automatica(self): """Detiene el ciclo de actualización periódica y los hilos/tareas de los módulos.""" if self.after_id: self.after_cancel(self.after_id) self.after_id = None print("Ciclo de actualización de gráficos detenido.") if self.net_monitor: self.net_monitor.stop() self.net_monitor.join() print("Hilo de TrafficMeter detenido.") # Llama a los métodos de detención de los módulos (si existen) for nombre, modulo in self.modulos.items(): if hasattr(modulo, 'detener_actualizacion'): modulo.detener_actualizacion()