"error": {
        "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.",
        "type": "insufficient_quota",
        "param": null,
        "code": "insufficient_quota"
    }
}
This commit is contained in:
BYolivia 2025-12-04 01:46:10 +01:00
parent 3916075743
commit 0ea61c5112
6 changed files with 260 additions and 117 deletions

View File

@ -0,0 +1,24 @@
# Módulo: logica/T2/getLocalTime.py
from datetime import datetime
def obtener_hora_local():
"""
Devuelve la fecha y hora actual del sistema en un formato legible.
Ej: "Jueves, 04 de Diciembre de 2025 | 12:07:05 AM"
"""
now = datetime.now()
# Formato de ejemplo:
# %A: Día de la semana completo (ej: Jueves)
# %d: Día del mes (ej: 04)
# %B: Mes completo (ej: Diciembre)
# %Y: Año (ej: 2025)
# %H: Hora (24h) o %I: Hora (12h)
# %p: AM/PM
# Usaremos una simple y clara: DD/MM/YYYY HH:MM:SS
formato_completo = now.strftime("%d/%m/%Y %H:%M:%S")
return formato_completo

18
logica/T2/getWeather.py Normal file
View File

@ -0,0 +1,18 @@
# Módulo: logica/T2/getWeather.py
import random
def obtener_datos_clima():
"""
Simula la obtención de la temperatura y el estado del tiempo local.
Devuelve la temperatura en grados Celsius y un estado del tiempo.
"""
# Simulación de datos
temperatura = random.randint(10, 28)
# Simulación de estado del tiempo
estados = ["Soleado ☀️", "Nublado ☁️", "Lluvia 🌧️", "Tormenta ⛈️"]
estado_tiempo = random.choice(estados)
return f"{temperatura}°C ({estado_tiempo})"

33
vista/config.py Normal file
View File

@ -0,0 +1,33 @@
# Módulo: vista/config.py
# --- CONSTANTES GENERALES ---
# Dimensiones y Tiempos
ANCHO_PANEL_LATERAL = 300
INTERVALO_RELOJ_MS = 1000
INTERVALO_CLIMA_MS = 60000
INTERVALO_RECURSOS_MS = 1000
ANCHO_CARACTERES_PANEL_LATERAL = 35
# --- CONSTANTES DE ESTILO (Colores) ---
COLOR_FONDO = "#f9f9f9" # Fondo principal
COLOR_ACCION = "#0078d4" # Azul para botones de acción/texto principal
COLOR_ACCION_HOVER = "#005a9e" # Azul oscuro para estado activo/hover
COLOR_ACCION_PRESSED = "#003c6e"# Azul muy oscuro para estado presionado
COLOR_EXITO = "#4CAF50" # Verde para mensajes de éxito/notas
COLOR_ADVERTENCIA = "#ffc107" # Amarillo para advertencias/chat
COLOR_TEXTO = "#333333" # Texto oscuro principal
COLOR_BLANCO = "white" # Fondo de áreas de contenido, texto en botones
# --- CONSTANTES DE FUENTE ---
# Familia de fuente principal
FUENTE_FAMILIA = 'Arial'
# Tamaños y Estilos
FUENTE_NORMAL = (FUENTE_FAMILIA, 9)
FUENTE_NEGOCIOS = (FUENTE_FAMILIA, 10, 'bold') # Para botones y pestañas
FUENTE_TITULO = (FUENTE_FAMILIA, 18, 'bold') # Para títulos grandes (ej: Chat)
FUENTE_NOTA = (FUENTE_FAMILIA, 9, 'italic') # Para notas y texto de estado
FUENTE_MONO = ('Consolas', 10) # Fuente monoespaciada para código/logs

View File

@ -2,30 +2,29 @@
import tkinter as tk
from tkinter import ttk
from logica.T1.trafficMeter import iniciar_monitor_red
from logica.T1.graficos import crear_grafico_recursos, actualizar_historial_datos
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
# --- IMPORTACIÓN DE CONSTANTES DESDE vista/config.py ---
from vista.config import *
class PanelCentral(ttk.Frame):
"""Contiene el Notebook (subpestañas de T1), el panel de Notas y el panel de Chat."""
# Reducimos el intervalo de actualización a 1000ms (1 segundo) para gráficos fluidos
INTERVALO_ACTUALIZACION_MS = 1000
# Usamos la constante importada para el intervalo de actualización
INTERVALO_ACTUALIZACION_MS = INTERVALO_RECURSOS_MS
def __init__(self, parent, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.after_id = None # ID para controlar el timer de tk.after
self.after_id = None
self.parent_root = parent.winfo_toplevel()
# 1. INICIALIZACIÓN DE VARIABLES
self.net_monitor = iniciar_monitor_red() # <-- INICIAR EL HILO DE RED
# Objeto Figure de Matplotlib (debe crearse antes de crear_sub_pestañas_t1)
self.net_monitor = iniciar_monitor_red()
self.figure = Figure(figsize=(5, 4), dpi=100)
self.canvas = None # Se inicializará en crear_sub_pestañas_t1
self.canvas = None
# 2. CONFIGURACIÓN DEL LAYOUT
self.grid_columnconfigure(0, weight=3)
@ -35,7 +34,6 @@ class PanelCentral(ttk.Frame):
self.crear_area_principal_y_notas()
self.crear_panel_chat_y_alumnos()
# La actualización se inicia al final de __init__
self.iniciar_actualizacion_automatica()
def crear_area_principal_y_notas(self):
@ -47,15 +45,15 @@ class PanelCentral(ttk.Frame):
frame_izquierdo.grid_rowconfigure(1, weight=1)
frame_izquierdo.grid_columnconfigure(0, weight=1)
# 1. El Notebook de T1 (Sub-pestañas)
self.crear_sub_pestañas_t1(frame_izquierdo)
self.crear_sub_pestañas_t1(frame_izquierdo) # Llamada al método que crea las pestañas
# 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=('Arial', 9, 'italic')).pack(
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):
@ -81,21 +79,19 @@ class PanelCentral(ttk.Frame):
self.canvas_widget = self.canvas.get_tk_widget()
self.canvas_widget.pack(expand=True, fill="both")
# La actualización automática iniciada en __init__ se encarga de esto
sub_notebook.select(i)
# Contenido de otras pestañas
elif sub_tab_text == "Navegador":
contenido_area = tk.Text(frame, wrap="word", padx=15, pady=15, bg='white', relief="groove",
borderwidth=1, font=('Consolas', 10), foreground="#555555")
# 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.")
">>> Á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)
def actualizar_grafico_recursos(self):
"""
Obtiene los datos del sistema (incluyendo Red) y dibuja/redibuja el gráfico.
"""
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()
@ -104,32 +100,28 @@ class PanelCentral(ttk.Frame):
actualizar_historial_datos(net_in, net_out)
# 3. Redibujar el gráfico
crear_grafico_recursos(self.figure)
self.canvas.draw()
if self.canvas:
crear_grafico_recursos(self.figure)
self.canvas.draw()
except Exception as e:
error_msg = f"Error al generar el gráfico de recursos: {e}"
print(error_msg)
# Manejo de errores visual en la pestaña (limpiar y mostrar error)
if 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)
# Detener la actualización para evitar bucle de error
self.detener_actualizacion_automatica()
# 4. Programar la siguiente llamada (solo si no hay error crítico que detenga la actualización)
self.after_id = self.after(self.INTERVALO_ACTUALIZACION_MS, self.actualizar_grafico_recursos)
# 4. Programar la siguiente llamada
self.after_id = self.after(self.INTERVALO_ACTUALIZACION_MS, self.actualizar_recursos)
# --- Control del Ciclo de Vida ---
# --- 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.")
# Programar la primera llamada inmediatamente
self.after_id = self.after(0, self.actualizar_grafico_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."""
@ -138,10 +130,9 @@ class PanelCentral(ttk.Frame):
self.after_id = None
print("Ciclo de actualización de gráficos detenido.")
# Detener el monitor de red al cerrar la aplicación
if self.net_monitor:
self.net_monitor.stop()
self.net_monitor.join() # Esperar a que el hilo termine
self.net_monitor.join()
print("Hilo de TrafficMeter detenido.")
def crear_panel_chat_y_alumnos(self, ):
@ -153,19 +144,20 @@ class PanelCentral(ttk.Frame):
panel_chat.grid_rowconfigure(7, weight=0)
panel_chat.grid_columnconfigure(0, weight=1)
# 1. Título "Chat"
ttk.Label(panel_chat, text="Chat", foreground="#0078d4", font=("Arial", 18, "bold"), style='TLabel').grid(row=0,
column=0,
pady=(
0,
10),
sticky="w")
# 1. Título "Chat" - Usando COLOR_ACCION y FUENTE_TITULO
ttk.Label(panel_chat, text="Chat", foreground=COLOR_ACCION, font=FUENTE_TITULO, style='TLabel').grid(row=0,
column=0,
pady=(
0,
10),
sticky="w")
# 2. Área de Mensaje
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,
font=('Arial', 10))
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")
@ -176,18 +168,19 @@ class PanelCentral(ttk.Frame):
frame_alumno.grid(row=3 + i, column=0, sticky="ew", pady=5)
frame_alumno.grid_columnconfigure(0, weight=1)
ttk.Label(frame_alumno, text=f"Alumno {i}", font=("Arial", 11, "bold"), style='Alumno.TLabel').grid(row=0,
# 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")
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")
sticky="ne")
# 4. Reproductor de Música (Simulado)
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)
style='TLabel').pack(fill="x", padx=5, pady=5)

View File

@ -9,16 +9,15 @@ from logica.T1.runVScode import abrir_vscode
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 ---
from vista.config import *
class PanelLateral(ttk.Frame):
"""Contiene el menú de botones, entradas para las tareas y el editor simple para res/notes."""
# Definimos un ancho fijo en caracteres. Esto es crucial para que Tkinter
# no intente expandir el panel lateral más allá de lo deseado.
ANCHO_CARACTERES_FIJO = 35
ANCHO_CARACTERES_FIJO = 35
# 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)
@ -26,15 +25,15 @@ class PanelLateral(ttk.Frame):
self.configurar_estilos_locales(parent)
# 1. Entrada superior (amarilla) - ¡Guardamos la referencia!
# 1. Entrada superior (amarilla)
self.entrada_superior = ttk.Entry(self, width=self.ANCHO_CARACTERES_FIJO, style='Yellow.TEntry')
self.entrada_superior.pack(fill="x", pady=10, padx=5, ipady=3)
self.entrada_superior.bind('<Return>', self.manejar_navegacion) # Opcional: Ejecutar con Enter
self.entrada_superior.bind('<Return>', self.manejar_navegacion)
# 2. Área de Extracción/Navegación
acciones_extraccion = [
("Extraer datos", self.manejar_extraccion_datos),
# 2. Asignamos el nuevo método de manejo a este botón
# 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"))
]
@ -69,27 +68,29 @@ class PanelLateral(ttk.Frame):
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."""
ttk.Label(self, text="Editor Simple (res/notes)", font=('Arial', 11, 'bold')).pack(fill="x", pady=(10, 0),
padx=5)
# 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
# 1. Widget de texto - Aplicamos el ancho fijo, COLOR_BLANCO y FUENTE_MONO
self.notes_text_editor = tk.Text(
frame_editor,
height=8,
width=self.ANCHO_CARACTERES_FIJO,
wrap="word",
bg='white',
bg=COLOR_BLANCO,
relief="solid",
borderwidth=1,
font=('Consolas', 9) # Fuente tipo terminal
font=FUENTE_MONO # Fuente tipo terminal
)
self.notes_text_editor.pack(fill="x", expand=False)
@ -97,19 +98,18 @@ class PanelLateral(ttk.Frame):
frame_botones = ttk.Frame(frame_editor)
frame_botones.pack(fill="x", pady=(5, 0))
# Se usa 'SmallAction.TButton' para reducir el padding y asegurar que quepan
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) # Carga inicial
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) # Limpiar contenido actual
self.notes_text_editor.delete("1.0", tk.END)
if "Error al cargar:" in contenido:
self.notes_text_editor.insert(tk.END, contenido)
@ -123,9 +123,6 @@ class PanelLateral(ttk.Frame):
def guardar_res_notes(self):
"""Guarda el contenido del editor de texto lateral en res/notes."""
# CORRECCIÓN: Quitamos .strip() para que el guardado refleje fielmente el contenido
# del widget, incluyendo saltos de línea finales, lo que soluciona la confusión
# de que "se guarda, pero el archivo está vacío".
contenido = self.notes_text_editor.get("1.0", tk.END)
success, message = guardar_contenido_res_notes(contenido)
@ -140,12 +137,12 @@ class PanelLateral(ttk.Frame):
# --- 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).
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_grafico_recursos()
self.central_panel.actualizar_recursos()
else:
messagebox.showerror("Error", "El Panel Central no está inicializado.")
@ -160,29 +157,35 @@ class PanelLateral(ttk.Frame):
messagebox.showerror("❌ Error en el Backup", message)
def configurar_estilos_locales(self, parent):
"""Configura estilos para los widgets del panel lateral."""
"""Configura estilos para los widgets del panel lateral, usando constantes importadas."""
style = ttk.Style(parent)
# Estilos existentes
style.configure('Yellow.TEntry', fieldbackground='#fff8e1', foreground='#333333', padding=[5, 5],
# Estilos existentes (Usando constantes importadas)
style.configure('Yellow.TEntry', fieldbackground='#fff8e1', foreground=COLOR_TEXTO, padding=[5, 5],
relief='solid', borderwidth=1)
style.configure('Green.TButton', background='#4CAF50', foreground='white', font=('Arial', 10, 'bold'),
relief='flat', padding=[10, 5])
style.map('Green.TButton', background=[('active', '#388E3C'), ('pressed', '#1B5E20')])
style.configure('Action.TButton', background='#0078d4', foreground='white', font=('Arial', 10, 'bold'),
# 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('Action.TButton', background=[('active', '#005a9e'), ('pressed', '#003c6e')])
style.map('Green.TButton', background=[('active', '#388E3C'), ('pressed',
'#1B5E20')]) # Manteniendo los tonos verdes originales para hover/pressed
# NUEVO ESTILO: Botones pequeños para el editor de notas
style.configure('SmallAction.TButton', background='#0078d4', foreground='white', font=('Arial', 9, 'bold'),
relief='flat', padding=[5, 3]) # <-- Padding reducido
style.map('SmallAction.TButton', background=[('active', '#005a9e'), ('pressed', '#003c6e')])
# 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'),
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:
ttk.Label(parent_frame, text=titulo, font=('Arial', 11, 'bold')).pack(fill="x", pady=(10, 0), padx=5)
# 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')
frame_botones.pack(fill="x", pady=5, padx=5)

View File

@ -4,29 +4,35 @@ import tkinter as tk
from tkinter import ttk
from vista.panel_lateral import PanelLateral
from vista.panel_central import PanelCentral
from logica.T2.getLocalTime import obtener_hora_local
from logica.T2.getWeather import obtener_datos_clima
# --- IMPORTACIÓN DE CONSTANTES DESDE vista/config.py ---
from vista.config import *
class VentanaPrincipal(tk.Tk):
"""Clase principal de la aplicación que monta la interfaz con estilos nativos mejorados."""
# Definimos el ancho deseado para el panel lateral
ANCHO_PANEL_LATERAL = 300
"""Ventana principal de la aplicación con panel lateral y central, reloj y clima."""
def __init__(self):
super().__init__()
self.title("Proyecto Integrado - PSP (Estilo Moderno Nativo)")
self.geometry("1200x800")
self.label_reloj = None
self.reloj_after_id = None
self.label_clima = None
self.clima_after_id = None
style = ttk.Style()
style.theme_use('clam')
self.config(bg="#f9f9f9")
# Usando la constante importada
self.config(bg=COLOR_FONDO)
self.configurar_estilos(style)
# Configuración del manejador de protocolo para el botón de cierre (X)
self.protocol("WM_DELETE_WINDOW", self.on_closing)
# Columna 0 (Lateral) no se expande (weight=0), Columna 1 (Central) sí se expande (weight=1)
self.grid_rowconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=0)
self.grid_columnconfigure(0, weight=0)
@ -35,59 +41,120 @@ class VentanaPrincipal(tk.Tk):
self.crear_paneles_principales()
self.crear_barra_inferior()
def configurar_estilos(self, s: ttk.Style):
"""Define estilos visuales personalizados sin dependencias externas."""
self.iniciar_actualizacion_reloj()
self.iniciar_actualizacion_clima()
COLOR_FONDO = "#f9f9f9"
COLOR_ACCION = "#0078d4"
COLOR_EXITO = "#4CAF50"
COLOR_ADVERTENCIA = "#ffc107"
COLOR_TEXTO = "#333333"
def configurar_estilos(self, s: ttk.Style):
"""Define estilos visuales personalizados sin dependencias externas usando constantes de config."""
# Los colores se importan, ya no se definen aquí
s.configure('TFrame', background=COLOR_FONDO)
s.configure('TLabel', background=COLOR_FONDO, foreground=COLOR_TEXTO, font=('Arial', 9))
s.configure('TLabel', background=COLOR_FONDO, foreground=COLOR_TEXTO, font=FUENTE_NORMAL)
s.configure('Action.TButton', background=COLOR_ACCION, foreground='white', font=('Arial', 10, 'bold'),
relief='flat', padding=[10, 5])
s.map('Action.TButton', background=[('active', '#005a9e'), ('pressed', '#003c6e')])
s.configure('Action.TButton',
background=COLOR_ACCION,
foreground=COLOR_BLANCO,
font=FUENTE_NEGOCIOS, # Usando FUENTE_NEGOCIOS
relief='flat',
padding=[10, 5])
# Mapeo de colores de estado de botón (usando las constantes de hover/pressed)
s.map('Action.TButton',
background=[('active', COLOR_ACCION_HOVER),
('pressed', COLOR_ACCION_PRESSED)])
s.configure('Note.TFrame', background=COLOR_EXITO, borderwidth=0, relief="solid")
s.configure('Note.TLabel', background=COLOR_EXITO, foreground='white', font=('Arial', 9, 'italic'))
s.configure('Note.TLabel', background=COLOR_EXITO, foreground=COLOR_BLANCO, font=FUENTE_NOTA)
s.configure('Chat.TFrame', background=COLOR_ADVERTENCIA)
s.configure('Chat.TLabel', background=COLOR_ADVERTENCIA)
s.configure('Alumno.TFrame', background='white', borderwidth=1, relief='solid')
s.configure('Alumno.TLabel', background='white', foreground=COLOR_TEXTO)
s.configure('Alumno.TFrame', background=COLOR_BLANCO, borderwidth=1, relief='solid')
s.configure('Alumno.TLabel', background=COLOR_BLANCO, foreground=COLOR_TEXTO, font=FUENTE_NORMAL)
s.configure('TNotebook.Tab', padding=[10, 5], font=('Arial', 10, 'bold'))
s.configure('TNotebook.Tab', padding=[10, 5], font=FUENTE_NEGOCIOS)
s.map('TNotebook.Tab', background=[('selected', COLOR_FONDO)], foreground=[('selected', COLOR_ACCION)])
def crear_paneles_principales(self):
"""Ensambla el panel lateral y el panel central en la rejilla, asegurando el ancho del lateral."""
# Panel Central (se expande en columna 1)
self.panel_central = PanelCentral(self)
self.panel_central.grid(row=0, column=1, sticky="nswe", padx=(5, 10), pady=10)
# Panel Lateral (se le aplica un ancho fijo para que no se expanda con el contenido)
self.panel_lateral = PanelLateral(self, central_panel=self.panel_central, width=self.ANCHO_PANEL_LATERAL)
# Usando la constante importada
self.panel_lateral = PanelLateral(self, central_panel=self.panel_central, width=ANCHO_PANEL_LATERAL)
self.panel_lateral.grid(row=0, column=0, sticky="nswe", padx=(10, 5), pady=10)
# Forzamos que el widget base respete el ancho definido, ignorando el tamaño del contenido (propagate=False)
self.panel_lateral.grid_propagate(False)
# --- LÓGICA DE ACTUALIZACIÓN DE RELOJ ---
def actualizar_reloj(self):
"""Obtiene la hora local y actualiza la etiqueta del reloj."""
try:
hora_actual = obtener_hora_local()
if self.label_reloj:
self.label_reloj.config(text=f"Día y Hora: {hora_actual}")
except Exception as e:
if self.label_reloj:
self.label_reloj.config(text="Error al obtener la hora")
print(f"Error en el reloj: {e}")
self.detener_actualizacion_reloj()
return
# Usando la constante importada
self.reloj_after_id = self.after(INTERVALO_RELOJ_MS, self.actualizar_reloj)
def iniciar_actualizacion_reloj(self):
"""Inicia el ciclo de actualización del reloj."""
print("Iniciando actualización automática del reloj.")
self.reloj_after_id = self.after(0, self.actualizar_reloj)
def detener_actualizacion_reloj(self):
"""Detiene el ciclo de actualización del reloj."""
if self.reloj_after_id:
self.after_cancel(self.reloj_after_id)
self.reloj_after_id = None
print("Ciclo de actualización del reloj detenido.")
# --- LÓGICA DE ACTUALIZACIÓN DE CLIMA ---
def actualizar_clima(self):
"""Obtiene la temperatura local y actualiza la etiqueta en la barra inferior."""
try:
datos_clima = obtener_datos_clima()
if self.label_clima:
self.label_clima.config(text=f"Temperatura: {datos_clima}")
except Exception as e:
if self.label_clima:
self.label_clima.config(text="Error al obtener el clima")
print(f"Error en la actualización del clima: {e}")
self.detener_actualizacion_clima()
return
# Usando la constante importada
self.clima_after_id = self.after(INTERVALO_CLIMA_MS, self.actualizar_clima)
def iniciar_actualizacion_clima(self):
"""Inicia el ciclo de actualización del clima."""
print("Iniciando actualización automática del clima.")
self.clima_after_id = self.after(0, self.actualizar_clima)
def detener_actualizacion_clima(self):
"""Detiene el ciclo de actualización del clima."""
if self.clima_after_id:
self.after_cancel(self.clima_after_id)
self.clima_after_id = None
print("Ciclo de actualización del clima detenido.")
# --- FUNCIÓN DE CIERRE ---
def on_closing(self):
"""
Se ejecuta cuando se presiona el botón 'X'. Detiene el ciclo de actualización
y cierra la ventana principal de forma limpia.
"""
"""Se ejecuta cuando se presiona el botón 'X'. Detiene todos los ciclos y cierra la ventana."""
self.detener_actualizacion_reloj()
self.detener_actualizacion_clima()
if self.panel_central:
# Llamar al método de limpieza del Panel Central (ciclo tk.after)
self.panel_central.detener_actualizacion_automatica()
# Destruir el objeto Tkinter y terminar mainloop
self.destroy()
print("Aplicación cerrada limpiamente.")
@ -104,11 +171,16 @@ class VentanaPrincipal(tk.Tk):
ttk.Label(frame_correo, text="Correos sin leer: 0", style='TLabel').pack(side="left")
ttk.Button(frame_correo, text="", width=3, style='Action.TButton').pack(side="left", padx=5)
# Marco de Temperatura
frame_temp = ttk.Frame(frame_inferior, style='TFrame')
frame_temp.grid(row=0, column=1)
ttk.Button(frame_temp, text="", width=3, style='Action.TButton').pack(side="left", padx=5)
ttk.Label(frame_temp, text="Temperatura local: --", style='TLabel').pack(side="left")
self.label_clima = ttk.Label(frame_temp, text="Temperatura: --", style='TLabel')
self.label_clima.pack(side="left")
# Marco de Fecha y Hora
frame_fecha = ttk.Frame(frame_inferior, style='TFrame')
frame_fecha.grid(row=0, column=2, sticky="e")
ttk.Label(frame_fecha, text="Fecha Día y Hora: --/--/--", style='TLabel').pack(side="left")
self.label_reloj = ttk.Label(frame_fecha, text="Día y Hora: --/--/--", style='TLabel')
self.label_reloj.pack(side="left")