From bc768e9ca7eb2c9f4641d727cb11eaf570f5b4be Mon Sep 17 00:00:00 2001 From: Miguel Date: Mon, 2 Jun 2025 17:04:34 +0200 Subject: [PATCH] Refactorizacion de nombres de scripts --- ...factoring_guide.md => Guida_Desarrollo.md} | 4 +- .vscode/settings.json | 4 + MaVCalcv2.lnk | Bin 1748 -> 1730 bytes bin_type.py | 6 +- calc.py | 255 +++++++ chr_type.py | 6 +- dec_type.py | 6 +- hex_type.py | 6 +- hybrid_calc_history.txt | 12 +- hybrid_calc_settings.json | 4 +- icon.png | Bin 0 -> 12018 bytes ip4_type.py | 10 +- main_calc.py | 624 ------------------ main_calc_app.py | 189 ++++-- main_evaluation.py | 34 +- hybrid_base.py => sympy_Base.py | 2 +- sympy_helper.py | 52 +- 17 files changed, 504 insertions(+), 710 deletions(-) rename .doc/{refactoring_guide.md => Guida_Desarrollo.md} (99%) create mode 100644 .vscode/settings.json create mode 100644 calc.py create mode 100644 icon.png delete mode 100644 main_calc.py rename hybrid_base.py => sympy_Base.py (99%) 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 08766e004fdbc463f919b3a31e784b1252567ccb..8cf8571b737c6fbc939c73b796177f62c46935f3 100644 GIT binary patch delta 32 ncmcb@dx&?#XC_9~$pOr!yk!hV3^5GJ42cXm49S~4n0uH3qSOe; delta 50 zcmX@adxdwyXC_9y$#%@9@?{K03^5G342cYx40#Ok3^_nnDUg-Skin43P_)^MxrZ45 DTMrC^ 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 0000000000000000000000000000000000000000..081efe7d2921def112b6ddb9f76a767f35906e4a GIT binary patch literal 12018 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.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