feat(vista): Integra alarmas, refactoriza y corrige (main)

Integra T4: Sistema de alarmas y mejoras generales.
* Añade pestaña "Alarmas" para programar temporizadores.
* Implementa creación, cancelación y visualización de alarmas.
* Refactoriza y corrige la lógica de reproducción de radios (T3).
* Mueve el editor de notas al panel central (pestaña "Tareas").
* Elimina el editor de notas del panel lateral.
* Corrige la gestión de volumen en el reproductor de radios.
* Actualiza dependencias y rutas de ficheros.
This commit is contained in:
BYolivia 2025-12-05 01:50:25 +01:00
parent 983836d94c
commit 8911576bb7
7 changed files with 555 additions and 190 deletions

View File

@ -40,6 +40,6 @@ python -m ProyectoGlobal
4. Scraping
5. Juego de los camellos / autos de choque / etc. (aplicar resolución de sincronización para evitar problemas de interbloqueos)
5. ~~Juego de los camellos~~ / autos de choque / etc. (aplicar resolución de sincronización para evitar problemas de interbloqueos)
6. Música de fondo (reproducción de mp3 o midi)
6. ~~Música de fondo (reproducción de mp3 o midi)~~

View File

@ -0,0 +1,131 @@
# Módulo: logica/T2/alarm.py
import tkinter as tk
from datetime import datetime
import os
import sys
try:
from logica.T2.musicReproductor import MusicReproductor
except ImportError:
class MusicReproductor:
def __init__(self, *args, **kwargs): pass
def ajustar_volumen(self, valor): pass
def cargar_y_reproducir(self, url): print(f"🎶 SIMULANDO PLAY: {url}")
def detener(self): print("🔇 SIMULANDO STOP")
class AlarmManager:
"""
Gestiona la creación, seguimiento y disparo de múltiples alarmas (temporizadores).
"""
def __init__(self, root, trigger_callback):
self.root = root
self.active_alarms = {}
self.next_id = 1
self.trigger_callback = trigger_callback
self.alarm_sound_player = MusicReproductor()
self.ALARM_SOUND_PATH = os.path.join(os.path.dirname(__file__), '..', '..', 'res', 'alarm.mp3')
# === MODIFICACIÓN CLAVE: Acepta total_seconds en lugar de minutes ===
def set_alarm(self, total_seconds):
"""
Programa una nueva alarma para sonar después de un número de segundos.
:param total_seconds: Número de segundos hasta que suena la alarma.
:return: ID único de la alarma.
"""
if total_seconds <= 0:
raise ValueError("El tiempo de alarma debe ser positivo.")
alarm_id = self.next_id
self.next_id += 1
ms_delay = total_seconds * 1000
ms_delay_int = int(ms_delay)
tiempo_disparo_ts = datetime.now().timestamp() + total_seconds
tiempo_disparo_dt = datetime.fromtimestamp(tiempo_disparo_ts)
name = f"{tiempo_disparo_dt.strftime('%H:%M:%S')}"
alarm_data = {
'nombre': name,
'total_seconds': total_seconds, # Almacenamos el tiempo total en segundos
'tiempo_creacion': datetime.now(),
'tiempo_disparo': tiempo_disparo_ts,
'after_id': None,
'sonada': False
}
after_id = self.root.after(ms_delay_int, lambda: self._trigger_alarm(alarm_id))
alarm_data['after_id'] = after_id
self.active_alarms[alarm_id] = alarm_data
print(f"🔔 Alarma '{name}' ({alarm_id}) programada para sonar en {total_seconds} segundos.")
return alarm_id
def _trigger_alarm(self, alarm_id):
# ... (código sin cambios aquí) ...
if alarm_id in self.active_alarms:
alarm = self.active_alarms[alarm_id]
print(f"🚨 ¡ALARMA! La alarma '{alarm['nombre']}' ({alarm_id}) ha sonado.")
alarm['sonada'] = True
self.alarm_sound_player.cargar_y_reproducir(self.ALARM_SOUND_PATH)
self.trigger_callback(alarm['nombre'], alarm_id)
def stop_alarm_sound(self):
self.alarm_sound_player.detener()
def cancel_alarm(self, alarm_id):
# ... (código sin cambios aquí) ...
if alarm_id in self.active_alarms:
alarm = self.active_alarms[alarm_id]
if not alarm['sonada'] and alarm['after_id']:
self.root.after_cancel(alarm['after_id'])
print(f"❌ Alarma '{alarm['nombre']}' ({alarm_id}) cancelada.")
del self.active_alarms[alarm_id]
return True
return False
def get_active_alarms(self):
"""
Retorna una lista de las alarmas activas, incluyendo el tiempo restante.
"""
alarms_to_show = []
current_time = datetime.now().timestamp()
for alarm_id, alarm in list(self.active_alarms.items()):
if not alarm['sonada']:
time_diff = alarm['tiempo_disparo'] - current_time
if time_diff > 0:
remaining_sec = int(time_diff)
hours = remaining_sec // 3600
minutes = (remaining_sec % 3600) // 60
seconds = remaining_sec % 60
remaining_str = f"{hours:02d}h:{minutes:02d}m:{seconds:02d}s"
alarms_to_show.append({
'id': alarm_id,
'nombre': alarm['nombre'],
'restante': remaining_str,
'total_seconds': alarm['total_seconds'] # Usar total_seconds
})
return sorted(alarms_to_show, key=lambda x: x['restante'])

View File

@ -1,97 +1,77 @@
# Módulo: logica/T2/musicReproductor.py
import vlc
import threading
import os
import sys
# --- BLOQUE DE CÓDIGO OPCIONAL PARA SOLUCIÓN DE ERRORES DE RUTA DE VLC ---
# Si al ejecutar el programa obtienes un error de "ImportError: DLL load failed"
# o similar con 'vlc', DESCOMENTA el siguiente bloque y AJUSTA la ruta de vlc_path.
# Esto ayuda a que Python encuentre las librerías principales de VLC.
#
# if sys.platform.startswith('win'):
# # RUTA DE EJEMPLO PARA WINDOWS (AJUSTA según tu instalación)
# vlc_path = r"C:\Program Files\VideoLAN\VLC"
# if vlc_path not in os.environ.get('PATH', ''):
# os.environ['PATH'] += os.pathsep + vlc_path
# -------------------------------------------------------------------------
class MusicReproductor:
"""
Gestiona la reproducción de streams de radio usando la librería python-vlc.
Clase para gestionar la reproducción de audio utilizando la librería python-vlc.
Se asegura de que el objeto 'player' de VLC sea liberado y recreado correctamente
para poder manejar múltiples eventos de alarma o cambiar de stream de radio sin fallos.
"""
def __init__(self, initial_volume=50.0):
"""Inicializa la instancia de VLC y el reproductor."""
# Instancia de VLC y objeto Reproductor
def __init__(self, initial_volume=50):
# 1. Crear la instancia de VLC, que debe ser única por aplicación.
self.instance = vlc.Instance()
# 2. Creamos el player inicial.
self.player = self.instance.media_player_new()
self.current_media = None
self.is_playing = False
# Configurar volumen inicial
self.volumen = initial_volume
self.ajustar_volumen(initial_volume)
print(f"🎵 [VLC] Reproductor inicializado. Volumen: {self.player.audio_get_volume()}")
print(f"🎵 [VLC] Reproductor inicializado. Volumen: {self.volumen}")
def ajustar_volumen(self, valor_porcentual):
def ajustar_volumen(self, valor):
"""Ajusta el volumen del reproductor."""
self.volumen = int(valor)
if self.player:
self.player.audio_set_volume(self.volumen)
def cargar_y_reproducir(self, url):
"""
Ajusta el volumen del reproductor (0 a 100).
Carga el archivo o stream y lo reproduce.
**Corrección:** Recrea self.player si fue liberado (release) por el método detener().
"""
volumen_int = int(max(0, min(100, valor_porcentual)))
self.player.audio_set_volume(volumen_int)
# No imprimimos el volumen aquí para evitar saturar la consola con cada movimiento del Scale
print(f"🔄 [VLC] Intentando cargar y reproducir: {url}")
def cargar_y_reproducir(self, url_stream):
"""
Carga una nueva URL de stream y comienza la reproducción.
"""
if not url_stream:
print("❌ [VLC] URL del stream vacía.")
return
# 1. Recrear el reproductor si fue liberado.
if not self.player:
self.player = self.instance.media_player_new()
self.ajustar_volumen(self.volumen) # Restaurar volumen
print(f"🔄 [VLC] Intentando cargar y reproducir: {url_stream}")
# 2. Detener la reproducción actual de forma segura antes de cargar una nueva media.
# Esto previene el AttributeError, ya que self.player ahora está garantizado que no es None.
if self.player:
self.player.stop()
self.player.stop()
# 3. Cargar y reproducir la nueva media.
media = self.instance.media_new(url)
self.player.set_media(media)
self.current_media = self.instance.media_new(url_stream)
self.player.set_media(self.current_media)
self.player.play()
self.is_playing = True
print("✅ [VLC] Reproducción iniciada.")
def reproducir(self):
"""
Reanuda la reproducción si está pausada.
"""
if self.player.get_state() == vlc.State.Paused:
self.player.play()
self.is_playing = True
print("▶️ [VLC] Reproducción reanudada.")
if self.player.play() == 0:
print("✅ [VLC] Reproducción iniciada.")
else:
print(" [VLC] Ya está reproduciéndose o esperando un stream.")
def pausar(self):
"""
Pausa la reproducción.
"""
if self.player.get_state() == vlc.State.Playing:
self.player.pause()
self.is_playing = False
print("⏸️ [VLC] Reproducción pausada.")
else:
print(" [VLC] No se puede pausar, el reproductor no está en estado de reproducción.")
print("❌ [VLC] Error al intentar iniciar la reproducción.")
def detener(self):
"""
Detiene la reproducción y libera los recursos. Crucial al cerrar la aplicación.
Detiene la reproducción, libera los recursos del player y lo establece a None.
Esto es crucial para que el sistema de audio no se quede bloqueado por VLC.
"""
if self.player:
self.player.stop()
# Limpiar referencias para asegurar que VLC se libere correctamente
del self.player
del self.instance
print("⏹️ [VLC] Reproductor detenido y recursos liberados.")
self.player.release()
self.player = None # <--- Obliga a recrear el player en la próxima llamada a cargar_y_reproducir
print("⏹️ [VLC] Reproductor detenido y recursos liberados.")
def pausar(self):
"""Pausa la reproducción."""
if self.player and self.player.is_playing():
self.player.pause()
def reproducir(self):
"""Reanuda la reproducción."""
if self.player:
self.player.play()

BIN
res/alarm.mp3 Normal file

Binary file not shown.

View File

@ -16,5 +16,23 @@
"url_stream": "https://stream.serviciospararadios.es/listen/activa_fm/activafm-tunein.mp3",
"pais": "ES",
"genero": null
},
{
"nombre": "Cope Denia",
"url_stream": "https://denia-copesedes-rrcast.flumotion.com/copesedes/denia-low.mp3",
"pais": "ES",
"genero": "Noticias"
},
{
"nombre": "KPOO",
"url_stream": "http://amber.streamguys.com:5220/xstream",
"pais": null,
"genero": null
},
{
"nombre": "Cope Valencia",
"url_stream": "https://valencia-copesedes-rrcast.flumotion.com/copesedes/valencia-low.mp3",
"pais": "ES",
"genero": "Noticias"
}
]

View File

@ -3,13 +3,16 @@
import random
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
import json
from datetime import datetime
import os
import sys # Necesario para el bloque opcional de VLC
import sys
# --- LÓGICA DE T1 (MONITOR DE RECURSOS) ---
from logica.T1.trafficMeter import iniciar_monitor_red
from logica.T1.graficos import crear_grafico_recursos, actualizar_historial_datos
from logica.T1.textEditor import cargar_contenido_res_notes, guardar_contenido_res_notes # <--- AÑADIDO
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
@ -21,26 +24,28 @@ from logica.T2.carreraCamellos import (
)
# --- LÓGICA DE T3 (REPRODUCTOR DE MÚSICA) ---
# Se necesita el bloque opcional aquí, ya que el import 'vlc' está en este módulo
# Bloque para manejar la dependencia de VLC
try:
from logica.T2.musicReproductor import MusicReproductor
except ImportError:
# Bloque OPCIONAL si falla el import de MusicReproductor por problemas de VLC en el entorno
print("⚠️ Error al importar MusicReproductor. Asegúrate de tener 'python-vlc' instalado y VLC en tu sistema.")
print("⚠️ Error al importar MusicReproductor. Usando simulador.")
class MusicReproductor:
def __init__(self, *args, **kwargs): pass
def ajustar_volumen(self, valor): print(f"Volumen (Simulado): {valor}")
def ajustar_volumen(self, valor): pass
def cargar_y_reproducir(self, url): print(f"Reproduciendo (Simulado): {url}")
def cargar_y_reproducir(self, url): print(f"🎶 SIMULANDO PLAY: {url}")
def reproducir(self): print("Reproducir (Simulado)")
def reproducir(self): pass
def pausar(self): print("Pausar (Simulado)")
def pausar(self, *args): pass
def detener(self): print("Detener (Simulado)")
def detener(self): pass
# 🟢 LÓGICA DE T4 (ALARMAS)
from logica.T2.alarm import AlarmManager
# --- IMPORTACIÓN UNIVERSAL DE CONSTANTES ---
from vista.config import *
@ -48,12 +53,11 @@ from vista.config import *
class PanelCentral(ttk.Frame):
"""Contiene el Notebook (subpestañas), el panel de Notas y el panel de Chat,
y gestiona directamente la lógica de control de T1, T2 y T3."""
y gestiona directamente la lógica de control de T1, T2, T3 y T4."""
INTERVALO_ACTUALIZACION_MS = INTERVALO_RECURSOS_MS
INTERVALO_CARRERA_MS = 200
# ✅ CORRECCIÓN DE RUTA
NOMBRE_FICHERO_RADIOS = "res/radios.json"
def __init__(self, parent, root, *args, **kwargs):
@ -62,6 +66,9 @@ class PanelCentral(ttk.Frame):
self.after_id = None
self.after_carrera_id = None
self.after_alarm_id = None
# T2
self.camellos = []
self.progreso_labels = {}
self.frame_carrera_controles = None
@ -69,37 +76,56 @@ class PanelCentral(ttk.Frame):
self.carrera_info_label = None
self.carrera_estado_label = None
# 1. INICIALIZACIÓN DE VARIABLES (T1)
# T1
self.net_monitor = iniciar_monitor_red()
self.figure = Figure(figsize=(5, 4), dpi=100)
self.canvas = None
# 2. INICIALIZACIÓN DE VARIABLES Y LÓGICA DE RADIO (T3)
# T3 (Radios)
self.emisoras_cargadas = self.cargar_emisoras()
self.radio_seleccionada = tk.StringVar(value="---")
self.volumen_var = tk.DoubleVar(value=50.0)
self.reproductor = MusicReproductor(initial_volume=self.volumen_var.get())
# 3. CONFIGURACIÓN DEL LAYOUT
# 🟢 T4 (Alarmas) - Inicialización de variables UI.
self.alarm_manager = None
self.alarm_list_frame = None
self.scrollable_frame = None
self.alarm_hours_entry = None
self.alarm_minutes_entry = None
self.alarm_seconds_entry = None
# 📄 Tareas (res/notes)
self.notes_text_editor = None # <--- AÑADIDO
# 2. CONFIGURACIÓN DEL LAYOUT
self.grid_columnconfigure(0, weight=3)
self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(0, weight=1)
self.crear_area_principal_y_notas()
self.crear_area_principal() # <--- FUNCIÓN SIMPLIFICADA
self.crear_panel_chat_y_alumnos()
# 4. INICIO DE CICLOS DE ACTUALIZACIÓN
# Inicializar el AlarmManager solo después de que show_alarm_popup esté definido.
self.inicializar_alarmas()
# 3. INICIO DE CICLOS DE ACTUALIZACIÓN
self.iniciar_actualizacion_automatica()
self.iniciar_actualizacion_carrera()
self.iniciar_actualizacion_alarmas()
def inicializar_alarmas(self):
"""Inicializa AlarmManager, pasándole el método de callback show_alarm_popup."""
self.alarm_manager = AlarmManager(self.root, self.show_alarm_popup)
# -------------------------------------------------------------
# 📻 LÓGICA Y VISTA DE T3 (REPRODUCTOR DE RADIOS)
# ... (El código de cargar_emisoras, crear_interfaz_radios, seleccionar_radio, controlar_reproduccion, cambiar_volumen no tiene cambios)
# -------------------------------------------------------------
def cargar_emisoras(self):
"""Carga la lista de emisoras desde el archivo radios.json."""
try:
# ✅ La ruta ahora apunta correctamente al subdirectorio 'res'
with open(self.NOMBRE_FICHERO_RADIOS, 'r', encoding='utf-8') as f:
return json.load(f)
except FileNotFoundError:
@ -152,11 +178,9 @@ class PanelCentral(ttk.Frame):
emisora = self.emisoras_cargadas[indice]
url = emisora['url_stream']
# 1. Actualizar la interfaz
self.radio_seleccionada.set(emisora['nombre'])
self.url_seleccionada_label.config(text=url)
# 2. Llamar a la lógica del reproductor
self.reproductor.cargar_y_reproducir(url)
def controlar_reproduccion(self, accion):
@ -168,43 +192,308 @@ class PanelCentral(ttk.Frame):
def cambiar_volumen(self, valor):
"""
Llama al método de control de volumen del reproductor,
asegurando que el valor sea un entero para evitar saltos del Scale.
Ajusta el volumen, asegurando que el valor sea un entero para estabilizar el Scale.
"""
# ✅ CORRECCIÓN DE LA BARRA DE VOLUMEN
# 1. Convertir el valor de punto flotante a entero
valor_entero = int(float(valor))
# 2. Actualizar la variable de control con el valor entero.
self.volumen_var.set(valor_entero)
# 3. Llamar a la lógica del reproductor.
self.reproductor.ajustar_volumen(valor_entero)
# -------------------------------------------------------------
# 🔔 LÓGICA Y VISTA DE T4 (ALARMAS / TEMPORIZADORES)
# -------------------------------------------------------------
def crear_interfaz_alarmas(self, parent_frame):
"""Crea la interfaz para programar y visualizar alarmas (H:M:S)."""
frame = ttk.Frame(parent_frame, padding=10, style='TFrame')
frame.pack(expand=True, fill="both")
ttk.Label(frame, text="Programar Nuevo Temporizador (H:M:S)", font=FUENTE_NEGOCIOS).pack(pady=(0, 10))
# --- Controles de Nueva Alarma (H:M:S) ---
frame_input = ttk.Frame(frame, style='TFrame')
frame_input.pack(fill='x', pady=5)
# Horas
ttk.Label(frame_input, text="Horas:").pack(side='left', padx=(0, 2))
self.alarm_hours_entry = ttk.Entry(frame_input, width=3)
self.alarm_hours_entry.pack(side='left', padx=(0, 10))
self.alarm_hours_entry.insert(0, "0")
# Minutos
ttk.Label(frame_input, text="Minutos:").pack(side='left', padx=(0, 2))
self.alarm_minutes_entry = ttk.Entry(frame_input, width=3)
self.alarm_minutes_entry.pack(side='left', padx=(0, 10))
self.alarm_minutes_entry.insert(0, "1")
# Segundos
ttk.Label(frame_input, text="Segundos:").pack(side='left', padx=(0, 2))
self.alarm_seconds_entry = ttk.Entry(frame_input, width=3)
self.alarm_seconds_entry.pack(side='left', padx=(0, 15))
self.alarm_seconds_entry.insert(0, "0")
ttk.Button(frame_input, text=" Crear Alarma", command=self.manejar_nueva_alarma,
style='Action.TButton').pack(side='left')
ttk.Separator(frame, orient='horizontal').pack(fill='x', pady=15)
# --- Listado de Alarmas Activas ---
ttk.Label(frame, text="Alarmas Activas (Tiempo Restante)", font=FUENTE_NEGOCIOS).pack(pady=(0, 5))
self.alarm_list_frame = ttk.Frame(frame)
self.alarm_list_frame.pack(fill="both", expand=True)
canvas = tk.Canvas(self.alarm_list_frame, borderwidth=0, background=COLOR_BLANCO)
vscroll = ttk.Scrollbar(self.alarm_list_frame, orient="vertical", command=canvas.yview)
self.scrollable_frame = ttk.Frame(canvas)
self.scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(
scrollregion=canvas.bbox("all")
)
)
canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=vscroll.set)
canvas.pack(side="left", fill="both", expand=True)
vscroll.pack(side="right", fill="y")
def manejar_nueva_alarma(self):
"""Captura los datos del formulario (H:M:S), los convierte a segundos y llama al AlarmManager."""
try:
# 1. Leer los valores (usando 'or 0' para manejar campos vacíos como 0)
hours = int(self.alarm_hours_entry.get() or 0)
minutes = int(self.alarm_minutes_entry.get() or 0)
seconds = int(self.alarm_seconds_entry.get() or 0)
# 2. Calcular el total de segundos
total_seconds = (hours * 3600) + (minutes * 60) + seconds
if total_seconds <= 0:
print("⚠️ El tiempo de alarma debe ser un número positivo (H:M:S > 0).")
return
# 3. Llamar al AlarmManager con el total de segundos
self.alarm_manager.set_alarm(total_seconds)
# 4. Limpiar y preparar para la siguiente alarma (Default: 1 minuto)
self.alarm_hours_entry.delete(0, tk.END)
self.alarm_hours_entry.insert(0, "0")
self.alarm_minutes_entry.delete(0, tk.END)
self.alarm_minutes_entry.insert(0, "1")
self.alarm_seconds_entry.delete(0, tk.END)
self.alarm_seconds_entry.insert(0, "0")
self.actualizar_lista_alarmas()
except ValueError:
print("⚠️ Por favor, introduce números enteros válidos para el tiempo.")
except AttributeError:
print("⚠️ Error: AlarmManager no inicializado.")
def manejar_cancelar_alarma(self, alarm_id):
"""Cancela la alarma usando su ID."""
if self.alarm_manager.cancel_alarm(alarm_id):
self.actualizar_lista_alarmas()
def actualizar_lista_alarmas(self):
"""Actualiza la visualización de las alarmas activas con botones de cancelación individuales."""
if not self.scrollable_frame:
self.after_alarm_id = self.after(1000, self.actualizar_lista_alarmas)
return
for widget in self.scrollable_frame.winfo_children():
widget.destroy()
active_alarms = self.alarm_manager.get_active_alarms()
if not active_alarms:
ttk.Label(self.scrollable_frame, text="--- No hay alarmas activas ---", font=('Consolas', 10),
foreground=COLOR_TEXTO).pack(padx=10, pady=10)
for alarm in active_alarms:
self.add_alarm_row(self.scrollable_frame, alarm)
self.after_alarm_id = self.after(1000, self.actualizar_lista_alarmas)
def add_alarm_row(self, parent, alarm_data):
"""Añade una fila con la info de la alarma y su botón de cancelación."""
row_frame = ttk.Frame(parent, padding=5, style='Note.TFrame')
row_frame.pack(fill='x', padx=5, pady=2)
# Convertir total_seconds a formato Hh:Mm:Ss para la visualización del tiempo total
total_s = alarm_data['total_seconds']
h = total_s // 3600
m = (total_s % 3600) // 60
s = total_s % 60
total_time_str = f"{h:02d}h:{m:02d}m:{s:02d}s"
# Info de la alarma
info_text = (f"[ID{alarm_data['id']}] {alarm_data['restante']} -> {alarm_data['nombre']} "
f"({total_time_str} total)")
ttk.Label(row_frame, text=info_text, font=('Consolas', 10), style='Note.TLabel').pack(side='left', fill='x',
expand=True)
# Botón de Cancelación Individual
ttk.Button(row_frame, text="❌ Cancelar", style='Danger.TButton', width=10,
command=lambda id=alarm_data['id']: self.manejar_cancelar_alarma(id)).pack(side='right')
def iniciar_actualizacion_alarmas(self):
"""Inicia el ciclo de actualización de la lista de alarmas."""
if self.alarm_manager:
self.after_alarm_id = self.after(0, self.actualizar_lista_alarmas)
def detener_actualizacion_alarmas(self):
"""Detiene el ciclo de actualización de la lista de alarmas."""
if hasattr(self, 'after_alarm_id') and self.after_alarm_id:
self.after_cancel(self.after_alarm_id)
self.after_alarm_id = None
print("Ciclo de actualización de alarmas detenido.")
# -------------------------------------------------------------
# 🔔 POPUP DE ALARMA (Notificación)
# -------------------------------------------------------------
def show_alarm_popup(self, alarm_name, alarm_id):
"""Muestra una ventana Toplevel sin barra de título ni botón de cierre."""
# 1. Crear la ventana popup
popup = tk.Toplevel(self.root)
popup.title("🚨 ¡ALARMA!")
popup.geometry("350x150")
popup.resizable(False, False)
# ✅ Eliminar la barra de título y los botones (incluido el de cierre 'X')
popup.overrideredirect(True)
# Hacer que el popup sea modal (siempre encima)
popup.transient(self.root)
popup.grab_set()
# 2. Centrar la ventana
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}')
# 3. Función para cerrar y detener la música
def close_and_stop(event=None):
"""Función para cerrar el popup y detener el sonido."""
self.alarm_manager.stop_alarm_sound()
self.alarm_manager.cancel_alarm(alarm_id)
popup.destroy()
self.actualizar_lista_alarmas()
# 4. Contenido
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)
# 5. Configurar el cierre
# La forma de cerrar es mediante un clic en la ventana.
popup.bind("<Button-1>", close_and_stop)
frame.bind("<Button-1>", close_and_stop)
# 6. Esperar a que se cierre para continuar la ejecución del hilo principal de Tkinter
self.root.wait_window(popup)
# -------------------------------------------------------------
# 📄 LÓGICA Y VISTA DE TAREAS (Editor de Notas)
# -------------------------------------------------------------
def crear_interfaz_tareas(self, parent_frame):
"""Crea el editor de texto simple para el archivo res/notes dentro de la pestaña Tareas."""
frame = ttk.Frame(parent_frame, padding=15, style='TFrame')
frame.pack(expand=True, fill="both")
ttk.Label(frame, text="Editor de Notas ", font=FUENTE_TITULO).pack(pady=(0, 10), anchor="w")
ttk.Label(frame, text="Use este panel para tomar notas rápidas sobre la ejecución de tareas.",
font=FUENTE_NEGOCIOS).pack(pady=(0, 15), anchor="w")
# 1. Widget de texto
self.notes_text_editor = tk.Text(
frame,
height=20,
wrap="word",
bg=COLOR_BLANCO,
relief="solid",
borderwidth=1,
font=FUENTE_MONO
)
self.notes_text_editor.pack(fill="both", expand=True, pady=(0, 10))
# 2. Botones de Cargar y Guardar
frame_botones = ttk.Frame(frame)
frame_botones.pack(fill="x", pady=(5, 0))
ttk.Button(frame_botones, text="Guardar Cambios", command=self.guardar_res_notes, style='Action.TButton').pack(
side=tk.RIGHT)
ttk.Button(frame_botones, text="Cargar Archivo", command=self.cargar_res_notes, style='Action.TButton').pack(
side=tk.LEFT)
self.cargar_res_notes(initial_load=True) # Carga inicial al crear la interfaz
def cargar_res_notes(self, initial_load=False):
"""Carga el contenido de res/notes al editor de texto."""
if not self.notes_text_editor: return
contenido = cargar_contenido_res_notes()
self.notes_text_editor.delete("1.0", tk.END)
if "Error al cargar:" in contenido:
self.notes_text_editor.insert(tk.END, contenido)
else:
if initial_load and not contenido.strip():
self.notes_text_editor.insert(tk.END, "# Escriba aquí sus notas (res/notes)")
else:
self.notes_text_editor.insert(tk.END, contenido)
if initial_load:
print("Cargado 'res/notes' en la pestaña Tareas.")
def guardar_res_notes(self):
"""Guarda el contenido del editor de texto en res/notes."""
if not self.notes_text_editor: return
contenido = self.notes_text_editor.get("1.0", tk.END)
success, message = guardar_contenido_res_notes(contenido)
if success:
messagebox.showinfo("✅ Guardado", "Notas guardadas exitosamente.")
print(message)
else:
messagebox.showerror("❌ Error al Guardar", message)
print(f"FALLO AL GUARDAR: {message}")
# -------------------------------------------------------------
# 📦 ESTRUCTURA PRINCIPAL DEL PANEL
# -------------------------------------------------------------
def crear_area_principal_y_notas(self):
"""Crea el contenedor de las subpestañas y el panel de notas."""
def crear_area_principal(self):
"""Crea el contenedor de las subpestañas."""
frame_izquierdo = ttk.Frame(self, style='TFrame')
frame_izquierdo.grid(row=0, column=0, sticky="nsew")
frame_izquierdo.grid_rowconfigure(0, weight=4)
frame_izquierdo.grid_rowconfigure(1, weight=1)
frame_izquierdo.grid_rowconfigure(0, weight=1)
frame_izquierdo.grid_columnconfigure(0, weight=1)
self.crear_notebook_pestañas(frame_izquierdo)
panel_notas = ttk.Frame(frame_izquierdo, style='Note.TFrame')
panel_notas.grid(row=1, column=0, sticky="nsew", pady=(5, 0))
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_notebook_pestañas(self, parent_frame):
"""Crea las pestañas internas para las tareas (T1, Carrera, Radios)."""
"""Crea las pestañas internas para las tareas (T1, Carrera, Radios, Tareas, Alarmas, etc.)."""
sub_notebook = ttk.Notebook(parent_frame)
sub_notebook.grid(row=0, column=0, sticky="nsew")
@ -230,8 +519,16 @@ class PanelCentral(ttk.Frame):
elif sub_tab_text == "Radios":
self.crear_interfaz_radios(frame)
# -------------------------------------------------------------
elif sub_tab_text == "Tareas":
self.crear_interfaz_tareas(frame) # <--- Llamada a la nueva interfaz
elif sub_tab_text == "Alarmas":
self.crear_interfaz_alarmas(frame)
# -------------------------------------------------------------
# 🐪 LÓGICA DE T2 (CARRERA DE CAMELLOS)
# ... (El código de carreraCamellos no cambia)
# -------------------------------------------------------------
def crear_interfaz_carrera(self, parent_frame):
@ -414,7 +711,7 @@ class PanelCentral(ttk.Frame):
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 (T1) y T3."""
"""Detiene el ciclo de actualización periódica y los hilos/tareas."""
if self.after_id:
self.after_cancel(self.after_id)
self.after_id = None
@ -426,8 +723,8 @@ class PanelCentral(ttk.Frame):
print("Hilo de TrafficMeter detenido.")
self.detener_actualizacion_carrera()
self.detener_actualizacion_alarmas()
# Detener el reproductor al cerrar la aplicación
if self.reproductor:
self.reproductor.detener()
@ -471,7 +768,7 @@ class PanelCentral(ttk.Frame):
ttk.Button(frame_alumno, text="", width=3, style='Action.TButton').grid(row=0, column=1, rowspan=2, padx=5,
sticky="ne")
# --- FILA 8: Reproductor Música ---
# --- FILA 8: Reproductor Música (T3) ---
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))
@ -498,6 +795,4 @@ class PanelCentral(ttk.Frame):
ttk.Label(musica_frame, textvariable=self.volumen_var, style='TLabel').grid(row=2, column=2, sticky="w")
musica_frame.grid_columnconfigure(1, weight=1)
panel_chat.grid_rowconfigure(8, weight=0)
musica_frame.grid_columnconfigure(1, weight=1)

View File

@ -6,7 +6,8 @@ from tkinter import messagebox
from logica.controlador import accion_placeholder
from logica.T1.backup import accion_backup_t1
from logica.T1.runVScode import abrir_vscode
from logica.T1.textEditor import cargar_contenido_res_notes, guardar_contenido_res_notes
# NO necesitamos importar cargar/guardar notas aquí, ya que la lógica se mueve al Panel Central
# from logica.T1.textEditor import cargar_contenido_res_notes, guardar_contenido_res_notes
from logica.T1.openBrowser import navegar_a_url
# --- IMPORTACIÓN DE CONSTANTES DESDE vista/config.py ---
@ -14,14 +15,14 @@ from vista.config import *
class PanelLateral(ttk.Frame):
"""Contiene el menú de botones, entradas para las tareas y el editor simple para res/notes."""
"""Contiene el menú de botones y entradas para las tareas."""
# Usamos la constante importada
ANCHO_CARACTERES_FIJO = ANCHO_CARACTERES_PANEL_LATERAL
def __init__(self, parent, central_panel=None, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
# La referencia al PanelCentral es esencial para iniciar la carrera
# La referencia al PanelCentral es esencial para iniciar la carrera y acceder a sus métodos
self.central_panel = central_panel
self.configurar_estilos_locales(parent)
@ -40,15 +41,11 @@ 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 (Carrera 🏁)", app2_comando), # <--- CONEXIÓN REALIZADA
("App2 (Carrera 🏁)", app2_comando),
("App3", lambda: accion_placeholder("App3"))
]
self.crear_seccion(self, titulo="Aplicaciones", acciones=acciones_aplicaciones)
@ -60,19 +57,21 @@ class PanelLateral(ttk.Frame):
self.crear_seccion(self, titulo="Procesos batch", acciones=acciones_batch)
# 5. Espacio expandible
# Ahora este marco se expandirá para ocupar todo el espacio restante.
tk.Frame(self, height=1).pack(expand=True, fill="both")
# 6. Panel de Notas
self.crear_editor_res_notes()
# 6. Panel de Notas - ELIMINADO: Se moverá a la pestaña Tareas del Panel Central.
# self.crear_editor_res_notes() # <--- LÍNEA ELIMINADA
# --- MÉTODOS DE LÓGICA / CONTROL ---
# --- 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
# Llamada a la función expuesta por PanelCentral
self.central_panel.manejar_inicio_carrera()
# Opcional: Cambiar automáticamente a la pestaña Resultados
@ -84,7 +83,6 @@ class PanelLateral(ttk.Frame):
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.
@ -93,67 +91,10 @@ class PanelLateral(ttk.Frame):
if navegar_a_url(url):
self.entrada_superior.delete(0, tk.END)
def crear_editor_res_notes(self):
"""Crea el editor de texto simple para el archivo res/notes."""
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
self.notes_text_editor = tk.Text(
frame_editor,
height=8,
width=self.ANCHO_CARACTERES_FIJO,
wrap="word",
bg=COLOR_BLANCO,
relief="solid",
borderwidth=1,
font=FUENTE_MONO
)
self.notes_text_editor.pack(fill="x", expand=False)
# 2. Botones de Cargar y Guardar
frame_botones = ttk.Frame(frame_editor)
frame_botones.pack(fill="x", pady=(5, 0))
ttk.Button(frame_botones, text="Guardar", command=self.guardar_res_notes, style='SmallAction.TButton').pack(
side=tk.RIGHT)
ttk.Button(frame_botones, text="Cargar", command=self.cargar_res_notes, style='SmallAction.TButton').pack(
side=tk.LEFT)
self.cargar_res_notes(initial_load=True)
def cargar_res_notes(self, initial_load=False):
"""Carga el contenido de res/notes al editor de texto lateral."""
contenido = cargar_contenido_res_notes()
self.notes_text_editor.delete("1.0", tk.END)
if "Error al cargar:" in contenido:
self.notes_text_editor.insert(tk.END, contenido)
else:
if initial_load and not contenido.strip():
self.notes_text_editor.insert(tk.END, "# Escriba aquí sus notas (res/notes)")
else:
self.notes_text_editor.insert(tk.END, contenido)
print("Cargado 'res/notes' en el editor lateral.")
def guardar_res_notes(self):
"""Guarda el contenido del editor de texto lateral en res/notes."""
contenido = self.notes_text_editor.get("1.0", tk.END)
success, message = guardar_contenido_res_notes(contenido)
if success:
messagebox.showinfo("✅ Guardado", "Notas guardadas exitosamente.")
print(message)
else:
messagebox.showerror("❌ Error al Guardar", message)
print(f"FALLO AL GUARDAR: {message}")
# --- MÉTODOS DE NOTAS ELIMINADOS (Se moverán a PanelCentral) ---
# def crear_editor_res_notes(self): ...
# def cargar_res_notes(self, initial_load=False): ...
# def guardar_res_notes(self): ...
def manejar_extraccion_datos(self):
"""