add debounce delay for start/stop button

This commit is contained in:
2025-07-07 21:55:20 +02:00
parent 40ada7f96d
commit 9074b9387b
+299 -12
View File
@@ -37,21 +37,40 @@ if not os.path.exists(CONFIG_DIR):
os.makedirs(CONFIG_DIR) os.makedirs(CONFIG_DIR)
class Scenario: class Scenario:
"""
Represents a scenario, which consists of a name and a list of steps.
Handles saving/loading scenario data to/from disk.
"""
def __init__(self, name, steps=None): def __init__(self, name, steps=None):
"""
Initialize a Scenario with a name and optional steps.
"""
self.name = name self.name = name
self.steps = steps or [] # List of Step dicts self.steps = steps or [] # List of Step dicts
def to_dict(self): def to_dict(self):
"""
Return a dictionary representation of the scenario.
"""
return {'name': self.name, 'steps': self.steps} return {'name': self.name, 'steps': self.steps}
@staticmethod @staticmethod
def from_dict(data): def from_dict(data):
"""
Create a Scenario object from a dictionary.
"""
return Scenario(data['name'], data.get('steps', [])) return Scenario(data['name'], data.get('steps', []))
def get_scenario_dir(self): def get_scenario_dir(self):
"""
Return the directory path for this scenario's files.
"""
return os.path.join(CONFIG_DIR, self.name) return os.path.join(CONFIG_DIR, self.name)
def save(self): def save(self):
"""
Save the scenario to disk as a JSON file.
"""
try: try:
scenario_dir = self.get_scenario_dir() scenario_dir = self.get_scenario_dir()
if not os.path.exists(scenario_dir): if not os.path.exists(scenario_dir):
@@ -65,6 +84,9 @@ class Scenario:
@staticmethod @staticmethod
def load(name): def load(name):
"""
Load a scenario from disk by name.
"""
try: try:
scenario_dir = os.path.join(CONFIG_DIR, name) scenario_dir = os.path.join(CONFIG_DIR, name)
with open(os.path.join(scenario_dir, 'scenario.json'), 'r') as f: with open(os.path.join(scenario_dir, 'scenario.json'), 'r') as f:
@@ -77,9 +99,16 @@ class Scenario:
@staticmethod @staticmethod
def list_all(): def list_all():
"""
List all scenario directories.
"""
return [d for d in os.listdir(CONFIG_DIR) if os.path.isdir(os.path.join(CONFIG_DIR, d))] return [d for d in os.listdir(CONFIG_DIR) if os.path.isdir(os.path.join(CONFIG_DIR, d))]
def take_screenshot_with_tkinter(): def take_screenshot_with_tkinter():
"""
Use Tkinter to let the user select a region of the screen for a screenshot.
Returns a dict with x, y, width, height.
"""
root = tk.Tk() root = tk.Tk()
root.attributes("-alpha", 0.3) root.attributes("-alpha", 0.3)
root.attributes("-fullscreen", True) root.attributes("-fullscreen", True)
@@ -130,7 +159,15 @@ def take_screenshot_with_tkinter():
return selection_rect return selection_rect
class MainWindow(QtWidgets.QMainWindow): class MainWindow(QtWidgets.QMainWindow):
"""
Main application window for Scenario Image Automation.
Handles UI, scenario management, and automation logic.
"""
def automation_loop(self): def automation_loop(self):
"""
Main loop for running automation steps. Continuously looks for image matches
in the selected window or screen and performs actions as defined in the scenario.
"""
logger.info('Automation loop started.') logger.info('Automation loop started.')
try: try:
while self.running: while self.running:
@@ -206,6 +243,9 @@ class MainWindow(QtWidgets.QMainWindow):
self.set_state('Paused') self.set_state('Paused')
def _perform_step_action(self, action, loc, shape): def _perform_step_action(self, action, loc, shape):
"""
Perform a single step action (click, key, scroll, delay) at the given location.
"""
act_type = action['type'] act_type = action['type']
params = action['params'] params = action['params']
logger.debug(f'Performing step action: {act_type}, params={params}, loc={loc}, shape={shape}') logger.debug(f'Performing step action: {act_type}, params={params}, loc={loc}, shape={shape}')
@@ -245,16 +285,31 @@ class MainWindow(QtWidgets.QMainWindow):
except Exception as e: except Exception as e:
logger.error(f'Error performing step action {act_type}: {e}') logger.error(f'Error performing step action {act_type}: {e}')
def set_state(self, text): def set_state(self, text):
"""
Update the state label in the UI.
"""
self.state_text = text self.state_text = text
self.state_label.setText(f'State: {text}') self.state_label.setText(f'State: {text}')
def toggle_automation(self): def toggle_automation(self):
"""
Toggle the automation state (start/stop), but prevent toggling twice within 5 seconds.
"""
now = time.time()
if now - self.last_toggle_time < 5:
logger.info("Start/Stop toggle ignored: pressed too quickly.")
QtWidgets.QMessageBox.information(self, "Wait", "Please wait at least 5 seconds before toggling again.")
return
self.last_toggle_time = now
if self.running: if self.running:
self.stop_automation() self.stop_automation()
else: else:
self.start_automation() self.start_automation()
def start_automation(self): def start_automation(self):
"""
Start the automation process and worker thread.
"""
logger.info('Starting automation.') logger.info('Starting automation.')
if not self.current_scenario or self.running: if not self.current_scenario or self.running:
logger.warning('Start Automation: No scenario selected or already running.') logger.warning('Start Automation: No scenario selected or already running.')
@@ -268,6 +323,9 @@ class MainWindow(QtWidgets.QMainWindow):
self.listener.start() self.listener.start()
def stop_automation(self): def stop_automation(self):
"""
Stop the automation process and worker thread.
"""
logger.info('Stopping automation.') logger.info('Stopping automation.')
self.running = False self.running = False
self.set_state('Paused') self.set_state('Paused')
@@ -277,6 +335,9 @@ class MainWindow(QtWidgets.QMainWindow):
self.listener = None self.listener = None
def __init__(self): def __init__(self):
"""
Initialize the main window, UI, and load scenarios.
"""
super().__init__() super().__init__()
self.setWindowTitle('Scenario Image Automation') self.setWindowTitle('Scenario Image Automation')
# Make window smaller and more compact # Make window smaller and more compact
@@ -290,10 +351,15 @@ class MainWindow(QtWidgets.QMainWindow):
self.worker = None self.worker = None
self.current_scenario = None self.current_scenario = None
self.selected_step_idx = None self.selected_step_idx = None
self.last_toggle_time = 0 # For debounce of start/stop
self.init_ui() self.init_ui()
self.load_scenarios() self.load_scenarios()
def init_ui(self): def init_ui(self):
"""
Initializes the main UI components and layouts.
Sets up scenario and window dropdowns, step list, and action buttons.
"""
# Set compact font and style # Set compact font and style
font = QtGui.QFont() font = QtGui.QFont()
font.setPointSize(9) font.setPointSize(9)
@@ -316,7 +382,7 @@ class MainWindow(QtWidgets.QMainWindow):
scenario_group_layout = QtWidgets.QVBoxLayout() scenario_group_layout = QtWidgets.QVBoxLayout()
self.combo = QtWidgets.QComboBox() self.combo = QtWidgets.QComboBox()
self.combo.setMinimumWidth(90) self.combo.setMinimumWidth(90)
self.combo.currentIndexChanged.connect(self.select_scenario) self.combo.currentIndexChanged.connect(self._log_combo_scenario)
scenario_group_layout.addWidget(self.combo) scenario_group_layout.addWidget(self.combo)
# Window selection dropdown # Window selection dropdown
@@ -324,21 +390,21 @@ class MainWindow(QtWidgets.QMainWindow):
self.window_combo.setMinimumWidth(90) self.window_combo.setMinimumWidth(90)
scenario_group_layout.addWidget(QtWidgets.QLabel('Target Window:')) scenario_group_layout.addWidget(QtWidgets.QLabel('Target Window:'))
scenario_group_layout.addWidget(self.window_combo) scenario_group_layout.addWidget(self.window_combo)
self.window_combo.currentIndexChanged.connect(self.save_selected_window) self.window_combo.currentIndexChanged.connect(self._log_combo_window)
self.refresh_window_list() self.refresh_window_list()
# Scenario actions # Scenario actions
self.btn_new = QtWidgets.QPushButton('New') self.btn_new = QtWidgets.QPushButton('New')
self.btn_new.setToolTip('Create new scenario') self.btn_new.setToolTip('Create new scenario')
self.btn_new.clicked.connect(self.create_scenario) self.btn_new.clicked.connect(self._log_btn_new)
self.btn_import = QtWidgets.QPushButton('Import') self.btn_import = QtWidgets.QPushButton('Import')
self.btn_import.setToolTip('Import scenario') self.btn_import.setToolTip('Import scenario')
self.btn_import.clicked.connect(self.import_scenario) self.btn_import.clicked.connect(self._log_btn_import)
self.btn_export = QtWidgets.QPushButton('Export') self.btn_export = QtWidgets.QPushButton('Export')
self.btn_export.setToolTip('Export scenario') self.btn_export.setToolTip('Export scenario')
self.btn_export.clicked.connect(self.export_scenario) self.btn_export.clicked.connect(self._log_btn_export)
self.btn_rename_scenario = QtWidgets.QPushButton("Rename") self.btn_rename_scenario = QtWidgets.QPushButton("Rename")
self.btn_rename_scenario.setToolTip('Rename scenario') self.btn_rename_scenario.setToolTip('Rename scenario')
self.btn_rename_scenario.clicked.connect(self.rename_scenario) self.btn_rename_scenario.clicked.connect(self._log_btn_rename_scenario)
scenario_btn_group_layout = QtWidgets.QHBoxLayout() scenario_btn_group_layout = QtWidgets.QHBoxLayout()
scenario_btn_group_layout.setSpacing(4) scenario_btn_group_layout.setSpacing(4)
scenario_btn_group_layout.setContentsMargins(4, 4, 4, 4) scenario_btn_group_layout.setContentsMargins(4, 4, 4, 4)
@@ -354,21 +420,21 @@ class MainWindow(QtWidgets.QMainWindow):
steps_group_layout = QtWidgets.QVBoxLayout() steps_group_layout = QtWidgets.QVBoxLayout()
self.steps_list = QtWidgets.QListWidget() self.steps_list = QtWidgets.QListWidget()
self.steps_list.setMinimumHeight(120) self.steps_list.setMinimumHeight(120)
self.steps_list.currentRowChanged.connect(self.select_step) self.steps_list.currentRowChanged.connect(self._log_steps_list)
steps_group_layout.addWidget(self.steps_list) steps_group_layout.addWidget(self.steps_list)
# Step actions # Step actions
self.btn_add_step = QtWidgets.QPushButton('Add') self.btn_add_step = QtWidgets.QPushButton('Add')
self.btn_add_step.setToolTip('Add step') self.btn_add_step.setToolTip('Add step')
self.btn_add_step.clicked.connect(self.add_step) self.btn_add_step.clicked.connect(self._log_btn_add_step)
self.btn_edit_step = QtWidgets.QPushButton('Edit') self.btn_edit_step = QtWidgets.QPushButton('Edit')
self.btn_edit_step.setToolTip('Edit step') self.btn_edit_step.setToolTip('Edit step')
self.btn_edit_step.clicked.connect(self.edit_step) self.btn_edit_step.clicked.connect(self._log_btn_edit_step)
self.btn_del_step = QtWidgets.QPushButton('Delete') self.btn_del_step = QtWidgets.QPushButton('Delete')
self.btn_del_step.setToolTip('Delete step') self.btn_del_step.setToolTip('Delete step')
self.btn_del_step.clicked.connect(self.delete_step) self.btn_del_step.clicked.connect(self._log_btn_del_step)
self.btn_rename_step = QtWidgets.QPushButton("Rename") self.btn_rename_step = QtWidgets.QPushButton("Rename")
self.btn_rename_step.setToolTip('Rename step') self.btn_rename_step.setToolTip('Rename step')
self.btn_rename_step.clicked.connect(self.rename_step) self.btn_rename_step.clicked.connect(self._log_btn_rename_step)
step_btn_group_layout = QtWidgets.QHBoxLayout() step_btn_group_layout = QtWidgets.QHBoxLayout()
step_btn_group_layout.setSpacing(4) step_btn_group_layout.setSpacing(4)
step_btn_group_layout.setContentsMargins(4, 4, 4, 4) step_btn_group_layout.setContentsMargins(4, 4, 4, 4)
@@ -382,7 +448,7 @@ class MainWindow(QtWidgets.QMainWindow):
# Start/Stop Combined # Start/Stop Combined
self.btn_start_stop = QtWidgets.QPushButton(f'Start ({self.hotkey.upper()})') self.btn_start_stop = QtWidgets.QPushButton(f'Start ({self.hotkey.upper()})')
self.btn_start_stop.setMinimumWidth(80) self.btn_start_stop.setMinimumWidth(80)
self.btn_start_stop.clicked.connect(self.toggle_automation) self.btn_start_stop.clicked.connect(self._log_btn_start_stop)
# State label # State label
self.state_label = QtWidgets.QLabel('State: Paused') self.state_label = QtWidgets.QLabel('State: Paused')
@@ -418,10 +484,100 @@ class MainWindow(QtWidgets.QMainWindow):
central.setLayout(layout) central.setLayout(layout)
self.setCentralWidget(central) self.setCentralWidget(central)
def _log_combo_scenario(self, idx):
"""
Log scenario combo box change and call select_scenario.
"""
logger.info(f"UI: Scenario combo changed to index {idx} ({self.combo.currentText()})")
self.select_scenario()
def _log_combo_window(self, idx):
"""
Log window combo box change and call save_selected_window.
"""
logger.info(f"UI: Window combo changed to index {idx} ({self.window_combo.currentText()})")
self.save_selected_window()
def _log_btn_new(self):
"""
Log New Scenario button press and call create_scenario.
"""
logger.info("UI: New Scenario button pressed")
self.create_scenario()
def _log_btn_import(self):
"""
Log Import Scenario button press and call import_scenario.
"""
logger.info("UI: Import Scenario button pressed")
self.import_scenario()
def _log_btn_export(self):
"""
Log Export Scenario button press and call export_scenario.
"""
logger.info("UI: Export Scenario button pressed")
self.export_scenario()
def _log_btn_rename_scenario(self):
"""
Log Rename Scenario button press and call rename_scenario.
"""
logger.info("UI: Rename Scenario button pressed")
self.rename_scenario()
def _log_steps_list(self, idx):
"""
Log steps list row change and call select_step.
"""
logger.info(f"UI: Steps list row changed to {idx}")
self.select_step(idx)
def _log_btn_add_step(self):
"""
Log Add Step button press and call add_step.
"""
logger.info("UI: Add Step button pressed")
self.add_step()
def _log_btn_edit_step(self):
"""
Log Edit Step button press and call edit_step.
"""
logger.info("UI: Edit Step button pressed")
self.edit_step()
def _log_btn_del_step(self):
"""
Log Delete Step button press and call delete_step.
"""
logger.info("UI: Delete Step button pressed")
self.delete_step()
def _log_btn_rename_step(self):
"""
Log Rename Step button press and call rename_step.
"""
logger.info("UI: Rename Step button pressed")
self.rename_step()
def _log_btn_start_stop(self):
"""
Log Start/Stop button press and call toggle_automation.
"""
logger.info("UI: Start/Stop button pressed")
self.toggle_automation()
def select_step(self, idx): def select_step(self, idx):
"""
Set the selected step index in the UI.
"""
self.selected_step_idx = idx self.selected_step_idx = idx
def add_step(self): def add_step(self):
"""
Open the dialog to add a new step to the current scenario.
"""
self.setWindowState(QtCore.Qt.WindowState.WindowMinimized) self.setWindowState(QtCore.Qt.WindowState.WindowMinimized)
try: try:
dlg = StepDialog(self) dlg = StepDialog(self)
@@ -437,6 +593,9 @@ class MainWindow(QtWidgets.QMainWindow):
logger.info(f'Added step: {step}') logger.info(f'Added step: {step}')
def edit_step(self): def edit_step(self):
"""
Open the dialog to edit the selected step in the current scenario.
"""
idx = self.selected_step_idx idx = self.selected_step_idx
if idx is None or idx < 0 or idx >= len(self.current_scenario.steps): if idx is None or idx < 0 or idx >= len(self.current_scenario.steps):
return return
@@ -455,6 +614,9 @@ class MainWindow(QtWidgets.QMainWindow):
logger.info(f'Edited step at idx {idx}') logger.info(f'Edited step at idx {idx}')
def delete_step(self): def delete_step(self):
"""
Delete the selected step from the current scenario.
"""
idx = self.selected_step_idx idx = self.selected_step_idx
if idx is None or idx < 0 or idx >= len(self.current_scenario.steps): if idx is None or idx < 0 or idx >= len(self.current_scenario.steps):
return return
@@ -464,6 +626,9 @@ class MainWindow(QtWidgets.QMainWindow):
logger.info(f'Deleted step at idx {idx}') logger.info(f'Deleted step at idx {idx}')
def save_last_scenario(self, name): def save_last_scenario(self, name):
"""
Save the name of the last selected scenario to disk.
"""
try: try:
with open(os.path.join(CONFIG_DIR, 'last_scenario.txt'), 'w') as f: with open(os.path.join(CONFIG_DIR, 'last_scenario.txt'), 'w') as f:
f.write(name) f.write(name)
@@ -471,6 +636,9 @@ class MainWindow(QtWidgets.QMainWindow):
logger.error(f"Could not save last scenario: {e}") logger.error(f"Could not save last scenario: {e}")
def read_last_scenario(self): def read_last_scenario(self):
"""
Read the name of the last selected scenario from disk.
"""
try: try:
path = os.path.join(CONFIG_DIR, 'last_scenario.txt') path = os.path.join(CONFIG_DIR, 'last_scenario.txt')
if not os.path.exists(path): if not os.path.exists(path):
@@ -482,6 +650,9 @@ class MainWindow(QtWidgets.QMainWindow):
return None return None
def load_scenarios(self): def load_scenarios(self):
"""
Load all scenarios and populate the scenario combo box.
"""
logger.debug('Loading scenarios...') logger.debug('Loading scenarios...')
self.combo.clear() self.combo.clear()
scenarios = Scenario.list_all() scenarios = Scenario.list_all()
@@ -508,6 +679,9 @@ class MainWindow(QtWidgets.QMainWindow):
self.combo.setCurrentIndex(0) self.combo.setCurrentIndex(0)
def select_scenario(self): def select_scenario(self):
"""
Load the selected scenario and update the UI.
"""
name = self.combo.currentText() name = self.combo.currentText()
logger.info(f'Scenario selected: {name}') logger.info(f'Scenario selected: {name}')
if name: if name:
@@ -517,6 +691,9 @@ class MainWindow(QtWidgets.QMainWindow):
self.load_selected_window_from_config() self.load_selected_window_from_config()
def create_scenario(self): def create_scenario(self):
"""
Open a dialog to create a new scenario.
"""
name, ok = QtWidgets.QInputDialog.getText(self, 'New Scenario', 'Enter scenario name:') name, ok = QtWidgets.QInputDialog.getText(self, 'New Scenario', 'Enter scenario name:')
if ok and name: if ok and name:
logger.info(f'Creating new scenario: {name}') logger.info(f'Creating new scenario: {name}')
@@ -526,6 +703,9 @@ class MainWindow(QtWidgets.QMainWindow):
self.combo.setCurrentText(name) self.combo.setCurrentText(name)
def rename_scenario(self): def rename_scenario(self):
"""
Open a dialog to rename the current scenario.
"""
if not self.current_scenario: if not self.current_scenario:
return return
@@ -550,6 +730,9 @@ class MainWindow(QtWidgets.QMainWindow):
self.combo.setCurrentText(new_name) self.combo.setCurrentText(new_name)
def rename_step(self): def rename_step(self):
"""
Open a dialog to rename the selected step.
"""
if not self.current_scenario or self.selected_step_idx is None: if not self.current_scenario or self.selected_step_idx is None:
return return
@@ -563,6 +746,9 @@ class MainWindow(QtWidgets.QMainWindow):
self.refresh_lists() self.refresh_lists()
def import_scenario(self): def import_scenario(self):
"""
Open a file dialog to import a scenario from a zip file.
"""
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, 'Import Scenario', '', 'ZIP Files (*.zip)') path, _ = QtWidgets.QFileDialog.getOpenFileName(self, 'Import Scenario', '', 'ZIP Files (*.zip)')
if path: if path:
try: try:
@@ -596,6 +782,9 @@ class MainWindow(QtWidgets.QMainWindow):
QtWidgets.QMessageBox.critical(self, "Import Error", f"Failed to import scenario.\n{e}") QtWidgets.QMessageBox.critical(self, "Import Error", f"Failed to import scenario.\n{e}")
def export_scenario(self): def export_scenario(self):
"""
Open a file dialog to export the current scenario to a zip file.
"""
if not self.current_scenario: if not self.current_scenario:
logger.warning('No scenario selected for export.') logger.warning('No scenario selected for export.')
return return
@@ -618,6 +807,12 @@ class MainWindow(QtWidgets.QMainWindow):
def add_action(self): def add_action(self):
"""
Open the dialog to add a new action to the step.
"""
"""
Open the dialog to add an action to the current scenario.
"""
logger.debug('Add Action: Opening action dialog.') logger.debug('Add Action: Opening action dialog.')
dlg = ActionDialog(self, self.current_scenario.images) dlg = ActionDialog(self, self.current_scenario.images)
if dlg.exec(): if dlg.exec():
@@ -628,6 +823,9 @@ class MainWindow(QtWidgets.QMainWindow):
logger.info(f'Action added: {action}') logger.info(f'Action added: {action}')
def refresh_lists(self): def refresh_lists(self):
"""
Refresh the step list widget with the current scenario's steps.
"""
logger.debug('Refreshing steps list.') logger.debug('Refreshing steps list.')
self.steps_list.clear() self.steps_list.clear()
if not self.current_scenario: if not self.current_scenario:
@@ -640,6 +838,9 @@ class MainWindow(QtWidgets.QMainWindow):
self.steps_list.addItem(f"{name} [{cond}] imgs: {imgs} actions: {acts}") self.steps_list.addItem(f"{name} [{cond}] imgs: {imgs} actions: {acts}")
def refresh_window_list(self): def refresh_window_list(self):
"""
Populate the window selection dropdown with all open windows and 'Entire Screen'.
"""
self.window_combo.blockSignals(True) self.window_combo.blockSignals(True)
self.window_combo.clear() self.window_combo.clear()
self.window_combo.addItem('Entire Screen') self.window_combo.addItem('Entire Screen')
@@ -653,6 +854,9 @@ class MainWindow(QtWidgets.QMainWindow):
self.window_combo.blockSignals(False) self.window_combo.blockSignals(False)
def save_selected_window(self): def save_selected_window(self):
"""
Save the currently selected window to the scenario's config file.
"""
if self.current_scenario: if self.current_scenario:
selected_window = self.window_combo.currentText() selected_window = self.window_combo.currentText()
scenario_dir = self.current_scenario.get_scenario_dir() scenario_dir = self.current_scenario.get_scenario_dir()
@@ -664,6 +868,9 @@ class MainWindow(QtWidgets.QMainWindow):
logger.error(f'Failed to save window selection: {e}') logger.error(f'Failed to save window selection: {e}')
def load_selected_window_from_config(self): def load_selected_window_from_config(self):
"""
Load the selected window from the scenario's config file and update the dropdown.
"""
if self.current_scenario: if self.current_scenario:
scenario_dir = self.current_scenario.get_scenario_dir() scenario_dir = self.current_scenario.get_scenario_dir()
config_path = os.path.join(scenario_dir, 'window_config.json') config_path = os.path.join(scenario_dir, 'window_config.json')
@@ -684,7 +891,14 @@ class MainWindow(QtWidgets.QMainWindow):
self.window_combo.setCurrentIndex(0) self.window_combo.setCurrentIndex(0)
# StepDialog for creating/editing a step # StepDialog for creating/editing a step
class StepDialog(QtWidgets.QDialog): class StepDialog(QtWidgets.QDialog):
"""
Dialog for creating or editing a step in a scenario.
Allows editing images, actions, and step properties.
"""
def __init__(self, parent=None, step=None): def __init__(self, parent=None, step=None):
"""
Initialize the StepDialog for creating or editing a step.
"""
super().__init__(parent) super().__init__(parent)
self.setWindowTitle('Step Editor') self.setWindowTitle('Step Editor')
self.resize(600, 400) self.resize(600, 400)
@@ -769,6 +983,9 @@ class StepDialog(QtWidgets.QDialog):
self.setLayout(layout) self.setLayout(layout)
def move_action_up(self): def move_action_up(self):
"""
Move the selected action up in the action list.
"""
idx = self.act_list.currentRow() idx = self.act_list.currentRow()
if idx > 0: if idx > 0:
self.actions[idx-1], self.actions[idx] = self.actions[idx], self.actions[idx-1] self.actions[idx-1], self.actions[idx] = self.actions[idx], self.actions[idx-1]
@@ -777,6 +994,9 @@ class StepDialog(QtWidgets.QDialog):
self.act_list.setCurrentRow(idx-1) self.act_list.setCurrentRow(idx-1)
def move_action_down(self): def move_action_down(self):
"""
Move the selected action down in the action list.
"""
idx = self.act_list.currentRow() idx = self.act_list.currentRow()
if idx < len(self.actions) - 1 and idx >= 0: if idx < len(self.actions) - 1 and idx >= 0:
self.actions[idx+1], self.actions[idx] = self.actions[idx], self.actions[idx+1] self.actions[idx+1], self.actions[idx] = self.actions[idx], self.actions[idx+1]
@@ -786,6 +1006,9 @@ class StepDialog(QtWidgets.QDialog):
class NameImageDialog(QtWidgets.QDialog): class NameImageDialog(QtWidgets.QDialog):
def __init__(self, pixmap, parent=None): def __init__(self, pixmap, parent=None):
"""
Initialize the dialog for naming a new image.
"""
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("Name Your Image") self.setWindowTitle("Name Your Image")
@@ -810,10 +1033,16 @@ class StepDialog(QtWidgets.QDialog):
layout.addWidget(button_box) layout.addWidget(button_box)
def get_name(self): def get_name(self):
"""
Return the entered image name.
"""
return self.name_edit.text() return self.name_edit.text()
def add_image_to_step(self): def add_image_to_step(self):
"""
Add a new image to the step by taking a screenshot and letting the user select a region.
"""
main_window = self.parent() main_window = self.parent()
step_name = self.name_edit.text() step_name = self.name_edit.text()
if not step_name: if not step_name:
@@ -888,6 +1117,9 @@ class StepDialog(QtWidgets.QDialog):
QtWidgets.QMessageBox.critical(self, 'Error', 'Failed to save the screenshot file.') QtWidgets.QMessageBox.critical(self, 'Error', 'Failed to save the screenshot file.')
def update_image_preview(self, current, previous): def update_image_preview(self, current, previous):
"""
Update the image preview label when a new image is selected.
"""
if current: if current:
idx = self.img_list.row(current) idx = self.img_list.row(current)
path = self.images[idx]['path'] path = self.images[idx]['path']
@@ -897,6 +1129,9 @@ class StepDialog(QtWidgets.QDialog):
self.img_preview.setText('Image Preview') self.img_preview.setText('Image Preview')
def update_sensitivity_spin(self, idx): def update_sensitivity_spin(self, idx):
"""
Update the sensitivity spinbox for the selected image.
"""
if idx < 0 or idx >= len(self.images): if idx < 0 or idx >= len(self.images):
self.sensitivity_spin.setValue(0.9) self.sensitivity_spin.setValue(0.9)
self.sensitivity_spin.setEnabled(False) self.sensitivity_spin.setEnabled(False)
@@ -906,18 +1141,27 @@ class StepDialog(QtWidgets.QDialog):
self.sensitivity_spin.setValue(img.get('sensitivity', 0.9)) self.sensitivity_spin.setValue(img.get('sensitivity', 0.9))
def save_sensitivity_for_image(self, value): def save_sensitivity_for_image(self, value):
"""
Save the sensitivity value for the currently selected image.
"""
idx = self.img_list.currentRow() idx = self.img_list.currentRow()
if idx < 0 or idx >= len(self.images): if idx < 0 or idx >= len(self.images):
return return
self.images[idx]['sensitivity'] = value self.images[idx]['sensitivity'] = value
def delete_image(self): def delete_image(self):
"""
Delete the currently selected image from the step.
"""
idx = self.img_list.currentRow() idx = self.img_list.currentRow()
if idx >= 0: if idx >= 0:
self.images.pop(idx) self.images.pop(idx)
self.img_list.takeItem(idx) self.img_list.takeItem(idx)
def rename_image(self): def rename_image(self):
"""
Open a dialog to rename the currently selected image.
"""
idx = self.img_list.currentRow() idx = self.img_list.currentRow()
if idx < 0: if idx < 0:
return return
@@ -931,6 +1175,9 @@ class StepDialog(QtWidgets.QDialog):
self.img_list.item(idx).setText(new_name) self.img_list.item(idx).setText(new_name)
def retake_screenshot(self): def retake_screenshot(self):
"""
Retake the screenshot for the currently selected image.
"""
idx = self.img_list.currentRow() idx = self.img_list.currentRow()
if idx < 0: if idx < 0:
return return
@@ -977,12 +1224,18 @@ class StepDialog(QtWidgets.QDialog):
self.act_list.addItem(act.get('type', 'action')) self.act_list.addItem(act.get('type', 'action'))
def delete_action(self): def delete_action(self):
"""
Delete the currently selected action from the step.
"""
idx = self.act_list.currentRow() idx = self.act_list.currentRow()
if idx >= 0: if idx >= 0:
self.actions.pop(idx) self.actions.pop(idx)
self.act_list.takeItem(idx) self.act_list.takeItem(idx)
def get_step(self): def get_step(self):
"""
Return the step as a dictionary for saving.
"""
return { return {
'name': self.name_edit.text(), 'name': self.name_edit.text(),
'condition': self.cond_combo.currentText(), 'condition': self.cond_combo.currentText(),
@@ -992,7 +1245,14 @@ class StepDialog(QtWidgets.QDialog):
# StepActionDialog for adding actions to a step # StepActionDialog for adding actions to a step
class StepActionDialog(QtWidgets.QDialog): class StepActionDialog(QtWidgets.QDialog):
"""
Dialog for adding or editing an action for a step.
Supports click, key, scroll, and delay actions.
"""
def __init__(self, parent=None): def __init__(self, parent=None):
"""
Initialize the StepActionDialog for adding or editing a step action.
"""
super().__init__(parent) super().__init__(parent)
self.setWindowTitle('Add Action') self.setWindowTitle('Add Action')
self.type_combo = QtWidgets.QComboBox() self.type_combo = QtWidgets.QComboBox()
@@ -1074,6 +1334,9 @@ class StepActionDialog(QtWidgets.QDialog):
self.update_fields() self.update_fields()
def update_fields(self): def update_fields(self):
"""
Update the visibility of fields based on the selected action type.
"""
t = self.type_combo.currentText() t = self.type_combo.currentText()
self.click_group.setVisible(t == 'click') self.click_group.setVisible(t == 'click')
self.scroll_group.setVisible(t == 'scroll') self.scroll_group.setVisible(t == 'scroll')
@@ -1102,6 +1365,9 @@ class StepActionDialog(QtWidgets.QDialog):
self.click_group.setVisible(False) self.click_group.setVisible(False)
def pick_absolute_position(self): def pick_absolute_position(self):
"""
Open an overlay to pick an absolute position on the screen.
"""
# Overlay to pick a position on screen # Overlay to pick a position on screen
class PosOverlay(QtWidgets.QWidget): class PosOverlay(QtWidgets.QWidget):
pos_picked = QtCore.pyqtSignal(int, int) pos_picked = QtCore.pyqtSignal(int, int)
@@ -1121,10 +1387,16 @@ class StepActionDialog(QtWidgets.QDialog):
overlay.showFullScreen() overlay.showFullScreen()
def set_absolute_position(self, x, y): def set_absolute_position(self, x, y):
"""
Set the absolute position fields to the picked coordinates.
"""
self.abs_x_spin.setValue(x) self.abs_x_spin.setValue(x)
self.abs_y_spin.setValue(y) self.abs_y_spin.setValue(y)
def capture_key(self): def capture_key(self):
"""
Open a dialog to capture a key press from the user.
"""
# Open a modal dialog to capture a key press # Open a modal dialog to capture a key press
dlg = QtWidgets.QDialog(self) dlg = QtWidgets.QDialog(self)
dlg.setWindowTitle('Press a key') dlg.setWindowTitle('Press a key')
@@ -1155,6 +1427,9 @@ class StepActionDialog(QtWidgets.QDialog):
dlg.exec() dlg.exec()
def get_action(self): def get_action(self):
"""
Return the action as a dictionary for saving.
"""
t = self.type_combo.currentText() t = self.type_combo.currentText()
params = {} params = {}
if t == 'click': if t == 'click':
@@ -1178,6 +1453,9 @@ class StepActionDialog(QtWidgets.QDialog):
def perform_step_action(self, action, loc, shape): def perform_step_action(self, action, loc, shape):
"""
Perform the given step action at the specified location.
"""
act_type = action['type'] act_type = action['type']
params = action['params'] params = action['params']
logger.debug(f'Performing step action: {act_type}, params={params}, loc={loc}, shape={shape}') logger.debug(f'Performing step action: {act_type}, params={params}, loc={loc}, shape={shape}')
@@ -1218,7 +1496,13 @@ class StepActionDialog(QtWidgets.QDialog):
logger.error(f'Error performing step action {act_type}: {e}') logger.error(f'Error performing step action {act_type}: {e}')
class ActionDialog(QtWidgets.QDialog): class ActionDialog(QtWidgets.QDialog):
"""
Dialog for adding a generic action to a scenario (legacy/unused in main flow).
"""
def __init__(self, parent=None, images=None): def __init__(self, parent=None, images=None):
"""
Initialize the ActionDialog for adding a generic action.
"""
super().__init__(parent) super().__init__(parent)
self.setWindowTitle('Add Action') self.setWindowTitle('Add Action')
self.type_combo = QtWidgets.QComboBox() self.type_combo = QtWidgets.QComboBox()
@@ -1253,6 +1537,9 @@ class ActionDialog(QtWidgets.QDialog):
self.setLayout(layout) self.setLayout(layout)
def get_action(self): def get_action(self):
"""
Return the action as a dictionary for saving.
"""
try: try:
params = json.loads(self.param_edit.text()) params = json.loads(self.param_edit.text())
except Exception: except Exception: