fix edit dialogs and run sequence, add version

This commit is contained in:
VALLONGOL 2025-05-08 10:47:33 +02:00
parent e32fe63dd4
commit 326baf3c03
8 changed files with 634 additions and 253 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 46 KiB

90
launchertool/_version.py Normal file
View File

@ -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}]"

View File

@ -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

View File

@ -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("<<TreeviewSelect>>", 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)

View File

@ -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()

View File

@ -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("<<TreeviewSelect>>", 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)

View File

@ -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("<<ComboboxSelected>>", 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('<Double-1>', 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("<Return>", lambda e: self._finish_editing_param_value(save=True))
self.current_edit_widget.bind("<KP_Enter>", lambda e: self._finish_editing_param_value(save=True))
self.current_edit_widget.bind("<Escape>", 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()

View File

@ -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()