diff --git a/.doc/refactoring_guide.md b/.doc/Guida_Desarrollo.md similarity index 99% rename from .doc/refactoring_guide.md rename to .doc/Guida_Desarrollo.md index 11f6c22..e591038 100644 --- a/.doc/refactoring_guide.md +++ b/.doc/Guida_Desarrollo.md @@ -12,7 +12,7 @@ Este documento describe el estado actual y los objetivos de desarrollo para la * - La interfaz de usuario se divide en 2 columnas, a la izquierda el area de ingreso de datos y equaciones a ser evaluadas, a la derecha el area de resultados que se colorean segun cada tipo de respuesta. ### 2. **Sintaxis Simplificada con Corchetes (Única)** -- Usar **exclusivamente** `Class[args]` en lugar de `Class("args")` +- Usar **exclusivamente** `Class[args]` en lugar de `Class("args")` . Cuando se usan los corchetes los parametros se separan por `;` - Los corchetes indican parsing especial (agregar comillas automáticamente) - Mantiene sintaxis limpia y reduce errores de tipeo @@ -91,6 +91,7 @@ x=? → solve(x) ### 1. **Bracket Parser y Detección Contextual de Ecuaciones** - Preprocesador que convierte `Class[args]` → `Class("args")` +- La separacion de argumentos se hace con el ";" - Soporte para clases: `IP4`, `Hex`, `Bin`, `Date`, `Dec`, `Chr` - **Detección contextual inteligente de ecuaciones**: - Ecuaciones standalone: `3+b=5+c` (detectar como ecuación) @@ -314,6 +315,7 @@ Cada tipo de objeto (por ejemplo, `IP4`, `Hex`, etc.) debe definir una función - Recibe el string de entrada del usuario. - Decide, usando su propia lógica (por ejemplo, expresiones regulares), si puede ofrecer una ayuda relevante. - Devuelve un string de ayuda (ejemplo de uso, sintaxis, sugerencia) o `None` si no aplica. +- Las respuestas no pueden tener mas de una linea nunca. **Ejemplo:** ```python diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..56c56a0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.analysis.autoImportCompletions": true, + "python.analysis.typeCheckingMode": "off" +} \ No newline at end of file diff --git a/MaVCalcv2.lnk b/MaVCalcv2.lnk index 08766e0..8cf8571 100644 Binary files a/MaVCalcv2.lnk and b/MaVCalcv2.lnk differ diff --git a/bin_type.py b/bin_type.py index 7f7385e..c887f8f 100644 --- a/bin_type.py +++ b/bin_type.py @@ -1,16 +1,16 @@ """ Clase híbrida para números binarios """ -from hybrid_base import HybridCalcType +from sympy_Base import SympyClassBase import re -class HybridBin(HybridCalcType): +class Class_Bin(SympyClassBase): """Clase híbrida para números binarios""" def __new__(cls, value_input): """Crear objeto SymPy válido""" - obj = HybridCalcType.__new__(cls) + obj = SympyClassBase.__new__(cls) return obj def __init__(self, value_input): diff --git a/calc.py b/calc.py new file mode 100644 index 0000000..378898f --- /dev/null +++ b/calc.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +""" +Launcher principal para Calculadora MAV - CAS Híbrido +Este script maneja la inicialización y ejecución de la aplicación +""" +import sys +import os +import subprocess +import tkinter as tk +from tkinter import messagebox +from pathlib import Path +import importlib.util +import logging +import datetime +import traceback +import platform +import platform + + +def setup_logging(): + """Configura el sistema de logging completo""" + log_dir = Path("logs") + log_dir.mkdir(exist_ok=True) + + # Archivo de log con timestamp + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + log_file = log_dir / f"mav_calc_{timestamp}.log" + + # Configurar logging + logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s', + handlers=[ + logging.FileHandler(log_file, encoding='utf-8'), + logging.StreamHandler(sys.stdout) + ] + ) + + logger = logging.getLogger(__name__) + + + return logger, log_file + + + +def log_error_with_context(logger, error, context=""): + """Registra un error con contexto completo""" + logger.error("=" * 50) + logger.error("ERROR DETECTADO") + logger.error("=" * 50) + if context: + logger.error(f"Contexto: {context}") + logger.error(f"Tipo de error: {type(error).__name__}") + logger.error(f"Mensaje: {str(error)}") + logger.error("Traceback completo:") + logger.error(traceback.format_exc()) + logger.error("Variables locales en el momento del error:") + + # Intentar capturar variables locales del frame donde ocurrió el error + try: + tb = traceback.extract_tb(error.__traceback__) + if tb: + last_frame = tb[-1] + logger.error(f" Archivo: {last_frame.filename}") + logger.error(f" Línea: {last_frame.lineno}") + logger.error(f" Función: {last_frame.name}") + logger.error(f" Código: {last_frame.line}") + except Exception as frame_error: + logger.error(f"No se pudieron obtener detalles del frame: {frame_error}") + + logger.error("=" * 50) + + +def show_error_with_log_info(error, log_file, context=""): + """Muestra error al usuario con información del log""" + error_msg = f"""Error en Calculadora MAV: + +{context} + +Error: {type(error).__name__}: {str(error)} + +INFORMACIÓN DE DEBUGGING: +• Log completo guardado en: {log_file} +• Para soporte, enviar el archivo de log +• Timestamp: {datetime.datetime.now()} + +¿Qué hacer ahora? +1. Revisar el archivo de log para más detalles +2. Intentar reiniciar la aplicación +3. Verificar dependencias con: python launcher.py --setup +4. Ejecutar tests con: python launcher.py --test +""" + + try: + root = tk.Tk() + root.withdraw() + + # Crear ventana de error personalizada + error_window = tk.Toplevel(root) + error_window.title("Error - Calculadora MAV") + error_window.geometry("600x400") + error_window.configure(bg="#2b2b2b") + + # Hacer la ventana modal + error_window.transient(root) + error_window.grab_set() + + # Centrar ventana + error_window.update_idletasks() + x = (error_window.winfo_screenwidth() // 2) - (error_window.winfo_width() // 2) + y = (error_window.winfo_screenheight() // 2) - (error_window.winfo_height() // 2) + error_window.geometry(f"+{x}+{y}") + + # Contenido + from tkinter import scrolledtext + + text_widget = scrolledtext.ScrolledText( + error_window, + font=("Consolas", 10), + bg="#1e1e1e", + fg="#ff6b6b", + wrap=tk.WORD + ) + text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + text_widget.insert("1.0", error_msg) + text_widget.config(state="disabled") + + # Botones + button_frame = tk.Frame(error_window, bg="#2b2b2b") + button_frame.pack(fill=tk.X, padx=10, pady=5) + + def open_log_folder(): + try: + if platform.system() == "Windows": + os.startfile(log_file.parent) + elif platform.system() == "Darwin": # macOS + subprocess.run(["open", str(log_file.parent)]) + else: # Linux + subprocess.run(["xdg-open", str(log_file.parent)]) + except Exception: + pass + + def copy_log_path(): + error_window.clipboard_clear() + error_window.clipboard_append(str(log_file)) + + tk.Button( + button_frame, + text="Abrir Carpeta de Logs", + command=open_log_folder, + bg="#4fc3f7", + fg="white" + ).pack(side=tk.LEFT, padx=5) + + tk.Button( + button_frame, + text="Copiar Ruta del Log", + command=copy_log_path, + bg="#82aaff", + fg="white" + ).pack(side=tk.LEFT, padx=5) + + tk.Button( + button_frame, + text="Cerrar", + command=error_window.destroy, + bg="#ff6b6b", + fg="white" + ).pack(side=tk.RIGHT, padx=5) + + # Esperar a que se cierre la ventana + error_window.wait_window() + root.destroy() + + except Exception as gui_error: + # Si falla la GUI, mostrar en consola + print("ERROR: No se pudo mostrar ventana de error") + print(error_msg) + print(f"Error adicional: {gui_error}") + + +# Variable global para el logger +logger = None +log_file = None + +def launch_application(): + + try: + # Importar y ejecutar la aplicación + from main_calc_app import HybridCalculatorApp + root = tk.Tk() + + app = HybridCalculatorApp(root) + root.mainloop() + + logger.info("Aplicación cerrada normalmente") + + except ImportError as e: + logger.error(f"Error de importación: {e}") + log_error_with_context(logger, e, "Importación de módulos de la aplicación") + show_error_with_log_info(e, log_file, "Error importando módulos de la aplicación") + except Exception as e: + logger.error(f"Error durante ejecución de la aplicación: {e}") + log_error_with_context(logger, e, "Ejecución de la aplicación principal") + show_error_with_log_info(e, log_file, "Error durante la ejecución de la aplicación") + + +def main(): + """Función principal del launcher""" + global logger, log_file + + # Configurar logging al inicio + try: + logger, log_file = setup_logging() + + except Exception as e: + print(f"ERROR: No se pudo configurar logging: {e}") + # Continuar sin logging si falla + logger = None + log_file = None + + logger.info("Iniciando verificaciones del sistema...") + + try: + + launch_application() + + logger.info("Aplicación cerrada - fin de sesión") + logger.info("=" * 60) + + except KeyboardInterrupt: + logger.info("Aplicación interrumpida por el usuario (Ctrl+C)") + sys.exit(0) + except Exception as e: + logger.error("Error crítico en main():") + log_error_with_context(logger, e, "Función principal del launcher") + show_error_with_log_info(e, log_file, "Error crítico durante el inicio") + sys.exit(1) + + +if __name__ == "__main__": + # Configurar logging básico para manejo de argumentos + temp_logger = None + temp_log_file = None + + # Inicio normal + try: + main() + except Exception as e: + if temp_logger: + log_error_with_context(temp_logger, e, "Error crítico en __main__") + print(f"ERROR CRÍTICO: {e}") + if temp_log_file: + print(f"Ver log completo en: {temp_log_file}") + sys.exit(1) diff --git a/chr_type.py b/chr_type.py index 41e8f7d..78bbe8e 100644 --- a/chr_type.py +++ b/chr_type.py @@ -1,16 +1,16 @@ """ Clase híbrida para caracteres """ -from hybrid_base import HybridCalcType +from sympy_Base import SympyClassBase import re -class HybridChr(HybridCalcType): +class Class_Chr(SympyClassBase): """Clase híbrida para caracteres""" def __new__(cls, str_input): """Crear objeto SymPy válido""" - obj = HybridCalcType.__new__(cls) + obj = SympyClassBase.__new__(cls) return obj def __init__(self, str_input: str): diff --git a/dec_type.py b/dec_type.py index db063da..d231999 100644 --- a/dec_type.py +++ b/dec_type.py @@ -1,16 +1,16 @@ """ Clase híbrida para números decimales """ -from hybrid_base import HybridCalcType +from sympy_Base import SympyClassBase import re -class HybridDec(HybridCalcType): +class Class_Dec(SympyClassBase): """Clase híbrida para números decimales""" def __new__(cls, value_input): """Crear objeto SymPy válido""" - obj = HybridCalcType.__new__(cls) + obj = SympyClassBase.__new__(cls) return obj def __init__(self, value_input): diff --git a/hex_type.py b/hex_type.py index aa1c8ef..2bd3883 100644 --- a/hex_type.py +++ b/hex_type.py @@ -1,16 +1,16 @@ """ Clase híbrida para números hexadecimales """ -from hybrid_base import HybridCalcType +from sympy_Base import SympyClassBase import re -class HybridHex(HybridCalcType): +class Class_Hex(SympyClassBase): """Clase híbrida para números hexadecimales""" def __new__(cls, value_input): """Crear objeto SymPy válido""" - obj = HybridCalcType.__new__(cls) + obj = SympyClassBase.__new__(cls) return obj def __init__(self, value_input): diff --git a/hybrid_calc_history.txt b/hybrid_calc_history.txt index bc8d254..d559ad4 100644 --- a/hybrid_calc_history.txt +++ b/hybrid_calc_history.txt @@ -1,9 +1,17 @@ Hex[FF] -ip=IP4[192.168.1.100/24] +ip=IP4[110.1.30.70/24] + +ip.NetworkAddress() 500/25 Dec[Hex[ff]] -Hex[ff] \ No newline at end of file +Hex[ff] + +Hex[ff].toDecimal() + +IP4[110.1.30.70,255.255.255.0] + +IP4[110.1.30.70,255.255.255.0] \ No newline at end of file diff --git a/hybrid_calc_settings.json b/hybrid_calc_settings.json index dc5d1cb..bf1a2fc 100644 --- a/hybrid_calc_settings.json +++ b/hybrid_calc_settings.json @@ -1,4 +1,4 @@ { - "window_geometry": "1000x700+327+101", - "sash_pos_x": 339 + "window_geometry": "1020x700+2638+160", + "sash_pos_x": 303 } \ No newline at end of file diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..081efe7 Binary files /dev/null and b/icon.png differ diff --git a/ip4_type.py b/ip4_type.py index df6719d..38a21c5 100644 --- a/ip4_type.py +++ b/ip4_type.py @@ -1,17 +1,17 @@ """ Clase híbrida para direcciones IPv4 """ -from hybrid_base import HybridCalcType +from sympy_Base import SympyClassBase from typing import Optional, Tuple import re -class HybridIP4(HybridCalcType): +class Class_IP4(SympyClassBase): """Clase híbrida para direcciones IPv4""" def __new__(cls, *args): """Crear objeto SymPy válido""" - obj = HybridCalcType.__new__(cls) + obj = SympyClassBase.__new__(cls) return obj def __init__(self, *args): @@ -148,7 +148,7 @@ class HybridIP4(HybridCalcType): ] network_str = '.'.join(str(x) for x in parts) - return HybridIP4(f"{network_str}/{self._prefix}") + return Class_IP4(f"{network_str}/{self._prefix}") def BroadcastAddress(self): """Obtiene la dirección de broadcast""" @@ -170,7 +170,7 @@ class HybridIP4(HybridCalcType): ] broadcast_str = '.'.join(str(x) for x in parts) - return HybridIP4(f"{broadcast_str}/{self._prefix}") + return Class_IP4(f"{broadcast_str}/{self._prefix}") def Nodes(self): """Obtiene el número de nodos disponibles""" diff --git a/main_calc.py b/main_calc.py deleted file mode 100644 index 3294791..0000000 --- a/main_calc.py +++ /dev/null @@ -1,624 +0,0 @@ -#!/usr/bin/env python3 -""" -Launcher principal para Calculadora MAV - CAS Híbrido -Este script maneja la inicialización y ejecución de la aplicación -""" -import sys -import os -import subprocess -import tkinter as tk -from tkinter import messagebox -from pathlib import Path -import importlib.util -import logging -import datetime -import traceback -import platform - - -import platform - - -def setup_logging(): - """Configura el sistema de logging completo""" - log_dir = Path("logs") - log_dir.mkdir(exist_ok=True) - - # Archivo de log con timestamp - timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") - log_file = log_dir / f"mav_calc_{timestamp}.log" - - # Configurar logging - logging.basicConfig( - level=logging.DEBUG, - format='%(asctime)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s', - handlers=[ - logging.FileHandler(log_file, encoding='utf-8'), - logging.StreamHandler(sys.stdout) - ] - ) - - logger = logging.getLogger(__name__) - - # Log información del sistema - logger.info("=" * 60) - logger.info("CALCULADORA MAV - CAS HÍBRIDO - INICIO DE SESIÓN") - logger.info("=" * 60) - logger.info(f"Timestamp: {datetime.datetime.now()}") - logger.info(f"Python: {sys.version}") - logger.info(f"Plataforma: {platform.platform()}") - logger.info(f"Directorio de trabajo: {Path.cwd()}") - logger.info(f"Argumentos: {sys.argv}") - logger.info(f"Archivo de log: {log_file}") - logger.info("-" * 60) - - return logger, log_file - - -def log_system_info(logger): - """Registra información detallada del sistema""" - try: - logger.info("INFORMACIÓN DEL SISTEMA:") - logger.info(f" OS: {platform.system()} {platform.release()}") - logger.info(f" Arquitectura: {platform.machine()}") - logger.info(f" Procesador: {platform.processor()}") - logger.info(f" Python executable: {sys.executable}") - logger.info(f" Python path: {sys.path[:3]}...") # Solo primeros 3 elementos - - # Información de memoria si está disponible - try: - import psutil - memory = psutil.virtual_memory() - logger.info(f" RAM total: {memory.total // (1024**3)} GB") - logger.info(f" RAM disponible: {memory.available // (1024**3)} GB") - except ImportError: - logger.info(" Información de memoria: No disponible (psutil no instalado)") - - except Exception as e: - logger.error(f"Error obteniendo información del sistema: {e}") - - -def log_error_with_context(logger, error, context=""): - """Registra un error con contexto completo""" - logger.error("=" * 50) - logger.error("ERROR DETECTADO") - logger.error("=" * 50) - if context: - logger.error(f"Contexto: {context}") - logger.error(f"Tipo de error: {type(error).__name__}") - logger.error(f"Mensaje: {str(error)}") - logger.error("Traceback completo:") - logger.error(traceback.format_exc()) - logger.error("Variables locales en el momento del error:") - - # Intentar capturar variables locales del frame donde ocurrió el error - try: - tb = traceback.extract_tb(error.__traceback__) - if tb: - last_frame = tb[-1] - logger.error(f" Archivo: {last_frame.filename}") - logger.error(f" Línea: {last_frame.lineno}") - logger.error(f" Función: {last_frame.name}") - logger.error(f" Código: {last_frame.line}") - except Exception as frame_error: - logger.error(f"No se pudieron obtener detalles del frame: {frame_error}") - - logger.error("=" * 50) - - -def show_error_with_log_info(error, log_file, context=""): - """Muestra error al usuario con información del log""" - error_msg = f"""Error en Calculadora MAV: - -{context} - -Error: {type(error).__name__}: {str(error)} - -INFORMACIÓN DE DEBUGGING: -• Log completo guardado en: {log_file} -• Para soporte, enviar el archivo de log -• Timestamp: {datetime.datetime.now()} - -¿Qué hacer ahora? -1. Revisar el archivo de log para más detalles -2. Intentar reiniciar la aplicación -3. Verificar dependencias con: python launcher.py --setup -4. Ejecutar tests con: python launcher.py --test -""" - - try: - root = tk.Tk() - root.withdraw() - - # Crear ventana de error personalizada - error_window = tk.Toplevel(root) - error_window.title("Error - Calculadora MAV") - error_window.geometry("600x400") - error_window.configure(bg="#2b2b2b") - - # Hacer la ventana modal - error_window.transient(root) - error_window.grab_set() - - # Centrar ventana - error_window.update_idletasks() - x = (error_window.winfo_screenwidth() // 2) - (error_window.winfo_width() // 2) - y = (error_window.winfo_screenheight() // 2) - (error_window.winfo_height() // 2) - error_window.geometry(f"+{x}+{y}") - - # Contenido - from tkinter import scrolledtext - - text_widget = scrolledtext.ScrolledText( - error_window, - font=("Consolas", 10), - bg="#1e1e1e", - fg="#ff6b6b", - wrap=tk.WORD - ) - text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) - text_widget.insert("1.0", error_msg) - text_widget.config(state="disabled") - - # Botones - button_frame = tk.Frame(error_window, bg="#2b2b2b") - button_frame.pack(fill=tk.X, padx=10, pady=5) - - def open_log_folder(): - try: - if platform.system() == "Windows": - os.startfile(log_file.parent) - elif platform.system() == "Darwin": # macOS - subprocess.run(["open", str(log_file.parent)]) - else: # Linux - subprocess.run(["xdg-open", str(log_file.parent)]) - except Exception: - pass - - def copy_log_path(): - error_window.clipboard_clear() - error_window.clipboard_append(str(log_file)) - - tk.Button( - button_frame, - text="Abrir Carpeta de Logs", - command=open_log_folder, - bg="#4fc3f7", - fg="white" - ).pack(side=tk.LEFT, padx=5) - - tk.Button( - button_frame, - text="Copiar Ruta del Log", - command=copy_log_path, - bg="#82aaff", - fg="white" - ).pack(side=tk.LEFT, padx=5) - - tk.Button( - button_frame, - text="Cerrar", - command=error_window.destroy, - bg="#ff6b6b", - fg="white" - ).pack(side=tk.RIGHT, padx=5) - - # Esperar a que se cierre la ventana - error_window.wait_window() - root.destroy() - - except Exception as gui_error: - # Si falla la GUI, mostrar en consola - print("ERROR: No se pudo mostrar ventana de error") - print(error_msg) - print(f"Error adicional: {gui_error}") - - -# Variable global para el logger -logger = None -log_file = None - - -def check_dependencies(): - """Verifica que todas las dependencias estén disponibles""" - logger.info("Verificando dependencias...") - - required_modules = { - 'sympy': 'SymPy (motor algebraico)', - 'matplotlib': 'Matplotlib (plotting)', - 'numpy': 'NumPy (cálculos numéricos)', - 'tkinter': 'Tkinter (interfaz gráfica)' - } - - missing_modules = [] - - for module, description in required_modules.items(): - try: - if module == 'tkinter': - import tkinter - logger.info(f"✅ {module} - {description}") - else: - importlib.import_module(module) - logger.info(f"✅ {module} - {description}") - except ImportError as e: - missing_modules.append((module, description)) - logger.error(f"❌ {module} - {description} - Error: {e}") - - if missing_modules: - logger.warning(f"Módulos faltantes: {[m[0] for m in missing_modules]}") - else: - logger.info("Todas las dependencias están disponibles") - - return missing_modules - - -def install_missing_dependencies(missing_modules): - """Intenta instalar dependencias faltantes""" - logger.info("Intentando instalar dependencias faltantes...") - - installable_modules = [ - ('sympy', 'sympy>=1.12'), - ('matplotlib', 'matplotlib>=3.7.0'), - ('numpy', 'numpy>=1.24.0') - ] - - modules_to_install = [] - for module_name, _ in missing_modules: - for inst_name, inst_package in installable_modules: - if module_name == inst_name: - modules_to_install.append(inst_package) - break - - if not modules_to_install: - logger.info("No hay módulos para instalar automáticamente") - return True - - logger.info(f"Instalando: {', '.join(modules_to_install)}") - - try: - cmd = [sys.executable, "-m", "pip", "install"] + modules_to_install - logger.info(f"Ejecutando comando: {' '.join(cmd)}") - - result = subprocess.run( - cmd, - capture_output=True, - text=True, - timeout=300 # 5 minutos timeout - ) - - logger.info(f"Código de salida pip: {result.returncode}") - logger.info(f"Stdout pip: {result.stdout}") - - if result.stderr: - logger.warning(f"Stderr pip: {result.stderr}") - - return result.returncode == 0 - - except subprocess.TimeoutExpired: - logger.error("Timeout durante instalación de dependencias") - return False - except Exception as e: - logger.error(f"Error durante instalación: {e}") - log_error_with_context(logger, e, "Instalación de dependencias") - return False - - -def show_dependency_error(missing_modules): - """Muestra error de dependencias faltantes""" - error_msg = "Dependencias faltantes:\n\n" - for module, description in missing_modules: - error_msg += f"• {module}: {description}\n" - - error_msg += "\nPara instalar dependencias:\n" - error_msg += "pip install sympy matplotlib numpy\n\n" - - if any(module == 'tkinter' for module, _ in missing_modules): - error_msg += "Para tkinter en Linux:\n" - error_msg += "sudo apt-get install python3-tk\n" - - error_msg += "\nO ejecute: python launcher.py --setup" - - logger.error("DEPENDENCIAS FALTANTES:") - for module, description in missing_modules: - logger.error(f" • {module}: {description}") - - show_error_with_log_info( - Exception("Dependencias faltantes"), - log_file, - "Faltan dependencias requeridas para ejecutar la aplicación" - ) - - -def check_app_files(): - """Verifica que todos los archivos de la aplicación estén presentes""" - logger.info("Verificando archivos de la aplicación...") - - required_files = [ - 'tl_bracket_parser.py', - 'hybrid_base.py', - 'ip4_type.py', - 'hex_type.py', - 'bin_type.py', - 'dec_type.py', - 'chr_type.py', - 'main_evaluation.py', - 'tl_popup.py', - 'main_calc_app.py' - ] - - current_dir = Path(__file__).parent - missing_files = [] - - for filename in required_files: - file_path = current_dir / filename - if file_path.exists(): - logger.info(f"✅ {filename} - Tamaño: {file_path.stat().st_size} bytes") - else: - missing_files.append(filename) - logger.error(f"❌ {filename} - No encontrado") - - if missing_files: - logger.error(f"Archivos faltantes: {missing_files}") - else: - logger.info("Todos los archivos de la aplicación están presentes") - - return missing_files - - -def show_file_error(missing_files): - """Muestra error de archivos faltantes""" - error_msg = "Archivos de aplicación faltantes:\n\n" - for filename in missing_files: - error_msg += f"• {filename}\n" - - error_msg += "\nAsegúrese de tener todos los archivos del proyecto." - - logger.error("ARCHIVOS FALTANTES:") - for filename in missing_files: - logger.error(f" • {filename}") - - show_error_with_log_info( - Exception("Archivos faltantes"), - log_file, - "Faltan archivos necesarios para la aplicación" - ) - - -def launch_application(): - """Lanza la aplicación principal""" - logger.info("Iniciando aplicación principal...") - - try: - # Importar y ejecutar la aplicación - logger.info("Importando módulo principal...") - from main_calc_app import HybridCalculatorApp - - logger.info("Creando ventana principal...") - root = tk.Tk() - - logger.info("Inicializando aplicación...") - app = HybridCalculatorApp(root) - - logger.info("✅ Calculadora MAV - CAS Híbrido iniciada correctamente") - logger.info("Iniciando loop principal de tkinter...") - - root.mainloop() - - logger.info("Aplicación cerrada normalmente") - - except ImportError as e: - logger.error(f"Error de importación: {e}") - log_error_with_context(logger, e, "Importación de módulos de la aplicación") - show_error_with_log_info(e, log_file, "Error importando módulos de la aplicación") - except Exception as e: - logger.error(f"Error durante ejecución de la aplicación: {e}") - log_error_with_context(logger, e, "Ejecución de la aplicación principal") - show_error_with_log_info(e, log_file, "Error durante la ejecución de la aplicación") - - -def main(): - """Función principal del launcher""" - global logger, log_file - - # Configurar logging al inicio - try: - logger, log_file = setup_logging() - log_system_info(logger) - except Exception as e: - print(f"ERROR: No se pudo configurar logging: {e}") - # Continuar sin logging si falla - logger = None - log_file = None - - logger.info("Iniciando verificaciones del sistema...") - - try: - # Verificar archivos de aplicación - logger.info("Verificando archivos de aplicación...") - missing_files = check_app_files() - if missing_files: - logger.error("❌ Archivos faltantes detectados") - show_file_error(missing_files) - logger.info("Cerrando debido a archivos faltantes") - sys.exit(1) - - logger.info("✅ Todos los archivos están presentes") - - # Verificar dependencias - logger.info("Verificando dependencias...") - missing_deps = check_dependencies() - if missing_deps: - logger.warning("❌ Dependencias faltantes detectadas") - - # Intentar instalación automática para módulos pip - installable_deps = [ - (module, desc) for module, desc in missing_deps - if module in ['sympy', 'matplotlib', 'numpy'] - ] - - if installable_deps: - logger.info("Preguntando al usuario sobre instalación automática...") - response = input("¿Instalar dependencias automáticamente? (s/n): ").lower().strip() - logger.info(f"Respuesta del usuario: {response}") - - if response in ['s', 'si', 'y', 'yes']: - logger.info("Iniciando instalación automática...") - if install_missing_dependencies(installable_deps): - logger.info("✅ Dependencias instaladas correctamente") - # Re-verificar después de instalación - missing_deps = check_dependencies() - else: - logger.error("❌ Error durante instalación automática") - else: - logger.info("Usuario declinó instalación automática") - - if missing_deps: - logger.error("Dependencias aún faltantes, mostrando error al usuario") - show_dependency_error(missing_deps) - logger.info("Cerrando debido a dependencias faltantes") - sys.exit(1) - - logger.info("✅ Todas las dependencias disponibles") - - # Splash screen eliminado - logger.info("Splash screen ha sido eliminado.") - - # Lanzar aplicación - logger.info("Lanzando aplicación principal...") - launch_application() - - logger.info("Aplicación cerrada - fin de sesión") - logger.info("=" * 60) - - except KeyboardInterrupt: - logger.info("Aplicación interrumpida por el usuario (Ctrl+C)") - sys.exit(0) - except Exception as e: - logger.error("Error crítico en main():") - log_error_with_context(logger, e, "Función principal del launcher") - show_error_with_log_info(e, log_file, "Error crítico durante el inicio") - sys.exit(1) - - -def show_help(): - """Muestra ayuda del launcher""" - help_text = """ -Calculadora MAV - CAS Híbrido Launcher - -Uso: python launcher.py [opciones] - -Opciones: - --help Muestra esta ayuda - --no-splash Inicia sin splash screen - --test Ejecuta tests antes de iniciar - --setup Ejecuta setup de dependencias - --debug Activa logging detallado - -Descripción: - Sistema de álgebra computacional híbrido que combina - SymPy con clases especializadas para networking, - programación y cálculos numéricos. - -Dependencias: - - Python 3.8+ - - SymPy (motor algebraico) - - Matplotlib (plotting) - - NumPy (cálculos numéricos) - - Tkinter (interfaz gráfica) - -Logging: - - Los logs se guardan automáticamente en: ./logs/ - - Cada ejecución genera un archivo con timestamp - - En caso de error, se muestra la ubicación del log - - Los logs incluyen información del sistema y debugging - -Resolución de problemas: - 1. Revisar logs en ./logs/ para errores detallados - 2. Ejecutar: python launcher.py --test - 3. Verificar dependencias: python launcher.py --setup - 4. Modo debug: python launcher.py --debug - -Para más información, consulte la documentación. -""" - print(help_text) - - -if __name__ == "__main__": - # Configurar logging básico para manejo de argumentos - temp_logger = None - temp_log_file = None - - try: - temp_logger, temp_log_file = setup_logging() - temp_logger.info("Procesando argumentos de línea de comandos...") - except: - pass # Si falla el logging, continuar sin él - - # Manejar argumentos de línea de comandos - if "--help" in sys.argv or "-h" in sys.argv: - if temp_logger: - temp_logger.info("Mostrando ayuda") - show_help() - sys.exit(0) - - if "--test" in sys.argv: - if temp_logger: - temp_logger.info("Ejecutando tests...") - try: - from test_suite import run_all_tests - if not run_all_tests(): - error_msg = "❌ Tests fallaron - no se iniciará la aplicación" - print(error_msg) - if temp_logger: - temp_logger.error(error_msg) - sys.exit(1) - success_msg = "✅ Tests pasaron - iniciando aplicación" - print(success_msg) - if temp_logger: - temp_logger.info(success_msg) - except ImportError as e: - warning_msg = "⚠️ Tests no disponibles - continuando con inicio" - print(warning_msg) - if temp_logger: - temp_logger.warning(f"{warning_msg} - Error: {e}") - except Exception as e: - if temp_logger: - log_error_with_context(temp_logger, e, "Ejecución de tests") - print(f"❌ Error ejecutando tests: {e}") - sys.exit(1) - - if "--setup" in sys.argv: - if temp_logger: - temp_logger.info("Ejecutando setup...") - try: - import setup_script - setup_script.main() - sys.exit(0) - except ImportError: - error_msg = "❌ Setup script no disponible" - print(error_msg) - if temp_logger: - temp_logger.error(error_msg) - sys.exit(1) - except Exception as e: - if temp_logger: - log_error_with_context(temp_logger, e, "Ejecución de setup") - print(f"❌ Error en setup: {e}") - sys.exit(1) - - # Logging adicional para debug - if "--debug" in sys.argv: - if temp_logger: - temp_logger.setLevel(logging.DEBUG) - temp_logger.info("Modo debug activado") - - # Inicio normal - try: - main() - except Exception as e: - if temp_logger: - log_error_with_context(temp_logger, e, "Error crítico en __main__") - print(f"ERROR CRÍTICO: {e}") - if temp_log_file: - print(f"Ver log completo en: {temp_log_file}") - sys.exit(1) diff --git a/main_calc_app.py b/main_calc_app.py index a896df7..ef5a71b 100644 --- a/main_calc_app.py +++ b/main_calc_app.py @@ -6,6 +6,7 @@ from tkinter import scrolledtext, messagebox, Menu, filedialog import tkinter.font as tkFont import json import os +from pathlib import Path # Added for robust path handling import threading from typing import List, Dict, Any, Optional import re @@ -13,13 +14,13 @@ import re # Importar componentes del CAS híbrido from main_evaluation import HybridEvaluationEngine, EvaluationResult from tl_popup import InteractiveResultManager -from ip4_type import HybridIP4 as IP4 -from hex_type import HybridHex as Hex -from bin_type import HybridBin as Bin -from dec_type import HybridDec as Dec -from chr_type import HybridChr as Chr +from ip4_type import Class_IP4 +from hex_type import Class_Hex +from bin_type import Class_Bin +from dec_type import Class_Dec +from chr_type import Class_Chr import sympy -from sympy_helper import Helper as SympyHelper +from sympy_helper import SympyTools as SympyHelper class HybridCalculatorApp: @@ -29,12 +30,12 @@ class HybridCalculatorApp: HISTORY_FILE = "hybrid_calc_history.txt" HELPERS = [ - IP4.Helper, - Hex.Helper, - Bin.Helper, - Dec.Helper, - Chr.Helper, - SympyHelper, + Class_IP4.Helper, + Class_Hex.Helper, + Class_Bin.Helper, + Class_Dec.Helper, + Class_Chr.Helper, + SympyHelper.Helper, ] def __init__(self, root: tk.Tk): @@ -70,10 +71,23 @@ class HybridCalculatorApp: def _setup_icon(self): """Configura el ícono de la aplicación""" try: - self.app_icon = tk.PhotoImage(file="icon.png") + # Construct path relative to this script file (main_calc_app.py) + script_dir = Path(__file__).resolve().parent + icon_path = script_dir / "icon.png" + + if not icon_path.is_file(): + print(f"Advertencia: Archivo de ícono no encontrado en '{icon_path}'.") + # Optionally, set a default Tk icon or simply return + return + + self.app_icon = tk.PhotoImage(file=str(icon_path)) self.root.iconphoto(True, self.app_icon) - except tk.TclError: - print("Advertencia: No se pudo cargar 'icon.png' como ícono.") + except tk.TclError as e: + # Provide more specific error, including the path and Tkinter's error message + print(f"Advertencia: No se pudo cargar el ícono desde '{icon_path}'. Error de Tkinter: {e}") + except Exception as e: + # Catch other potential errors during icon loading + print(f"Advertencia: Ocurrió un error inesperado al cargar el ícono desde '{icon_path}': {e}") def _load_settings(self) -> Dict[str, Any]: """Carga configuración de la aplicación""" @@ -261,6 +275,8 @@ class HybridCalculatorApp: self.output_text.tag_configure("ip", foreground="#fff176") self.output_text.tag_configure("date", foreground="#ff8a80") self.output_text.tag_configure("chr_type", foreground="#80cbc4") + # Agregar tag para ayuda contextual + self.output_text.tag_configure("helper", foreground="#ffd700", font=("Consolas", 11, "italic")) def on_key_release(self, event=None): """Maneja eventos de teclado""" @@ -279,9 +295,58 @@ class HybridCalculatorApp: cursor_index_str = self.input_text.index(tk.INSERT) line_num_str, char_num_str = cursor_index_str.split('.') current_line_num = int(line_num_str) - char_idx_of_dot = int(char_num_str) - obj_expr_str = self.input_text.get(f"{current_line_num}.0", f"{current_line_num}.{char_idx_of_dot -1}").strip() + char_idx_after_dot = int(char_num_str) + + if char_idx_after_dot == 0: # Should not happen if a dot was typed + print("DEBUG: _handle_dot_autocomplete called with cursor at beginning of line somehow.") + return + + text_before_dot = self.input_text.get(f"{current_line_num}.0", f"{current_line_num}.{char_idx_after_dot - 1}") + + # Si no hay nada o solo espacios antes del punto, ofrecer sugerencias globales + if not text_before_dot.strip(): + print("DEBUG: Dot on empty line or after spaces. Offering global suggestions.") + suggestions = [] + custom_classes_suggestions = [ + ("Hex", "Tipo Hexadecimal. Ej: Hex[FF]"), + ("Bin", "Tipo Binario. Ej: Bin[1010]"), + ("Dec", "Tipo Decimal. Ej: Dec[42]"), + ("IP4", "Tipo Dirección IPv4. Ej: IP4[1.2.3.4/24]"), + ("Chr", "Tipo Carácter. Ej: Chr[A]"), + ] + suggestions.extend(custom_classes_suggestions) + + try: + sympy_functions = SympyHelper.PopupFunctionList() + if sympy_functions: + suggestions.extend(sympy_functions) + except Exception as e: + print(f"DEBUG: Error calling SympyHelper.PopupFunctionList() for global: {e}") + + if suggestions: + self._show_autocomplete_popup(suggestions, is_global_popup=True) + return + + # Si hay texto antes del punto, es para autocompletado de métodos de un objeto + obj_expr_str = text_before_dot.strip() + print(f"DEBUG: Autocomplete triggered for object. Expression: '{obj_expr_str}'") + if not obj_expr_str: + # Esto no debería ocurrir si la lógica anterior para popup global es correcta + print("DEBUG: Object expression is empty. No autocomplete.") + return + + # Caso especial para el módulo sympy + if obj_expr_str == "sympy": + print(f"DEBUG: Detected 'sympy.', using SympyHelper for suggestions.") + try: + methods = SympyHelper.PopupFunctionList() + if methods: + self._show_autocomplete_popup(methods, is_global_popup=False) + else: + print(f"DEBUG: SympyHelper.PopupFunctionList returned no methods.") + except Exception as e: + print(f"DEBUG: Error calling SympyHelper.PopupFunctionList(): {e}") return # Preprocesar para convertir sintaxis de corchetes a llamada de clase @@ -289,24 +354,28 @@ class HybridCalculatorApp: bracket_match = re.match(r"([A-Za-z_][A-Za-z0-9_]*)\[(.*)\]$", obj_expr_str) if bracket_match: class_name, arg = bracket_match.groups() - # Si el argumento es un número, no poner comillas if arg.isdigit(): obj_expr_str = f"{class_name}({arg})" else: obj_expr_str = f"{class_name}('{arg}')" + print(f"DEBUG: Preprocessed bracket syntax to: '{obj_expr_str}'") eval_context = self.engine._get_full_context() if hasattr(self.engine, '_get_full_context') else {} obj = None try: + print(f"DEBUG: Attempting to eval: '{obj_expr_str}'") obj = eval(obj_expr_str, eval_context) - except Exception: + print(f"DEBUG: Eval successful. Object: {type(obj)}, Value: {obj}") + except Exception as e: + print(f"DEBUG: Error evaluating object expression '{obj_expr_str}': {e}") return + if obj is not None and hasattr(obj, 'PopupFunctionList'): methods = obj.PopupFunctionList() if methods: - self._show_autocomplete_popup(methods) + self._show_autocomplete_popup(methods, is_global_popup=False) - def _show_autocomplete_popup(self, suggestions): + def _show_autocomplete_popup(self, suggestions, is_global_popup=False): # suggestions: lista de tuplas (nombre, hint) cursor_bbox = self.input_text.bbox(tk.INSERT) if not cursor_bbox: @@ -330,18 +399,29 @@ class HybridCalculatorApp: self._autocomplete_listbox.select_set(0) self._autocomplete_listbox.pack(expand=True, fill=tk.BOTH) self._autocomplete_listbox.bind("", self._on_autocomplete_select) + self._autocomplete_listbox.bind("", self._on_autocomplete_select) self._autocomplete_listbox.bind("", lambda e: self._close_autocomplete_popup()) - self._autocomplete_listbox.bind("", self._on_autocomplete_select) + self._autocomplete_listbox.bind("", lambda e, g=is_global_popup: self._on_autocomplete_select(e, is_global=g)) self._autocomplete_listbox.focus_set() self._autocomplete_listbox.bind("", lambda e: self._navigate_autocomplete(e, -1)) self._autocomplete_listbox.bind("", lambda e: self._navigate_autocomplete(e, 1)) - self.input_text.bind("", lambda e: self._close_autocomplete_popup(), add=True) + + # self.input_text.bind("", lambda e: self._close_autocomplete_popup(), add=True) # Removed: Caused popup to close immediately self.input_text.bind("", lambda e: self._close_autocomplete_popup(), add=True) self.root.bind("", lambda e: self._close_autocomplete_popup(), add=True) - self.input_text.bind("", lambda e: self._close_autocomplete_popup(), add=True) + # self.input_text.bind("", lambda e: self._close_autocomplete_popup(), add=True) # Removed: Too aggressive + + # Pasar el flag is_global_popup a los bindings que llaman a _on_autocomplete_select + self._autocomplete_listbox.bind("", lambda e, g=is_global_popup: self._on_autocomplete_select(e, is_global=g)) + self._autocomplete_listbox.bind("", lambda e, g=is_global_popup: self._on_autocomplete_select(e, is_global=g)) + max_len = max(len(name) for name, _ in suggestions) if suggestions else 10 width = max(15, min(max_len + 10, 50)) height = min(len(suggestions), 10) + # Calcular el ancho basado en el texto completo que se muestra en el listbox + full_text_suggestions = [f"{name} — {hint}" for name, hint in suggestions] + max_full_len = max(len(text) for text in full_text_suggestions) if full_text_suggestions else 20 + width = max(20, min(max_full_len + 5, 80)) # Ajustar el +5 y el límite 80 según sea necesario self._autocomplete_listbox.config(width=width, height=height) else: self._close_autocomplete_popup() @@ -363,17 +443,39 @@ class HybridCalculatorApp: self._autocomplete_listbox.see(new_idx) return "break" - def _on_autocomplete_select(self, event): + def _on_autocomplete_select(self, event, is_global=False): if not hasattr(self, '_autocomplete_listbox') or not self._autocomplete_listbox: return "break" selection = self._autocomplete_listbox.curselection() if not selection: self._close_autocomplete_popup() return "break" - selected = self._autocomplete_listbox.get(selection[0]) - method_name = selected.split()[0] - self.input_text.insert(tk.INSERT, method_name + "()") - self.input_text.mark_set(tk.INSERT, f"{tk.INSERT}-1c") # Cursor dentro de los paréntesis + + selected_text_with_hint = self._autocomplete_listbox.get(selection[0]) + # Extraer solo el nombre del ítem, antes de " —" + item_name = selected_text_with_hint.split(" —")[0].strip() + + if is_global: + # Eliminar el punto que activó el popup y luego insertar el nombre + cursor_pos_str = self.input_text.index(tk.INSERT) # Posición actual (después del punto) + line_num, char_num = map(int, cursor_pos_str.split('.')) + + # El punto está en char_num - 1 en la línea actual + dot_pos_on_line = char_num - 1 + dot_index_str = f"{line_num}.{dot_pos_on_line}" + + self.input_text.delete(dot_index_str) + + # Insertar el nombre de la función/clase seguido de "()" + insert_text = item_name + "()" + self.input_text.insert(dot_index_str, insert_text) + # Colocar cursor dentro de los paréntesis: después del nombre y el '(' + self.input_text.mark_set(tk.INSERT, f"{dot_index_str}+{len(item_name)+1}c") + else: + # Comportamiento existente para métodos de objeto + self.input_text.insert(tk.INSERT, item_name + "()") + self.input_text.mark_set(tk.INSERT, f"{tk.INSERT}-1c") # Cursor dentro de los paréntesis + self._close_autocomplete_popup() self.input_text.focus_set() self.on_key_release() # Trigger re-evaluation @@ -383,6 +485,17 @@ class HybridCalculatorApp: if hasattr(self, '_autocomplete_popup') and self._autocomplete_popup: self._autocomplete_popup.destroy() self._autocomplete_popup = None + # Consider unbinding the Button-1 events if they were stored with IDs, + # for now, their guard condition `if self._autocomplete_popup:` handles multiple calls. + # Example of how to unbind if IDs were stored: + # if hasattr(self, '_input_text_b1_bind_id'): + # self.input_text.unbind("", self._input_text_b1_bind_id) + # del self._input_text_b1_bind_id + # if hasattr(self, '_root_b1_bind_id'): + # self.root.unbind("", self._root_b1_bind_id) + # del self._root_b1_bind_id + + if hasattr(self, '_autocomplete_listbox') and self._autocomplete_listbox: self._autocomplete_listbox = None def _evaluate_and_update(self): @@ -428,7 +541,11 @@ class HybridCalculatorApp: if result.is_error: ayuda = self.obtener_ayuda(result.original_line) if ayuda: - output_parts.append(("helper", ayuda)) + # Mostrar ayuda en un solo renglón, truncando si es necesario + ayuda_linea = ayuda.replace("\n", " ").replace("\r", " ") + if len(ayuda_linea) > 120: + ayuda_linea = ayuda_linea[:117] + "..." + output_parts.append(("helper", ayuda_linea)) else: output_parts.append(("error", f"Error: {result.error}")) elif result.result_type == "comment": @@ -445,9 +562,7 @@ class HybridCalculatorApp: # Verificar si es resultado interactivo if self.interactive_manager and result.is_interactive: - interactive_tag, display_text = self.interactive_manager.create_interactive_tag( - result.result, self.output_text, "1.0" - ) + interactive_tag, display_text = self.interactive_manager.create_interactive_tag(result.result, self.output_text, "1.0") if interactive_tag: output_parts.append((interactive_tag, display_text)) else: @@ -467,13 +582,13 @@ class HybridCalculatorApp: def _get_result_tag(self, result: Any) -> str: """Determina el tag de color para un resultado""" - if isinstance(result, Hex): + if isinstance(result, Class_Hex): return "hex" - elif isinstance(result, Bin): + elif isinstance(result, Class_Bin): return "bin" - elif isinstance(result, IP4): + elif isinstance(result, Class_IP4): return "ip" - elif isinstance(result, Chr): + elif isinstance(result, Class_Chr): return "chr_type" elif isinstance(result, sympy.Basic): return "symbolic" diff --git a/main_evaluation.py b/main_evaluation.py index 46541d1..b367d86 100644 --- a/main_evaluation.py +++ b/main_evaluation.py @@ -10,12 +10,12 @@ from contextlib import contextmanager from tl_bracket_parser import BracketParser from tl_popup import PlotResult -from hybrid_base import HybridCalcType -from ip4_type import HybridIP4 as IP4 -from hex_type import HybridHex as Hex -from bin_type import HybridBin as Bin -from dec_type import HybridDec as Dec -from chr_type import HybridChr as Chr +from sympy_Base import SympyClassBase +from ip4_type import Class_IP4 as Class_IP4 +from hex_type import Class_Hex as Class_Hex +from bin_type import Class_Bin as Class_Bin +from dec_type import Class_Dec as Class_Dec +from chr_type import Class_Chr as Class_Chr class HybridEvaluationEngine: @@ -85,17 +85,17 @@ class HybridEvaluationEngine: # Clases especializadas specialized_classes = { - 'Hex': Hex, - 'Bin': Bin, - 'Dec': Dec, - 'IP4': IP4, - 'Chr': Chr, + 'Hex': Class_Hex, + 'Bin': Class_Bin, + 'Dec': Class_Dec, + 'IP4': Class_IP4, + 'Chr': Class_Chr, # Alias en minúsculas - 'hex': Hex, - 'bin': Bin, - 'dec': Dec, - 'ip4': IP4, - 'chr': Chr, + 'hex': Class_Hex, + 'bin': Class_Bin, + 'dec': Class_Dec, + 'ip4': Class_IP4, + 'chr': Class_Chr, } # Funciones de utilidad @@ -314,7 +314,7 @@ class HybridEvaluationEngine: result = eval(expression, {"__builtins__": {}}, context) # Si el resultado es un objeto híbrido, integrarlo con SymPy si es necesario - if isinstance(result, HybridCalcType): + if isinstance(result, SympyClassBase): return result elif isinstance(result, PlotResult): if self.debug: diff --git a/hybrid_base.py b/sympy_Base.py similarity index 99% rename from hybrid_base.py rename to sympy_Base.py index bf1274f..f0a49b1 100644 --- a/hybrid_base.py +++ b/sympy_Base.py @@ -7,7 +7,7 @@ from typing import Any, Optional, Dict import re -class HybridCalcType(Basic): +class SympyClassBase(Basic): """ Clase base híbrida que combina SymPy Basic con funcionalidad de calculadora Todas las clases especializadas deben heredar de esta diff --git a/sympy_helper.py b/sympy_helper.py index f81a370..7615c9f 100644 --- a/sympy_helper.py +++ b/sympy_helper.py @@ -1,25 +1,59 @@ import re -def Helper(input_str): - """Ayuda contextual para funciones SymPy comunes""" - sympy_funcs = { +class SympyTools: + """ + Utilidades para la integración con SymPy, incluyendo ayuda contextual + y listas de funciones para autocompletado. + """ + _sympy_func_details = { "diff": "Derivada: diff(expr, var). Ej: diff(sin(x), x)", "integrate": "Integral: integrate(expr, var). Ej: integrate(x**2, x)", "solve": "Resolver ecuaciones: solve(expr, var). Ej: solve(x**2-1, x)", "limit": "Límite: limit(expr, var, valor). Ej: limit(sin(x)/x, x, 0)", "series": "Serie de Taylor: series(expr, var, punto, n). Ej: series(exp(x), x, 0, 5)", "Matrix": "Matrices: Matrix([[a, b], [c, d]]). Ej: Matrix([[1,2],[3,4]])", - "plot": "Gráfica: plot(expr, (var, a, b)). Ej: plot(sin(x), (x, 0, 2*pi))", + "plot": "Gráfica: plot(expr, (var, a, b)). Ej: plot(sin(x), (x, -2*pi, 2*pi))", # Corregido ejemplo de plot "plot3d": "Gráfica 3D: plot3d(expr, (x, a, b), (y, c, d))", "simplify": "Simplificar: simplify(expr). Ej: simplify((x**2 + 2*x + 1))", "expand": "Expandir: expand(expr). Ej: expand((x+1)**2)", "factor": "Factorizar: factor(expr). Ej: factor(x**2 + 2*x + 1)", "collect": "Agrupar: collect(expr, var). Ej: collect(x*y + x, x)", "cancel": "Cancelar: cancel(expr). Ej: cancel((x**2 + 2*x + 1)/(x+1))", - "apart": "Fracciones parciales: apart(expr, var). Ej: apart(1/(x*(x+1)), x)", + "apart": "Fracciones parciales: apart(expr). Ej: apart(1/(x*(x+1)))", # Var opcional en apart "together": "Unir fracciones: together(expr). Ej: together(1/x + 1/y)", } - for func, ayuda in sympy_funcs.items(): - if input_str.strip().startswith(func): - return ayuda - return None \ No newline at end of file + + @staticmethod + def Helper(input_str: str) -> str | None: + """ + Ayuda contextual para funciones SymPy comunes. + Se activa si alguna palabra en input_str es un prefijo de un nombre de función conocido. + """ + cleaned_input = input_str.strip() + if not cleaned_input: + return None + + # Extraer palabras (posibles identificadores de función) de la entrada + words_in_input = re.findall(r'[a-zA-Z_][a-zA-Z0-9_]*', cleaned_input) + if not words_in_input: + return None + + # Iterar sobre las funciones de SymPy definidas + for func_name, help_text in SympyTools._sympy_func_details.items(): + # Comprobar si alguna palabra de la entrada es un prefijo del nombre de la función + for word in words_in_input: + # len(word) > 0 para cumplir "no hay minimo" (una palabra no vacía) + if func_name.startswith(word) and len(word) > 0: + # Ej: func_name="solve", word="solv" -> True. Devuelve ayuda para "solve". + # Ej: func_name="solve", word="s" -> True. Devuelve ayuda para "solve" + # (o la primera función en el diccionario que empiece con "s"). + return help_text + return None + + @staticmethod + def PopupFunctionList(): + """Lista de métodos y sus descripciones breves para el popup de autocompletado.""" + return [ + (name, desc.split(". Ej:")[0] if ". Ej:" in desc else desc) + for name, desc in SympyTools._sympy_func_details.items() + ] \ No newline at end of file