SXXXXXXX_CppPythonDebug/cpp_python_debug/gui/action_editor_window.py
2025-05-23 15:34:47 +02:00

330 lines
18 KiB
Python

# File: cpp_python_debug/gui/action_editor_window.py
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox, filedialog # Aggiunto filedialog se non c'era
import logging
import os # Aggiunto os
from typing import Dict, Any, Optional, List
# Import per la nuova funzionalità
from ..core.gdb_controller import GDBSession
from ..core.config_manager import AppSettings # Per ottenere gdb_path, timeouts, etc.
logger = logging.getLogger(__name__)
class FunctionSelectorDialog(tk.Toplevel):
"""Dialog to select a function from a list."""
def __init__(self, parent, functions_list: List[str], title="Select Function"):
super().__init__(parent)
self.title(title)
self.geometry("500x400")
self.transient(parent)
self.grab_set()
self.result: Optional[str] = None
list_frame = ttk.Frame(self, padding="10")
list_frame.pack(expand=True, fill=tk.BOTH)
list_frame.rowconfigure(0, weight=1)
list_frame.columnconfigure(0, weight=1)
self.listbox = tk.Listbox(list_frame, selectmode=tk.SINGLE)
self.listbox.grid(row=0, column=0, sticky="nsew")
scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.listbox.yview)
scrollbar.grid(row=0, column=1, sticky="ns")
self.listbox.configure(yscrollcommand=scrollbar.set)
for func_name in functions_list:
self.listbox.insert(tk.END, func_name)
self.listbox.bind("<Double-Button-1>", self._on_ok)
button_frame = ttk.Frame(self, padding=(10,0,10,10))
button_frame.pack(fill=tk.X)
ttk.Button(button_frame, text="OK", command=self._on_ok).pack(side=tk.RIGHT, padx=5)
ttk.Button(button_frame, text="Cancel", command=self._on_cancel).pack(side=tk.RIGHT)
if functions_list:
self.listbox.selection_set(0) # Seleziona il primo elemento
self.listbox.focus_set()
self.protocol("WM_DELETE_WINDOW", self._on_cancel)
self.wait_window()
def _on_ok(self, event=None): # Aggiunto event=None per il double-click
selection = self.listbox.curselection()
if selection:
self.result = self.listbox.get(selection[0])
self.destroy()
def _on_cancel(self):
self.result = None
self.destroy()
class ActionEditorWindow(tk.Toplevel):
def __init__(self, parent: tk.Widget,
action_data: Optional[Dict[str, Any]] = None,
is_new: bool = True,
target_executable_path: Optional[str] = None,
app_settings: Optional[AppSettings] = None,
symbol_analysis_data: Optional[Dict[str, Any]] = None): # NUOVO PARAMETRO
super().__init__(parent)
self.parent_window = parent
self.is_new_action = is_new
self.result: Optional[Dict[str, Any]] = None
self.target_executable_path = target_executable_path
self.app_settings = app_settings
self.symbol_analysis_data = symbol_analysis_data # NUOVO ATTRIBUTO
# ... (resto dell'init come prima) ...
title = "Add New Action" if self.is_new_action else "Edit Action"
self.title(title)
self.geometry("650x580") # Potrebbe servire aggiustare
self.resizable(False, False)
self.transient(parent)
self.grab_set()
self.breakpoint_var = tk.StringVar()
self.output_format_var = tk.StringVar()
self.output_directory_var = tk.StringVar()
self.filename_pattern_var = tk.StringVar()
self.continue_after_dump_var = tk.BooleanVar()
self._initial_action_data = action_data.copy() if action_data else None
self._create_widgets()
self._load_action_data(action_data)
self.protocol("WM_DELETE_WINDOW", self._on_cancel)
self.wait_window()
def _create_widgets(self) -> None:
main_frame = ttk.Frame(self, padding="15")
main_frame.pack(expand=True, fill=tk.BOTH)
main_frame.columnconfigure(1, weight=1) # Colonna per gli input si espande
row_idx = 0
# --- Breakpoint Location (Layout Corretto) ---
ttk.Label(main_frame, text="Breakpoint Location:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=5)
bp_input_frame = ttk.Frame(main_frame) # Frame per Entry e Bottone Browse
bp_input_frame.grid(row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=5)
bp_input_frame.columnconfigure(0, weight=1) # L'Entry si espande dentro questo frame
self.bp_entry = ttk.Entry(bp_input_frame, textvariable=self.breakpoint_var)
self.bp_entry.grid(row=0, column=0, sticky="ew", padx=(0,5)) # padx a destra per spaziare dal bottone
self.browse_funcs_button = ttk.Button(bp_input_frame, text="Browse Functions...", command=self._browse_functions)
self.browse_funcs_button.grid(row=0, column=1, sticky=tk.E) # Bottone a destra dell'entry
# Stato iniziale del bottone "Browse Functions..."
if not self.target_executable_path or not self.app_settings or \
not os.path.isfile(self.target_executable_path): # Aggiunto controllo isfile
self.browse_funcs_button.config(state=tk.DISABLED)
if self.target_executable_path and not os.path.isfile(self.target_executable_path):
logger.warning(f"Target executable '{self.target_executable_path}' not found, disabling function browser.")
row_idx += 1
# --- Fine Breakpoint Location ---
bp_help_label = ttk.Label(main_frame, text="(e.g., main, file.cpp:123, MyClass::foo)", foreground="gray")
bp_help_label.grid(row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0,10))
row_idx += 1
# Variables to Dump
ttk.Label(main_frame, text="Variables to Dump:").grid(row=row_idx, column=0, sticky="nw", padx=5, pady=5)
self.variables_text = scrolledtext.ScrolledText(main_frame, wrap=tk.WORD, height=5, width=58, font=("Consolas", 9)) # width era 58
self.variables_text.grid(row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=5)
row_idx += 1
vars_help_label = ttk.Label(main_frame, text="(One variable/expression per line)", foreground="gray")
vars_help_label.grid(row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0,10))
row_idx += 1
# Output Format
ttk.Label(main_frame, text="Output Format:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=5)
self.output_format_combo = ttk.Combobox(main_frame, textvariable=self.output_format_var, values=["json", "csv"], state="readonly", width=10)
self.output_format_combo.grid(row=row_idx, column=1, sticky="w", padx=5, pady=5) # columnspan rimosso, non necessario
row_idx += 1
# Output Directory
ttk.Label(main_frame, text="Output Directory:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=5)
self.output_dir_entry = ttk.Entry(main_frame, textvariable=self.output_directory_var) # width rimosso per farlo espandere con la colonna
self.output_dir_entry.grid(row=row_idx, column=1, sticky="ew", padx=5, pady=5)
ttk.Button(main_frame, text="Browse...", command=self._browse_output_dir).grid(row=row_idx, column=2, padx=5, pady=5)
row_idx += 1
# Filename Pattern
ttk.Label(main_frame, text="Filename Pattern:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=5)
self.filename_pattern_entry = ttk.Entry(main_frame, textvariable=self.filename_pattern_var) # width rimosso
self.filename_pattern_entry.grid(row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=5)
row_idx += 1
pattern_help_label = ttk.Label(main_frame, text="(Placeholders: {profile_name}, {breakpoint}, {variable}, {timestamp}, {format})", foreground="gray")
pattern_help_label.grid(row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0,10))
row_idx += 1
# Continue After Dump
self.continue_check = ttk.Checkbutton(main_frame, text="Continue execution after dump", variable=self.continue_after_dump_var)
self.continue_check.grid(row=row_idx, column=0, columnspan=3, sticky="w", padx=5, pady=10) # sticky w
row_idx += 1
# Buttons Frame
buttons_frame = ttk.Frame(main_frame)
buttons_frame.grid(row=row_idx, column=0, columnspan=3, pady=(15,5), sticky="e") # sticky e
ttk.Button(buttons_frame, text="OK", command=self._on_ok, width=10).pack(side=tk.RIGHT, padx=5)
ttk.Button(buttons_frame, text="Cancel", command=self._on_cancel, width=10).pack(side=tk.RIGHT, padx=5)
def _load_action_data(self, action_data: Optional[Dict[str, Any]]) -> None:
# Importa DEFAULT_ACTION qui se non è già globale o un attributo di classe
# Dobbiamo importarlo da ProfileManagerWindow o definirlo qui. Per ora, assumiamo sia disponibile.
# Idealmente, DEFAULT_ACTION dovrebbe essere in un modulo `defaults` o simile.
# Assumiamo per ora che sia stato passato correttamente o che ProfileManagerWindow lo gestisca.
# Per questo esempio, lo ridefinisco localmente se necessario.
_DEFAULT_ACTION_LOCAL = {
"breakpoint_location": "main", "variables_to_dump": ["my_variable"],
"output_format": "json", "output_directory": "./debug_dumps",
"filename_pattern": "{profile_name}_{breakpoint}_{variable}_{timestamp}.{format}",
"continue_after_dump": True
}
data_to_load = action_data if action_data else _DEFAULT_ACTION_LOCAL.copy()
self.breakpoint_var.set(data_to_load.get("breakpoint_location", _DEFAULT_ACTION_LOCAL["breakpoint_location"]))
variables_list = data_to_load.get("variables_to_dump", _DEFAULT_ACTION_LOCAL["variables_to_dump"])
if isinstance(variables_list, list):
self.variables_text.insert(tk.END, "\n".join(variables_list))
else:
self.variables_text.insert(tk.END, str(variables_list))
self.output_format_var.set(data_to_load.get("output_format", _DEFAULT_ACTION_LOCAL["output_format"]))
self.output_directory_var.set(data_to_load.get("output_directory", _DEFAULT_ACTION_LOCAL["output_directory"]))
self.filename_pattern_var.set(data_to_load.get("filename_pattern", _DEFAULT_ACTION_LOCAL["filename_pattern"]))
self.continue_after_dump_var.set(data_to_load.get("continue_after_dump", _DEFAULT_ACTION_LOCAL["continue_after_dump"]))
def _browse_output_dir(self) -> None: # Invariato
current_path = self.output_directory_var.get()
initial_dir = current_path if current_path and os.path.isdir(current_path) else None
path = filedialog.askdirectory(
title="Select Output Directory for Dumps",
initialdir=initial_dir,
parent=self
)
if path:
self.output_directory_var.set(path)
def _validate_data(self) -> bool: # Invariato
if not self.breakpoint_var.get().strip():
messagebox.showerror("Validation Error", "Breakpoint Location cannot be empty.", parent=self)
return False
variables_str = self.variables_text.get("1.0", tk.END).strip()
if not variables_str:
messagebox.showerror("Validation Error", "Variables to Dump cannot be empty.", parent=self)
return False
if not self.output_directory_var.get().strip():
messagebox.showerror("Validation Error", "Output Directory cannot be empty.", parent=self)
return False
if not self.filename_pattern_var.get().strip():
messagebox.showerror("Validation Error", "Filename Pattern cannot be empty.", parent=self)
return False
if not any(p in self.filename_pattern_var.get() for p in ["{breakpoint}", "{variable}", "{timestamp}"]):
messagebox.showwarning("Validation Warning",
"Filename Pattern seems to be missing common placeholders like {breakpoint}, {variable}, or {timestamp}. This might lead to overwritten files.",
parent=self)
return True
def _on_ok(self) -> None: # Invariato
if not self._validate_data():
return
variables_list = [line.strip() for line in self.variables_text.get("1.0", tk.END).strip().splitlines() if line.strip()]
if not variables_list :
messagebox.showerror("Validation Error", "Variables to Dump cannot be empty after processing.", parent=self)
return
self.result = {
"breakpoint_location": self.breakpoint_var.get().strip(),
"variables_to_dump": variables_list,
"output_format": self.output_format_var.get(),
"output_directory": self.output_directory_var.get().strip(),
"filename_pattern": self.filename_pattern_var.get().strip(),
"continue_after_dump": self.continue_after_dump_var.get()
}
self.destroy()
def _on_cancel(self) -> None: # Invariato
self.result = None
self.destroy()
def get_result(self) -> Optional[Dict[str, Any]]: # Invariato
return self.result
# --- NUOVA FUNZIONE ---
def _browse_functions(self) -> None:
# --- LOGICA AGGIORNATA ---
functions_to_show: List[str] = []
source_of_functions = "live GDB query" # Default
# 1. Prova a usare i dati di analisi dei simboli pre-calcolati, se validi
if self.symbol_analysis_data and isinstance(self.symbol_analysis_data, dict):
# Verifica che i dati di analisi siano per l'eseguibile corrente
# (confronto semplice del path, idealmente si usa checksum/timestamp in futuro)
analyzed_exe = self.symbol_analysis_data.get("analyzed_executable_path")
if analyzed_exe and os.path.normpath(analyzed_exe) == os.path.normpath(str(self.target_executable_path)): # Confronta path normalizzati
cached_functions = self.symbol_analysis_data.get("symbols", {}).get("functions", [])
if isinstance(cached_functions, list) and cached_functions: # Se abbiamo funzioni cachate
functions_to_show = cached_functions
source_of_functions = "cached analysis"
logger.info(f"Using {len(functions_to_show)} cached functions for browsing.")
else:
logger.info("Cached symbol analysis is for a different executable or path mismatch. Will perform live query if target is valid.")
if analyzed_exe: logger.debug(f"Analyzed exe: '{analyzed_exe}', Current target: '{self.target_executable_path}'")
# 2. Se non ci sono funzioni cachate valide, esegui una query live a GDB
if not functions_to_show:
if not self.target_executable_path or not os.path.isfile(self.target_executable_path):
messagebox.showerror("Error", "Target executable for the profile is not set or invalid. Cannot browse functions.", parent=self)
return
if not self.app_settings:
messagebox.showerror("Error", "Application settings not available. Cannot determine GDB path.", parent=self)
return
gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path")
if not gdb_exe_path or not os.path.isfile(gdb_exe_path):
messagebox.showerror("GDB Error", f"GDB executable not found or not configured: {gdb_exe_path}", parent=self)
return
self.config(cursor="watch")
self.update_idletasks()
temp_gdb_session: Optional[GDBSession] = None
live_query_error = False
try:
logger.info(f"Performing live GDB query for functions. Target: {self.target_executable_path}")
temp_gdb_session = GDBSession(gdb_exe_path, self.target_executable_path, None, {})
startup_timeout = self.app_settings.get_setting("timeouts", "gdb_start", 30) // 2
command_timeout = self.app_settings.get_setting("timeouts", "gdb_command", 30) // 2
temp_gdb_session.start(timeout=startup_timeout)
if not temp_gdb_session.symbols_found:
messagebox.showwarning("No Debug Symbols",
"GDB reported no debugging symbols in the executable. "
"The function list (live query) might be empty or incomplete.", parent=self)
functions_to_show = temp_gdb_session.list_functions(timeout=command_timeout)
except Exception as e:
logger.error(f"Error during live function query: {e}", exc_info=True)
messagebox.showerror("GDB Query Error", f"Could not retrieve functions from GDB: {e}", parent=self)
live_query_error = True
finally:
if temp_gdb_session and temp_gdb_session.is_alive():
quit_timeout = self.app_settings.get_setting("timeouts", "gdb_quit", 10) // 2
try: temp_gdb_session.quit(timeout=quit_timeout)
except Exception: pass # Ignora errori nel quit della sessione temporanea
self.config(cursor="")
if live_query_error: return # Non mostrare la dialog se c'è stato un errore
# 3. Mostra la dialog con le funzioni (cachate o da query live)
if functions_to_show:
logger.info(f"Displaying FunctionSelectorDialog with {len(functions_to_show)} functions from '{source_of_functions}'.")
dialog = FunctionSelectorDialog(self, functions_to_show, title=f"Select Function (from {source_of_functions})")
selected_function = dialog.result
if selected_function:
self.breakpoint_var.set(selected_function)
else:
messagebox.showinfo("No Functions", f"No functions found (source: {source_of_functions}). Ensure target is compiled with debug symbols.", parent=self)