diff --git a/.github/workflows/dev_python3_10.yml b/.github/workflows/dev_python3_10.yml index 71dac82..0c42d63 100644 --- a/.github/workflows/dev_python3_10.yml +++ b/.github/workflows/dev_python3_10.yml @@ -75,8 +75,6 @@ jobs: run: python ./test/unit_test/json/json_test.py - name: Test Generate Json Report run: python ./test/unit_test/generate_report/json_report.py - - name: Test Timeout Module - run: python ./test/unit_test/timeout/timeout_test.py - name: Test Generate HTML Report run: python ./test/unit_test/generate_report/html_report_test.py @@ -91,7 +89,5 @@ jobs: - name: Test Get Mouse Info run: python ./test/unit_test/get_info/mouse_info.py - - name: Test Get Special Info - run: python ./test/unit_test/get_info/special_info.py - name: Test Get Keyboard Info run: python ./test/unit_test/get_info/keyboard_info.py diff --git a/.github/workflows/dev_python3_11.yml b/.github/workflows/dev_python3_11.yml index a94a267..0ea093e 100644 --- a/.github/workflows/dev_python3_11.yml +++ b/.github/workflows/dev_python3_11.yml @@ -75,8 +75,6 @@ jobs: run: python ./test/unit_test/json/json_test.py - name: Test Generate Json Report run: python ./test/unit_test/generate_report/json_report.py - - name: Test Timeout Module - run: python ./test/unit_test/timeout/timeout_test.py - name: Test Generate HTML Report run: python ./test/unit_test/generate_report/html_report_test.py @@ -91,7 +89,5 @@ jobs: - name: Test Get Mouse Info run: python ./test/unit_test/get_info/mouse_info.py - - name: Test Get Special Info - run: python ./test/unit_test/get_info/special_info.py - name: Test Get Keyboard Info run: python ./test/unit_test/get_info/keyboard_info.py diff --git a/.github/workflows/dev_python3_12.yml b/.github/workflows/dev_python3_12.yml index a1fd222..91d11bf 100644 --- a/.github/workflows/dev_python3_12.yml +++ b/.github/workflows/dev_python3_12.yml @@ -75,8 +75,6 @@ jobs: run: python ./test/unit_test/json/json_test.py - name: Test Generate Json Report run: python ./test/unit_test/generate_report/json_report.py - - name: Test Timeout Module - run: python ./test/unit_test/timeout/timeout_test.py - name: Test Generate HTML Report run: python ./test/unit_test/generate_report/html_report_test.py @@ -91,7 +89,5 @@ jobs: - name: Test Get Mouse Info run: python ./test/unit_test/get_info/mouse_info.py - - name: Test Get Special Info - run: python ./test/unit_test/get_info/special_info.py - name: Test Get Keyboard Info run: python ./test/unit_test/get_info/keyboard_info.py diff --git a/.github/workflows/stable_python3_10.yml b/.github/workflows/stable_python3_10.yml index bc49cf1..1ad7a80 100644 --- a/.github/workflows/stable_python3_10.yml +++ b/.github/workflows/stable_python3_10.yml @@ -75,8 +75,6 @@ jobs: run: python ./test/unit_test/json/json_test.py - name: Test Generate Json Report run: python ./test/unit_test/generate_report/json_report.py - - name: Test Timeout Module - run: python ./test/unit_test/timeout/timeout_test.py - name: Test Generate HTML Report run: python ./test/unit_test/generate_report/html_report_test.py @@ -91,7 +89,5 @@ jobs: - name: Test Get Mouse Info run: python ./test/unit_test/get_info/mouse_info.py - - name: Test Get Special Info - run: python ./test/unit_test/get_info/special_info.py - name: Test Get Keyboard Info run: python ./test/unit_test/get_info/keyboard_info.py diff --git a/.github/workflows/stable_python3_11.yml b/.github/workflows/stable_python3_11.yml index fa31dce..b8f1b42 100644 --- a/.github/workflows/stable_python3_11.yml +++ b/.github/workflows/stable_python3_11.yml @@ -75,8 +75,6 @@ jobs: run: python ./test/unit_test/json/json_test.py - name: Test Generate Json Report run: python ./test/unit_test/generate_report/json_report.py - - name: Test Timeout Module - run: python ./test/unit_test/timeout/timeout_test.py - name: Test Generate HTML Report run: python ./test/unit_test/generate_report/html_report_test.py @@ -91,7 +89,5 @@ jobs: - name: Test Get Mouse Info run: python ./test/unit_test/get_info/mouse_info.py - - name: Test Get Special Info - run: python ./test/unit_test/get_info/special_info.py - name: Test Get Keyboard Info run: python ./test/unit_test/get_info/keyboard_info.py diff --git a/.github/workflows/stable_python3_12.yml b/.github/workflows/stable_python3_12.yml index da9b528..d264ebf 100644 --- a/.github/workflows/stable_python3_12.yml +++ b/.github/workflows/stable_python3_12.yml @@ -75,8 +75,6 @@ jobs: run: python ./test/unit_test/json/json_test.py - name: Test Generate Json Report run: python ./test/unit_test/generate_report/json_report.py - - name: Test Timeout Module - run: python ./test/unit_test/timeout/timeout_test.py - name: Test Generate HTML Report run: python ./test/unit_test/generate_report/html_report_test.py @@ -91,7 +89,5 @@ jobs: - name: Test Get Mouse Info run: python ./test/unit_test/get_info/mouse_info.py - - name: Test Get Special Info - run: python ./test/unit_test/get_info/special_info.py - name: Test Get Keyboard Info run: python ./test/unit_test/get_info/keyboard_info.py diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 23a5847..1d8bc7b 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -5,9 +5,8 @@ - - + - { - "keyToString": { - "DefaultHtmlFileTemplate": "HTML File", - "Python.auto_control_keyboard.executor": "Run", - "Python.auto_control_mouse.executor": "Run", - "Python.calculator.executor": "Run", - "Python.callback_test.executor": "Run", - "Python.create_project_test.executor": "Run", - "Python.critical_exit_test.executor": "Run", - "Python.executor_one_file.executor": "Run", - "Python.get_pixel_test.executor": "Run", - "Python.keyboard_is_press_test.executor": "Run", - "Python.main_widget.executor": "Run", - "Python.main_window.executor": "Run", - "Python.record_test.executor": "Run", - "Python.screen_test.executor": "Run", - "Python.screenshot_test.executor": "Run", - "Python.test.executor": "Run", - "Python.video_recording.executor": "Run", - "Python.win32_screen.executor": "Run", - "RunOnceActivity.OpenProjectViewOnStart": "true", - "RunOnceActivity.ShowReadmeOnStart": "true", - "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", - "RunOnceActivity.git.unshallow": "true", - "WebServerToolWindowFactoryState": "false", - "git-widget-placeholder": "dev", - "ignore.virus.scanning.warn.message": "true", - "junie.onboarding.icon.badge.shown": "true", - "last_opened_file_path": "C:/CodeWorkspace/Python/AutoControlGUI", - "node.js.detected.package.eslint": "true", - "node.js.detected.package.tslint": "true", - "node.js.selected.package.eslint": "(autodetect)", - "node.js.selected.package.tslint": "(autodetect)", - "nodejs_package_manager_path": "npm", - "settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable", - "to.speed.mode.migration.done": "true", - "vue.rearranger.settings.migration": "true" + +}]]> + - @@ -129,8 +132,8 @@ - - + + - + - + - + + + + - - - - @@ -607,6 +610,11 @@ + + + + + @@ -632,6 +640,7 @@ + @@ -639,7 +648,7 @@ - + @@ -648,6 +657,7 @@ + \ No newline at end of file diff --git a/README.md b/README.md index ec814d3..cb9efe4 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,56 @@ -### AutoControl +# AutoControl -[![Downloads](https://static.pepy.tech/badge/je-auto-control)](https://pepy.tech/project/je-auto-control) +AutoControl is a cross‑platform GUI automation framework that provides powerful and efficient features for mouse, keyboard, and image‑based automation. -[![Codacy Badge](https://app.codacy.com/project/badge/Grade/db0f6e626a614f67bf2b6b1f54325a24)](https://www.codacy.com/gh/JE-Chen/AutoControl/dashboard?utm_source=github.com&utm_medium=referral&utm_content=JE-Chen/AutoControl&utm_campaign=Badge_Grade) +## Features -[![AutoControl Stable Python3.8](https://github.com/Intergration-Automation-Testing/AutoControl/actions/workflows/stable_python3_8.yml/badge.svg)](https://github.com/Intergration-Automation-Testing/AutoControl/actions/workflows/stable_python3_8.yml) +* Powerful and practical GUI automation. +* Image recognition (template matching). +* Coordinate‑based operations. +* Mouse automation. +* Keyboard automation. +* Locate images. +* AutoControl scripting support. +* Generate JSON / HTML / XML reports. +* Remote automation support. +* Shell command integration. +* Screenshot support. +* OS‑independent design. +* Project & template management. -[![AutoControl Stable Python3.9](https://github.com/Intergration-Automation-Testing/AutoControl/actions/workflows/stable_python3_9.yml/badge.svg)](https://github.com/Intergration-Automation-Testing/AutoControl/actions/workflows/stable_python3_9.yml) +## ⚠️ Notice +Currently Unix/Linux Wayland GUI is not supported. +This may be added as a future feature. -[![AutoControl Stable Python3.10](https://github.com/Intergration-Automation-Testing/AutoControl/actions/workflows/stable_python3_10.yml/badge.svg)](https://github.com/Intergration-Automation-Testing/AutoControl/actions/workflows/stable_python3_10.yml) - -[![AutoControl Stable Python3.11](https://github.com/Intergration-Automation-Testing/AutoControl/actions/workflows/stable_python3_11.yml/badge.svg)](https://github.com/Intergration-Automation-Testing/AutoControl/actions/workflows/stable_python3_11.yml) - -### Documentation -[![Documentation Status](https://readthedocs.org/projects/autocontrol/badge/?version=latest)](https://autocontrol.readthedocs.io/en/latest/?badge=latest) - -[AutoControl Doc Click Here!](https://autocontrol.readthedocs.io/en/latest/) - ---- - -> Project Kanban \ -> https://github.com/orgs/Integration-Automation/projects/2/views/1 \ -> * Powerful and useful GUI Automation. -> * Image recognition. -> * Coordinate-based. -> * Mouse automation. -> * Keyboard automation. -> * Locate image less than 0.5 sec. -> * AutoControl script. -> * Generate JSON/HTML/XML report. -> * Remote Automation support. -> * 1 sec / thousands keyboard event. -> * 1 sec / thousands mouse event. -> * Open another process support. -> * Shell command support. -> * Screenshot support. -> * OS Independent. -> * Project & Template support. - ---- - -### NOTICE -> We don't support Unix/Linux Wayland GUI Now \ -> May be future feature ---- - -## install +## Installation ``` # make sure you have install cmake libssl-dev (on linux) pip install je_auto_control ``` -## Info +## Requirements + +* Python 3.9 or later +* pip 19.3 or later -> * requirement ->> * Python 3.9 or later ->> * pip 19.3 or later -> * Dev env ->> * windows 11 ->> * osx 11 big sur ->> * ubuntu 20.0.4 +## Development Environment +* Windows 11 +* macOS 11 Big Sur +* Ubuntu 20.04 -> * Test on ->> * Windows 10 ~ 11 ->> * osx 10.5 ~ 11 big sur ->> * ubuntu 20.0.4 ->> * raspberry pi 3B and 4B +## Tested On +* Windows 10 ~ 11 +* macOS 10.15 ~ 11 Big Sur +* Ubuntu 20.04 +* Raspberry Pi 3B / 4B + +## Setting Up Development Environment +```commandline +pip install -r dev_requirements.txt +``` -## How to set dev environment -> * Clone repo on GitHub or download source code -> * Prepare a python venv -> * Run command "pip install --upgrade pip" -> * Run command "pip install -r dev_requirements.txt" -### Architecture Diagram -![architecture_diagram](architecture_diagram/AutoControl_Architecture.drawio.png) diff --git a/dev.toml b/dev.toml index 50e6b9b..79b7a62 100644 --- a/dev.toml +++ b/dev.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" [project] name = "je_auto_control_dev" -version = "0.0.127" +version = "0.0.131" authors = [ { name = "JE-Chen", email = "jechenmailman@gmail.com" }, ] diff --git a/je_auto_control/__init__.py b/je_auto_control/__init__.py index 0619a37..8064411 100644 --- a/je_auto_control/__init__.py +++ b/je_auto_control/__init__.py @@ -73,9 +73,6 @@ # test record from je_auto_control.utils.test_record.record_test_class import \ test_record_instance -# timeout -from je_auto_control.utils.timeout.multiprocess_timeout import \ - multiprocess_timeout # Windows from je_auto_control.windows.window import windows_window_manage from je_auto_control.wrapper.auto_control_image import locate_all_image @@ -84,7 +81,6 @@ # import keyboard from je_auto_control.wrapper.auto_control_keyboard import check_key_is_press from je_auto_control.wrapper.auto_control_keyboard import get_keyboard_keys_table -from je_auto_control.wrapper.auto_control_keyboard import get_special_table from je_auto_control.wrapper.auto_control_keyboard import hotkey from je_auto_control.wrapper.auto_control_keyboard import keyboard_keys_table from je_auto_control.wrapper.auto_control_keyboard import press_keyboard_key @@ -96,7 +92,7 @@ from je_auto_control.wrapper.auto_control_mouse import click_mouse from je_auto_control.wrapper.auto_control_mouse import get_mouse_position from je_auto_control.wrapper.auto_control_mouse import mouse_keys_table -from je_auto_control.wrapper.auto_control_mouse import mouse_scroll +from je_auto_control.wrapper.auto_control_mouse import mouse_scroll_error_message from je_auto_control.wrapper.auto_control_mouse import press_mouse from je_auto_control.wrapper.auto_control_mouse import release_mouse from je_auto_control.wrapper.auto_control_mouse import send_mouse_event_to_window @@ -112,7 +108,7 @@ __all__ = [ "click_mouse", "mouse_keys_table", "get_mouse_position", "press_mouse", "release_mouse", - "mouse_scroll", "set_mouse_position", "special_mouse_keys_table", + "mouse_scroll_error_message", "set_mouse_position", "special_mouse_keys_table", "keyboard_keys_table", "press_keyboard_key", "release_keyboard_key", "type_keyboard", "check_key_is_press", "write", "hotkey", "start_exe", "get_keyboard_keys_table", "screen_size", "screenshot", "locate_all_image", "locate_image_center", "locate_and_click", @@ -121,10 +117,10 @@ "AutoControlScreenException", "ImageNotFoundException", "AutoControlJsonActionException", "AutoControlRecordException", "AutoControlActionNullException", "AutoControlActionException", "record", "stop_record", "read_action_json", "write_action_json", "execute_action", "execute_files", "executor", - "add_command_to_executor", "multiprocess_timeout", "test_record_instance", "screenshot", "pil_screenshot", + "add_command_to_executor", "test_record_instance", "screenshot", "pil_screenshot", "generate_html", "generate_html_report", "generate_json", "generate_json_report", "generate_xml", "generate_xml_report", "get_dir_files_as_list", "create_project_dir", "start_autocontrol_socket_server", - "callback_executor", "package_manager", "get_special_table", "ShellManager", "default_shell_manager", + "callback_executor", "package_manager", "ShellManager", "default_shell_manager", "RecordingThread", "send_key_event_to_window", "send_mouse_event_to_window", "windows_window_manage", "ScreenRecorder", "get_pixel" ] diff --git a/je_auto_control/__main__.py b/je_auto_control/__main__.py index 117cafc..330d0c0 100644 --- a/je_auto_control/__main__.py +++ b/je_auto_control/__main__.py @@ -4,7 +4,7 @@ import sys from je_auto_control.utils.exception.exception_tags import \ - argparse_get_wrong_data + argparse_get_wrong_data_error_message from je_auto_control.utils.exception.exceptions import \ AutoControlArgparseException from je_auto_control.utils.executor.action_executor import execute_action @@ -62,7 +62,7 @@ def preprocess_read_str_execute_action(execute_str: str): if value is not None: argparse_event_dict.get(key)(value) if all(value is None for value in args.values()): - raise AutoControlArgparseException(argparse_get_wrong_data) + raise AutoControlArgparseException(argparse_get_wrong_data_error_message) except Exception as error: print(repr(error), file=sys.stderr) sys.exit(1) diff --git a/je_auto_control/gui/main_widget.py b/je_auto_control/gui/main_widget.py index e0ded4d..9e54e3d 100644 --- a/je_auto_control/gui/main_widget.py +++ b/je_auto_control/gui/main_widget.py @@ -6,30 +6,33 @@ ) from je_auto_control.gui.language_wrapper.multi_language_wrapper import language_wrapper -from je_auto_control.utils.executor.action_executor import execute_action from je_auto_control.wrapper.auto_control_keyboard import type_keyboard from je_auto_control.wrapper.auto_control_mouse import click_mouse -from je_auto_control.wrapper.auto_control_record import record, stop_record from je_auto_control.wrapper.platform_wrapper import keyboard_keys_table, mouse_keys_table class AutoControlGUIWidget(QWidget): + """ + AutoControl GUI Widget + 自動控制 GUI 元件 + 提供滑鼠與鍵盤操作的自動化設定介面 + """ def __init__(self, parent=None): super().__init__(parent) main_layout = QVBoxLayout() - # Grid for input fields + # === Grid for input fields 輸入欄位區塊 === grid = QGridLayout() - # Interval time + # Interval time 間隔時間 grid.addWidget(QLabel(language_wrapper.language_word_dict.get("interval_time")), 0, 0) self.interval_input = QLineEdit() self.interval_input.setValidator(QIntValidator()) grid.addWidget(self.interval_input, 0, 1) - # Cursor X/Y + # Cursor X/Y 滑鼠座標 grid.addWidget(QLabel(language_wrapper.language_word_dict.get("cursor_x")), 2, 0) self.cursor_x_input = QLineEdit() self.cursor_x_input.setValidator(QIntValidator()) @@ -40,25 +43,25 @@ def __init__(self, parent=None): self.cursor_y_input.setValidator(QIntValidator()) grid.addWidget(self.cursor_y_input, 3, 1) - # Mouse button + # Mouse button 滑鼠按鍵 grid.addWidget(QLabel(language_wrapper.language_word_dict.get("mouse_button")), 4, 0) self.mouse_button_combo = QComboBox() self.mouse_button_combo.addItems(mouse_keys_table) grid.addWidget(self.mouse_button_combo, 4, 1) - # Keyboard button + # Keyboard button 鍵盤按鍵 grid.addWidget(QLabel(language_wrapper.language_word_dict.get("keyboard_button")), 5, 0) self.keyboard_button_combo = QComboBox() self.keyboard_button_combo.addItems(keyboard_keys_table.keys()) grid.addWidget(self.keyboard_button_combo, 5, 1) - # Click type + # Click type 點擊類型 grid.addWidget(QLabel(language_wrapper.language_word_dict.get("click_type")), 6, 0) self.click_type_combo = QComboBox() self.click_type_combo.addItems(["Single Click", "Double Click"]) grid.addWidget(self.click_type_combo, 6, 1) - # Input method selection + # Input method selection 輸入方式選擇 grid.addWidget(QLabel(language_wrapper.language_word_dict.get("input_method")), 7, 0) self.mouse_radio = QRadioButton(language_wrapper.language_word_dict.get("mouse_radio")) self.keyboard_radio = QRadioButton(language_wrapper.language_word_dict.get("keyboard_radio")) @@ -71,7 +74,7 @@ def __init__(self, parent=None): main_layout.addLayout(grid) - # Repeat options + # === Repeat options 重複執行選項 === repeat_layout = QHBoxLayout() self.repeat_until_stopped = QRadioButton(language_wrapper.language_word_dict.get("repeat_until_stopped_radio")) self.repeat_count_times = QRadioButton(language_wrapper.language_word_dict.get("repeat_radio")) @@ -90,7 +93,7 @@ def __init__(self, parent=None): repeat_layout.addWidget(self.repeat_count_input) main_layout.addLayout(repeat_layout) - # Start/Stop buttons + # === Start/Stop buttons 開始/停止按鈕 === button_layout = QHBoxLayout() self.start_button = QPushButton(language_wrapper.language_word_dict.get("start")) self.start_button.clicked.connect(self.start_autocontrol) @@ -100,30 +103,55 @@ def __init__(self, parent=None): button_layout.addWidget(self.stop_button) main_layout.addLayout(button_layout) - # Timer + # Timer 計時器 self.start_autocontrol_timer = QTimer() - # Connect input method toggle + # Connect input method toggle 輸入方式切換 self.mouse_radio.toggled.connect(self.update_input_mode) self.keyboard_radio.toggled.connect(self.update_input_mode) self.update_input_mode() self.setLayout(main_layout) + # === 更新輸入模式 === def update_input_mode(self): + """ + Enable/Disable input fields based on selected mode + 根據選擇的輸入方式啟用/停用相關欄位 + """ use_mouse = self.mouse_radio.isChecked() self.cursor_x_input.setEnabled(use_mouse) self.cursor_y_input.setEnabled(use_mouse) self.mouse_button_combo.setEnabled(use_mouse) self.keyboard_button_combo.setEnabled(not use_mouse) + # === 開始自動控制 === def start_autocontrol(self): - self.start_autocontrol_timer.setInterval(int(self.interval_input.text())) - self.start_autocontrol_timer.timeout.connect(lambda: self.start_timer_function()) + """ + Start auto control with timer + 啟動計時器開始自動控制 + """ + try: + interval = int(self.interval_input.text()) + except ValueError: + QMessageBox.warning(self, "Warning", "Interval must be a number\n間隔必須是數字") + return + + self.start_autocontrol_timer.setInterval(interval) + self.start_autocontrol_timer.timeout.connect(self.start_timer_function) self.start_autocontrol_timer.start() - self.repeat_max = int(self.repeat_count_input.text()) + try: + self.repeat_max = int(self.repeat_count_input.text()) + except ValueError: + self.repeat_max = 0 + + # === 計時器觸發函式 === def start_timer_function(self): + """ + Timer callback function + 計時器回呼函式 + """ if self.repeat_until_stopped.isChecked(): self.trigger_autocontrol_function() elif self.repeat_count_times.isChecked(): @@ -135,32 +163,52 @@ def start_timer_function(self): self.repeat_max = 0 self.start_autocontrol_timer.stop() + # === 執行自動控制動作 === def trigger_autocontrol_function(self): + """ + Execute mouse or keyboard action + 執行滑鼠或鍵盤操作 + """ click_type = self.click_type_combo.currentText() + if self.mouse_radio.isChecked(): - trigger_function = click_mouse button = self.mouse_button_combo.currentText() - x = int(self.cursor_x_input.text()) - y = int(self.cursor_y_input.text()) - if click_type == "Single Click": - trigger_function(mouse_keycode=button, x=x, y=y) - elif click_type == "Double Click": - trigger_function(mouse_keycode=button, x=x, y=y) - trigger_function(mouse_keycode=button, x=x, y=y) + try: + x = int(self.cursor_x_input.text()) + y = int(self.cursor_y_input.text()) + except ValueError: + QMessageBox.warning(self, "Warning", "Cursor position must be numbers\n座標必須是數字") + return + self._execute_click(click_mouse, click_type, button, x, y) + elif self.keyboard_radio.isChecked(): - trigger_function = type_keyboard button = self.keyboard_button_combo.currentText() - if click_type == "Single Click": - trigger_function(keycode=button) - elif click_type == "Double Click": - trigger_function(keycode=button) - trigger_function(keycode=button) - + self._execute_click(type_keyboard, click_type, button) + + def _execute_click(self, func, click_type, *args, **kwargs): + """ + Helper function to execute single/double click + 輔助函式:執行單擊或雙擊 + """ + func(*args, **kwargs) + if click_type == "Double Click": + func(*args, **kwargs) + + # === 停止自動控制 === def stop_autocontrol(self): + """ + Stop auto control + 停止自動控制 + """ self.start_autocontrol_timer.stop() - + # === 鍵盤快捷鍵事件 === def keyPressEvent(self, event: QKeyEvent): + """ + Handle keyboard shortcut + 處理鍵盤快捷鍵事件 + Ctrl + 4 停止自動控制 + """ if event.modifiers() == Qt.KeyboardModifier.ControlModifier and event.key() == Qt.Key.Key_4: self.start_autocontrol_timer.stop() else: diff --git a/je_auto_control/gui/main_window.py b/je_auto_control/gui/main_window.py index 6beb779..8f7d3ed 100644 --- a/je_auto_control/gui/main_window.py +++ b/je_auto_control/gui/main_window.py @@ -8,17 +8,34 @@ class AutoControlGUIUI(QMainWindow, QtStyleTools): + """ + AutoControl GUI Main Window + 自動控制 GUI 主視窗 + - 提供應用程式主要介面 + - 套用 Qt Material 樣式 + """ def __init__(self): super().__init__() - self.id = language_wrapper.language_word_dict.get("application_name") + + # === Application ID 應用程式 ID === + # 用於 Windows 工作列顯示正確的應用程式名稱 + self.app_id = language_wrapper.language_word_dict.get("application_name") + if sys.platform in ["win32", "cygwin", "msys"]: from ctypes import windll - windll.shell32.SetCurrentProcessExplicitAppUserModelID(self.id) + windll.shell32.SetCurrentProcessExplicitAppUserModelID(self.app_id) + + # === Style 設定字型與樣式 === self.setStyleSheet( - f"font-size: 12pt;" - f"font-family: 'Lato';" + "font-size: 12pt;" + "font-family: 'Lato';" ) + + # 套用 Qt Material 樣式 (可替換不同主題檔案) self.apply_stylesheet(self, "dark_amber.xml") + + # === Central Widget 主控元件 === + # 將 AutoControlGUIWidget 作為主視窗中央元件 self.auto_control_gui_widget = AutoControlGUIWidget() - self.setCentralWidget(self.auto_control_gui_widget) + self.setCentralWidget(self.auto_control_gui_widget) \ No newline at end of file diff --git a/je_auto_control/linux_with_x11/core/utils/x11_linux_display.py b/je_auto_control/linux_with_x11/core/utils/x11_linux_display.py index 3014e1b..748848c 100644 --- a/je_auto_control/linux_with_x11/core/utils/x11_linux_display.py +++ b/je_auto_control/linux_with_x11/core/utils/x11_linux_display.py @@ -1,10 +1,10 @@ import sys -from je_auto_control.utils.exception.exception_tags import linux_import_error +from je_auto_control.utils.exception.exception_tags import linux_import_error_message from je_auto_control.utils.exception.exceptions import AutoControlException if sys.platform not in ["linux", "linux2"]: - raise AutoControlException(linux_import_error) + raise AutoControlException(linux_import_error_message) import os from Xlib.display import Display diff --git a/je_auto_control/linux_with_x11/core/utils/x11_linux_vk.py b/je_auto_control/linux_with_x11/core/utils/x11_linux_vk.py index dc17983..05f44a7 100644 --- a/je_auto_control/linux_with_x11/core/utils/x11_linux_vk.py +++ b/je_auto_control/linux_with_x11/core/utils/x11_linux_vk.py @@ -1,10 +1,10 @@ import sys -from je_auto_control.utils.exception.exception_tags import linux_import_error +from je_auto_control.utils.exception.exception_tags import linux_import_error_message from je_auto_control.utils.exception.exceptions import AutoControlException if sys.platform not in ["linux", "linux2"]: - raise AutoControlException(linux_import_error) + raise AutoControlException(linux_import_error_message) from Xlib import XK from je_auto_control.linux_with_x11.core.utils.x11_linux_display import display diff --git a/je_auto_control/linux_with_x11/keyboard/x11_linux_keyboard_control.py b/je_auto_control/linux_with_x11/keyboard/x11_linux_keyboard_control.py index fb2d4ae..fbd036a 100644 --- a/je_auto_control/linux_with_x11/keyboard/x11_linux_keyboard_control.py +++ b/je_auto_control/linux_with_x11/keyboard/x11_linux_keyboard_control.py @@ -1,11 +1,13 @@ import sys import time -from je_auto_control.utils.exception.exception_tags import linux_import_error +from je_auto_control.utils.exception.exception_tags import linux_import_error_message from je_auto_control.utils.exception.exceptions import AutoControlException +# === 平台檢查 Platform Check === +# 僅允許在 Linux 環境執行,否則拋出例外 if sys.platform not in ["linux", "linux2"]: - raise AutoControlException(linux_import_error) + raise AutoControlException(linux_import_error_message) from je_auto_control.linux_with_x11.core.utils.x11_linux_display import display from Xlib.ext.xtest import fake_input @@ -14,23 +16,51 @@ def press_key(keycode: int) -> None: """ - :param keycode which keycode we want to press + Press a key using X11 fake_input + 使用 X11 fake_input 模擬按下鍵盤按鍵 + + :param keycode: (int) The keycode to press 要按下的鍵盤代碼 """ - time.sleep(0.01) + if not isinstance(keycode, int): + raise ValueError("Keycode must be an integer 鍵盤代碼必須是整數") + + time.sleep(0.01) # Small delay to ensure event stability 確保事件穩定的小延遲 fake_input(display, X.KeyPress, keycode) display.sync() def release_key(keycode: int) -> None: """ - :param keycode which keycode we want to release + Release a key using X11 fake_input + 使用 X11 fake_input 模擬釋放鍵盤按鍵 + + :param keycode: (int) The keycode to release 要釋放的鍵盤代碼 """ + if not isinstance(keycode, int): + raise ValueError("Keycode must be an integer 鍵盤代碼必須是整數") + time.sleep(0.01) fake_input(display, X.KeyRelease, keycode) display.sync() -def send_key_event_to_window(window_id, keycode: int): - window = display.create_resource_object('window', window_id) + +def send_key_event_to_window(window_id: int, keycode: int) -> None: + """ + Send key press + release event directly to a specific window + 將鍵盤按下與釋放事件直接送到指定視窗 + + :param window_id: (int) Target window ID 目標視窗 ID + :param keycode: (int) Keycode to send 要送出的鍵盤代碼 + """ + if not isinstance(window_id, int): + raise ValueError("Window ID must be an integer 視窗 ID 必須是整數") + if not isinstance(keycode, int): + raise ValueError("Keycode must be an integer 鍵盤代碼必須是整數") + + # 建立目標視窗物件 Create target window object + window = display.create_resource_object("window", window_id) + + # 建立 KeyPress 事件 Create KeyPress event event = protocol.event.KeyPress( time=X.CurrentTime, root=display.screen().root, @@ -41,7 +71,13 @@ def send_key_event_to_window(window_id, keycode: int): state=0, detail=keycode ) + + # 傳送 KeyPress 事件 Send KeyPress event window.send_event(event, propagate=True) + + # 修改為 KeyRelease 並傳送 Modify to KeyRelease and send event.type = X.KeyRelease window.send_event(event, propagate=True) + + # 刷新事件 Flush events display.flush() \ No newline at end of file diff --git a/je_auto_control/linux_with_x11/listener/x11_linux_listener.py b/je_auto_control/linux_with_x11/listener/x11_linux_listener.py index aa77c94..013c9ec 100644 --- a/je_auto_control/linux_with_x11/listener/x11_linux_listener.py +++ b/je_auto_control/linux_with_x11/listener/x11_linux_listener.py @@ -1,31 +1,35 @@ import sys from queue import Queue +from threading import Thread -from je_auto_control.utils.exception.exception_tags import linux_import_error -from je_auto_control.utils.exception.exception_tags import listener_error +from je_auto_control.utils.exception.exception_tags import linux_import_error_message, listener_error_message from je_auto_control.utils.exception.exceptions import AutoControlException +# === 平台檢查 Platform Check === +# 僅允許在 Linux 環境執行,否則拋出例外 if sys.platform not in ["linux", "linux2"]: - raise AutoControlException(linux_import_error) + raise AutoControlException(linux_import_error_message) from Xlib.display import Display from Xlib import X from Xlib.ext import record from Xlib.protocol import rq -from threading import Thread - -# get current display +# 取得目前的 X11 Display current_display = Display() class KeypressHandler(Thread): + """ + KeypressHandler + 鍵盤事件處理器 + - 負責解析 X11 事件 + - 可選擇記錄事件到 Queue + """ def __init__(self, default_daemon: bool = True): """ - default damon is true - still listener : continue listener keycode ? - event_key_code : now current key code default is 0 + :param default_daemon: 是否設為守護執行緒 (程式結束時自動停止) """ super().__init__() self.daemon = default_daemon @@ -33,61 +37,67 @@ def __init__(self, default_daemon: bool = True): self.record_flag = False self.record_queue = None self.event_keycode = 0 - self.event_position = 0, 0 + self.event_position = (0, 0) - # two times because press and release def check_is_press(self, keycode: int) -> bool: """ - :param keycode we want to check + 檢查指定 keycode 是否被按下 + Check if the given keycode was pressed """ if keycode == self.event_keycode: self.event_keycode = 0 return True - else: - return False + return False def run(self, reply) -> None: """ - :param reply listener return data - get data - while data not null and still listener - get event - + 處理 X11 回傳的事件資料 + Handle X11 reply data and parse events """ try: data = reply.data while len(data) and self.still_listener: - event, data = rq.EventField(None).parse_binary_value(data, current_display.display, None, None) + event, data = rq.EventField(None).parse_binary_value( + data, current_display.display, None, None + ) if event.detail != 0: - if event.type is X.ButtonRelease or event.type is X.KeyRelease: + if event.type in (X.ButtonRelease, X.KeyRelease): self.event_keycode = event.detail - self.event_position = event.root_x, event.root_y - if self.record_flag is True: + self.event_position = (event.root_x, event.root_y) + + # 如果開啟記錄模式,將事件放入 Queue + if self.record_flag and self.record_queue is not None: temp = (event.type, event.detail, event.root_x, event.root_y) self.record_queue.put(temp) - except AutoControlException: - raise AutoControlException(listener_error) + except Exception: + raise AutoControlException(listener_error_message) - def record(self, record_queue) -> None: + def record(self, record_queue: Queue) -> None: """ - :param record_queue the queue test_record action + 開始記錄事件 + Start recording events into the given queue """ self.record_flag = True self.record_queue = record_queue def stop_record(self) -> Queue: + """ + 停止記錄事件並回傳 Queue + Stop recording and return the recorded queue + """ self.record_flag = False return self.record_queue class XWindowsKeypressListener(Thread): + """ + XWindowsKeypressListener + X11 鍵盤/滑鼠事件監聽器 + - 建立 Record Context + - 啟動 KeypressHandler + """ def __init__(self, default_daemon=True): - """ - :param default_daemon default kill when program down - create handler - set root - """ super().__init__() self.daemon = default_daemon self.still_listener = True @@ -97,22 +107,20 @@ def __init__(self, default_daemon=True): def check_is_press(self, keycode: int) -> bool: """ - :param keycode check this keycode is press? + 檢查指定 keycode 是否被按下 + Check if the given keycode was pressed """ return self.handler.check_is_press(keycode) def run(self) -> None: """ - while still listener - get context - set handler - set test_record - get event + 啟動監聽迴圈 + Start listening loop for X11 events """ if self.still_listener: try: - # Monitor keypress and button press if self.context is None: + # 建立 Record Context self.context = current_display.record_create_context( 0, [record.AllClients], @@ -126,44 +134,59 @@ def run(self) -> None: 'errors': (0, 0), 'client_started': False, 'client_died': False, - }]) + }] + ) + # 啟用事件監聽 current_display.record_enable_context(self.context, self.handler.run) current_display.record_free_context(self.context) - # keep running this to get event + + # 持續等待事件 self.root.display.next_event() - except AutoControlException: - raise AutoControlException(listener_error) + except Exception: + raise AutoControlException(listener_error_message) finally: self.handler.still_listener = False self.still_listener = False - def record(self, record_queue) -> None: + def record(self, record_queue: Queue) -> None: + """ + 開始記錄事件 + Start recording events + """ self.handler.record(record_queue) def stop_record(self) -> Queue: + """ + 停止記錄事件 + Stop recording events + """ return self.handler.stop_record() +# === 全域監聽器 Global Listener === xwindows_listener = XWindowsKeypressListener() xwindows_listener.start() def check_key_is_press(keycode: int) -> bool: """ - :param keycode check this keycode is press? + 檢查指定 keycode 是否被按下 + Check if the given keycode was pressed """ return xwindows_listener.check_is_press(keycode) -def x11_linux_record(record_queue) -> None: +def x11_linux_record(record_queue: Queue) -> None: """ - :param record_queue the queue test_record action + 開始記錄事件 + Start recording events into the given queue """ xwindows_listener.record(record_queue) def x11_linux_stop_record() -> Queue: """ - stop test_record action + 停止記錄事件並回傳 Queue + Stop recording and return the recorded queue """ - return xwindows_listener.stop_record() + return xwindows_listener.stop_record() \ No newline at end of file diff --git a/je_auto_control/linux_with_x11/mouse/x11_linux_mouse_control.py b/je_auto_control/linux_with_x11/mouse/x11_linux_mouse_control.py index 0cd9f6e..1d091ef 100644 --- a/je_auto_control/linux_with_x11/mouse/x11_linux_mouse_control.py +++ b/je_auto_control/linux_with_x11/mouse/x11_linux_mouse_control.py @@ -2,17 +2,19 @@ import time from typing import Tuple -from je_auto_control.utils.exception.exception_tags import linux_import_error +from je_auto_control.utils.exception.exception_tags import linux_import_error_message from je_auto_control.utils.exception.exceptions import AutoControlException +# === 平台檢查 Platform Check === +# 僅允許在 Linux 環境執行,否則拋出例外 if sys.platform not in ["linux", "linux2"]: - raise AutoControlException(linux_import_error) + raise AutoControlException(linux_import_error_message) from Xlib import X, protocol from Xlib.ext.xtest import fake_input - from je_auto_control.linux_with_x11.core.utils.x11_linux_display import display +# === 滑鼠按鍵與滾動方向定義 Mouse button & scroll direction constants === x11_linux_mouse_left = 1 x11_linux_mouse_middle = 2 x11_linux_mouse_right = 3 @@ -24,7 +26,8 @@ def position() -> Tuple[int, int]: """ - get mouse current position + Get current mouse position + 取得目前滑鼠座標位置 """ coord = display.screen().root.query_pointer()._data return coord["root_x"], coord["root_y"] @@ -32,8 +35,11 @@ def position() -> Tuple[int, int]: def set_position(x: int, y: int) -> None: """ - :param x we want to set mouse x position - :param y we want to set mouse y position + Move mouse to specific position + 移動滑鼠到指定座標 + + :param x: target x position 目標 X 座標 + :param y: target y position 目標 Y 座標 """ time.sleep(0.01) fake_input(display, X.MotionNotify, x=x, y=y) @@ -42,7 +48,10 @@ def set_position(x: int, y: int) -> None: def press_mouse(mouse_keycode: int) -> None: """ - :param mouse_keycode mouse keycode we want to press + Press mouse button + 模擬按下滑鼠按鍵 + + :param mouse_keycode: mouse button code 滑鼠按鍵代碼 """ time.sleep(0.01) fake_input(display, X.ButtonPress, mouse_keycode) @@ -51,41 +60,59 @@ def press_mouse(mouse_keycode: int) -> None: def release_mouse(mouse_keycode: int) -> None: """ - :param mouse_keycode which mouse keycode we want to release + Release mouse button + 模擬釋放滑鼠按鍵 + + :param mouse_keycode: mouse button code 滑鼠按鍵代碼 """ time.sleep(0.01) fake_input(display, X.ButtonRelease, mouse_keycode) display.sync() -def click_mouse(mouse_keycode: int, x=None, y=None) -> None: +def click_mouse(mouse_keycode: int, x: int = None, y: int = None) -> None: """ - :param mouse_keycode which mouse keycode we want to click - :param x set mouse x position - :param y set mouse y position + Perform mouse click (press + release) + 模擬滑鼠點擊(按下 + 釋放) + + :param mouse_keycode: mouse button code 滑鼠按鍵代碼 + :param x: optional x position 選擇性 X 座標 + :param y: optional y position 選擇性 Y 座標 """ - if x and y is not None: + if x is not None and y is not None: set_position(x, y) press_mouse(mouse_keycode) release_mouse(mouse_keycode) def scroll(scroll_value: int, scroll_direction: int) -> None: - """" - :param scroll_value scroll unit - :param scroll_direction what direction you want to scroll - scroll_direction = 4 : direction up - scroll_direction = 5 : direction down - scroll_direction = 6 : direction left - scroll_direction = 7 : direction right """ - total = 0 - for i in range(scroll_value): + Perform mouse scroll + 模擬滑鼠滾動 + + :param scroll_value: number of scroll units 滾動次數 + :param scroll_direction: scroll direction 滾動方向 + 4 = up 上 + 5 = down 下 + 6 = left 左 + 7 = right 右 + """ + for _ in range(scroll_value): click_mouse(scroll_direction) - total = total + i -def send_mouse_event_to_window(window_id, mouse_keycode: int, x: int = None, y: int = None): - window = display.create_resource_object('window', window_id) + +def send_mouse_event_to_window(window_id: int, mouse_keycode: int, + x: int = None, y: int = None) -> None: + """ + Send mouse event directly to a specific window + 將滑鼠事件直接送到指定視窗 + + :param window_id: target window ID 目標視窗 ID + :param mouse_keycode: mouse button code 滑鼠按鍵代碼 + :param x: optional x position 選擇性 X 座標 + :param y: optional y position 選擇性 Y 座標 + """ + window = display.create_resource_object("window", window_id) for ev_type in (X.ButtonPress, X.ButtonRelease): ev = protocol.event.ButtonPress( time=X.CurrentTime, @@ -99,7 +126,4 @@ def send_mouse_event_to_window(window_id, mouse_keycode: int, x: int = None, y: ) ev.type = ev_type window.send_event(ev, propagate=True) - display.flush() - - - + display.flush() \ No newline at end of file diff --git a/je_auto_control/linux_with_x11/record/x11_linux_record.py b/je_auto_control/linux_with_x11/record/x11_linux_record.py index 9e4fba0..90e8e1f 100644 --- a/je_auto_control/linux_with_x11/record/x11_linux_record.py +++ b/je_auto_control/linux_with_x11/record/x11_linux_record.py @@ -1,50 +1,73 @@ import sys from typing import Any +from queue import Queue -from je_auto_control.utils.exception.exception_tags import linux_import_error +from je_auto_control.utils.exception.exception_tags import linux_import_error_message from je_auto_control.utils.exception.exceptions import AutoControlException +# === 平台檢查 Platform Check === +# 僅允許在 Linux 環境執行,否則拋出例外 if sys.platform not in ["linux", "linux2"]: - raise AutoControlException(linux_import_error) - -from je_auto_control.linux_with_x11.listener.x11_linux_listener import x11_linux_record -from je_auto_control.linux_with_x11.listener.x11_linux_listener import x11_linux_stop_record + raise AutoControlException(linux_import_error_message) -from queue import Queue +from je_auto_control.linux_with_x11.listener.x11_linux_listener import ( + x11_linux_record, + x11_linux_stop_record, +) -type_dict = {5: "mouse", 3: "AC_type_keyboard"} -detail_dict = {1: "AC_mouse_left", 2: "AC_mouse_middle", 3: "AC_mouse_right"} +# === 事件類型與細節對照表 Event type & detail mapping === +type_dict = { + 5: "mouse", # 事件類型 5 -> 滑鼠事件 + 3: "AC_type_keyboard", # 事件類型 3 -> 鍵盤事件 +} +detail_dict = { + 1: "AC_mouse_left", # 滑鼠左鍵 + 2: "AC_mouse_middle", # 滑鼠中鍵 + 3: "AC_mouse_right", # 滑鼠右鍵 +} -class X11LinuxRecorder(object): +class X11LinuxRecorder: """ - test_record controller + X11 Linux Recorder + X11 錄製控制器 + - 負責建立 Queue 並啟動事件錄製 + - 將原始事件轉換成可讀的動作序列 """ def __init__(self): - self.record_queue = None - self.result_queue = None + self.record_queue: Queue | None = None + self.result_queue: Queue | None = None def record(self) -> None: """ - create a new queue and start test_record + Start recording events + 開始錄製事件,建立新的 Queue """ self.record_queue = Queue() x11_linux_record(self.record_queue) def stop_record(self) -> Queue[Any]: """ - stop test_record - make a format action queue + Stop recording and format results + 停止錄製,並將結果轉換成動作序列 Queue """ self.result_queue = x11_linux_stop_record() action_queue = Queue() - for details in self.result_queue.queue: - if details[0] == 5: - action_queue.put((detail_dict.get(details[1]), details[2], details[3])) - elif details[0] == 3: - action_queue.put((type_dict.get(details[0]), details[1])) + + # 將原始事件轉換成可讀格式 + for details in list(self.result_queue.queue): + if details[0] == 5: # 滑鼠事件 + action_queue.put( + (detail_dict.get(details[1]), details[2], details[3]) + ) + elif details[0] == 3: # 鍵盤事件 + action_queue.put( + (type_dict.get(details[0]), details[1]) + ) + return action_queue -x11_linux_recoder = X11LinuxRecorder() +# === 全域 Recorder 實例 Global Recorder Instance === +x11_linux_recorder = X11LinuxRecorder() \ No newline at end of file diff --git a/je_auto_control/linux_with_x11/screen/x11_linux_screen.py b/je_auto_control/linux_with_x11/screen/x11_linux_screen.py index 12f0b80..0d4560e 100644 --- a/je_auto_control/linux_with_x11/screen/x11_linux_screen.py +++ b/je_auto_control/linux_with_x11/screen/x11_linux_screen.py @@ -1,25 +1,43 @@ import sys from typing import Tuple -from je_auto_control.utils.exception.exception_tags import linux_import_error +from je_auto_control.utils.exception.exception_tags import linux_import_error_message from je_auto_control.utils.exception.exceptions import AutoControlException +# === 平台檢查 Platform Check === +# 僅允許在 Linux 環境執行,否則拋出例外 if sys.platform not in ["linux", "linux2"]: - raise AutoControlException(linux_import_error) + raise AutoControlException(linux_import_error_message) from Xlib import X from je_auto_control.linux_with_x11.core.utils.x11_linux_display import display + def size() -> Tuple[int, int]: """ - get screen size + Get screen size + 取得螢幕大小 (寬度, 高度) + + :return: (width, height) 螢幕寬度與高度 """ return display.screen().width_in_pixels, display.screen().height_in_pixels def get_pixel_rgb(x: int, y: int) -> Tuple[int, int, int]: - root = display.Display().screen().root - root.get_image(x, y, 1, 1, X.ZPixmap, 0xffffffff) + """ + Get RGB value of pixel at given coordinates + 取得指定座標的像素 RGB 值 + + :param x: X coordinate X 座標 + :param y: Y coordinate Y 座標 + :return: (R, G, B) 三原色值 + """ + # 建立 root window 物件 Create root window object + root = display.screen().root + + # 取得影像資料 Get image data raw = root.get_image(x, y, 1, 1, X.ZPixmap, 0xffffffff) - pixel = tuple(raw.data)[:3] # (R, G, B) - return pixel + + # raw.data 是 bytes,需要轉換成 RGB + pixel = tuple(raw.data[:3]) # (R, G, B) + return pixel \ No newline at end of file diff --git a/je_auto_control/osx/core/utils/osx_vk.py b/je_auto_control/osx/core/utils/osx_vk.py index 9aecead..e6c7d4a 100644 --- a/je_auto_control/osx/core/utils/osx_vk.py +++ b/je_auto_control/osx/core/utils/osx_vk.py @@ -1,10 +1,10 @@ import sys -from je_auto_control.utils.exception.exception_tags import osx_import_error +from je_auto_control.utils.exception.exception_tags import osx_import_error_message from je_auto_control.utils.exception.exceptions import AutoControlException if sys.platform not in ["darwin"]: - raise AutoControlException(osx_import_error) + raise AutoControlException(osx_import_error_message) # osx keyboard virtual keycode diff --git a/je_auto_control/osx/keyboard/osx_keyboard.py b/je_auto_control/osx/keyboard/osx_keyboard.py index 61a3243..281d5d0 100644 --- a/je_auto_control/osx/keyboard/osx_keyboard.py +++ b/je_auto_control/osx/keyboard/osx_keyboard.py @@ -1,16 +1,19 @@ import sys -from je_auto_control.utils.exception.exception_tags import osx_import_error +from je_auto_control.utils.exception.exception_tags import osx_import_error_message from je_auto_control.utils.exception.exceptions import AutoControlException +# === 平台檢查 Platform Check === +# 僅允許在 macOS (Darwin) 環境執行,否則拋出例外 if sys.platform not in ["darwin"]: - raise AutoControlException(osx_import_error) + raise AutoControlException(osx_import_error_message) import AppKit import Quartz from je_auto_control.osx.core.utils.osx_vk import osx_key_shift +# === 特殊鍵對照表 Special key mapping === special_key_table = { "key_sound_up": 0, "key_sound_down": 1, @@ -41,13 +44,15 @@ def normal_key(keycode: int, is_shift: bool, is_down: bool) -> None: """ - :param keycode which keycode we want to press or release - :param is_shift use shift key ? - :param is_down is_down true = press; false = release - create event - post event + Simulate normal key press/release + 模擬普通鍵盤按下/釋放 + + :param keycode: 要模擬的鍵盤代碼 + :param is_shift: 是否同時按下 Shift + :param is_down: True = 按下, False = 釋放 """ try: + # 如果需要 Shift,先送出 Shift 事件 if is_shift: event = Quartz.CGEventCreateKeyboardEvent( None, @@ -55,24 +60,32 @@ def normal_key(keycode: int, is_shift: bool, is_down: bool) -> None: is_down ) Quartz.CGEventPost(Quartz.kCGHIDEventTap, event) + + # 送出目標鍵盤事件 event = Quartz.CGEventCreateKeyboardEvent( None, keycode, is_down ) Quartz.CGEventPost(Quartz.kCGHIDEventTap, event) + except ValueError as error: print(repr(error), file=sys.stderr) -def special_key(keycode: int, is_shift: bool) -> None: +def special_key(keycode: str, is_shift: bool) -> None: """ - :param keycode which keycode we want to press or release - :param is_shift use shift key ? - create event - post event + Simulate special key press/release + 模擬特殊鍵盤按下/釋放 (例如音量、亮度、播放鍵) + + :param keycode: 特殊鍵名稱 (必須存在於 special_key_table) + :param is_shift: 是否同時按下 Shift """ - keycode = special_key_table[keycode] + if keycode not in special_key_table: + raise ValueError(f"Unknown special key: {keycode}") + + mapped_code = special_key_table[keycode] + event = AppKit.NSEvent.otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2( Quartz.NSSystemDefined, (0, 0), @@ -81,16 +94,19 @@ def special_key(keycode: int, is_shift: bool) -> None: 0, 0, 8, - (keycode << 16) | ((0xa if is_shift else 0xb) << 8), + (mapped_code << 16) | ((0xa if is_shift else 0xb) << 8), -1 ) Quartz.CGEventPost(0, event) -def press_key(keycode: int, is_shift: bool) -> None: +def press_key(keycode: int | str, is_shift: bool) -> None: """ - :param keycode which keycode we want to press - :param is_shift is shift press? + Press a key (normal or special) + 模擬按下鍵盤按鍵 (普通或特殊) + + :param keycode: 鍵盤代碼或特殊鍵名稱 + :param is_shift: 是否同時按下 Shift """ if keycode in special_key_table: special_key(keycode, is_shift) @@ -98,12 +114,15 @@ def press_key(keycode: int, is_shift: bool) -> None: normal_key(keycode, is_shift, True) -def release_key(keycode: int, is_shift: bool) -> None: +def release_key(keycode: int | str, is_shift: bool) -> None: """ - :param keycode which keycode we want to release - :param is_shift is shift press? + Release a key (normal or special) + 模擬釋放鍵盤按鍵 (普通或特殊) + + :param keycode: 鍵盤代碼或特殊鍵名稱 + :param is_shift: 是否同時按下 Shift """ if keycode in special_key_table: special_key(keycode, is_shift) else: - normal_key(keycode, is_shift, False) + normal_key(keycode, is_shift, False) \ No newline at end of file diff --git a/je_auto_control/osx/keyboard/osx_keyboard_check.py b/je_auto_control/osx/keyboard/osx_keyboard_check.py index 7f39079..1134523 100644 --- a/je_auto_control/osx/keyboard/osx_keyboard_check.py +++ b/je_auto_control/osx/keyboard/osx_keyboard_check.py @@ -1,16 +1,24 @@ import sys -from je_auto_control.utils.exception.exception_tags import osx_import_error +from je_auto_control.utils.exception.exception_tags import osx_import_error_message from je_auto_control.utils.exception.exceptions import AutoControlException +# === 平台檢查 Platform Check === +# 僅允許在 macOS (Darwin) 環境執行,否則拋出例外 if sys.platform not in ["darwin"]: - raise AutoControlException(osx_import_error) + raise AutoControlException(osx_import_error_message) import Quartz def check_key_is_press(keycode: int) -> bool: """ - :param keycode which keycode we want to check + Check if a specific key is currently pressed + 檢查指定的鍵是否正在被按下 + + :param keycode: (int) The keycode to check 要檢查的鍵盤代碼 + :return: True if pressed, False otherwise 若按下則回傳 True,否則 False """ - return Quartz.CGEventSourceKeyState(0, keycode) + # Quartz.CGEventSourceKeyState(source, keycode) + # source = 0 表示使用預設事件來源 + return Quartz.CGEventSourceKeyState(0, keycode) \ No newline at end of file diff --git a/je_auto_control/osx/listener/osx_listener.py b/je_auto_control/osx/listener/osx_listener.py index d89ffcf..37a9e2a 100644 --- a/je_auto_control/osx/listener/osx_listener.py +++ b/je_auto_control/osx/listener/osx_listener.py @@ -1,52 +1,91 @@ import sys +from queue import Queue -from je_auto_control.utils.exception.exception_tags import osx_import_error +from je_auto_control.utils.exception.exception_tags import osx_import_error_message from je_auto_control.utils.exception.exceptions import AutoControlException +# === 平台檢查 Platform Check === +# 僅允許在 macOS (Darwin) 環境執行,否則拋出例外 if sys.platform not in ["darwin"]: - raise AutoControlException(osx_import_error) + raise AutoControlException(osx_import_error_message) from Cocoa import * from Foundation import * from PyObjCTools import AppHelper -from queue import Queue - +# === 全域事件記錄 Queue Global event record queue === record_queue = Queue() +# 建立 NSApplication 實例 Create NSApplication instance app = NSApplication.sharedApplication() class AppDelegate(NSObject): + """ + AppDelegate + 應用程式委派類別 + - 負責在應用程式啟動後註冊全域事件監聽器 + """ + def applicationDidFinishLaunching_(self, aNotification): - NSEvent.addGlobalMonitorForEventsMatchingMask_handler_(NSEventMaskKeyDown, keyboard_handler) - NSEvent.addGlobalMonitorForEventsMatchingMask_handler_(NSEventMaskLeftMouseDown, mouse_left_handler) - NSEvent.addGlobalMonitorForEventsMatchingMask_handler_(NSEventMaskRightMouseDown, mouse_right_handler) + """ + 註冊全域事件監聽器 + Register global event monitors + """ + NSEvent.addGlobalMonitorForEventsMatchingMask_handler_( + NSEventMaskKeyDown, keyboard_handler + ) + NSEvent.addGlobalMonitorForEventsMatchingMask_handler_( + NSEventMaskLeftMouseDown, mouse_left_handler + ) + NSEvent.addGlobalMonitorForEventsMatchingMask_handler_( + NSEventMaskRightMouseDown, mouse_right_handler + ) def mouse_left_handler(event) -> None: + """ + 滑鼠左鍵事件處理器 + Mouse left button handler + """ loc = NSEvent.mouseLocation() record_queue.put(("AC_mouse_left", loc.x, loc.y)) def mouse_right_handler(event) -> None: + """ + 滑鼠右鍵事件處理器 + Mouse right button handler + """ loc = NSEvent.mouseLocation() record_queue.put(("AC_mouse_right", loc.x, loc.y)) def keyboard_handler(event) -> None: - if int(event.keyCode()) == 98: - pass - else: - record_queue.put(("AC_type_keyboard", int(hex(event.keyCode()), 16))) - print(event) + """ + 鍵盤事件處理器 + Keyboard event handler + """ + keycode = int(event.keyCode()) + if keycode == 98: # 特殊情況:忽略 keycode 98 + return + record_queue.put(("AC_type_keyboard", keycode)) + print(event) def osx_record() -> None: + """ + 開始錄製事件 + Start recording events + """ delegate = AppDelegate.alloc().init() app.setDelegate_(delegate) AppHelper.runEventLoop() def osx_stop_record() -> Queue: - return record_queue + """ + 停止錄製並回傳事件 Queue + Stop recording and return event queue + """ + return record_queue \ No newline at end of file diff --git a/je_auto_control/osx/mouse/osx_mouse.py b/je_auto_control/osx/mouse/osx_mouse.py index e1fdce9..a7aaf28 100644 --- a/je_auto_control/osx/mouse/osx_mouse.py +++ b/je_auto_control/osx/mouse/osx_mouse.py @@ -1,34 +1,44 @@ import sys +import time from typing import Tuple -from je_auto_control.utils.exception.exception_tags import osx_import_error +from je_auto_control.utils.exception.exception_tags import osx_import_error_message from je_auto_control.utils.exception.exceptions import AutoControlException +# === 平台檢查 Platform Check === +# 僅允許在 macOS (Darwin) 環境執行,否則拋出例外 if sys.platform not in ["darwin"]: - raise AutoControlException(osx_import_error) - -import time + raise AutoControlException(osx_import_error_message) import Quartz -from je_auto_control.osx.core.utils.osx_vk import osx_mouse_left -from je_auto_control.osx.core.utils.osx_vk import osx_mouse_middle -from je_auto_control.osx.core.utils.osx_vk import osx_mouse_right +from je_auto_control.osx.core.utils.osx_vk import ( + osx_mouse_left, + osx_mouse_middle, + osx_mouse_right, +) def position() -> Tuple[int, int]: """ - get mouse current position + Get current mouse position + 取得目前滑鼠座標位置 + + :return: (x, y) 滑鼠座標 """ - return Quartz.NSEvent.mouseLocation().x, Quartz.NSEvent.mouseLocation().y + loc = Quartz.NSEvent.mouseLocation() + return int(loc.x), int(loc.y) -def mouse_event(event, x: int, y: int, mouse_button: int) -> None: +def mouse_event(event: int, x: int, y: int, mouse_button: int) -> None: """ - :param event which event we want to use - :param x event x - :param y event y - :param mouse_button which mouse button will use event + Create and post a mouse event + 建立並送出滑鼠事件 + + :param event: Quartz event type 事件類型 (例如 kCGEventMouseMoved) + :param x: X coordinate X 座標 + :param y: Y coordinate Y 座標 + :param mouse_button: Mouse button code 滑鼠按鍵代碼 """ curr_event = Quartz.CGEventCreateMouseEvent(None, event, (x, y), mouse_button) Quartz.CGEventPost(Quartz.kCGHIDEventTap, curr_event) @@ -36,72 +46,76 @@ def mouse_event(event, x: int, y: int, mouse_button: int) -> None: def set_position(x: int, y: int) -> None: """ - :param x we want to set mouse x position - :param y we want to set mouse y position + Move mouse to specific position + 移動滑鼠到指定座標 + + :param x: target x position 目標 X 座標 + :param y: target y position 目標 Y 座標 """ mouse_event(Quartz.kCGEventMouseMoved, x, y, 0) def press_mouse(x: int, y: int, mouse_button: int) -> None: """ - :param x event x - :param y event y - :param mouse_button which mouse button press + Press mouse button + 模擬按下滑鼠按鍵 + + :param x: X coordinate X 座標 + :param y: Y coordinate Y 座標 + :param mouse_button: Mouse button code 滑鼠按鍵代碼 """ - if mouse_button is osx_mouse_left: + if mouse_button == osx_mouse_left: mouse_event(Quartz.kCGEventLeftMouseDown, x, y, Quartz.kCGMouseButtonLeft) - elif mouse_button is osx_mouse_middle: + elif mouse_button == osx_mouse_middle: mouse_event(Quartz.kCGEventOtherMouseDown, x, y, Quartz.kCGMouseButtonCenter) - elif mouse_button is osx_mouse_right: + elif mouse_button == osx_mouse_right: mouse_event(Quartz.kCGEventRightMouseDown, x, y, Quartz.kCGMouseButtonRight) def release_mouse(x: int, y: int, mouse_button: int) -> None: """ - :param x event x - :param y event y - :param mouse_button which mouse button release + Release mouse button + 模擬釋放滑鼠按鍵 + + :param x: X coordinate X 座標 + :param y: Y coordinate Y 座標 + :param mouse_button: Mouse button code 滑鼠按鍵代碼 """ - if mouse_button is osx_mouse_left: + if mouse_button == osx_mouse_left: mouse_event(Quartz.kCGEventLeftMouseUp, x, y, Quartz.kCGMouseButtonLeft) - elif mouse_button is osx_mouse_middle: + elif mouse_button == osx_mouse_middle: mouse_event(Quartz.kCGEventOtherMouseUp, x, y, Quartz.kCGMouseButtonCenter) - elif mouse_button is osx_mouse_right: + elif mouse_button == osx_mouse_right: mouse_event(Quartz.kCGEventRightMouseUp, x, y, Quartz.kCGMouseButtonRight) def click_mouse(x: int, y: int, mouse_button: int) -> None: """ - :param x event x - :param y event y - :param mouse_button which mouse button click + Perform mouse click (press + release) + 模擬滑鼠點擊(按下 + 釋放) + + :param x: X coordinate X 座標 + :param y: Y coordinate Y 座標 + :param mouse_button: Mouse button code 滑鼠按鍵代碼 """ - if mouse_button is osx_mouse_left: - press_mouse(x, y, mouse_button) - time.sleep(.001) - release_mouse(x, y, mouse_button) - elif mouse_button is osx_mouse_middle: - press_mouse(x, y, mouse_button) - time.sleep(.001) - release_mouse(x, y, mouse_button) - elif mouse_button is osx_mouse_right: - press_mouse(x, y, mouse_button) - time.sleep(.001) - release_mouse(x, y, mouse_button) + press_mouse(x, y, mouse_button) + time.sleep(0.001) # 小延遲確保事件正確送出 + release_mouse(x, y, mouse_button) def scroll(scroll_value: int) -> None: """ - :param scroll_value scroll count + Perform mouse scroll + 模擬滑鼠滾動 + + :param scroll_value: scroll count 滾動次數 (正數=向上, 負數=向下) """ scroll_value = int(scroll_value) - total = 0 - for do_scroll in range(abs(scroll_value)): + for _ in range(abs(scroll_value)): scroll_event = Quartz.CGEventCreateScrollWheelEvent( None, - 0, - 1, - 1 if scroll_value >= 0 else -1 + Quartz.kCGScrollEventUnitLine, # 單位:行 + 1, # 軸數 (1 = 垂直) + 1 if scroll_value >= 0 else -1 # 滾動方向 ) - Quartz.CGEventPost(Quartz.kCGHIDEventTap, scroll_event) - total = total + do_scroll + Quartz.CGEventPost(Quartz.kCGHIDEventTap, scroll_event) \ No newline at end of file diff --git a/je_auto_control/osx/pid/pid_control.py b/je_auto_control/osx/pid/pid_control.py index 1a07ccb..b9bee71 100644 --- a/je_auto_control/osx/pid/pid_control.py +++ b/je_auto_control/osx/pid/pid_control.py @@ -1,21 +1,44 @@ import objc -from Quartz import CGEventCreateKeyboardEvent, kCGEventKeyDown, kCGEventKeyUp -from ApplicationServices import ProcessSerialNumber, GetProcessForPID -from ctypes import cdll, c_void_p import subprocess +from ctypes import cdll, c_void_p + +from Quartz import CGEventCreateKeyboardEvent +from ApplicationServices import ProcessSerialNumber, GetProcessForPID +# 載入 Carbon 函式庫 Load Carbon framework carbon = cdll.LoadLibrary('/System/Library/Frameworks/Carbon.framework/Carbon') -def send_key_to_pid(pid, keycode): +def send_key_to_pid(pid: int, keycode: int) -> None: + """ + Send a key press + release event to a specific process by PID + 將鍵盤事件 (按下 + 釋放) 傳送到指定的 PID + + :param pid: Process ID 目標應用程式的 PID + :param keycode: Keycode 要傳送的鍵盤代碼 + """ psn = ProcessSerialNumber() GetProcessForPID(pid, objc.byref(psn)) + + # 建立按下事件 Create key down event event_down = CGEventCreateKeyboardEvent(None, keycode, True) + # 建立釋放事件 Create key up event event_up = CGEventCreateKeyboardEvent(None, keycode, False) + + # 傳送事件到指定的 ProcessSerialNumber carbon.CGEventPostToPSN(c_void_p(id(psn)), event_down) carbon.CGEventPostToPSN(c_void_p(id(psn)), event_up) -def get_pid_by_window_title(title: str): + +def get_pid_by_window_title(title: str) -> int | None: + """ + Get process PID by window title + 透過視窗標題取得應用程式的 PID + + :param title: Window title 視窗標題 + :return: PID (int) or None 若找到則回傳 PID,否則回傳 None + """ + # AppleScript 腳本,用來搜尋視窗標題 script = f''' set targetWindowName to "{title}" tell application "System Events" @@ -35,4 +58,4 @@ def get_pid_by_window_title(title: str): ).decode().strip() return int(pid_str) if pid_str else None except subprocess.CalledProcessError: - return None + return None \ No newline at end of file diff --git a/je_auto_control/osx/record/osx_record.py b/je_auto_control/osx/record/osx_record.py index 8f34737..f119508 100644 --- a/je_auto_control/osx/record/osx_record.py +++ b/je_auto_control/osx/record/osx_record.py @@ -1,33 +1,52 @@ import sys from queue import Queue -from je_auto_control.utils.exception.exception_tags import osx_import_error -from je_auto_control.utils.exception.exceptions import AutoControlException +from je_auto_control.utils.exception.exception_tags import osx_import_error_message +from je_auto_control.utils.exception.exceptions import AutoControlException, AutoControlJsonActionException +# === 平台檢查 Platform Check === +# 僅允許在 macOS (Darwin) 環境執行,否則拋出例外 if sys.platform not in ["darwin"]: - raise AutoControlException(osx_import_error) + raise AutoControlException(osx_import_error_message) -from je_auto_control.osx.listener.osx_listener import osx_record -from je_auto_control.osx.listener.osx_listener import osx_stop_record +from je_auto_control.osx.listener.osx_listener import osx_record, osx_stop_record -from je_auto_control.utils.exception.exceptions import AutoControlJsonActionException - -class OSXRecorder(object): +class OSXRecorder: + """ + OSXRecorder + macOS 事件錄製控制器 + - 提供開始與停止錄製的介面 + - 將錄製結果存入 Queue + """ def __init__(self): - self.record_flag = False + self.record_flag: bool = False def record(self) -> None: + """ + Start recording events + 開始錄製事件 + """ self.record_flag = True osx_record() def stop_record(self) -> Queue: + """ + Stop recording and return recorded events + 停止錄製並回傳事件隊列 + + :raises AutoControlJsonActionException: 若沒有錄製到任何事件 + :return: Queue of recorded events 錄製事件的隊列 + """ record_queue = osx_stop_record() self.record_flag = False + if record_queue is None: raise AutoControlJsonActionException - return osx_stop_record() + + return record_queue -osx_recorder = OSXRecorder() +# === 全域 Recorder 實例 Global Recorder Instance === +osx_recorder = OSXRecorder() \ No newline at end of file diff --git a/je_auto_control/osx/screen/osx_screen.py b/je_auto_control/osx/screen/osx_screen.py index d14338e..184e354 100644 --- a/je_auto_control/osx/screen/osx_screen.py +++ b/je_auto_control/osx/screen/osx_screen.py @@ -3,30 +3,47 @@ from ctypes import c_void_p, c_double, c_uint32 from typing import Tuple -from je_auto_control.utils.exception.exception_tags import osx_import_error +from je_auto_control.utils.exception.exception_tags import osx_import_error_message from je_auto_control.utils.exception.exceptions import AutoControlException +# === 平台檢查 Platform Check === +# 僅允許在 macOS (Darwin) 環境執行,否則拋出例外 if sys.platform not in ["darwin"]: - raise AutoControlException(osx_import_error) + raise AutoControlException(osx_import_error_message) import Quartz def size() -> Tuple[int, int]: """ - get screen size + Get screen size + 取得螢幕大小 (寬度, 高度) + + :return: (width, height) 螢幕寬度與高度 """ - return Quartz.CGDisplayPixelsWide((Quartz.CGMainDisplayID())), Quartz.CGDisplayPixelsHigh(Quartz.CGMainDisplayID()) + return ( + Quartz.CGDisplayPixelsWide(Quartz.CGMainDisplayID()), + Quartz.CGDisplayPixelsHigh(Quartz.CGMainDisplayID()) + ) + def get_pixel(x: int, y: int) -> Tuple[int, int, int, int]: - # Load CoreGraphics and CoreFoundation frameworks + """ + Get RGBA value of pixel at given coordinates + 取得指定座標的像素 RGBA 值 + + :param x: X coordinate X 座標 + :param y: Y coordinate Y 座標 + :return: (R, G, B, A) 四原色值 + """ + # 載入 CoreGraphics 與 CoreFoundation 函式庫 cg = ctypes.CDLL("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics") cf = ctypes.CDLL("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation") - # Define CGRect structure as 4 doubles: x, y, width, height + # 定義 CGRect 結構 (x, y, width, height) CGRect = ctypes.c_double * 4 - # Function signatures + # 設定函式簽名 Function signatures cg.CGWindowListCreateImage.argtypes = [CGRect, c_uint32, c_uint32, c_uint32] cg.CGWindowListCreateImage.restype = c_void_p @@ -45,34 +62,49 @@ def get_pixel(x: int, y: int) -> Tuple[int, int, int, int]: cf.CFRelease.argtypes = [c_void_p] cf.CFRelease.restype = None - # Constants + # 常數 Constants kCGWindowListOptionOnScreenOnly = 1 kCGNullWindowID = 0 kCGWindowImageDefault = 0 + + # 建立擷取範圍 Create capture rect rect = CGRect(x, y, 1.0, 1.0) - img = cg.CGWindowListCreateImage(rect, - kCGWindowListOptionOnScreenOnly, - kCGNullWindowID, - kCGWindowImageDefault) + + # 擷取螢幕影像 Capture screen image + img = cg.CGWindowListCreateImage( + rect, + kCGWindowListOptionOnScreenOnly, + kCGNullWindowID, + kCGWindowImageDefault + ) if not img: - raise RuntimeError("Unable to capture screen image. Please ensure Screen Recording permission is granted.") + raise RuntimeError( + "Unable to capture screen image. 請確認已授予螢幕錄製權限" + ) - # Get the data provider from the image + # 取得影像資料供應器 Get data provider provider = cg.CGImageGetDataProvider(img) - # Copy image data + + # 複製影像資料 Copy image data cfdata = cg.CGDataProviderCopyData(provider) - # Get length of data + + # 取得資料長度 Get data length length = cf.CFDataGetLength(cfdata) - # Get pointer to byte data + if length < 4: + cf.CFRelease(cfdata) + cf.CFRelease(provider) + cf.CFRelease(img) + raise RuntimeError("Invalid pixel data. 資料不足") + + # 取得 byte pointer Get byte pointer buf = cf.CFDataGetBytePtr(cfdata) - # Default pixel format is BGRA + # 預設像素格式為 BGRA Default pixel format is BGRA b, g, r, a = buf[0], buf[1], buf[2], buf[3] - # Release CoreFoundation objects to avoid memory leaks + # 釋放 CoreFoundation 物件 Release CF objects cf.CFRelease(cfdata) cf.CFRelease(provider) cf.CFRelease(img) - return r, g, b, a - + return r, g, b, a \ No newline at end of file diff --git a/je_auto_control/utils/callback/callback_function_executor.py b/je_auto_control/utils/callback/callback_function_executor.py index 839ed88..26ce439 100644 --- a/je_auto_control/utils/callback/callback_function_executor.py +++ b/je_auto_control/utils/callback/callback_function_executor.py @@ -3,64 +3,64 @@ # utils cv2_utils from je_auto_control.utils.cv2_utils.screenshot import pil_screenshot -from je_auto_control.utils.exception.exception_tags import get_bad_trigger_method, get_bad_trigger_function +from je_auto_control.utils.exception.exception_tags import get_bad_trigger_method_error_message, get_bad_trigger_function_error_message from je_auto_control.utils.exception.exceptions import CallbackExecutorException # executor -from je_auto_control.utils.executor.action_executor import execute_action -from je_auto_control.utils.executor.action_executor import execute_files +from je_auto_control.utils.executor.action_executor import execute_action, execute_files # file process from je_auto_control.utils.file_process.get_dir_file_list import get_dir_files_as_list # html report -from je_auto_control.utils.generate_report.generate_html_report import generate_html -from je_auto_control.utils.generate_report.generate_html_report import generate_html_report -from je_auto_control.utils.generate_report.generate_json_report import generate_json -from je_auto_control.utils.generate_report.generate_json_report import generate_json_report -# xml -from je_auto_control.utils.generate_report.generate_xml_report import generate_xml -from je_auto_control.utils.generate_report.generate_xml_report import generate_xml_report -# json -from je_auto_control.utils.json.json_file import read_action_json -from je_auto_control.utils.json.json_file import write_action_json -from je_auto_control.utils.package_manager.package_manager_class import \ - package_manager +from je_auto_control.utils.generate_report.generate_html_report import generate_html, generate_html_report +# json report +from je_auto_control.utils.generate_report.generate_json_report import generate_json, generate_json_report +# xml report +from je_auto_control.utils.generate_report.generate_xml_report import generate_xml, generate_xml_report +# json file +from je_auto_control.utils.json.json_file import read_action_json, write_action_json +# package manager +from je_auto_control.utils.package_manager.package_manager_class import package_manager +# project from je_auto_control.utils.project.create_project_structure import create_project_dir +# shell from je_auto_control.utils.shell_process.shell_exec import ShellManager # socket server from je_auto_control.utils.socket_server.auto_control_socket_server import start_autocontrol_socket_server +# process from je_auto_control.utils.start_exe.start_another_process import start_exe # test record from je_auto_control.utils.test_record.record_test_class import test_record_instance -# import cv2_utils -from je_auto_control.wrapper.auto_control_image import locate_all_image -from je_auto_control.wrapper.auto_control_image import locate_and_click -from je_auto_control.wrapper.auto_control_image import locate_image_center -from je_auto_control.wrapper.auto_control_keyboard import check_key_is_press, get_special_table, get_keyboard_keys_table -from je_auto_control.wrapper.auto_control_keyboard import hotkey -# import keyboard -from je_auto_control.wrapper.auto_control_keyboard import press_keyboard_key -from je_auto_control.wrapper.auto_control_keyboard import release_keyboard_key -from je_auto_control.wrapper.auto_control_keyboard import type_keyboard -from je_auto_control.wrapper.auto_control_keyboard import write -# import mouse -from je_auto_control.wrapper.auto_control_mouse import click_mouse, get_mouse_table -from je_auto_control.wrapper.auto_control_mouse import get_mouse_position -from je_auto_control.wrapper.auto_control_mouse import mouse_scroll -from je_auto_control.wrapper.auto_control_mouse import press_mouse -from je_auto_control.wrapper.auto_control_mouse import release_mouse -from je_auto_control.wrapper.auto_control_mouse import set_mouse_position -# test_record -from je_auto_control.wrapper.auto_control_record import record -from je_auto_control.wrapper.auto_control_record import stop_record -# import screen -from je_auto_control.wrapper.auto_control_screen import screen_size -from je_auto_control.wrapper.auto_control_screen import screenshot - - -class CallbackFunctionExecutor(object): +# image wrapper +from je_auto_control.wrapper.auto_control_image import locate_all_image, locate_and_click, locate_image_center +# keyboard wrapper +from je_auto_control.wrapper.auto_control_keyboard import ( + check_key_is_press, get_keyboard_keys_table, + hotkey, press_keyboard_key, release_keyboard_key, + type_keyboard, write +) +# mouse wrapper +from je_auto_control.wrapper.auto_control_mouse import ( + click_mouse, get_mouse_table, get_mouse_position, + mouse_scroll_error_message, press_mouse, release_mouse, set_mouse_position +) +# record wrapper +from je_auto_control.wrapper.auto_control_record import record, stop_record +# screen wrapper +from je_auto_control.wrapper.auto_control_screen import screen_size, screenshot + + +class CallbackFunctionExecutor: + """ + CallbackFunctionExecutor + 回呼函式執行器 + - 提供統一的事件字典 event_dict + - 可透過 trigger_function_name 執行對應功能 + - 執行後可呼叫 callback_function + """ def __init__(self): + # 事件字典,對應字串名稱到實際函式 self.event_dict: dict = { - # mouse + # mouse 滑鼠相關 "AC_mouse_left": click_mouse, "AC_mouse_right": click_mouse, "AC_mouse_middle": click_mouse, @@ -69,10 +69,10 @@ def __init__(self): "AC_get_mouse_position": get_mouse_position, "AC_press_mouse": press_mouse, "AC_release_mouse": release_mouse, - "AC_mouse_scroll": mouse_scroll, + "AC_mouse_scroll": mouse_scroll_error_message, "AC_set_mouse_position": set_mouse_position, - "AC_get_special_table": get_special_table, - # keyboard + + # keyboard 鍵盤相關 "AC_get_keyboard_keys_table": get_keyboard_keys_table, "AC_type_keyboard": type_keyboard, "AC_press_keyboard_key": press_keyboard_key, @@ -80,42 +80,58 @@ def __init__(self): "AC_check_key_is_press": check_key_is_press, "AC_write": write, "AC_hotkey": hotkey, - # cv2_utils + + # cv2_utils 影像辨識 "AC_locate_all_image": locate_all_image, "AC_locate_image_center": locate_image_center, "AC_locate_and_click": locate_and_click, - # screen + + # screen 螢幕相關 "AC_screen_size": screen_size, "AC_screenshot": screenshot, - # test record + + # test record 測試紀錄 "AC_set_record_enable": test_record_instance.set_record_enable, - # only generate + + # report 報告生成 "AC_generate_html": generate_html, "AC_generate_json": generate_json, "AC_generate_xml": generate_xml, - # generate report "AC_generate_html_report": generate_html_report, "AC_generate_json_report": generate_json_report, "AC_generate_xml_report": generate_xml_report, - # record + + # record 錄製 "AC_record": record, "AC_stop_record": stop_record, - # execute + + # executor 執行器 "AC_execute_action": execute_action, "AC_execute_files": execute_files, + + # project 專案 "create_template_dir": create_project_dir, + "AC_create_project": create_project_dir, + + # file process 檔案處理 "get_dir_files_as_list": get_dir_files_as_list, - "pil_screenshot": pil_screenshot, "read_action_json": read_action_json, "write_action_json": write_action_json, + + # screenshot 截圖 + "pil_screenshot": pil_screenshot, + + # socket server "start_autocontrol_socket_server": start_autocontrol_socket_server, + + # package manager "AC_add_package_to_executor": package_manager.add_package_to_executor, "AC_add_package_to_callback_executor": package_manager.add_package_to_callback_executor, - # project - "AC_create_project": create_project_dir, - # Shell + + # shell command "AC_shell_command": ShellManager().exec_shell, - # Another process + + # process "AC_execute_process": start_exe, } @@ -123,35 +139,45 @@ def callback_function( self, trigger_function_name: str, callback_function: Callable, - callback_function_param: [dict, None] = None, + callback_function_param: dict | None = None, callback_param_method: str = "kwargs", **kwargs ) -> Any: """ - :param trigger_function_name: what function we want to trigger only accept function in event_dict. - :param callback_function: what function we want to callback. - :param callback_function_param: callback function's param only accept dict. - :param callback_param_method: what type param will use on callback function only accept kwargs and args. - :param kwargs: trigger_function's param. - :return: trigger_function_name return value. + Execute a trigger function and then call a callback function + 執行指定的 trigger_function,並在完成後呼叫 callback_function + + :param trigger_function_name: 要觸發的函式名稱 (必須存在於 event_dict) + :param callback_function: 要呼叫的回呼函式 + :param callback_function_param: 回呼函式的參數 (dict 或 list) + :param callback_param_method: 回呼函式參數傳遞方式 ("kwargs" 或 "args") + :param kwargs: 傳給 trigger_function 的參數 + :return: trigger_function 的回傳值 """ try: - if trigger_function_name not in self.event_dict.keys(): - raise CallbackExecutorException(get_bad_trigger_function) - execute_return_value = self.event_dict.get(trigger_function_name)(**kwargs) + if trigger_function_name not in self.event_dict: + raise CallbackExecutorException(get_bad_trigger_function_error_message) + + # 執行 trigger function + execute_return_value = self.event_dict[trigger_function_name](**kwargs) + + # 呼叫 callback function if callback_function_param is not None: if callback_param_method not in ["kwargs", "args"]: - raise CallbackExecutorException(get_bad_trigger_method) + raise CallbackExecutorException(get_bad_trigger_method_error_message) if callback_param_method == "kwargs": callback_function(**callback_function_param) else: callback_function(*callback_function_param) else: callback_function() + return execute_return_value + except Exception as error: print(repr(error), file=stderr) +# === 全域 Callback Executor 實例 Global Instance === callback_executor = CallbackFunctionExecutor() -package_manager.callback_executor = callback_executor +package_manager.callback_executor = callback_executor \ No newline at end of file diff --git a/je_auto_control/utils/critical_exit/critcal_exit.py b/je_auto_control/utils/critical_exit/critcal_exit.py index 78e9882..86b52f2 100644 --- a/je_auto_control/utils/critical_exit/critcal_exit.py +++ b/je_auto_control/utils/critical_exit/critcal_exit.py @@ -8,43 +8,53 @@ class CriticalExit(Thread): """ - use to make program interrupt + CriticalExit + 緊急退出監聽器 + - 透過指定的鍵盤按鍵中斷主程式 + - 預設為 F7 鍵 """ def __init__(self, default_daemon: bool = True): """ - default interrupt is keyboard F7 key - :param default_daemon bool thread setDaemon + 初始化 CriticalExit + Initialize CriticalExit + + :param default_daemon: 是否設為守護執行緒 (程式結束時自動停止) """ super().__init__() self.daemon = default_daemon + # 預設退出鍵為 F7 Default exit key is F7 self._exit_check_key: int = keyboard_keys_table.get("f7") - def set_critical_key(self, keycode: [int, str] = None) -> None: + def set_critical_key(self, keycode: int | str = None) -> None: """ - set interrupt key - :param keycode interrupt key + 設定退出鍵 + Set critical exit key + + :param keycode: 可傳入 int (keycode) 或 str (鍵名) """ if isinstance(keycode, int): self._exit_check_key = keycode - else: + elif isinstance(keycode, str): self._exit_check_key = keyboard_keys_table.get(keycode) def run(self) -> None: """ - listener keycode _exit_check_key to interrupt + 執行監聽迴圈 + Run listener loop + - 持續監聽指定鍵盤按鍵 + - 當按下時觸發中斷主程式 """ try: while True: if keyboard_check.check_key_is_press(self._exit_check_key): - _thread.interrupt_main() + _thread.interrupt_main() # 中斷主程式 Interrupt main thread except Exception as error: print(repr(error), file=sys.stderr) def init_critical_exit(self) -> None: """ - should only use this to start critical exit - may this function will add more + 啟動緊急退出監聽器 + Initialize critical exit listener """ - critical_thread = self - critical_thread.start() + self.start() \ No newline at end of file diff --git a/je_auto_control/utils/cv2_utils/screen_record.py b/je_auto_control/utils/cv2_utils/screen_record.py index 3fcb34c..98592ff 100644 --- a/je_auto_control/utils/cv2_utils/screen_record.py +++ b/je_auto_control/utils/cv2_utils/screen_record.py @@ -1,40 +1,91 @@ import threading from typing import Dict, Tuple - -from cv2 import VideoWriter +import cv2 from je_auto_control.wrapper.auto_control_screen import screenshot -class ScreenRecorder(object): +class ScreenRecorder: + """ + ScreenRecorder + 螢幕錄影器管理類別 + - 可同時管理多個錄影執行緒 + """ def __init__(self): self.running_recorder: Dict[str, ScreenRecordThread] = {} - def start_new_recode(self, recoder_name: str, path_and_filename: str = "output.avi", codec: str = "XVID", - frame_per_sec: int = 30, resolution: Tuple[int, int] = (1920, 1080)): + def start_new_record( + self, + recorder_name: str, + path_and_filename: str = "output.avi", + codec: str = "XVID", + frame_per_sec: int = 30, + resolution: Tuple[int, int] = (1920, 1080) + ): + """ + Start a new screen recording + 開始新的螢幕錄影 + + :param recorder_name: 錄影器名稱 + :param path_and_filename: 輸出檔案名稱 + :param codec: 編碼器 (例如 "XVID") + :param frame_per_sec: 每秒幀數 + :param resolution: 解析度 (寬, 高) + """ record_thread = ScreenRecordThread(path_and_filename, codec, frame_per_sec, resolution) - old_record = self.running_recorder.get(recoder_name, None) + + # 如果已有同名錄影器,先停止舊的 + old_record = self.running_recorder.get(recorder_name) if old_record is not None: - old_record.record_flag = False + old_record.stop() + record_thread.daemon = True record_thread.start() - self.running_recorder.update({recoder_name: record_thread}) + self.running_recorder[recorder_name] = record_thread + + def stop_record(self, recorder_name: str): + """ + Stop a specific recorder + 停止指定的錄影器 + """ + if recorder_name in self.running_recorder: + self.running_recorder[recorder_name].stop() + del self.running_recorder[recorder_name] class ScreenRecordThread(threading.Thread): + """ + ScreenRecordThread + 螢幕錄影執行緒 + - 持續擷取螢幕畫面並寫入影片檔案 + """ def __init__(self, path_and_filename, codec, frame_per_sec, resolution: Tuple[int, int]): super().__init__() - self.fourcc = VideoWriter.fourcc(*codec) - self.video_writer = VideoWriter(path_and_filename, self.fourcc, frame_per_sec, resolution) + self.fourcc = cv2.VideoWriter.fourcc(*codec) + self.video_writer = cv2.VideoWriter(path_and_filename, self.fourcc, frame_per_sec, resolution) self.record_flag = False + self.resolution = resolution def run(self) -> None: self.record_flag = True while self.record_flag: - # Get raw pixels from the screen, save it to a Numpy array + # 擷取螢幕畫面 Capture screen frame image = screenshot() + + # 確保影像大小符合設定解析度 Ensure frame size matches resolution + if image.shape[1] != self.resolution[0] or image.shape[0] != self.resolution[1]: + image = cv2.resize(image, self.resolution) + self.video_writer.write(image) - else: - self.video_writer.release() + + # 錄影結束後釋放資源 Release resources after recording + self.video_writer.release() + + def stop(self) -> None: + """ + Stop recording + 停止錄影 + """ + self.record_flag = False \ No newline at end of file diff --git a/je_auto_control/utils/cv2_utils/screenshot.py b/je_auto_control/utils/cv2_utils/screenshot.py index 8625402..239e31b 100644 --- a/je_auto_control/utils/cv2_utils/screenshot.py +++ b/je_auto_control/utils/cv2_utils/screenshot.py @@ -1,16 +1,29 @@ from PIL import ImageGrab, Image +from typing import List, Optional -def pil_screenshot(file_path: str = None, screen_region: list = None) -> Image: +def pil_screenshot(file_path: Optional[str] = None, screen_region: Optional[List[int]] = None) -> Image.Image: """ - use pil to make a screenshot - :param file_path save screenshot path (None is no save) - :param screen_region screenshot screen_region on screen [left, top, right, bottom] + Take a screenshot using PIL (Pillow). + 使用 PIL (Pillow) 擷取螢幕畫面 + + :param file_path: (str | None) Path to save the screenshot. If None, do not save. + 螢幕截圖的存檔路徑,若為 None 則不存檔 + :param screen_region: (list[int] | None) Region to capture [left, top, right, bottom]. + 擷取的螢幕區域 [左, 上, 右, 下],若為 None 則擷取全螢幕 + :return: PIL.Image.Image object 擷取到的影像物件 """ + # 擷取螢幕畫面 Capture screen if screen_region is not None: image = ImageGrab.grab(bbox=screen_region) else: image = ImageGrab.grab() - if file_path is not None: - image.save(file_path) - return image + + # 如果指定了存檔路徑,則存檔 Save if file_path is provided + if file_path: + try: + image.save(file_path) + except Exception as e: + print(f"Failed to save screenshot: {e}") + + return image \ No newline at end of file diff --git a/je_auto_control/utils/cv2_utils/template_detection.py b/je_auto_control/utils/cv2_utils/template_detection.py index d99934e..66fda46 100644 --- a/je_auto_control/utils/cv2_utils/template_detection.py +++ b/je_auto_control/utils/cv2_utils/template_detection.py @@ -1,28 +1,47 @@ from typing import List - from PIL import ImageGrab from je_open_cv import template_detection -def find_image(image, detect_threshold: float = 1, draw_image: bool = False) -> List[int]: +def find_image(image, detect_threshold: float = 1.0, draw_image: bool = False) -> List[int]: """ - Find image with detect threshold on screen. - :param image: which cv2_utils we want to find on screen - :param detect_threshold: detect precision 0.0 ~ 1.0; 1 is absolute equal - :param draw_image: draw detect tag on return cv2_utils + Find a single image on the screen using template detection. + 使用模板匹配在螢幕上尋找單一影像 + + :param image: Template image 模板影像 (要尋找的影像) + :param detect_threshold: Detection precision (0.0 ~ 1.0, 1.0 = 完全相同) + :param draw_image: Whether to draw detection markers 是否在回傳影像上標記偵測結果 + :return: List[int] [x, y] 座標位置 """ + # 擷取螢幕畫面 Capture screen grab_image = ImageGrab.grab() - return template_detection.find_object(image=grab_image, template=image, - detect_threshold=detect_threshold, draw_image=draw_image) + + # 使用模板匹配 Find object + return template_detection.find_object( + image=grab_image, + template=image, + detect_threshold=detect_threshold, + draw_image=draw_image + ) -def find_image_multi(image, detect_threshold: float = 1, draw_image: bool = False) -> List[List[int]]: +def find_image_multi(image, detect_threshold: float = 1.0, draw_image: bool = False) -> List[List[int]]: """ - Find multi image with detect threshold on screen. - :param image: which cv2_utils we want to find on screen - :param detect_threshold: detect precision 0.0 ~ 1.0; 1 is absolute equal - :param draw_image: draw detect tag on return cv2_utils + Find multiple occurrences of an image on the screen using template detection. + 使用模板匹配在螢幕上尋找多個影像 + + :param image: Template image 模板影像 (要尋找的影像) + :param detect_threshold: Detection precision (0.0 ~ 1.0, 1.0 = 完全相同) + :param draw_image: Whether to draw detection markers 是否在回傳影像上標記偵測結果 + :return: List[List[int]] 多個座標位置 [[x1, y1], [x2, y2], ...] """ + # 擷取螢幕畫面 Capture screen grab_image = ImageGrab.grab() - return template_detection.find_multi_object(image=grab_image, template=image, - detect_threshold=detect_threshold, draw_image=draw_image) + + # 使用模板匹配 Find multiple objects + return template_detection.find_multi_object( + image=grab_image, + template=image, + detect_threshold=detect_threshold, + draw_image=draw_image + ) \ No newline at end of file diff --git a/je_auto_control/utils/cv2_utils/video_recording.py b/je_auto_control/utils/cv2_utils/video_recording.py index 7a03409..9969969 100644 --- a/je_auto_control/utils/cv2_utils/video_recording.py +++ b/je_auto_control/utils/cv2_utils/video_recording.py @@ -1,5 +1,4 @@ import threading - import cv2 import numpy as np from mss import mss @@ -8,29 +7,67 @@ class RecordingThread(threading.Thread): + """ + RecordingThread + 螢幕錄影執行緒 + - 使用 mss 擷取螢幕畫面 + - 使用 OpenCV VideoWriter 寫入影片檔案 + """ - def __init__(self, video_name: str = "autocontrol_recoding"): - autocontrol_logger.info("Init RecordingThread") + def __init__(self, video_name: str = "autocontrol_recording", fps: int = 20): super().__init__() - self.recoding_flag = True + autocontrol_logger.info("Init RecordingThread") + self.recording_flag = True self.video_name = video_name self.daemon = True - self.fps = 20 + self.fps = fps + + def set_recording_flag(self, recording_flag: bool): + """ + 設定錄影旗標 + Set recording flag - def set_recoding_flag(self, recoding_flag: bool): - autocontrol_logger.info(f"RecordingThread set_recoding_flag recoding_flag: {recoding_flag}") - self.recoding_flag = recoding_flag + :param recording_flag: True = 繼續錄影, False = 停止錄影 + """ + autocontrol_logger.info(f"RecordingThread set_recording_flag: {recording_flag}") + self.recording_flag = recording_flag + + def stop(self): + """ + 停止錄影 + Stop recording + """ + self.set_recording_flag(False) def run(self): + """ + 執行錄影迴圈 + Run recording loop + """ with mss() as sct: resolution = sct.monitors[0] - self.video_name = self.video_name + '.mp4' - fourcc = cv2.VideoWriter_fourcc(*'mp4v') - video_writer = cv2.VideoWriter(self.video_name, fourcc, self.fps, - (resolution['width'], resolution['height'])) - while self.recoding_flag: - screen_image = sct.grab(resolution) - image_rgb = cv2.cvtColor(np.array(screen_image), cv2.COLOR_BGRA2BGR) - video_writer.write(image_rgb) - else: + output_file = self.video_name + ".mp4" + + fourcc = cv2.VideoWriter_fourcc(*"mp4v") + video_writer = cv2.VideoWriter( + output_file, + fourcc, + self.fps, + (resolution["width"], resolution["height"]) + ) + + if not video_writer.isOpened(): + autocontrol_logger.error("Failed to open VideoWriter") + return + + try: + while self.recording_flag: + # 擷取螢幕畫面 Capture screen frame + screen_image = sct.grab(resolution) + image_rgb = cv2.cvtColor(np.array(screen_image), cv2.COLOR_BGRA2BGR) + video_writer.write(image_rgb) + except Exception as e: + autocontrol_logger.error(f"RecordingThread error: {e}") + finally: video_writer.release() + autocontrol_logger.info("RecordingThread stopped and video released") \ No newline at end of file diff --git a/je_auto_control/utils/exception/exception_tags.py b/je_auto_control/utils/exception/exception_tags.py index 36428ad..d997f15 100644 --- a/je_auto_control/utils/exception/exception_tags.py +++ b/je_auto_control/utils/exception/exception_tags.py @@ -1,80 +1,77 @@ # error tags -je_auto_control_error: str = "Auto-control error" -je_auto_control_critical_exit_error: str = "Auto-control critical exit error" +je_auto_control_error_message: str = "Auto-control error" +je_auto_control_critical_exit_error_message: str = "Auto-control critical exit error" # os tags -linux_import_error: str = "Should only be loaded on Linux" -osx_import_error: str = "Should only be loaded on macOS" -windows_import_error: str = "Should only be loaded on Windows" -macos_record_error: str = "Cannot use recorder on macOS" +linux_import_error_message: str = "Should only be loaded on Linux" +osx_import_error_message: str = "Should only be loaded on macOS" +windows_import_error_message: str = "Should only be loaded on Windows" +macos_record_error_message: str = "Cannot use recorder on macOS" # keyboard tags -keyboard_error: str = "Auto-control keyboard error" -keyboard_press_key: str = "Keyboard key press error" -keyboard_release_key: str = "Keyboard key release error" -keyboard_type_key: str = "Keyboard key type error" -keyboard_write: str = "Keyboard write error" -keyboard_write_cant_find: str = "Keyboard write error: key not found" -keyboard_hotkey: str = "Keyboard hotkey error" +keyboard_error_message: str = "Auto-control keyboard error" +keyboard_press_key_error_message: str = "Keyboard key press error" +keyboard_release_key_error_message: str = "Keyboard key release error" +keyboard_type_key_error_message: str = "Keyboard key type error" +keyboard_write_error_message: str = "Keyboard write error" +keyboard_write_cant_find_error_message: str = "Keyboard write error: key not found" +keyboard_hotkey_error_message: str = "Keyboard hotkey error" # mouse tags -mouse_error: str = "Auto-control mouse error" -mouse_get_position: str = "Mouse position retrieval error" -mouse_set_position: str = "Mouse position set error" -mouse_press_mouse: str = "Mouse press error" -mouse_release_mouse: str = "Mouse release error" -mouse_click_mouse: str = "Mouse click error" -mouse_scroll: str = "Mouse scroll error" -mouse_wrong_value: str = "Mouse value error" +mouse_error_message: str = "Auto-control mouse error" +mouse_get_position_error_message: str = "Mouse position retrieval error" +mouse_set_position_error_message: str = "Mouse position set error" +mouse_press_mouse_error_message: str = "Mouse press error" +mouse_release_mouse_error_message: str = "Mouse release error" +mouse_click_mouse_error_message: str = "Mouse click error" +mouse_scroll_error_message: str = "Mouse scroll error" +mouse_wrong_value_error_message: str = "Mouse value error" # screen tags -screen_error: str = "Auto-control screen error" -screen_get_size: str = "Screen size retrieval error" -screen_screenshot: str = "Screen screenshot error" +screen_error_message: str = "Auto-control screen error" +screen_get_size_error_message: str = "Screen size retrieval error" +screen_screenshot_error_message: str = "Screen screenshot error" # table tags -table_cant_find_key: str = "Cannot find key error" +table_cant_find_key_error_message: str = "Cannot find key error" # cv2_utils tags -cant_find_image: str = "Cannot find image" -find_image_error_variable: str = "Variable error" +cant_find_image_error_message: str = "Cannot find image" +find_image_error_variable_error_message: str = "Variable error" # listener tags -listener_error: str = "Auto-control listener error" +listener_error_message: str = "Auto-control listener error" # test_record tags -record_queue_error: str = "Cannot get test_record queue: it is None. Are you stopping test_record before running it?" -record_not_found_action_error: str = "test_record action not found" +record_queue_error_message: str = "Cannot get test_record queue: it is None. Are you stopping test_record before running it?" +record_not_found_action_error_message: str = "test_record action not found" # json tag -cant_execute_action_error: str = "Cannot execute action" -cant_generate_json_report: str = "Cannot generate JSON report" -cant_find_json_error: str = "Cannot find JSON file" -cant_save_json_error: str = "Cannot save JSON file" -action_is_null_error: str = "JSON action is null" - -# timeout tag -timeout_need_on_main_error: str = "Timeout function must be in main" +cant_execute_action_error_message: str = "Cannot execute action" +cant_generate_json_report_error_message: str = "Cannot generate JSON report" +cant_find_json_error_message: str = "Cannot find JSON file" +cant_save_json_error_message: str = "Cannot save JSON file" +action_is_null_error_message: str = "JSON action is null" # HTML -html_generate_no_data_tag: str = "Record is None" +html_generate_no_data_tag_error_message: str = "Record is None" # add command -add_command_exception: str = "Command value must be a method or function" +add_command_exception_error_message: str = "Command value must be a method or function" # executor -executor_list_error: str = "Executor received invalid data: list is None or wrong type" +executor_list_error_message: str = "Executor received invalid data: list is None or wrong type" # argparse -argparse_get_wrong_data: str = "Argparse received invalid data" +argparse_get_wrong_data_error_message: str = "Argparse received invalid data" # XML -cant_read_xml_error: str = "Cannot read XML" -xml_type_error: str = "XML type error" +cant_read_xml_error_message: str = "Cannot read XML" +xml_type_error_message: str = "XML type error" # Callback executor -get_bad_trigger_method: str = "Invalid trigger method: only kwargs and args accepted" -get_bad_trigger_function: str = "Invalid trigger function: only functions in event_dict accepted" +get_bad_trigger_method_error_message: str = "Invalid trigger method: only kwargs and args accepted" +get_bad_trigger_function_error_message: str = "Invalid trigger function: only functions in event_dict accepted" # Can't find file -can_not_find_file: str = "Cannot find file" \ No newline at end of file +can_not_find_file_error_message: str = "Cannot find file" \ No newline at end of file diff --git a/je_auto_control/utils/exception/exceptions.py b/je_auto_control/utils/exception/exceptions.py index 30e47cf..50d30cb 100644 --- a/je_auto_control/utils/exception/exceptions.py +++ b/je_auto_control/utils/exception/exceptions.py @@ -64,11 +64,6 @@ class AutoControlArgparseException(Exception): pass -# timeout -class AutoControlTimeoutException(Exception): - pass - - # html exception class AutoControlHTMLException(Exception): diff --git a/je_auto_control/utils/executor/action_executor.py b/je_auto_control/utils/executor/action_executor.py index af0a5e2..33182f3 100644 --- a/je_auto_control/utils/executor/action_executor.py +++ b/je_auto_control/utils/executor/action_executor.py @@ -3,17 +3,17 @@ from inspect import getmembers, isbuiltin from typing import Any, Dict, List, Union -from je_auto_control.utils.exception.exception_tags import action_is_null_error, add_command_exception, \ - executor_list_error -from je_auto_control.utils.exception.exception_tags import cant_execute_action_error -from je_auto_control.utils.exception.exceptions import AutoControlActionException, AutoControlAddCommandException -from je_auto_control.utils.exception.exceptions import AutoControlActionNullException -from je_auto_control.utils.generate_report.generate_html_report import generate_html -from je_auto_control.utils.generate_report.generate_html_report import generate_html_report -from je_auto_control.utils.generate_report.generate_json_report import generate_json -from je_auto_control.utils.generate_report.generate_json_report import generate_json_report -from je_auto_control.utils.generate_report.generate_xml_report import generate_xml -from je_auto_control.utils.generate_report.generate_xml_report import generate_xml_report +from je_auto_control.utils.exception.exception_tags import ( + action_is_null_error_message, add_command_exception_error_message, + executor_list_error_message, cant_execute_action_error_message +) +from je_auto_control.utils.exception.exceptions import ( + AutoControlActionException, AutoControlAddCommandException, + AutoControlActionNullException +) +from je_auto_control.utils.generate_report.generate_html_report import generate_html, generate_html_report +from je_auto_control.utils.generate_report.generate_json_report import generate_json, generate_json_report +from je_auto_control.utils.generate_report.generate_xml_report import generate_xml, generate_xml_report from je_auto_control.utils.json.json_file import read_action_json from je_auto_control.utils.logging.loggin_instance import autocontrol_logger from je_auto_control.utils.package_manager.package_manager_class import package_manager @@ -22,23 +22,31 @@ from je_auto_control.utils.start_exe.start_another_process import start_exe from je_auto_control.utils.test_record.record_test_class import record_action_to_list, test_record_instance from je_auto_control.wrapper.auto_control_image import locate_all_image, locate_and_click, locate_image_center -from je_auto_control.wrapper.auto_control_keyboard import check_key_is_press -from je_auto_control.wrapper.auto_control_keyboard import get_special_table, get_keyboard_keys_table -from je_auto_control.wrapper.auto_control_keyboard import press_keyboard_key, release_keyboard_key, hotkey, \ - type_keyboard, write -from je_auto_control.wrapper.auto_control_mouse import get_mouse_position, press_mouse, release_mouse, click_mouse, \ - mouse_scroll -from je_auto_control.wrapper.auto_control_mouse import get_mouse_table -from je_auto_control.wrapper.auto_control_mouse import set_mouse_position +from je_auto_control.wrapper.auto_control_keyboard import ( + check_key_is_press, get_keyboard_keys_table, + press_keyboard_key, release_keyboard_key, hotkey, type_keyboard, write +) +from je_auto_control.wrapper.auto_control_mouse import ( + get_mouse_position, press_mouse, release_mouse, click_mouse, + mouse_scroll_error_message, get_mouse_table, set_mouse_position +) from je_auto_control.wrapper.auto_control_record import record, stop_record from je_auto_control.wrapper.auto_control_screen import screenshot, screen_size -class Executor(object): +class Executor: + """ + Executor + 指令執行器 + - 提供 event_dict 對應字串名稱到函式 + - 支援滑鼠、鍵盤、螢幕、影像辨識、報告生成等功能 + - 可執行 action list 或 action file + """ def __init__(self): + # 事件字典,對應字串名稱到函式 self.event_dict: dict = { - # mouse + # Mouse 滑鼠相關 "AC_mouse_left": click_mouse, "AC_mouse_right": click_mouse, "AC_mouse_middle": click_mouse, @@ -47,10 +55,10 @@ def __init__(self): "AC_get_mouse_position": get_mouse_position, "AC_press_mouse": press_mouse, "AC_release_mouse": release_mouse, - "AC_mouse_scroll": mouse_scroll, + "AC_mouse_scroll": mouse_scroll_error_message, "AC_set_mouse_position": set_mouse_position, - "AC_get_special_table": get_special_table, - # keyboard + + # Keyboard 鍵盤相關 "AC_get_keyboard_keys_table": get_keyboard_keys_table, "AC_type_keyboard": type_keyboard, "AC_press_keyboard_key": press_keyboard_key, @@ -58,44 +66,60 @@ def __init__(self): "AC_check_key_is_press": check_key_is_press, "AC_write": write, "AC_hotkey": hotkey, - # cv2_utils + + # Image 影像辨識 "AC_locate_all_image": locate_all_image, "AC_locate_image_center": locate_image_center, "AC_locate_and_click": locate_and_click, - # screen + + # Screen 螢幕相關 "AC_screen_size": screen_size, "AC_screenshot": screenshot, - # test record + + # Test record 測試紀錄 "AC_set_record_enable": test_record_instance.set_record_enable, - # only generate + + # Report 報告生成 "AC_generate_html": generate_html, "AC_generate_json": generate_json, "AC_generate_xml": generate_xml, - # generate report "AC_generate_html_report": generate_html_report, "AC_generate_json_report": generate_json_report, "AC_generate_xml_report": generate_xml_report, - # record + + # Record 錄製 "AC_record": record, "AC_stop_record": stop_record, - # execute + + # Executor 執行器 "AC_execute_action": self.execute_action, "AC_execute_files": self.execute_files, "AC_add_package_to_executor": package_manager.add_package_to_executor, "AC_add_package_to_callback_executor": package_manager.add_package_to_callback_executor, - # project + + # Project 專案 "AC_create_project": create_project_dir, + # Shell "AC_shell_command": ShellManager().exec_shell, - # Another process + + # Process "AC_execute_process": start_exe, } - # get all builtin function and add to event dict + + # 加入所有 Python 內建函式 Add all Python builtins for function in getmembers(builtins, isbuiltin): - self.event_dict.update({str(function[0]): function[1]}) + self.event_dict[str(function[0])] = function[1] def _execute_event(self, action: list) -> Any: + """ + 執行單一事件 + Execute a single event + """ event = self.event_dict.get(action[0]) + if event is None: + raise AutoControlActionException(f"Unknown action: {action[0]}") + if len(action) == 2: if isinstance(action[1], dict): return event(**action[1]) @@ -104,71 +128,80 @@ def _execute_event(self, action: list) -> Any: elif len(action) == 1: return event() else: - raise AutoControlActionException(cant_execute_action_error + " " + str(action)) + raise AutoControlActionException(cant_execute_action_error_message + " " + str(action)) - def execute_action(self, action_list: [list, dict]) -> Dict[str, str]: + def execute_action(self, action_list: Union[list, dict]) -> Dict[str, str]: """ - use to execute all action on action list(action file or program list) - :param action_list the list include action - for loop the list and execute action + 執行 action list + Execute all actions in action list + + :param action_list: list 或 dict (包含 auto_control key) + :return: 執行紀錄字典 """ autocontrol_logger.info(f"execute_action, action_list: {action_list}") + if isinstance(action_list, dict): - action_list: list = action_list.get("auto_control") + action_list = action_list.get("auto_control") if action_list is None: - raise AutoControlActionNullException(executor_list_error) - execute_record_dict = dict() - try: - if len(action_list) < 0 or isinstance(action_list, list) is False: - raise AutoControlActionNullException(action_is_null_error) - except Exception as error: - record_action_to_list("AC_execute_action", action_list, repr(error)) - autocontrol_logger.info( - f"execute_action, action_list: {action_list}, " - f"failed: {repr(error)}") + raise AutoControlActionNullException(executor_list_error_message) + + if not isinstance(action_list, list) or len(action_list) == 0: + raise AutoControlActionNullException(action_is_null_error_message) + + execute_record_dict = {} + for action in action_list: try: event_response = self._execute_event(action) execute_record = "execute: " + str(action) - execute_record_dict.update({execute_record: event_response}) + execute_record_dict[execute_record] = event_response except Exception as error: autocontrol_logger.info( - f"execute_action, action_list: {action_list}, " - f"action: {action}, failed: {repr(error)}") + f"execute_action failed, action: {action}, error: {repr(error)}" + ) record_action_to_list("AC_execute_action", None, repr(error)) execute_record = "execute: " + str(action) - execute_record_dict.update({execute_record: repr(error)}) + execute_record_dict[execute_record] = repr(error) + + # 輸出執行結果 Print results for key, value in execute_record_dict.items(): print(key, flush=True) print(value, flush=True) + return execute_record_dict def execute_files(self, execute_files_list: list) -> List[Dict[str, str]]: """ - :param execute_files_list: list include execute files path - :return: every execute detail as list + 執行 action files + Execute actions from files + + :param execute_files_list: list of file paths + :return: 每個檔案的執行結果 """ autocontrol_logger.info(f"execute_files, execute_files_list: {execute_files_list}") - execute_detail_list: list = list() + execute_detail_list = [] for file in execute_files_list: execute_detail_list.append(self.execute_action(read_action_json(file))) return execute_detail_list - +# === 全域 Executor 實例 Global Executor Instance === executor = Executor() package_manager.executor = executor def add_command_to_executor(command_dict: dict) -> None: """ - :param command_dict: dict include command we want to add to event_dict + 新增自訂指令到 Executor + Add custom commands to Executor + + :param command_dict: dict {command_name: function} """ for command_name, command in command_dict.items(): if isinstance(command, (types.MethodType, types.FunctionType)): - executor.event_dict.update({command_name: command}) + executor.event_dict[command_name] = command else: - raise AutoControlAddCommandException(add_command_exception) + raise AutoControlAddCommandException(add_command_exception_error_message) def execute_action(action_list: list) -> Dict[str, str]: @@ -176,4 +209,4 @@ def execute_action(action_list: list) -> Dict[str, str]: def execute_files(execute_files_list: list) -> List[Dict[str, str]]: - return executor.execute_files(execute_files_list) + return executor.execute_files(execute_files_list) \ No newline at end of file diff --git a/je_auto_control/utils/file_process/get_dir_file_list.py b/je_auto_control/utils/file_process/get_dir_file_list.py index 852f84a..3a6ce10 100644 --- a/je_auto_control/utils/file_process/get_dir_file_list.py +++ b/je_auto_control/utils/file_process/get_dir_file_list.py @@ -1,21 +1,24 @@ -from os import getcwd -from os import walk -from os.path import abspath -from os.path import join +from os import getcwd, walk +from os.path import abspath, join from typing import List def get_dir_files_as_list( - dir_path: str = getcwd(), - default_search_file_extension: str = ".json") -> List[str]: + dir_path: str = getcwd(), + default_search_file_extension: str = ".json" +) -> List[str]: """ - get dir file when end with default_search_file_extension - :param dir_path: which dir we want to walk and get file list - :param default_search_file_extension: which extension we want to search - :return: [] if nothing searched or [file1, file2.... files] file was searched + Get all files in a directory that end with a specific extension. + 遍歷指定目錄,取得所有符合副檔名的檔案清單 + + :param dir_path: Directory path to search 要搜尋的目錄路徑 + :param default_search_file_extension: File extension to filter 要搜尋的副檔名 (預設 ".json") + :return: List of absolute file paths 符合條件的檔案絕對路徑清單 """ + extension = default_search_file_extension.lower() return [ - abspath(join(dir_path, file)) for root, dirs, files in walk(dir_path) + abspath(join(root, file)) + for root, dirs, files in walk(dir_path) for file in files - if file.endswith(default_search_file_extension.lower()) - ] + if file.lower().endswith(extension) + ] \ No newline at end of file diff --git a/je_auto_control/utils/generate_report/generate_html_report.py b/je_auto_control/utils/generate_report/generate_html_report.py index aef5e10..d245c8b 100644 --- a/je_auto_control/utils/generate_report/generate_html_report.py +++ b/je_auto_control/utils/generate_report/generate_html_report.py @@ -1,95 +1,80 @@ from threading import Lock -from je_auto_control.utils.exception.exception_tags import html_generate_no_data_tag +from je_auto_control.utils.exception.exception_tags import html_generate_no_data_tag_error_message from je_auto_control.utils.exception.exceptions import AutoControlHTMLException from je_auto_control.utils.logging.loggin_instance import autocontrol_logger from je_auto_control.utils.test_record.record_test_class import test_record_instance _lock = Lock() -_html_string = \ - r""" +# HTML 模板 HTML template +_html_string = r""" AutoControl Report - -

- Test Report -

+

Test Report

{event_table} """.strip() -_event_table = \ - r""" - - +# 單一事件表格模板 Single event table template +_event_table = r""" +
+ - - + + @@ -106,61 +91,73 @@ - -
Test Report
function_name {function_name}exception {exception}
-
- """.strip() + + +
+""".strip() def make_html_table(event_str: str, record_data: dict, table_head: str) -> str: - event_str = "".join( - [ - event_str, - _event_table.format( - table_head_class=table_head, - function_name=record_data.get("function_name"), - param=record_data.get("local_param"), - time=record_data.get("time"), - exception=record_data.get("program_exception"), - ) - ] - ) - return event_str + """ + 建立單一事件的 HTML 表格 + Create HTML table for a single event + + :param event_str: 現有的 HTML 字串 Existing HTML string + :param record_data: 單一事件紀錄 Single event record + :param table_head: 表頭樣式 (成功/失敗) Table head style + :return: 更新後的 HTML 字串 Updated HTML string + """ + return "".join([ + event_str, + _event_table.format( + table_head_class=table_head, + function_name=record_data.get("function_name"), + param=record_data.get("local_param"), + time=record_data.get("time"), + exception=record_data.get("program_exception"), + ) + ]) def generate_html() -> str: + """ + 產生完整 HTML 報告字串 + Generate full HTML report string + + :return: HTML 字串 HTML string + """ autocontrol_logger.info("generate_html") - """ this function will create html string - :return: html_string """ - if len(test_record_instance.test_record_list) == 0: - raise AutoControlHTMLException(html_generate_no_data_tag) - else: - event_str: str = "" - for record_data in test_record_instance.test_record_list: - # because data on record_data all is str - if record_data.get("program_exception") == "None": - event_str = make_html_table(event_str, record_data, "event_table_head") - else: - event_str = make_html_table(event_str, record_data, "failure_table_head") - new_html_string = _html_string.format(event_table=event_str) - return new_html_string + + if not test_record_instance.test_record_list: + raise AutoControlHTMLException(html_generate_no_data_tag_error_message) + + event_str = "" + for record_data in test_record_instance.test_record_list: + # 判斷是否有例外,決定表格樣式 + if record_data.get("program_exception") == "None": + event_str = make_html_table(event_str, record_data, "event_table_head") + else: + event_str = make_html_table(event_str, record_data, "failure_table_head") + + return _html_string.format(event_table=event_str) def generate_html_report(html_name: str = "default_name") -> None: - autocontrol_logger.info(f"generate_html_report, html_name: {html_name}") """ - Output html report file - :param html_name: save html file name + 輸出 HTML 報告檔案 + Output HTML report file + + :param html_name: 檔案名稱 (不含副檔名) File name without extension """ + autocontrol_logger.info(f"generate_html_report, html_name: {html_name}") + new_html_string = generate_html() - _lock.acquire() - try: - with open(html_name + ".html", "w+") as file_to_write: - file_to_write.write( - new_html_string - ) - except Exception as error: - autocontrol_logger.error( - f"generate_html_report, html_name: {html_name}, failed: {repr(error)}") - finally: - _lock.release() + + with _lock: # 使用 with 確保 Lock 正確釋放 Ensure lock is released properly + try: + with open(html_name + ".html", "w+", encoding="utf-8") as file_to_write: + file_to_write.write(new_html_string) + except Exception as error: + autocontrol_logger.error( + f"generate_html_report failed, html_name: {html_name}, error: {repr(error)}" + ) \ No newline at end of file diff --git a/je_auto_control/utils/generate_report/generate_json_report.py b/je_auto_control/utils/generate_report/generate_json_report.py index 94e9b0c..59f3d28 100644 --- a/je_auto_control/utils/generate_report/generate_json_report.py +++ b/je_auto_control/utils/generate_report/generate_json_report.py @@ -2,79 +2,73 @@ from threading import Lock from typing import Dict, Tuple -from je_auto_control.utils.exception.exception_tags import cant_generate_json_report +from je_auto_control.utils.exception.exception_tags import cant_generate_json_report_error_message from je_auto_control.utils.exception.exceptions import AutoControlGenerateJsonReportException from je_auto_control.utils.logging.loggin_instance import autocontrol_logger from je_auto_control.utils.test_record.record_test_class import test_record_instance def generate_json() -> Tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, str]]]: - autocontrol_logger.info("generate_json") """ - :return: two dict {success_dict}, {failure_dict} + Generate JSON data from test records. + 從測試紀錄生成 JSON 資料 + + :return: (success_dict, failure_dict) """ - if len(test_record_instance.test_record_list) == 0: - raise AutoControlGenerateJsonReportException(cant_generate_json_report) - else: - success_dict = dict() - failure_dict = dict() - failure_count: int = 1 - failure_test_str: str = "Failure_Test" - success_count: int = 1 - success_test_str: str = "Success_Test" - for record_data in test_record_instance.test_record_list: - if record_data.get("program_exception") == "None": - success_dict.update( - { - success_test_str + str(success_count): { - "function_name": str(record_data.get("function_name")), - "param": str(record_data.get("local_param")), - "time": str(record_data.get("time")), - "exception": str(record_data.get("program_exception")) - } - } - ) - success_count = success_count + 1 - else: - failure_dict.update( - { - failure_test_str + str(failure_count): { - "function_name": str(record_data.get("function_name")), - "param": str(record_data.get("local_param")), - "time": str(record_data.get("time")), - "exception": str(record_data.get("program_exception")) - } - } - ) - failure_count = failure_count + 1 + autocontrol_logger.info("generate_json") + + if not test_record_instance.test_record_list: + raise AutoControlGenerateJsonReportException(cant_generate_json_report_error_message) + + success_dict: Dict[str, Dict[str, str]] = {} + failure_dict: Dict[str, Dict[str, str]] = {} + + success_count, failure_count = 1, 1 + for record_data in test_record_instance.test_record_list: + record_entry = { + "function_name": str(record_data.get("function_name")), + "param": str(record_data.get("local_param")), + "time": str(record_data.get("time")), + "exception": str(record_data.get("program_exception")), + } + if record_data.get("program_exception") == "None": + success_dict[f"Success_Test{success_count}"] = record_entry + success_count += 1 + else: + failure_dict[f"Failure_Test{failure_count}"] = record_entry + failure_count += 1 + return success_dict, failure_dict -def generate_json_report(json_file_name: str = "default_name"): - autocontrol_logger.info(f"generate_json_report, json_file_name: {json_file_name}") +def _write_json_file(file_name: str, data: Dict[str, Dict[str, str]], lock: Lock) -> None: """ - Output json report file - :param json_file_name: save json file's name + Write JSON data to file safely with lock. + 使用 Lock 安全地將 JSON 資料寫入檔案 + + :param file_name: 檔案名稱 + :param data: 要寫入的 JSON 資料 + :param lock: 執行緒鎖 """ - lock = Lock() + with lock: + try: + with open(file_name, "w+", encoding="utf-8") as file_to_write: + json.dump(data, file_to_write, indent=4, ensure_ascii=False) + except Exception as error: + autocontrol_logger.error(f"Failed to write {file_name}, error: {repr(error)}") + + +def generate_json_report(json_file_name: str = "default_name") -> None: + """ + Output JSON report files (success and failure). + 輸出 JSON 報告檔案 (成功與失敗) + + :param json_file_name: 檔案名稱前綴 + """ + autocontrol_logger.info(f"generate_json_report, json_file_name: {json_file_name}") + success_dict, failure_dict = generate_json() - lock.acquire() - try: - with open(json_file_name + "_success.json", "w+") as file_to_write: - json.dump(dict(success_dict), file_to_write, indent=4) - except Exception as error: - autocontrol_logger.error( - f"generate_json_report, json_file_name: {json_file_name}, " - f"failed: {repr(error)}") - finally: - lock.release() - lock.acquire() - try: - with open(json_file_name + "_failure.json", "w+") as file_to_write: - json.dump(dict(failure_dict), file_to_write, indent=4) - except Exception as error: - autocontrol_logger.error( - f"generate_json_report, json_file_name: {json_file_name}, " - f"failed: {repr(error)}") - finally: - lock.release() + lock = Lock() + + _write_json_file(json_file_name + "_success.json", success_dict, lock) + _write_json_file(json_file_name + "_failure.json", failure_dict, lock) \ No newline at end of file diff --git a/je_auto_control/utils/generate_report/generate_xml_report.py b/je_auto_control/utils/generate_report/generate_xml_report.py index 7493c00..0739f89 100644 --- a/je_auto_control/utils/generate_report/generate_xml_report.py +++ b/je_auto_control/utils/generate_report/generate_xml_report.py @@ -8,46 +8,56 @@ def generate_xml() -> Tuple[Union[str, bytes], Union[str, bytes]]: - autocontrol_logger.info("generate_xml") """ - :return: two dict {success_dict}, {failure_dict} + Generate XML strings from test records. + 從測試紀錄生成 XML 字串 + + :return: (success_xml, failure_xml) """ + autocontrol_logger.info("generate_xml") + success_dict, failure_dict = generate_json() - success_dict = dict({"xml_data": success_dict}) - failure_dict = dict({"xml_data": failure_dict}) - success_json_to_xml = dict_to_elements_tree(success_dict) - failure_json_to_xml = dict_to_elements_tree(failure_dict) - return success_json_to_xml, failure_json_to_xml + success_dict = {"xml_data": success_dict} + failure_dict = {"xml_data": failure_dict} + success_xml = dict_to_elements_tree(success_dict) + failure_xml = dict_to_elements_tree(failure_dict) -def generate_xml_report(xml_file_name: str = "default_name"): - autocontrol_logger.info(f"generate_xml_report, xml_file_name: {xml_file_name}") + return success_xml, failure_xml + + +def _write_xml_file(file_name: str, xml_content: str, lock: Lock) -> None: """ - :param xml_file_name: save xml file name + Write XML content to file safely with lock. + 使用 Lock 安全地將 XML 內容寫入檔案 + + :param file_name: 檔案名稱 + :param xml_content: XML 字串 + :param lock: 執行緒鎖 """ + with lock: + try: + with open(file_name, "w+", encoding="utf-8") as file_to_write: + file_to_write.write(xml_content) + except Exception as error: + autocontrol_logger.error(f"Failed to write {file_name}, error: {repr(error)}") + + +def generate_xml_report(xml_file_name: str = "default_name") -> None: + """ + Output XML report files (success and failure). + 輸出 XML 報告檔案 (成功與失敗) + + :param xml_file_name: 檔案名稱前綴 + """ + autocontrol_logger.info(f"generate_xml_report, xml_file_name: {xml_file_name}") + success_xml, failure_xml = generate_xml() - success_xml = parseString(success_xml) - failure_xml = parseString(failure_xml) - success_xml = success_xml.toprettyxml() - failure_xml = failure_xml.toprettyxml() + + # 格式化 XML 內容 Format XML content + success_xml = parseString(success_xml).toprettyxml() + failure_xml = parseString(failure_xml).toprettyxml() + lock = Lock() - lock.acquire() - try: - with open(xml_file_name + "_failure.xml", "w+") as file_to_write: - file_to_write.write(failure_xml) - except Exception as error: - autocontrol_logger.error( - f"generate_xml_report, xml_file_name: {xml_file_name}, " - f"failed: {repr(error)}") - finally: - lock.release() - lock.acquire() - try: - with open(xml_file_name + "_success.xml", "w+") as file_to_write: - file_to_write.write(success_xml) - except Exception as error: - autocontrol_logger.error( - f"generate_xml_report, xml_file_name: {xml_file_name}, " - f"failed: {repr(error)}") - finally: - lock.release() + _write_xml_file(xml_file_name + "_success.xml", success_xml, lock) + _write_xml_file(xml_file_name + "_failure.xml", failure_xml, lock) \ No newline at end of file diff --git a/je_auto_control/utils/json/json_file.py b/je_auto_control/utils/json/json_file.py index 0003f3c..d7cbbce 100644 --- a/je_auto_control/utils/json/json_file.py +++ b/je_auto_control/utils/json/json_file.py @@ -3,8 +3,7 @@ from threading import Lock from typing import List, Dict -from je_auto_control.utils.exception.exception_tags import cant_find_json_error -from je_auto_control.utils.exception.exception_tags import cant_save_json_error +from je_auto_control.utils.exception.exception_tags import cant_find_json_error_message, cant_save_json_error_message from je_auto_control.utils.exception.exceptions import AutoControlJsonActionException _lock = Lock() @@ -12,32 +11,35 @@ def read_action_json(json_file_path: str) -> List[List[Dict[str, Dict[str, str]]]]: """ - use to read action file - :param json_file_path json file's path to read + Read action JSON file. + 讀取動作 JSON 檔案 + + :param json_file_path: JSON 檔案路徑 + :return: JSON 內容 (list of list of dict) """ - _lock.acquire() - try: - file_path = Path(json_file_path) - if file_path.exists() and file_path.is_file(): - with open(json_file_path) as read_file: - return json.loads(read_file.read()) - except AutoControlJsonActionException: - raise AutoControlJsonActionException(cant_find_json_error) - finally: - _lock.release() + with _lock: + try: + file_path = Path(json_file_path) + if file_path.exists() and file_path.is_file(): + with open(json_file_path, encoding="utf-8") as read_file: + return json.load(read_file) + else: + raise AutoControlJsonActionException(cant_find_json_error_message) + except Exception as error: + raise AutoControlJsonActionException(f"{cant_find_json_error_message}: {repr(error)}") def write_action_json(json_save_path: str, action_json: list) -> None: """ - use to save action file - :param json_save_path json save path - :param action_json the json str include action to write + Write action JSON file. + 寫入動作 JSON 檔案 + + :param json_save_path: JSON 檔案儲存路徑 + :param action_json: 要寫入的 JSON 資料 """ - _lock.acquire() - try: - with open(json_save_path, "w+") as file_to_write: - file_to_write.write(json.dumps(action_json, indent=4)) - except AutoControlJsonActionException: - raise AutoControlJsonActionException(cant_save_json_error) - finally: - _lock.release() + with _lock: + try: + with open(json_save_path, "w+", encoding="utf-8") as file_to_write: + json.dump(action_json, file_to_write, indent=4, ensure_ascii=False) + except Exception as error: + raise AutoControlJsonActionException(f"{cant_save_json_error_message}: {repr(error)}") \ No newline at end of file diff --git a/je_auto_control/utils/logging/loggin_instance.py b/je_auto_control/utils/logging/loggin_instance.py index 80288ea..500f280 100644 --- a/je_auto_control/utils/logging/loggin_instance.py +++ b/je_auto_control/utils/logging/loggin_instance.py @@ -1,25 +1,50 @@ import logging from logging.handlers import RotatingFileHandler +# 設定 root logger 等級 Set root logger level logging.root.setLevel(logging.DEBUG) -autocontrol_logger = logging.getLogger("AutoControlGUI") -formatter = logging.Formatter('%(asctime)s | %(name)s | %(levelname)s | %(message)s') +# 建立 AutoControlGUI 專用 logger Create dedicated logger +autocontrol_logger = logging.getLogger("AutoControlGUI") -class AutoControlGUILoggingHandler(RotatingFileHandler): +# 日誌格式 Formatter +formatter = logging.Formatter( + "%(asctime)s | %(name)s | %(levelname)s | %(message)s" +) - # redirect logging stderr output to queue - def __init__(self, filename: str = "AutoControlGUI.log", mode="w", - maxBytes: int = 1073741824, backupCount: int = 0): - super().__init__(filename=filename, mode=mode, maxBytes=maxBytes, backupCount=backupCount) - self.formatter = formatter - self.setLevel(logging.DEBUG) +class AutoControlGUILoggingHandler(RotatingFileHandler): + """ + AutoControlGUILoggingHandler + 自訂日誌處理器,繼承 RotatingFileHandler + - 支援檔案大小輪替 + - 預設輸出到 AutoControlGUI.log + """ + + def __init__( + self, + filename: str = "AutoControlGUI.log", + mode: str = "w", + max_bytes: int = 1073741824, # 1GB + backup_count: int = 0, + ): + super().__init__( + filename=filename, + mode=mode, + maxBytes=max_bytes, + backupCount=backup_count, + ) + self.setFormatter(formatter) # 設定格式器 + self.setLevel(logging.DEBUG) # 設定等級 def emit(self, record: logging.LogRecord) -> None: + """ + Emit log record. + 輸出日誌紀錄 + """ super().emit(record) -# File handler +# 建立並加入檔案處理器 Add file handler to logger file_handler = AutoControlGUILoggingHandler() -autocontrol_logger.addHandler(file_handler) +autocontrol_logger.addHandler(file_handler) \ No newline at end of file diff --git a/je_auto_control/utils/package_manager/package_manager_class.py b/je_auto_control/utils/package_manager/package_manager_class.py index 434307c..7b7d06f 100644 --- a/je_auto_control/utils/package_manager/package_manager_class.py +++ b/je_auto_control/utils/package_manager/package_manager_class.py @@ -1,101 +1,92 @@ -from importlib import import_module +import importlib from importlib.util import find_spec from inspect import getmembers, isfunction, isbuiltin, isclass +from types import ModuleType from sys import stderr -from typing import Union, Callable, Dict +from typing import Optional from je_auto_control.utils.logging.loggin_instance import autocontrol_logger -class PackageManager(object): +class PackageManager: + """ + PackageManager + 套件管理器 + - 動態載入外部套件 + - 將套件中的函式/類別加入到 Executor 或 CallbackExecutor 的事件字典 + """ def __init__(self): - self.installed_package_dict = { - } + self.installed_package_dict: dict[str, ModuleType] = {} self.executor = None self.callback_executor = None - def check_package(self, package: str) -> Union[None, Dict[str, Callable]]: + def check_package(self, package: str) -> Optional[ModuleType]: """ - :param package: package to check exists or not - :return: package if find else None + 檢查並載入套件 + Check and import package + + :param package: 套件名稱 Package name + :return: 套件模組 ModuleType 或 None """ - if self.installed_package_dict.get(package, None) is None: + if package not in self.installed_package_dict: found_spec = find_spec(package) if found_spec is not None: try: - installed_package = import_module(found_spec.name) - self.installed_package_dict.update( - {found_spec.name: installed_package}) + installed_package = importlib.import_module(found_spec.name) + self.installed_package_dict[found_spec.name] = installed_package except ModuleNotFoundError as error: print(repr(error), file=stderr) - return self.installed_package_dict.get(package, None) + return self.installed_package_dict.get(package) - def add_package_to_executor(self, package) -> None: - autocontrol_logger.info(f"add_package_to_executor, package: {package}") + def add_package_to_executor(self, package: str) -> None: """ - :param package: package's function will add to executor + 將套件成員加入 Executor + Add package members to Executor """ - self.add_package_to_target( - package=package, - target=self.executor - ) + autocontrol_logger.info(f"add_package_to_executor, package: {package}") + self.add_package_to_target(package, self.executor) - def add_package_to_callback_executor(self, package) -> None: - autocontrol_logger.info(f"add_package_to_callback_executor, package: {package}") + def add_package_to_callback_executor(self, package: str) -> None: """ - :param package: package's function will add to callback_executor + 將套件成員加入 CallbackExecutor + Add package members to CallbackExecutor """ - self.add_package_to_target( - package=package, - target=self.callback_executor - ) + autocontrol_logger.info(f"add_package_to_callback_executor, package: {package}") + self.add_package_to_target(package, self.callback_executor) - def get_member(self, package, predicate, target) -> None: + def get_member(self, package: str, predicate, target) -> None: """ - :param package: package we want to get member - :param predicate: predicate - :param target: which event_dict will be added + 取得套件成員並加入事件字典 + Get package members and add to event_dict + + :param package: 套件名稱 Package name + :param predicate: 過濾條件 (isfunction, isbuiltin, isclass) + :param target: 目標 Executor/CallbackExecutor """ installed_package = self.check_package(package) if installed_package is not None and target is not None: for member in getmembers(installed_package, predicate): - target.event_dict.update( - {str(package) + "_" + str(member[0]): member[1]}) + target.event_dict[f"{package}_{member[0]}"] = member[1] elif installed_package is None: - print(repr(ModuleNotFoundError(f"Can't find package {package}")), - file=stderr) + print(repr(ModuleNotFoundError(f"Can't find package {package}")), file=stderr) else: print(f"Executor error {self.executor}", file=stderr) - def add_package_to_target(self, package, target) -> None: + def add_package_to_target(self, package: str, target) -> None: """ - :param package: package we want to get member - :param target: which event_dict will be added + 將套件所有成員加入目標事件字典 + Add all package members to target event_dict + + :param package: 套件名稱 Package name + :param target: 目標 Executor/CallbackExecutor """ try: - self.get_member( - package=package, - predicate=isfunction, - target=target - ) - self.get_member( - package=package, - predicate=isbuiltin, - target=target - ) - self.get_member( - package=package, - predicate=isfunction, - target=target - ) - self.get_member( - package=package, - predicate=isclass, - target=target - ) + for predicate in (isfunction, isbuiltin, isclass): + self.get_member(package, predicate, target) except Exception as error: print(repr(error), file=stderr) -package_manager = PackageManager() +# 全域 PackageManager 實例 Global instance +package_manager = PackageManager() \ No newline at end of file diff --git a/je_auto_control/utils/project/create_project_structure.py b/je_auto_control/utils/project/create_project_structure.py index ea3cfd4..585fc28 100644 --- a/je_auto_control/utils/project/create_project_structure.py +++ b/je_auto_control/utils/project/create_project_structure.py @@ -4,65 +4,90 @@ from je_auto_control.utils.json.json_file import write_action_json from je_auto_control.utils.logging.loggin_instance import autocontrol_logger -from je_auto_control.utils.project.template.template_executor import executor_template_1, \ - executor_template_2, bad_executor_template_1 -from je_auto_control.utils.project.template.template_keyword import template_keyword_1, \ - template_keyword_2, bad_template_1 +from je_auto_control.utils.project.template.template_executor import ( + executor_template_1, executor_template_2, bad_executor_template_1 +) +from je_auto_control.utils.project.template.template_keyword import ( + template_keyword_1, template_keyword_2, bad_template_1 +) def create_dir(dir_name: str) -> None: """ - :param dir_name: create dir use dir name - :return: None + Create directory if not exists. + 建立目錄 (若不存在則建立) + + :param dir_name: 目錄名稱 Directory name """ - Path(dir_name).mkdir( - parents=True, - exist_ok=True - ) + Path(dir_name).mkdir(parents=True, exist_ok=True) + + +def _write_file(file_path: Path, content: str) -> None: + """ + Write content to file. + 將內容寫入檔案 + + :param file_path: 檔案路徑 File path + :param content: 要寫入的內容 Content to write + """ + with open(file_path, "w+", encoding="utf-8") as file: + file.write(content) def create_template(parent_name: str, project_path: str = None) -> None: + """ + Create template files in keyword and executor directories. + 在 keyword 與 executor 目錄中建立範例模板檔案 + + :param parent_name: 專案主目錄名稱 Project parent directory name + :param project_path: 專案路徑 Project path (預設為當前工作目錄) + """ if project_path is None: project_path = getcwd() - keyword_dir_path = Path(project_path + "/" + parent_name + "/keyword") - executor_dir_path = Path(project_path + "/" + parent_name + "/executor") + + keyword_dir_path = Path(project_path) / parent_name / "keyword" + executor_dir_path = Path(project_path) / parent_name / "executor" lock = Lock() + + # 建立 keyword JSON 檔案 Create keyword JSON files if keyword_dir_path.exists() and keyword_dir_path.is_dir(): - write_action_json(project_path + "/" + parent_name + "/keyword/keyword1.json", template_keyword_1) - write_action_json(project_path + "/" + parent_name + "/keyword/keyword2.json", template_keyword_2) - write_action_json(project_path + "/" + parent_name + "/keyword/bad_keyword_1.json", bad_template_1) - if executor_dir_path.exists() and keyword_dir_path.is_dir(): - lock.acquire() - try: - with open(project_path + "/" + parent_name + "/executor/executor_one_file.py", "w+") as file: - file.write( - executor_template_1.replace( - "{temp}", - project_path + "/" + parent_name + "/keyword/keyword1.json" - ) - ) - with open(project_path + "/" + parent_name + "/executor/executor_bad_file.py", "w+") as file: - file.write( - bad_executor_template_1.replace( - "{temp}", - project_path + "/" + parent_name + "/keyword/bad_keyword_1.json" - ) - ) - with open(project_path + "/" + parent_name + "/executor/executor_folder.py", "w+") as file: - file.write( - executor_template_2.replace( - "{temp}", - project_path + "/" + parent_name + "/keyword" - ) - ) - finally: - lock.release() + write_action_json(str(keyword_dir_path) + "keyword1.json", template_keyword_1) + write_action_json(str(keyword_dir_path) + "keyword2.json", template_keyword_2) + write_action_json(str(keyword_dir_path) + "bad_keyword_1.json", bad_template_1) + + # 建立 executor Python 檔案 Create executor Python files + if executor_dir_path.exists() and executor_dir_path.is_dir(): + with lock: + _write_file( + executor_dir_path / "executor_one_file.py", + executor_template_1.replace("{temp}", str(keyword_dir_path / "keyword1.json")) + ) + _write_file( + executor_dir_path / "executor_bad_file.py", + bad_executor_template_1.replace("{temp}", str(keyword_dir_path / "bad_keyword_1.json")) + ) + _write_file( + executor_dir_path / "executor_folder.py", + executor_template_2.replace("{temp}", str(keyword_dir_path)) + ) def create_project_dir(project_path: str = None, parent_name: str = "AutoControl") -> None: + """ + Create project directory structure and templates. + 建立專案目錄結構並生成範例模板檔案 + + :param project_path: 專案路徑 Project path (預設為當前工作目錄) + :param parent_name: 專案主目錄名稱 Project parent directory name + """ autocontrol_logger.info(f"create_project_dir, project_path: {project_path}, parent_name: {parent_name}") + if project_path is None: project_path = getcwd() - create_dir(project_path + "/" + parent_name + "/keyword") - create_dir(project_path + "/" + parent_name + "/executor") - create_template(parent_name) + + # 建立 keyword 與 executor 子目錄 Create keyword and executor subdirectories + create_dir(str(Path(project_path)) + parent_name + "keyword") + create_dir(str(Path(project_path)) + parent_name + "executor") + + # 建立範例模板檔案 Create template files + create_template(parent_name, project_path) \ No newline at end of file diff --git a/je_auto_control/utils/shell_process/shell_exec.py b/je_auto_control/utils/shell_process/shell_exec.py index e7a130f..c30ae5f 100644 --- a/je_auto_control/utils/shell_process/shell_exec.py +++ b/je_auto_control/utils/shell_process/shell_exec.py @@ -8,20 +8,23 @@ from je_auto_control.utils.logging.loggin_instance import autocontrol_logger -class ShellManager(object): +class ShellManager: + """ + ShellManager + Shell 指令管理器 + - 執行外部 shell 指令 + - 使用背景執行緒持續讀取 stdout / stderr + - 將輸出放入 queue,供 pull_text() 取出 + """ - def __init__( - self, - shell_encoding: str = "utf-8", - program_buffer: int = 10240000, - ): + def __init__(self, shell_encoding: str = "utf-8", program_buffer: int = 10240000): """ :param shell_encoding: shell command read output encoding :param program_buffer: buffer size """ - self.read_program_error_output_from_thread = None - self.read_program_output_from_thread = None - self.still_run_shell: bool = True + self.read_program_error_output_from_thread: Union[Thread, None] = None + self.read_program_output_from_thread: Union[Thread, None] = None + self.still_run_shell: bool = False self.process: Union[subprocess.Popen, None] = None self.run_output_queue: queue.Queue = queue.Queue() self.run_error_queue: queue.Queue = queue.Queue() @@ -30,103 +33,112 @@ def __init__( def exec_shell(self, shell_command: Union[str, list]) -> None: """ - :param shell_command: shell command will run - :return: if error return result and True else return result and False + Execute shell command. + 執行 shell 指令 """ autocontrol_logger.info(f"exec_shell, shell_command: {shell_command}") try: self.exit_program() + if sys.platform in ["win32", "cygwin", "msys"]: - args = shell_command + args = shell_command if isinstance(shell_command, str) else " ".join(shell_command) + self.process = subprocess.Popen( + args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True, + ) else: - args = shlex.split(shell_command) - self.process = subprocess.Popen( - args=args, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - shell=True, - ) + args = shlex.split(shell_command) if isinstance(shell_command, str) else shell_command + self.process = subprocess.Popen( + args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=False, + ) + self.still_run_shell = True - # program output message queue thread + + # stdout thread self.read_program_output_from_thread = Thread( - target=self.read_program_output_from_process, + target=self._read_stream, + args=(self.process.stdout, self.run_output_queue), daemon=True ) self.read_program_output_from_thread.start() - # program error message queue thread + + # stderr thread self.read_program_error_output_from_thread = Thread( - target=self.read_program_error_output_from_process, + target=self._read_stream, + args=(self.process.stderr, self.run_error_queue), daemon=True ) self.read_program_error_output_from_thread.start() + except Exception as error: - autocontrol_logger.error( - f"exec_shell, shell_command: {shell_command}, failed: {repr(error)}") + autocontrol_logger.error(f"exec_shell failed, shell_command: {shell_command}, error: {repr(error)}") - # tkinter_ui update method def pull_text(self) -> None: + """ + Pull text from queues and print. + 從 queue 取出訊息並輸出 + """ try: - if not self.run_error_queue.empty(): - error_message = self.run_error_queue.get_nowait() - error_message = str(error_message).strip() + while not self.run_error_queue.empty(): + error_message = self.run_error_queue.get_nowait().strip() if error_message: print(error_message, file=sys.stderr) - if not self.run_output_queue.empty(): - output_message = self.run_output_queue.get_nowait() - output_message = str(output_message).strip() + + while not self.run_output_queue.empty(): + output_message = self.run_output_queue.get_nowait().strip() if output_message: print(output_message) + except queue.Empty: pass - if self.process.returncode == 0: - self.exit_program() - elif self.process.returncode is not None: + + if self.process and self.process.poll() is not None: self.exit_program() - if self.still_run_shell: - # poll return code - self.process.poll() - # exit program change run flag to false and clean read thread and queue and process def exit_program(self) -> None: + """ + Exit program and clean resources. + 結束程式並清理資源 + """ self.still_run_shell = False - if self.read_program_output_from_thread is not None: - self.read_program_output_from_thread = None - if self.read_program_error_output_from_thread is not None: - self.read_program_error_output_from_thread = None - self.print_and_clear_queue() + if self.process is not None: self.process.terminate() print(f"Shell command exit with code {self.process.returncode}") self.process = None + self.print_and_clear_queue() + def print_and_clear_queue(self) -> None: - try: - for std_output in iter(self.run_output_queue.get_nowait, None): - std_output = str(std_output).strip() - if std_output: - print(std_output) - for std_err in iter(self.run_error_queue.get_nowait, None): - std_err = str(std_err).strip() - if std_err: - print(std_err, file=sys.stderr) - except queue.Empty: - pass + """ + Print and clear queues. + 輸出並清空 queue + """ + while not self.run_output_queue.empty(): + print(self.run_output_queue.get_nowait().strip()) + + while not self.run_error_queue.empty(): + print(self.run_error_queue.get_nowait().strip(), file=sys.stderr) + self.run_output_queue = queue.Queue() self.run_error_queue = queue.Queue() - def read_program_output_from_process(self) -> None: - while self.still_run_shell: - program_output_data = self.process.stdout.readline( - self.program_buffer) \ - .decode(self.program_encoding, "replace") - self.run_output_queue.put_nowait(program_output_data) - - def read_program_error_output_from_process(self) -> None: - while self.still_run_shell: - program_error_output_data = self.process.stderr.readline( - self.program_buffer) \ - .decode(self.program_encoding, "replace") - self.run_error_queue.put_nowait(program_error_output_data) + def _read_stream(self, stream, target_queue: queue.Queue) -> None: + """ + Read stream line by line and put into queue. + 讀取輸出流並放入 queue + """ + while self.still_run_shell and stream: + line = stream.readline(self.program_buffer) + if not line: + break + target_queue.put_nowait(line.decode(self.program_encoding, "replace")) -default_shell_manager = ShellManager() +# 預設 ShellManager 實例 Default instance +default_shell_manager = ShellManager() \ No newline at end of file diff --git a/je_auto_control/utils/start_exe/start_another_process.py b/je_auto_control/utils/start_exe/start_another_process.py index da8ff76..12002d8 100644 --- a/je_auto_control/utils/start_exe/start_another_process.py +++ b/je_auto_control/utils/start_exe/start_another_process.py @@ -1,18 +1,35 @@ from pathlib import Path -from je_auto_control.utils.exception.exception_tags import can_not_find_file +from je_auto_control.utils.exception.exception_tags import can_not_find_file_error_message from je_auto_control.utils.exception.exceptions import AutoControlException from je_auto_control.utils.logging.loggin_instance import autocontrol_logger from je_auto_control.utils.shell_process.shell_exec import ShellManager def start_exe(exe_path: str) -> None: - autocontrol_logger.info(f"start_another_process.py start_exe, exe_path: {exe_path}") - exe_path = Path(exe_path) - if exe_path.exists() and exe_path.is_file(): - process_manager = ShellManager() - process_manager.exec_shell(str(exe_path)) + """ + Start an external executable file. + 啟動外部可執行檔 + + :param exe_path: 可執行檔路徑 Path to executable file + :raises AutoControlException: 當檔案不存在或不是檔案時拋出例外 + """ + autocontrol_logger.info(f"start_exe, exe_path: {exe_path}") + + exe_path_obj = Path(exe_path) + + if exe_path_obj.exists() and exe_path_obj.is_file(): + try: + process_manager = ShellManager() + process_manager.exec_shell(str(exe_path_obj)) + autocontrol_logger.info(f"Successfully started executable: {exe_path_obj}") + except Exception as error: + autocontrol_logger.error( + f"start_exe, exe_path: {exe_path_obj}, exec_shell failed: {repr(error)}" + ) + raise AutoControlException(f"Failed to execute {exe_path_obj}: {repr(error)}") else: autocontrol_logger.error( - f"start_exe, exe_path: {exe_path}, failed: {AutoControlException(can_not_find_file)}") - raise AutoControlException(can_not_find_file) + f"start_exe, exe_path: {exe_path_obj}, failed: {AutoControlException(can_not_find_file_error_message)}" + ) + raise AutoControlException(can_not_find_file_error_message) \ No newline at end of file diff --git a/je_auto_control/utils/test_record/record_test_class.py b/je_auto_control/utils/test_record/record_test_class.py index b53d5d4..bdb9338 100644 --- a/je_auto_control/utils/test_record/record_test_class.py +++ b/je_auto_control/utils/test_record/record_test_class.py @@ -1,34 +1,64 @@ import datetime - from je_auto_control.utils.logging.loggin_instance import autocontrol_logger -class TestRecord(object): +class TestRecord: + """ + TestRecord + 測試紀錄管理類別 + - 控制是否啟用紀錄 + - 儲存測試紀錄清單 + """ def __init__(self, init_record: bool = False): + """ + 初始化 TestRecord + Initialize TestRecord + + :param init_record: 是否啟用紀錄 Flag to enable recording + """ self.init_record: bool = init_record - self.test_record_list: list = list() + self.test_record_list: list[dict] = [] def clean_record(self) -> None: - self.test_record_list = list() + """ + 清空紀錄 + Clear all records + """ + self.test_record_list = [] + + def set_record_enable(self, set_enable: bool = True) -> None: + """ + 設定是否啟用紀錄 + Enable or disable recording - def set_record_enable(self, set_enable: bool = True): + :param set_enable: True = 啟用, False = 停用 + """ autocontrol_logger.info(f"set_record_enable, set_enable: {set_enable}") self.init_record = set_enable +# 全域測試紀錄實例 Global test record instance test_record_instance = TestRecord() def record_action_to_list(function_name: str, local_param, program_exception: str = None) -> None: + """ + 將動作紀錄加入清單 + Record action to list + + :param function_name: 函式名稱 Function name + :param local_param: 執行參數 Local parameters + :param program_exception: 例外訊息 Exception message (預設 None) + """ if not test_record_instance.init_record: - pass - else: - test_record_instance.test_record_list.append( - { - "function_name": function_name, - "local_param": local_param, - "time": str(datetime.datetime.now()), - "program_exception": repr(program_exception) - } - ) + return + + test_record_instance.test_record_list.append( + { + "function_name": function_name, + "local_param": local_param, + "time": datetime.datetime.now().isoformat(), # 使用 ISO 格式更標準 + "program_exception": repr(program_exception), + } + ) \ No newline at end of file diff --git a/je_auto_control/utils/timeout/__init__.py b/je_auto_control/utils/timeout/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/je_auto_control/utils/timeout/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/je_auto_control/utils/timeout/multiprocess_timeout.py b/je_auto_control/utils/timeout/multiprocess_timeout.py deleted file mode 100644 index bdde3fd..0000000 --- a/je_auto_control/utils/timeout/multiprocess_timeout.py +++ /dev/null @@ -1,18 +0,0 @@ -from multiprocessing import Process - -from je_auto_control.utils.exception.exception_tags import timeout_need_on_main_error -from je_auto_control.utils.exception.exceptions import AutoControlTimeoutException - - -def multiprocess_timeout(check_function, time: int) -> str: - try: - new_process: Process = Process(target=check_function) - new_process.start() - new_process.join(timeout=time) - except AutoControlTimeoutException: - raise AutoControlTimeoutException(timeout_need_on_main_error) - new_process.terminate() - if new_process.exitcode is None: - return "timeout" - else: - return "success" diff --git a/je_auto_control/utils/xml/change_xml_structure/change_xml_structure.py b/je_auto_control/utils/xml/change_xml_structure/change_xml_structure.py index fcb33ab..6b9b293 100644 --- a/je_auto_control/utils/xml/change_xml_structure/change_xml_structure.py +++ b/je_auto_control/utils/xml/change_xml_structure/change_xml_structure.py @@ -1,23 +1,39 @@ from collections import defaultdict from xml.etree import ElementTree +from typing import Union, Dict, Any -def elements_tree_to_dict(elements_tree): +def elements_tree_to_dict(elements_tree: ElementTree.Element) -> Dict[str, Any]: """ - :param elements_tree: full xml string - :return: xml str to dict + Convert XML ElementTree to dictionary. + 將 XML ElementTree 轉換成 Python dict + + :param elements_tree: XML Element + :return: dict representation of XML """ elements_dict: dict = {elements_tree.tag: {} if elements_tree.attrib else None} - children: list = list(elements_tree) + children = list(elements_tree) + + # 遞迴處理子節點 Recursively process children if children: default_dict = defaultdict(list) for dc in map(elements_tree_to_dict, children): for key, value in dc.items(): default_dict[key].append(value) - elements_dict: dict = { - elements_tree.tag: {key: value[0] if len(value) == 1 else value for key, value in default_dict.items()}} + elements_dict = { + elements_tree.tag: { + key: value[0] if len(value) == 1 else value + for key, value in default_dict.items() + } + } + + # 加入屬性 Add attributes if elements_tree.attrib: - elements_dict[elements_tree.tag].update(('@' + key, value) for key, value in elements_tree.attrib.items()) + elements_dict[elements_tree.tag].update( + ('@' + key, value) for key, value in elements_tree.attrib.items() + ) + + # 加入文字內容 Add text content if elements_tree.text: text = elements_tree.text.strip() if children or elements_tree.attrib: @@ -25,37 +41,45 @@ def elements_tree_to_dict(elements_tree): elements_dict[elements_tree.tag]['#text'] = text else: elements_dict[elements_tree.tag] = text + return elements_dict -def dict_to_elements_tree(json_dict: dict): +def dict_to_elements_tree(json_dict: Dict[str, Any]) -> str: """ - :param json_dict: json dict - :return: json dict to xml string + Convert dictionary to XML string. + 將 Python dict 轉換成 XML 字串 + + :param json_dict: dict representation of XML + :return: XML string """ - def _to_elements_tree(json_dict: dict, root): + def _to_elements_tree(json_dict: Any, root: ElementTree.Element) -> None: if isinstance(json_dict, str): root.text = json_dict elif isinstance(json_dict, dict): for key, value in json_dict.items(): assert isinstance(key, str) if key.startswith('#'): + # 處理文字節點 Handle text node assert key == '#text' and isinstance(value, str) root.text = value elif key.startswith('@'): + # 處理屬性 Handle attributes assert isinstance(value, str) root.set(key[1:], value) elif isinstance(value, list): - for elements in value: - _to_elements_tree(elements, ElementTree.SubElement(root, key)) + # 處理多個子節點 Handle multiple children + for element in value: + _to_elements_tree(element, ElementTree.SubElement(root, key)) else: + # 處理單一子節點 Handle single child _to_elements_tree(value, ElementTree.SubElement(root, key)) else: - raise TypeError('invalid type: ' + str(type(json_dict))) + raise TypeError(f"Invalid type: {type(json_dict)}") assert isinstance(json_dict, dict) and len(json_dict) == 1 tag, body = next(iter(json_dict.items())) node = ElementTree.Element(tag) _to_elements_tree(body, node) - return str(ElementTree.tostring(node), encoding="utf-8") + return ElementTree.tostring(node, encoding="utf-8").decode("utf-8") \ No newline at end of file diff --git a/je_auto_control/utils/xml/xml_file/xml_file.py b/je_auto_control/utils/xml/xml_file/xml_file.py index 9b07a90..c2a914f 100644 --- a/je_auto_control/utils/xml/xml_file/xml_file.py +++ b/je_auto_control/utils/xml/xml_file/xml_file.py @@ -1,67 +1,92 @@ import xml.dom.minidom from xml.etree import ElementTree +from xml.etree.ElementTree import ParseError -from je_auto_control.utils.exception.exception_tags import cant_read_xml_error -from je_auto_control.utils.exception.exception_tags import xml_type_error -from je_auto_control.utils.exception.exceptions import XMLException -from je_auto_control.utils.exception.exceptions import XMLTypeException +from je_auto_control.utils.exception.exception_tags import cant_read_xml_error_message, xml_type_error_message +from je_auto_control.utils.exception.exceptions import XMLException, XMLTypeException -def reformat_xml_file(xml_string: str): +def reformat_xml_file(xml_string: str) -> str: + """ + Reformat XML string into pretty-printed format. + 將 XML 字串重新排版成漂亮格式 + + :param xml_string: 原始 XML 字串 Raw XML string + :return: 美化後的 XML 字串 Pretty XML string + """ dom = xml.dom.minidom.parseString(xml_string) - return dom.toprettyxml() + return dom.toprettyxml(indent=" ", encoding="utf-8").decode("utf-8") -class XMLParser(object): +class XMLParser: + """ + XMLParser + XML 解析器 + - 支援從字串或檔案載入 XML + - 可輸出 XML 檔案 + """ def __init__(self, xml_string: str, xml_type: str = "string"): """ - :param xml_string: full xml string - :param xml_type: file or string + Initialize XMLParser. + 初始化 XMLParser + + :param xml_string: XML 字串或檔案路徑 XML string or file path + :param xml_type: "string" 或 "file" """ - self.element_tree = ElementTree - self.tree = None - self.xml_root = None - self.xml_from_type = "string" - self.xml_string = xml_string.strip() + self.tree: ElementTree.ElementTree | None = None + self.xml_root: ElementTree.Element | None = None + self.xml_from_type: str = "string" + self.xml_string: str = xml_string.strip() + xml_type = xml_type.lower() if xml_type not in ["file", "string"]: - raise XMLTypeException(xml_type_error) + raise XMLTypeException(xml_type_error_message) + if xml_type == "string": self.xml_parser_from_string() else: self.xml_parser_from_file() - def xml_parser_from_string(self, **kwargs): + def xml_parser_from_string(self, **kwargs) -> ElementTree.Element: """ - :param kwargs: any another param - :return: xml root element tree + Parse XML from string. + 從字串解析 XML + + :return: XML root element """ try: self.xml_root = ElementTree.fromstring(self.xml_string, **kwargs) - except XMLException: - raise XMLException(cant_read_xml_error) + except ParseError as e: + raise XMLException(f"{cant_read_xml_error_message}: {repr(e)}") return self.xml_root - def xml_parser_from_file(self, **kwargs): + def xml_parser_from_file(self, **kwargs) -> ElementTree.Element: """ - :param kwargs: any another param - :return: xml root element tree + Parse XML from file. + 從檔案解析 XML + + :return: XML root element """ try: self.tree = ElementTree.parse(self.xml_string, **kwargs) - except XMLException: - raise XMLException(cant_read_xml_error) + except (OSError, ParseError) as e: + raise XMLException(f"{cant_read_xml_error_message}: {repr(e)}") self.xml_root = self.tree.getroot() self.xml_from_type = "file" return self.xml_root - def write_xml(self, write_xml_filename: str, write_content: str): + def write_xml(self, write_xml_filename: str, write_content: str) -> None: """ - :param write_xml_filename: xml file name - :param write_content: content to write + Write XML content to file. + 將 XML 內容寫入檔案 + + :param write_xml_filename: 輸出檔案名稱 Output file name + :param write_content: XML 字串 XML string """ - write_content = write_content.strip() - content = self.element_tree.fromstring(write_content) - tree = self.element_tree.ElementTree(content) - tree.write(write_xml_filename, encoding="utf-8") + try: + content = ElementTree.fromstring(write_content.strip()) + tree = ElementTree.ElementTree(content) + tree.write(write_xml_filename, encoding="utf-8", xml_declaration=True) + except ParseError as e: + raise XMLException(f"{cant_read_xml_error_message}: {repr(e)}") \ No newline at end of file diff --git a/je_auto_control/windows/core/utils/win32_ctype_input.py b/je_auto_control/windows/core/utils/win32_ctype_input.py index 777b380..df82249 100644 --- a/je_auto_control/windows/core/utils/win32_ctype_input.py +++ b/je_auto_control/windows/core/utils/win32_ctype_input.py @@ -1,10 +1,10 @@ import sys -from je_auto_control.utils.exception.exception_tags import windows_import_error +from je_auto_control.utils.exception.exception_tags import windows_import_error_message from je_auto_control.utils.exception.exceptions import AutoControlException if sys.platform not in ["win32", "cygwin", "msys"]: - raise AutoControlException(windows_import_error) + raise AutoControlException(windows_import_error_message) import ctypes from ctypes import wintypes diff --git a/je_auto_control/windows/core/utils/win32_keypress_check.py b/je_auto_control/windows/core/utils/win32_keypress_check.py index bceb01e..4162781 100644 --- a/je_auto_control/windows/core/utils/win32_keypress_check.py +++ b/je_auto_control/windows/core/utils/win32_keypress_check.py @@ -1,11 +1,11 @@ import sys from typing import Union -from je_auto_control.utils.exception.exception_tags import windows_import_error +from je_auto_control.utils.exception.exception_tags import windows_import_error_message from je_auto_control.utils.exception.exceptions import AutoControlException if sys.platform not in ["win32", "cygwin", "msys"]: - raise AutoControlException(windows_import_error) + raise AutoControlException(windows_import_error_message) import ctypes diff --git a/je_auto_control/windows/core/utils/win32_vk.py b/je_auto_control/windows/core/utils/win32_vk.py index 5757848..81b598f 100644 --- a/je_auto_control/windows/core/utils/win32_vk.py +++ b/je_auto_control/windows/core/utils/win32_vk.py @@ -1,10 +1,10 @@ import sys -from je_auto_control.utils.exception.exception_tags import windows_import_error +from je_auto_control.utils.exception.exception_tags import windows_import_error_message from je_auto_control.utils.exception.exceptions import AutoControlException if sys.platform not in ["win32", "cygwin", "msys"]: - raise AutoControlException(windows_import_error) + raise AutoControlException(windows_import_error_message) # windows mouse virtual keycode diff --git a/je_auto_control/windows/keyboard/win32_ctype_keyboard_control.py b/je_auto_control/windows/keyboard/win32_ctype_keyboard_control.py index 72031e6..8aac4b6 100644 --- a/je_auto_control/windows/keyboard/win32_ctype_keyboard_control.py +++ b/je_auto_control/windows/keyboard/win32_ctype_keyboard_control.py @@ -1,10 +1,11 @@ import sys -from je_auto_control.utils.exception.exception_tags import windows_import_error +from je_auto_control.utils.exception.exception_tags import windows_import_error_message from je_auto_control.utils.exception.exceptions import AutoControlException +# 僅允許在 Windows 平台使用 Only allow on Windows platform if sys.platform not in ["win32", "cygwin", "msys"]: - raise AutoControlException(windows_import_error) + raise AutoControlException(windows_import_error_message) from je_auto_control.windows.core.utils.win32_ctype_input import Input, user32 from je_auto_control.windows.core.utils.win32_ctype_input import Keyboard @@ -16,7 +17,10 @@ def press_key(keycode: int) -> None: """ - :param keycode which keycode we want to press + 模擬按下鍵盤按鍵 + Simulate pressing a key + + :param keycode: 鍵盤虛擬鍵碼 Virtual key code """ keyboard = Input(type=Keyboard, ki=KeyboardInput(wVk=keycode)) SendInput(1, ctypes.byref(keyboard), ctypes.sizeof(keyboard)) @@ -24,14 +28,28 @@ def press_key(keycode: int) -> None: def release_key(keycode: int) -> None: """ - :param keycode which keycode we want to release + 模擬放開鍵盤按鍵 + Simulate releasing a key + + :param keycode: 鍵盤虛擬鍵碼 Virtual key code """ keyboard = Input(type=Keyboard, ki=KeyboardInput(wVk=keycode, dwFlags=WIN32_EventF_KEYUP)) SendInput(1, ctypes.byref(keyboard), ctypes.sizeof(keyboard)) -def send_key_event_to_window(window: str, keycode: int): + +def send_key_event_to_window(window: str, keycode: int) -> None: + """ + 將鍵盤事件送到指定視窗 + Send key event to a specific window + + :param window: 視窗標題 Window title + :param keycode: 鍵盤虛擬鍵碼 Virtual key code + """ WM_KEYDOWN = 0x0100 WM_KEYUP = 0x0101 - window = user32.FindWindowW(None, window) - user32.PostMessageW(window, WM_KEYDOWN, keycode, 0) - user32.PostMessageW(window, WM_KEYUP, keycode, 0) + hwnd = user32.FindWindowW(None, window) + if hwnd: + user32.PostMessageW(hwnd, WM_KEYDOWN, keycode, 0) + user32.PostMessageW(hwnd, WM_KEYUP, keycode, 0) + else: + raise AutoControlException(f"Window '{window}' not found") \ No newline at end of file diff --git a/je_auto_control/windows/listener/win32_keyboard_listener.py b/je_auto_control/windows/listener/win32_keyboard_listener.py index 8efa749..1c76f64 100644 --- a/je_auto_control/windows/listener/win32_keyboard_listener.py +++ b/je_auto_control/windows/listener/win32_keyboard_listener.py @@ -1,79 +1,118 @@ import sys - -from je_auto_control.utils.exception.exception_tags import windows_import_error -from je_auto_control.utils.exception.exceptions import AutoControlException - -if sys.platform not in ["win32", "cygwin", "msys"]: - raise AutoControlException(windows_import_error) - from ctypes import windll, WINFUNCTYPE, c_int, POINTER, c_void_p, byref from ctypes.wintypes import MSG - from threading import Thread - from queue import Queue +from typing import Optional -_user32: windll.user32 = windll.user32 -_kernel32: windll.kernel32 = windll.kernel32 +from je_auto_control.utils.exception.exception_tags import windows_import_error_message +from je_auto_control.utils.exception.exceptions import AutoControlException + +# 僅允許在 Windows 平台使用 Only allow on Windows platform +if sys.platform not in ["win32", "cygwin", "msys"]: + raise AutoControlException(windows_import_error_message) + +_user32 = windll.user32 +_kernel32 = windll.kernel32 _wm_keydown: int = 0x100 def _get_function_pointer(function) -> WINFUNCTYPE: + """ + 將 Python 函式轉換成 Win32 API 可用的函式指標 + Convert Python function to Win32-compatible function pointer + """ win_function = WINFUNCTYPE(c_int, c_int, c_int, POINTER(c_void_p)) return win_function(function) class Win32KeyboardListener(Thread): + """ + Win32KeyboardListener + Windows 鍵盤事件監聽器 + - 使用 SetWindowsHookExA 設置鍵盤 hook + - 將鍵盤事件記錄到 Queue + """ def __init__(self): super().__init__() self.daemon = True - self.hooked: [None, int] = None - self.record_queue: [None, Queue] = None + self.hooked: Optional[int] = None + self.record_queue: Optional[Queue] = None self.record_flag: bool = False - self.hook_event_code_int: int = 13 + self.hook_event_code_int: int = 13 # WH_KEYBOARD_LL def _set_win32_hook(self, point) -> bool: + """ + 設置鍵盤 hook + Set keyboard hook + """ self.hooked = _user32.SetWindowsHookExA( self.hook_event_code_int, point, 0, 0 ) - if not self.hooked: - return False - return True + return bool(self.hooked) def _remove_win32_hook_proc(self) -> None: - if self.hooked is None: - return - _user32.UnhookWindowsHookEx(self.hooked) - self.hooked = None - - def _win32_hook_proc(self, code, w_param, l_param) -> _user32.CallNextHookEx: - if w_param is not _wm_keydown: + """ + 移除鍵盤 hook + Remove keyboard hook + """ + if self.hooked: + _user32.UnhookWindowsHookEx(self.hooked) + self.hooked = None + + def _win32_hook_proc(self, code, w_param, l_param): + """ + 鍵盤事件處理函式 + Keyboard hook procedure + """ + if w_param != _wm_keydown: return _user32.CallNextHookEx(self.hooked, code, w_param, l_param) - if self.record_flag is True: - # int to hex + + if self.record_flag and self.record_queue is not None: + # 將 l_param 轉換成 keycode temp = hex(l_param[0] & 0xFFFFFFFF) self.record_queue.put(("AC_type_keyboard", int(temp, 16))) + return _user32.CallNextHookEx(self.hooked, code, w_param, l_param) def _start_listener(self) -> None: + """ + 啟動鍵盤監聽 + Start keyboard listener + """ pointer = _get_function_pointer(self._win32_hook_proc) - self._set_win32_hook(pointer) + if not self._set_win32_hook(pointer): + raise AutoControlException("Failed to set keyboard hook") + message = MSG() + # 進入訊息迴圈 Enter message loop _user32.GetMessageA(byref(message), 0, 0, 0) - def record(self, want_to_record_queue) -> None: + def record(self, want_to_record_queue: Queue) -> None: + """ + 開始紀錄鍵盤事件 + Start recording keyboard events + """ self.record_flag = True self.record_queue = want_to_record_queue self.start() def stop_record(self) -> Queue: + """ + 停止紀錄並移除 hook + Stop recording and remove hook + """ self.record_flag = False self._remove_win32_hook_proc() return self.record_queue def run(self) -> None: - self._start_listener() + """ + Thread 執行入口 + Thread run entry + """ + self._start_listener() \ No newline at end of file diff --git a/je_auto_control/windows/listener/win32_mouse_listener.py b/je_auto_control/windows/listener/win32_mouse_listener.py index 03ddddb..5ed3da1 100644 --- a/je_auto_control/windows/listener/win32_mouse_listener.py +++ b/je_auto_control/windows/listener/win32_mouse_listener.py @@ -1,89 +1,127 @@ import sys - -from je_auto_control.utils.exception.exception_tags import windows_import_error -from je_auto_control.utils.exception.exceptions import AutoControlException - -if sys.platform not in ["win32", "cygwin", "msys"]: - raise AutoControlException(windows_import_error) - from ctypes import windll, WINFUNCTYPE, c_int, POINTER, c_void_p, byref from ctypes.wintypes import MSG - from threading import Thread - from queue import Queue +from typing import Optional +from je_auto_control.utils.exception.exception_tags import windows_import_error_message +from je_auto_control.utils.exception.exceptions import AutoControlException from je_auto_control.windows.mouse.win32_ctype_mouse_control import position -_user32: windll.user32 = windll.user32 -_kernel32: windll.kernel32 = windll.kernel32 -# Left mouse button down 0x0201 -# Right mouse button down 0x0204 -# Middle mouse button down 0x0207 -_wm_mouse_key_code: list = [0x0201, 0x0204, 0x0207] +# 僅允許在 Windows 平台使用 Only allow on Windows platform +if sys.platform not in ["win32", "cygwin", "msys"]: + raise AutoControlException(windows_import_error_message) + +_user32 = windll.user32 +_kernel32 = windll.kernel32 + +# 滑鼠按鍵事件 Mouse button events +WM_LBUTTONDOWN = 0x0201 +WM_RBUTTONDOWN = 0x0204 +WM_MBUTTONDOWN = 0x0207 +_wm_mouse_key_code = [WM_LBUTTONDOWN, WM_RBUTTONDOWN, WM_MBUTTONDOWN] def _get_function_pointer(function) -> WINFUNCTYPE: + """ + 將 Python 函式轉換成 Win32 API 可用的函式指標 + Convert Python function to Win32-compatible function pointer + """ win_function = WINFUNCTYPE(c_int, c_int, c_int, POINTER(c_void_p)) return win_function(function) class Win32MouseListener(Thread): + """ + Win32MouseListener + Windows 滑鼠事件監聽器 + - 使用 SetWindowsHookExA 設置滑鼠 hook + - 將滑鼠事件記錄到 Queue + """ def __init__(self): super().__init__() self.daemon = True - self.hooked: [None, int] = None - self.record_queue: [None, Queue] = None + self.hooked: Optional[int] = None + self.record_queue: Optional[Queue] = None self.record_flag: bool = False - self.hook_event_code_int: int = 14 + self.hook_event_code_int: int = 14 # WH_MOUSE_LL def _set_win32_hook(self, point) -> bool: + """ + 設置滑鼠 hook + Set mouse hook + """ self.hooked = _user32.SetWindowsHookExA( self.hook_event_code_int, point, 0, 0 ) - if not self.hooked: - return False - return True + return bool(self.hooked) def _remove_win32_hook_proc(self) -> None: - if self.hooked is None: - return - _user32.UnhookWindowsHookEx(self.hooked) - self.hooked = None - - def _win32_hook_proc(self, code, w_param, l_param) -> _user32.CallNextHookEx: + """ + 移除滑鼠 hook + Remove mouse hook + """ + if self.hooked: + _user32.UnhookWindowsHookEx(self.hooked) + self.hooked = None + + def _win32_hook_proc(self, code, w_param, l_param): + """ + 滑鼠事件處理函式 + Mouse hook procedure + """ if w_param not in _wm_mouse_key_code: return _user32.CallNextHookEx(self.hooked, code, w_param, l_param) - if w_param == _wm_mouse_key_code[0] and self.record_flag is True: - x, y = position() - self.record_queue.put(("AC_mouse_left", x, y)) - elif w_param == _wm_mouse_key_code[1] and self.record_flag is True: - x, y = position() - self.record_queue.put(("AC_mouse_right", x, y)) - elif w_param == _wm_mouse_key_code[2] and self.record_flag is True: + + if self.record_flag and self.record_queue is not None: x, y = position() - self.record_queue.put(("AC_mouse_middle", x, y)) + if w_param == WM_LBUTTONDOWN: + self.record_queue.put(("AC_mouse_left", x, y)) + elif w_param == WM_RBUTTONDOWN: + self.record_queue.put(("AC_mouse_right", x, y)) + elif w_param == WM_MBUTTONDOWN: + self.record_queue.put(("AC_mouse_middle", x, y)) + return _user32.CallNextHookEx(self.hooked, code, w_param, l_param) def _start_listener(self) -> None: + """ + 啟動滑鼠監聽 + Start mouse listener + """ pointer = _get_function_pointer(self._win32_hook_proc) - self._set_win32_hook(pointer) + if not self._set_win32_hook(pointer): + raise AutoControlException("Failed to set mouse hook") + message = MSG() _user32.GetMessageA(byref(message), 0, 0, 0) - def record(self, want_to_record_queue) -> None: + def record(self, want_to_record_queue: Queue) -> None: + """ + 開始紀錄滑鼠事件 + Start recording mouse events + """ self.record_flag = True self.record_queue = want_to_record_queue self.start() def stop_record(self) -> Queue: + """ + 停止紀錄並移除 hook + Stop recording and remove hook + """ self.record_flag = False self._remove_win32_hook_proc() return self.record_queue def run(self) -> None: - self._start_listener() + """ + Thread 執行入口 + Thread run entry + """ + self._start_listener() \ No newline at end of file diff --git a/je_auto_control/windows/message/window_message.py b/je_auto_control/windows/message/window_message.py index 526d569..c2152b4 100644 --- a/je_auto_control/windows/message/window_message.py +++ b/je_auto_control/windows/message/window_message.py @@ -1,9 +1,13 @@ +from je_auto_control.utils.exception.exceptions import AutoControlException + from je_auto_control.windows.core.utils.win32_ctype_input import user32 from je_auto_control.windows.window.windows_window_manage import FindWindowW +# Win32 API 函式指標 Win32 API function pointers PostMessageW = user32.PostMessageW SendMessageW = user32.SendMessageW +# 常見的 Windows 訊息對照表 Common Windows messages messages = { "WM_ACTIVATEAPP": 0x001C, "WM_CANCELMODE": 0x001F, @@ -37,31 +41,57 @@ "WM_THEMECHANGED": 0x031A, "WM_USERCHANGED": 0x0054, "WM_WINDOWPOSCHANGED": 0x0047, - "WM_WINDOWPOSCHANGING": 0x0046 + "WM_WINDOWPOSCHANGING": 0x0046, } def send_message_to_window(window_name: str, action_message: int, key_code_1: int, key_code_2: int): - _hwnd = FindWindowW(window_name) - post_status = SendMessageW(_hwnd, action_message, key_code_1, key_code_2) - return _hwnd, post_status + """ + 使用 SendMessageW 對指定視窗名稱傳送訊息 + Send message to a window by name using SendMessageW + + :param window_name: 視窗標題 Window title + :param action_message: Windows 訊息代碼 Windows message code + :param key_code_1: wParam + :param key_code_2: lParam + :return: (HWND, 傳送狀態) + """ + hwnd = FindWindowW(window_name) + if not hwnd: + raise AutoControlException(f"Window '{window_name}' not found") + post_status = SendMessageW(hwnd, action_message, key_code_1, key_code_2) + return hwnd, post_status -def send_message_to_window_hwnd(_hwnd, action_message: int, +def send_message_to_window_hwnd(hwnd, action_message: int, key_code_1: int, key_code_2: int): - post_status = SendMessageW(_hwnd, action_message, key_code_1, key_code_2) - return _hwnd, post_status + """ + 使用 SendMessageW 對指定 HWND 傳送訊息 + Send message to a window by HWND using SendMessageW + """ + post_status = SendMessageW(hwnd, action_message, key_code_1, key_code_2) + return hwnd, post_status def post_message_to_window(window_name: str, action_message: int, key_code_1: int, key_code_2: int): - _hwnd = FindWindowW(window_name) - post_status = PostMessageW(_hwnd, action_message, key_code_1, key_code_2) - return _hwnd, post_status + """ + 使用 PostMessageW 對指定視窗名稱投遞訊息 + Post message to a window by name using PostMessageW + """ + hwnd = FindWindowW(window_name) + if not hwnd: + raise AutoControlException(f"Window '{window_name}' not found") + post_status = PostMessageW(hwnd, action_message, key_code_1, key_code_2) + return hwnd, post_status -def post_message_to_window_hwnd(_hwnd, action_message: int, +def post_message_to_window_hwnd(hwnd, action_message: int, key_code_1: int, key_code_2: int): - post_status = PostMessageW(_hwnd, action_message, key_code_1, key_code_2) - return _hwnd, post_status + """ + 使用 PostMessageW 對指定 HWND 投遞訊息 + Post message to a window by HWND using PostMessageW + """ + post_status = PostMessageW(hwnd, action_message, key_code_1, key_code_2) + return hwnd, post_status \ No newline at end of file diff --git a/je_auto_control/windows/mouse/win32_ctype_mouse_control.py b/je_auto_control/windows/mouse/win32_ctype_mouse_control.py index 67d70c3..ee11c5b 100644 --- a/je_auto_control/windows/mouse/win32_ctype_mouse_control.py +++ b/je_auto_control/windows/mouse/win32_ctype_mouse_control.py @@ -1,117 +1,153 @@ import sys -from typing import Tuple - -from je_auto_control.utils.exception.exception_tags import windows_import_error +from typing import Tuple, Optional +from ctypes import windll +from je_auto_control.utils.exception.exception_tags import windows_import_error_message from je_auto_control.utils.exception.exceptions import AutoControlException if sys.platform not in ["win32", "cygwin", "msys"]: - raise AutoControlException(windows_import_error) - -from je_auto_control.windows.core.utils.win32_ctype_input import Input, user32 -from je_auto_control.windows.core.utils.win32_vk import WIN32_LEFTDOWN -from je_auto_control.windows.core.utils.win32_vk import WIN32_LEFTUP -from je_auto_control.windows.core.utils.win32_vk import WIN32_MIDDLEDOWN -from je_auto_control.windows.core.utils.win32_vk import WIN32_MIDDLEUP -from je_auto_control.windows.core.utils.win32_ctype_input import Mouse -from je_auto_control.windows.core.utils.win32_ctype_input import MouseInput -from je_auto_control.windows.core.utils.win32_vk import WIN32_RIGHTDOWN -from je_auto_control.windows.core.utils.win32_vk import WIN32_RIGHTUP -from je_auto_control.windows.core.utils.win32_ctype_input import SendInput -from je_auto_control.windows.core.utils.win32_vk import WIN32_XBUTTON1 -from je_auto_control.windows.core.utils.win32_vk import WIN32_XBUTTON2 -from je_auto_control.windows.core.utils.win32_vk import WIN32_DOWN -from je_auto_control.windows.core.utils.win32_vk import WIN32_XUP -from ctypes import windll -from je_auto_control.windows.core.utils.win32_ctype_input import wintypes -from je_auto_control.windows.core.utils.win32_vk import WIN32_WHEEL -from je_auto_control.windows.core.utils.win32_ctype_input import ctypes + raise AutoControlException(windows_import_error_message) + +from je_auto_control.windows.core.utils.win32_ctype_input import ( + Input, user32, Mouse, MouseInput, SendInput, wintypes, ctypes +) +from je_auto_control.windows.core.utils.win32_vk import ( + WIN32_LEFTDOWN, WIN32_LEFTUP, + WIN32_MIDDLEDOWN, WIN32_MIDDLEUP, + WIN32_RIGHTDOWN, WIN32_RIGHTUP, + WIN32_XBUTTON1, WIN32_XBUTTON2, + WIN32_DOWN, WIN32_XUP, + WIN32_WHEEL +) from je_auto_control.windows.screen.win32_screen import size -win32_mouse_left: Tuple = (WIN32_LEFTUP, WIN32_LEFTDOWN, 0) -win32_mouse_middle: Tuple = (WIN32_MIDDLEUP, WIN32_MIDDLEDOWN, 0) -win32_mouse_right: Tuple = (WIN32_RIGHTUP, WIN32_RIGHTDOWN, 0) -win32_mouse_x1: Tuple = (WIN32_XUP, WIN32_DOWN, WIN32_XBUTTON1) -win32_mouse_x2: Tuple = (WIN32_XUP, WIN32_DOWN, WIN32_XBUTTON2) +# 定義滑鼠按鍵事件 Define mouse button events +win32_mouse_left: Tuple[int, int, int] = (WIN32_LEFTUP, WIN32_LEFTDOWN, 0) +win32_mouse_middle: Tuple[int, int, int] = (WIN32_MIDDLEUP, WIN32_MIDDLEDOWN, 0) +win32_mouse_right: Tuple[int, int, int] = (WIN32_RIGHTUP, WIN32_RIGHTDOWN, 0) +win32_mouse_x1: Tuple[int, int, int] = (WIN32_XUP, WIN32_DOWN, WIN32_XBUTTON1) +win32_mouse_x2: Tuple[int, int, int] = (WIN32_XUP, WIN32_DOWN, WIN32_XBUTTON2) -_get_cursor_pos: windll.user32.GetCursorPos = windll.user32.GetCursorPos -_set_cursor_pos: windll.user32.SetCursorPos = windll.user32.SetCursorPos +_get_cursor_pos = windll.user32.GetCursorPos +_set_cursor_pos = windll.user32.SetCursorPos -def mouse_event(event, x: int, y: int, dwData: int = 0) -> None: +def _convert_position(x: int, y: int) -> Tuple[int, int]: """ - :param event which event we use - :param x event x - :param y event y - :param dwData still 0 + 將螢幕座標轉換成絕對座標 + Convert screen coordinates to absolute coordinates """ width, height = size() converted_x = 65536 * x // width + 1 converted_y = 65536 * y // height + 1 - ctypes.windll.user32.mouse_event(event, ctypes.c_long(converted_x), ctypes.c_long(converted_y), dwData, 0) + return converted_x, converted_y -def position() -> tuple[int, int] | None: +def mouse_event(event: int, x: int, y: int, dwData: int = 0) -> None: """ - get mouse position + 觸發滑鼠事件 + Trigger mouse event + + :param event: 滑鼠事件代碼 Mouse event code + :param x: X 座標 X position + :param y: Y 座標 Y position + :param dwData: 滾輪數值 Wheel data + """ + converted_x, converted_y = _convert_position(x, y) + ctypes.windll.user32.mouse_event( + event, + ctypes.c_long(converted_x), + ctypes.c_long(converted_y), + dwData, + 0 + ) + + +def position() -> Optional[Tuple[int, int]]: + """ + 取得滑鼠目前位置 + Get current mouse position """ point = wintypes.POINT() if _get_cursor_pos(ctypes.byref(point)): return point.x, point.y - else: - return None + return None def set_position(x: int, y: int) -> None: """ - :param x set mouse position x - :param y set mouse position y + 設定滑鼠位置 + Set mouse position """ - pos = x, y - _set_cursor_pos(*pos) + _set_cursor_pos(x, y) -def press_mouse(press_button: int) -> None: +def press_mouse(press_button: Tuple[int, int, int]) -> None: """ - :param press_button which button we want to press + 模擬按下滑鼠按鍵 + Simulate mouse button press """ - SendInput(1, ctypes.byref( - Input(type=Mouse, _input=Input.INPUTUnion( - mi=MouseInput(dwFlags=press_button[1], mouseData=press_button[2])))), - ctypes.sizeof(Input)) + SendInput( + 1, + ctypes.byref(Input(type=Mouse, _input=Input.INPUTUnion( + mi=MouseInput(dwFlags=press_button[1], mouseData=press_button[2]) + ))), + ctypes.sizeof(Input) + ) -def release_mouse(release_button: int) -> None: +def release_mouse(release_button: Tuple[int, int, int]) -> None: """ - :param release_button which button we want to release + 模擬放開滑鼠按鍵 + Simulate mouse button release """ - SendInput(1, ctypes.byref( - Input(type=Mouse, _input=Input.INPUTUnion( - mi=MouseInput(dwFlags=release_button[0], mouseData=release_button[2])))), - ctypes.sizeof(Input)) + SendInput( + 1, + ctypes.byref(Input(type=Mouse, _input=Input.INPUTUnion( + mi=MouseInput(dwFlags=release_button[0], mouseData=release_button[2]) + ))), + ctypes.sizeof(Input) + ) -def click_mouse(mouse_keycode: int, x: int = None, y: int = None) -> None: +def click_mouse(mouse_keycode: Tuple[int, int, int], x: Optional[int] = None, y: Optional[int] = None) -> None: """ - :param mouse_keycode which mouse keycode we want to click - :param x mouse x position - :param y mouse y position + 模擬滑鼠點擊 + Simulate mouse click + + :param mouse_keycode: 滑鼠按鍵代碼 Mouse keycode tuple + :param x: X 座標 (可選) X position (optional) + :param y: Y 座標 (可選) Y position (optional) """ - if x and y is not None: + if x is not None and y is not None: set_position(x, y) press_mouse(mouse_keycode) release_mouse(mouse_keycode) -def scroll(scroll_value: int, x: int = None, y: int = None) -> None: +def scroll(scroll_value: int, x: int = 0, y: int = 0) -> None: """ - :param scroll_value scroll count - :param x scroll x - :param y scroll y + 模擬滑鼠滾輪 + Simulate mouse scroll + + :param scroll_value: 滾動數值 Scroll value + :param x: X 座標 X position + :param y: Y 座標 Y position """ mouse_event(WIN32_WHEEL, x, y, dwData=scroll_value) -def send_mouse_event_to_window(window, mouse_keycode: int, x: int = None, y: int = None): +def send_mouse_event_to_window(window, mouse_keycode: int, x: int = 0, y: int = 0): + """ + 將滑鼠事件送到指定視窗 + Send mouse event to a specific window + + :param window: 視窗 HWND Window handle + :param mouse_keycode: 滑鼠事件代碼 Mouse event code + :param x: X 座標 X position + :param y: Y 座標 Y position + """ + if window is None: + raise AutoControlException("Invalid window handle") lparam = (y << 16) | x user32.PostMessageW(window, mouse_keycode, 1, lparam) - user32.PostMessageW(window, mouse_keycode, 0, lparam) + user32.PostMessageW(window, mouse_keycode, 0, lparam) \ No newline at end of file diff --git a/je_auto_control/windows/record/win32_record.py b/je_auto_control/windows/record/win32_record.py index e88e178..989c51f 100644 --- a/je_auto_control/windows/record/win32_record.py +++ b/je_auto_control/windows/record/win32_record.py @@ -1,26 +1,36 @@ import sys +from typing import Optional +from queue import Queue -from je_auto_control.utils.exception.exception_tags import windows_import_error +from je_auto_control.utils.exception.exception_tags import windows_import_error_message from je_auto_control.utils.exception.exceptions import AutoControlException if sys.platform not in ["win32", "cygwin", "msys"]: - raise AutoControlException(windows_import_error) + raise AutoControlException(windows_import_error_message) from je_auto_control.windows.listener.win32_keyboard_listener import Win32KeyboardListener from je_auto_control.windows.listener.win32_mouse_listener import Win32MouseListener -from queue import Queue - -class Win32Recorder(object): +class Win32Recorder: + """ + Win32Recorder + Windows 錄製器 + - 可同時錄製滑鼠與鍵盤事件 + - 可選擇只錄製滑鼠或鍵盤 + """ def __init__(self): - self.mouse_record_listener: [None, Win32MouseListener] = None - self.keyboard_record_listener: [None, Win32KeyboardListener] = None - self.record_queue: [None, Queue] = None - self.result_queue: [None, Queue] = None + self.mouse_record_listener: Optional[Win32MouseListener] = None + self.keyboard_record_listener: Optional[Win32KeyboardListener] = None + self.record_queue: Optional[Queue] = None + self.result_queue: Optional[Queue] = None def record(self) -> None: + """ + 開始錄製滑鼠與鍵盤事件 + Start recording both mouse and keyboard events + """ self.mouse_record_listener = Win32MouseListener() self.keyboard_record_listener = Win32KeyboardListener() self.record_queue = Queue() @@ -28,30 +38,59 @@ def record(self) -> None: self.keyboard_record_listener.record(self.record_queue) def stop_record(self) -> Queue: - self.result_queue = self.mouse_record_listener.stop_record() - self.result_queue = self.keyboard_record_listener.stop_record() + """ + 停止錄製並回傳事件 + Stop recording and return recorded events + """ + mouse_queue = self.mouse_record_listener.stop_record() if self.mouse_record_listener else Queue() + keyboard_queue = self.keyboard_record_listener.stop_record() if self.keyboard_record_listener else Queue() + + # 合併兩個 Queue 的內容 Merge both queues + self.result_queue = Queue() + while not mouse_queue.empty(): + self.result_queue.put(mouse_queue.get()) + while not keyboard_queue.empty(): + self.result_queue.put(keyboard_queue.get()) + self.record_queue = None return self.result_queue def record_mouse(self) -> None: + """ + 開始錄製滑鼠事件 + Start recording mouse events + """ self.mouse_record_listener = Win32MouseListener() self.record_queue = Queue() self.mouse_record_listener.record(self.record_queue) def stop_record_mouse(self) -> Queue: - self.result_queue = self.mouse_record_listener.stop_record() + """ + 停止錄製滑鼠事件並回傳結果 + Stop recording mouse events and return results + """ + self.result_queue = self.mouse_record_listener.stop_record() if self.mouse_record_listener else Queue() self.record_queue = None return self.result_queue def record_keyboard(self) -> None: + """ + 開始錄製鍵盤事件 + Start recording keyboard events + """ self.keyboard_record_listener = Win32KeyboardListener() self.record_queue = Queue() self.keyboard_record_listener.record(self.record_queue) def stop_record_keyboard(self) -> Queue: - self.result_queue = self.keyboard_record_listener.stop_record() + """ + 停止錄製鍵盤事件並回傳結果 + Stop recording keyboard events and return results + """ + self.result_queue = self.keyboard_record_listener.stop_record() if self.keyboard_record_listener else Queue() self.record_queue = None return self.result_queue -win32_recorder = Win32Recorder() +# 全域錄製器實例 Global recorder instance +win32_recorder = Win32Recorder() \ No newline at end of file diff --git a/je_auto_control/windows/screen/win32_screen.py b/je_auto_control/windows/screen/win32_screen.py index 3b5737d..52a0d2d 100644 --- a/je_auto_control/windows/screen/win32_screen.py +++ b/je_auto_control/windows/screen/win32_screen.py @@ -1,34 +1,48 @@ import sys -from typing import List, Union, Tuple +from typing import List, Tuple -from je_auto_control.utils.exception.exception_tags import windows_import_error +from je_auto_control.utils.exception.exception_tags import windows_import_error_message from je_auto_control.utils.exception.exceptions import AutoControlException +# 僅允許在 Windows 平台使用 Only allow on Windows platform if sys.platform not in ["win32", "cygwin", "msys"]: - raise AutoControlException(windows_import_error) + raise AutoControlException(windows_import_error_message) import ctypes -_user32: ctypes.windll.user32 = ctypes.windll.user32 -_user32.SetProcessDPIAware() +# 初始化 Win32 API 函式 Initialize Win32 API functions +_user32 = ctypes.windll.user32 +_user32.SetProcessDPIAware() # 確保 DPI 感知,避免座標偏移 _gdi32 = ctypes.windll.gdi32 -def size() -> List[Union[int, int]]: +def size() -> List[int]: """ - get screen size + 取得螢幕大小 + Get screen size + + :return: [width, height] """ return [_user32.GetSystemMetrics(0), _user32.GetSystemMetrics(1)] def get_pixel(x: int, y: int, hwnd: int = 0) -> Tuple[int, int, int]: + """ + 取得指定座標的像素顏色 + Get pixel color at given coordinates + + :param x: X 座標 X position + :param y: Y 座標 Y position + :param hwnd: 視窗 handle (預設為桌面) Window handle (default = desktop) + :return: (R, G, B) + """ dc = _user32.GetDC(hwnd) if not dc: raise RuntimeError("GetDC failed") try: pixel = _gdi32.GetPixel(dc, x, y) - if pixel == 0xFFFFFFFF: + if pixel == 0xFFFFFFFF: # GetPixel 失敗時回傳 -1 (0xFFFFFFFF) raise RuntimeError("GetPixel failed") r = pixel & 0xFF @@ -36,5 +50,4 @@ def get_pixel(x: int, y: int, hwnd: int = 0) -> Tuple[int, int, int]: b = (pixel >> 16) & 0xFF return r, g, b finally: - _user32.ReleaseDC(hwnd, dc) - + _user32.ReleaseDC(hwnd, dc) \ No newline at end of file diff --git a/je_auto_control/windows/window/windows_window_manage.py b/je_auto_control/windows/window/windows_window_manage.py index 85c134c..b99aee9 100644 --- a/je_auto_control/windows/window/windows_window_manage.py +++ b/je_auto_control/windows/window/windows_window_manage.py @@ -1,8 +1,9 @@ from ctypes import WINFUNCTYPE, c_bool, c_int, POINTER, create_unicode_buffer -from typing import Union +from typing import Union, List, Tuple, Optional from je_auto_control.windows.core.utils.win32_ctype_input import user32 +# Win32 API 函式指標 Win32 API function pointers EnumWindows = user32.EnumWindows EnumWindowsProc = WINFUNCTYPE(c_bool, POINTER(c_int), POINTER(c_int)) GetWindowText = user32.GetWindowTextW @@ -13,10 +14,16 @@ DestroyWindow = user32.DestroyWindow -def get_all_window_hwnd(): - window_info = [] +def get_all_window_hwnd() -> List[Tuple[int, str]]: + """ + 列舉所有可見視窗 + Enumerate all visible windows - def _foreach_window(hwnd, l_param): + :return: [(hwnd, window_title), ...] + """ + window_info: List[Tuple[int, str]] = [] + + def _foreach_window(hwnd: int, l_param: int) -> bool: if IsWindowVisible(hwnd): length = GetWindowTextLength(hwnd) buff = create_unicode_buffer(length + 1) @@ -28,29 +35,56 @@ def _foreach_window(hwnd, l_param): return window_info -def get_one_window_hwnd(window_class: Union[None, str], window_name: Union[None, str]): +def get_one_window_hwnd(window_class: Optional[str], window_name: Optional[str]) -> int: + """ + 取得指定視窗的 HWND + Get window handle by class name and/or window title + """ return FindWindowW(window_class, window_name) -def close_window(hwnd) -> bool: - return CloseWindow(hwnd) +def close_window(hwnd: int) -> bool: + """ + 嘗試關閉視窗 (最小化) + Attempt to close (minimize) a window + """ + return bool(CloseWindow(hwnd)) -def destroy_window(hwnd) -> bool: - return DestroyWindow(hwnd) +def destroy_window(hwnd: int) -> bool: + """ + 銷毀視窗 + Destroy a window + """ + return bool(DestroyWindow(hwnd)) -def set_foreground_window(hwnd) -> None: +def set_foreground_window(hwnd: int) -> None: + """ + 設定視窗為前景視窗 + Set window to foreground + """ user32.SetForegroundWindow(hwnd) -def set_window_positon(hwnd, position: int) -> None: - swp_no_size = 0x0001 - swp_no_move = 0x0002 - user32.SetWindowPos(hwnd, position, 0, 0, 0, 0, swp_no_move | swp_no_size) -def show_window(hwnd, size: int) -> None: - if size < 0 or size > 3: - size = 3 - user32.ShowWindow(hwnd, size) - user32.SetForegroundWindow(hwnd) +def set_window_position(hwnd: int, position: int) -> None: + """ + 設定視窗位置 (僅改變 Z-order,不改變大小與座標) + Set window position (only Z-order, no resize or move) + """ + SWP_NO_SIZE = 0x0001 + SWP_NO_MOVE = 0x0002 + user32.SetWindowPos(hwnd, position, 0, 0, 0, 0, SWP_NO_MOVE | SWP_NO_SIZE) + + +def show_window(hwnd: int, cmd_show: int) -> None: + """ + 顯示或隱藏視窗 + Show or hide a window + :param cmd_show: Win32 ShowWindow flag (e.g., 0=Hide, 1=Normal, 2=Minimized, 3=Maximized) + """ + if cmd_show < 0 or cmd_show > 11: # Win32 ShowWindow 常見範圍 + cmd_show = 1 # 預設為 Normal + user32.ShowWindow(hwnd, cmd_show) + user32.SetForegroundWindow(hwnd) \ No newline at end of file diff --git a/je_auto_control/wrapper/auto_control_image.py b/je_auto_control/wrapper/auto_control_image.py index 222cd38..b9f7f1a 100644 --- a/je_auto_control/wrapper/auto_control_image.py +++ b/je_auto_control/wrapper/auto_control_image.py @@ -1,157 +1,103 @@ -from typing import List, Union +from typing import List, Tuple, Optional, Union from je_auto_control.utils.cv2_utils import template_detection from je_auto_control.utils.cv2_utils.screenshot import pil_screenshot -from je_auto_control.utils.exception.exception_tags import cant_find_image -from je_auto_control.utils.exception.exception_tags import find_image_error_variable +from je_auto_control.utils.exception.exception_tags import cant_find_image_error_message, find_image_error_variable_error_message from je_auto_control.utils.exception.exceptions import ImageNotFoundException from je_auto_control.utils.logging.loggin_instance import autocontrol_logger from je_auto_control.utils.test_record.record_test_class import record_action_to_list -from je_auto_control.wrapper.auto_control_mouse import click_mouse -from je_auto_control.wrapper.auto_control_mouse import set_mouse_position +from je_auto_control.wrapper.auto_control_mouse import click_mouse, set_mouse_position -def locate_all_image(image, detect_threshold: [float, int] = 1, - draw_image: bool = False) -> List[int]: +def locate_all_image(image, detect_threshold: float = 1.0, + draw_image: bool = False) -> List[List[int]]: """ - use to locate all cv2_utils that detected and then return detected images list - :param image which cv2_utils we want to find on screen (png or PIL ImageGrab.grab()) - :param detect_threshold detect precision 0.0 ~ 1.0; 1 is absolute equal (float or int) - :param draw_image draw detect tag on return cv2_utils (bool) + 找出螢幕上所有符合的影像位置 + Locate all matching images on screen + + :param image: 影像檔路徑或 PIL Image + :param detect_threshold: 偵測精度 (0.0 ~ 1.0) + :param draw_image: 是否在結果上標記 + :return: 符合影像的區域清單 [[x1, y1, x2, y2], ...] """ - autocontrol_logger.info( - f"Find multi cv2_utils {image}, with threshold {detect_threshold}" - ) - param = locals() + autocontrol_logger.info(f"Find multi images {image}, threshold={detect_threshold}") try: - try: - image_data_array = template_detection.find_image_multi(image, detect_threshold, draw_image) - except ImageNotFoundException as error: - autocontrol_logger.error( - f"Find multi cv2_utils {image}, with threshold {detect_threshold} failed. " - f"failed: {repr(find_image_error_variable + ' ' + repr(error) + ' ' + str(image))}") - raise ImageNotFoundException(find_image_error_variable + " " + repr(error) + " " + str(image)) - if image_data_array[0] is True: - record_action_to_list("locate_all_image", param) + image_data_array = template_detection.find_image_multi(image, detect_threshold, draw_image) + if image_data_array[0]: + record_action_to_list("locate_all_image", {"image": image, "threshold": detect_threshold}) return image_data_array[1] - else: - autocontrol_logger.error( - f"Find multi cv2_utils {image}, with threshold {detect_threshold} failed. " - f"failed: {repr(ImageNotFoundException(cant_find_image + ' / ' + repr(image)))}") - raise ImageNotFoundException(cant_find_image + " / " + repr(image)) + raise ImageNotFoundException(f"{cant_find_image_error_message} / {image}") except Exception as error: - record_action_to_list("locate_all_image", param, repr(error)) - autocontrol_logger.error( - f"Find multi cv2_utils {image}, with threshold {detect_threshold} failed. " - f"failed: {repr(error)}") + record_action_to_list("locate_all_image", {"image": image}, repr(error)) + autocontrol_logger.error(f"locate_all_image failed: {repr(error)}") + raise -def locate_image_center(image, detect_threshold: [float, int] = 1, draw_image: bool = False) -> List[Union[int, int]]: +def locate_image_center(image, detect_threshold: float = 1.0, + draw_image: bool = False) -> Tuple[int, int]: """ - use to locate cv2_utils and return cv2_utils center position - :param image which cv2_utils we want to find on screen (png or PIL ImageGrab.grab()) - :param detect_threshold detect precision 0.0 ~ 1.0; 1 is absolute equal (float or int) - :param draw_image draw detect tag on return cv2_utils (bool) + 找出單一影像並回傳中心座標 + Locate image and return its center position + + :return: (center_x, center_y) """ - autocontrol_logger.info( - f"Try to locate cv2_utils center {image} with threshold {detect_threshold}") - param = locals() + autocontrol_logger.info(f"Locate image center {image}, threshold={detect_threshold}") try: - try: - image_data_array = template_detection.find_image(image, detect_threshold, draw_image) - except ImageNotFoundException as error: - autocontrol_logger.error( - f"Locate cv2_utils center failed. cv2_utils: {image}, with threshold {detect_threshold}, " - f"{repr(ImageNotFoundException(find_image_error_variable + ' ' + repr(error) + ' ' + str(image)))}" - ) - raise ImageNotFoundException(find_image_error_variable + " " + repr(error) + " " + str(image)) - if image_data_array[0] is True: - height = image_data_array[1][2] - image_data_array[1][0] - width = image_data_array[1][3] - image_data_array[1][1] - center = [int(height / 2), int(width / 2)] - record_action_to_list("locate_image_center", param) - return [int(image_data_array[1][0] + center[0]), int(image_data_array[1][1] + center[1])] - else: - autocontrol_logger.error( - f"Locate cv2_utils center failed. cv2_utils: {image}, with threshold {detect_threshold}, " - f"failed: {repr(ImageNotFoundException(cant_find_image + ' / ' + repr(image)))}" - ) - raise ImageNotFoundException(cant_find_image + " / " + repr(image)) + image_data_array = template_detection.find_image(image, detect_threshold, draw_image) + if image_data_array[0]: + x1, y1, x2, y2 = image_data_array[1] + center_x = int((x1 + x2) / 2) + center_y = int((y1 + y2) / 2) + record_action_to_list("locate_image_center", {"image": image, "threshold": detect_threshold}) + return center_x, center_y + raise ImageNotFoundException(f"{cant_find_image_error_message} / {image}") except Exception as error: - record_action_to_list("locate_image_center", param, repr(error)) - autocontrol_logger.error( - f"Locate cv2_utils center failed. cv2_utils: {image}, with threshold {detect_threshold}, " - f"failed: {repr(error)}") + record_action_to_list("locate_image_center", {"image": image}, repr(error)) + autocontrol_logger.error(f"locate_image_center failed: {repr(error)}") + raise -def locate_and_click( - image, mouse_keycode: [int, str], - detect_threshold: [float, int] = 1, - draw_image: bool = False) -> List[Union[int, int]]: +def locate_and_click(image, mouse_keycode: Union[int, str], + detect_threshold: float = 1.0, + draw_image: bool = False) -> Tuple[int, int]: """ - use to locate cv2_utils and click cv2_utils center position and the return cv2_utils center position - :param image which cv2_utils we want to find on screen (png or PIL ImageGrab.grab()) - :param mouse_keycode which mouse keycode we want to click - :param detect_threshold detect precision 0.0 ~ 1.0; 1 is absolute equal (float or int) - :param draw_image draw detect tag on return cv2_utils (bool) + 找出影像後自動移動滑鼠並點擊 + Locate image and click its center + + :return: (center_x, center_y) """ - autocontrol_logger.info( - f"locate_and_click, cv2_utils: {image}, keycode: {mouse_keycode}, detect threshold: {detect_threshold}, " - f"draw cv2_utils: {draw_image}" - ) - param = locals() + autocontrol_logger.info(f"Locate and click {image}, keycode={mouse_keycode}, threshold={detect_threshold}") try: - try: - image_data_array = template_detection.find_image(image, detect_threshold, draw_image) - except ImageNotFoundException: - autocontrol_logger.error( - f"Locate and click failed, cv2_utils: {image}, keycode: {mouse_keycode}, " - f"detect_threshold: {detect_threshold}, " - f"failed: {repr(ImageNotFoundException(find_image_error_variable))}" - ) - raise ImageNotFoundException(find_image_error_variable) - if image_data_array[0] is True: - height = image_data_array[1][2] - image_data_array[1][0] - width = image_data_array[1][3] - image_data_array[1][1] - center = [int(height / 2), int(width / 2)] - image_center_x = image_data_array[1][0] + center[0] - image_center_y = image_data_array[1][1] + center[1] - set_mouse_position(int(image_center_x), int(image_center_y)) + image_data_array = template_detection.find_image(image, detect_threshold, draw_image) + if image_data_array[0]: + x1, y1, x2, y2 = image_data_array[1] + center_x = int((x1 + x2) / 2) + center_y = int((y1 + y2) / 2) + set_mouse_position(center_x, center_y) click_mouse(mouse_keycode) - record_action_to_list("locate_and_click", param) - return [int(image_center_x), int(image_center_y)] - else: - autocontrol_logger.error( - f"Locate and click failed, cv2_utils: {image}, keycode: {mouse_keycode}, " - f"detect_threshold: {detect_threshold}, " - f"failed: {repr(ImageNotFoundException(cant_find_image + ' / ' + repr(image)))}" - ) - raise ImageNotFoundException(cant_find_image + " / " + repr(image)) + record_action_to_list("locate_and_click", {"image": image, "threshold": detect_threshold}) + return center_x, center_y + raise ImageNotFoundException(f"{cant_find_image_error_message} / {image}") except Exception as error: - record_action_to_list("locate_and_click", param, repr(error)) - autocontrol_logger.error( - f"Locate and click failed, cv2_utils: {image}, keycode: {mouse_keycode}, " - f"detect_threshold: {detect_threshold}, " - f"failed: {repr(error)}" - ) + record_action_to_list("locate_and_click", {"image": image}, repr(error)) + autocontrol_logger.error(f"locate_and_click failed: {repr(error)}") + raise -def screenshot(file_path: str = None, region: list = None) -> List[Union[int, int]]: +def screenshot(file_path: Optional[str] = None, region: Optional[List[int]] = None): """ - use to get now screen cv2_utils return cv2_utils - :param file_path save screenshot path (None is no save) - :param region screenshot screen_region (screenshot screen_region on screen) + 擷取螢幕畫面 + Take a screenshot + + :param file_path: 儲存路徑 (None = 不儲存) + :param region: 擷取區域 [x1, y1, x2, y2] + :return: PIL Image """ - autocontrol_logger.info( - f"screenshot, file path: {file_path}, region: {region}" - ) - param = locals() + autocontrol_logger.info(f"screenshot, file={file_path}, region={region}") try: - record_action_to_list("screenshot", param) + record_action_to_list("screenshot", {"file_path": file_path, "region": region}) return pil_screenshot(file_path, region) except Exception as error: - autocontrol_logger.error( - f"Screenshot failed, file path: {file_path}, region: {region}, " - f"failed: {repr(error)}" - ) - record_action_to_list("screenshot", param, repr(error)) + record_action_to_list("screenshot", {"file_path": file_path, "region": region}, repr(error)) + autocontrol_logger.error(f"screenshot failed: {repr(error)}") + raise \ No newline at end of file diff --git a/je_auto_control/wrapper/auto_control_keyboard.py b/je_auto_control/wrapper/auto_control_keyboard.py index b508d40..db72877 100644 --- a/je_auto_control/wrapper/auto_control_keyboard.py +++ b/je_auto_control/wrapper/auto_control_keyboard.py @@ -1,327 +1,226 @@ import sys -from typing import Union - -from je_auto_control.utils.exception.exception_tags import keyboard_hotkey -from je_auto_control.utils.exception.exception_tags import keyboard_press_key -from je_auto_control.utils.exception.exception_tags import keyboard_release_key -from je_auto_control.utils.exception.exception_tags import keyboard_type_key -from je_auto_control.utils.exception.exception_tags import keyboard_write -from je_auto_control.utils.exception.exception_tags import keyboard_write_cant_find -from je_auto_control.utils.exception.exception_tags import table_cant_find_key -from je_auto_control.utils.exception.exceptions import AutoControlCantFindKeyException -from je_auto_control.utils.exception.exceptions import AutoControlKeyboardException +from typing import Optional, Union, Tuple + +from je_auto_control.utils.exception.exception_tags import ( + keyboard_press_key_error_message, keyboard_release_key_error_message, keyboard_type_key_error_message, + table_cant_find_key_error_message, keyboard_write_cant_find_error_message, keyboard_write_error_message, keyboard_hotkey_error_message +) +from je_auto_control.utils.exception.exceptions import ( + AutoControlCantFindKeyException, AutoControlKeyboardException +) from je_auto_control.utils.logging.loggin_instance import autocontrol_logger from je_auto_control.utils.test_record.record_test_class import record_action_to_list -from je_auto_control.wrapper.platform_wrapper import keyboard, special_mouse_keys_table -from je_auto_control.wrapper.platform_wrapper import keyboard_check -from je_auto_control.wrapper.platform_wrapper import keyboard_keys_table - - -def get_special_table() -> dict: - return special_mouse_keys_table - +from je_auto_control.wrapper.platform_wrapper import keyboard, keyboard_keys_table, keyboard_check def get_keyboard_keys_table() -> dict: + """ + 取得鍵盤對應表 + Get keyboard keys table + """ return keyboard_keys_table -def press_keyboard_key(keycode: Union[int, str], is_shift: bool = False, skip_record: bool = False) -> str | None: +def _resolve_keycode(keycode: Union[int, str]) -> int: + """ + 將字串鍵名轉換成對應的 keycode + Resolve string key name to keycode """ - use to press a key still press to use release key - or use critical exit + if isinstance(keycode, str): + resolved = keyboard_keys_table.get(keycode) + if resolved is None: + raise AutoControlCantFindKeyException(table_cant_find_key_error_message) + return resolved return keycode - :param keycode which keycode we want to press - :param is_shift press shift True or False - :param skip_record skip record on record total list True or False + + +def press_keyboard_key(keycode: Union[int, str], is_shift: bool = False, + skip_record: bool = False) -> Optional[str]: + """ + 按下指定鍵 + Press a keyboard key + + :param keycode: 鍵盤代碼或字串 Keycode or string + :param is_shift: 是否同時按下 Shift + :param skip_record: 是否跳過紀錄 + :return: keycode 字串 """ - autocontrol_logger.info( - f"press_keyboard_key, keycode: {keycode}, is_shift: {is_shift}, skip_record: {skip_record}" - ) - param = locals() + autocontrol_logger.info(f"press_keyboard_key, keycode={keycode}, is_shift={is_shift}, skip_record={skip_record}") try: - if isinstance(keycode, str): - try: - keycode = keyboard_keys_table.get(keycode) - except AutoControlCantFindKeyException: - autocontrol_logger.error( - f"press_keyboard_key failed, keycode: {keycode}, is_shift: {is_shift}, " - f"failed: {repr(AutoControlCantFindKeyException(table_cant_find_key))}" - ) - raise AutoControlCantFindKeyException(table_cant_find_key) - try: - if sys.platform in ["win32", "cygwin", "msys", "linux", "linux2"]: - keyboard.press_key(keycode) - elif sys.platform in ["darwin"]: - keyboard.press_key(keycode, is_shift=is_shift) - if skip_record is False: - record_action_to_list("press_key", param) - return str(keycode) - except AutoControlKeyboardException as error: - if skip_record is False: - record_action_to_list("press_key", param, repr(error)) - autocontrol_logger.error( - f"press_keyboard_key failed, keycode: {keycode}, is_shift: {is_shift}, " - f"{repr(AutoControlKeyboardException(keyboard_press_key + ' ' + repr(error)))}" - ) - raise AutoControlKeyboardException(keyboard_press_key + " " + repr(error)) - except TypeError as error: - if skip_record is False: - record_action_to_list("press_key", param, repr(error)) - autocontrol_logger.error( - f"press_keyboard_key failed, keycode: {keycode}, is_shift: {is_shift}, " - f"failed: {repr(AutoControlKeyboardException)}" - ) - raise AutoControlKeyboardException(repr(error)) + keycode = _resolve_keycode(keycode) + if sys.platform in ["win32", "cygwin", "msys", "linux", "linux2"]: + keyboard.press_key(keycode) + elif sys.platform == "darwin": + keyboard.press_key(keycode, is_shift=is_shift) + + if not skip_record: + record_action_to_list("press_key", {"keycode": keycode, "is_shift": is_shift}) + return str(keycode) + except Exception as error: - if skip_record is False: - record_action_to_list("press_key", param, repr(error)) - autocontrol_logger.error( - f"press_keyboard_key failed, keycode: {keycode}, is_shift: {is_shift}, " - f"failed: {repr(error)}" - ) + if not skip_record: + record_action_to_list("press_key", {"keycode": keycode}, repr(error)) + autocontrol_logger.error(f"press_keyboard_key failed: {repr(error)}") + raise AutoControlKeyboardException(f"{keyboard_press_key_error_message} {repr(error)}") -def release_keyboard_key(keycode: Union[int, str], is_shift: bool = False, skip_record: bool = False) -> str | None: +def release_keyboard_key(keycode: Union[int, str], is_shift: bool = False, + skip_record: bool = False) -> Optional[str]: """ - use to release pressed key return keycode - :param keycode which keycode we want to release - :param is_shift press shift True or False - :param skip_record skip record on record total list True or False + 放開指定鍵 + Release a keyboard key """ - autocontrol_logger.info( - f"release_keyboard_key, keycode: {keycode}, is_shift: {is_shift}, skip_record: {skip_record}" - ) - param = locals() + autocontrol_logger.info(f"release_keyboard_key, keycode={keycode}, is_shift={is_shift}, skip_record={skip_record}") try: - if isinstance(keycode, str): - try: - keycode = keyboard_keys_table.get(keycode) - except AutoControlCantFindKeyException: - raise AutoControlCantFindKeyException(table_cant_find_key) - try: - if sys.platform in ["win32", "cygwin", "msys", "linux", "linux2"]: - keyboard.release_key(keycode) - elif sys.platform in ["darwin"]: - keyboard.release_key(keycode, is_shift=is_shift) - if not skip_record: - record_action_to_list("release_key", param) - return str(keycode) - except AutoControlKeyboardException as error: - if not skip_record: - record_action_to_list("release_key", param, repr(error)) - autocontrol_logger.error( - f"release_keyboard_key, keycode: {keycode}, is_shift: {is_shift}, skip_record: {skip_record}, " - f"failed: {AutoControlKeyboardException(keyboard_release_key + ' ' + repr(error))}" - ) - raise AutoControlKeyboardException(keyboard_release_key + " " + repr(error)) - except TypeError as error: - if skip_record is False: - record_action_to_list("release_key", param, repr(error)) - autocontrol_logger.error( - f"release_keyboard_key, keycode: {keycode}, is_shift: {is_shift}, skip_record: {skip_record}, " - f"failed: {AutoControlKeyboardException(error)}" - ) - raise AutoControlKeyboardException(error) + keycode = _resolve_keycode(keycode) + if sys.platform in ["win32", "cygwin", "msys", "linux", "linux2"]: + keyboard.release_key(keycode) + elif sys.platform == "darwin": + keyboard.release_key(keycode, is_shift=is_shift) + + if not skip_record: + record_action_to_list("release_key", {"keycode": keycode, "is_shift": is_shift}) + return str(keycode) + except Exception as error: - if skip_record is False: - record_action_to_list("release_key", param, repr(error)) - autocontrol_logger.error( - f"release_keyboard_key, keycode: {keycode}, is_shift: {is_shift}, skip_record: {skip_record}, " - f"failed: {repr(error)}" - ) + if not skip_record: + record_action_to_list("release_key", {"keycode": keycode}, repr(error)) + autocontrol_logger.error(f"release_keyboard_key failed: {repr(error)}") + raise AutoControlKeyboardException(f"{keyboard_release_key_error_message} {repr(error)}") -def type_keyboard(keycode: Union[int, str], is_shift: bool = False, skip_record: bool = False) -> str | None: +def type_keyboard(keycode: Union[int, str], is_shift: bool = False, + skip_record: bool = False) -> Optional[str]: """ - press and release key return keycode - :param keycode which keycode we want to type - :param is_shift press shift True or False - :param skip_record skip record on record total list True or False + 模擬輸入 (按下再放開) + Type a keyboard key (press and release) """ - autocontrol_logger.info( - f"type_keyboard, keycode: {keycode}, is_shift: {is_shift}, skip_record: {skip_record}" - ) - param = locals() + autocontrol_logger.info(f"type_keyboard, keycode={keycode}, is_shift={is_shift}, skip_record={skip_record}") try: - try: - press_keyboard_key(keycode, is_shift, skip_record=True) - release_keyboard_key(keycode, is_shift, skip_record=True) - if not skip_record: - record_action_to_list("type_keyboard", param) - return str(keycode) - except AutoControlKeyboardException as error: - if not skip_record: - record_action_to_list("type_keyboard", param, repr(error)) - autocontrol_logger.error( - f"type_keyboard, keycode: {keycode}, is_shift: {is_shift}, skip_record: {skip_record}, " - f"failed: {repr(AutoControlKeyboardException(keyboard_type_key + ' ' + repr(error)))}" - ) - raise AutoControlKeyboardException(keyboard_type_key + " " + repr(error)) - except TypeError as error: - if not skip_record: - record_action_to_list("type_keyboard", param, repr(error)) - autocontrol_logger.error( - f"type_keyboard, keycode: {keycode}, is_shift: {is_shift}, skip_record: {skip_record}, " - f"failed: {repr(AutoControlKeyboardException(repr(error)))}" - ) - raise AutoControlKeyboardException(repr(error)) - except Exception as error: + press_keyboard_key(keycode, is_shift, skip_record=True) + release_keyboard_key(keycode, is_shift, skip_record=True) + if not skip_record: - record_action_to_list("type_keyboard", param, repr(error)) - autocontrol_logger.error( - f"type_keyboard, keycode: {keycode}, is_shift: {is_shift}, skip_record: {skip_record}, " - f"failed: {repr(error)}" - ) + record_action_to_list("type_keyboard", {"keycode": keycode, "is_shift": is_shift}) + return str(keycode) + except Exception as error: + if not skip_record: + record_action_to_list("type_keyboard", {"keycode": keycode}, repr(error)) + autocontrol_logger.error(f"type_keyboard failed: {repr(error)}") + raise AutoControlKeyboardException(f"{keyboard_type_key_error_message} {repr(error)}") -def check_key_is_press(keycode: Union[int, str]) -> bool | None: +def check_key_is_press(keycode: Union[int, str]) -> Optional[bool]: """ - use to check key is press return True or False - :param keycode check key is press or not + 檢查某個鍵是否正在被按下 + Check if a key is currently pressed + + :param keycode: 鍵盤代碼或字串 Keycode or string + :return: True / False / None """ - autocontrol_logger.info( - f"check_key_is_press, keycode: {keycode}" - ) - param = locals() + autocontrol_logger.info(f"check_key_is_press, keycode={keycode}") try: - if isinstance(keycode, int): - get_key_code = keycode - else: - get_key_code = keyboard_keys_table.get(keycode) - record_action_to_list("check_key_is_press", param) + get_key_code = keycode if isinstance(keycode, int) else keyboard_keys_table.get(keycode) + record_action_to_list("check_key_is_press", {"keycode": keycode}) return keyboard_check.check_key_is_press(keycode=get_key_code) except Exception as error: - record_action_to_list("check_key_is_press", param, repr(error)) - autocontrol_logger.error( - f"check_key_is_press, keycode: {keycode}, " - f"failed: {repr(error)}" - ) + record_action_to_list("check_key_is_press", {"keycode": keycode}, repr(error)) + autocontrol_logger.error(f"check_key_is_press failed: {repr(error)}") + return None -def write(write_string: str, is_shift: bool = False) -> None | str: +def write(write_string: str, is_shift: bool = False) -> Optional[str]: """ - use to press and release whole we get this function str - return all press and release str - :param write_string while string not on write_string+1 type_keyboard(string) - :param is_shift press shift True or False + 模擬輸入整個字串 + Type a whole string + + :param write_string: 要輸入的字串 String to type + :param is_shift: 是否同時按下 Shift + :return: 輸入的字串 """ - autocontrol_logger.info( - f"write, write_string: {write_string}, is_shift: {is_shift}" - ) - param = locals() + autocontrol_logger.info(f"write, write_string={write_string}, is_shift={is_shift}") try: - try: - record_write_string = "" - for single_string in write_string: - try: - key = keyboard_keys_table.get(single_string, None) - if key is not None: - record_write_string = "".join( - [ - record_write_string, - type_keyboard(key, is_shift, skip_record=True) - ] - ) - elif single_string.isspace(): - record_write_string = "".join( - [ - record_write_string, - type_keyboard("space", is_shift, skip_record=True) - ] - ) - else: - autocontrol_logger.error( - f"write, write_string: {write_string}, is_shift: {is_shift}, " - f"failed: {AutoControlKeyboardException(keyboard_write_cant_find)}" - ) - raise AutoControlKeyboardException(keyboard_write_cant_find) - except AutoControlKeyboardException as error: - autocontrol_logger.error( - f"write, write_string: {write_string}, is_shift: {is_shift}, " - f"failed: {repr(error)}, keyboard_write_cant_find, {single_string}" - ) - raise AutoControlKeyboardException(keyboard_write_cant_find) - record_action_to_list("write", param) - return record_write_string - except AutoControlKeyboardException as error: - autocontrol_logger.error( - f"write, write_string: {write_string}, is_shift: {is_shift}, " - f"failed: {AutoControlKeyboardException(keyboard_write + ' ' + repr(error))}" - ) - raise AutoControlKeyboardException(keyboard_write + " " + repr(error)) + record_write_chars = [] + for single_char in write_string: + key = keyboard_keys_table.get(single_char) + if key is not None: + record_write_chars.append(type_keyboard(key, is_shift, skip_record=True)) + elif single_char.isspace(): + record_write_chars.append(type_keyboard("space", is_shift, skip_record=True)) + else: + autocontrol_logger.error(f"write failed: {keyboard_write_cant_find_error_message}, char={single_char}") + raise AutoControlKeyboardException(keyboard_write_cant_find_error_message) + + result = "".join(record_write_chars) + record_action_to_list("write", {"write_string": write_string, "is_shift": is_shift}) + return result + except Exception as error: - record_action_to_list("write", param, repr(error)) - autocontrol_logger.error( - f"write, write_string: {write_string}, is_shift: {is_shift}, " - f"failed: {repr(error)}" - ) + record_action_to_list("write", {"write_string": write_string}, repr(error)) + autocontrol_logger.error(f"write failed: {repr(error)}") + raise AutoControlKeyboardException(f"{keyboard_write_error_message} {repr(error)}") -def hotkey(key_code_list: list, is_shift: bool = False) -> tuple[str, str] | None: +def hotkey(key_code_list: list, is_shift: bool = False) -> Optional[Tuple[str, str]]: """ - use to press and release all key on key_code_list - then reverse list press and release again - return [press_str_list, release_str_list] - :param key_code_list press and release all key on list and reverse - :param is_shift press shift True or False + 模擬組合鍵 (依序按下,再反向放開) + Simulate hotkey (press all keys, then release in reverse order) + + :param key_code_list: 鍵盤代碼清單 List of keycodes + :param is_shift: 是否同時按下 Shift + :return: (press_str, release_str) """ - autocontrol_logger.info( - f"hotkey, key_code_list: {key_code_list}, is_shift: {is_shift}" - ) - param = locals() + autocontrol_logger.info(f"hotkey, key_code_list={key_code_list}, is_shift={is_shift}") try: - try: - record_hotkey_press_string = "" - record_hotkey_release_string = "" - for key in key_code_list: - record_hotkey_press_string = ",".join( - [ - record_hotkey_press_string, - press_keyboard_key(key, is_shift, skip_record=True) - ] - ) - key_code_list.reverse() - for key in key_code_list: - record_hotkey_release_string = ",".join( - [ - record_hotkey_release_string, - release_keyboard_key(key, is_shift, skip_record=True) - ] - ) - record_action_to_list("hotkey", param) - return record_hotkey_press_string, record_hotkey_release_string - except AutoControlKeyboardException as error: - autocontrol_logger.error( - f"hotkey, key_code_list: {key_code_list}, is_shift: {is_shift}, " - f"failed: {AutoControlKeyboardException(keyboard_hotkey + ' ' + repr(error))}" - ) - raise AutoControlKeyboardException(keyboard_hotkey + " " + repr(error)) + press_list = [] + release_list = [] + + for key in key_code_list: + press_list.append(press_keyboard_key(key, is_shift, skip_record=True)) + + for key in reversed(key_code_list): + release_list.append(release_keyboard_key(key, is_shift, skip_record=True)) + + press_str = ",".join(filter(None, press_list)) + release_str = ",".join(filter(None, release_list)) + + record_action_to_list("hotkey", {"keys": key_code_list, "is_shift": is_shift}) + return press_str, release_str + except Exception as error: - record_action_to_list("hotkey", param, repr(error)) - autocontrol_logger.error( - f"hotkey, key_code_list: {key_code_list}, is_shift: {is_shift}, " - f"failed: {repr(error)}" - ) - -def send_key_event_to_window(window_title, keycode: int): - autocontrol_logger.info( - f"send_key_event_to_window window:{window_title}, keycode:{keycode}" - ) - param = locals() + record_action_to_list("hotkey", {"keys": key_code_list}, repr(error)) + autocontrol_logger.error(f"hotkey failed: {repr(error)}") + raise AutoControlKeyboardException(f"{keyboard_hotkey_error_message} {repr(error)}") + +def send_key_event_to_window(window_title: str, keycode: Union[int, str]) -> None: + """ + 將鍵盤事件送到指定視窗 + Send a key event to a specific window + + :param window_title: 視窗標題 Window title + :param keycode: 鍵盤代碼或字串 Keycode or string + """ + autocontrol_logger.info(f"send_key_event_to_window, window={window_title}, keycode={keycode}") try: - if sys.platform in ["darwin"]: + # macOS 不支援直接送鍵盤事件 + if sys.platform == "darwin": return + + # 解析 keycode Resolve keycode if isinstance(keycode, int): get_key_code = keycode else: get_key_code = keyboard_keys_table.get(keycode) - keyboard.send_key_event_to_window( - window_title, - keycode=keycode - ) + if get_key_code is None: + raise AutoControlKeyboardException(f"Key not found: {keycode}") + + # 呼叫底層 API Send event + keyboard.send_key_event_to_window(window_title, keycode=get_key_code) + + # 紀錄動作 Record action + record_action_to_list("send_key_event_to_window", {"window_title": window_title, "keycode": get_key_code}) + except Exception as error: - record_action_to_list("send_key_event_to_window", param, repr(error)) + record_action_to_list("send_key_event_to_window", {"window_title": window_title, "keycode": keycode}, repr(error)) autocontrol_logger.error( - f"send_key_event_to_window window:{window_title}, keycode:{keycode}" - f"failed: {repr(error)}" - ) + f"send_key_event_to_window failed, window={window_title}, keycode={keycode}, error={repr(error)}" + ) \ No newline at end of file diff --git a/je_auto_control/wrapper/auto_control_mouse.py b/je_auto_control/wrapper/auto_control_mouse.py index 8966d6a..1e15d73 100644 --- a/je_auto_control/wrapper/auto_control_mouse.py +++ b/je_auto_control/wrapper/auto_control_mouse.py @@ -2,44 +2,46 @@ import sys from typing import Tuple, Union -from je_auto_control.utils.exception.exception_tags import mouse_click_mouse -from je_auto_control.utils.exception.exception_tags import mouse_get_position -from je_auto_control.utils.exception.exception_tags import mouse_press_mouse -from je_auto_control.utils.exception.exception_tags import mouse_release_mouse -from je_auto_control.utils.exception.exception_tags import mouse_scroll -from je_auto_control.utils.exception.exception_tags import mouse_set_position -from je_auto_control.utils.exception.exception_tags import mouse_wrong_value -from je_auto_control.utils.exception.exception_tags import table_cant_find_key -from je_auto_control.utils.exception.exceptions import AutoControlCantFindKeyException -from je_auto_control.utils.exception.exceptions import AutoControlMouseException +from je_auto_control.utils.exception.exception_tags import ( + mouse_click_mouse_error_message, mouse_get_position_error_message, mouse_press_mouse_error_message, + mouse_release_mouse_error_message, mouse_scroll_error_message, mouse_set_position_error_message, + mouse_wrong_value_error_message, table_cant_find_key_error_message +) +from je_auto_control.utils.exception.exceptions import ( + AutoControlCantFindKeyException, AutoControlMouseException +) from je_auto_control.utils.logging.loggin_instance import autocontrol_logger from je_auto_control.utils.test_record.record_test_class import record_action_to_list from je_auto_control.wrapper.auto_control_screen import screen_size -from je_auto_control.wrapper.platform_wrapper import mouse -from je_auto_control.wrapper.platform_wrapper import mouse_keys_table -from je_auto_control.wrapper.platform_wrapper import special_mouse_keys_table +from je_auto_control.wrapper.platform_wrapper import mouse, mouse_keys_table, special_mouse_keys_table def get_mouse_table() -> dict: + """ + 取得滑鼠按鍵對應表 + Get mouse keys table + """ return mouse_keys_table -def mouse_preprocess(mouse_keycode: Union[int, str], x: int, y: int) -> Tuple[Union[int, str], int, int]: +def mouse_preprocess(mouse_keycode: Union[int, str], x: int, y: int) -> Tuple[int, int, int]: """ - check mouse keycode is verified or not - and then check current mouse position - if x or y is None set x, y is current position - :param mouse_keycode which mouse keycode we want to click - :param x mouse click x position - :param y mouse click y position + 前置處理:檢查 keycode 並補齊座標 + Preprocess mouse keycode and coordinates + + :param mouse_keycode: 滑鼠按鍵代碼或字串 Mouse keycode or string + :param x: X 座標 + :param y: Y 座標 + :return: (keycode, x, y) """ try: if isinstance(mouse_keycode, str): mouse_keycode = mouse_keys_table.get(mouse_keycode) - else: - pass + if mouse_keycode is None: + raise AutoControlCantFindKeyException(table_cant_find_key_error_message) except AutoControlCantFindKeyException: - raise AutoControlCantFindKeyException(table_cant_find_key) + raise AutoControlCantFindKeyException(table_cant_find_key_error_message) + try: now_x, now_y = get_mouse_position() if x is None: @@ -47,241 +49,188 @@ def mouse_preprocess(mouse_keycode: Union[int, str], x: int, y: int) -> Tuple[Un if y is None: y = now_y except AutoControlMouseException as error: - raise AutoControlMouseException(mouse_get_position + " " + repr(error)) + raise AutoControlMouseException(mouse_get_position_error_message + " " + repr(error)) + return mouse_keycode, x, y -def get_mouse_position() -> Tuple[int, int]: +def get_mouse_position() -> tuple[int, int] | None: """ - get mouse current position - return mouse_x, mouse_y + 取得滑鼠目前位置 + Get current mouse position + + :return: (x, y) """ autocontrol_logger.info("get_mouse_position") try: - try: - record_action_to_list("get_mouse_position", None) - return mouse.position() - except AutoControlMouseException as error: - raise AutoControlMouseException(mouse_get_position + " " + repr(error)) + record_action_to_list("get_mouse_position", None) + return mouse.position() + except AutoControlMouseException as error: + raise AutoControlMouseException(mouse_get_position_error_message + " " + repr(error)) except Exception as error: - record_action_to_list("position", None, repr(error)) + record_action_to_list("get_mouse_position", None, repr(error)) print(repr(error), file=sys.stderr) -def set_mouse_position(x: int, y: int) -> Tuple[int, int]: +def set_mouse_position(x: int, y: int) -> tuple[int, int] | None: """ - :param x set mouse position x - :param y set mouse position y - return x, y + 設定滑鼠位置 + Set mouse position + + :param x: X 座標 + :param y: Y 座標 + :return: (x, y) """ - autocontrol_logger.info(f"set_mouse_position, x: {x}, y: {y}") - param = locals() + autocontrol_logger.info(f"set_mouse_position, x={x}, y={y}") + param = {"x": x, "y": y} try: - try: - mouse.set_position(x=x, y=y) - record_action_to_list("position", param) - return x, y - except AutoControlMouseException as error: - autocontrol_logger.error( - f"set_mouse_position, x: {x}, y: {y}, " - f"failed: {AutoControlMouseException(mouse_set_position + ' ' + repr(error))}") - raise AutoControlMouseException(mouse_set_position + " " + repr(error)) - except ctypes.ArgumentError as error: - autocontrol_logger.error( - f"set_mouse_position, x: {x}, y: {y}, " - f"failed: {AutoControlMouseException(mouse_wrong_value + ' ' + repr(error))}") - raise AutoControlMouseException(mouse_wrong_value + " " + repr(error)) + mouse.set_position(x=x, y=y) + record_action_to_list("set_mouse_position", param) + return x, y + except AutoControlMouseException as error: + autocontrol_logger.error(f"set_mouse_position failed: {repr(error)}") + raise AutoControlMouseException(mouse_set_position_error_message + " " + repr(error)) + except ctypes.ArgumentError as error: + autocontrol_logger.error(f"set_mouse_position invalid args: {repr(error)}") + raise AutoControlMouseException(mouse_wrong_value_error_message + " " + repr(error)) except Exception as error: record_action_to_list("set_mouse_position", param, repr(error)) - autocontrol_logger.error( - f"set_mouse_position, x: {x}, y: {y}, " - f"failed: {repr(error)}") + autocontrol_logger.error(f"set_mouse_position failed: {repr(error)}") -def press_mouse(mouse_keycode: [int, str], x: int = None, y: int = None) -> Tuple[Union[int, str], int, int]: +def press_mouse(mouse_keycode: Union[int, str], x: int = None, y: int = None) -> tuple[int, int, int] | None: """ - press mouse keycode on x, y - return keycode, x, y - :param mouse_keycode which mouse keycode we want to press - :param x mouse click x position - :param y mouse click y position + 按下滑鼠按鍵 + Press mouse button + + :return: (keycode, x, y) """ - autocontrol_logger.info( - f"press_mouse, mouse_keycode: {mouse_keycode}, x: {x}, y: {y}" - ) - param = locals() + autocontrol_logger.info(f"press_mouse, keycode={mouse_keycode}, x={x}, y={y}") + param = {"keycode": mouse_keycode, "x": x, "y": y} try: mouse_keycode, x, y = mouse_preprocess(mouse_keycode, x, y) - try: - if sys.platform in ["win32", "cygwin", "msys", "linux", "linux2"]: - mouse.press_mouse(mouse_keycode) - elif sys.platform in ["darwin"]: - mouse.press_mouse(x, y, mouse_keycode) - record_action_to_list("press_mouse", param) - return mouse_keycode, x, y - except AutoControlMouseException as error: - autocontrol_logger.error( - f"press_mouse, mouse_keycode: {mouse_keycode}, x: {x}, y: {y}, " - f"failed: {AutoControlMouseException(mouse_press_mouse + ' ' + repr(error))}" - ) - raise AutoControlMouseException(mouse_press_mouse + " " + repr(error)) + if sys.platform in ["win32", "cygwin", "msys", "linux", "linux2"]: + mouse.press_mouse(mouse_keycode) + elif sys.platform == "darwin": + mouse.press_mouse(x, y, mouse_keycode) + record_action_to_list("press_mouse", param) + return mouse_keycode, x, y + except AutoControlMouseException as error: + autocontrol_logger.error(f"press_mouse failed: {repr(error)}") + raise AutoControlMouseException(mouse_press_mouse_error_message + " " + repr(error)) except Exception as error: record_action_to_list("press_mouse", param, repr(error)) - autocontrol_logger.error( - f"press_mouse, mouse_keycode: {mouse_keycode}, x: {x}, y: {y}, " - f"failed: {repr(error)}" - ) + autocontrol_logger.error(f"press_mouse failed: {repr(error)}") -def release_mouse(mouse_keycode: [int, str], x: int = None, y: int = None) -> Tuple[Union[int, str], int, int]: +def release_mouse(mouse_keycode: Union[int, str], x: int = None, y: int = None) -> tuple[int, int, int] | None: """ - release mouse keycode on x, y - return keycode, x, y - :param mouse_keycode which mouse keycode we want to release - :param x mouse click x position - :param y mouse click y position + 放開滑鼠按鍵 + Release mouse button + + :return: (keycode, x, y) """ - autocontrol_logger.info( - f"press_mouse, mouse_keycode: {mouse_keycode}, x: {x}, y: {y}" - ) - param = locals() + autocontrol_logger.info(f"release_mouse, keycode={mouse_keycode}, x={x}, y={y}") + param = {"keycode": mouse_keycode, "x": x, "y": y} try: mouse_keycode, x, y = mouse_preprocess(mouse_keycode, x, y) - try: - if sys.platform in ["win32", "cygwin", "msys", "linux", "linux2"]: - mouse.release_mouse(mouse_keycode) - elif sys.platform in ["darwin"]: - mouse.release_mouse(x, y, mouse_keycode) - record_action_to_list("press_mouse", param) - return mouse_keycode, x, y - except AutoControlMouseException as error: - autocontrol_logger.error( - f"press_mouse, mouse_keycode: {mouse_keycode}, x: {x}, y: {y}, " - f"failed: {AutoControlMouseException(mouse_release_mouse + ' ' + repr(error))}" - ) - raise AutoControlMouseException(mouse_release_mouse + " " + repr(error)) + if sys.platform in ["win32", "cygwin", "msys", "linux", "linux2"]: + mouse.release_mouse(mouse_keycode) + elif sys.platform == "darwin": + mouse.release_mouse(x, y, mouse_keycode) + record_action_to_list("release_mouse", param) + return mouse_keycode, x, y + except AutoControlMouseException as error: + autocontrol_logger.error(f"release_mouse failed: {repr(error)}") + raise AutoControlMouseException(mouse_release_mouse_error_message + " " + repr(error)) except Exception as error: record_action_to_list("release_mouse", param, repr(error)) - autocontrol_logger.error( - f"press_mouse, mouse_keycode: {mouse_keycode}, x: {x}, y: {y}, " - f"failed: {repr(error)}" - ) + autocontrol_logger.error(f"release_mouse failed: {repr(error)}") -def click_mouse(mouse_keycode: Union[int, str], x: int = None, y: int = None) -> Tuple[Union[int, str], int, int]: +def click_mouse(mouse_keycode: Union[int, str], x: int = None, y: int = None) -> Tuple[int, int, int]: """ - press and release mouse keycode on x, y - return keycode, x, y - :param mouse_keycode which mouse keycode we want to click - :param x mouse click x position - :param y mouse click y position + 在指定座標按下並放開滑鼠按鍵 + Click mouse button at given position + + :param mouse_keycode: 滑鼠按鍵代碼 Mouse keycode + :param x: X 座標 X position + :param y: Y 座標 Y position + :return: (keycode, x, y) """ - autocontrol_logger.info( - f"click_mouse, mouse_keycode: {mouse_keycode}, x: {x}, y: {y}" - ) - param = locals() + autocontrol_logger.info(f"click_mouse, keycode={mouse_keycode}, x={x}, y={y}") + param = {"keycode": mouse_keycode, "x": x, "y": y} try: mouse_keycode, x, y = mouse_preprocess(mouse_keycode, x, y) - try: - mouse.click_mouse(mouse_keycode, x, y) - record_action_to_list("click_mouse", param) - return mouse_keycode, x, y - except AutoControlMouseException as error: - record_action_to_list("click_mouse", param, mouse_click_mouse + " " + repr(error)) - autocontrol_logger.error( - f"click_mouse, mouse_keycode: {mouse_keycode}, x: {x}, y: {y}, " - f"failed: {AutoControlMouseException(mouse_click_mouse + ' ' + repr(error))}" - ) - raise AutoControlMouseException(mouse_click_mouse + " " + repr(error)) - except Exception as error: + mouse.click_mouse(mouse_keycode, x, y) + record_action_to_list("click_mouse", param) + return mouse_keycode, x, y + except AutoControlMouseException as error: record_action_to_list("click_mouse", param, repr(error)) - autocontrol_logger.error( - f"click_mouse, mouse_keycode: {mouse_keycode}, x: {x}, y: {y}, " - f"failed: {repr(error)}" - ) - - -def mouse_scroll( - scroll_value: int, x: int = None, y: int = None, scroll_direction: str = "scroll_down") -> Tuple[int, str]: - """" - :param scroll_value scroll count - :param x mouse click x position - :param y mouse click y position - :param scroll_direction which direction we want to scroll (only linux) - scroll_direction = scroll_up : direction up - scroll_direction = scroll_down : direction down - scroll_direction = scroll_left : direction left - scroll_direction = scroll_right : direction right + autocontrol_logger.error(f"click_mouse failed: {repr(error)}") + raise AutoControlMouseException(mouse_click_mouse_error_message + " " + repr(error)) + + +def mouse_scroll(scroll_value: int, x: int = None, y: int = None, + scroll_direction: str = "scroll_down") -> Tuple[int, str]: """ - autocontrol_logger.info( - f"mouse_scroll, scroll_value: {scroll_value}, x: {x}, y: {y}, scroll_direction: {scroll_direction}" - ) - param = locals() + 模擬滑鼠滾輪操作 + Simulate mouse scroll + + :param scroll_value: 滾動數值 Scroll value + :param x: X 座標 X position + :param y: Y 座標 Y position + :param scroll_direction: 滾動方向 (Linux only) Scroll direction + :return: (scroll_value, scroll_direction) + """ + autocontrol_logger.info(f"mouse_scroll, value={scroll_value}, x={x}, y={y}, direction={scroll_direction}") + param = {"scroll_value": scroll_value, "x": x, "y": y, "direction": scroll_direction} try: - try: - now_cursor_x, now_cursor_y = get_mouse_position() - except AutoControlMouseException as error: - record_action_to_list("scroll", param, repr(error)) - autocontrol_logger.error( - f"mouse_scroll, scroll_value: {scroll_value}, x: {x}, y: {y}, scroll_direction: {scroll_direction}, " - f"failed: {AutoControlMouseException(mouse_get_position)}" - ) - raise AutoControlMouseException(mouse_get_position) + now_x, now_y = get_mouse_position() width, height = screen_size() - if x is None: - x = now_cursor_x - else: - if x < 0: - x = 0 - elif x >= width: - x = width - 1 - if y is None: - y = now_cursor_y - else: - if y < 0: - y = 0 - elif y >= height: - y = height - 1 - try: - if sys.platform in ["win32", "cygwin", "msys"]: - mouse.scroll(scroll_value, x, y) - elif sys.platform in ["darwin"]: - mouse.scroll(scroll_value) - elif sys.platform in ["linux", "linux2"]: - scroll_direction = special_mouse_keys_table.get(scroll_direction) - mouse.scroll(scroll_value, scroll_direction) - return scroll_value, scroll_direction - except AutoControlMouseException as error: - autocontrol_logger.error( - f"mouse_scroll, scroll_value: {scroll_value}, x: {x}, y: {y}, scroll_direction: {scroll_direction}, " - f"failed: {AutoControlMouseException(mouse_scroll + ' ' + repr(error))}" - ) - raise AutoControlMouseException(mouse_scroll + " " + repr(error)) - except Exception as error: - record_action_to_list("scroll", param, repr(error)) - autocontrol_logger.error( - f"mouse_scroll, scroll_value: {scroll_value}, x: {x}, y: {y}, scroll_direction: {scroll_direction}, " - f"failed: {repr(error)}" - ) - -def send_mouse_event_to_window(window, mouse_keycode: Union[int, str], x: int = None, y: int = None): - autocontrol_logger.info( - f"send_mouse_event_to_window window:{window} mouse_keycode:{mouse_keycode}, x: {x}, y: {y}" - ) - param = locals() + + # 邊界檢查 Boundary check + x = now_x if x is None else max(0, min(x, width - 1)) + y = now_y if y is None else max(0, min(y, height - 1)) + + if sys.platform in ["win32", "cygwin", "msys"]: + mouse.scroll(scroll_value, x, y) + elif sys.platform == "darwin": + mouse.scroll(scroll_value) + elif sys.platform in ["linux", "linux2"]: + scroll_direction = special_mouse_keys_table.get(scroll_direction, scroll_direction) + mouse.scroll(scroll_value, scroll_direction) + + record_action_to_list("mouse_scroll", param) + return scroll_value, scroll_direction + + except AutoControlMouseException as error: + autocontrol_logger.error(f"mouse_scroll failed: {repr(error)}") + raise AutoControlMouseException(mouse_scroll_error_message + " " + repr(error)) + + +def send_mouse_event_to_window(window, mouse_keycode: Union[int, str], + x: int = None, y: int = None) -> None: + """ + 將滑鼠事件送到指定視窗 + Send mouse event to a specific window + + :param window: 視窗 handle Window handle + :param mouse_keycode: 滑鼠按鍵代碼 Mouse keycode + :param x: X 座標 X position + :param y: Y 座標 Y position + """ + autocontrol_logger.info(f"send_mouse_event_to_window, window={window}, keycode={mouse_keycode}, x={x}, y={y}") + param = {"window": window, "keycode": mouse_keycode, "x": x, "y": y} try: - if sys.platform in ["darwin"]: + if sys.platform == "darwin": + autocontrol_logger.warning("send_mouse_event_to_window not supported on macOS") return + mouse_keycode, x, y = mouse_preprocess(mouse_keycode, x, y) - mouse.send_mouse_event_to_window( - window, - mouse_keycode=mouse_keycode, - x=x, - y=y, - ) + mouse.send_mouse_event_to_window(window, mouse_keycode=mouse_keycode, x=x, y=y) + record_action_to_list("send_mouse_event_to_window", param) + except Exception as error: record_action_to_list("send_mouse_event_to_window", param, repr(error)) - autocontrol_logger.error( - f"send_mouse_event_to_window window:{window} mouse_keycode:{mouse_keycode}, x: {x}, y: {y}" - f"failed: {repr(error)}" - ) \ No newline at end of file + autocontrol_logger.error(f"send_mouse_event_to_window failed: {repr(error)}") diff --git a/je_auto_control/wrapper/auto_control_record.py b/je_auto_control/wrapper/auto_control_record.py index 256d7e4..f5c81fb 100644 --- a/je_auto_control/wrapper/auto_control_record.py +++ b/je_auto_control/wrapper/auto_control_record.py @@ -1,6 +1,6 @@ import sys -from je_auto_control.utils.exception.exception_tags import macos_record_error +from je_auto_control.utils.exception.exception_tags import macos_record_error_message from je_auto_control.utils.exception.exceptions import AutoControlException from je_auto_control.utils.exception.exceptions import AutoControlJsonActionException from je_auto_control.utils.logging.loggin_instance import autocontrol_logger @@ -15,7 +15,7 @@ def record() -> None: autocontrol_logger.info("record") try: if sys.platform == "darwin": - raise AutoControlException(macos_record_error) + raise AutoControlException(macos_record_error_message) record_action_to_list("record", None) recorder.record() except Exception as error: @@ -30,7 +30,7 @@ def stop_record() -> list: autocontrol_logger.info("stop_record") try: if sys.platform == "darwin": - raise AutoControlException(macos_record_error) + raise AutoControlException(macos_record_error_message) action_queue = recorder.stop_record() if action_queue is None: raise AutoControlJsonActionException diff --git a/je_auto_control/wrapper/auto_control_screen.py b/je_auto_control/wrapper/auto_control_screen.py index 2f929ee..fe34254 100644 --- a/je_auto_control/wrapper/auto_control_screen.py +++ b/je_auto_control/wrapper/auto_control_screen.py @@ -4,8 +4,8 @@ import numpy as np from je_auto_control.utils.cv2_utils.screenshot import pil_screenshot -from je_auto_control.utils.exception.exception_tags import screen_get_size -from je_auto_control.utils.exception.exception_tags import screen_screenshot +from je_auto_control.utils.exception.exception_tags import screen_get_size_error_message +from je_auto_control.utils.exception.exception_tags import screen_screenshot_error_message from je_auto_control.utils.exception.exceptions import AutoControlScreenException from je_auto_control.utils.logging.loggin_instance import autocontrol_logger from je_auto_control.utils.test_record.record_test_class import record_action_to_list @@ -22,8 +22,8 @@ def screen_size() -> Tuple[int, int]: record_action_to_list("size", None) return screen.size() except AutoControlScreenException: - autocontrol_logger.error(f"screen_size, failed: {repr(AutoControlScreenException(screen_get_size))}") - raise AutoControlScreenException(screen_get_size) + autocontrol_logger.error(f"screen_size, failed: {repr(AutoControlScreenException(screen_get_size_error_message))}") + raise AutoControlScreenException(screen_get_size_error_message) except Exception as error: record_action_to_list("size", None, repr(error)) autocontrol_logger.error(f"screen_size, failed: {repr(error)}") @@ -45,8 +45,8 @@ def screenshot(file_path: str = None, screen_region: list = None) -> List[int]: except AutoControlScreenException as error: autocontrol_logger.info( f"screen_size, file_path: {file_path}, screen_region: {screen_region}, " - f"failed: {AutoControlScreenException(screen_screenshot + ' ' + repr(error))}") - raise AutoControlScreenException(screen_screenshot + " " + repr(error)) + f"failed: {AutoControlScreenException(screen_screenshot_error_message + ' ' + repr(error))}") + raise AutoControlScreenException(screen_screenshot_error_message + " " + repr(error)) except Exception as error: record_action_to_list("AC_screenshot", None, repr(error)) autocontrol_logger.info( diff --git a/pyproject.toml b/pyproject.toml index 75c5881..a485869 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" [project] name = "je_auto_control" -version = "0.0.176" +version = "0.0.177" authors = [ { name = "JE-Chen", email = "jechenmailman@gmail.com" }, ] diff --git a/test/unit_test/get_info/special_info.py b/test/unit_test/get_info/special_info.py deleted file mode 100644 index ed949aa..0000000 --- a/test/unit_test/get_info/special_info.py +++ /dev/null @@ -1,3 +0,0 @@ -from je_auto_control import get_special_table - -print(get_special_table()) diff --git a/test/unit_test/keyboard/keyboard_type_test.py b/test/unit_test/keyboard/keyboard_type_test.py index e3eeab4..4bfe8e7 100644 --- a/test/unit_test/keyboard/keyboard_type_test.py +++ b/test/unit_test/keyboard/keyboard_type_test.py @@ -6,4 +6,4 @@ assert (type_keyboard("T") == "T") assert (type_keyboard("E") == "E") assert (type_keyboard("S") == "S") -assert (type_keyboard("T") == "T") +assert (type_keyboard("T") == "T") \ No newline at end of file diff --git a/test/unit_test/mouse/mouse_scroll_test.py b/test/unit_test/mouse/mouse_scroll_test.py index cd5c09a..649103a 100644 --- a/test/unit_test/mouse/mouse_scroll_test.py +++ b/test/unit_test/mouse/mouse_scroll_test.py @@ -1,6 +1,6 @@ from time import sleep -from je_auto_control import mouse_scroll +from je_auto_control import mouse_scroll_error_message sleep(3) -mouse_scroll(100) +mouse_scroll_error_message(100) diff --git a/test/unit_test/mouse/mouse_test.py b/test/unit_test/mouse/mouse_test.py index d6117c4..63a6251 100644 --- a/test/unit_test/mouse/mouse_test.py +++ b/test/unit_test/mouse/mouse_test.py @@ -3,8 +3,8 @@ from je_auto_control import AutoControlMouseException from je_auto_control import click_mouse -from je_auto_control import mouse_keys_table from je_auto_control import get_mouse_position +from je_auto_control import mouse_keys_table from je_auto_control import press_mouse from je_auto_control import release_mouse from je_auto_control import set_mouse_position @@ -16,40 +16,40 @@ print(mouse_keys_table.keys()) -press_mouse("AC_mouse_right") -release_mouse("AC_mouse_right") -press_mouse("AC_mouse_left") -release_mouse("AC_mouse_left") -click_mouse("AC_mouse_left") +press_mouse("mouse_right") +release_mouse("mouse_right") +press_mouse("mouse_left") +release_mouse("mouse_left") +click_mouse("mouse_left") try: set_mouse_position(6468684648, 4686468648864684684) except AutoControlMouseException as error: print(repr(error), file=sys.stderr) try: click_mouse("dawdawddadaawd") -except AutoControlMouseException as error: +except Exception as error: print(repr(error), file=sys.stderr) try: press_mouse("dawdawdawdawd") -except AutoControlMouseException as error: +except Exception as error: print(repr(error), file=sys.stderr) try: release_mouse("dwadawdadwdada") -except AutoControlMouseException as error: +except Exception as error: print(repr(error), file=sys.stderr) try: press_mouse(16515588646) -except AutoControlMouseException as error: +except Exception as error: print(repr(error), file=sys.stderr) try: release_mouse(1651651915) -except AutoControlMouseException as error: +except Exception as error: print(repr(error), file=sys.stderr) try: press_mouse("AC_mouse_left") -except AutoControlMouseException as error: +except Exception as error: print(repr(error), file=sys.stderr) try: release_mouse("AC_mouse_left") -except AutoControlMouseException as error: +except Exception as error: print(repr(error), file=sys.stderr) diff --git a/test/unit_test/timeout/timeout_test.py b/test/unit_test/timeout/timeout_test.py deleted file mode 100644 index fa2d3b9..0000000 --- a/test/unit_test/timeout/timeout_test.py +++ /dev/null @@ -1,21 +0,0 @@ -from itertools import count -from time import sleep - -from je_auto_control import multiprocess_timeout - -counter = count(1) - - -def time_not_out_function(): - print("Hello") - - -def time_out_test_function(): - while True: - sleep(1) - print(next(counter)) - - -if __name__ == "__main__": - print(multiprocess_timeout(time_not_out_function, 5)) - print(multiprocess_timeout(time_out_test_function, 5))