add chat system
This commit is contained in:
parent
073733b887
commit
1ac67ba9bd
|
|
@ -66,6 +66,12 @@ Características principales
|
|||
- Juego de carreras (canvas): [`proyecto.open_game_race`](proyecto.py)
|
||||
- Abrir aplicaciones macOS con `open`: [`proyecto.launch_app`](proyecto.py)
|
||||
- Monitor de red en la barra de estado: [`proyecto.network_monitor`](proyecto.py)
|
||||
- **Chat TCP (Multiusuario):**
|
||||
- **Acceso:** Desde pestaña "Enlaces" -> Botón "Chat TCP".
|
||||
- **Servidor:** Al lanzarlo, se integra en el **panel derecho de la ventana principal**. Desde ahí se visualizan logs de conexión y mensajería en tiempo real, y permite enviar mensajes como Administrador a todos los conectados. [Código servidor](chat_server.py).
|
||||
- **Cliente:**
|
||||
- *Desde la App:* Abre una terminal externa independiente para chatear.
|
||||
- *Desde otro PC:* Solo requiere el archivo `chat_client.py` (sin dependencias extra). Ejecutar: `python chat_client.py --host IP_SERVIDOR --name TuNombre`. [Código cliente](chat_client.py).
|
||||
|
||||
Configuración y datos sensibles
|
||||
-------------------------------
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,88 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
|
||||
|
||||
DEFAULT_PORT = 5050
|
||||
ENCODING = "utf-8"
|
||||
|
||||
|
||||
def _receiver_loop(rfile) -> None:
|
||||
while True:
|
||||
try:
|
||||
line = rfile.readline()
|
||||
except Exception:
|
||||
break
|
||||
if not line:
|
||||
break
|
||||
# Imprime mensajes entrantes en tiempo real
|
||||
sys.stdout.write(line)
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Cliente de chat TCP")
|
||||
parser.add_argument("--host", help="IP/host del servidor (ej: 192.168.1.10)")
|
||||
parser.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"Puerto (por defecto {DEFAULT_PORT})")
|
||||
parser.add_argument("--name", help="Tu nombre en el chat")
|
||||
args = parser.parse_args()
|
||||
|
||||
host = args.host or input("IP del servidor: ").strip()
|
||||
if not host:
|
||||
print("Host vacío. Abortando.")
|
||||
return 2
|
||||
|
||||
name = args.name or input("Tu nombre: ").strip()
|
||||
if not name:
|
||||
name = "Anon"
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.connect((host, args.port))
|
||||
|
||||
rfile = sock.makefile("r", encoding=ENCODING, newline="\n")
|
||||
wfile = sock.makefile("w", encoding=ENCODING, newline="\n")
|
||||
|
||||
# Handshake simple con el servidor
|
||||
first = rfile.readline()
|
||||
if first and first.strip() == "NAME?":
|
||||
wfile.write(f"NAME {name}\n")
|
||||
wfile.flush()
|
||||
else:
|
||||
# Si el servidor no pide nombre, lo mandamos igualmente.
|
||||
wfile.write(f"NAME {name}\n")
|
||||
wfile.flush()
|
||||
if first:
|
||||
sys.stdout.write(first)
|
||||
sys.stdout.flush()
|
||||
|
||||
t = threading.Thread(target=_receiver_loop, args=(rfile,), daemon=True)
|
||||
t.start()
|
||||
|
||||
print("Conectado. Escribe mensajes y pulsa Enter. /quit para salir.")
|
||||
|
||||
try:
|
||||
for line in sys.stdin:
|
||||
msg = line.rstrip("\n")
|
||||
wfile.write(msg + "\n")
|
||||
wfile.flush()
|
||||
if msg.lower() in {"/quit", "/exit"}:
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
try:
|
||||
wfile.write("/quit\n")
|
||||
wfile.flush()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
#!/usr/bin/env python3
|
||||
import socket
|
||||
import threading
|
||||
from dataclasses import dataclass
|
||||
|
||||
DEFAULT_HOST = "0.0.0.0"
|
||||
DEFAULT_PORT = 5050
|
||||
ENCODING = "utf-8"
|
||||
|
||||
@dataclass
|
||||
class ClientConn:
|
||||
sock: socket.socket
|
||||
addr: tuple
|
||||
name: str
|
||||
wfile: any
|
||||
|
||||
class ChatServer:
|
||||
def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT, on_log=None):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.on_log = on_log if on_log else print
|
||||
self.clients = {}
|
||||
self.clients_lock = threading.Lock()
|
||||
self.server_sock = None
|
||||
self.running = False
|
||||
self.thread = None
|
||||
|
||||
def log(self, message):
|
||||
"""Envía el mensaje al callback configurado (GUI o print)"""
|
||||
self.on_log(message)
|
||||
|
||||
def broadcast(self, message: str, exclude: socket.socket | None = None) -> None:
|
||||
"""Envía mensaje a todos los clientes conectados"""
|
||||
line = message.rstrip("\n") + "\n"
|
||||
with self.clients_lock:
|
||||
items = list(self.clients.items())
|
||||
|
||||
# Log local si no está excluido el propio servidor (opcional, pero útil ver lo que se envía)
|
||||
# self.log(f"BRD: {message}")
|
||||
|
||||
for sock, client in items:
|
||||
if exclude is not None and sock is exclude:
|
||||
continue
|
||||
try:
|
||||
client.wfile.write(line)
|
||||
client.wfile.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def send_server_message(self, text: str):
|
||||
"""Mensaje desde el servidor (admin)"""
|
||||
msg = f"[ADMIN] {text}"
|
||||
self.log(msg)
|
||||
self.broadcast(msg)
|
||||
|
||||
def _handle_client(self, conn: socket.socket, addr: tuple) -> None:
|
||||
try:
|
||||
rfile = conn.makefile("r", encoding=ENCODING, newline="\n")
|
||||
wfile = conn.makefile("w", encoding=ENCODING, newline="\n")
|
||||
|
||||
name = None
|
||||
|
||||
# Protocolo simple: pedir nombre
|
||||
try:
|
||||
wfile.write("NAME?\n")
|
||||
wfile.flush()
|
||||
except Exception:
|
||||
return
|
||||
|
||||
try:
|
||||
raw_name = rfile.readline()
|
||||
except Exception:
|
||||
raw_name = None
|
||||
|
||||
if not raw_name:
|
||||
return
|
||||
|
||||
raw_name = raw_name.strip()
|
||||
# Compatibilidad si el cliente manda "NAME Pepe"
|
||||
if raw_name.upper().startswith("NAME "):
|
||||
raw_name = raw_name[5:].strip()
|
||||
|
||||
name = raw_name or f"{addr[0]}:{addr[1]}"
|
||||
|
||||
with self.clients_lock:
|
||||
self.clients[conn] = ClientConn(sock=conn, addr=addr, name=name, wfile=wfile)
|
||||
|
||||
msg_join = f"* {name} se ha unido al chat *"
|
||||
self.log(msg_join)
|
||||
self.broadcast(msg_join)
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
line = rfile.readline()
|
||||
except Exception:
|
||||
break
|
||||
|
||||
if not line:
|
||||
break
|
||||
|
||||
msg = line.strip()
|
||||
if not msg:
|
||||
continue
|
||||
|
||||
if msg.lower() in {"/quit", "/exit"}:
|
||||
break
|
||||
|
||||
# Mostrar en servidor y reenviar a otros
|
||||
full_msg = f"[{name}] {msg}"
|
||||
self.log(full_msg)
|
||||
self.broadcast(full_msg)
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"Error gestionando cliente {addr}: {e}")
|
||||
finally:
|
||||
with self.clients_lock:
|
||||
self.clients.pop(conn, None)
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if name:
|
||||
msg_left = f"* {name} ha salido del chat *"
|
||||
self.log(msg_left)
|
||||
self.broadcast(msg_left)
|
||||
|
||||
def start_background(self):
|
||||
"""Inicia el servidor en un hilo secundario"""
|
||||
if self.running:
|
||||
return
|
||||
self.running = True
|
||||
self.thread = threading.Thread(target=self._run_server_loop, daemon=True)
|
||||
self.thread.start()
|
||||
|
||||
def stop(self):
|
||||
"""Detiene el servidor"""
|
||||
self.running = False
|
||||
if self.server_sock:
|
||||
try:
|
||||
self.server_sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.log("Servidor detenido.")
|
||||
|
||||
def _run_server_loop(self):
|
||||
self.server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
|
||||
try:
|
||||
self.server_sock.bind((self.host, self.port))
|
||||
self.server_sock.listen(100)
|
||||
self.server_sock.settimeout(1.0) # Timeout para permitir verificar self.running
|
||||
self.log(f"Servidor escuchando en {self.host}:{self.port}")
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
conn, addr = self.server_sock.accept()
|
||||
t = threading.Thread(target=self._handle_client, args=(conn, addr), daemon=True)
|
||||
t.start()
|
||||
except TimeoutError:
|
||||
continue
|
||||
except OSError:
|
||||
break
|
||||
except Exception as e:
|
||||
self.log(f"Error aceptando conexión: {e}")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"Error fatal en servidor: {e}")
|
||||
finally:
|
||||
self.running = False
|
||||
try:
|
||||
self.server_sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Modo standalone para pruebas
|
||||
import sys
|
||||
try:
|
||||
srv = ChatServer(port=5050)
|
||||
srv.start_background()
|
||||
print("Presiona Ctrl+C para salir.")
|
||||
while True:
|
||||
cmd = sys.stdin.readline()
|
||||
if not cmd: break
|
||||
if cmd.strip():
|
||||
srv.send_server_message(cmd.strip())
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
srv.stop()
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
|
||||
|
||||
DEFAULT_PORT = 5050
|
||||
ENCODING = "utf-8"
|
||||
|
||||
|
||||
def _receiver_loop(rfile) -> None:
|
||||
while True:
|
||||
try:
|
||||
line = rfile.readline()
|
||||
except Exception:
|
||||
break
|
||||
if not line:
|
||||
break
|
||||
# Imprime mensajes entrantes en tiempo real
|
||||
sys.stdout.write(line)
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Cliente de chat TCP")
|
||||
parser.add_argument("--host", help="IP/host del servidor (ej: 192.168.1.10)")
|
||||
parser.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"Puerto (por defecto {DEFAULT_PORT})")
|
||||
parser.add_argument("--name", help="Tu nombre en el chat")
|
||||
args = parser.parse_args()
|
||||
|
||||
host = args.host or input("IP del servidor: ").strip()
|
||||
if not host:
|
||||
print("Host vacío. Abortando.")
|
||||
return 2
|
||||
|
||||
name = args.name or input("Tu nombre: ").strip()
|
||||
if not name:
|
||||
name = "Anon"
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.connect((host, args.port))
|
||||
|
||||
rfile = sock.makefile("r", encoding=ENCODING, newline="\n")
|
||||
wfile = sock.makefile("w", encoding=ENCODING, newline="\n")
|
||||
|
||||
# Handshake simple con el servidor
|
||||
first = rfile.readline()
|
||||
if first and first.strip() == "NAME?":
|
||||
wfile.write(f"NAME {name}\n")
|
||||
wfile.flush()
|
||||
else:
|
||||
# Si el servidor no pide nombre, lo mandamos igualmente.
|
||||
wfile.write(f"NAME {name}\n")
|
||||
wfile.flush()
|
||||
if first:
|
||||
sys.stdout.write(first)
|
||||
sys.stdout.flush()
|
||||
|
||||
t = threading.Thread(target=_receiver_loop, args=(rfile,), daemon=True)
|
||||
t.start()
|
||||
|
||||
print("Conectado. Escribe mensajes y pulsa Enter. /quit para salir.")
|
||||
|
||||
try:
|
||||
for line in sys.stdin:
|
||||
msg = line.rstrip("\n")
|
||||
wfile.write(msg + "\n")
|
||||
wfile.flush()
|
||||
if msg.lower() in {"/quit", "/exit"}:
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
try:
|
||||
wfile.write("/quit\n")
|
||||
wfile.flush()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
175
proyecto.py
175
proyecto.py
|
|
@ -1,7 +1,7 @@
|
|||
import tkinter as tk
|
||||
from tkinter import Menu # Importar el widget Menu
|
||||
from tkinter import ttk # Importar el widget ttk
|
||||
from tkinter import Menu, ttk, scrolledtext # Importar el widget Menu, ttk y scrolledtext
|
||||
import threading
|
||||
import sys
|
||||
import time
|
||||
import datetime
|
||||
import webbrowser
|
||||
|
|
@ -52,6 +52,10 @@ try:
|
|||
except Exception:
|
||||
HAS_PYGAME = False
|
||||
|
||||
# Import chat TCP (servidor y cliente)
|
||||
import chat_server
|
||||
import chat_client
|
||||
|
||||
def update_time(label_widget):
|
||||
"""Función que actualiza la hora y el día de la semana en un label.
|
||||
|
||||
|
|
@ -168,6 +172,128 @@ def backup_ui():
|
|||
pass
|
||||
|
||||
|
||||
def open_chat_window():
|
||||
"""Abre ventana de configuración para lanzar servidor o cliente de chat"""
|
||||
win = tk.Toplevel(root)
|
||||
win.title("Chat TCP - Configuración")
|
||||
win.geometry("450x400")
|
||||
win.minsize(400, 350)
|
||||
|
||||
# Estilo personalizado para títulos
|
||||
header_font = ("Helvetica", 16, "bold")
|
||||
label_font = ("Helvetica", 11)
|
||||
|
||||
# Contenedor principal
|
||||
content = tk.Frame(win, bg="#f5f5f5", padx=20, pady=20)
|
||||
content.pack(fill="both", expand=True)
|
||||
|
||||
# Título
|
||||
tk.Label(content, text="Iniciar Chat", font=header_font, bg="#f5f5f5", fg="#333").pack(pady=(0, 20))
|
||||
|
||||
# --- Sección ROL ---
|
||||
rol_frame = tk.Frame(content, bg="white", bd=1, relief="solid", padx=15, pady=15)
|
||||
rol_frame.pack(fill="x", pady=(0, 15))
|
||||
|
||||
tk.Label(rol_frame, text="Selecciona tu rol:", font=("Helvetica", 12, "bold"), bg="white").pack(anchor="w", pady=(0, 10))
|
||||
|
||||
mode_var = tk.StringVar(value="server")
|
||||
|
||||
# Sub-frame para radios
|
||||
radios_f = tk.Frame(rol_frame, bg="white")
|
||||
radios_f.pack(fill="x")
|
||||
|
||||
r1 = tk.Radiobutton(radios_f, text="Servidor (Anfitrión)", variable=mode_var, value="server", bg="white", font=label_font)
|
||||
r1.pack(side="left", padx=(0, 20))
|
||||
r2 = tk.Radiobutton(radios_f, text="Cliente (Invitado)", variable=mode_var, value="client", bg="white", font=label_font)
|
||||
r2.pack(side="left")
|
||||
|
||||
# --- Sección DATOS ---
|
||||
data_frame = tk.Frame(content, bg="white", bd=1, relief="solid", padx=15, pady=15)
|
||||
data_frame.pack(fill="x", pady=(0, 20))
|
||||
|
||||
# Grid para formulario
|
||||
tk.Label(data_frame, text="Puerto:", bg="white", font=label_font).grid(row=0, column=0, sticky="e", padx=5, pady=5)
|
||||
port_var = tk.StringVar(value="5050")
|
||||
tk.Entry(data_frame, textvariable=port_var, width=10, font=label_font).grid(row=0, column=1, sticky="w", padx=5, pady=5)
|
||||
|
||||
tk.Label(data_frame, text="IP Servidor:", bg="white", font=label_font).grid(row=1, column=0, sticky="e", padx=5, pady=5)
|
||||
host_var = tk.StringVar(value="127.0.0.1")
|
||||
ip_entry = tk.Entry(data_frame, textvariable=host_var, width=20, font=label_font)
|
||||
ip_entry.grid(row=1, column=1, sticky="w", padx=5, pady=5)
|
||||
|
||||
tk.Label(data_frame, text="(Solo Cliente)", bg="white", fg="gray", font=("Arial", 9)).grid(row=1, column=2, sticky="w", padx=5)
|
||||
|
||||
def toggle_ip(*args):
|
||||
if mode_var.get() == "server":
|
||||
ip_entry.config(state="disabled", bg="#eee")
|
||||
else:
|
||||
ip_entry.config(state="normal", bg="white")
|
||||
|
||||
mode_var.trace_add("write", toggle_ip)
|
||||
toggle_ip()
|
||||
|
||||
# --- Botón Acción ---
|
||||
def run_action():
|
||||
mode = mode_var.get()
|
||||
try:
|
||||
port = int(port_var.get())
|
||||
except ValueError:
|
||||
mb.showerror("Error", "El puerto debe ser numérico")
|
||||
return
|
||||
|
||||
if mode == "server":
|
||||
win.destroy()
|
||||
start_server_in_ui(port)
|
||||
else:
|
||||
host = host_var.get()
|
||||
win.destroy()
|
||||
open_client_terminal(host, port)
|
||||
|
||||
# Botón usando ttk para asegurar renderizado nativo correcto sobre frame
|
||||
# A veces tk.Button da problemas de z-order. Usamos un botón grande.
|
||||
btn_frame = tk.Frame(content, bg="#f5f5f5", pady=10)
|
||||
btn_frame.pack(fill="x", side="bottom")
|
||||
|
||||
btn = tk.Button(btn_frame, text="LANZAR APLICACIÓN", command=run_action,
|
||||
bg="#007aff", fg="black", font=("Helvetica", 14, "bold"), height=2)
|
||||
# En macOS 'fg' a veces no va, pero el botón debería verse.
|
||||
# Usamos pack con fill x
|
||||
btn.pack(fill="x")
|
||||
|
||||
|
||||
|
||||
def open_client_terminal(host, port):
|
||||
"""Abre terminal externa para el cliente (interactivo)"""
|
||||
if not host:
|
||||
host = "127.0.0.1"
|
||||
|
||||
# Intentar detectar terminal según OS
|
||||
# macOS
|
||||
cmd_script = f'tell application "Terminal" to do script "cd \\"{os.getcwd()}\\" && \\"{sys.executable}\\" chat_client.py --host {host} --port {port} --name UsuarioGUI"'
|
||||
try:
|
||||
subprocess.run(["osascript", "-e", cmd_script])
|
||||
except Exception as e:
|
||||
mb.showerror("Error", f"No se pudo lanzar terminal:\n{e}")
|
||||
|
||||
|
||||
def start_server_in_ui(port):
|
||||
"""Inicia el servidor y conecta los logs al sidebar derecho"""
|
||||
global global_chat_server
|
||||
|
||||
if global_chat_server and global_chat_server.running:
|
||||
mb.showinfo("Servidor", "El servidor ya está corriendo. Detenlo primero si quieres reiniciar.")
|
||||
return
|
||||
|
||||
try:
|
||||
global_chat_server = chat_server.ChatServer(port=port, on_log=append_chat_log)
|
||||
global_chat_server.start_background()
|
||||
append_chat_log(f">>> Servidor iniciado en puerto {port}")
|
||||
mb.showinfo("Servidor", f"Servidor iniciado correctamente en puerto {port}.\nVer logs en panel derecho.")
|
||||
except Exception as e:
|
||||
mb.showerror("Error", f"Error al iniciar servidor:\n{e}")
|
||||
append_chat_log(f"Error inicio: {e}")
|
||||
|
||||
|
||||
def open_resource_window():
|
||||
if not HAS_MATPLOTLIB:
|
||||
mb.showwarning("Dependencia", "matplotlib no está disponible. Instálalo con pip install matplotlib")
|
||||
|
|
@ -918,18 +1044,38 @@ sec_batch.pack(fill="x", padx=8, pady=(12,6))
|
|||
btn_backup = ttk.Button(frame_izquierdo, text="Copias de seguridad", width=18, style="Secondary.TButton")
|
||||
btn_backup.pack(pady=6, padx=8, fill='x')
|
||||
# --- Contenido del sidebar derecho (chat y lista de alumnos) ---
|
||||
chat_title = tk.Label(frame_derecho, text="Chat", font=("Helvetica", 14, "bold"), bg=PALETTE["sidebar"])
|
||||
chat_title = tk.Label(frame_derecho, text="Chat Servidor", font=("Helvetica", 14, "bold"), bg=PALETTE["sidebar"])
|
||||
chat_title.pack(pady=(8,8))
|
||||
|
||||
msg_label = tk.Label(frame_derecho, text="Mensaje", bg=PALETTE["sidebar"], font=FONT_NORMAL)
|
||||
# Área de logs del chat (solo lectura)
|
||||
chat_log = scrolledtext.ScrolledText(frame_derecho, height=10, width=26, bd=0, relief="flat", state="disabled", font=("Helvetica", 9))
|
||||
chat_log.pack(padx=8, pady=(0, 6), fill="x", expand=False)
|
||||
|
||||
msg_label = tk.Label(frame_derecho, text="Mensaje Admin", bg=PALETTE["sidebar"], font=FONT_NORMAL)
|
||||
msg_label.pack(padx=8, anchor="w")
|
||||
|
||||
msg_text = tk.Text(frame_derecho, height=6, width=26, bd=0, relief="flat")
|
||||
msg_text.pack(padx=8, pady=(6,8), fill="x")
|
||||
msg_text = tk.Text(frame_derecho, height=3, width=26, bd=0, relief="flat")
|
||||
msg_text.pack(padx=8, pady=(2,8), fill="x")
|
||||
|
||||
send_btn = ttk.Button(frame_derecho, text="Enviar", style="Accent.TButton")
|
||||
send_btn.pack(padx=8, pady=(0,12))
|
||||
|
||||
# Variable global para la instancia del servidor
|
||||
global_chat_server = None
|
||||
|
||||
def append_chat_log(msg):
|
||||
"""Callback para añadir logs al área de chat del sidebar"""
|
||||
def _u():
|
||||
chat_log.config(state="normal")
|
||||
chat_log.insert("end", str(msg) + "\n")
|
||||
chat_log.see("end")
|
||||
chat_log.config(state="disabled")
|
||||
# Asegurar ejecución en hilo principal
|
||||
try:
|
||||
chat_log.after(0, _u)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
alumnos_label = tk.Label(frame_derecho, text="Alumnos", bg=PALETTE["sidebar"], font=FONT_TITLE)
|
||||
alumnos_label.pack(padx=8, anchor="w")
|
||||
|
||||
|
|
@ -972,16 +1118,22 @@ btn_buscar.config(command=fetch_weather_xabia)
|
|||
|
||||
# Los controles de alarma y reproducción de música están disponibles en la pestaña "Enlaces"
|
||||
|
||||
# Enviar mensaje (simulado)
|
||||
def send_message():
|
||||
# Enviar mensaje (Chat Servidor)
|
||||
def send_message(event=None):
|
||||
# Si viene de evento KeyRelease/Return, evitar salto de línea extra si es Text
|
||||
text = msg_text.get("1.0", "end-1c").strip()
|
||||
if not text:
|
||||
mb.showwarning("Mensaje", "El mensaje está vacío")
|
||||
return
|
||||
mb.showinfo("Mensaje", "Mensaje enviado (simulado)")
|
||||
return "break"
|
||||
|
||||
if global_chat_server and global_chat_server.running:
|
||||
global_chat_server.send_server_message(text)
|
||||
msg_text.delete("1.0", "end")
|
||||
else:
|
||||
mb.showwarning("Chat Servidor", "El servidor no está iniciado.\nVe a 'Enlaces > Chat TCP' e inicia el servidor.")
|
||||
return "break"
|
||||
|
||||
send_btn.config(command=send_message)
|
||||
msg_text.bind("<Return>", send_message)
|
||||
|
||||
# Dividir el frame central en dos partes (superior variable e inferior fija)
|
||||
frame_central.rowconfigure(0, weight=1) # Parte superior, tamaño variable
|
||||
|
|
@ -1155,6 +1307,7 @@ links_frame = tk.Frame(tab_enlaces)
|
|||
links_frame.pack(fill="both", expand=True, padx=8, pady=8)
|
||||
ttk.Button(links_frame, text="Abrir Visual Studio Code", command=lambda: launch_app("/Applications/Visual Studio Code.app"), style="Accent.TButton").pack(fill="x", pady=4)
|
||||
ttk.Button(links_frame, text="Abrir Spotify", command=lambda: launch_app("/Applications/Spotify.app"), style="Secondary.TButton").pack(fill="x", pady=4)
|
||||
ttk.Button(links_frame, text="Chat TCP (Servidor/Cliente)", command=open_chat_window, style="Accent.TButton").pack(fill="x", pady=4)
|
||||
ttk.Button(links_frame, text="Mostrar recursos (matplotlib)", command=open_resource_window, style="Secondary.TButton").pack(fill="x", pady=4)
|
||||
ttk.Button(links_frame, text="Reproducir música (archivo)", command=play_music_file, style="Secondary.TButton").pack(fill="x", pady=4)
|
||||
ttk.Button(links_frame, text="Detener música", command=stop_music, style="Secondary.TButton").pack(fill="x", pady=4)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue