From 326baf3c0375ba53d16c0cbca8fb337380462557 Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Thu, 8 May 2025 10:47:33 +0200 Subject: [PATCH] fix edit dialogs and run sequence, add version --- launchertool.ico | Bin 0 -> 47321 bytes launchertool/_version.py | 90 ++++++ launchertool/core/execution_handler.py | 3 +- .../gui/dialogs/application_dialogs.py | 271 ++++++++++++------ launchertool/gui/dialogs/parameter_dialog.py | 112 ++++++-- launchertool/gui/dialogs/sequence_dialogs.py | 204 ++++++++----- launchertool/gui/dialogs/step_dialogs.py | 169 ++++++----- launchertool/gui/main_window.py | 38 ++- 8 files changed, 634 insertions(+), 253 deletions(-) create mode 100644 launchertool/_version.py diff --git a/launchertool.ico b/launchertool.ico index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..019963fc74b031cc64cded5854765adf56abc0a0 100644 GIT binary patch literal 47321 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�XIz1XD57;tVNP&M!q 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 VFEy?n{4)Rm002ovPDHLkV1fXl+qM7z literal 0 HcmV?d00001 diff --git a/launchertool/_version.py b/launchertool/_version.py new file mode 100644 index 0000000..d23696a --- /dev/null +++ b/launchertool/_version.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# File generated by PyInstaller GUI Wrapper. DO NOT EDIT MANUALLY. +# Contains build-time information scraped from Git (if available) +# and a helper function to format version strings. + + +import re + +# --- Version Data (Generated) --- +# This section is automatically generated by the build process. +__version__ = "0339a04-dirty" +GIT_COMMIT_HASH = "0339a048f09f221e17d3d368e5efd52048ef44c7" +GIT_BRANCH = "master" +BUILD_TIMESTAMP = "2025-05-08T08:43:30Z" +IS_GIT_REPO = True + +# --- Default Values (for comparison or fallback) --- +DEFAULT_VERSION = "0.0.0+unknown" +DEFAULT_COMMIT = "Unknown" +DEFAULT_BRANCH = "Unknown" + +# --- Helper Function --- +def get_version_string(format_string=None): + """ + Returns a formatted string based on the build version information. + + Args: + format_string (str, optional): A format string using placeholders. + Defaults to "{{version}} ({{branch}}/{{commit_short}})" if None. + Placeholders: + {{version}}: Full version string (e.g., 'v1.0.0-5-gabcdef-dirty') + {{tag}}: Clean tag part if exists (e.g., 'v1.0.0'), else DEFAULT_VERSION. + {{commit}}: Full Git commit hash. + {{commit_short}}: Short Git commit hash (7 chars). + {{branch}}: Git branch name. + {{dirty}}: '-dirty' if the repo was dirty, empty otherwise. + {{timestamp}}: Full build timestamp (ISO 8601 UTC). + {{timestamp_short}}: Build date only (YYYY-MM-DD). + {{is_git}}: 'Git' if IS_GIT_REPO is True, 'Unknown' otherwise. + + Returns: + str: The formatted version string, or an error message if formatting fails. + """ + if format_string is None: + format_string = "{version} ({branch}/{commit_short})" # Sensible default + + replacements = {} + try: + # Prepare data dictionary for substitution + replacements['version'] = __version__ if __version__ else DEFAULT_VERSION + replacements['commit'] = GIT_COMMIT_HASH if GIT_COMMIT_HASH else DEFAULT_COMMIT + replacements['commit_short'] = GIT_COMMIT_HASH[:7] if GIT_COMMIT_HASH and len(GIT_COMMIT_HASH) >= 7 else DEFAULT_COMMIT + replacements['branch'] = GIT_BRANCH if GIT_BRANCH else DEFAULT_BRANCH + replacements['timestamp'] = BUILD_TIMESTAMP if BUILD_TIMESTAMP else "Unknown" + replacements['timestamp_short'] = BUILD_TIMESTAMP.split('T')[0] if BUILD_TIMESTAMP and 'T' in BUILD_TIMESTAMP else "Unknown" + replacements['is_git'] = "Git" if IS_GIT_REPO else "Unknown" + replacements['dirty'] = "-dirty" if __version__ and __version__.endswith('-dirty') else "" + + # Extract clean tag using regex (handles versions like v1.0.0, 1.0.0) + tag = DEFAULT_VERSION + if __version__ and IS_GIT_REPO: + # Match optional 'v' prefix, then major.minor.patch + match = re.match(r'^(v?([0-9]+)\.([0-9]+)\.([0-9]+))', __version__) + if match: + tag = match.group(1) # Get the full tag (e.g., 'v1.0.0') + replacements['tag'] = tag + + # Perform substitution using regex to find placeholders {placeholder} + output_string = format_string + # Iterate through placeholders and replace them in the format string + for placeholder, value in replacements.items(): + # Compile regex pattern for {placeholder}, allowing for whitespace inside braces + pattern = re.compile(r'{\s*' + re.escape(placeholder) + r'\s*}') + # Substitute found patterns with the corresponding string value + output_string = pattern.sub(str(value), output_string) + + # Optional: Check if any placeholders remain unsubstituted (could indicate typo) + if re.search(r'{\s*[\w_]+\s*}', output_string): + # You might want to log this or handle it, for now, we return the string as is + # print(f"Warning: Unsubstituted placeholders remain in version string: {output_string}") + pass + + return output_string + + except Exception as e: + # Return a simple error message in case of unexpected formatting issues + # Avoid printing directly from this generated function + return f"[Formatting Error: {e}]" + + diff --git a/launchertool/core/execution_handler.py b/launchertool/core/execution_handler.py index 310041d..f6bea09 100644 --- a/launchertool/core/execution_handler.py +++ b/launchertool/core/execution_handler.py @@ -248,9 +248,10 @@ class ExecutionHandler: self._log_and_output(f"Waiting {wait_time_seconds} seconds before next step...") + delay_ms = int(wait_time_seconds * 1000) # Schedule the next step self.step_delay_callback( - wait_time_seconds * 1000, # Convert to milliseconds for Tkinter's 'after' + delay_ms, self._execute_step, steps, step_index + 1 diff --git a/launchertool/gui/dialogs/application_dialogs.py b/launchertool/gui/dialogs/application_dialogs.py index d46b9a6..fe1ddff 100644 --- a/launchertool/gui/dialogs/application_dialogs.py +++ b/launchertool/gui/dialogs/application_dialogs.py @@ -5,12 +5,12 @@ Dialogs for adding and editing applications. import tkinter as tk from tkinter import ttk, messagebox, filedialog import logging -from typing import Optional, List, Dict, Any +from typing import Optional, List, Dict, Any, Callable # Aggiunto Callable -from launchertool.core.config_manager import ConfigManager -from launchertool.core.exceptions import DuplicateNameError, NameNotFoundError, ConfigError -from launchertool.gui.utils_gui import GuiUtils -from launchertool.gui.dialogs.parameter_dialog import AddParameterDialog, EditParameterDialog +from ...core.config_manager import ConfigManager +from ...core.exceptions import DuplicateNameError, NameNotFoundError, ConfigError, ApplicationNotFoundError +from ..utils_gui import GuiUtils +from ...gui.dialogs.parameter_dialog import AddParameterDialog, EditParameterDialog logger = logging.getLogger(__name__) @@ -19,28 +19,45 @@ class BaseApplicationDialog(tk.Toplevel): Base class for Add and Edit Application dialogs. Provides common widgets and functionality. """ - def __init__(self, parent: tk.Widget, config_manager: ConfigManager, title: str): + def __init__(self, parent: tk.Widget, config_manager: ConfigManager, title: str, load_data_method: Optional[Callable[[], None]] = None): + logger.debug(f"BaseApplicationDialog __init__ - START - Title: '{title}'") super().__init__(parent) self.transient(parent) self.title(title) - self.parent_widget = parent # Explicitly store parent for messagebox context + self.parent_widget = parent self.config_manager = config_manager self.result: Optional[Dict[str, Any]] = None - self.original_app_name: Optional[str] = None + # self.original_app_name è definito e gestito dalle sottoclassi prima di chiamare questo costruttore base, + # se necessario per load_data_method. + + logger.debug("BaseApplicationDialog: Calling _setup_widgets()") + self._setup_widgets() # Crea tutti i widget + logger.debug("BaseApplicationDialog: _setup_widgets() completed.") + + # Carica i dati specifici della sottoclasse (es. per EditDialog) PRIMA di rendere modale e attendere + if load_data_method: + logger.debug("BaseApplicationDialog: Calling provided load_data_method.") + load_data_method() + logger.debug("BaseApplicationDialog: load_data_method completed.") + else: + logger.debug("BaseApplicationDialog: No load_data_method provided or needed.") - self._setup_widgets() GuiUtils.center_window(self, parent) self.protocol("WM_DELETE_WINDOW", self._on_cancel) - self.grab_set() - self.focus_set() - self.wait_window(self) + + self.grab_set() # Rendi modale + self.focus_set() # Dai focus + logger.debug(f"BaseApplicationDialog __init__ for '{title}': Now calling wait_window().") + self.wait_window(self) # Blocca qui finché il dialogo non viene distrutto + logger.debug(f"BaseApplicationDialog __init__ - END - Title: '{title}' (after wait_window)") def _setup_widgets(self): main_frame = ttk.Frame(self, padding="10") main_frame.pack(expand=True, fill=tk.BOTH) + # --- Application Name --- name_frame = ttk.Frame(main_frame) name_frame.pack(fill=tk.X, pady=(0, 5)) ttk.Label(name_frame, text="Application Name:").pack(side=tk.LEFT, padx=(0, 5)) @@ -48,6 +65,7 @@ class BaseApplicationDialog(tk.Toplevel): self.name_entry = ttk.Entry(name_frame, textvariable=self.name_entry_var, width=50) self.name_entry.pack(side=tk.LEFT, expand=True, fill=tk.X) + # --- Application Path --- path_frame = ttk.Frame(main_frame) path_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Label(path_frame, text="Application Path:").pack(side=tk.LEFT, padx=(0, 5)) @@ -57,10 +75,11 @@ class BaseApplicationDialog(tk.Toplevel): self.browse_button = ttk.Button(path_frame, text="Browse...", command=self._browse_file) self.browse_button.pack(side=tk.LEFT) + # --- Parameters --- params_labelframe = ttk.LabelFrame(main_frame, text="Parameters") params_labelframe.pack(expand=True, fill=tk.BOTH, pady=(0, 10)) - params_tree_frame = ttk.Frame(params_labelframe) + params_tree_frame = ttk.Frame(params_labelframe) # Frame to hold tree and scrollbar params_tree_frame.pack(expand=True, fill=tk.BOTH, padx=5, pady=5) param_scrollbar_y = ttk.Scrollbar(params_tree_frame, orient=tk.VERTICAL) @@ -102,11 +121,12 @@ class BaseApplicationDialog(tk.Toplevel): self.delete_param_button.pack(side=tk.LEFT) self.params_tree.bind("<>", self._on_parameter_select) - self._on_parameter_select() + self._on_parameter_select() # Init button states + # --- Dialog Buttons (Save, Cancel) --- dialog_buttons_frame = ttk.Frame(main_frame) dialog_buttons_frame.pack(fill=tk.X, pady=(10, 0)) - ttk.Frame(dialog_buttons_frame).pack(side=tk.LEFT, expand=True) + ttk.Frame(dialog_buttons_frame).pack(side=tk.LEFT, expand=True) # Spacer self.save_button = ttk.Button(dialog_buttons_frame, text="Save", command=self._on_save) self.save_button.pack(side=tk.LEFT, padx=(0,5)) @@ -117,7 +137,7 @@ class BaseApplicationDialog(tk.Toplevel): filepath = filedialog.askopenfilename( title="Select Application Executable", filetypes=(("Executable files", "*.exe"), ("All files", "*.*")), - parent=self # Ensure dialog is modal to this Toplevel + parent=self ) if filepath: self.path_entry_var.set(filepath) @@ -128,7 +148,7 @@ class BaseApplicationDialog(tk.Toplevel): if app_name_suggestion: self.name_entry_var.set(app_name_suggestion) except Exception: - pass + pass # Ignore errors in suggestion def _get_parameters_from_tree(self) -> List[Dict[str, str]]: parameters = [] @@ -145,24 +165,29 @@ class BaseApplicationDialog(tk.Toplevel): def _populate_parameters_tree(self, parameters: List[Dict[str, str]]): for item in self.params_tree.get_children(): self.params_tree.delete(item) + + logger.debug(f"BaseApplicationDialog: Populating parameters tree with {len(parameters)} parameters.") + for i, param in enumerate(parameters): - # Use param name for iid if unique, otherwise generate one param_name = param.get('name','') - iid = param_name if param_name else f"param_gen_{i}" - # Ensure iid is unique if names can be non-unique (though they shouldn't) + description = param.get('description','') + default_value = param.get('default_value','') + param_type = param.get('type','string') + + iid_base = param_name if param_name and param_name.strip() else f"param_gen_{i}" # Ensure non-empty iid_base + + final_iid = iid_base counter = 0 - final_iid = iid + # Ensure iid is unique in the tree while self.params_tree.exists(final_iid): counter += 1 - final_iid = f"{iid}_{counter}" - + final_iid = f"{iid_base}_{counter}" + + logger.debug(f"BaseApplicationDialog: Inserting parameter: iid='{final_iid}', values=('{param_name}', '{description}', '{default_value}', '{param_type}')") self.params_tree.insert( '', tk.END, - iid=final_iid, - values=(param_name, - param.get('description',''), - param.get('default_value',''), - param.get('type','string')) + iid=final_iid, + values=(param_name, description, default_value, param_type) ) self._on_parameter_select() @@ -173,11 +198,11 @@ class BaseApplicationDialog(tk.Toplevel): self.delete_param_button.config(state=state) def _add_parameter(self): - logger.debug("Add Parameter button clicked.") - dialog = AddParameterDialog(self) + logger.debug("BaseApplicationDialog: Add Parameter button clicked.") + dialog = AddParameterDialog(self) # 'self' (BaseApplicationDialog) is the parent if dialog.result: - # Check for duplicate parameter name before adding to tree new_param_name = dialog.result['name'] + # Check for duplicate parameter name before adding to tree for item_id in self.params_tree.get_children(): if self.params_tree.item(item_id, 'values')[0] == new_param_name: messagebox.showerror("Duplicate Parameter", @@ -185,14 +210,14 @@ class BaseApplicationDialog(tk.Toplevel): parent=self) return - param_iid = new_param_name # Use name as iid, assuming unique - self.params_tree.insert('', tk.END, iid=param_iid, values=( + # Use the (now validated unique) new_param_name as iid + self.params_tree.insert('', tk.END, iid=new_param_name, values=( new_param_name, dialog.result['description'], dialog.result['default_value'], dialog.result['type'] )) - logger.info(f"Parameter '{new_param_name}' added to application dialog tree.") + logger.info(f"BaseApplicationDialog: Parameter '{new_param_name}' added to this dialog's parameter tree.") self._on_parameter_select() def _edit_parameter(self): @@ -201,7 +226,7 @@ class BaseApplicationDialog(tk.Toplevel): messagebox.showwarning("Edit Parameter", "Please select a parameter to edit.", parent=self) return - selected_iid = selected_item_ids[0] + selected_iid = selected_item_ids[0] # This is the iid of the selected item item_values = self.params_tree.item(selected_iid, 'values') param_data_to_edit = { @@ -210,39 +235,45 @@ class BaseApplicationDialog(tk.Toplevel): "default_value": item_values[2], "type": item_values[3] } - original_param_name = param_data_to_edit["name"] - logger.debug(f"Edit Parameter button clicked for: {original_param_name}") + original_param_name = param_data_to_edit["name"] # This is the name before editing + logger.debug(f"BaseApplicationDialog: Edit Parameter button clicked for: {original_param_name} (iid: {selected_iid})") - dialog = EditParameterDialog(self, param_data_to_edit) + dialog = EditParameterDialog(self, param_data_to_edit) # 'self' is the parent if dialog.result: new_param_name = dialog.result['name'] - # Check for duplicate name if name changed + + # Check for duplicate name if the name was changed if new_param_name != original_param_name: for item_id in self.params_tree.get_children(): + # Don't compare the item with itself (if its iid hasn't changed yet) + # Or, more robustly, check all OTHER items if item_id != selected_iid and self.params_tree.item(item_id, 'values')[0] == new_param_name: messagebox.showerror("Duplicate Parameter", - f"A parameter with the name '{new_param_name}' already exists.", + f"Another parameter with the name '{new_param_name}' already exists.", parent=self) return - # If name changed, we need to delete old and insert new, or re-tag. Simpler to update values & iid. - self.params_tree.item(selected_iid, values=( + new_values = ( new_param_name, dialog.result['description'], dialog.result['default_value'], dialog.result['type'] - )) - # If iid was based on name, and name changes, treeview might get confused. - # It's safer if iid is independent or handled carefully on name change. - # For now, assume iid can be the new name IF it's unique. - if new_param_name != original_param_name: - # Detach and re-insert with new iid if name (used as iid) changed - values = self.params_tree.item(selected_iid, 'values') + ) + + # If name changed AND iid was the original name, we need to re-insert with new iid + if new_param_name != original_param_name and selected_iid == original_param_name: + logger.debug(f"Parameter name changed from '{original_param_name}' to '{new_param_name}'. Re-inserting in tree.") self.params_tree.delete(selected_iid) - self.params_tree.insert('', tk.END, iid=new_param_name, values=values) - self.params_tree.selection_set(new_param_name) # Reselect + self.params_tree.insert('', tk.END, iid=new_param_name, values=new_values) + self.params_tree.selection_set(new_param_name) # Reselect the (new) item + else: + # Name didn't change, or iid was not the name, so just update values for the current iid + self.params_tree.item(selected_iid, values=new_values) + logger.debug(f"Parameter values updated for iid '{selected_iid}'. New name: '{new_param_name}'.") + + logger.info(f"BaseApplicationDialog: Parameter '{new_param_name}' (originally '{original_param_name}') updated.") + self._on_parameter_select() # Refresh button states - logger.info(f"Parameter '{new_param_name}' updated in application dialog tree.") def _delete_parameter(self): selected_item_ids = self.params_tree.selection() @@ -255,7 +286,7 @@ class BaseApplicationDialog(tk.Toplevel): f"Are you sure you want to delete parameter '{param_name}'?", parent=self): self.params_tree.delete(selected_item_ids[0]) - logger.debug(f"Parameter '{param_name}' deleted from tree.") + logger.debug(f"BaseApplicationDialog: Parameter '{param_name}' deleted from this dialog's tree.") self._on_parameter_select() @@ -274,18 +305,23 @@ class BaseApplicationDialog(tk.Toplevel): return True def _on_save(self): + # This method should be implemented by subclasses (AddApplicationDialog, EditApplicationDialog) raise NotImplementedError("Subclasses must implement _on_save") def _on_cancel(self): - logger.debug(f"{self.title()} cancelled or closed.") - self.result = None + logger.debug(f"BaseApplicationDialog: '{self.title()}' cancelled or closed by user.") + self.result = None # Explicitly set result to None for cancellation self.destroy() class AddApplicationDialog(BaseApplicationDialog): """Dialog for adding a new application.""" def __init__(self, parent: tk.Widget, config_manager: ConfigManager): - super().__init__(parent, config_manager, title="Add New Application") + # AddApplicationDialog does not need to load initial data from an existing app, + # so load_data_method is None. + super().__init__(parent, config_manager, title="Add New Application", load_data_method=None) + logger.debug("AddApplicationDialog __init__ completed.") + def _on_save(self): if not self._validate_inputs(): @@ -293,7 +329,7 @@ class AddApplicationDialog(BaseApplicationDialog): app_name = self.name_entry_var.get().strip() app_path = self.path_entry_var.get().strip() - parameters = self._get_parameters_from_tree() + parameters = self._get_parameters_from_tree() # Gets params currently in this dialog's tree application_data = { "name": app_name, @@ -303,52 +339,99 @@ class AddApplicationDialog(BaseApplicationDialog): try: self.config_manager.add_application(application_data) - self.result = application_data - logger.info(f"Application '{app_name}' added successfully through dialog.") - # Message shown by MainWindow after successful dialog.result - # messagebox.showinfo("Success", f"Application '{app_name}' added successfully.", parent=self.parent_widget) - self.destroy() + self.result = application_data # Set result upon successful save to ConfigManager + logger.info(f"AddApplicationDialog: Application '{app_name}' added successfully to config.") + self.destroy() # Close dialog except DuplicateNameError as e: - logger.warning(f"Failed to add application: {e}") + logger.warning(f"AddApplicationDialog: Failed to add application due to duplicate name: {e}") messagebox.showerror("Error Adding Application", str(e), parent=self) self.name_entry.focus_set() - except ConfigError as e: - logger.error(f"Configuration error adding application '{app_name}': {e}", exc_info=True) + except ConfigError as e: # Catch other config related errors (e.g., save failed) + logger.error(f"AddApplicationDialog: Configuration error adding application '{app_name}': {e}", exc_info=True) messagebox.showerror("Configuration Error", f"Could not save application:\n{e}", parent=self) - except Exception as e: - logger.error(f"Unexpected error adding application '{app_name}': {e}", exc_info=True) + except Exception as e: # Catch any other unexpected errors + logger.error(f"AddApplicationDialog: Unexpected error adding application '{app_name}': {e}", exc_info=True) messagebox.showerror("Unexpected Error", f"An unexpected error occurred:\n{e}", parent=self) class EditApplicationDialog(BaseApplicationDialog): """Dialog for editing an existing application.""" def __init__(self, parent: tk.Widget, config_manager: ConfigManager, application_name_to_edit: str): - self.original_app_name = application_name_to_edit - super().__init__(parent, config_manager, title=f"Edit Application: {application_name_to_edit}") - self._load_initial_data() + logger.debug(f"EditApplicationDialog __init__ - START - App to edit: '{application_name_to_edit}'") + self.original_app_name = application_name_to_edit # Must be set BEFORE super().__init__ if load_data_method uses it + + # Pass its own _load_initial_data method to the base class constructor + super().__init__(parent, config_manager, title=f"Edit Application: {self.original_app_name}", load_data_method=self._load_initial_data) + + # Code here (after super call) executes only after the dialog (and wait_window in base) has finished. + # Usually, nothing more is needed here as 'result' is checked by the caller (MainWindow). + logger.debug(f"EditApplicationDialog __init__ - END - for '{self.original_app_name}' (after super call has completed)") + def _load_initial_data(self): + # This method is now called by BaseApplicationDialog's __init__ via load_data_method + if not self.original_app_name: + # This case should ideally be caught before even calling super() in __init__ + # or by BaseApplicationDialog if load_data_method is None when it shouldn't be. + logger.error("EditApplicationDialog _load_initial_data: original_app_name is None or empty. Dialog should be closing.") + # If parent_widget is None (though it shouldn't be), messagebox might fail. + if self.parent_widget: + messagebox.showerror("Initialization Error", + "Cannot edit: No application name specified for editing.", + parent=self.parent_widget) + self.after_idle(self.destroy) # Ensure dialog closes + return + + logger.debug(f"EditApplicationDialog _load_initial_data: Attempting to load for: '{self.original_app_name}'") try: app_data = self.config_manager.get_application_by_name(self.original_app_name) + logger.debug(f"EditApplicationDialog _load_initial_data: Data from ConfigManager for '{self.original_app_name}': {app_data}") + + if not app_data: # Should be caught by NameNotFoundError, but as a safeguard + logger.error(f"EditApplicationDialog _load_initial_data: ConfigManager returned None (or empty) for '{self.original_app_name}'.") + messagebox.showerror("Load Error", + f"Could not retrieve valid data for application '{self.original_app_name}'.", + parent=self.parent_widget) + self.after_idle(self.destroy) + return + + # Populate the dialog's widgets with the loaded data self.name_entry_var.set(app_data.get("name", "")) self.path_entry_var.set(app_data.get("path", "")) - self._populate_parameters_tree(app_data.get("parameters", [])) - except NameNotFoundError: - logger.error(f"Cannot edit application. Name '{self.original_app_name}' not found.") - messagebox.showerror("Error", f"Application '{self.original_app_name}' not found. Cannot edit.", parent=self.parent_widget) - self.destroy() - except Exception as e: - logger.error(f"Unexpected error loading application data for edit: {e}", exc_info=True) - messagebox.showerror("Error", f"Could not load application data for editing:\n{e}", parent=self.parent_widget) - self.destroy() + logger.debug(f"EditApplicationDialog _load_initial_data: Name set to '{self.name_entry_var.get()}', Path to '{self.path_entry_var.get()}'") + parameters_data = app_data.get("parameters", []) + logger.debug(f"EditApplicationDialog _load_initial_data: Parameters to populate: {parameters_data}") + self._populate_parameters_tree(parameters_data) # Call base method to fill param tree + + logger.info(f"EditApplicationDialog _load_initial_data: Successfully loaded data for '{self.original_app_name}'.") + + except NameNotFoundError: + logger.error(f"EditApplicationDialog _load_initial_data: App '{self.original_app_name}' not found (NameNotFoundError).") + messagebox.showerror("Load Error", + f"Application '{self.original_app_name}' not found. Cannot edit.", + parent=self.parent_widget) # parent_widget is MainWindow + self.after_idle(self.destroy) + except Exception as e: + logger.error(f"EditApplicationDialog _load_initial_data: Unexpected error for '{self.original_app_name}': {e}", exc_info=True) + messagebox.showerror("Load Error", + f"An unexpected error occurred while loading data for '{self.original_app_name}':\n{e}", + parent=self.parent_widget) + self.after_idle(self.destroy) + def _on_save(self): - if not self._validate_inputs(): + # Ensure original_app_name is valid; if _load_initial_data failed, it might not be safe to save. + if not self.original_app_name: + messagebox.showerror("Save Error", "Cannot save: Critical information about the original application is missing.", parent=self) + logger.error("EditApplicationDialog _on_save: Attempted to save without a valid original_app_name.") + return + + if not self._validate_inputs(): # Validates current entries in the dialog return new_app_name = self.name_entry_var.get().strip() new_app_path = self.path_entry_var.get().strip() - new_parameters = self._get_parameters_from_tree() + new_parameters = self._get_parameters_from_tree() # Gets params currently in this dialog's tree updated_application_data = { "name": new_app_name, @@ -357,21 +440,21 @@ class EditApplicationDialog(BaseApplicationDialog): } try: + # Use self.original_app_name to identify which app to update in ConfigManager self.config_manager.update_application(self.original_app_name, updated_application_data) - self.result = updated_application_data - logger.info(f"Application '{self.original_app_name}' updated to '{new_app_name}' successfully through dialog.") - # messagebox.showinfo("Success", f"Application '{new_app_name}' updated successfully.", parent=self.parent_widget) - self.destroy() - except NameNotFoundError as e: - logger.error(f"Update failed: Original application '{self.original_app_name}' not found: {e}", exc_info=True) + self.result = updated_application_data # Set result to the successfully updated data + logger.info(f"EditApplicationDialog: Application '{self.original_app_name}' updated to '{new_app_name}' in config.") + self.destroy() # Close dialog + except NameNotFoundError as e: # If original_app_name is somehow no longer in config + logger.error(f"EditApplicationDialog _on_save: Original app '{self.original_app_name}' no longer found: {e}", exc_info=True) messagebox.showerror("Error Updating Application", str(e), parent=self) - except DuplicateNameError as e: - logger.warning(f"Failed to update application: {e}") + except DuplicateNameError as e: # If new_app_name conflicts with another existing app + logger.warning(f"EditApplicationDialog _on_save: Failed to update due to duplicate name: {e}") messagebox.showerror("Error Updating Application", str(e), parent=self) - self.name_entry.focus_set() - except ConfigError as e: - logger.error(f"Configuration error updating application '{new_app_name}': {e}", exc_info=True) + self.name_entry.focus_set() # Keep dialog open and focus on name field + except ConfigError as e: # Other config errors (e.g., save failed) + logger.error(f"EditApplicationDialog _on_save: Config error updating '{new_app_name}': {e}", exc_info=True) messagebox.showerror("Configuration Error", f"Could not save application updates:\n{e}", parent=self) - except Exception as e: - logger.error(f"Unexpected error updating application '{new_app_name}': {e}", exc_info=True) + except Exception as e: # Catch-all for other unexpected errors + logger.error(f"EditApplicationDialog _on_save: Unexpected error updating '{new_app_name}': {e}", exc_info=True) messagebox.showerror("Unexpected Error", f"An unexpected error occurred:\n{e}", parent=self) \ No newline at end of file diff --git a/launchertool/gui/dialogs/parameter_dialog.py b/launchertool/gui/dialogs/parameter_dialog.py index 08207bf..1158fd0 100644 --- a/launchertool/gui/dialogs/parameter_dialog.py +++ b/launchertool/gui/dialogs/parameter_dialog.py @@ -5,7 +5,7 @@ Dialogs for adding and editing application parameters. import tkinter as tk from tkinter import ttk, messagebox import logging -from typing import Optional, Dict, Any +from typing import Optional, Dict, Any, Callable # Aggiunto Callable from ..utils_gui import GuiUtils @@ -15,7 +15,8 @@ class BaseParameterDialog(tk.Toplevel): """ Base class for Add and Edit Parameter dialogs. """ - def __init__(self, parent: tk.Widget, title: str): + def __init__(self, parent: tk.Widget, title: str, load_data_method: Optional[Callable[[], None]] = None): # Aggiunto load_data_method + logger.debug(f"BaseParameterDialog __init__ - START - Title: '{title}'") super().__init__(parent) self.transient(parent) self.title(title) @@ -24,18 +25,33 @@ class BaseParameterDialog(tk.Toplevel): self._parameter_types = ["string", "integer", "boolean", "float", "file", "folder"] + logger.debug("BaseParameterDialog: Calling _setup_widgets()") self._setup_widgets() + logger.debug("BaseParameterDialog: _setup_widgets() completed.") + + # === MODIFICA CHIAVE === + if load_data_method: + logger.debug("BaseParameterDialog: Calling provided load_data_method.") + load_data_method() + logger.debug("BaseParameterDialog: load_data_method completed.") + else: + logger.debug("BaseParameterDialog: No load_data_method provided.") + # ======================= GuiUtils.center_window(self, parent) self.protocol("WM_DELETE_WINDOW", self._on_cancel) + self.grab_set() self.focus_set() + logger.debug(f"BaseParameterDialog __init__ for '{title}': Now calling wait_window().") self.wait_window(self) + logger.debug(f"BaseParameterDialog __init__ - END - Title: '{title}' (after wait_window)") def _setup_widgets(self): main_frame = ttk.Frame(self, padding="10") main_frame.pack(expand=True, fill=tk.BOTH) + # --- Parameter Name --- name_frame = ttk.Frame(main_frame) name_frame.pack(fill=tk.X, pady=(0, 5)) ttk.Label(name_frame, text="Name:").grid(row=0, column=0, sticky=tk.W, padx=(0,5)) @@ -44,9 +60,11 @@ class BaseParameterDialog(tk.Toplevel): self.name_entry.grid(row=0, column=1, sticky=tk.EW) name_frame.columnconfigure(1, weight=1) + # --- Description --- desc_frame = ttk.Frame(main_frame) desc_frame.pack(fill=tk.X, pady=(0, 5)) ttk.Label(desc_frame, text="Description:").grid(row=0, column=0, sticky=tk.NW, padx=(0,5)) + # Assicurati che self.description_text sia creato correttamente self.description_text = tk.Text(desc_frame, width=40, height=4, wrap=tk.WORD) self.description_text.grid(row=0, column=1, sticky=tk.EW) desc_scrollbar = ttk.Scrollbar(desc_frame, orient=tk.VERTICAL, command=self.description_text.yview) @@ -54,6 +72,7 @@ class BaseParameterDialog(tk.Toplevel): self.description_text.config(yscrollcommand=desc_scrollbar.set) desc_frame.columnconfigure(1, weight=1) + # --- Default Value --- def_val_frame = ttk.Frame(main_frame) def_val_frame.pack(fill=tk.X, pady=(0, 5)) ttk.Label(def_val_frame, text="Default Value:").grid(row=0, column=0, sticky=tk.W, padx=(0,5)) @@ -62,6 +81,7 @@ class BaseParameterDialog(tk.Toplevel): self.default_value_entry.grid(row=0, column=1, sticky=tk.EW) def_val_frame.columnconfigure(1, weight=1) + # --- Parameter Type --- type_frame = ttk.Frame(main_frame) type_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Label(type_frame, text="Type:").grid(row=0, column=0, sticky=tk.W, padx=(0,5)) @@ -77,6 +97,8 @@ class BaseParameterDialog(tk.Toplevel): self.type_combo.set("string") type_frame.columnconfigure(1, weight=1) + + # --- Dialog Buttons (Save, Cancel) --- dialog_buttons_frame = ttk.Frame(main_frame) dialog_buttons_frame.pack(fill=tk.X, pady=(10, 0)) ttk.Frame(dialog_buttons_frame).pack(side=tk.LEFT, expand=True) @@ -99,7 +121,7 @@ class BaseParameterDialog(tk.Toplevel): raise NotImplementedError("Subclasses must implement _on_save") def _on_cancel(self): - logger.debug(f"{self.title()} cancelled or closed.") + logger.debug(f"BaseParameterDialog: '{self.title()}' cancelled or closed by user.") self.result = None self.destroy() @@ -107,7 +129,10 @@ class BaseParameterDialog(tk.Toplevel): class AddParameterDialog(BaseParameterDialog): """Dialog for adding a new application parameter.""" def __init__(self, parent: tk.Widget): - super().__init__(parent, title="Add New Parameter") + # Non serve caricare dati + super().__init__(parent, title="Add New Parameter", load_data_method=None) + logger.debug("AddParameterDialog __init__ completed.") + def _on_save(self): if not self._validate_inputs(): @@ -124,36 +149,78 @@ class AddParameterDialog(BaseParameterDialog): "default_value": param_default_value, "type": param_type } - logger.info(f"Parameter '{param_name}' data prepared for adding.") + logger.info(f"AddParameterDialog: Parameter '{param_name}' data prepared.") self.destroy() class EditParameterDialog(BaseParameterDialog): """Dialog for editing an existing application parameter.""" def __init__(self, parent: tk.Widget, parameter_data_to_edit: Dict[str, Any]): - self.parameter_data_to_edit = parameter_data_to_edit - super().__init__(parent, title=f"Edit Parameter: {parameter_data_to_edit.get('name', '')}") - self._load_initial_data() + logger.debug(f"EditParameterDialog __init__ - START - Parameter to edit: {parameter_data_to_edit.get('name', 'N/A')}") + self.parameter_data_to_edit = parameter_data_to_edit # Imposta PRIMA di super() + + # Passa il metodo _load_initial_data a super() + param_name_for_title = parameter_data_to_edit.get('name', 'Unknown') + super().__init__(parent, title=f"Edit Parameter: {param_name_for_title}", load_data_method=self._load_initial_data) + + # Il codice qui viene eseguito solo dopo la chiusura del dialogo (wait_window) + logger.debug(f"EditParameterDialog __init__ - END - for '{param_name_for_title}' (after super call completed)") def _load_initial_data(self): + # Chiamato da BaseParameterDialog.__init__ if not self.parameter_data_to_edit: - logger.warning("EditParameterDialog opened without parameter data.") - self.destroy() + logger.error("EditParameterDialog _load_initial_data: parameter_data_to_edit is missing.") + if self.parent_widget: + messagebox.showerror("Initialization Error", + "Cannot edit parameter: Initial data is missing.", + parent=self.parent_widget) # Mostra su ApplicationDialog + self.after_idle(self.destroy) return - self.name_entry_var.set(self.parameter_data_to_edit.get("name", "")) - self.description_text.delete("1.0", tk.END) - self.description_text.insert("1.0", self.parameter_data_to_edit.get("description", "")) - self.default_value_entry_var.set(self.parameter_data_to_edit.get("default_value", "")) - - param_type = self.parameter_data_to_edit.get("type", "string") - if param_type in self._parameter_types: - self.type_combo_var.set(param_type) - else: - logger.warning(f"Unknown parameter type '{param_type}' for '{self.name_entry_var.get()}'. Defaulting to 'string'.") - self.type_combo_var.set("string") + param_name = self.parameter_data_to_edit.get("name", "") + logger.debug(f"EditParameterDialog _load_initial_data: Loading data for parameter '{param_name}'") + try: + self.name_entry_var.set(param_name) + + # Pulisci e inserisci nella Textbox + self.description_text.delete("1.0", tk.END) # Tenta di pulire + self.description_text.insert("1.0", self.parameter_data_to_edit.get("description", "")) # Tenta di inserire + logger.debug(f"EditParameterDialog _load_initial_data: Description set in Text widget.") + + self.default_value_entry_var.set(self.parameter_data_to_edit.get("default_value", "")) + + param_type = self.parameter_data_to_edit.get("type", "string") + if param_type in self._parameter_types: + self.type_combo_var.set(param_type) + else: + logger.warning(f"EditParameterDialog: Unknown type '{param_type}' for param '{param_name}'. Defaulting to 'string'.") + self.type_combo_var.set("string") + + logger.info(f"EditParameterDialog _load_initial_data: Data loaded successfully for '{param_name}'.") + + except tk.TclError as e: # Cattura specificamente l'errore TclError + logger.error(f"EditParameterDialog _load_initial_data: TclError operating on widget (likely description_text): {e}", exc_info=True) + if self.parent_widget: + messagebox.showerror("Widget Error", + f"Error loading data into the description field for '{param_name}'.\n" + "The dialog might not have initialized correctly.", + parent=self.parent_widget) + self.after_idle(self.destroy) # Chiudi se il widget non è utilizzabile + except Exception as e: # Cattura altri errori + logger.error(f"EditParameterDialog _load_initial_data: Unexpected error loading data for '{param_name}': {e}", exc_info=True) + if self.parent_widget: + messagebox.showerror("Load Error", + f"Could not load data for parameter '{param_name}':\n{e}", + parent=self.parent_widget) + self.after_idle(self.destroy) + def _on_save(self): + if not self.parameter_data_to_edit: # Sicurezza se _load fallisce + messagebox.showerror("Save Error", "Cannot save: Initial parameter data was not loaded correctly.", parent=self) + logger.error("EditParameterDialog _on_save: Attempted to save without valid initial data.") + return + if not self._validate_inputs(): return @@ -168,5 +235,6 @@ class EditParameterDialog(BaseParameterDialog): "default_value": new_param_default_value, "type": new_param_type } - logger.info(f"Parameter '{new_param_name}' (original: '{self.parameter_data_to_edit.get('name')}') data prepared.") + original_param_name = self.parameter_data_to_edit.get('name', 'N/A') + logger.info(f"EditParameterDialog: Parameter data for '{new_param_name}' (original: '{original_param_name}') prepared.") self.destroy() \ No newline at end of file diff --git a/launchertool/gui/dialogs/sequence_dialogs.py b/launchertool/gui/dialogs/sequence_dialogs.py index 6e6f1e5..44b6466 100644 --- a/launchertool/gui/dialogs/sequence_dialogs.py +++ b/launchertool/gui/dialogs/sequence_dialogs.py @@ -5,13 +5,13 @@ Dialogs for adding and editing sequences of applications. import tkinter as tk from tkinter import ttk, messagebox import logging -from typing import Optional, List, Dict, Any -import copy # For deep copying steps list if necessary +from typing import Optional, List, Dict, Any, Callable # Aggiunto Callable +import copy from ...core.config_manager import ConfigManager from ...core.exceptions import DuplicateNameError, NameNotFoundError, ConfigError from ..utils_gui import GuiUtils -from .step_dialogs import AddStepDialog, EditStepDialog +from .step_dialogs import AddStepDialog, EditStepDialog # Assicurati che sia importato logger = logging.getLogger(__name__) @@ -19,24 +19,41 @@ class BaseSequenceDialog(tk.Toplevel): """ Base class for Add and Edit Sequence dialogs. """ - def __init__(self, parent: tk.Widget, config_manager: ConfigManager, title: str): + def __init__(self, parent: tk.Widget, config_manager: ConfigManager, title: str, load_data_method: Optional[Callable[[], None]] = None): # Aggiunto load_data_method + logger.debug(f"BaseSequenceDialog __init__ - START - Title: '{title}'") super().__init__(parent) self.transient(parent) self.title(title) self.parent_widget = parent self.config_manager = config_manager self.result: Optional[Dict[str, Any]] = None - self.original_sequence_name: Optional[str] = None + # self.original_sequence_name è gestito dalle sottoclassi - self.current_steps_data: List[Dict[str, Any]] = [] + self.current_steps_data: List[Dict[str, Any]] = [] # Lista dei passi per la sequenza corrente + + logger.debug("BaseSequenceDialog: Calling _setup_widgets()") + self._setup_widgets() # Crea i widget + logger.debug("BaseSequenceDialog: _setup_widgets() completed.") + + # Carica i dati specifici della sottoclasse (es. per EditDialog) PRIMA di rendere modale e attendere + if load_data_method: + logger.debug("BaseSequenceDialog: Calling provided load_data_method.") + load_data_method() + logger.debug("BaseSequenceDialog: load_data_method completed.") + else: + logger.debug("BaseSequenceDialog: No load_data_method provided or needed (e.g., for Add dialog).") + if not hasattr(self, 'original_sequence_name'): # Se è AddDialog, popola albero vuoto + self._populate_steps_tree() - self._setup_widgets() GuiUtils.center_window(self, parent) self.protocol("WM_DELETE_WINDOW", self._on_cancel) + self.grab_set() self.focus_set() + logger.debug(f"BaseSequenceDialog __init__ for '{title}': Now calling wait_window().") self.wait_window(self) + logger.debug(f"BaseSequenceDialog __init__ - END - Title: '{title}' (after wait_window)") def _setup_widgets(self): @@ -44,6 +61,7 @@ class BaseSequenceDialog(tk.Toplevel): main_frame.pack(expand=True, fill=tk.BOTH) main_frame.columnconfigure(0, weight=1) + # --- Sequence Name --- name_frame = ttk.Frame(main_frame) name_frame.grid(row=0, column=0, sticky=tk.EW, pady=(0, 5)) ttk.Label(name_frame, text="Sequence Name:").pack(side=tk.LEFT, padx=(0, 5)) @@ -51,6 +69,7 @@ class BaseSequenceDialog(tk.Toplevel): self.name_entry = ttk.Entry(name_frame, textvariable=self.name_entry_var, width=50) self.name_entry.pack(side=tk.LEFT, expand=True, fill=tk.X) + # --- Steps --- steps_labelframe = ttk.LabelFrame(main_frame, text="Sequence Steps") steps_labelframe.grid(row=1, column=0, sticky=tk.NSEW, pady=(0, 10)) main_frame.rowconfigure(1, weight=1) @@ -107,11 +126,12 @@ class BaseSequenceDialog(tk.Toplevel): self.steps_tree.bind("<>", self._on_step_select) - self._on_step_select() + self._on_step_select() # Init button states + # --- Dialog Buttons (Save, Cancel) --- dialog_buttons_frame = ttk.Frame(main_frame) dialog_buttons_frame.grid(row=2, column=0, sticky=tk.EW, pady=(10, 0)) - ttk.Frame(dialog_buttons_frame).pack(side=tk.LEFT, expand=True) + ttk.Frame(dialog_buttons_frame).pack(side=tk.LEFT, expand=True) # Spacer self.save_button = ttk.Button(dialog_buttons_frame, text="Save", command=self._on_save) self.save_button.pack(side=tk.LEFT, padx=(0,5)) @@ -119,6 +139,7 @@ class BaseSequenceDialog(tk.Toplevel): self.cancel_button.pack(side=tk.LEFT) def _populate_steps_tree(self): + logger.debug(f"BaseSequenceDialog: Populating steps tree with {len(self.current_steps_data)} steps.") selected_iid_tuple = self.steps_tree.selection() selected_iid = selected_iid_tuple[0] if selected_iid_tuple else None @@ -128,15 +149,16 @@ class BaseSequenceDialog(tk.Toplevel): for i, step_data in enumerate(self.current_steps_data): app_name = step_data.get("application", "N/A") wait_time = step_data.get("wait_time", 0.0) + # "parameters" in step_data è un dizionario. Se non è vuoto, ci sono custom params. custom_params_set = "Yes" if step_data.get("parameters") else "No" - # Use index as iid - current_iid = str(i) + current_iid = str(i) # Usare l'indice come iid per facilitare riordino e selezione self.steps_tree.insert( '', tk.END, iid=current_iid, values=(i + 1, app_name, f"{float(wait_time):.1f}", custom_params_set) ) + logger.debug(f"Inserted step: iid='{current_iid}', values=({i+1}, '{app_name}', {wait_time}, '{custom_params_set}')") if selected_iid and self.steps_tree.exists(selected_iid): self.steps_tree.selection_set(selected_iid) @@ -155,9 +177,10 @@ class BaseSequenceDialog(tk.Toplevel): idx = -1 if is_selection: try: - idx = int(selected_ids[0]) + idx = int(selected_ids[0]) # iid è l'indice del passo except ValueError: - is_selection = False + logger.warning(f"Invalid iid '{selected_ids[0]}' in _on_step_select.") + is_selection = False # Tratta come se non ci fosse selezione valida per i pulsanti di spostamento can_move_up = is_selection and idx > 0 self.move_step_up_button.config(state=tk.NORMAL if can_move_up else tk.DISABLED) @@ -166,12 +189,12 @@ class BaseSequenceDialog(tk.Toplevel): self.move_step_down_button.config(state=tk.NORMAL if can_move_down else tk.DISABLED) def _add_step(self): - logger.debug("Add Step button clicked.") - dialog = AddStepDialog(self, self.config_manager) - if dialog.result: + logger.debug("BaseSequenceDialog: Add Step button clicked.") + dialog = AddStepDialog(self, self.config_manager) # 'self' (BaseSequenceDialog) è il parent + if dialog.result: # dialog.result è il dizionario step_data self.current_steps_data.append(dialog.result) - self._populate_steps_tree() - logger.info(f"Step for app '{dialog.result['application']}' added to sequence dialog.") + self._populate_steps_tree() # Aggiorna la treeview con il nuovo passo + logger.info(f"BaseSequenceDialog: Step for app '{dialog.result['application']}' added to current sequence steps.") def _edit_step(self): selected_ids = self.steps_tree.selection() @@ -180,20 +203,21 @@ class BaseSequenceDialog(tk.Toplevel): return try: - step_index = int(selected_ids[0]) + step_index = int(selected_ids[0]) # L'iid è l'indice step_data_to_edit = self.current_steps_data[step_index] - except (ValueError, IndexError): - logger.error(f"Could not get step data for editing, selection: {selected_ids[0]}") + except (ValueError, IndexError) as e: + logger.error(f"BaseSequenceDialog: Could not get step data for editing. Selection iid: '{selected_ids[0]}', Error: {e}") messagebox.showerror("Error", "Could not retrieve step data for editing.", parent=self) return - logger.debug(f"Edit Step button clicked for step index: {step_index}") - # Create a deep copy to pass to the dialog so modifications don't affect current_steps_data until save + logger.debug(f"BaseSequenceDialog: Edit Step button clicked for step at index {step_index}.") + # Passa una COPIA PROFONDA dei dati del passo al dialogo di modifica, così le modifiche + # non si riflettono su self.current_steps_data finché EditStepDialog non ritorna con successo. dialog = EditStepDialog(self, self.config_manager, copy.deepcopy(step_data_to_edit)) - if dialog.result: - self.current_steps_data[step_index] = dialog.result - self._populate_steps_tree() - logger.info(f"Step for app '{dialog.result['application']}' updated in sequence dialog.") + if dialog.result: # dialog.result è il dizionario step_data aggiornato + self.current_steps_data[step_index] = dialog.result # Sostituisci con i dati aggiornati + self._populate_steps_tree() # Aggiorna la treeview + logger.info(f"BaseSequenceDialog: Step for app '{dialog.result['application']}' (index {step_index}) updated.") def _delete_step(self): @@ -205,8 +229,8 @@ class BaseSequenceDialog(tk.Toplevel): try: step_index = int(selected_ids[0]) step_app_name = self.current_steps_data[step_index].get("application", "Unknown") - except (ValueError, IndexError): - logger.error(f"Could not get step data for deletion, selection: {selected_ids[0]}") + except (ValueError, IndexError) as e: + logger.error(f"BaseSequenceDialog: Could not get step data for deletion. Selection iid: '{selected_ids[0]}', Error: {e}") messagebox.showerror("Error", "Could not retrieve step data for deletion.", parent=self) return @@ -214,8 +238,8 @@ class BaseSequenceDialog(tk.Toplevel): f"Are you sure you want to delete step #{step_index + 1} ('{step_app_name}')?", parent=self): del self.current_steps_data[step_index] - self._populate_steps_tree() - logger.info(f"Step for app '{step_app_name}' deleted from sequence dialog.") + self._populate_steps_tree() # Ridisegna la treeview + logger.info(f"BaseSequenceDialog: Step for app '{step_app_name}' (original index {step_index}) deleted.") def _move_step_up(self): selected_ids = self.steps_tree.selection() @@ -225,13 +249,14 @@ class BaseSequenceDialog(tk.Toplevel): if idx > 0: self.current_steps_data[idx], self.current_steps_data[idx-1] = \ self.current_steps_data[idx-1], self.current_steps_data[idx] - new_selected_iid = str(idx-1) + new_selected_iid = str(idx-1) # Il nuovo iid (indice) dell'elemento spostato self._populate_steps_tree() - if self.steps_tree.exists(new_selected_iid): + if self.steps_tree.exists(new_selected_iid): # Riprova a selezionare self.steps_tree.selection_set(new_selected_iid) self.steps_tree.focus(new_selected_iid) + logger.debug(f"Moved step from index {idx} to {idx-1}.") except (ValueError, IndexError) as e: - logger.error(f"Error moving step up: {e}, selection: {selected_ids[0]}") + logger.error(f"Error moving step up: {e}, selection iid: {selected_ids[0]}") def _move_step_down(self): selected_ids = self.steps_tree.selection() @@ -246,8 +271,9 @@ class BaseSequenceDialog(tk.Toplevel): if self.steps_tree.exists(new_selected_iid): self.steps_tree.selection_set(new_selected_iid) self.steps_tree.focus(new_selected_iid) + logger.debug(f"Moved step from index {idx} to {idx+1}.") except (ValueError, IndexError) as e: - logger.error(f"Error moving step down: {e}, selection: {selected_ids[0]}") + logger.error(f"Error moving step down: {e}, selection iid: {selected_ids[0]}") def _validate_inputs(self) -> bool: @@ -262,7 +288,7 @@ class BaseSequenceDialog(tk.Toplevel): raise NotImplementedError("Subclasses must implement _on_save") def _on_cancel(self): - logger.debug(f"{self.title()} cancelled or closed.") + logger.debug(f"BaseSequenceDialog: '{self.title()}' cancelled or closed by user.") self.result = None self.destroy() @@ -270,87 +296,133 @@ class BaseSequenceDialog(tk.Toplevel): class AddSequenceDialog(BaseSequenceDialog): """Dialog for adding a new sequence.""" def __init__(self, parent: tk.Widget, config_manager: ConfigManager): - super().__init__(parent, config_manager, title="Add New Sequence") - self._populate_steps_tree() + # AddSequenceDialog non carica dati esistenti, passa None per load_data_method + # La _populate_steps_tree (vuota) viene chiamata dalla base se load_data_method è None + super().__init__(parent, config_manager, title="Add New Sequence", load_data_method=None) + logger.debug("AddSequenceDialog __init__ completed.") + def _on_save(self): if not self._validate_inputs(): return seq_name = self.name_entry_var.get().strip() + # self.current_steps_data è già aggiornata dai dialoghi dei passi e dal riordino + + # Salva una copia profonda per evitare che modifiche future a current_steps_data + # (se il dialogo venisse riutilizzato o manipolato dopo) influenzino i dati salvati. sequence_data = { "name": seq_name, - "steps": copy.deepcopy(self.current_steps_data) # Save a deep copy + "steps": copy.deepcopy(self.current_steps_data) } try: self.config_manager.add_sequence(sequence_data) - self.result = sequence_data - logger.info(f"Sequence '{seq_name}' added successfully through dialog.") - # messagebox.showinfo("Success", f"Sequence '{seq_name}' added successfully.", parent=self.parent_widget) - self.destroy() + self.result = sequence_data # Imposta result per MainWindow + logger.info(f"AddSequenceDialog: Sequence '{seq_name}' added successfully to config.") + self.destroy() # Chiudi dialogo except DuplicateNameError as e: - logger.warning(f"Failed to add sequence: {e}") + logger.warning(f"AddSequenceDialog: Failed to add sequence due to duplicate name: {e}") messagebox.showerror("Error Adding Sequence", str(e), parent=self) self.name_entry.focus_set() except ConfigError as e: - logger.error(f"Configuration error adding sequence '{seq_name}': {e}", exc_info=True) + logger.error(f"AddSequenceDialog: Configuration error adding sequence '{seq_name}': {e}", exc_info=True) messagebox.showerror("Configuration Error", f"Could not save sequence:\n{e}", parent=self) except Exception as e: - logger.error(f"Unexpected error adding sequence '{seq_name}': {e}", exc_info=True) + logger.error(f"AddSequenceDialog: Unexpected error adding sequence '{seq_name}': {e}", exc_info=True) messagebox.showerror("Unexpected Error", f"An unexpected error occurred:\n{e}", parent=self) class EditSequenceDialog(BaseSequenceDialog): """Dialog for editing an existing sequence.""" def __init__(self, parent: tk.Widget, config_manager: ConfigManager, sequence_name_to_edit: str): - self.original_sequence_name = sequence_name_to_edit - super().__init__(parent, config_manager, title=f"Edit Sequence: {sequence_name_to_edit}") - self._load_initial_data() + logger.debug(f"EditSequenceDialog __init__ - START - Sequence to edit: '{sequence_name_to_edit}'") + self.original_sequence_name = sequence_name_to_edit # Deve essere impostato PRIMA di super() + + # Passa self._load_initial_data al costruttore della classe base + super().__init__(parent, config_manager, title=f"Edit Sequence: {self.original_sequence_name}", load_data_method=self._load_initial_data) + + logger.debug(f"EditSequenceDialog __init__ - END - for '{self.original_sequence_name}' (after super call completed)") + def _load_initial_data(self): + # Questo metodo è chiamato da BaseSequenceDialog.__init__ + if not self.original_sequence_name: + logger.error("EditSequenceDialog _load_initial_data: original_sequence_name is None or empty. Dialog should close.") + if self.parent_widget: # Dovrebbe sempre esistere + messagebox.showerror("Initialization Error", + "Cannot edit: No sequence name specified for editing.", + parent=self.parent_widget) + self.after_idle(self.destroy) + return + + logger.debug(f"EditSequenceDialog _load_initial_data: Attempting to load for: '{self.original_sequence_name}'") try: seq_data = self.config_manager.get_sequence_by_name(self.original_sequence_name) + logger.debug(f"EditSequenceDialog _load_initial_data: Data from ConfigManager for '{self.original_sequence_name}': {seq_data}") + + if not seq_data: # Salvaguardia + logger.error(f"EditSequenceDialog _load_initial_data: ConfigManager returned None for '{self.original_sequence_name}'.") + messagebox.showerror("Load Error", + f"Could not retrieve data for sequence '{self.original_sequence_name}'.", + parent=self.parent_widget) + self.after_idle(self.destroy) + return + self.name_entry_var.set(seq_data.get("name", "")) - # Make a deep copy of steps for editing + # Usa copy.deepcopy per i passi, poiché contengono dizionari (mutabili) self.current_steps_data = copy.deepcopy(seq_data.get("steps", [])) - self._populate_steps_tree() + + logger.debug(f"EditSequenceDialog _load_initial_data: Name set to '{self.name_entry_var.get()}'. Number of steps: {len(self.current_steps_data)}") + self._populate_steps_tree() # Popola la treeview con i passi caricati + + logger.info(f"EditSequenceDialog _load_initial_data: Successfully loaded data for '{self.original_sequence_name}'.") + except NameNotFoundError: - logger.error(f"Cannot edit sequence. Name '{self.original_sequence_name}' not found.") - messagebox.showerror("Error", f"Sequence '{self.original_sequence_name}' not found.", parent=self.parent_widget) - self.destroy() + logger.error(f"EditSequenceDialog _load_initial_data: Sequence '{self.original_sequence_name}' not found (NameNotFoundError).") + messagebox.showerror("Load Error", + f"Sequence '{self.original_sequence_name}' not found. Cannot edit.", + parent=self.parent_widget) + self.after_idle(self.destroy) except Exception as e: - logger.error(f"Unexpected error loading sequence data for edit: {e}", exc_info=True) - messagebox.showerror("Error", f"Could not load sequence data for editing:\n{e}", parent=self.parent_widget) - self.destroy() - - + logger.error(f"EditSequenceDialog _load_initial_data: Unexpected error for '{self.original_sequence_name}': {e}", exc_info=True) + messagebox.showerror("Load Error", + f"An unexpected error occurred while loading data for '{self.original_sequence_name}':\n{e}", + parent=self.parent_widget) + self.after_idle(self.destroy) + def _on_save(self): + if not self.original_sequence_name: + messagebox.showerror("Save Error", "Cannot save: Critical information about the original sequence is missing.", parent=self) + logger.error("EditSequenceDialog _on_save: Attempted to save without a valid original_sequence_name.") + return + if not self._validate_inputs(): return new_seq_name = self.name_entry_var.get().strip() + # self.current_steps_data è già aggiornata + updated_sequence_data = { "name": new_seq_name, - "steps": copy.deepcopy(self.current_steps_data) # Save a deep copy + "steps": copy.deepcopy(self.current_steps_data) # Salva una copia profonda } try: self.config_manager.update_sequence(self.original_sequence_name, updated_sequence_data) self.result = updated_sequence_data - logger.info(f"Sequence '{self.original_sequence_name}' updated to '{new_seq_name}'.") - # messagebox.showinfo("Success", f"Sequence '{new_seq_name}' updated successfully.", parent=self.parent_widget) + logger.info(f"EditSequenceDialog: Sequence '{self.original_sequence_name}' updated to '{new_seq_name}' in config.") self.destroy() except NameNotFoundError as e: - logger.error(f"Update failed: Original sequence '{self.original_sequence_name}' not found: {e}", exc_info=True) + logger.error(f"EditSequenceDialog _on_save: Original sequence '{self.original_sequence_name}' no longer found: {e}", exc_info=True) messagebox.showerror("Error Updating Sequence", str(e), parent=self) except DuplicateNameError as e: - logger.warning(f"Failed to update sequence: {e}") + logger.warning(f"EditSequenceDialog _on_save: Failed to update due to duplicate name: {e}") messagebox.showerror("Error Updating Sequence", str(e), parent=self) self.name_entry.focus_set() except ConfigError as e: - logger.error(f"Configuration error updating sequence '{new_seq_name}': {e}", exc_info=True) + logger.error(f"EditSequenceDialog _on_save: Config error updating '{new_seq_name}': {e}", exc_info=True) messagebox.showerror("Configuration Error", f"Could not save sequence updates:\n{e}", parent=self) except Exception as e: - logger.error(f"Unexpected error updating sequence '{new_seq_name}': {e}", exc_info=True) + logger.error(f"EditSequenceDialog _on_save: Unexpected error updating '{new_seq_name}': {e}", exc_info=True) messagebox.showerror("Unexpected Error", f"An unexpected error occurred:\n{e}", parent=self) \ No newline at end of file diff --git a/launchertool/gui/dialogs/step_dialogs.py b/launchertool/gui/dialogs/step_dialogs.py index 3e54270..b7c2254 100644 --- a/launchertool/gui/dialogs/step_dialogs.py +++ b/launchertool/gui/dialogs/step_dialogs.py @@ -5,8 +5,8 @@ Dialogs for adding and editing steps within a sequence. import tkinter as tk from tkinter import ttk, messagebox import logging -from typing import Optional, List, Dict, Any -import copy # For deep copy of step_specific_parameters +from typing import Optional, List, Dict, Any, Callable # Aggiunto Callable +import copy from ...core.config_manager import ConfigManager from ...core.exceptions import ApplicationNotFoundError @@ -18,7 +18,11 @@ class BaseStepDialog(tk.Toplevel): """ Base class for Add and Edit Step dialogs. """ - def __init__(self, parent: tk.Widget, config_manager: ConfigManager, title: str): + # Aggiunto attributo per memorizzare i nomi delle app + _application_names: List[str] = [] + + def __init__(self, parent: tk.Widget, config_manager: ConfigManager, title: str, load_data_method: Optional[Callable[[], None]] = None): # Aggiunto load_data_method + logger.debug(f"BaseStepDialog __init__ - START - Title: '{title}'") super().__init__(parent) self.transient(parent) self.title(title) @@ -31,19 +35,35 @@ class BaseStepDialog(tk.Toplevel): self.step_specific_parameters: Dict[str, str] = {} self.application_defined_parameters: List[Dict[str, str]] = [] + logger.debug("BaseStepDialog: Calling _setup_widgets()") self._setup_widgets() + logger.debug("BaseStepDialog: _setup_widgets() completed.") + + if load_data_method: + logger.debug("BaseStepDialog: Calling provided load_data_method.") + load_data_method() + logger.debug("BaseStepDialog: load_data_method completed.") + else: + logger.debug("BaseStepDialog: No load_data_method provided or needed (e.g., for Add dialog).") + # Per AddDialog, carica i parametri per l'app eventualmente selezionata (nessuna) + self._load_parameters_for_selected_app(self.app_combo_var.get() or None) + GuiUtils.center_window(self, parent) self.protocol("WM_DELETE_WINDOW", self._on_cancel) + self.grab_set() self.focus_set() + logger.debug(f"BaseStepDialog __init__ for '{title}': Now calling wait_window().") self.wait_window(self) + logger.debug(f"BaseStepDialog __init__ - END - Title: '{title}' (after wait_window)") def _setup_widgets(self): main_frame = ttk.Frame(self, padding="10") main_frame.pack(expand=True, fill=tk.BOTH) main_frame.columnconfigure(0, weight=1) + # --- Application Selection --- app_frame = ttk.Frame(main_frame) app_frame.grid(row=0, column=0, sticky=tk.EW, pady=(0, 5)) ttk.Label(app_frame, text="Application:").pack(side=tk.LEFT, padx=(0,5)) @@ -55,13 +75,14 @@ class BaseStepDialog(tk.Toplevel): width=48 ) self.app_combo.pack(side=tk.LEFT, expand=True, fill=tk.X) - self._populate_applications_combobox() + self._populate_applications_combobox() # Chiama per riempire i valori self.app_combo.bind("<>", self._on_application_selected_event) + # --- Wait Time --- wait_frame = ttk.Frame(main_frame) wait_frame.grid(row=1, column=0, sticky=tk.EW, pady=(0, 10)) ttk.Label(wait_frame, text="Wait Time (seconds):").pack(side=tk.LEFT, padx=(0,5)) - self.wait_time_var = tk.StringVar(value="0.0") # Initialize here + self.wait_time_var = tk.StringVar(value="0.0") self.wait_time_spinbox = ttk.Spinbox( wait_frame, from_=0.0, @@ -73,6 +94,7 @@ class BaseStepDialog(tk.Toplevel): ) self.wait_time_spinbox.pack(side=tk.LEFT) + # --- Step-Specific Parameters --- self.params_labelframe = ttk.LabelFrame(main_frame, text="Configure Parameters for this Step") self.params_labelframe.grid(row=2, column=0, sticky=tk.NSEW, pady=(0,10)) main_frame.rowconfigure(2, weight=1) @@ -112,9 +134,10 @@ class BaseStepDialog(tk.Toplevel): self.params_tree.grid(row=0, column=0, sticky=tk.NSEW) self.params_tree.bind('', self._on_param_tree_double_click) - self.current_edit_item_iid = None # Store iid of item being edited + self.current_edit_item_iid = None self.current_edit_widget = None + # --- Dialog Buttons (Save, Cancel) --- dialog_buttons_frame = ttk.Frame(main_frame) dialog_buttons_frame.grid(row=3, column=0, sticky=tk.EW, pady=(10, 0)) ttk.Frame(dialog_buttons_frame).pack(side=tk.LEFT, expand=True) @@ -127,31 +150,32 @@ class BaseStepDialog(tk.Toplevel): def _populate_applications_combobox(self): try: - app_names = [app["name"] for app in self.config_manager.get_applications()] - self.app_combo['values'] = sorted(app_names) + # Salva i nomi nell'attributo di classe/istanza + self._application_names = sorted([app["name"] for app in self.config_manager.get_applications()]) + self.app_combo['values'] = self._application_names # Imposta i valori nel widget + logger.debug(f"Populated application combobox with names: {self._application_names}") except Exception as e: logger.error(f"Failed to populate applications combobox: {e}", exc_info=True) + self._application_names = [] # Assicura che sia una lista vuota in caso di errore + self.app_combo['values'] = [] messagebox.showerror("Error", "Could not load application list.", parent=self) def _on_application_selected_event(self, event=None): - """Handles the ComboboxSelected event, clears overrides if app changed by user.""" selected_app_name = self.app_combo_var.get() - # Only clear overrides if the app was changed by user interaction, - # not during initial load where step_application_name might be the same. + # Solo se l'utente cambia selezione rispetto a quella corrente if self.step_application_name is not None and self.step_application_name != selected_app_name: - logger.debug(f"Application changed by user from '{self.step_application_name}' to '{selected_app_name}'. Clearing step-specific parameter overrides.") + logger.debug(f"Application changed by user from '{self.step_application_name}' to '{selected_app_name}'. Clearing step overrides.") self.step_specific_parameters.clear() self._load_parameters_for_selected_app(selected_app_name) def _load_parameters_for_selected_app(self, app_name: Optional[str]): - """Loads application defined parameters and populates the tree.""" - self.step_application_name = app_name # Update current app for the step + self.step_application_name = app_name if not app_name: self.application_defined_parameters = [] - self._populate_step_parameters_tree() + self._populate_step_parameters_tree() # Aggiorna (svuota) albero parametri return try: @@ -159,12 +183,13 @@ class BaseStepDialog(tk.Toplevel): self.application_defined_parameters = app_data.get("parameters", []) self._populate_step_parameters_tree() except ApplicationNotFoundError: - logger.warning(f"Selected application '{app_name}' not found in config.") + logger.warning(f"Selected application '{app_name}' not found in config (in _load_parameters).") self.application_defined_parameters = [] - # self.step_specific_parameters.clear() # Already handled if app changed via event self._populate_step_parameters_tree() except Exception as e: logger.error(f"Error loading parameters for app '{app_name}': {e}", exc_info=True) + self.application_defined_parameters = [] + self._populate_step_parameters_tree() messagebox.showerror("Error", f"Could not load parameters for {app_name}.", parent=self) @@ -173,28 +198,22 @@ class BaseStepDialog(tk.Toplevel): for item in self.params_tree.get_children(): self.params_tree.delete(item) - if not self.step_application_name : # No app selected yet + if not self.step_application_name : self.params_labelframe.config(text="Select an Application to Configure Parameters") return if not self.application_defined_parameters: self.params_labelframe.config(text=f"No Parameters Defined for '{self.step_application_name}'") return - self.params_labelframe.config(text=f"Configure Parameters for '{self.step_application_name}' in this Step") + self.params_labelframe.config(text=f"Configure Parameters for '{self.step_application_name}' (Double-click 'Override Value' to edit)") for app_param in self.application_defined_parameters: param_name = app_param.get("name", "UnnamedParam") app_default = app_param.get("default_value", "") description = app_param.get("description", "") - - step_value = self.step_specific_parameters.get(param_name, "") + step_value = self.step_specific_parameters.get(param_name, "") # Ottiene override o stringa vuota - # Use param_name as iid, assuming it's unique within an app's params - self.params_tree.insert( - '', tk.END, - iid=param_name, - values=(param_name, app_default, step_value, description) - ) + self.params_tree.insert('', tk.END, iid=param_name, values=(param_name, app_default, step_value, description)) def _on_param_tree_double_click(self, event): self._finish_editing_param_value(save=True) @@ -202,7 +221,7 @@ class BaseStepDialog(tk.Toplevel): region = self.params_tree.identify_region(event.x, event.y) column_id_str = self.params_tree.identify_column(event.x) - if region != 'cell' or column_id_str != '#3': + if region != 'cell' or column_id_str != '#3': # Colonna 'Step Value' return self.current_edit_item_iid = self.params_tree.identify_row(event.y) @@ -211,7 +230,7 @@ class BaseStepDialog(tk.Toplevel): x, y, width, height = self.params_tree.bbox(self.current_edit_item_iid, column=column_id_str) current_values = self.params_tree.item(self.current_edit_item_iid, 'values') - current_step_value = current_values[2] # 'Step Value' + current_step_value = current_values[2] entry_var = tk.StringVar(value=current_step_value) self.current_edit_widget = ttk.Entry(self.params_tree, textvariable=entry_var) @@ -219,7 +238,6 @@ class BaseStepDialog(tk.Toplevel): self.current_edit_widget.focus_set() self.current_edit_widget.selection_range(0, tk.END) - self.current_edit_widget.bind("", lambda e: self._finish_editing_param_value(save=True)) self.current_edit_widget.bind("", lambda e: self._finish_editing_param_value(save=True)) self.current_edit_widget.bind("", lambda e: self._finish_editing_param_value(save=False)) @@ -227,21 +245,21 @@ class BaseStepDialog(tk.Toplevel): def _finish_editing_param_value(self, save: bool): if self.current_edit_widget and self.current_edit_item_iid: + param_name = self.current_edit_item_iid # iid è il nome del parametro if save: new_value = self.current_edit_widget.get() - # iid is the param_name - param_name = self.current_edit_item_iid - current_values = list(self.params_tree.item(self.current_edit_item_iid, 'values')) - current_values[2] = new_value + current_values[2] = new_value # Aggiorna la colonna 'Step Value' self.params_tree.item(self.current_edit_item_iid, values=tuple(current_values)) - if new_value.strip() == "" and param_name in self.step_specific_parameters : - del self.step_specific_parameters[param_name] - logger.debug(f"Override for '{param_name}' cleared.") - elif new_value.strip() != "": + # Aggiorna il dizionario degli override specifici del passo + if new_value.strip() == "": # Se l'override viene cancellato + if param_name in self.step_specific_parameters: + del self.step_specific_parameters[param_name] + logger.debug(f"Override for step parameter '{param_name}' cleared.") + else: # Se viene impostato un nuovo override self.step_specific_parameters[param_name] = new_value - logger.debug(f"Override for '{param_name}' set to '{new_value}'.") + logger.debug(f"Override for step parameter '{param_name}' set to '{new_value}'.") self.current_edit_widget.destroy() self.current_edit_widget = None @@ -263,13 +281,17 @@ class BaseStepDialog(tk.Toplevel): messagebox.showerror("Validation Error", "Wait Time cannot be negative.", parent=self) self.wait_time_spinbox.focus_set() return False - self.step_wait_time = current_step_wait_time # Store validated float + self.step_wait_time = current_step_wait_time # Salva il float validato except ValueError: messagebox.showerror("Validation Error", "Invalid Wait Time. Please enter a number.", parent=self) self.wait_time_spinbox.focus_set() return False - # self.step_application_name is already set by _load_parameters_for_selected_app + # self.step_application_name dovrebbe essere già impostato da _load_parameters_for_selected_app + if not self.step_application_name or self.step_application_name != selected_app: + logger.warning("Validation mismatch: step_application_name != selected_app in combobox.") + self.step_application_name = selected_app # Sincronizza per sicurezza + return True @@ -278,7 +300,7 @@ class BaseStepDialog(tk.Toplevel): def _on_cancel(self): self._finish_editing_param_value(save=False) - logger.debug(f"{self.title()} cancelled or closed.") + logger.debug(f"BaseStepDialog: '{self.title()}' cancelled or closed by user.") self.result = None self.destroy() @@ -286,61 +308,80 @@ class BaseStepDialog(tk.Toplevel): class AddStepDialog(BaseStepDialog): """Dialog for adding a new step to a sequence.""" def __init__(self, parent: tk.Widget, config_manager: ConfigManager): - super().__init__(parent, config_manager, title="Add New Step") - # Initial call to set the label frame text correctly if no app is selected initially - self._load_parameters_for_selected_app(None) - + # Non passa load_data_method, verrà gestito dalla base + super().__init__(parent, config_manager, title="Add New Step", load_data_method=None) + logger.debug("AddStepDialog __init__ completed.") def _on_save(self): - self._finish_editing_param_value(save=True) + self._finish_editing_param_value(save=True) # Finalizza edit inline if not self._validate_inputs(): return + # I valori validati sono in self.step_application_name, self.step_wait_time + # e self.step_specific_parameters self.result = { - "application": self.step_application_name, # Set by _load_parameters_for_selected_app - "wait_time": self.step_wait_time, # Set by _validate_inputs - "parameters": copy.deepcopy(self.step_specific_parameters) + "application": self.step_application_name, + "wait_time": self.step_wait_time, + "parameters": copy.deepcopy(self.step_specific_parameters) # Usa una copia } - logger.info(f"Step data for '{self.step_application_name}' prepared for adding.") + logger.info(f"AddStepDialog: Step data for '{self.step_application_name}' prepared.") self.destroy() class EditStepDialog(BaseStepDialog): """Dialog for editing an existing step in a sequence.""" def __init__(self, parent: tk.Widget, config_manager: ConfigManager, step_data_to_edit: Dict[str, Any]): - self.step_data_to_edit = step_data_to_edit - super().__init__(parent, config_manager, title=f"Edit Step: {step_data_to_edit.get('application', '')}") - self._load_initial_data() + logger.debug(f"EditStepDialog __init__ - START - Step data to edit: {step_data_to_edit}") + # Salva i dati PRIMA di chiamare super, perché _load_initial_data ne avrà bisogno + self.step_data_to_edit = step_data_to_edit + # Passa self._load_initial_data a super() + super().__init__(parent, config_manager, title=f"Edit Step: {step_data_to_edit.get('application', '')}", load_data_method=self._load_initial_data) + logger.debug(f"EditStepDialog __init__ - END (after super call completed)") def _load_initial_data(self): + # Chiamato da BaseStepDialog.__init__ + if not self.step_data_to_edit: # Sicurezza + logger.error("EditStepDialog _load_initial_data: step_data_to_edit is missing.") + self.after_idle(self.destroy) + return + app_name = self.step_data_to_edit.get("application") wait_time = self.step_data_to_edit.get("wait_time", 0.0) - # Deep copy to avoid modifying the original dict from sequence_dialog's current_steps_data + # Importante: Fa una copia profonda subito per step_specific_parameters self.step_specific_parameters = copy.deepcopy(self.step_data_to_edit.get("parameters", {})) + logger.debug(f"EditStepDialog _load_initial_data: Initial step overrides: {self.step_specific_parameters}") - if app_name and app_name in self.app_combo['values']: + # Usa la lista _application_names caricata da _populate_applications_combobox + if app_name and app_name in self._application_names: self.app_combo_var.set(app_name) - # This will trigger _on_application_selected_event, then _load_parameters_for_selected_app - # which loads app_defined_parameters and populates tree using existing step_specific_parameters + # Chiamata esplicita per caricare i parametri dell'app e popolare l'albero + # Non si affida all'evento ComboboxSelected qui perché potremmo essere ancora nel costruttore self._load_parameters_for_selected_app(app_name) else: if app_name: - logger.warning(f"App '{app_name}' from step data not in config. Clearing selection.") + logger.warning(f"EditStepDialog _load_initial_data: App '{app_name}' from step data not found in available apps: {self._application_names}. Clearing selection.") + messagebox.showwarning("Application Not Found", + f"The application '{app_name}' originally selected for this step was not found.\n" + "Please select a valid application.", parent=self) self.app_combo_var.set("") - self._load_parameters_for_selected_app(None) + self._load_parameters_for_selected_app(None) # Svuota l'albero dei parametri self.wait_time_var.set(f"{float(wait_time):.1f}") - # _populate_step_parameters_tree is implicitly called via _load_parameters_for_selected_app + logger.info(f"EditStepDialog _load_initial_data: Data loaded for app '{app_name}'.") + def _on_save(self): - self._finish_editing_param_value(save=True) + self._finish_editing_param_value(save=True) # Finalizza edit inline if not self._validate_inputs(): return + # I valori validati sono in self.step_application_name, self.step_wait_time + # e self.step_specific_parameters self.result = { "application": self.step_application_name, "wait_time": self.step_wait_time, - "parameters": copy.deepcopy(self.step_specific_parameters) + "parameters": copy.deepcopy(self.step_specific_parameters) # Usa una copia } - logger.info(f"Step data for '{self.step_application_name}' (original: '{self.step_data_to_edit.get('application')}') prepared.") + original_app = self.step_data_to_edit.get('application', 'N/A') + logger.info(f"EditStepDialog: Step data for '{self.step_application_name}' (original: '{original_app}') prepared.") self.destroy() \ No newline at end of file diff --git a/launchertool/gui/main_window.py b/launchertool/gui/main_window.py index b6a9874..ebb6cc3 100644 --- a/launchertool/gui/main_window.py +++ b/launchertool/gui/main_window.py @@ -25,6 +25,26 @@ from .dialogs.sequence_dialogs import AddSequenceDialog, EditSequenceDialog logger = logging.getLogger(__name__) + +# --- Import Version Info FOR THE WRAPPER ITSELF --- +try: + # Use absolute import based on package name + from launchertool import _version as wrapper_version + WRAPPER_APP_VERSION_STRING = f"{wrapper_version.__version__} ({wrapper_version.GIT_BRANCH}/{wrapper_version.GIT_COMMIT_HASH[:7]})" + WRAPPER_BUILD_INFO = f"Wrapper Built: {wrapper_version.BUILD_TIMESTAMP}" +except ImportError: + # This might happen if you run the wrapper directly from source + # without generating its _version.py first (if you use that approach for the wrapper itself) + WRAPPER_APP_VERSION_STRING = "(Dev Wrapper)" + WRAPPER_BUILD_INFO = "Wrapper build time unknown" +# --- End Import Version Info --- + +# --- Constants for Version Generation --- +DEFAULT_VERSION = "0.0.0+unknown" +DEFAULT_COMMIT = "Unknown" +DEFAULT_BRANCH = "Unknown" +# --- End Constants --- + class MainWindow(tk.Tk): """ Main application window for the Launcher Tool. @@ -39,7 +59,7 @@ class MainWindow(tk.Tk): config_file_path (str): Path to the configuration file. """ super().__init__() - self.title("Application Launcher") + self.title(f"Application Launcher - {WRAPPER_APP_VERSION_STRING}") logger.info("Initializing MainWindow.") self.config_manager = ConfigManager(config_file_path=config_file_path) @@ -183,13 +203,19 @@ class MainWindow(tk.Tk): if not selected_item_ids: messagebox.showinfo("Edit Application", "Please select an application to edit.", parent=self) return - # Assuming iid is the application name - application_name = selected_item_ids[0] - logger.info(f"Showing Edit Application dialog for: {application_name}") + + application_name = selected_item_ids[0] # Questo è l'iid + logger.info(f"Attempting to show Edit Application dialog for: '{application_name}'") # LOG + + if not application_name: # Aggiungi un controllo esplicito + messagebox.showerror("Error", "Selected application has an invalid name.", parent=self) + logger.error("Selected application for edit has no name (iid was empty).") + return + dialog = EditApplicationDialog(self, self.config_manager, application_name) - if dialog.result: + if dialog.result: # dialog.result è impostato solo se il dialogo salva con successo self.refresh_applications_tree() - self.refresh_sequences_tree() + self.refresh_sequences_tree() # App name change impacts sequences def _delete_selected_application(self): selected_item_ids = self.applications_tree.selection()