# File: cpp_python_debug/gui/action_editor_window.py # Provides a Toplevel window for editing a single debug action. import tkinter as tk from tkinter import ttk, scrolledtext, messagebox import logging from typing import Dict, Any, Optional, List logger = logging.getLogger(__name__) class ActionEditorWindow(tk.Toplevel): """ A modal dialog for creating or editing a single debug action for a profile. """ def __init__(self, parent: tk.Widget, action_data: Optional[Dict[str, Any]] = None, is_new: bool = True): super().__init__(parent) self.parent_window = parent self.is_new_action = is_new self.result: Optional[Dict[str, Any]] = None # To store the action data on OK title = "Add New Action" if self.is_new_action else "Edit Action" self.title(title) self.geometry("600x550") # Adjusted size self.resizable(False, False) self.transient(parent) self.grab_set() # Make modal # --- StringVars and other Tkinter variables for action fields --- self.breakpoint_var = tk.StringVar() # variables_to_dump will be handled by ScrolledText 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.max_hits_var = tk.StringVar() # For later self._initial_action_data = action_data.copy() if action_data else None # Store original for cancel/comparison self._create_widgets() self._load_action_data(action_data) self.protocol("WM_DELETE_WINDOW", self._on_cancel) self.wait_window() # Important for modal dialog behavior to get result 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) # Make entry fields expand row_idx = 0 # Breakpoint Location ttk.Label(main_frame, text="Breakpoint Location:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=5) ttk.Entry(main_frame, textvariable=self.breakpoint_var, width=60).grid(row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=5) row_idx += 1 # MODIFIED LINE: 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) # nw for top-alignment self.variables_text = scrolledtext.ScrolledText(main_frame, wrap=tk.WORD, height=5, width=58, font=("Consolas", 9)) self.variables_text.grid(row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=5) row_idx += 1 # MODIFIED LINE: 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) 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) ttk.Entry(main_frame, textvariable=self.output_directory_var, width=50).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) ttk.Entry(main_frame, textvariable=self.filename_pattern_var, width=60).grid(row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=5) row_idx += 1 # MODIFIED LINE: 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) 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") 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: """Loads action data into the form fields.""" from ..gui.profile_manager_window import DEFAULT_ACTION # Lazy import for default data_to_load = action_data if action_data else DEFAULT_ACTION.copy() self.breakpoint_var.set(data_to_load.get("breakpoint_location", DEFAULT_ACTION["breakpoint_location"])) variables_list = data_to_load.get("variables_to_dump", DEFAULT_ACTION["variables_to_dump"]) if isinstance(variables_list, list): self.variables_text.insert(tk.END, "\n".join(variables_list)) else: # Fallback if it's not a list for some reason self.variables_text.insert(tk.END, str(variables_list)) self.output_format_var.set(data_to_load.get("output_format", DEFAULT_ACTION["output_format"])) self.output_directory_var.set(data_to_load.get("output_directory", DEFAULT_ACTION["output_directory"])) self.filename_pattern_var.set(data_to_load.get("filename_pattern", DEFAULT_ACTION["filename_pattern"])) self.continue_after_dump_var.set(data_to_load.get("continue_after_dump", DEFAULT_ACTION["continue_after_dump"])) def _browse_output_dir(self) -> None: 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 # Ensure dialog is on top ) if path: self.output_directory_var.set(path) def _validate_data(self) -> bool: """Validates the current form data.""" 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 # Basic check for common placeholders 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: """Handles the OK button click.""" 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 : # Double check after stripping 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() } # logger.debug(f"ActionEditorWindow result on OK: {self.result}") self.destroy() def _on_cancel(self) -> None: """Handles the Cancel button click or window close.""" self.result = None # Explicitly set result to None on cancel # logger.debug("ActionEditorWindow cancelled.") self.destroy() # Public method to get the result after the dialog closes def get_result(self) -> Optional[Dict[str, Any]]: return self.result