From 0ea61c511288ea6060e71356e6af918d5078b806 Mon Sep 17 00:00:00 2001 From: BYolivia Date: Thu, 4 Dec 2025 01:46:10 +0100 Subject: [PATCH] { "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" } } --- logica/T2/getLocalTime.py | 24 +++++++ logica/T2/getWeather.py | 18 +++++ vista/config.py | 33 +++++++++ vista/panel_central.py | 85 ++++++++++------------ vista/panel_lateral.py | 75 ++++++++++---------- vista/ventana_principal.py | 142 ++++++++++++++++++++++++++++--------- 6 files changed, 260 insertions(+), 117 deletions(-) create mode 100644 logica/T2/getWeather.py create mode 100644 vista/config.py diff --git a/logica/T2/getLocalTime.py b/logica/T2/getLocalTime.py index e69de29..39f41b9 100644 --- a/logica/T2/getLocalTime.py +++ b/logica/T2/getLocalTime.py @@ -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 \ No newline at end of file diff --git a/logica/T2/getWeather.py b/logica/T2/getWeather.py new file mode 100644 index 0000000..ef661aa --- /dev/null +++ b/logica/T2/getWeather.py @@ -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})" \ No newline at end of file diff --git a/vista/config.py b/vista/config.py new file mode 100644 index 0000000..58c0088 --- /dev/null +++ b/vista/config.py @@ -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 \ No newline at end of file diff --git a/vista/panel_central.py b/vista/panel_central.py index 85185cd..94ed333 100644 --- a/vista/panel_central.py +++ b/vista/panel_central.py @@ -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) \ No newline at end of file + style='TLabel').pack(fill="x", padx=5, pady=5) \ No newline at end of file diff --git a/vista/panel_lateral.py b/vista/panel_lateral.py index 477cf5c..6edfcbc 100644 --- a/vista/panel_lateral.py +++ b/vista/panel_lateral.py @@ -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('', self.manejar_navegacion) # Opcional: Ejecutar con Enter + self.entrada_superior.bind('', 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) diff --git a/vista/ventana_principal.py b/vista/ventana_principal.py index 3821161..c4acc3f 100644 --- a/vista/ventana_principal.py +++ b/vista/ventana_principal.py @@ -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") \ No newline at end of file + + self.label_reloj = ttk.Label(frame_fecha, text="Día y Hora: --/--/--", style='TLabel') + self.label_reloj.pack(side="left") \ No newline at end of file