From d6a39f33078cebefed64402b0d8e3326740500c0 Mon Sep 17 00:00:00 2001 From: BYolivia Date: Mon, 1 Dec 2025 20:57:53 +0100 Subject: [PATCH] add backup functionality with platform-specific scripts and resource monitoring --- Readme.md | 2 + logica/T1/backup.py | 125 +++++++++++++++++++++++++++++-- logica/T1/geterSystemRecource.py | 31 ++++++++ logica/T1/graficos.py | 55 ++++++++++++++ logica/T1/runVScode.py | 4 - logica/controlador.py | 46 +++++++----- res/scripts/script.ps1 | 54 ++++++++++++- res/scripts/script.sh | 50 +++++++++++++ res/scripts/sript.sh | 1 - vista/panel_central.py | 107 ++++++++++++++++++++------ vista/panel_lateral.py | 76 ++++++++++--------- vista/ventana_principal.py | 58 +++++++------- 12 files changed, 494 insertions(+), 115 deletions(-) create mode 100644 logica/T1/graficos.py mode change 100644 => 100755 res/scripts/script.ps1 create mode 100755 res/scripts/script.sh delete mode 100644 res/scripts/sript.sh diff --git a/Readme.md b/Readme.md index dabc8d4..0b87d07 100644 --- a/Readme.md +++ b/Readme.md @@ -1,6 +1,8 @@ ## Requisitos ## tkinter => hay que instalar python3-tk +matplotlib => pip install matplotlib +psutil => pip install psutil ## Como Ejecutar ## > [!NOTE] > Desde la carpeta anterior diff --git a/logica/T1/backup.py b/logica/T1/backup.py index 517f3e2..c00a7f9 100644 --- a/logica/T1/backup.py +++ b/logica/T1/backup.py @@ -1,5 +1,120 @@ -""" -hay que crear el script de backup - ver el sistema operativo - ejecutar el script segun SO -""" +# Módulo: logica/T1/backup.py + +import subprocess +import os # Necesario para la verificación y, si es necesario, cambio de permisos + +# Importamos las funciones necesarias del controlador +from logica.controlador import getPlataforma, get_scripts_dir + +_scripts = None + + +# --- FUNCIONES AUXILIARES DE DETECCIÓN --- + +def _systemToExtension(plataforma_set): + """Mapea el set de plataforma a la extensión de script necesaria.""" + if 'WINDOWS' in plataforma_set: + return 'ps1' + else: + # Incluye 'LINUX' y 'MACOS' + return 'sh' + + +def _get_scripts(): + """Carga la lista de nombres de scripts disponibles en la carpeta 'res/scripts'.""" + global _scripts + if _scripts is None: + script_dir_path = get_scripts_dir() + _scripts = [document.name for document in script_dir_path.iterdir() if document.is_file()] + return _scripts + + +def _get_valid_script_path(): + """ + Busca el script de backup adecuado y devuelve su ruta absoluta. + """ + plataforma = getPlataforma() + extension = _systemToExtension(plataforma) + scripts = _get_scripts() + script_name = None + + for script in scripts: + if script.endswith(extension): + script_name = script + break + + if script_name: + return get_scripts_dir() / script_name + + return None + + +# --- FUNCIÓN PRINCIPAL DE ACCIÓN Y EJECUCIÓN --- + +def accion_backup_t1(): + """ + Función de acción para el botón de backup. + 1. Obtiene la ruta del script a ejecutar. + 2. Ejecuta el script con subprocess. + + :return: Una tupla (bool, str) que indica éxito/fracaso y el mensaje. + """ + + script_path = _get_valid_script_path() + + # 1. VERIFICACIÓN DE EXISTENCIA DEL SCRIPT + if script_path is None or not script_path.exists(): + return False, f"ERROR: No se encontró ningún script válido (.ps1 o .sh) en la carpeta de recursos." + + plataforma = getPlataforma() + system = None + + if 'WINDOWS' in plataforma: + system = "Windows" + elif 'LINUX' in plataforma: + # En Linux, aseguramos el permiso de ejecución antes de intentar ejecutar. + try: + os.chmod(script_path, 0o755) # 0o755: rwxr-xr-x + system = "Linux" + except Exception as e: + return False, f"ERROR de Permiso (Linux): No se pudo establecer el permiso de ejecución para {script_path.name}. Intente ejecutar 'chmod +x {script_path}' manualmente. Detalle: {e}" + elif 'MACOS' in plataforma: + system = "macOS" + else: + return False, f"Sistema operativo '{plataforma}' no soportado para la ejecución." + + print(f"Backup T1: Ejecutando script: {script_path.name} en {system}") + + # 2. DEFINICIÓN DEL COMANDO + command = [] + if system == "Windows": + command = [ + "powershell.exe", + "-ExecutionPolicy", "Bypass", + "-File", str(script_path) + ] + elif system == "Linux" or system == "macOS": + command = [str(script_path)] + + # 3. EJECUCIÓN DEL COMANDO + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + check=True, + encoding='utf-8', + ) + + # Copia exitosa + return True, result.stdout + + except subprocess.CalledProcessError as e: + error_output = e.stderr if e.stderr else e.stdout + return False, f"El script falló (Código {e.returncode}). Mensaje del script:\n{error_output}" + + except FileNotFoundError: + interpeter = 'powershell.exe' if system == 'Windows' else 'el intérprete de shell' + return False, f"ERROR: El intérprete de comandos ({interpeter}) no se encontró en el PATH del sistema." + except Exception as e: + return False, f"Error inesperado al ejecutar el script: {e}" \ No newline at end of file diff --git a/logica/T1/geterSystemRecource.py b/logica/T1/geterSystemRecource.py index e69de29..c3c40f1 100644 --- a/logica/T1/geterSystemRecource.py +++ b/logica/T1/geterSystemRecource.py @@ -0,0 +1,31 @@ +# Módulo: logica/T1/geterSystemRecource.py + +import psutil + + +def obtener_datos_cpu_ram(): + """ + Función que utiliza psutil para recopilar información de CPU, RAM y procesos. + :return: Diccionario con métricas. + """ + + cpu_percent_total = psutil.cpu_percent(interval=None) + cpu_percent_per_core = psutil.cpu_percent(interval=None, percpu=True) + + mem = psutil.virtual_memory() + + num_procesos = len(psutil.pids()) + + cpu_freq = psutil.cpu_freq() + + datos = { + 'cpu_total': cpu_percent_total, + 'cpu_cores': cpu_percent_per_core, + 'ram_total_gb': round(mem.total / (1024 ** 3), 2), + 'ram_uso_gb': round(mem.used / (1024 ** 3), 2), + 'ram_percent': mem.percent, + 'num_hilos': num_procesos, + 'cpu_freq_mhz': cpu_freq.current if cpu_freq else 0 + } + + return datos \ No newline at end of file diff --git a/logica/T1/graficos.py b/logica/T1/graficos.py new file mode 100644 index 0000000..7780a55 --- /dev/null +++ b/logica/T1/graficos.py @@ -0,0 +1,55 @@ +# Módulo: logica/T1/graficos.py + +import matplotlib.pyplot as plt +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +import tkinter as tk + +COLOR_PRINCIPAL = '#0078d4' +COLOR_RAM = '#4CAF50' + + +def crear_grafico_recursos(parent_frame: tk.Frame, datos: dict): + """ + Genera un gráfico de matplotlib que muestra el uso de CPU y RAM, + e integra este gráfico en un Frame de Tkinter. + """ + + # Limpiamos el frame padre para redibujar + for widget in parent_frame.winfo_children(): + widget.destroy() + + # 1. Crear la figura (2 subplots) + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4)) + fig.patch.set_facecolor('white') + + # --- GRÁFICO 1: USO DE CPU (Gráfico de Barras) --- + core_labels = [f'Núcleo {i + 1}' for i in range(len(datos['cpu_cores']))] + ax1.bar(core_labels, datos['cpu_cores'], color=COLOR_PRINCIPAL) + ax1.axhline(datos['cpu_total'], color='red', linestyle='--', linewidth=1, label=f'Total: {datos["cpu_total"]}%') + + ax1.set_title(f'Uso de CPU por Núcleo (Total: {datos["cpu_total"]}%)', fontsize=10) + ax1.set_ylabel('Uso (%)') + ax1.set_ylim(0, 100) + ax1.tick_params(axis='x', rotation=45) + ax1.legend(loc='upper right') + + # --- GRÁFICO 2: USO DE RAM (Gráfico Circular/Pie) --- + labels = ['Usada', 'Libre'] + sizes = [datos['ram_percent'], 100 - datos['ram_percent']] + colors = [COLOR_RAM, '#d3d3d3'] + explode = (0.1, 0) + + ax2.pie(sizes, explode=explode, labels=labels, colors=colors, + autopct='%1.1f%%', shadow=False, startangle=90) + ax2.axis('equal') + + ram_title = f'RAM Total: {datos["ram_total_gb"]} GB\nUso: {datos["ram_uso_gb"]} GB' + ax2.set_title(ram_title, fontsize=10) + + # 3. Integración en Tkinter + canvas = FigureCanvasTkAgg(fig, master=parent_frame) + canvas_widget = canvas.get_tk_widget() + fig.tight_layout(pad=3.0) + canvas_widget.pack(fill=tk.BOTH, expand=True) + + return canvas_widget \ No newline at end of file diff --git a/logica/T1/runVScode.py b/logica/T1/runVScode.py index ef420e2..c47472c 100644 --- a/logica/T1/runVScode.py +++ b/logica/T1/runVScode.py @@ -5,10 +5,6 @@ import platform def abrir_vscode(): """ Intenta abrir Visual Studio Code utilizando el comando 'code'. - - Este comando asume que VS Code ha sido instalado correctamente y que - el ejecutable 'code' está disponible en el PATH del sistema (lo cual - suele ser el caso si se habilita la opción al instalar VS Code). """ comando = 'code' diff --git a/logica/controlador.py b/logica/controlador.py index 785650f..484ae6e 100644 --- a/logica/controlador.py +++ b/logica/controlador.py @@ -1,36 +1,44 @@ import platform - +from pathlib import Path _os = None +_root_dir = Path(__file__).parent.parent +_scripts_dir = _root_dir / "res" / "scripts" + +# --- Funciones de Configuración y Sistema --- + +def get_scripts_dir(): + """Devuelve la ruta absoluta al directorio de scripts.""" + return _scripts_dir + def getPlataforma(): + """Detecta y devuelve el conjunto que identifica el SO ('WINDOWS', 'LINUX', 'MACOS').""" global _os - if _os==None: + if _os is None: _os = _obtener_datos_sistema() return _os - -def accion_placeholder(nombre_accion): - """ - Función placeholder temporal para acciones que aún no tienen implementación. - Simplemente imprime un mensaje en la consola. - """ - print(f"Acción pendiente de implementación: {nombre_accion}") - def _obtener_datos_sistema(): - """ - Función placeholder para la tarea T1.3 (recursos del sistema). - Esta función se llenará con la lógica para obtener datos de CPU/RAM. - """ - print("Iniciando la recopilación de datos del sistema...") - # Lógica a añadir aquí en el futuro (usando psutil, por ejemplo) + """Lógica para detectar el sistema operativo.""" tmpVar = platform.system().lower() - if tmpVar.__contains__("windows"): + + if "windows" in tmpVar: print("Sistema operativo detectado: Windows") return {'WINDOWS'} - elif tmpVar.__contains__("darwin"): + elif "darwin" in tmpVar: print("Sistema operativo detectado: MacOS") return {'MACOS'} else: print("Sistema operativo detectado: Linux/Unix") - return {'LINUX'} \ No newline at end of file + return {'LINUX'} + +# --- Función Placeholder --- + +def accion_placeholder(nombre_accion): + """ + Función placeholder temporal para acciones que aún no tienen implementación. + """ + print(f"Acción pendiente de implementación: {nombre_accion}") + +# Nota: La lógica de 'subprocess.run' se encuentra ahora en logica/T1/backup.py. \ No newline at end of file diff --git a/res/scripts/script.ps1 b/res/scripts/script.ps1 old mode 100644 new mode 100755 index 3562d68..7fa9f15 --- a/res/scripts/script.ps1 +++ b/res/scripts/script.ps1 @@ -1,5 +1,55 @@ out-null cls -$saludo='Hola Usuario' -Write-Host $saludo \ No newline at end of file +# SCRIPT: backup_script.ps1 + +# --- CONFIGURACIÓN DE RUTAS --- +# La carpeta de ORIGEN es la carpeta 'Pictures' (Imágenes) del usuario +$SourceFolder = Join-Path $env:USERPROFILE "Pictures" + +# La carpeta base de DESTINO es 'BACKUPS' dentro del directorio raíz del usuario +$DestinationBasePath = Join-Path $env:USERPROFILE "BACKUPS" +# ------------------------------ + +# 1. Verificar que la carpeta de origen exista +if (-not (Test-Path $SourceFolder)) { + Write-Error "ERROR: La carpeta de origen '$SourceFolder' (Imágenes) no existe." + exit 1 # Código de error +} + +# 2. Generar el nombre de la carpeta con la fecha y hora (Formato: backup-AAAA-MM-DD_HH-MM-SS) +$Timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss" +$BackupFolderName = "backup-$Timestamp" + +# 3. Crear la ruta final de destino +$FinalDestinationPath = Join-Path $DestinationBasePath $BackupFolderName + +# 4. Crear la carpeta base 'BACKUPS' si no existe y luego la carpeta de la copia +try { + # New-Item -ItemType Directory -Path $FinalDestinationPath -Force: + # Crea la estructura completa ($DestinationBasePath/backup-...) + Write-Host "Creando carpeta de destino: $FinalDestinationPath" + $null = New-Item -ItemType Directory -Path $FinalDestinationPath -Force +} catch { + Write-Error "ERROR: No se pudo crear la carpeta de destino: $($_.Exception.Message)" + exit 1 +} + +# 5. Ejecutar la copia recursiva (Copiando el *contenido* de la carpeta de Imágenes) +Write-Host "Iniciando copia de seguridad de '$SourceFolder' a '$FinalDestinationPath'..." +try { + # Copia todo el contenido de $SourceFolder (el asterisco es importante) + Copy-Item -Path "$SourceFolder\*" -Destination $FinalDestinationPath -Recurse -Force + + # 6. Mensaje de finalización + Write-Host "" + Write-Host "✅ Copia de seguridad completada con éxito." + Write-Host " Origen: $SourceFolder" + Write-Host " Destino: $FinalDestinationPath" + +} catch { + Write-Error "ERROR: La copia de seguridad falló durante la copia de archivos: $($_.Exception.Message)" + exit 1 +} + +exit 0 # Código de éxito \ No newline at end of file diff --git a/res/scripts/script.sh b/res/scripts/script.sh new file mode 100755 index 0000000..bdb8f0d --- /dev/null +++ b/res/scripts/script.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# SCRIPT: res/scripts/sript.sh + +# --- CONFIGURACIÓN DE RUTAS --- +USER_HOME=~ +# CAMBIO CLAVE: Usamos 'Imágenes' en lugar de 'Pictures' +SOURCE_FOLDER="$USER_HOME/Imágenes" +DESTINATION_BASE_PATH="$USER_HOME/BACKUPS" +# ------------------------------ + +# 1. Verificar que la carpeta de o2rigen exista +if [ ! -d "$SOURCE_FOLDER" ]; then + echo "ERROR: La carpeta de origen '$SOURCE_FOLDER' no existe." >&2 + exit 1 +fi + +# 2. Generar el nombre de la carpeta con la fecha y hora (Formato: backup-AAAA-MM-DD_HH-MM-SS) +TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S) +BACKUP_FOLDER_NAME="backup-$TIMESTAMP" + +# 3. Crear la ruta final de destino +FINAL_DESTINATION_PATH="$DESTINATION_BASE_PATH/$BACKUP_FOLDER_NAME" + +# 4. Crear la carpeta base 'BACKUPS' si no existe y luego la carpeta de la copia +mkdir -p "$FINAL_DESTINATION_PATH" + +if [ $? -ne 0 ]; then + echo "ERROR: No se pudo crear la carpeta de destino: $FINAL_DESTINATION_PATH" >&2 + exit 1 +fi + +# 5. Ejecutar la copia recursiva con rsync +echo "Iniciando copia de seguridad de '$SOURCE_FOLDER' a '$FINAL_DESTINATION_PATH'..." +# -a: modo archivo (preserva permisos, dueño, timestamps, etc.) +# -v: modo verbose (muestra progreso/qué está copiando) +# -z: comprime los datos durante la transferencia (útil si copias a red, pero no hace daño aquí) +# --exclude: ignora la carpeta de destino si por alguna razón ya existe dentro del origen (seguridad) +rsync -avz --exclude 'BACKUPS' "$SOURCE_FOLDER/" "$FINAL_DESTINATION_PATH/" + +# $?: guarda el código de salida del último comando ejecutado (rsync) +if [ $? -eq 0 ]; then + echo "" + echo "✅ Copia de seguridad completada con éxito." + echo " Origen: $SOURCE_FOLDER" + echo " Destino: $FINAL_DESTINATION_PATH" + exit 0 +else + echo "ERROR: La copia de seguridad falló durante rsync." >&2 + exit 1 +fi \ No newline at end of file diff --git a/res/scripts/sript.sh b/res/scripts/sript.sh deleted file mode 100644 index 8214f7b..0000000 --- a/res/scripts/sript.sh +++ /dev/null @@ -1 +0,0 @@ -echo "Hello, World!" \ No newline at end of file diff --git a/vista/panel_central.py b/vista/panel_central.py index 117b370..a8717b4 100644 --- a/vista/panel_central.py +++ b/vista/panel_central.py @@ -1,13 +1,22 @@ +# Módulo: vista/panel_central.py + import tkinter as tk from tkinter import ttk +from logica.T1.geterSystemRecource import obtener_datos_cpu_ram +from logica.T1.graficos import crear_grafico_recursos +from logica.controlador import accion_placeholder class PanelCentral(ttk.Frame): """Contiene el Notebook (subpestañas de T1), el panel de Notas y el panel de Chat.""" + # Definimos el intervalo de actualización en milisegundos (5000 ms = 5 segundos) + INTERVALO_ACTUALIZACION_MS = 5000 + def __init__(self, parent, *args, **kwargs): super().__init__(parent, *args, **kwargs) - # Columna 0: Área Principal (3/4) | Columna 1: Chat (1/4) + self.after_id = None # ID para controlar el timer de tk.after + self.grid_columnconfigure(0, weight=3) self.grid_columnconfigure(1, weight=1) self.grid_rowconfigure(0, weight=1) @@ -15,12 +24,13 @@ class PanelCentral(ttk.Frame): self.crear_area_principal_y_notas() self.crear_panel_chat_y_alumnos() + self.iniciar_actualizacion_automatica() + def crear_area_principal_y_notas(self): """Crea el contenedor de las subpestañas de T1 y el panel de notas (inferior izquierda).""" frame_izquierdo = ttk.Frame(self, style='TFrame') frame_izquierdo.grid(row=0, column=0, sticky="nsew") - # Fila 0: Subpestañas (4 partes) | Fila 1: Notas (1 parte) frame_izquierdo.grid_rowconfigure(0, weight=4) frame_izquierdo.grid_rowconfigure(1, weight=1) frame_izquierdo.grid_columnconfigure(0, weight=1) @@ -28,51 +38,98 @@ class PanelCentral(ttk.Frame): # 1. El Notebook de T1 (Sub-pestañas) self.crear_sub_pestañas_t1(frame_izquierdo) - # 2. El Panel de Notas (área verde clara inferior izquierda) + # 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)) - 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(expand=True, fill="both") + 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( + 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.""" sub_notebook = ttk.Notebook(parent_frame) - sub_notebook.grid(row=0, column=0, sticky="nsew") # Ocupa Fila 0, Columna 0 + sub_notebook.grid(row=0, column=0, sticky="nsew") + + sub_tabs = ["Recursos", "Resultados", "Navegador", "Correos", "Tareas", "Alarmas", "Enlaces"] + self.tabs = {} - sub_tabs = ["Resultados", "Navegador", "Correos", "Tareas", "Alarmas", "Enlaces"] for i, sub_tab_text in enumerate(sub_tabs): frame = ttk.Frame(sub_notebook, style='TFrame') 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": + self.grafico_frame = ttk.Frame(frame, style='TFrame') + self.grafico_frame.pack(expand=True, fill="both", padx=10, pady=10) + + # Llamada inicial, la auto-actualización tomará el control + self.actualizar_grafico_recursos() - # Contenido del área central (simulando el panel rayado) - if sub_tab_text == "Navegador": - # Usamos un widget Text con fondo blanco y un borde sutil para que destaque - contenido_area = tk.Text(frame, wrap="word", padx=15, pady=15,bg='white', relief="groove", borderwidth=1,font=('Consolas', 10), foreground="#555555") - 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) sub_notebook.select(i) - def crear_panel_chat_y_alumnos(self): + # 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") + 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) + + def actualizar_grafico_recursos(self): + """Obtiene los datos del sistema y dibuja/redibuja el gráfico.""" + try: + datos_sistema = obtener_datos_cpu_ram() + crear_grafico_recursos(self.grafico_frame, datos_sistema) + except Exception as e: + error_msg = f"Error al generar el gráfico de recursos: {e}" + print(error_msg) + for widget in self.grafico_frame.winfo_children(): + widget.destroy() + ttk.Label(self.grafico_frame, text=error_msg, foreground='red', style='TLabel').pack(pady=20) + + def iniciar_actualizacion_automatica(self): + """ + Programa la actualización periódica del gráfico de recursos usando tk.after, + y almacena el ID para poder cancelarlo. + """ + # 1. Ejecuta la actualización + self.actualizar_grafico_recursos() + + # 2. Programa la siguiente llamada y almacena el ID + self.after_id = self.after(self.INTERVALO_ACTUALIZACION_MS, self.iniciar_actualizacion_automatica) + + def detener_actualizacion_automatica(self): + """Detiene el ciclo de actualización periódica del gráfico.""" + if self.after_id: + self.after_cancel(self.after_id) + print("Ciclo de actualización de gráficos detenido.") + + def crear_panel_chat_y_alumnos(self, ): """Crea el panel de chat, lista de Alumnos y Reproductor de Música (columna derecha).""" panel_chat = ttk.Frame(self, style='TFrame', padding="10") panel_chat.grid(row=0, column=1, sticky="nsew") - # Configuración interna del panel de chat 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" - ttk.Label(panel_chat, text="Chat", foreground="#0078d4", font=("Arial", 18, "bold"), style='TLabel').grid(row=0,column=0,pady=(0,10),sticky="w") + ttk.Label(panel_chat, text="Chat", foreground="#0078d4", font=("Arial", 18, "bold"), 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") - # Área de texto para el mensaje (el recuadro amarillo) - chat_text = tk.Text(panel_chat, height=6, width=30, bg='#fff8e1', relief="solid", borderwidth=1,font=('Arial', 10)) + chat_text = tk.Text(panel_chat, height=6, width=30, bg='#fff8e1', relief="solid", borderwidth=1, + font=('Arial', 10)) chat_text.grid(row=2, column=0, sticky="ew", pady=(0, 5)) - # Botón Enviar 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) @@ -81,13 +138,17 @@ 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,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.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") - # Botón de Recargar/Actualizar - ttk.Button(frame_alumno, text="↻", width=3, style='Action.TButton').grid(row=0, column=1, rowspan=2, padx=5,sticky="ne") + 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) 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 + 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 diff --git a/vista/panel_lateral.py b/vista/panel_lateral.py index 17e9314..a08286f 100644 --- a/vista/panel_lateral.py +++ b/vista/panel_lateral.py @@ -1,27 +1,30 @@ +# Módulo: vista/panel_lateral.py + import tkinter as tk from tkinter import ttk -# Importación de la lógica específica para abrir VS Code +from tkinter import messagebox +from logica.controlador import accion_placeholder, getPlataforma +from logica.T1.backup import accion_backup_t1 from logica.T1.runVScode import abrir_vscode -# Importación de las acciones generales (placeholders y futuras funciones) -from logica.controlador import accion_placeholder, obtener_datos_sistema class PanelLateral(ttk.Frame): """Contiene el menú de botones y entradas para las tareas de T1.""" - def __init__(self, parent, *args, **kwargs): + def __init__(self, parent, central_panel=None, *args, **kwargs): super().__init__(parent, *args, **kwargs) - self.pack(fill="y", padx=5, pady=5) + self.central_panel = central_panel # Guardamos la referencia - self.configurar_estilos_locales(parent) # Asegura que los estilos funcionen + # ❌ ELIMINAR ESTA LÍNEA: self.pack(fill="y", padx=5, pady=5) + + self.configurar_estilos_locales(parent) # Entrada superior (amarilla) ttk.Entry(self, width=25, style='Yellow.TEntry').pack(fill="x", pady=10, padx=5, ipady=3) # 1. Área de Extracción/Navegación acciones_extraccion = [ - # T1.3 - Conectamos el botón de extracción a la función de obtención de datos del sistema - ("Extraer datos", lambda: obtener_datos_sistema()), + ("Extraer datos", self.manejar_extraccion_datos), ("Navegar", lambda: accion_placeholder("Navegar")), ("Buscar API Google", lambda: accion_placeholder("Buscar API Google")) ] @@ -29,7 +32,6 @@ class PanelLateral(ttk.Frame): # 2. Área de Aplicaciones acciones_aplicaciones = [ - # CONEXIÓN: Usa la función específica abrir_vscode ("Visual Code", abrir_vscode), ("App2", lambda: accion_placeholder("App2")), ("App3", lambda: accion_placeholder("App3")) @@ -38,45 +40,49 @@ class PanelLateral(ttk.Frame): # 3. Área de Procesos Batch acciones_batch = [ - ("Copias de seguridad", lambda: accion_placeholder("Copias de seguridad")) + ("Copias de seguridad", self.manejar_backup) ] self.crear_seccion(self, titulo="Procesos batch", acciones=acciones_batch) - # Espaciador para empujar los elementos inferiores si los hubiera - # CORRECCIÓN: Eliminamos el argumento 'bg' problemático y permitimos la herencia de color. tk.Frame(self, height=1).pack(expand=True, fill="both") - def configurar_estilos_locales(self, parent): - """Configura estilos que deberían estar en la ventana principal, o crea placeholders.""" - style = ttk.Style(parent) - # Estilo de la entrada amarilla - style.configure('Yellow.TEntry', - fieldbackground='#fff8e1', - foreground='#333333', - padding=[5, 5], - relief='solid', - borderwidth=1) + 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). + """ + if self.central_panel: + print("Activando actualización del gráfico de Recursos (Manual)...") + self.central_panel.actualizar_grafico_recursos() + else: + messagebox.showerror("Error", "El Panel Central no está inicializado.") - # Estilo de los botones (Verde) - style.configure('Green.TButton', - background='#4CAF50', - foreground='white', - font=('Arial', 10, 'bold'), - relief='flat', - padding=[10, 5]) + def manejar_backup(self): + """Llama a la lógica de backup de T1 e informa al usuario del resultado.""" + print("Iniciando proceso de Copia de Seguridad...") + success, message = accion_backup_t1() + + if success: + messagebox.showinfo("Backup Completado", message) + else: + messagebox.showerror("Error en el Backup", message) + + def configurar_estilos_locales(self, parent): + """Configura estilos para los widgets del panel lateral.""" + style = ttk.Style(parent) + style.configure('Yellow.TEntry', fieldbackground='#fff8e1', foreground='#333333', 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')]) def crear_seccion(self, parent_frame, titulo, acciones): - """ - Función helper para crear secciones de etiquetas y botones. - 'acciones' es una lista de tuplas: (texto_boton, comando_a_ejecutar) - """ + """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) - frame_botones = ttk.Frame(parent_frame, style='TFrame') # Usando TFrame para el contenedor de botones + frame_botones = ttk.Frame(parent_frame, style='TFrame') frame_botones.pack(fill="x", pady=5, padx=5) for texto_boton, comando in acciones: - # Conexión del botón: ttk.Button(frame_botones, text=texto_boton, command=comando, style='Green.TButton').pack(fill="x", pady=5) \ No newline at end of file diff --git a/vista/ventana_principal.py b/vista/ventana_principal.py index ba1302d..f4dcc84 100644 --- a/vista/ventana_principal.py +++ b/vista/ventana_principal.py @@ -1,3 +1,5 @@ +# Módulo: ventana_principal.py + import tkinter as tk from tkinter import ttk from vista.panel_lateral import PanelLateral @@ -12,69 +14,78 @@ class VentanaPrincipal(tk.Tk): self.title("Proyecto Integrado - PSP (Estilo Moderno Nativo)") self.geometry("1200x800") - # 1. Usar el tema 'clam' para un aspecto más plano y moderno style = ttk.Style() style.theme_use('clam') - self.config(bg="#f9f9f9") # Fondo global muy claro - + self.config(bg="#f9f9f9") self.configurar_estilos(style) - # Configuración de la rejilla principal de la ventana + # Configuración del manejador de protocolo para el botón de cierre (X) + self.protocol("WM_DELETE_WINDOW", self.on_closing) + self.grid_rowconfigure(0, weight=1) self.grid_rowconfigure(1, weight=0) - self.grid_columnconfigure(0, weight=0) # Panel lateral es de ancho fijo - self.grid_columnconfigure(1, weight=1) # Panel central se expande + self.grid_columnconfigure(0, weight=0) + self.grid_columnconfigure(1, weight=1) self.crear_paneles_principales() self.crear_barra_inferior() + # ... (código de configurar_estilos) ... def configurar_estilos(self, s: ttk.Style): """Define estilos visuales personalizados sin dependencias externas.""" - # Paleta de Colores COLOR_FONDO = "#f9f9f9" - COLOR_ACCION = "#0078d4" # Azul principal - COLOR_EXITO = "#4CAF50" # Verde para éxito/notas - COLOR_ADVERTENCIA = "#ffc107" # Amarillo para chat + COLOR_ACCION = "#0078d4" + COLOR_EXITO = "#4CAF50" + COLOR_ADVERTENCIA = "#ffc107" COLOR_TEXTO = "#333333" - # TFrame y TLabel base (fondo claro) s.configure('TFrame', background=COLOR_FONDO) s.configure('TLabel', background=COLOR_FONDO, foreground=COLOR_TEXTO, font=('Arial', 9)) - # Botones de Acción (Simulando el color azul/verde de tu diseño) 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')]) - # Estilo para el área de notas (simulando el recuadro verde claro) s.configure('Note.TFrame', background=COLOR_EXITO, borderwidth=0, relief="solid") s.configure('Note.TLabel', background=COLOR_EXITO, foreground='white', font=('Arial', 9, 'italic')) - # Estilo para el área de chat/entrada (simulando el recuadro amarillo) s.configure('Chat.TFrame', background=COLOR_ADVERTENCIA) s.configure('Chat.TLabel', background=COLOR_ADVERTENCIA) - # Estilo para los recuadros de Alumnos (fondo blanco con borde) s.configure('Alumno.TFrame', background='white', borderwidth=1, relief='solid') s.configure('Alumno.TLabel', background='white', foreground=COLOR_TEXTO) - # Configuración de las pestañas (Notebook interno - subpestañas) s.configure('TNotebook.Tab', padding=[10, 5], font=('Arial', 10, 'bold')) 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.""" - # Panel Lateral (columna 0) - self.panel_lateral = PanelLateral(self) - self.panel_lateral.grid(row=0, column=0, sticky="nswe", padx=(10, 5), pady=10) - - # Panel Central (columna 1) + # Panel Central (debe crearse primero) self.panel_central = PanelCentral(self) self.panel_central.grid(row=0, column=1, sticky="nswe", padx=(5, 10), pady=10) + # Panel Lateral (se le pasa la referencia del Central) + self.panel_lateral = PanelLateral(self, central_panel=self.panel_central) + self.panel_lateral.grid(row=0, column=0, sticky="nswe", padx=(10, 5), pady=10) + + # --- 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. + """ + if self.panel_central: + # Llamar al método de limpieza del Panel Central + self.panel_central.detener_actualizacion_automatica() + + # Destruir el objeto Tkinter y terminar mainloop + self.destroy() + print("Aplicación cerrada limpiamente.") + + # ... (código de crear_barra_inferior) ... def crear_barra_inferior(self): """Crea la barra de estado o información inferior.""" frame_inferior = ttk.Frame(self, relief="flat", padding=[10, 5, 10, 5], style='TFrame', borderwidth=0) @@ -83,21 +94,16 @@ class VentanaPrincipal(tk.Tk): frame_inferior.grid_columnconfigure(1, weight=1) frame_inferior.grid_columnconfigure(2, weight=1) - # Elementos de la barra inferior (más discretos) - - # Correos sin leer frame_correo = ttk.Frame(frame_inferior, style='TFrame') frame_correo.grid(row=0, column=0, sticky="w") 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) - # Temperatura local 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") - # 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