# ui_layout.py import tkinter as tk from tkinter import Menu, ttk, messagebox from tkinter.scrolledtext import ScrolledText import threading import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import datetime import os # Importar funciones y variables import system_utils import monitor_manager import config def crear_ui_completa(root): """Configura el layout principal, crea todos los widgets e inicia hilos.""" # Aplicar un tema más moderno para ttk style = ttk.Style() style.theme_use('clam') # --- FUNCIONES AUXILIARES DE UI (Para llamadas a eventos y botones) --- def abrir_editor_alarma(event_or_none, treeview_alarmas): """ Verifica la selección en el Treeview y abre la ventana flotante con los datos cargados. Puede ser llamada por un evento (doble clic) o por un botón. """ # Obtenemos el ítem seleccionado (focus() funciona para botón y doble clic si hay foco) selected_item = treeview_alarmas.focus() # Si no hay selección, salimos. if not selected_item: messagebox.showwarning("Advertencia", "Selecciona una alarma para modificar.") return # Obtenemos el ID de la alarma (primer valor de la fila) alarma_id = int(treeview_alarmas.item(selected_item, 'values')[0]) data = config.alarmas_programadas.get(alarma_id) if not data: messagebox.showerror("Error", "No se encontraron los datos de la alarma seleccionada.") return # Llamamos a la función flotante en modo modificación mostrar_selector_alarma_flotante(treeview_alarmas, alarma_id, data) def on_closing(): config.monitor_running = False system_utils.detener_sonido_alarma() system_utils.detener_mp3() # Detener música al cerrar root.destroy() root.protocol("WM_DELETE_WINDOW", on_closing) # Configuración de Layout root.columnconfigure(0, weight=0) root.columnconfigure(1, weight=1) root.columnconfigure(2, weight=0) root.rowconfigure(0, weight=1) root.rowconfigure(1, weight=0) # --- Creación de Frames Principales --- frame_izquierdo = tk.Frame(root, bg="#f0f0f0", width=200) frame_central = tk.Frame(root, bg="white") frame_derecho = tk.Frame(root, bg="#f0f0f0", width=10) frame_izquierdo.grid(row=0, column=0, sticky="nsew") frame_central.grid(row=0, column=1, sticky="nsew") frame_derecho.grid(row=0, column=2, sticky="nsew") frame_izquierdo.grid_propagate(False) frame_derecho.grid_propagate(False) # Layout del Frame Central frame_central.rowconfigure(0, weight=1) frame_central.rowconfigure(1, weight=0) frame_central.columnconfigure(0, weight=1) frame_superior = tk.Frame(frame_central, bg="lightyellow") frame_inferior = tk.Frame(frame_central, bg="lightgray", height=100) frame_superior.grid(row=0, column=0, sticky="nsew") frame_inferior.grid(row=1, column=0, sticky="ew") frame_inferior.grid_propagate(False) # --- Implementación del Progressbar (Frame Inferior) --- progress_bar = ttk.Progressbar(frame_inferior, orient="horizontal", length=800, mode="determinate") progress_bar.pack(pady=10, padx=20, fill="x") config.progress_bar = progress_bar # ----------------------------------------------- # Notebook para las pestañas notebook = ttk.Notebook(frame_superior) notebook.pack(fill="both", expand=True) # --- PESTAÑA 1: PROGRAMADOR DE ALARMAS --- alarma_tab = ttk.Frame(notebook) notebook.add(alarma_tab, text="Programador de Alarmas") # --- PESTAÑA 2: BLOC DE NOTAS --- editor_tab = ttk.Frame(notebook) notebook.add(editor_tab, text="Bloc de Notas") # --- PESTAÑA 3: MONITOR DEL SISTEMA (DEFINICIÓN ÚNICA) --- monitor_tab = ttk.Frame(notebook) notebook.add(monitor_tab, text="Monitor del Sistema", padding=4) # --- PESTAÑA 4: WEB SCRAPING --- scraping_tab = ttk.Frame(notebook) notebook.add(scraping_tab, text="Web Scraping") # --- PESTAÑA 5: JUEGOS --- games_tab = ttk.Frame(notebook) notebook.add(games_tab, text="Juegos 🎲") # --- PESTAÑA 6: MÚSICA (NUEVO) --- music_tab = ttk.Frame(notebook) notebook.add(music_tab, text="Música 🎵") # --------------------------------------------- # =============================================== # FUNCIÓN PARA MOSTRAR EL SELECTOR DE ALARMA FLOTANTE # =============================================== def mostrar_selector_alarma_flotante(treeview_alarmas, alarma_id=None, data=None): """ Crea y muestra la interfaz de selección de hora flotante. Si se proporciona alarma_id y data, funciona en modo MODIFICACIÓN. """ is_modifying = alarma_id is not None popup = tk.Toplevel(root) popup.title("Modificar Alarma" if is_modifying else "Añadir Alarma") popup.geometry("450x300") # TAMAÑO AJUSTADO PARA VISIBILIDAD popup.resizable(False, False) popup.transient(root) # CORRECCIÓN CLAVE: Retrasar grab_set para evitar 'grab failed: window not viewable' popup.after(10, popup.grab_set) # Variables de entrada (Carga de datos si es modo modificación) initial_hora = data['time'].strftime("%H") if is_modifying else datetime.datetime.now().strftime("%H") initial_minuto = data['time'].strftime("%M") if is_modifying else datetime.datetime.now().strftime("%M") initial_tarea = data['message'] if is_modifying else "" initial_sound_file = data['sound_file'] if is_modifying else config.ALERTA_SOUND_FILE # Inicializar con valores cargados o por defecto hora_var = tk.StringVar(value=initial_hora) minuto_var = tk.StringVar(value=initial_minuto) tarea_var = tk.StringVar(value=initial_tarea) # --- Configuración del Label de Sonido --- config.ALERTA_SOUND_FILE = initial_sound_file # Seteamos la global para la función seleccionar_archivo_alarma initial_sound_text = os.path.basename(config.ALERTA_SOUND_FILE) if config.ALERTA_SOUND_FILE else "[No seleccionado]" main_frame = ttk.Frame(popup, padding="15") main_frame.pack(fill='both', expand=True) # Grid para el layout main_frame.columnconfigure(1, weight=1) main_frame.columnconfigure(2, weight=1) # --- SECCIÓN HORA Y MINUTO --- tk.Label(main_frame, text="Hora (HH):").grid(row=0, column=0, pady=5, sticky='w') tk.Label(main_frame, text="Minuto (MM):").grid(row=0, column=2, pady=5, sticky='w') horas = [f"{h:02d}" for h in range(24)] minutos = [f"{m:02d}" for m in range(60)] hora_cb = ttk.Combobox(main_frame, values=horas, textvariable=hora_var, width=5, state="readonly") minuto_cb = ttk.Combobox(main_frame, values=minutos, textvariable=minuto_var, width=5, state="readonly") hora_cb.grid(row=1, column=0, padx=(5, 10), sticky='ew') minuto_cb.grid(row=1, column=2, padx=(10, 5), sticky='ew') # --- SECCIÓN SONIDO --- tk.Label(main_frame, text="Sonido:", font=('Helvetica', 9, 'bold')).grid(row=2, column=0, pady=(10, 5), sticky='w') label_archivo_seleccionado = tk.Label(main_frame, text=initial_sound_text, anchor='w') label_archivo_seleccionado.grid(row=4, column=0, columnspan=3, padx=5, pady=(0, 0), sticky='ew') # Botón para abrir el diálogo de selección de archivo ttk.Button( main_frame, text="Seleccionar Audio (.wav/.mp3)", command=lambda: system_utils.seleccionar_archivo_alarma(root, label_archivo_seleccionado) ).grid(row=3, column=0, columnspan=3, padx=5, pady=(5, 5), sticky='ew') # --- SECCIÓN MENSAJE --- tk.Label(main_frame, text="Mensaje/Tarea:").grid(row=5, column=0, columnspan=3, pady=(10, 5), sticky='w') tarea_entry = ttk.Entry(main_frame, textvariable=tarea_var, width=35) tarea_entry.grid(row=6, column=0, columnspan=3, pady=5, sticky='ew') # Frame para botones de control (OK/CANCEL) button_frame = ttk.Frame(main_frame) button_frame.grid(row=7, column=0, columnspan=3, pady=(15, 0), sticky='e') # --- Lógica de Comando --- if is_modifying: action_command = lambda: system_utils.modificar_alarma_existente( root, alarma_id, hora_var.get(), minuto_var.get(), tarea_var.get(), treeview_alarmas, popup, config.ALERTA_SOUND_FILE ) action_text = "💾 Guardar Cambios" else: action_command = lambda: system_utils.agregar_alarma( root, hora_var.get(), minuto_var.get(), tarea_var.get(), treeview_alarmas, popup, config.ALERTA_SOUND_FILE ) action_text = "➕ Añadir" # Botón OK (Programar/Modificar) ttk.Button( button_frame, text=action_text, width=18, command=action_command ).pack(side=tk.LEFT, padx=5) # Botón Cancelar ttk.Button( button_frame, text="Cancelar", width=10, command=popup.destroy ).pack(side=tk.LEFT) # =============================================== # CONTENIDO DE LA SOLAPA DE ALARMA # =============================================== main_alarm_frame = tk.Frame(alarma_tab) main_alarm_frame.pack(fill="both", expand=True) # --- Lista de Alarmas (Treeview) --- columns = ('ID', 'Hora', 'Tarea', 'Estado', 'Fecha') treeview_alarmas = ttk.Treeview(main_alarm_frame, columns=columns, show='headings') for col in columns: treeview_alarmas.heading(col, text=col, anchor=tk.W) treeview_alarmas.column('ID', width=40) treeview_alarmas.column('Hora', width=40) treeview_alarmas.column('Estado', width=70, anchor=tk.CENTER) treeview_alarmas.column('Tarea', minwidth=150, stretch=tk.YES) treeview_alarmas.column('Fecha', width=80) treeview_alarmas.pack(fill='both', expand=True, padx=10, pady=10) # [NUEVO] Llamar a cargar alarmas DESPUÉS de crear el Treeview system_utils.cargar_alarmas(treeview_alarmas, root) # --- Evento de Doble Clic para Modificar (CORREGIDO) --- def handle_double_click(event): abrir_editor_alarma(event, treeview_alarmas) treeview_alarmas.bind('', handle_double_click) # --- Panel de Control de Alarmas (Parte inferior) --- control_frame = tk.LabelFrame(main_alarm_frame, text="Control", padx=15, pady=15) control_frame.pack(fill='x', padx=10, pady=(0, 10)) # Botones principales ttk.Button( control_frame, text="➕ Programar Nueva Alarma", command=lambda: mostrar_selector_alarma_flotante(treeview_alarmas) # Llama a la función que abre el popup ).pack(side=tk.LEFT, padx=10) # Botón Modificar (CORREGIDO: usa la función auxiliar) ttk.Button( control_frame, text="✏️ Modificar Seleccionada", command=lambda: abrir_editor_alarma(None, treeview_alarmas) ).pack(side=tk.LEFT, padx=10) ttk.Button( control_frame, text="✅ Activar/Desactivar Seleccionada", command=lambda: system_utils.toggle_alarma(treeview_alarmas) ).pack(side=tk.LEFT, padx=10) ttk.Button( control_frame, text="🗑️ Eliminar Seleccionada", command=lambda: system_utils.eliminar_alarma(treeview_alarmas) ).pack(side=tk.LEFT, padx=10) # NUEVO: Separador ttk.Separator(control_frame, orient='vertical').pack(side=tk.LEFT, padx=10, fill='y') # NUEVO: Control de Sonido ttk.Button( control_frame, text="🔇 Detener Sonido Alarma", command=system_utils.detener_sonido_alarma ).pack(side=tk.LEFT, padx=10) tk.Label(control_frame, text="Volumen:").pack(side=tk.LEFT) volumen_scale = ttk.Scale( control_frame, from_=0, to=100, orient='horizontal', length=100, command=system_utils.ajustar_volumen_alarma ) volumen_scale.set(config.alarma_volumen * 100) volumen_scale.pack(side=tk.LEFT, padx=5) # =============================================== # CONTENIDO DE LA SOLAPA BLOC DE NOTAS # =============================================== # Frame para los botones de control control_frame_editor = tk.Frame(editor_tab) control_frame_editor.pack(pady=5) # Botones del Editor ttk.Button(control_frame_editor, text="Nuevo", command=system_utils.nuevo_archivo).pack(side=tk.LEFT, padx=5) ttk.Button(control_frame_editor, text="Abrir TXT...", command=lambda: system_utils.abrir_archivo(root)).pack(side=tk.LEFT, padx=5) ttk.Button(control_frame_editor, text="Guardar TXT", command=lambda: system_utils.guardar_texto(root)).pack(side=tk.LEFT, padx=15) ttk.Button( control_frame_editor, text="📂 Abrir Carpeta Notas", # CAMBIO DE ETIQUETA command=system_utils.abrir_carpeta_notas # LLAMA A LA FUNCIÓN DE NOTAS ).pack(side=tk.LEFT, padx=5) # Widget de Texto editor_text_widget = tk.Text(editor_tab, wrap='word', undo=True, font=('Courier New', 10)) editor_text_widget.pack(fill="both", expand=True, padx=5, pady=5) # Asignar a la variable global config.editor_texto = editor_text_widget # --- Creación de la Barra de Estado --- barra_estado = tk.Label(root, text="Barra de estado", bg="lightgray", anchor="w") barra_estado.grid(row=1, column=0, columnspan=3, sticky="ew") label_1 = tk.Label(barra_estado, text="Estado Backup", bg="green", anchor="w", width=20) label_2 = tk.Label(barra_estado, text="Registro: Detenido", bg="gray", anchor="w", width=20) label_fecha_hora = tk.Label(barra_estado, text="Hilo fecha-hora", font=("Helvetica", 14), bd=1, fg="blue", relief="sunken", anchor="w", width=20, padx=10) label_1.pack(side="left", fill="x", expand=True) label_2.pack(side="left", fill="x", expand=True) label_fecha_hora.pack(side="right", fill="x", expand=True) # Asignar los labels a la configuración global para que los hilos los encuentren config.label_1 = label_1 config.label_2 = label_2 config.label_fecha_hora = label_fecha_hora # --- Inicialización del Panel Lateral --- monitor_manager.crear_panel_lateral(frame_izquierdo, root) # --- Creación del Menú Superior (SE MODIFICA) --- menu_bar = Menu(root) file_menu = Menu(menu_bar, tearoff=0); file_menu.add_command(label="Salir", command=on_closing) # MODIFICADO: Se elimina el comando de YouTube launch_menu = Menu(menu_bar, tearoff=0); launch_menu.add_command(label="ChatGPT", command=lambda: system_utils.lanzar_url("https://chat.openai.com")) launch_menu.add_command(label="Apuntes PSP", command=lambda: system_utils.lanzar_url("https://apuntes-informatica.ieslamar.org/psp/proyecto")) launch_menu.add_command(label="Solitario Google", command=lambda: system_utils.lanzar_url("https://www.google.com/logos/fnbx/solitaire/standalone.html")) launch_menu.add_command(label="Aules FP", command=lambda: system_utils.lanzar_url("https://aules.edu.gva.es/fp/my/")) # MODIFICADO: Se ELIMINA el comando del juego thread-safe de este menú tools_menu = Menu(menu_bar, tearoff=0); tools_menu.add_command(label="Ejecutar Copia de Seguridad", command=lambda: system_utils.ejecutar_script_en_hilo(label_1, root)) tools_menu.add_command(label="Iniciar/Detener Registro CSV", command=lambda: system_utils.manejar_registro_csv(label_2)) # tools_menu.add_command(label="Simular Juego (Thread-Safe) 🐫", command=lambda: system_utils.simular_juego_camellos(root)) # ELIMINADO tools_menu.add_command(label="📂 Abrir Carpeta Scraping", command=lambda: system_utils.abrir_carpeta_especifica(config.SCRAPING_FOLDER, "Scraping")) tools_menu.add_command(label="📂 Abrir Config Scraping", command=lambda: system_utils.abrir_carpeta_especifica(config.SCRAPING_CONFIG_FOLDER, "Config Scraping")) menu_bar.add_cascade(label="Archivo", menu=file_menu) menu_bar.add_cascade(label="Herramientas", menu=tools_menu) menu_bar.add_cascade(label="Lanzadores", menu=launch_menu) menu_bar.add_cascade(label="Ayuda", menu=Menu(menu_bar, tearoff=0)) root.config(menu=menu_bar) # =============================================== # Solapa de Monitor del Sistema (CONTENIDO ÚNICO) # =============================================== # Reutilizamos monitor_tab creado arriba para evitar la duplicidad main_monitor_frame = tk.Frame(monitor_tab) main_monitor_frame.pack(fill=tk.BOTH, expand=True) main_monitor_frame.rowconfigure(0, weight=3) main_monitor_frame.rowconfigure(1, weight=1) main_monitor_frame.columnconfigure(0, weight=1) # --- Fila 0: Gráficos --- plot_frame = tk.Frame(main_monitor_frame) plot_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) fig = plt.figure(figsize=(10, 8)) gs = fig.add_gridspec(2, 3, hspace=0.6, wspace=0.3) ax_cpu = fig.add_subplot(gs[0, 0]) ax_mem = fig.add_subplot(gs[0, 1]) ax_cores = fig.add_subplot(gs[0, 2]) ax_net = fig.add_subplot(gs[1, 0]) ax_pie = fig.add_subplot(gs[1, 1], aspect="equal") ax_disk_io = fig.add_subplot(gs[1, 2]) plt.style.use('ggplot') canvas = FigureCanvasTkAgg(fig, master=plot_frame) canvas_widget = canvas.get_tk_widget() canvas_widget.pack(side=tk.TOP, fill=tk.BOTH, expand=True) # --- Fila 1: Log y Procesos --- bottom_frame = tk.Frame(main_monitor_frame) bottom_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=5) bottom_frame.columnconfigure(0, weight=1) bottom_frame.columnconfigure(1, weight=1) # 1. Log de Eventos (Sección Izquierda) log_frame = tk.LabelFrame(bottom_frame, text="Log de Eventos del Sistema") log_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) log_frame.rowconfigure(0, weight=1) log_frame.columnconfigure(0, weight=1) config.system_log = ScrolledText(log_frame, height=8, font=("Courier", 8), bg="#2c3e50", fg="lightgray") config.system_log.grid(row=0, column=0, sticky="nsew") # 2. Treeview de Procesos (Sección Derecha) process_frame = tk.LabelFrame(bottom_frame, text=f"Top {10} Procesos (Ordenados por CPU)") process_frame.grid(row=0, column=1, sticky="nsew", padx=5, pady=5) process_frame.columnconfigure(0, weight=1) process_frame.rowconfigure(0, weight=1) columns = ('PID', 'CPU', 'MEM', 'HILOS', 'NOMBRE') treeview_processes = ttk.Treeview(process_frame, columns=columns, show='headings') for col in columns: treeview_processes.heading(col, text=col, anchor=tk.W) treeview_processes.column(col, width=50, anchor=tk.W) treeview_processes.column('PID', width=50) treeview_processes.column('CPU', width=60) treeview_processes.column('MEM', width=70) treeview_processes.column('HILOS', width=60) treeview_processes.column('NOMBRE', minwidth=150, stretch=tk.YES) treeview_processes.grid(row=0, column=0, sticky="nsew") kill_button = tk.Button( process_frame, text="Terminar Proceso Seleccionado (⚠️ DANGER)", command=lambda: monitor_manager.terminar_proceso(treeview_processes), bg='darkred', fg='white', font=('Helvetica', 10, 'bold') ) kill_button.grid(row=1, column=0, sticky="ew", pady=(5,0)) # =============================================== # Solapa de Web Scraping (Implementación completa y estética) # =============================================== # Marco principal para el scraping main_scraping_frame = ttk.Frame(scraping_tab, padding="15") main_scraping_frame.pack(fill=tk.BOTH, expand=True) main_scraping_frame.columnconfigure(0, weight=1) main_scraping_frame.columnconfigure(1, weight=0) main_scraping_frame.rowconfigure(5, weight=1) # Fila 5 es el Text Output # --- Fila 0 & 1: URL, Opciones y Configuración Personalizada --- # 1. Título y Carga de Configuración header_frame = ttk.Frame(main_scraping_frame) header_frame.grid(row=0, column=0, columnspan=2, sticky='ew', pady=(0, 5)) header_frame.columnconfigure(0, weight=1) # CORRECCIÓN DE ORTOGRAFÍA: "Scrappear" -> "Scrapear" ttk.Label(header_frame, text="🌐 URL a Scrapear:", font=('Helvetica', 10, 'bold')).pack(side=tk.LEFT, padx=(0, 5)) config.scraping_config_file_label = ttk.Label(header_frame, text="Config: [Ninguna]", foreground='blue') config.scraping_config_file_label.pack(side=tk.RIGHT, padx=5) ttk.Button( header_frame, text="⚙️ Cargar Config. (.json)", command=lambda: system_utils.abrir_archivo_scraping_config(root) ).pack(side=tk.RIGHT) # 2. Entrada de URL url_var = tk.StringVar(value="https://www.example.com") url_entry = ttk.Entry(main_scraping_frame, textvariable=url_var, width=80) url_entry.grid(row=1, column=0, columnspan=2, sticky='ew', pady=(0, 10)) # ASIGNAR A CONFIG para que system_utils pueda actualizarlo config.scraping_url_input = url_var # --- Fila 2: Controles Detallados --- control_frame_row2 = ttk.Frame(main_scraping_frame) control_frame_row2.grid(row=2, column=0, columnspan=2, sticky='ew', pady=5) control_frame_row2.columnconfigure(2, weight=1) # Opciones de Extracción ttk.Label(control_frame_row2, text="Tipo de Extracción:", font=('Helvetica', 9, 'bold')).grid(row=0, column=0, sticky='w', padx=(0, 5)) tipo_extraccion_var = tk.StringVar(value="Título y Metadatos") extracciones = [ "Título y Metadatos", "Primeros Párrafos", "Enlaces (Links)", "Imágenes (URLs)", "Tablas (Estructura Básica)", "Portátiles Gamer (Enlace + Precio)", # NUEVA OPCIÓN COMBINADA "-> Texto Específico (CSS Selector)", "-> Atributo Específico (CSS Selector + Attr)" ] extraccion_combobox = ttk.Combobox( control_frame_row2, values=extracciones, textvariable=tipo_extraccion_var, state="readonly", width=30 ) extraccion_combobox.grid(row=0, column=1, sticky='w', padx=10) # Selector CSS ttk.Label(control_frame_row2, text="Selector CSS/Tag (avanzado):").grid(row=0, column=3, sticky='w', padx=(10, 5)) config.scraping_selector_input = ttk.Entry(control_frame_row2, width=40) config.scraping_selector_input.grid(row=0, column=4, sticky='ew', padx=(0, 10)) # Atributo ttk.Label(control_frame_row2, text="Atributo (ej: href/src):").grid(row=0, column=5, sticky='w', padx=(10, 5)) config.scraping_attr_input = ttk.Entry(control_frame_row2, width=15) config.scraping_attr_input.grid(row=0, column=6, sticky='ew') # --- Fila 3: Ejecución y Control --- control_execution_frame = ttk.Frame(main_scraping_frame) control_execution_frame.grid(row=3, column=0, columnspan=2, sticky='ew', pady=(10, 5)) control_execution_frame.columnconfigure(0, weight=1) # Botón Scrappear btn_scrap = ttk.Button( control_execution_frame, text="🚀 Iniciar Scrapear", command=lambda: monitor_manager.scrappear_pagina_principal( url_var.get(), tipo_extraccion_var.get(), config.scraping_output_text, config.scraping_progress_bar, config.scraping_selector_input.get(), config.scraping_attr_input.get(), config.scraping_config_data, root ) ) btn_scrap.pack(side=tk.LEFT, padx=(0, 10)) # Barra de Progreso config.scraping_progress_bar = ttk.Progressbar(control_execution_frame, orient="horizontal", mode="indeterminate") config.scraping_progress_bar.pack(side=tk.LEFT, fill='x', expand=True, padx=(0, 10)) # Botón Detener ttk.Button( control_execution_frame, text="🛑 Detener", command=lambda: system_utils.detener_scraping() ).pack(side=tk.LEFT) # --- Fila 4 & 5: Área de Resultado y Guardar --- ttk.Label(main_scraping_frame, text="📊 Resultado de la Extracción:", font=('Helvetica', 10, 'bold')).grid(row=4, column=0, columnspan=2, sticky='w', pady=(10, 5)) # Widget de Texto para la salida config.scraping_output_text = ScrolledText(main_scraping_frame, wrap='word', font=('Courier New', 9), height=18, bg='#f9f9f9') config.scraping_output_text.grid(row=5, column=0, columnspan=2, sticky='nsew', pady=(0, 10)) # Botón Guardar Resultado ttk.Button( main_scraping_frame, text="💾 Guardar Resultado en /data/scraping", # CORRECCIÓN DE RUTA command=lambda: system_utils.guardar_scraping(config.scraping_output_text.get("1.0", tk.END), root) ).grid(row=6, column=0, columnspan=2, sticky='ew') # =============================================== # CONTENIDO DE LA SOLAPA DE JUEGOS # =============================================== main_games_frame = ttk.Frame(games_tab, padding="20") main_games_frame.pack(fill=tk.BOTH, expand=True) ttk.Label( main_games_frame, text="Simulaciones de Entretenimiento (Thread-Safe)", font=('Helvetica', 14, 'bold') ).pack(pady=15) ttk.Separator(main_games_frame, orient='horizontal').pack(fill='x', pady=5) # Botón de juego de camellos ttk.Button( main_games_frame, text="🏆 Iniciar Carrera de Camellos (Thread-Safe)", command=lambda: system_utils.simular_juego_camellos(root), cursor="hand2" ).pack(pady=10) ttk.Label( main_games_frame, text="Este juego se ejecuta en una ventana 'Toplevel' para garantizar la seguridad del hilo principal.", font=('Helvetica', 9, 'italic'), foreground='gray' ).pack(pady=5) # =============================================== # CONTENIDO DE LA SOLAPA DE MÚSICA (NUEVO) # =============================================== music_frame = ttk.Frame(music_tab, padding="20") music_frame.pack(fill=tk.BOTH, expand=True) ttk.Label( music_frame, text="Reproductor de Audio Local (.mp3 / .wav)", font=('Helvetica', 14, 'bold') ).pack(pady=15) # 1. Archivo Seleccionado ttk.Label(music_frame, text="Archivo Seleccionado:").pack(pady=(10, 5)) label_music_file = ttk.Label(music_frame, text="[Ningún archivo cargado]", anchor='center', foreground='blue') label_music_file.pack(fill='x', padx=50) # 2. Botón Seleccionar ttk.Button( music_frame, text="📂 Seleccionar MP3/WAV", command=lambda: system_utils.seleccionar_mp3(root, label_music_file) ).pack(pady=10, padx=50, fill='x') ttk.Separator(music_frame, orient='horizontal').pack(fill='x', pady=10, padx=50) # 3. Controles de Reproducción control_music_frame = ttk.Frame(music_frame) control_music_frame.pack(pady=10) ttk.Button( control_music_frame, text="▶️ Reproducir", command=lambda: system_utils.reproducir_mp3(root) ).pack(side=tk.LEFT, padx=10) ttk.Button( control_music_frame, text="⏹️ Detener", command=system_utils.detener_mp3 ).pack(side=tk.LEFT, padx=10) # 4. Control de Volumen ttk.Label(music_frame, text="Volumen:").pack(pady=(15, 5)) volumen_scale_music = ttk.Scale( music_frame, from_=0, to=100, orient='horizontal', length=200, command=system_utils.ajustar_volumen_mp3 # Reutilizamos la función que ajusta Pygame Mixer ) volumen_scale_music.set(config.alarma_volumen * 100) volumen_scale_music.pack(pady=5) # --- Iniciar Hilos --- system_utils.log_event("Monitor de sistema iniciado. Esperando la primera lectura de métricas...") monitor_manager.iniciar_monitor_sistema(fig, canvas, ax_cpu, ax_mem, ax_net, ax_cores, ax_pie, ax_disk_io, treeview_processes, root) update_thread = threading.Thread(target=lambda: system_utils.update_time(label_fecha_hora, root)) update_thread.daemon = True update_thread.start()