From f9cfafe3fe684083a7b60508beeb3fccbbd60e00 Mon Sep 17 00:00:00 2001 From: BYolivia Date: Wed, 3 Dec 2025 18:39:17 +0100 Subject: [PATCH] add simple text editor for res/notes with load and save functionality --- Readme.md | 6 +- logica/T1/textEditor.py | 48 ++++++++++++++++ res/notes | 2 + vista/panel_lateral.py | 111 +++++++++++++++++++++++++++++++++---- vista/ventana_principal.py | 19 ++++--- 5 files changed, 166 insertions(+), 20 deletions(-) create mode 100644 res/notes diff --git a/Readme.md b/Readme.md index 0b87d07..8005606 100644 --- a/Readme.md +++ b/Readme.md @@ -16,14 +16,16 @@ python -m ProyectoGlobal 1. Lanzar aplicaciones externas con parámetros (por ejemplo navegadores externos con url) -2. Copias de seguridad realizadas con scripts powershell (.ps1) +2. ~~Copias de seguridad realizadas con scripts powershell (.ps1)~~ -3. Ver los recursos del sistema (memoria, procesador, hilos, etc.) utilizando gráficas (matplotlib) gráficos de barras, de áreas, líneas, etc. +3. ~~Ver los recursos del sistema (memoria, procesador, hilos, etc.) utilizando gráficas (matplotlib) gráficos de barras, de áreas, líneas, etc.~~ 4. Editor de texto (estilo notepad). 5. Hilo que cuente en kilobytes el tráfico de entrada y de salida de nuestra conexión de red. psutil.net_io_counters() +6. ~~Abrir VScode desde el programa~~ + ### T2. Multihilos ### 1. Hora del sistema / Fecha del sistema diff --git a/logica/T1/textEditor.py b/logica/T1/textEditor.py index e69de29..018c63c 100644 --- a/logica/T1/textEditor.py +++ b/logica/T1/textEditor.py @@ -0,0 +1,48 @@ +# Módulo: logica/T1/textEditor.py + +import os + +# Definimos la ruta del archivo de notas (asumiendo que res/ está un nivel arriba) +# Esto calcula la ruta absoluta: .../ProyectoGlobal/res/notes +BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # logica/T1 -> logica -> ProyectoGlobal +ARCHIVO_NOTAS_RES = os.path.join(BASE_DIR, "res", "notes") + + +def cargar_contenido_res_notes(): + """ + Carga el contenido del archivo 'res/notes'. Si no existe, lo crea y devuelve una cadena vacía. + """ + try: + if not os.path.exists(os.path.dirname(ARCHIVO_NOTAS_RES)): + os.makedirs(os.path.dirname(ARCHIVO_NOTAS_RES), exist_ok=True) + + if not os.path.exists(ARCHIVO_NOTAS_RES): + with open(ARCHIVO_NOTAS_RES, 'w', encoding='utf-8') as f: + f.write("") # Crear vacío si no existe + return "" + + with open(ARCHIVO_NOTAS_RES, 'r', encoding='utf-8') as f: + return f.read() + + except Exception as e: + print(f"Error al cargar el archivo {ARCHIVO_NOTAS_RES}: {e}") + return f"Error al cargar: {e}" + + +def guardar_contenido_res_notes(contenido: str): + """ + Guarda el contenido proporcionado en el archivo 'res/notes'. + """ + try: + # Aseguramos que la carpeta exista antes de intentar escribir + os.makedirs(os.path.dirname(ARCHIVO_NOTAS_RES), exist_ok=True) + + with open(ARCHIVO_NOTAS_RES, 'w', encoding='utf-8') as f: + f.write(contenido) + + return True, f"Contenido guardado exitosamente en {ARCHIVO_NOTAS_RES}" + + except Exception as e: + error_msg = f"Error al guardar en {ARCHIVO_NOTAS_RES}: {e}" + print(error_msg) + return False, error_msg \ No newline at end of file diff --git a/res/notes b/res/notes new file mode 100644 index 0000000..daedf58 --- /dev/null +++ b/res/notes @@ -0,0 +1,2 @@ +'oijp; +hfgdhfgdh \ No newline at end of file diff --git a/vista/panel_lateral.py b/vista/panel_lateral.py index a08286f..630f4eb 100644 --- a/vista/panel_lateral.py +++ b/vista/panel_lateral.py @@ -6,23 +6,28 @@ 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 +from logica.T1.textEditor import cargar_contenido_res_notes, guardar_contenido_res_notes +import os class PanelLateral(ttk.Frame): - """Contiene el menú de botones y entradas para las tareas de T1.""" + """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 def __init__(self, parent, central_panel=None, *args, **kwargs): super().__init__(parent, *args, **kwargs) - self.central_panel = central_panel # Guardamos la referencia - - # ❌ ELIMINAR ESTA LÍNEA: self.pack(fill="y", padx=5, pady=5) + self.central_panel = central_panel 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. Entrada superior (amarilla) - Aplicamos el ancho fijo + ttk.Entry(self, width=self.ANCHO_CARACTERES_FIJO, style='Yellow.TEntry').pack(fill="x", pady=10, padx=5, + ipady=3) - # 1. Área de Extracción/Navegación + # 2. Área de Extracción/Navegación acciones_extraccion = [ ("Extraer datos", self.manejar_extraccion_datos), ("Navegar", lambda: accion_placeholder("Navegar")), @@ -30,7 +35,7 @@ class PanelLateral(ttk.Frame): ] self.crear_seccion(self, titulo="", acciones=acciones_extraccion) - # 2. Área de Aplicaciones + # 3. Área de Aplicaciones acciones_aplicaciones = [ ("Visual Code", abrir_vscode), ("App2", lambda: accion_placeholder("App2")), @@ -38,14 +43,87 @@ class PanelLateral(ttk.Frame): ] self.crear_seccion(self, titulo="Aplicaciones", acciones=acciones_aplicaciones) - # 3. Área de Procesos Batch + # 4. Área de Procesos Batch acciones_batch = [ ("Copias de seguridad", self.manejar_backup) ] self.crear_seccion(self, titulo="Procesos batch", acciones=acciones_batch) + # 5. Espacio expandible (Empuja los elementos superiores hacia arriba) tk.Frame(self, height=1).pack(expand=True, fill="both") + # 6. Panel de Notas (Editor res/notes, ubicado abajo) + self.crear_editor_res_notes() + + # --- 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) + + 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 + self.notes_text_editor = tk.Text( + frame_editor, + height=8, + width=self.ANCHO_CARACTERES_FIJO, + wrap="word", + bg='white', + relief="solid", + borderwidth=1, + font=('Consolas', 9) # Fuente tipo terminal + ) + 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)) + + # 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 + + 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 + + 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.""" + # 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) + + 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 EXISTENTES --- def manejar_extraccion_datos(self): """ Llama a la lógica de actualización del gráfico de recursos @@ -63,19 +141,30 @@ class PanelLateral(ttk.Frame): success, message = accion_backup_t1() if success: - messagebox.showinfo("Backup Completado", message) + messagebox.showinfo("✅ Backup Completado", message) else: - messagebox.showerror("Error en el Backup", message) + 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) + + # Estilos existentes 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')]) + style.configure('Action.TButton', background='#0078d4', foreground='white', font=('Arial', 10, 'bold'), + relief='flat', padding=[10, 5]) + style.map('Action.TButton', background=[('active', '#005a9e'), ('pressed', '#003c6e')]) + + # 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')]) + def crear_seccion(self, parent_frame, titulo, acciones): """Función helper para crear secciones de etiquetas y botones.""" if titulo: diff --git a/vista/ventana_principal.py b/vista/ventana_principal.py index f4dcc84..3821161 100644 --- a/vista/ventana_principal.py +++ b/vista/ventana_principal.py @@ -9,6 +9,9 @@ from vista.panel_central import PanelCentral 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 + def __init__(self): super().__init__() self.title("Proyecto Integrado - PSP (Estilo Moderno Nativo)") @@ -23,6 +26,7 @@ class VentanaPrincipal(tk.Tk): # 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) @@ -31,7 +35,6 @@ class VentanaPrincipal(tk.Tk): 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.""" @@ -61,16 +64,19 @@ class VentanaPrincipal(tk.Tk): 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.""" + """Ensambla el panel lateral y el panel central en la rejilla, asegurando el ancho del lateral.""" - # Panel Central (debe crearse primero) + # 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 pasa la referencia del Central) - self.panel_lateral = PanelLateral(self, central_panel=self.panel_central) + # 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) 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) + # --- FUNCIÓN DE CIERRE --- def on_closing(self): """ @@ -78,14 +84,13 @@ class VentanaPrincipal(tk.Tk): y cierra la ventana principal de forma limpia. """ if self.panel_central: - # Llamar al método de limpieza del 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.") - # ... (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)