Calc/.doc/OLD_calc.py

1263 lines
61 KiB
Python

"""
Calculadora MAV - Archivo principal
"""
import tkinter as tk
from tkinter import scrolledtext, messagebox, Menu, filedialog
import ast
import json
import os
import math
import sympy
import tkinter.font as tkFont
# Importar las clases desde los módulos separados
from base_types import BaseCalcType
from hex_type import Hex
from bin_type import Bin
from ip4_type import IP4
from date_type import Date
from dec_type import Dec
from chr_type import Chr # Importar la nueva clase Chr
from solve_type import solve
from line_analyzer import LineResultCapture
from sympy_integration import SymPyIntegration
# Para la ayuda en HTML
MARKDOWN_AVAILABLE = False
HTML_VIEWER_TYPE = None
try:
import markdown
MARKDOWN_AVAILABLE = True
except ImportError:
# markdown not available, MARKDOWN_AVAILABLE remains False
pass # Se mostrará una advertencia más adelante si es necesario
# Intentar importar visores HTML
try:
import tkinterweb
HTML_VIEWER_TYPE = "tkinterweb"
except ImportError:
try:
from tkhtmlview import HTMLScrolledText
HTML_VIEWER_TYPE = "tkhtmlview"
except ImportError:
HTML_VIEWER_TYPE = None # Ambos visores fallaron
if not MARKDOWN_AVAILABLE:
print("Advertencia: La librería 'markdown' no está instalada. La ayuda podría mostrarse en texto plano o con formato limitado.")
if MARKDOWN_AVAILABLE and HTML_VIEWER_TYPE is None: # Si markdown está pero no hay visor
print("Advertencia: 'markdown' está disponible, pero no se encontró un visor HTML (tkinterweb/tkhtmlview). La ayuda se mostrará en texto plano.")
class CalculatorApp:
SETTINGS_FILE = "calc_settings.json"
CUSTOM_CLASSES_WITH_HELPERS = [Hex, Bin, IP4, Date, Dec, solve, Chr] # Agregar Chr
OPERATOR_HELP_MESSAGES = {
"^": "Operador XOR a nivel de bits (ej: 5 ^ 3) o diferencia simétrica para conjuntos (ej: {1,2} ^ {2,3}). Requiere un segundo operando.",
"+": "Operador de suma. Requiere un segundo operando.",
"-": "Operador de resta. Requiere un segundo operando.",
"*": "Operador de multiplicación. Requiere un segundo operando.",
"/": "Operador de división. Requiere un segundo operando.",
"**": "Operador de potencia. Requiere un segundo operando.",
"//": "Operador de división entera. Requiere un segundo operando.",
"%": "Operador de módulo. Requiere un segundo operando.",
"&": "Operador AND a nivel de bits. Requiere un segundo operando.",
"|": "Operador OR a nivel de bits. Requiere un segundo operando.",
}
def __init__(self, root):
self.root = root
self.root.title("Calculadora MAV")
# Cargar y establecer el ícono de la aplicación
try:
self.app_icon = tk.PhotoImage(file="icon.png")
self.root.iconphoto(True, self.app_icon)
except tk.TclError:
print("Advertencia: No se pudo cargar o establecer 'icon.png' como ícono de la aplicación.")
self.settings = self._load_app_settings()
self.root.geometry(self.settings.get("window_geometry", "900x600"))
self.root.configure(bg="#2b2b2b")
# Integración con SymPy
self.sympy_integration = SymPyIntegration()
# Funciones y constantes matemáticas
math_functions = {
"pi": math.pi, "e": math.e,
"sin": math.sin, "cos": math.cos, "tan": math.tan,
"asin": math.asin, "acos": math.acos, "atan": math.atan,
"degrees": math.degrees, "radians": math.radians,
"exp": math.exp, "log": math.log, "log10": math.log10, "log2": math.log2,
"sqrt": math.sqrt,
}
self.calc_context_base = {
"Hex": Hex, "Bin": Bin, "IP4": IP4, "Date": Date, "Dec": Dec, "Chr": Chr,
"solve": solve,
"hex": Hex, "bin": Bin, "ip4": IP4, "date": Date, "dec": Dec, "chr": Chr, # Alias en minúscula
**math_functions,
"__builtins__": {
"print": self._custom_print,
"abs": abs, "round": round, "len": len, "str": str, "int": int, "float": float,
"list": list, "dict": dict, "tuple": tuple, "pow": pow, "sum": sum,
"SyntaxError": SyntaxError,
"min": min, "max": max, "range": range, "enumerate": enumerate, "zip": zip,
"Hex": Hex, "Bin": Bin, "IP4": IP4, "Date": Date, "Dec": Dec, "Chr": Chr,
"solve": solve,
"hex": Hex, "bin": Bin, "ip4": IP4, "date": Date, "dec": Dec, "chr": Chr, # Alias en minúscula
**math_functions,
},
}
self.output_buffer = []
self._debounce_job = None
self._syncing_yview = False
self._cached_input_font = None # Para cachear el objeto de fuente del panel de entrada
self.create_widgets()
self.load_persistent_input()
def _load_app_settings(self):
if os.path.exists(self.SETTINGS_FILE):
try:
with open(self.SETTINGS_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except (IOError, json.JSONDecodeError):
return {}
return {}
def _save_app_settings(self):
self.settings["window_geometry"] = self.root.winfo_geometry()
if hasattr(self, "paned_window"):
try:
sash_x_pos = self.paned_window.sash_coord(0)[0]
self.settings["sash_pos_x"] = sash_x_pos
except tk.TclError:
pass
try:
with open(self.SETTINGS_FILE, "w", encoding="utf-8") as f:
json.dump(self.settings, f, indent=4)
except IOError:
messagebox.showwarning("Error", "No se pudieron guardar los ajustes de la interfaz.")
def create_widgets(self):
main_frame = tk.Frame(self.root, bg="#2b2b2b", bd=0)
main_frame.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)
self.paned_window = tk.PanedWindow(
main_frame, orient=tk.HORIZONTAL, bg="#2b2b2b",
sashrelief=tk.FLAT, sashwidth=4, bd=0,
showhandle=False, opaqueresize=True,
)
self.paned_window.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)
initial_input_width = self.settings.get("sash_pos_x", 400)
self.input_text = scrolledtext.ScrolledText(
self.paned_window, font=("Consolas", 11), bg="#1e1e1e", fg="#d4d4d4",
insertbackground="#ffffff", selectbackground="#264f78", undo=True,
wrap=tk.NONE, borderwidth=0, highlightthickness=0, relief=tk.FLAT,
)
self.paned_window.add(self.input_text, width=initial_input_width, stretch="always", minsize=150)
self.output_text = scrolledtext.ScrolledText(
self.paned_window, font=("Consolas", 11), bg="#0f0f0f", fg="#00ff00",
state="disabled", wrap=tk.NONE, borderwidth=0,
highlightthickness=0, relief=tk.FLAT,
)
self.paned_window.add(self.output_text, stretch="always", minsize=150)
self.input_text.bind("<KeyRelease>", self.on_key_release)
self.input_text.bind("<Button-3>", lambda e: self._show_context_menu(e, self.input_text, "input"))
self.output_text.bind("<Button-3>", lambda e: self._show_context_menu(e, self.output_text, "output"))
self.setup_scroll_sync()
self.setup_output_tags()
def _yscroll_input_command(self, *args):
self.input_text.vbar.set(*args)
if not self._syncing_yview:
self._syncing_yview = True
self.output_text.yview_moveto(args[0])
self._syncing_yview = False
def _yscroll_output_command(self, *args):
self.output_text.vbar.set(*args)
if not self._syncing_yview:
self._syncing_yview = True
self.input_text.yview_moveto(args[0])
self._syncing_yview = False
def _unified_mouse_wheel(self, event):
if self._syncing_yview:
return "break"
if hasattr(event, "widget") and event.widget:
event.widget.yview_scroll(int(-1 * (event.delta / 120)), "units")
return "break"
def setup_scroll_sync(self):
self.input_text.config(yscrollcommand=self._yscroll_input_command)
self.output_text.config(yscrollcommand=self._yscroll_output_command)
self.input_text.bind("<MouseWheel>", self._unified_mouse_wheel)
self.output_text.bind("<MouseWheel>", self._unified_mouse_wheel)
def _show_context_menu(self, event, widget_ref, widget_type):
context_menu = Menu(
self.root, tearoff=0, bg="#3c3c3c", fg="white",
activebackground="#007acc", activeforeground="white",
relief=tk.FLAT, bd=1,
)
if widget_type == "input":
context_menu.add_command(label="Cortar", command=lambda: widget_ref.event_generate("<<Cut>>"))
context_menu.add_command(label="Copiar", command=lambda: widget_ref.event_generate("<<Copy>>"))
context_menu.add_command(label="Pegar", command=lambda: widget_ref.event_generate("<<Paste>>"))
context_menu.add_separator(background="#555555")
context_menu.add_command(label="Borrar Todo", command=self._clear_input_text)
context_menu.add_separator(background="#555555")
context_menu.add_command(label="Guardar como...", command=self._save_input_as)
context_menu.add_command(label="Cargar archivo...", command=self._load_input_from_file)
elif widget_type == "output":
context_menu.add_command(label="Copiar Todo", command=self._copy_all_output)
context_menu.add_command(label="Borrar Salida", command=self._clear_output_text)
context_menu.add_separator(background="#555555")
context_menu.add_command(label="Ayuda", command=self._show_help_window)
try:
context_menu.tk_popup(event.x_root, event.y_root)
finally:
context_menu.grab_release()
def _clear_output_text(self):
self.output_text.config(state=tk.NORMAL)
self.output_text.delete("1.0", tk.END)
self.output_text.config(state=tk.DISABLED)
def _copy_all_output(self):
self.output_text.config(state=tk.NORMAL)
content = self.output_text.get("1.0", tk.END).strip()
self.output_text.config(state=tk.DISABLED)
if content:
self.root.clipboard_clear()
self.root.clipboard_append(content)
def _show_help_window(self):
help_win = tk.Toplevel(self.root)
help_win.title("Ayuda - Calculadora")
help_win.geometry("750x600")
# Usar un fondo oscuro consistente para la ventana de ayuda
help_win.configure(bg="#1e1e1e") # Coincide con el fondo del HTMLScrolledText y CSS
help_win.transient(self.root)
readme_content = self._get_help_content()
if MARKDOWN_AVAILABLE and HTML_VIEWER_TYPE:
try:
# CSS para un tema oscuro, consistente con la UI de la calculadora
# Simplificado para no usar selectores 'body' o 'html'
# y asumiendo que los estilos base se aplican al widget.
dark_theme_css = """
<style>
h1, h2, h3, h4, h5, h6 {
color: #569cd6;
margin-top: 1.2em;
margin-bottom: 0.6em;
border-bottom: 1px solid #3a3a3a;
padding-bottom: 0.3em;
}
h1 { font-size: 1.8em; }
h2 { font-size: 1.5em; }
h3 { font-size: 1.3em; }
p {
line-height: 1.65;
margin-bottom: 0.8em;
}
a {
color: #4fc3f7;
text-decoration: none;
}
a:hover {
color: #80dfff;
text-decoration: underline;
}
code {
font-family: "Consolas", "Courier New", monospace;
background-color: #2d2d2d;
color: #ce9178;
padding: 3px 6px;
border-radius: 4px;
border: 1px solid #3c3c3c;
font-size: 0.95em;
}
pre > code {
background-color: transparent !important;
color: inherit !important;
padding: 0 !important;
border-radius: 0 !important;
border: none !important;
font-size: 1em !important;
}
ul, ol { padding-left: 25px; color: #c8c8c8; }
li { margin-bottom: 0.4em; }
hr { border: 0; height: 1px; background: #3a3a3a; margin: 1.5em 0; }
table { border-collapse: collapse; width: 90%; margin: 1em auto; }
th, td { border: 1px solid #4a4a4a; padding: 8px; text-align: left; }
th { background-color: #2d2d2d; color: #9cdcfe; }
</style>
"""
html_fragment = markdown.markdown(
readme_content, extensions=["fenced_code", "codehilite", "tables", "nl2br", "admonition"],
extension_configs={"codehilite": {"noclasses": True, "pygments_style": "monokai"}}
)
# Construir un documento HTML completo
content_for_viewer = f"""
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Ayuda de Calculadora</title>
{dark_theme_css}
</head>
<body style="background-color: #1e1e1e; color: #d4d4d4; font-family: 'Segoe UI', sans-serif; font-size: 10pt; margin:0; padding:12px;">
{html_fragment}
</body>
</html>
"""
if HTML_VIEWER_TYPE == "tkinterweb":
# tkinterweb.HtmlFrame no toma bg/fg directamente en constructor para el contenido
html_viewer = tkinterweb.HtmlFrame(help_win, messages_enabled=False)
html_viewer.load_html(content_for_viewer)
# El fondo del Frame en sí se puede configurar si es necesario,
# pero el body style debería controlarlo.
# html_viewer.configure(background="#1e1e1e")
elif HTML_VIEWER_TYPE == "tkhtmlview":
html_viewer = HTMLScrolledText(help_win)
html_viewer.configure(bg="#1e1e1e")
html_viewer.set_html(content_for_viewer)
html_viewer.pack(padx=0, pady=0, fill=tk.BOTH, expand=True) # El padding se maneja en el CSS
except Exception as e:
print(f"Error al renderizar Markdown a HTML: {e}") # Para depuración
# Fallback to text if HTML fails
self._show_text_help(help_win, readme_content)
else:
self._show_text_help(help_win, readme_content)
close_button = tk.Button(
help_win, text="Cerrar", command=help_win.destroy,
bg="#3c3c3c", fg="white", relief=tk.FLAT, padx=10,
)
close_button.pack(pady=(5, 10))
def _get_help_content(self):
"""Obtiene el contenido de ayuda desde readme.md o genera uno por defecto"""
try:
readme_path = "readme.md"
if os.path.exists(readme_path):
with open(readme_path, "r", encoding="utf-8") as f:
return f.read()
except IOError:
pass
# Contenido por defecto si no se encuentra readme.md
return """# Calculadora MAV - Ayuda
## Nuevas Características:
- **Ecuaciones algebraicas**: Usa comillas para definir ecuaciones: `"x + 10 = 15"`
- **Resolver ecuaciones**: Usa `solve(x)` para resolver una variable
- **Resolver múltiples variables**: `solve(x, y)` o `solve()` para todas
- **Contexto dinámico**: Las variables definidas después de solve se usan automáticamente
## Ejemplo de uso algebraico:
```
"x + 10 = 15"
"a*y - 5 = 20"
solve(x) # x = 5
solve(y) # y = 25/a
a = 3
y/25 # 1/3
```
## Clases disponibles:
- `Hex()`: Números hexadecimales
- `Bin()`: Números binarios
- `IP4()`: Direcciones IPv4
- `Date()`: Fechas
- `Dec()`: Decimales
## Funciones matemáticas:
- Constantes: `pi`, `e`
- Trigonométricas: `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`
- Conversión Angular: `degrees()`, `radians()`
- Exponenciales/Logarítmicas: `exp()`, `log()`, `log10()`, `log2()`, `sqrt()`
- Variable `last` para el último resultado
## Menú Contextual (clic derecho):
- En entrada: Cortar, Copiar, Pegar, Borrar Todo, Guardar como..., Cargar archivo...
- En salida: Copiar Todo, Borrar Salida
"""
def _show_text_help(self, help_win, content):
"""Muestra la ayuda en texto plano cuando markdown no está disponible"""
text_widget = scrolledtext.ScrolledText(
help_win, font=("Consolas", 10), bg="#1e1e1e", fg="#d4d4d4",
wrap=tk.WORD, borderwidth=0, highlightthickness=0
)
text_widget.insert("1.0", content)
text_widget.config(state="disabled")
text_widget.pack(padx=5, pady=5, fill=tk.BOTH, expand=True)
def setup_output_tags(self):
self.output_text.tag_configure("error", foreground="#ff6b6b", font=("Consolas", 11, "bold"))
self.output_text.tag_configure("result", foreground="#abdbe3")
self.output_text.tag_configure("hex", foreground="#f9a825")
self.output_text.tag_configure("bin", foreground="#4fc3f7")
self.output_text.tag_configure("ip", foreground="#fff176")
self.output_text.tag_configure("date", foreground="#ff8a80")
self.output_text.tag_configure("comment", foreground="#7f8c8d")
self.output_text.tag_configure("print_output", foreground="#b0bec5")
self.output_text.tag_configure("chr_type_tag", foreground="#80cbc4") # Teal-ish para Chr
self.output_text.tag_configure("equation", foreground="#c792ea")
self.output_text.tag_configure("type_hint", foreground="#6a6a6a") # Gris oscuro para el hint de tipo
self.output_text.tag_configure("symbolic", foreground="#82aaff")
def _custom_print(self, *args, **kwargs):
sep = kwargs.get("sep", " ")
output = sep.join(map(str, args))
self.output_buffer.append(("print", output))
def evaluate_all_lines(self):
input_content = self.input_text.get("1.0", tk.END)
if not input_content.strip():
self.set_output_text_with_colors([])
return
lines = input_content.splitlines()
output_data = []
# Limpiar ecuaciones de SymPy para re-evaluación completa
self.sympy_integration.clear()
# Crear un nuevo contexto para esta pasada de evaluación
current_pass_context = self.calc_context_base.copy()
current_pass_context["__builtins__"] = self.calc_context_base["__builtins__"].copy()
last_val_for_current_line = None
line_result_capture = LineResultCapture()
# Store the context after each line for potential use by autocomplete
# This is a simplification; a more robust system might be needed.
self._line_contexts_for_autocomplete = {}
for line_number, line_text_original in enumerate(lines, 1):
self.output_buffer = []
current_pass_context["last"] = last_val_for_current_line
current_line_outputs = []
comment_part = ""
code_to_evaluate = line_text_original
# Separar comentarios
if "#" in code_to_evaluate:
code_to_evaluate, comment_part = code_to_evaluate.split("#", 1)
comment_part = "#" + comment_part
code_to_evaluate_stripped = code_to_evaluate.strip()
# Reemplazar "var = ?" por "solve('var')"
if code_to_evaluate_stripped.endswith("=?"):
parts = code_to_evaluate_stripped.rsplit("=?", 1)
var_name_to_solve = parts[0].strip()
if var_name_to_solve:
# Escapar comillas dobles si estuvieran en var_name_to_solve (poco probable para nombres de var)
safe_var_name = var_name_to_solve.replace('"', '\\"')
code_to_evaluate_stripped = f'solve("{safe_var_name}")'
# Verificar si es solo el nombre de una clase (para mostrar ayuda)
showed_proactive_hint_for_class_name = False
if code_to_evaluate_stripped:
for cls in self.CUSTOM_CLASSES_WITH_HELPERS:
if code_to_evaluate_stripped.lower() == cls.__name__.lower():
hint = cls.Helper(code_to_evaluate_stripped)
if hint:
current_line_outputs.append(("comment", hint))
if comment_part:
current_line_outputs.append(("comment", " " + comment_part.strip()))
showed_proactive_hint_for_class_name = True
break
if showed_proactive_hint_for_class_name:
line_actual_object = last_val_for_current_line
output_data.append(current_line_outputs)
continue
# Línea vacía
if not code_to_evaluate_stripped:
if comment_part:
current_line_outputs.append(("comment", comment_part.strip()))
else:
current_line_outputs.append(("", ""))
line_actual_object = last_val_for_current_line
else:
# Evaluar la línea
if "__resultado_linea__" in current_pass_context:
del current_pass_context["__resultado_linea__"]
try:
final_code_to_exec = line_result_capture.analyze_line(code_to_evaluate_stripped)
processed_result = False # Inicializar al inicio
# Verificar si es una ecuación antes de ejecutar
if '=' in code_to_evaluate_stripped:
# Intentar agregar como ecuación primero
if self.sympy_integration.add_equation_from_string(code_to_evaluate_stripped, line_number):
current_line_outputs.append(("equation", f"Ecuación: {code_to_evaluate_stripped}"))
line_actual_object = None
processed_result = True
if not processed_result:
exec(final_code_to_exec, current_pass_context)
line_actual_object = current_pass_context.get("__resultado_linea__")
if isinstance(line_actual_object, SyntaxError):
raise line_actual_object
# Actualizar el contexto de SymPy una vez después de exec y antes del procesamiento simbólico
self.sympy_integration.update_context(current_pass_context)
# Si el resultado de exec es una expresión SymPy, intentar evaluarla/simplificarla más
if isinstance(line_actual_object, sympy.Expr):
original_expr_from_exec = line_actual_object
substitutions = self.sympy_integration._create_substitutions()
if substitutions:
evaluated_expr = original_expr_from_exec.subs(substitutions)
else:
evaluated_expr = original_expr_from_exec
if evaluated_expr.is_number:
line_actual_object = self.sympy_integration._format_number(evaluated_expr)
else:
simplified_expr = sympy.simplify(evaluated_expr)
if simplified_expr.is_number:
line_actual_object = self.sympy_integration._format_number(simplified_expr)
else:
line_actual_object = simplified_expr
# Procesar el resultado
if self.sympy_integration.is_equation_string(line_actual_object):
if self.sympy_integration.add_equation_from_string(line_actual_object, line_number):
current_line_outputs.append(("equation", f"Ecuación: {line_actual_object}"))
else:
current_line_outputs.append(("error", f"Error parseando ecuación: {line_actual_object}"))
processed_result = True
# Manejar instancias de solve
elif isinstance(line_actual_object, solve):
solve_obj = line_actual_object
solve_obj.execute(self.sympy_integration, current_pass_context)
# Mostrar output de print si hay
for type_tag, printed_text in self.output_buffer:
current_line_outputs.append((type_tag, printed_text))
if solve_obj.error:
current_line_outputs.append(("error", solve_obj.error))
line_actual_object = last_val_for_current_line
else:
current_line_outputs.append(("symbolic", str(solve_obj)))
if solve_obj.solutions is not None:
if isinstance(solve_obj.solutions, dict):
for var_name, sol_value in solve_obj.solutions.items():
if isinstance(sol_value, (int, float, complex, sympy.Expr, list, tuple)):
current_pass_context[var_name] = sol_value
elif solve_obj.var_names and len(solve_obj.var_names) == 1:
var_name = solve_obj.var_names[0]
if isinstance(solve_obj.solutions, (int, float, complex, sympy.Expr, list, tuple)):
current_pass_context[var_name] = solve_obj.solutions
line_actual_object = solve_obj.solutions
processed_result = True
# Verificar si podemos evaluar simbólicamente expresiones con variables no definidas
elif not processed_result and isinstance(line_actual_object, NameError):
sympy_result = self.sympy_integration.evaluate_expression(
code_to_evaluate_stripped, current_pass_context
)
if sympy_result is not None:
current_line_outputs.append(("symbolic", sympy_result))
line_actual_object = sympy_result
processed_result = True
else:
current_line_outputs.append(("error", f"NameError: {line_actual_object}"))
line_actual_object = last_val_for_current_line
processed_result = True
# Resultado normal
if not processed_result:
# Mostrar output de print
for type_tag, printed_text in self.output_buffer:
current_line_outputs.append((type_tag, printed_text))
# Mostrar resultado si no es None
if line_actual_object is not None:
current_line_outputs.append(("result", line_actual_object))
except NameError as e:
# Verificar si el error contiene un '=' y tratarlo como ecuación
if '=' in code_to_evaluate_stripped:
# Intentar agregar como ecuación
if self.sympy_integration.add_equation_from_string(code_to_evaluate_stripped, line_number):
current_line_outputs.append(("equation", f"Ecuación: {code_to_evaluate_stripped}"))
line_actual_object = None # No hay resultado directo
else:
# Intentar evaluar simbólicamente
sympy_result = self.sympy_integration.evaluate_expression(
code_to_evaluate_stripped, current_pass_context
)
if sympy_result is not None:
current_line_outputs.append(("symbolic", sympy_result))
line_actual_object = sympy_result
else:
# Mostrar error normal si no se pudo agregar como ecuación
current_line_outputs.append(("error", f"NameError: {e}"))
line_actual_object = last_val_for_current_line
else:
# Intentar evaluar simbólicamente
sympy_result = self.sympy_integration.evaluate_expression(
code_to_evaluate_stripped, current_pass_context
)
if sympy_result is not None:
current_line_outputs.append(("symbolic", sympy_result))
line_actual_object = sympy_result
else:
# Mostrar error normal
current_line_outputs.append(("error", f"NameError: {e}"))
line_actual_object = last_val_for_current_line
except Exception as e:
# Mostrar output de print antes del error
for type_tag, printed_text in self.output_buffer:
current_line_outputs.append((type_tag, printed_text))
# Manejo de errores mejorado
custom_error_message_applied = False
if isinstance(e, SyntaxError):
# Verificar si es un operador al final
import re
op_match = re.match(r"(.+?)\s*([+\-*/%^&|<>=!]{1,2})\s*$", code_to_evaluate_stripped)
if op_match:
operator = op_match.group(2)
if operator == "*" and code_to_evaluate_stripped.endswith("**"):
operator = "**"
elif operator == "/" and code_to_evaluate_stripped.endswith("//"):
operator = "//"
if operator in self.OPERATOR_HELP_MESSAGES:
help_text = self.OPERATOR_HELP_MESSAGES[operator]
error_msg = f"Error de sintaxis cerca de '{operator}'. Ayuda: {help_text}"
current_line_outputs.append(("error", error_msg))
custom_error_message_applied = True
if not custom_error_message_applied:
# Verificar si hay ayuda disponible
hint_message_from_helper = None
for cls in self.CUSTOM_CLASSES_WITH_HELPERS:
hint = cls.Helper(code_to_evaluate_stripped)
if hint:
hint_message_from_helper = hint
break
if hint_message_from_helper:
current_line_outputs.append(("comment", hint_message_from_helper))
else:
error_type_name = type(e).__name__
error_detail = e.msg if hasattr(e, 'msg') else str(e)
if hasattr(e, 'text') and e.text and not str(error_detail).startswith("Sintaxis inválida:"):
error_detail += f" (en: '{e.text.strip()}')"
standard_error_message = f"{error_type_name}: {error_detail}"
current_line_outputs.append(("error", standard_error_message))
line_actual_object = last_val_for_current_line
# Limpiar variable temporal
if "__resultado_linea__" in current_pass_context:
del current_pass_context["__resultado_linea__"]
# Agregar comentario si existe
if comment_part and code_to_evaluate_stripped:
prefix_space = " " if current_line_outputs else ""
current_line_outputs.append(("comment", prefix_space + comment_part.strip()))
output_data.append(current_line_outputs)
last_val_for_current_line = line_actual_object
# Store a copy of the context as it is after this line's evaluation
self._line_contexts_for_autocomplete[line_number] = current_pass_context.copy()
self.set_output_text_with_colors(output_data)
def _get_input_font(self):
"""Obtiene o crea y cachea el objeto tk.Font para el panel de entrada."""
if not self._cached_input_font:
# Asume la fuente configurada en create_widgets: ("Consolas", 11)
self._cached_input_font = tkFont.Font(family="Consolas", size=11)
return self._cached_input_font
def _adjust_input_pane_width(self):
"""Ajusta el ancho del panel de entrada según su contenido."""
if not hasattr(self, 'paned_window') or not self.paned_window.winfo_exists():
return
# Esperar a que la ventana tenga un tamaño válido
if self.paned_window.winfo_width() <= 1:
return # Se reintentará en la siguiente llamada (ej. por KeyRelease)
# Obtener contenido excluyendo el último newline automático del widget Text
input_content = self.input_text.get("1.0", f"{tk.END}-1c")
lines = input_content.splitlines()
input_font = self._get_input_font()
max_pixel_width = 0
if not input_content.strip(): # Si está vacío o solo espacios en blanco
max_pixel_width = 5 # Ancho mínimo para el cursor o como placeholder
else:
for line in lines:
measured_width = input_font.measure(line) if line.strip() else input_font.measure(" ")
if measured_width > max_pixel_width:
max_pixel_width = measured_width
padding = 40 # Relleno para barra de desplazamiento, márgenes, etc.
width_needed_by_text = max_pixel_width + padding
# DEBUGGING PRINTS (descomentar si el problema persiste)
# print(f"--- Adjusting Input Pane ---")
# print(f"Input content: '{input_content[:50]}...'") # Muestra los primeros 50 chars del input
# print(f"Max pixel width of text: {max_pixel_width}")
# print(f"Width needed by text (max_pixel_width + padding): {width_needed_by_text}")
# END DEBUGGING PRINTS
min_input_pane_width = 150 # Definido en create_widgets
min_output_pane_width = 150 # Definido en create_widgets
total_width = self.paned_window.winfo_width()
current_sash_pos = 0
try:
sash_coords = self.paned_window.sash_coord(0)
if sash_coords:
current_sash_pos = sash_coords[0]
else:
# print("DEBUG: Could not get sash_coord.") # DEBUG
return
except tk.TclError:
# print("DEBUG: TclError getting sash_coord.") # DEBUG
return
# print(f"Current sash position (input pane width): {current_sash_pos}") # DEBUG
if width_needed_by_text > current_sash_pos:
# print(f"Condition MET: Text needs more space ({width_needed_by_text} > {current_sash_pos})") # DEBUG
new_input_width = width_needed_by_text # Punto de partida
# Asegurar que el nuevo ancho no sea menor que el mínimo del panel de entrada
new_input_width = max(new_input_width, min_input_pane_width)
# Asegurar que el panel de salida conserve su espacio mínimo
if total_width - new_input_width < min_output_pane_width:
new_input_width = total_width - min_output_pane_width
new_input_width = max(new_input_width, min_input_pane_width) # Re-verificar mínimo del input
# Aplicar un ratio máximo para el panel de entrada
max_input_ratio = 0.75 # Podría ser una constante de clase
max_width_by_ratio = int(total_width * max_input_ratio)
if new_input_width > max_width_by_ratio:
if max_width_by_ratio >= min_input_pane_width and \
(total_width - max_width_by_ratio) >= min_output_pane_width:
new_input_width = max_width_by_ratio
final_new_input_width = max(0, int(new_input_width)) # No debe ser negativo
# print(f"Calculated final new input width: {final_new_input_width}") # DEBUG
# Mover el sash solo si el nuevo ancho es significativamente mayor que el actual (umbral ajustado)
sash_adjustment_threshold = 3 # Píxeles
if final_new_input_width > current_sash_pos and \
(final_new_input_width - current_sash_pos) >= sash_adjustment_threshold:
# print(f"Condition MET for sash_place: New width {final_new_input_width} is significantly larger (diff >= {sash_adjustment_threshold}).") # DEBUG
try:
if self.paned_window.winfo_exists() and total_width >= (min_input_pane_width + min_output_pane_width): # type: ignore
self.paned_window.sash_place(0, final_new_input_width, 0) # Añadido el argumento y=0
# print(f"Sash placed at: {final_new_input_width}") # DEBUG
# else:
# print("DEBUG: Paned window not ready or total width too small for sash_place.") #DEBUG
except tk.TclError as e_sash:
# print(f"DEBUG: TclError during sash_place: {e_sash}") # DEBUG
pass
# else:
# print(f"Condition NOT MET for sash_place: final_new_input_width ({final_new_input_width}) vs current_sash_pos ({current_sash_pos}) or threshold ({sash_adjustment_threshold}).") # DEBUG
# else:
# print(f"Condition NOT MET: Text does not need more space ({width_needed_by_text} <= {current_sash_pos})") # DEBUG
# print(f"--- End Adjusting Input Pane ---")
_autocomplete_popup = None
_autocomplete_listbox = None
def _build_context_for_autocomplete(self, current_line_num_1_based):
"""
Builds an evaluation context based on lines executed *before* the current line.
Uses a temporary SymPyIntegration instance to avoid side effects on the main one.
"""
lines = self.input_text.get("1.0", tk.END).splitlines()
# Start with the base context
eval_context = self.calc_context_base.copy()
eval_context["__builtins__"] = self.calc_context_base["__builtins__"].copy()
# Use a temporary SymPyIntegration for this context building
temp_sympy_integration = SymPyIntegration()
temp_sympy_integration.update_context(eval_context) # Prime with base symbols/values
last_val_for_context_build = None
line_result_capture = LineResultCapture()
for i in range(current_line_num_1_based - 1): # Iterate lines *before* current
if i >= len(lines): break # Should not happen if current_line_num is valid
line_text = lines[i]
eval_context["last"] = last_val_for_context_build
code_to_run = line_text
if "#" in code_to_run:
code_to_run, _ = code_to_run.split("#", 1)
code_to_run_stripped = code_to_run.strip()
if not code_to_run_stripped:
# Carry over 'last' or set to None if line is empty.
# For simplicity, let's assume an empty line doesn't change 'last' from previous.
continue
try:
final_code_to_exec = line_result_capture.analyze_line(code_to_run_stripped)
exec(final_code_to_exec, eval_context)
line_res_obj = eval_context.get("__resultado_linea__")
if isinstance(line_res_obj, solve):
line_res_obj.execute(temp_sympy_integration, eval_context)
if line_res_obj.solutions is not None:
if isinstance(line_res_obj.solutions, dict):
for var_name, sol_val in line_res_obj.solutions.items():
eval_context[var_name] = sol_val
elif line_res_obj.var_names and len(line_res_obj.var_names) == 1:
eval_context[line_res_obj.var_names[0]] = line_res_obj.solutions
last_val_for_context_build = line_res_obj.solutions
elif temp_sympy_integration.is_equation_string(line_res_obj):
temp_sympy_integration.add_equation_from_string(line_res_obj)
last_val_for_context_build = line_res_obj
else:
last_val_for_context_build = line_res_obj
temp_sympy_integration.update_context(eval_context)
if "__resultado_linea__" in eval_context:
del eval_context["__resultado_linea__"]
except Exception:
pass # Ignore errors in preceding lines for autocomplete context
eval_context["last"] = last_val_for_context_build # Set 'last' for the current line's expression
return eval_context
def _handle_dot_autocomplete(self):
self._close_autocomplete_popup() # Close any existing one first
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) # 1-based
char_idx_of_dot = int(char_num_str) # 0-based index of char after dot (where cursor is)
# Get the text on the current line BEFORE the dot
# The dot is at char_idx_of_dot - 1
obj_expr_str = self.input_text.get(f"{current_line_num}.0", f"{current_line_num}.{char_idx_of_dot -1}")
if not obj_expr_str.strip():
return
# Build context from lines *before* the current one
eval_context = self._build_context_for_autocomplete(current_line_num)
obj = None
try:
obj = eval(obj_expr_str, eval_context)
except Exception:
return # Can't evaluate object expression
if obj is not None:
methods = []
# Prioritize known, useful methods for custom types
if isinstance(obj, IP4):
methods.extend(["Mask", "Prefix", "Nodes", "NetworkAddress", "BroadcastAddress", "toHex"])
elif isinstance(obj, Date):
methods.extend([]) # No specific simple methods other than operators yet
# Add other callable attributes, excluding most dunder/private
# Define methods from BaseCalcType that should be excluded from suggestions
# if not overridden by the subclass in a meaningful public way.
base_calc_type_public_methods_to_exclude = {"Helper", "original_str_for_repr"}
for attr_name in dir(obj):
if attr_name.startswith('_') and not attr_name.startswith('__'): # Exclude single underscore
continue
if attr_name.startswith('__'): # Exclude all attributes starting with double underscore
continue
if isinstance(obj, BaseCalcType) and attr_name in base_calc_type_public_methods_to_exclude:
continue
try:
attr = getattr(obj, attr_name)
if callable(attr):
if attr_name not in methods: # Avoid duplicates
methods.append(attr_name)
except Exception: continue
methods.sort()
if methods:
self._show_autocomplete_popup(methods, obj_expr_str, eval_context)
def _show_autocomplete_popup(self, suggestions, obj_expr_str_for_paren, eval_context_for_paren):
cursor_bbox = self.input_text.bbox(tk.INSERT)
if not cursor_bbox: return
x, y, _, height = cursor_bbox
popup_x = self.input_text.winfo_rootx() + x
popup_y = self.input_text.winfo_rooty() + y + height + 2
self._autocomplete_popup = tk.Toplevel(self.root)
self._autocomplete_popup.wm_overrideredirect(True)
self._autocomplete_popup.wm_geometry(f"+{popup_x}+{popup_y}")
self._autocomplete_popup.attributes('-topmost', True)
self.root.after(100, lambda: self._autocomplete_popup.attributes('-topmost', False) if self._autocomplete_popup else None)
self._autocomplete_listbox = tk.Listbox(
self._autocomplete_popup, bg="#3c3f41", fg="#bbbbbb",
selectbackground="#007acc", selectforeground="white",
borderwidth=1, relief="solid", exportselection=False, activestyle="none"
)
for item in suggestions: self._autocomplete_listbox.insert(tk.END, item)
if suggestions:
self._autocomplete_listbox.select_set(0)
self._autocomplete_listbox.pack(expand=True, fill=tk.BOTH)
# Pass the expression and context needed for parenthesis logic
self._autocomplete_listbox.bind("<Return>", lambda e, expr=obj_expr_str_for_paren, ctx=eval_context_for_paren: self._on_autocomplete_select(e, expr, ctx))
self._autocomplete_listbox.bind("<Escape>", lambda e: self._close_autocomplete_popup())
self._autocomplete_listbox.bind("<Double-Button-1>", lambda e, expr=obj_expr_str_for_paren, ctx=eval_context_for_paren: self._on_autocomplete_select(e, expr, ctx))
self._autocomplete_listbox.focus_set()
self._autocomplete_listbox.bind("<Up>", lambda e: self._navigate_autocomplete(e, -1))
self._autocomplete_listbox.bind("<Down>", lambda e: self._navigate_autocomplete(e, 1))
self.input_text.bind("<FocusOut>", self._on_input_focus_out_while_autocomplete, add=True)
self.input_text.bind("<Button-1>", lambda e: self._close_autocomplete_popup(), add=True) # Click in input text
self.root.bind("<Button-1>", self._on_root_click_while_autocomplete, add=True) # Click outside input text
self.input_text.bind("<Key>", self._on_input_keypress_while_autocomplete, add=True)
max_len = max(len(s) for s in suggestions) if suggestions else 10
width = max(15, min(max_len + 2, 50))
height = min(len(suggestions), 10)
self._autocomplete_listbox.config(width=width, height=height)
else:
self._close_autocomplete_popup()
def _navigate_autocomplete(self, event, direction):
if not self._autocomplete_listbox: return "break"
current_selection = self._autocomplete_listbox.curselection()
if not current_selection:
new_idx = 0 if direction == 1 else self._autocomplete_listbox.size() -1
else:
idx = current_selection[0]
new_idx = idx + direction
if 0 <= new_idx < self._autocomplete_listbox.size():
if current_selection: self._autocomplete_listbox.select_clear(current_selection[0])
self._autocomplete_listbox.select_set(new_idx)
self._autocomplete_listbox.activate(new_idx)
self._autocomplete_listbox.see(new_idx)
return "break"
def _on_autocomplete_select(self, event, obj_expr_str, eval_context):
if not self._autocomplete_listbox: return "break"
selection = self._autocomplete_listbox.curselection()
if not selection: self._close_autocomplete_popup(); return "break"
selected_method = self._autocomplete_listbox.get(selection[0])
self.input_text.insert(tk.INSERT, selected_method)
try: # Add parentheses if it's a method that usually takes them
obj = eval(obj_expr_str, eval_context) # Re-eval to get the object
method_attr = getattr(obj, selected_method, None)
# Heuristic: if it's callable and not a class type itself (like Hex referring to Hex class)
# and not a dunder that isn't usually called with () like __str__
if callable(method_attr) and not isinstance(method_attr, type) \
and not (selected_method.startswith("__") and selected_method.endswith("__")):
self.input_text.insert(tk.INSERT, "()")
self.input_text.mark_set(tk.INSERT, f"{tk.INSERT}-1c") # Cursor inside ()
except Exception: pass # If unsure, just insert name
self._close_autocomplete_popup()
self.input_text.focus_set()
self.on_key_release() # Trigger re-evaluation
return "break"
def _close_autocomplete_popup(self, event=None):
if self._autocomplete_popup:
self.input_text.unbind("<FocusOut>", self._on_input_focus_out_while_autocomplete_binding_id)
self.input_text.unbind("<Button-1>") # Reconsider if this is too broad
self.root.unbind("<Button-1>", self._on_root_click_while_autocomplete_binding_id)
self.input_text.unbind("<Key>", self._on_input_keypress_while_autocomplete_binding_id)
self._autocomplete_popup.destroy()
self._autocomplete_popup = None
self._autocomplete_listbox = None
# Do not shift focus here if it was closed due to focus out or click elsewhere
# Store binding IDs to unbind them correctly
_on_input_focus_out_while_autocomplete_binding_id = None
_on_root_click_while_autocomplete_binding_id = None
_on_input_keypress_while_autocomplete_binding_id = None
def _on_input_focus_out_while_autocomplete(self, event):
# Close if focus is not going to the popup itself
if self._autocomplete_popup and self.root.focus_get() != self._autocomplete_listbox:
self._close_autocomplete_popup()
def _on_root_click_while_autocomplete(self, event):
if self._autocomplete_popup:
# Check if the click was outside the input_text and outside the popup
if event.widget != self.input_text and event.widget != self._autocomplete_listbox and \
(not isinstance(event.widget, tk.Listbox) or event.widget.master != self._autocomplete_popup):
self._close_autocomplete_popup()
def _on_input_keypress_while_autocomplete(self, event):
if self._autocomplete_popup:
if self.root.focus_get() == self._autocomplete_listbox:
# Let listbox handle Up, Down, Return, Escape
if event.keysym in ["Up", "Down", "Return", "Escape"]:
return
# For other keys (letters, numbers, backspace, space, etc.) when input has focus
if event.keysym not in ["Shift_L", "Shift_R", "Control_L", "Control_R", "Alt_L", "Alt_R", "Caps_Lock", "Tab"]:
if event.char != '.': # Typing a new dot will re-trigger _handle_dot_autocomplete
self._close_autocomplete_popup()
# Do not return "break", let the key press be processed by input_text
# Allow event to propagate
# In create_widgets, after self.input_text.bind("<Button-3>", ...):
# Store binding IDs for later unbinding
# self._on_input_focus_out_while_autocomplete_binding_id = self.input_text.bind("<FocusOut>", self._on_input_focus_out_while_autocomplete, add=True)
# self._on_root_click_while_autocomplete_binding_id = self.root.bind("<Button-1>", self._on_root_click_while_autocomplete, add=True)
# self._on_input_keypress_while_autocomplete_binding_id = self.input_text.bind("<Key>", self._on_input_keypress_while_autocomplete, add=True)
# The above bindings are now added dynamically when popup is shown.
def set_output_text_with_colors(self, output_data_lines):
self.output_text.config(state="normal")
self.output_text.delete("1.0", tk.END)
num_input_lines_str = self.input_text.index(f"{tk.END}-1c").split(".")[0]
num_input_lines = int(num_input_lines_str) if num_input_lines_str else 0
for i, line_parts in enumerate(output_data_lines):
is_output_line_effectively_empty = not line_parts or \
(len(line_parts) == 1 and line_parts[0][0] == "" and line_parts[0][1] == "")
if not is_output_line_effectively_empty:
first_part_on_line = True
for item_index, (type_tag, content_obj) in enumerate(line_parts):
if not first_part_on_line:
prev_type_tag = line_parts[item_index - 1][0] if item_index > 0 else ""
if type_tag == "comment" and prev_type_tag not in ["", "comment"]:
self.output_text.insert(tk.END, " ")
elif prev_type_tag != "":
self.output_text.insert(tk.END, " ; ")
content_str = ""
type_hint_str = ""
if content_obj is not None:
is_pre_stringified_content = type_tag in ["equation", "error", "comment", "print_output"] or \
(type_tag == "symbolic" and isinstance(content_obj, str))
if is_pre_stringified_content:
content_str = str(content_obj)
else:
content_str = str(content_obj)
type_name = ""
if isinstance(content_obj, BaseCalcType):
type_name = content_obj.__class__.__name__
elif isinstance(content_obj, sympy.Basic): # sympy.Expr, sympy.Integer, etc.
type_name = "sympy" # Ejemplo: y = 229/3 [sympy]
elif isinstance(content_obj, list):
if content_obj and all(isinstance(item, sympy.Basic) for item in content_obj):
type_name = "list[sympy]"
else:
type_name = "list"
elif isinstance(content_obj, tuple):
type_name = "tuple"
elif isinstance(content_obj, dict):
type_name = "dict"
elif not (isinstance(content_obj, BaseCalcType) or \
isinstance(content_obj, sympy.Basic) or \
isinstance(content_obj, (list, tuple, dict, str))): # Excluir str aquí
type_name = type(content_obj).__name__
elif isinstance(content_obj, str) and type_tag == "result": # String resultado de una función
type_name = "str"
if type_name:
type_hint_str = f" [{type_name}]"
tag_to_apply = type_tag
# Aplicar tags especiales según el tipo de objeto
if type_tag == "result":
if isinstance(content_obj, Hex): tag_to_apply = "hex"
elif isinstance(content_obj, Bin): tag_to_apply = "bin"
elif isinstance(content_obj, IP4): tag_to_apply = "ip"
elif isinstance(content_obj, Date): tag_to_apply = "date"
elif isinstance(content_obj, Chr): tag_to_apply = "chr_type_tag"
if content_str or type_hint_str: # Solo insertar si hay algo que mostrar
if content_str: # Puede ser que solo haya hint si content_str es "" (para None, pero se evita)
self.output_text.insert(tk.END, content_str, tag_to_apply)
if type_hint_str:
self.output_text.insert(tk.END, type_hint_str, "type_hint")
first_part_on_line = False
# Agregar saltos de línea
if i < len(output_data_lines) - 1:
self.output_text.insert(tk.END, "\n")
elif i < num_input_lines - 1:
self.output_text.insert(tk.END, "\n")
self.output_text.config(state="disabled")
def save_persistent_input(self):
try:
content_to_save = self.input_text.get("1.0", tk.END).rstrip("\n")
if content_to_save:
with open("calc_input_history.txt", "w", encoding="utf-8") as f:
f.write(content_to_save)
elif os.path.exists("calc_input_history.txt"):
os.remove("calc_input_history.txt")
except Exception as e:
messagebox.showwarning("Error", f"No se pudo guardar el historial: {e}")
def load_persistent_input(self):
try:
if os.path.exists("calc_input_history.txt"):
with open("calc_input_history.txt", "r", encoding="utf-8") as f:
content = f.read()
if content:
self.input_text.insert("1.0", content)
self.root.after_idle(self._process_input_and_adjust_layout)
return
# Ejemplo por defecto
example_code = '''# Calculadora MAV - Ejemplos
# Evaluación directa de Python
2 + 2 * 3
pi * 2
sin(pi/2)
# Clases especiales
Hex("FF") + 1
IP4("192.168.1.1") + 255
# Nueva clase Chr para ASCII
Chr("A")
Chr("Hello")
Dec(66).toChr() # Devuelve Chr('B')
Hex(0x43).toChr() # Devuelve Chr('C')
# Ecuaciones algebraicas (usar comillas)
"x + 10 = 15"
"a*y - 5 = 20"
# Resolver ecuaciones
solve(x)
solve(y)
# Definir variables después
a = 3
# Evaluar expresiones con variables
y/25
# Expresiones simbólicas
b + c * 2
'''
self.input_text.insert("1.0", example_code)
self.root.after_idle(self._process_input_and_adjust_layout)
except Exception as e:
messagebox.showerror("Error", f"No se pudo cargar el historial o ejemplos: {e}")
def on_close(self):
self.save_persistent_input()
self._save_app_settings()
self.root.destroy()
def _process_input_and_adjust_layout(self):
"""Evalúa todas las líneas y luego ajusta el ancho del panel de entrada."""
self.evaluate_all_lines()
self._adjust_input_pane_width()
# --- Métodos actualizados para usar _process_input_and_adjust_layout ---
def on_key_release(self, event=None):
if self._debounce_job:
self.root.after_cancel(self._debounce_job)
if event and event.char == '.' and self.input_text.focus_get() == self.input_text:
self._handle_dot_autocomplete()
self._debounce_job = self.root.after(250, self._process_input_and_adjust_layout)
def _clear_input_text(self):
self.input_text.delete("1.0", tk.END)
self.sympy_integration.clear()
self._process_input_and_adjust_layout()
def _save_input_as(self): # Este método no cambia su lógica interna de guardado
filepath = filedialog.asksaveasfilename(
defaultextension=".txt",
filetypes=[("Text Files", "*.txt"), ("Python Files", "*.py"), ("All Files", "*.*")],
title="Guardar entrada como..."
)
if not filepath: return
try:
with open(filepath, "w", encoding="utf-8") as f:
f.write(self.input_text.get("1.0", tk.END))
except IOError as e:
messagebox.showerror("Error al guardar", f"No se pudo guardar el archivo:\n{e}")
def _load_input_from_file(self):
filepath = filedialog.askopenfilename(
filetypes=[("Text Files", "*.txt"), ("Python Files", "*.py"), ("All Files", "*.*")],
title="Cargar entrada desde archivo..."
)
if not filepath: return
try:
with open(filepath, "r", encoding="utf-8") as f: content = f.read()
self.input_text.delete("1.0", tk.END)
self.input_text.insert("1.0", content)
self._process_input_and_adjust_layout() # Evaluar y ajustar layout
except IOError as e: messagebox.showerror("Error al cargar", f"No se pudo cargar el archivo:\n{e}")
except Exception as e: messagebox.showerror("Error al procesar", f"Error al procesar el archivo:\n{e}")
def main():
root = tk.Tk()
app = CalculatorApp(root)
root.protocol("WM_DELETE_WINDOW", app.on_close)
try:
root.iconname("Calculadora MAV")
except tk.TclError:
pass
root.mainloop()
if __name__ == "__main__":
main()