diff --git a/launchertool.ico b/launchertool.ico index e69de29..019963f 100644 Binary files a/launchertool.ico and b/launchertool.ico differ 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()