# Macro editor dialog with command builder (PySide6) import os from typing import Optional, List, Dict from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLabel, QLineEdit, QPushButton, QListWidget, QListWidgetItem, QComboBox, QSpinBox, QMessageBox, QFileDialog, QWidget, QGroupBox, QScrollArea ) from PySide6.QtCore import Qt, Signal from PySide6.QtGui import QPixmap, QIcon from config import THEME, IMAGE_EXTENSIONS class CommandItem(QWidget): """Widget representing a single command in the list.""" delete_clicked = Signal() move_up_clicked = Signal() move_down_clicked = Signal() edit_clicked = Signal() def __init__(self, command: dict, parent=None): super().__init__(parent) self.command = command layout = QHBoxLayout(self) layout.setContentsMargins(4, 4, 4, 4) layout.setSpacing(8) # Type label type_label = QLabel(command.get("type", "").upper()) type_label.setFixedWidth(60) type_label.setStyleSheet(f""" background-color: {THEME['accent_color']}; color: white; padding: 4px 8px; border-radius: 4px; font-size: 10px; font-weight: bold; """) type_label.setAlignment(Qt.AlignCenter) layout.addWidget(type_label) # Value label value_label = QLabel(self._get_display_value()) value_label.setStyleSheet(f"color: {THEME['fg_color']}; font-family: monospace;") layout.addWidget(value_label, 1) # Action buttons btn_style = f""" QPushButton {{ background-color: {THEME['button_bg']}; color: {THEME['fg_color']}; border: none; padding: 4px 8px; border-radius: 4px; }} QPushButton:hover {{ background-color: {THEME['highlight_color']}; }} """ edit_btn = QPushButton("Edit") edit_btn.setStyleSheet(btn_style) edit_btn.clicked.connect(self.edit_clicked.emit) layout.addWidget(edit_btn) up_btn = QPushButton("^") up_btn.setStyleSheet(btn_style) up_btn.setFixedWidth(30) up_btn.clicked.connect(self.move_up_clicked.emit) layout.addWidget(up_btn) down_btn = QPushButton("v") down_btn.setStyleSheet(btn_style) down_btn.setFixedWidth(30) down_btn.clicked.connect(self.move_down_clicked.emit) layout.addWidget(down_btn) del_btn = QPushButton("X") del_btn.setStyleSheet(f""" QPushButton {{ background-color: #dc3545; color: white; border: none; padding: 4px 8px; border-radius: 4px; }} QPushButton:hover {{ background-color: #c82333; }} """) del_btn.setFixedWidth(30) del_btn.clicked.connect(self.delete_clicked.emit) layout.addWidget(del_btn) def _get_display_value(self) -> str: """Get display text for the command.""" cmd_type = self.command.get("type", "") if cmd_type == "text": return self.command.get("value", "")[:50] elif cmd_type == "key": return self.command.get("value", "") elif cmd_type == "hotkey": return " + ".join(self.command.get("keys", [])) elif cmd_type == "wait": return f"{self.command.get('ms', 0)}ms" elif cmd_type == "app": return self.command.get("command", "")[:50] return "" class CommandBuilder(QWidget): """Widget for building command sequences.""" commands_changed = Signal() def __init__(self, parent=None): super().__init__(parent) self.commands: List[dict] = [] layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) # Command list self.list_widget = QListWidget() self.list_widget.setStyleSheet(f""" QListWidget {{ background-color: {THEME['bg_color']}; border: 1px solid {THEME['button_bg']}; border-radius: 4px; }} QListWidget::item {{ padding: 4px; }} """) self.list_widget.setMinimumHeight(150) layout.addWidget(self.list_widget) # Add command buttons btn_layout = QHBoxLayout() for cmd_type, label in [ ("text", "+ Text"), ("key", "+ Key"), ("hotkey", "+ Hotkey"), ("wait", "+ Wait"), ("app", "+ App") ]: btn = QPushButton(label) btn.setStyleSheet(f""" QPushButton {{ background-color: {THEME['button_bg']}; color: {THEME['fg_color']}; border: none; padding: 8px 12px; border-radius: 4px; }} QPushButton:hover {{ background-color: {THEME['accent_color']}; }} """) btn.clicked.connect(lambda checked, t=cmd_type: self.add_command(t)) btn_layout.addWidget(btn) layout.addLayout(btn_layout) def set_commands(self, commands: List[dict]): """Set the command list.""" self.commands = list(commands) self.refresh() def get_commands(self) -> List[dict]: """Get the command list.""" return list(self.commands) def refresh(self): """Refresh the command list display.""" self.list_widget.clear() for i, cmd in enumerate(self.commands): item = QListWidgetItem(self.list_widget) widget = CommandItem(cmd) widget.delete_clicked.connect(lambda idx=i: self.remove_command(idx)) widget.move_up_clicked.connect(lambda idx=i: self.move_command(idx, -1)) widget.move_down_clicked.connect(lambda idx=i: self.move_command(idx, 1)) widget.edit_clicked.connect(lambda idx=i: self.edit_command(idx)) item.setSizeHint(widget.sizeHint()) self.list_widget.addItem(item) self.list_widget.setItemWidget(item, widget) def add_command(self, cmd_type: str): """Add a new command.""" command = {"type": cmd_type} if cmd_type == "text": from PySide6.QtWidgets import QInputDialog text, ok = QInputDialog.getText(self, "Text Command", "Enter text to type:") if not ok or not text: return command["value"] = text elif cmd_type == "key": from PySide6.QtWidgets import QInputDialog keys = ["enter", "tab", "escape", "space", "backspace", "delete", "up", "down", "left", "right", "home", "end", "pageup", "pagedown", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12"] key, ok = QInputDialog.getItem(self, "Key Command", "Select key:", keys, 0, True) if not ok or not key: return command["value"] = key.lower() elif cmd_type == "hotkey": from PySide6.QtWidgets import QInputDialog text, ok = QInputDialog.getText( self, "Hotkey Command", "Enter key combination (comma separated, e.g., ctrl,c):" ) if not ok or not text: return command["keys"] = [k.strip().lower() for k in text.split(",")] elif cmd_type == "wait": from PySide6.QtWidgets import QInputDialog ms, ok = QInputDialog.getInt( self, "Wait Command", "Enter delay in milliseconds:", 500, 0, 60000, 100 ) if not ok: return command["ms"] = ms elif cmd_type == "app": from PySide6.QtWidgets import QInputDialog cmd, ok = QInputDialog.getText(self, "App Command", "Enter application command:") if not ok or not cmd: return command["command"] = cmd self.commands.append(command) self.refresh() self.commands_changed.emit() def remove_command(self, index: int): """Remove a command at index.""" if 0 <= index < len(self.commands): del self.commands[index] self.refresh() self.commands_changed.emit() def move_command(self, index: int, direction: int): """Move a command up or down.""" new_index = index + direction if 0 <= new_index < len(self.commands): self.commands[index], self.commands[new_index] = \ self.commands[new_index], self.commands[index] self.refresh() self.commands_changed.emit() def edit_command(self, index: int): """Edit a command at index.""" if not (0 <= index < len(self.commands)): return cmd = self.commands[index] cmd_type = cmd.get("type", "") from PySide6.QtWidgets import QInputDialog if cmd_type == "text": text, ok = QInputDialog.getText( self, "Edit Text", "Enter text:", text=cmd.get("value", "") ) if ok and text: cmd["value"] = text elif cmd_type == "key": keys = ["enter", "tab", "escape", "space", "backspace", "delete", "up", "down", "left", "right", "home", "end", "pageup", "pagedown", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12"] current = keys.index(cmd.get("value", "enter")) if cmd.get("value") in keys else 0 key, ok = QInputDialog.getItem(self, "Edit Key", "Select key:", keys, current, True) if ok and key: cmd["value"] = key.lower() elif cmd_type == "hotkey": text, ok = QInputDialog.getText( self, "Edit Hotkey", "Enter key combination:", text=",".join(cmd.get("keys", [])) ) if ok and text: cmd["keys"] = [k.strip().lower() for k in text.split(",")] elif cmd_type == "wait": ms, ok = QInputDialog.getInt( self, "Edit Wait", "Enter delay in milliseconds:", cmd.get("ms", 500), 0, 60000, 100 ) if ok: cmd["ms"] = ms elif cmd_type == "app": text, ok = QInputDialog.getText( self, "Edit App", "Enter application command:", text=cmd.get("command", "") ) if ok and text: cmd["command"] = text self.refresh() self.commands_changed.emit() class MacroEditorDialog(QDialog): """Dialog for creating/editing macros.""" def __init__(self, macro_manager, macro_id: Optional[str] = None, parent=None): super().__init__(parent) self.macro_manager = macro_manager self.macro_id = macro_id self.image_path = "" self.setWindowTitle("Edit Macro" if macro_id else "Add Macro") self.setMinimumSize(500, 500) self.setStyleSheet(f""" QDialog {{ background-color: {THEME['highlight_color']}; }} QLabel {{ color: {THEME['fg_color']}; }} QLineEdit {{ background-color: {THEME['bg_color']}; border: 1px solid {THEME['button_bg']}; border-radius: 4px; padding: 8px; color: {THEME['fg_color']}; }} QLineEdit:focus {{ border-color: {THEME['accent_color']}; }} """) self.setup_ui() # Load existing macro data if editing if macro_id: self.load_macro() def setup_ui(self): """Setup the dialog UI.""" layout = QVBoxLayout(self) layout.setSpacing(16) # Scroll area for content scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setStyleSheet("border: none;") content = QWidget() content_layout = QVBoxLayout(content) content_layout.setSpacing(12) # Name field name_group = QGroupBox("Macro Name") name_group.setStyleSheet(f""" QGroupBox {{ color: {THEME['fg_color']}; font-weight: bold; border: 1px solid {THEME['button_bg']}; border-radius: 4px; margin-top: 8px; padding-top: 8px; }} QGroupBox::title {{ subcontrol-origin: margin; left: 10px; }} """) name_layout = QVBoxLayout(name_group) self.name_input = QLineEdit() self.name_input.setPlaceholderText("Enter macro name") name_layout.addWidget(self.name_input) content_layout.addWidget(name_group) # Category field category_group = QGroupBox("Category (optional)") category_group.setStyleSheet(name_group.styleSheet()) category_layout = QVBoxLayout(category_group) self.category_input = QLineEdit() self.category_input.setPlaceholderText("Enter category") category_layout.addWidget(self.category_input) content_layout.addWidget(category_group) # Command builder commands_group = QGroupBox("Commands") commands_group.setStyleSheet(name_group.styleSheet()) commands_layout = QVBoxLayout(commands_group) self.command_builder = CommandBuilder() commands_layout.addWidget(self.command_builder) content_layout.addWidget(commands_group) # Image selection image_group = QGroupBox("Image (optional)") image_group.setStyleSheet(name_group.styleSheet()) image_layout = QHBoxLayout(image_group) self.image_preview = QLabel() self.image_preview.setFixedSize(64, 64) self.image_preview.setStyleSheet(f""" background-color: {THEME['bg_color']}; border-radius: 4px; """) self.image_preview.setAlignment(Qt.AlignCenter) image_layout.addWidget(self.image_preview) image_btn_layout = QVBoxLayout() select_btn = QPushButton("Select Image") select_btn.setStyleSheet(f""" QPushButton {{ background-color: {THEME['button_bg']}; color: {THEME['fg_color']}; border: none; padding: 8px 16px; border-radius: 4px; }} QPushButton:hover {{ background-color: {THEME['accent_color']}; }} """) select_btn.clicked.connect(self.select_image) image_btn_layout.addWidget(select_btn) clear_btn = QPushButton("Clear Image") clear_btn.setStyleSheet(select_btn.styleSheet()) clear_btn.clicked.connect(self.clear_image) image_btn_layout.addWidget(clear_btn) image_layout.addLayout(image_btn_layout) image_layout.addStretch() content_layout.addWidget(image_group) content_layout.addStretch() scroll.setWidget(content) layout.addWidget(scroll) # Dialog buttons btn_layout = QHBoxLayout() if self.macro_id: delete_btn = QPushButton("Delete") delete_btn.setStyleSheet(f""" QPushButton {{ background-color: #dc3545; color: white; border: none; padding: 10px 20px; border-radius: 4px; font-weight: bold; }} QPushButton:hover {{ background-color: #c82333; }} """) delete_btn.clicked.connect(self.delete_macro) btn_layout.addWidget(delete_btn) btn_layout.addStretch() cancel_btn = QPushButton("Cancel") cancel_btn.setStyleSheet(f""" QPushButton {{ background-color: {THEME['button_bg']}; color: {THEME['fg_color']}; border: none; padding: 10px 20px; border-radius: 4px; }} QPushButton:hover {{ background-color: {THEME['highlight_color']}; }} """) cancel_btn.clicked.connect(self.reject) btn_layout.addWidget(cancel_btn) save_btn = QPushButton("Save") save_btn.setStyleSheet(f""" QPushButton {{ background-color: {THEME['accent_color']}; color: white; border: none; padding: 10px 20px; border-radius: 4px; font-weight: bold; }} QPushButton:hover {{ background-color: #0096ff; }} """) save_btn.clicked.connect(self.save_macro) btn_layout.addWidget(save_btn) layout.addLayout(btn_layout) def load_macro(self): """Load existing macro data into the form.""" macro = self.macro_manager.get_macro(self.macro_id) if not macro: return self.name_input.setText(macro.get("name", "")) self.category_input.setText(macro.get("category", "")) self.command_builder.set_commands(macro.get("commands", [])) if macro.get("image_path"): self.image_path = macro["image_path"] pixmap = QPixmap(self.image_path) if not pixmap.isNull(): self.image_preview.setPixmap( pixmap.scaled(64, 64, Qt.KeepAspectRatio, Qt.SmoothTransformation) ) def select_image(self): """Open file dialog to select an image.""" ext_filter = "Images (" + " ".join(f"*{ext}" for ext in IMAGE_EXTENSIONS) + ")" file_path, _ = QFileDialog.getOpenFileName( self, "Select Image", "", ext_filter ) if file_path: self.image_path = file_path pixmap = QPixmap(file_path) self.image_preview.setPixmap( pixmap.scaled(64, 64, Qt.KeepAspectRatio, Qt.SmoothTransformation) ) def clear_image(self): """Clear the selected image.""" self.image_path = "" self.image_preview.clear() def save_macro(self): """Save the macro.""" name = self.name_input.text().strip() if not name: QMessageBox.warning(self, "Error", "Please enter a macro name") return commands = self.command_builder.get_commands() if not commands: QMessageBox.warning(self, "Error", "Please add at least one command") return category = self.category_input.text().strip() if self.macro_id: # Update existing macro self.macro_manager.update_macro( self.macro_id, name=name, commands=commands, category=category, image_path=self.image_path if self.image_path else None ) else: # Create new macro self.macro_manager.add_macro( name=name, commands=commands, category=category, image_path=self.image_path ) self.accept() def delete_macro(self): """Delete the current macro.""" reply = QMessageBox.question( self, "Delete Macro", "Are you sure you want to delete this macro?", QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: self.macro_manager.delete_macro(self.macro_id) self.accept()