From 813b2b7d65c216b57556caf2ae891544a5827ebc Mon Sep 17 00:00:00 2001 From: BYolivia Date: Thu, 4 Dec 2025 02:41:22 +0100 Subject: [PATCH] ``` feat(vista): Integra carrera camellos (main) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integra T2: simulación de carrera de camellos con hilos. * Añade interfaz gráfica en pestaña "Resultados". * Implementa lógica de inicio, progreso y resultado. * Utiliza locks para evitar deadlocks en tramos críticos. * Guarda el resultado final de la carrera. * Refactoriza panel central y lateral para integrar la app. * Agrega botón en panel lateral para iniciar la carrera. * Usa un número aleatorio de camellos entre 10 y 20. * Corrige error de cuota insuficiente. ``` --- logica/T2/carreraCamellos.py | 189 +++++++++++++++++++++++ logica/T2/miniGame.py | 0 vista/panel_central.py | 291 +++++++++++++++++++++++++++++------ vista/panel_lateral.py | 50 +++--- 4 files changed, 462 insertions(+), 68 deletions(-) create mode 100644 logica/T2/carreraCamellos.py delete mode 100644 logica/T2/miniGame.py diff --git a/logica/T2/carreraCamellos.py b/logica/T2/carreraCamellos.py new file mode 100644 index 0000000..2e270e3 --- /dev/null +++ b/logica/T2/carreraCamellos.py @@ -0,0 +1,189 @@ +# Módulo: logica/T2/carreraCamellos.py + +import threading +import time +import random +import uuid + +# --- RECURSOS Y ESTADO GLOBAL PERSISTENTE --- + +# Locks para simular tramos críticos y prevenir DEADLOCKS (Resource Ordering) +lock_tramo_1 = threading.Lock() +lock_tramo_2 = threading.Lock() + +# Evento para controlar el inicio de la carrera +CARRERA_EN_CURSO = threading.Event() + +# Variable GLOBAL y PERSISTENTE para guardar el ÚLTIMO resultado completo de la carrera. +# Se inicializa a None y se actualiza al finalizar CADA carrera. +# Estructura: {'id': 'uuid', 'activa': bool, 'meta': int, 'camellos': [...], 'ganador': str} +RESULTADO_ULTIMO = None + + +# --- CLASE DEL HILO CAMELLO --- + +class Camello(threading.Thread): + """Representa un camello que avanza y necesita adquirir recursos (Locks).""" + + def __init__(self, nombre, carrera_id, distancia_meta=50): + super().__init__() + self.nombre = nombre + self.carrera_id = carrera_id + self.progreso = 0 + self.distancia_meta = distancia_meta + self.posicion_final = None + self.estado = "Esperando" + + def run(self): + self.estado = "Listo" + CARRERA_EN_CURSO.wait() + self.estado = "Corriendo" + + # Lista local para guardar el progreso para la actualización final + progreso_local = [] + + while self.progreso < self.distancia_meta: + if not CARRERA_EN_CURSO.is_set(): # Salir si la carrera se detiene manualmente + self.estado = "Abortado" + break + + avance = random.randint(1, 5) + self.progreso += avance + if self.progreso >= self.distancia_meta: + self.progreso = self.distancia_meta + break + + # 1. TRAMO 1: Adquirir Lock 1 + if self.progreso >= 15 and self.progreso < 30: + self.intentar_avanzar_tramo(lock_tramo_1, "TRAMO 1") + + # 2. TRAMO 2: Adquirir Lock 2 (Respetando la jerarquía para evitar deadlock) + elif self.progreso >= 30 and self.progreso < 45: + # Simulación de que necesita el Lock 1 y luego el Lock 2 (Jerarquía 1 -> 2) + self.intentar_avanzar_tramo_jerarquico() + + time.sleep(0.1) + progreso_local.append(self.progreso) + + if self.progreso >= self.distancia_meta: + self.finalizar_carrera() + + def intentar_avanzar_tramo(self, lock_actual, nombre_tramo): + """Intenta adquirir un Lock simple para un tramo crítico.""" + self.estado = f"Esperando {nombre_tramo}" + with lock_actual: + self.estado = f"En {nombre_tramo} 🔒" + time.sleep(random.uniform(0.5, 1.0)) + self.estado = "Corriendo" + + def intentar_avanzar_tramo_jerarquico(self): + """Simula la necesidad de adquirir Locks en ORDEN (1 luego 2) para prevenir interbloqueo.""" + self.estado = "Esperando TRAMO 2 (Lock 1 y 2)" + + # ⚠️ Clave de la prevención de Deadlock: Adquirir siempre en el mismo orden (Lock 1, luego Lock 2) + with lock_tramo_1: + with lock_tramo_2: + self.estado = "En TRAMO 2 (Lock 1+2) 🔒🔒" + time.sleep(random.uniform(1.0, 2.0)) + self.estado = "Corriendo" + + def finalizar_carrera(self): + """Registra el resultado final de la carrera y lo guarda en RESULTADO_ULTIMO.""" + global RESULTADO_ULTIMO + + # Creamos una copia local del estado para que sea seguro leer desde el hilo principal + estado_camello = { + 'nombre': self.nombre, + 'progreso': self.progreso, + 'estado': "Meta", + 'posicion': None, # Se asigna después + 'carrera_id': self.carrera_id + } + + # Utilizamos lock_tramo_1 para serializar la escritura en la variable global compartida + with lock_tramo_1: + if RESULTADO_ULTIMO and RESULTADO_ULTIMO['id'] == self.carrera_id: + RESULTADO_ULTIMO['camellos'].append(estado_camello) + + # Asignar posición: len actual de la lista es la posición + self.posicion_final = len(RESULTADO_ULTIMO['camellos']) + RESULTADO_ULTIMO['camellos'][-1]['posicion'] = self.posicion_final + + if self.posicion_final == 1: + RESULTADO_ULTIMO['ganador'] = self.nombre + + self.estado = "Meta" + print(f"🐫 {self.nombre} ha llegado en la posición {self.posicion_final}.") + + +# --- FUNCIONES DE CONTROL DE LA CARRERA --- + +def iniciar_carrera(nombres_camellos): + """Inicializa y comienza los hilos de la carrera, limpiando el resultado anterior.""" + global RESULTADO_ULTIMO + + carrera_id = str(uuid.uuid4()) + + # 1. Limpiar el estado global y preparar el nuevo resultado PERSISTENTE + RESULTADO_ULTIMO = { + 'id': carrera_id, + 'activa': True, + 'meta': 50, + 'camellos': [], # Aquí se acumularán los resultados finales + 'ganador': 'Nadie' + } + + CARRERA_EN_CURSO.clear() + + camellos = [Camello(nombre, carrera_id) for nombre in nombres_camellos] + + for camello in camellos: + camello.start() + + CARRERA_EN_CURSO.set() + + return camellos + + +def obtener_estado_carrera(camellos): + """ + Devuelve el estado actual de los camellos (mientras corren) O el último resultado guardado + (si ya han terminado). + """ + global RESULTADO_ULTIMO + + if RESULTADO_ULTIMO and not CARRERA_EN_CURSO.is_set(): + # Si la carrera ya terminó, devolvemos el resultado guardado + return { + 'tipo': 'final', + 'datos': RESULTADO_ULTIMO + } + + # Si la carrera está activa o no ha comenzado, devolvemos el progreso en tiempo real + estado_tiempo_real = [ + { + 'nombre': c.nombre, + 'progreso': c.progreso, + 'estado': c.estado, + 'posicion': c.posicion_final + } for c in camellos if c.is_alive() or c.progreso == c.distancia_meta + ] + + # Si todos los hilos han terminado, marcamos la carrera como inactiva + if estado_tiempo_real and all( + e['progreso'] >= RESULTADO_ULTIMO['meta'] for e in estado_tiempo_real) and RESULTADO_ULTIMO: + with lock_tramo_1: + RESULTADO_ULTIMO['activa'] = False + + return { + 'tipo': 'activo', + 'datos': { + 'activa': CARRERA_EN_CURSO.is_set(), + 'camellos': estado_tiempo_real + } + } + + +def detener_carrera(): + """Detiene la señal de la carrera.""" + CARRERA_EN_CURSO.clear() \ No newline at end of file diff --git a/logica/T2/miniGame.py b/logica/T2/miniGame.py deleted file mode 100644 index e69de29..0000000 diff --git a/vista/panel_central.py b/vista/panel_central.py index 94ed333..395b549 100644 --- a/vista/panel_central.py +++ b/vista/panel_central.py @@ -1,5 +1,6 @@ # Módulo: vista/panel_central.py +import random import tkinter as tk from tkinter import ttk from logica.T1.trafficMeter import iniciar_monitor_red @@ -7,21 +8,37 @@ from logica.T1.graficos import crear_grafico_recursos, actualizar_historial_dato from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg -# --- IMPORTACIÓN DE CONSTANTES DESDE vista/config.py --- +# --- IMPORTACIÓN DE LA LÓGICA DE T2 (CARRERA DE CAMELLOS) --- +from logica.T2.carreraCamellos import ( + iniciar_carrera, + obtener_estado_carrera, + detener_carrera, + RESULTADO_ULTIMO # Variable de estado persistente +) + +# --- IMPORTACIÓN UNIVERSAL DE CONSTANTES --- +# Asume que todas las constantes de estilo y colores necesarias están definidas en vista.config from vista.config import * -class PanelCentral(ttk.Frame): - """Contiene el Notebook (subpestañas de T1), el panel de Notas y el panel de Chat.""" - # Usamos la constante importada para el intervalo de actualización +class PanelCentral(ttk.Frame): + """Contiene el Notebook (subpestañas de T1, T2 en Resultados), el panel de Notas y el panel de Chat.""" + INTERVALO_ACTUALIZACION_MS = INTERVALO_RECURSOS_MS + INTERVALO_CARRERA_MS = 200 def __init__(self, parent, *args, **kwargs): super().__init__(parent, *args, **kwargs) self.after_id = None - self.parent_root = parent.winfo_toplevel() + self.after_carrera_id = None + self.camellos = [] + self.progreso_labels = {} + self.frame_carrera_controles = None + self.frame_progreso = None + self.carrera_info_label = None + self.carrera_estado_label = None - # 1. INICIALIZACIÓN DE VARIABLES + # 1. INICIALIZACIÓN DE VARIABLES (T1) self.net_monitor = iniciar_monitor_red() self.figure = Figure(figsize=(5, 4), dpi=100) self.canvas = None @@ -35,9 +52,14 @@ class PanelCentral(ttk.Frame): self.crear_panel_chat_y_alumnos() self.iniciar_actualizacion_automatica() + self.iniciar_actualizacion_carrera() + + # ------------------------------------------------------------- + # 📦 ESTRUCTURA PRINCIPAL DEL PANEL + # ------------------------------------------------------------- def crear_area_principal_y_notas(self): - """Crea el contenedor de las subpestañas de T1 y el panel de notas (inferior izquierda).""" + """Crea el contenedor de las subpestañas y el panel de notas.""" frame_izquierdo = ttk.Frame(self, style='TFrame') frame_izquierdo.grid(row=0, column=0, sticky="nsew") @@ -45,19 +67,17 @@ class PanelCentral(ttk.Frame): frame_izquierdo.grid_rowconfigure(1, weight=1) frame_izquierdo.grid_columnconfigure(0, weight=1) - self.crear_sub_pestañas_t1(frame_izquierdo) # Llamada al método que crea las pestañas + self.crear_notebook_pestañas(frame_izquierdo) - # 2. El Panel de Notas panel_notas = ttk.Frame(frame_izquierdo, style='Note.TFrame') panel_notas.grid(row=1, column=0, sticky="nsew", pady=(5, 0)) - # Usando FUENTE_NOTA ttk.Label(panel_notas, text="Panel para notas informativas y mensajes sobre la ejecución de los hilos.", style='Note.TLabel', anchor="nw", justify=tk.LEFT, padding=10, font=FUENTE_NOTA).pack( expand=True, fill="both") - def crear_sub_pestañas_t1(self, parent_frame): - """Crea las pestañas internas para la tarea T1 y las empaqueta en la rejilla.""" + def crear_notebook_pestañas(self, parent_frame): + """Crea las pestañas internas para las tareas (T1, Carrera en Resultados).""" sub_notebook = ttk.Notebook(parent_frame) sub_notebook.grid(row=0, column=0, sticky="nsew") @@ -69,37 +89,191 @@ class PanelCentral(ttk.Frame): sub_notebook.add(frame, text=sub_tab_text) self.tabs[sub_tab_text] = frame - # LÓGICA DE LA PESTAÑA DE RECURSOS if sub_tab_text == "Recursos": + # Lógica de T1 - Gráfico de recursos self.grafico_frame = ttk.Frame(frame, style='TFrame') self.grafico_frame.pack(expand=True, fill="both", padx=10, pady=10) - # Inicialización del Canvas de Matplotlib self.canvas = FigureCanvasTkAgg(self.figure, master=self.grafico_frame) self.canvas_widget = self.canvas.get_tk_widget() self.canvas_widget.pack(expand=True, fill="both") - sub_notebook.select(i) + # --- INTEGRACIÓN DE LA CARRERA EN LA PESTAÑA "Resultados" --- + elif sub_tab_text == "Resultados": + self.crear_interfaz_carrera(frame) - # Contenido de otras pestañas - elif sub_tab_text == "Navegador": - # Usando COLOR_BLANCO, FUENTE_MONO y COLOR_TEXTO - contenido_area = tk.Text(frame, wrap="word", padx=15, pady=15, bg=COLOR_BLANCO, relief="groove", - borderwidth=1, font=FUENTE_MONO, foreground=COLOR_TEXTO) - contenido_area.insert(tk.END, - ">>> ÁREA DE CONTENIDO / VISOR DE NAVEGADOR (Para mostrar resultados o web scraping)\n\n""Este es el espacio dedicado a la visualización de datos o interfaces específicas de cada tarea.") - contenido_area.pack(expand=True, fill="both", padx=5, pady=5) + # ------------------------------------------------------------- + # 🐪 LÓGICA DE T2 (CARRERA DE CAMELLOS) + # ------------------------------------------------------------- + + def crear_interfaz_carrera(self, parent_frame): + """Crea los controles y la visualización de la Carrera de Camellos.""" + + # 1. Título y Controles (Solo título, el botón de inicio va en el lateral) + frame_controles = ttk.Frame(parent_frame, style='TFrame', padding=10) + frame_controles.pack(fill="x") + self.frame_carrera_controles = frame_controles + + ttk.Label(frame_controles, text="Resultado de Carrera de Camellos (T2 Sincronización)", + style='TLabel', font=FUENTE_NEGOCIOS).pack(side="left", padx=5) + + self.carrera_estado_label = ttk.Label(frame_controles, text="Estado.", style='TLabel', font=FUENTE_NEGOCIOS) + self.carrera_estado_label.pack(side="right", padx=10) + + # 2. Marco de visualización de progreso + self.frame_progreso = ttk.Frame(parent_frame, style='TFrame', padding=10) + self.frame_progreso.pack(fill="both", expand=True) + + ttk.Label(self.frame_progreso, + text="Presiona el botón 'App2 (T2-Carrera 🏁)' en el panel lateral para iniciar la simulación de hilos.", + style='TLabel').pack(pady=20) + + def manejar_inicio_carrera(self): + """Inicia una nueva carrera de camellos con un número aleatorio de participantes.""" + if self.camellos and any(c.is_alive() for c in self.camellos): + self.carrera_estado_label.config(text="⚠️ Ya hay una carrera en curso.") + return + + print("Iniciando Carrera de Camellos con número variable de participantes...") + + # Genera un número aleatorio de camellos entre 10 y 20 + num_camellos = random.randint(10, 20) + nombres = [f"Camello {i + 1}" for i in range(num_camellos)] + + # Limpiar la visualización anterior + for widget in self.frame_progreso.winfo_children(): + widget.destroy() + self.progreso_labels = {} + + self.camellos = iniciar_carrera(nombres) + + # --- MENSAJE SIMPLIFICADO --- + self.carrera_estado_label.config(text=f"¡Carrera en marcha! 💨 ({num_camellos} participantes)") + + self.crear_visualizacion_carrera(nombres) + self.iniciar_actualizacion_carrera() + + def crear_visualizacion_carrera(self, nombres): + """Prepara el layout de la carrera.""" + for widget in self.frame_progreso.winfo_children(): + widget.destroy() + + for i, nombre in enumerate(nombres): + ttk.Label(self.frame_progreso, text=f"{nombre}: ", style='TLabel', font=FUENTE_NEGOCIOS).grid(row=i, + column=0, + sticky="w") + + label_progreso = ttk.Label(self.frame_progreso, text="[Esperando...]", style='TLabel', + foreground=COLOR_TEXTO) + label_progreso.grid(row=i, column=1, sticky="w", padx=10) + self.progreso_labels[nombre] = label_progreso + + label_posicion = ttk.Label(self.frame_progreso, text="", style='TLabel') + label_posicion.grid(row=i, column=2, sticky="w") + self.progreso_labels[f'{nombre}_pos'] = label_posicion + + self.carrera_info_label = ttk.Label(self.frame_progreso, text="", style='TLabel', font=FUENTE_NEGOCIOS) + self.carrera_info_label.grid(row=len(nombres), column=0, columnspan=3, sticky="w", pady=(10, 0)) + + def mostrar_progreso_activo(self, datos_activos): + """Actualiza la visualización de la carrera mientras los hilos están corriendo.""" + + if not datos_activos['camellos']: + return + + nombres_activos = [c['nombre'] for c in datos_activos['camellos']] + if not self.progreso_labels or any(n not in self.progreso_labels for n in nombres_activos): + self.crear_visualizacion_carrera(nombres_activos) + + for estado in datos_activos['camellos']: + nombre = estado['nombre'] + progreso = estado['progreso'] + etiqueta_progreso = self.progreso_labels.get(nombre) + etiqueta_posicion = self.progreso_labels.get(f'{nombre}_pos') + + if etiqueta_progreso: + barra = "█" * (progreso // 2) + texto_progreso = f"[{barra}{' ' * (25 - len(barra))}] ({progreso}/50) Estado: {estado['estado']}" + etiqueta_progreso.config(text=texto_progreso) + + if etiqueta_posicion and estado['posicion']: + etiqueta_posicion.config(text=f"POS: {estado['posicion']} 🏆", foreground=COLOR_ACCION) + + # --- MENSAJE SIMPLIFICADO --- + self.carrera_estado_label.config(text="Carrera en curso...") + self.carrera_info_label.config(text="") + + def mostrar_resultado_final(self, resultado_final): + """Muestra el resultado final persistente de la carrera.""" + + nombres_finales = [c['nombre'] for c in resultado_final['camellos']] + if not self.progreso_labels or any(n not in self.progreso_labels for n in nombres_finales): + self.crear_visualizacion_carrera(nombres_finales) + + camellos_ordenados = sorted(resultado_final['camellos'], key=lambda x: x['posicion']) + + for estado in camellos_ordenados: + nombre = estado['nombre'] + etiqueta_progreso = self.progreso_labels.get(nombre) + etiqueta_posicion = self.progreso_labels.get(f'{nombre}_pos') + + if etiqueta_progreso: + barra = "█" * (estado['progreso'] // 2) + texto_progreso = f"[{barra}{' ' * (25 - len(barra))}] (50/50) Estado: Meta" + etiqueta_progreso.config(text=texto_progreso) + + if etiqueta_posicion: + etiqueta_posicion.config(text=f"POS: {estado['posicion']} {'🏆' if estado['posicion'] == 1 else ''}", + foreground=COLOR_ACCION) + + # --- MENSAJE SIMPLIFICADO --- + self.carrera_estado_label.config(text="✅ Carrera Terminada.") + self.carrera_info_label.config(text=f"¡El ganador es: {resultado_final['ganador']}!", + font=FUENTE_TITULO, foreground=COLOR_EXITO) + + def actualizar_carrera(self): + """Ciclo de actualización visual: lee el estado activo o el resultado final persistente.""" + + estado = obtener_estado_carrera(self.camellos) + + if estado['tipo'] == 'final': + self.mostrar_resultado_final(estado['datos']) + self.detener_actualizacion_carrera() + return + + elif estado['tipo'] == 'activo': + self.mostrar_progreso_activo(estado['datos']) + + self.after_carrera_id = self.after(self.INTERVALO_CARRERA_MS, self.actualizar_carrera) + + def iniciar_actualizacion_carrera(self): + """Inicia el ciclo de actualización visual (o carga el resultado guardado al inicio).""" + self.detener_actualizacion_carrera() + + if RESULTADO_ULTIMO and not RESULTADO_ULTIMO.get('activa', True) and self.frame_progreso: + self.mostrar_resultado_final(RESULTADO_ULTIMO) + else: + # --- MENSAJE SIMPLIFICADO AL CARGAR --- + self.carrera_estado_label.config(text="Carrera lista para empezar.") + self.after_carrera_id = self.after(0, self.actualizar_carrera) + + def detener_actualizacion_carrera(self): + """Detiene el ciclo de actualización visual de la carrera.""" + if self.after_carrera_id: + self.after_cancel(self.after_carrera_id) + self.after_carrera_id = None + print("Ciclo de actualización de carrera detenido.") + + # ------------------------------------------------------------- + # 📈 LÓGICA DE T1 (MONITOR DE RECURSOS) + # ------------------------------------------------------------- def actualizar_recursos(self): """Obtiene los datos del sistema (incluyendo Red) y dibuja/redibuja el gráfico.""" try: - # 1. Obtener datos de red del hilo (en KB/s) net_in, net_out = self.net_monitor.get_io_data_kb() - - # 2. Actualizar el historial global (CPU y RAM se obtienen dentro de esta función) actualizar_historial_datos(net_in, net_out) - # 3. Redibujar el gráfico if self.canvas: crear_grafico_recursos(self.figure) self.canvas.draw() @@ -107,24 +281,21 @@ class PanelCentral(ttk.Frame): except Exception as e: error_msg = f"Error al generar el gráfico de recursos: {e}" print(error_msg) - if self.canvas_widget.winfo_exists(): + if self.canvas_widget and self.canvas_widget.winfo_exists(): self.canvas_widget.pack_forget() error_label = ttk.Label(self.grafico_frame, text=error_msg, foreground='red', style='TLabel') error_label.pack(pady=20) self.detener_actualizacion_automatica() - # 4. Programar la siguiente llamada self.after_id = self.after(self.INTERVALO_ACTUALIZACION_MS, self.actualizar_recursos) - # --- Control del Ciclo de Vida (Actualizar los nombres de las llamadas) --- - def iniciar_actualizacion_automatica(self): """Inicia el ciclo de actualización del gráfico de recursos.""" print("Iniciando actualización automática de recursos.") self.after_id = self.after(0, self.actualizar_recursos) def detener_actualizacion_automatica(self): - """Detiene el ciclo de actualización periódica y el hilo de red.""" + """Detiene el ciclo de actualización periódica y el hilo de red (T1) y la carrera (T2).""" if self.after_id: self.after_cancel(self.after_id) self.after_id = None @@ -135,52 +306,72 @@ class PanelCentral(ttk.Frame): self.net_monitor.join() print("Hilo de TrafficMeter detenido.") + self.detener_actualizacion_carrera() + + # ------------------------------------------------------------- + # 💬 PANEL CHAT, ALUMNOS Y APPS (PANEL LATERAL) + # ------------------------------------------------------------- + def crear_panel_chat_y_alumnos(self, ): - """Crea el panel de chat, lista de Alumnos y Reproductor de Música (columna derecha).""" + """Crea el panel de chat y lista de Alumnos (columna derecha).""" panel_chat = ttk.Frame(self, style='TFrame', padding="10") panel_chat.grid(row=0, column=1, sticky="nsew") + # Configuración de expansión (La fila 5, que contiene los alumnos, es la que se expande) panel_chat.grid_rowconfigure(5, weight=1) - panel_chat.grid_rowconfigure(7, weight=0) panel_chat.grid_columnconfigure(0, weight=1) - # 1. Título "Chat" - Usando COLOR_ACCION y FUENTE_TITULO + # --- FILA 0: Título --- ttk.Label(panel_chat, text="Chat", foreground=COLOR_ACCION, font=FUENTE_TITULO, style='TLabel').grid(row=0, - column=0, - pady=( - 0, - 10), - sticky="w") + column=0, + pady=( + 0, + 10), + sticky="w") - # 2. Área de Mensaje + # --- FILAS 1-3: Chat Input --- ttk.Label(panel_chat, text="Mensaje", style='TLabel').grid(row=1, column=0, sticky="w") - # Usando color de fondo simulado para el chat - chat_text = tk.Text(panel_chat, height=6, width=30, bg='#fff8e1', relief="solid", borderwidth=1, + # Usando COLOR_BLANCO para el fondo del Text + chat_text = tk.Text(panel_chat, height=6, width=30, bg=COLOR_BLANCO, relief="solid", borderwidth=1, font=('Arial', 10)) chat_text.grid(row=2, column=0, sticky="ew", pady=(0, 5)) - ttk.Button(panel_chat, text="Enviar", style='Action.TButton').grid(row=3, column=0, pady=(0, 15), sticky="e") - # 3. Lista de Alumnos (Simulación) + # --- FILAS 4-7: Alumnos (Se expanden) --- for i in range(1, 4): frame_alumno = ttk.Frame(panel_chat, style='Alumno.TFrame', padding=8) frame_alumno.grid(row=3 + i, column=0, sticky="ew", pady=5) frame_alumno.grid_columnconfigure(0, weight=1) - # El font de Alumno está en los estilos de VentanaPrincipal ttk.Label(frame_alumno, text=f"Alumno {i}", font=('Arial', 11, 'bold'), style='Alumno.TLabel').grid(row=0, column=0, sticky="w") - ttk.Label(frame_alumno, text="Lorem ipsum dolor sit amet, consectetur adipiscing elit.", wraplength=250, justify=tk.LEFT, style='Alumno.TLabel').grid(row=1, column=0, sticky="w") - ttk.Button(frame_alumno, text="↻", width=3, style='Action.TButton').grid(row=0, column=1, rowspan=2, padx=5, sticky="ne") - # 4. Reproductor de Música (Simulado) + # --- FILA 8: Música --- musica_frame = ttk.LabelFrame(panel_chat, text="Reproductor Música", padding=10, style='TFrame') musica_frame.grid(row=8, column=0, sticky="ew", pady=(15, 0)) ttk.Label(musica_frame, text="[ Botones de Play/Stop y Control de Volumen ]", anchor="center", - style='TLabel').pack(fill="x", padx=5, pady=5) \ No newline at end of file + style='TLabel').pack(fill="x", padx=5, pady=5) + + # --- FILA 9: Botones de Aplicación (Minijuegos) --- + frame_apps = ttk.LabelFrame(panel_chat, text="Minijuegos / Apps", padding=10, style='TFrame') + frame_apps.grid(row=9, column=0, sticky="ew", pady=(15, 0)) + + # Botón 1: Placeholder + ttk.Button(frame_apps, text="App1 (T3-Red)", width=15, style='Action.TButton').pack(side="left", padx=5) + + # Botón 2: Inicia la Carrera de Camellos (T2 Sincronización) + ttk.Button(frame_apps, + text="App2 (T2-Carrera 🏁)", + command=self.manejar_inicio_carrera, + width=15, + style='Action.TButton').pack(side="left", padx=5) + + # Asegurar que las filas de abajo no se expandan + panel_chat.grid_rowconfigure(8, weight=0) + panel_chat.grid_rowconfigure(9, weight=0) \ No newline at end of file diff --git a/vista/panel_lateral.py b/vista/panel_lateral.py index 6edfcbc..f41fbcf 100644 --- a/vista/panel_lateral.py +++ b/vista/panel_lateral.py @@ -21,6 +21,7 @@ class PanelLateral(ttk.Frame): def __init__(self, parent, central_panel=None, *args, **kwargs): super().__init__(parent, *args, **kwargs) + # La referencia al PanelCentral es esencial para iniciar la carrera self.central_panel = central_panel self.configurar_estilos_locales(parent) @@ -32,7 +33,6 @@ class PanelLateral(ttk.Frame): # 2. Área de Extracción/Navegación acciones_extraccion = [ - # NOTA: Cambiamos el nombre de este comando si maneja una acción manual ("Actualizar Recursos", self.manejar_extraccion_datos), ("Navegar", self.manejar_navegacion), ("Buscar API Google", lambda: accion_placeholder("Buscar API Google")) @@ -40,9 +40,15 @@ class PanelLateral(ttk.Frame): self.crear_seccion(self, titulo="", acciones=acciones_extraccion) # 3. Área de Aplicaciones + + # --- CAMBIO CLAVE: CONEXIÓN DE APP2 --- + + # Definimos el comando para App2 usando el método que llama a PanelCentral + app2_comando = self.manejar_inicio_carrera_t2 + acciones_aplicaciones = [ ("Visual Code", abrir_vscode), - ("App2", lambda: accion_placeholder("App2")), + ("App2 (Carrera 🏁)", app2_comando), # <--- CONEXIÓN REALIZADA ("App3", lambda: accion_placeholder("App3")) ] self.crear_seccion(self, titulo="Aplicaciones", acciones=acciones_aplicaciones) @@ -59,29 +65,44 @@ class PanelLateral(ttk.Frame): # 6. Panel de Notas self.crear_editor_res_notes() - # --- NUEVO MÉTODO PARA MANEJAR LA NAVEGACIÓN --- + # --- NUEVO MÉTODO PARA MANEJAR LA CARRERA (App2) --- + def manejar_inicio_carrera_t2(self): + """ + Llama al método 'manejar_inicio_carrera' del Panel Central. + """ + if self.central_panel: + print("Botón App2 presionado. Iniciando Carrera de Camellos en Panel Central...") + # Aquí es donde se llama a la función expuesta por PanelCentral + self.central_panel.manejar_inicio_carrera() + + # Opcional: Cambiar automáticamente a la pestaña Resultados + if "Resultados" in self.central_panel.tabs: + notebook = self.central_panel.tabs["Resultados"].winfo_toplevel().winfo_children()[0] + if isinstance(notebook, ttk.Notebook): + # Asume que el Notebook es el primer widget hijo del frame principal + notebook.select(self.central_panel.tabs["Resultados"]) + else: + messagebox.showerror("Error", "El Panel Central no está inicializado.") + + # --- MÉTODOS EXISTENTES --- def manejar_navegacion(self, event=None): """ Obtiene el texto de la entrada superior y llama a la función de navegación. """ url = self.entrada_superior.get() if navegar_a_url(url): - # Limpiar la casilla si la navegación fue exitosa self.entrada_superior.delete(0, tk.END) - # --- LÓGICA DEL EDITOR res/notes --- - def crear_editor_res_notes(self): """Crea el editor de texto simple para el archivo res/notes.""" - # Usando FUENTE_NEGOCIOS ttk.Label(self, text="Editor Simple (res/notes)", font=FUENTE_NEGOCIOS).pack(fill="x", pady=(10, 0), padx=5) frame_editor = ttk.Frame(self, padding=5) frame_editor.pack(fill="x", padx=5, pady=(0, 10)) - # 1. Widget de texto - Aplicamos el ancho fijo, COLOR_BLANCO y FUENTE_MONO + # 1. Widget de texto self.notes_text_editor = tk.Text( frame_editor, height=8, @@ -90,7 +111,7 @@ class PanelLateral(ttk.Frame): bg=COLOR_BLANCO, relief="solid", borderwidth=1, - font=FUENTE_MONO # Fuente tipo terminal + font=FUENTE_MONO ) self.notes_text_editor.pack(fill="x", expand=False) @@ -134,12 +155,10 @@ class PanelLateral(ttk.Frame): messagebox.showerror("❌ Error al Guardar", message) print(f"FALLO AL GUARDAR: {message}") - # --- MÉTODOS EXISTENTES --- def manejar_extraccion_datos(self): """ Llama a la lógica de actualización del gráfico de recursos en el panel central (actualización manual). """ - # NOTA: Renombramos a 'actualizar_recursos' para ser consistente con panel_central.py if self.central_panel: print("Activando actualización del gráfico de Recursos (Manual)...") self.central_panel.actualizar_recursos() @@ -160,31 +179,26 @@ class PanelLateral(ttk.Frame): """Configura estilos para los widgets del panel lateral, usando constantes importadas.""" style = ttk.Style(parent) - # Estilos existentes (Usando constantes importadas) style.configure('Yellow.TEntry', fieldbackground='#fff8e1', foreground=COLOR_TEXTO, padding=[5, 5], relief='solid', borderwidth=1) - # Botones de Acción (Green.TButton) style.configure('Green.TButton', background=COLOR_EXITO, foreground=COLOR_BLANCO, font=FUENTE_NEGOCIOS, relief='flat', padding=[10, 5]) style.map('Green.TButton', background=[('active', '#388E3C'), ('pressed', - '#1B5E20')]) # Manteniendo los tonos verdes originales para hover/pressed + '#1B5E20')]) - # Botones de Acción Global (Action.TButton) style.configure('Action.TButton', background=COLOR_ACCION, foreground=COLOR_BLANCO, font=FUENTE_NEGOCIOS, relief='flat', padding=[10, 5]) style.map('Action.TButton', background=[('active', COLOR_ACCION_HOVER), ('pressed', COLOR_ACCION_PRESSED)]) - # NUEVO ESTILO: Botones pequeños para el editor de notas (SmallAction.TButton) style.configure('SmallAction.TButton', background=COLOR_ACCION, foreground=COLOR_BLANCO, - font=('Arial', 9, 'bold'), + font=(FUENTE_FAMILIA, 9, 'bold'), relief='flat', padding=[5, 3]) style.map('SmallAction.TButton', background=[('active', COLOR_ACCION_HOVER), ('pressed', COLOR_ACCION_PRESSED)]) def crear_seccion(self, parent_frame, titulo, acciones): """Función helper para crear secciones de etiquetas y botones.""" if titulo: - # Usando FUENTE_NEGOCIOS ttk.Label(parent_frame, text=titulo, font=FUENTE_NEGOCIOS).pack(fill="x", pady=(10, 0), padx=5) frame_botones = ttk.Frame(parent_frame, style='TFrame')