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 @@
-
-
+
@@ -74,52 +73,56 @@
- {
- "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 @@
-
-
+
+
@@ -139,12 +142,12 @@
-
+
-
+
@@ -153,7 +156,7 @@
-
+
@@ -162,12 +165,12 @@
-
+
-
+
@@ -176,7 +179,7 @@
-
+
@@ -185,12 +188,12 @@
-
+
-
+
@@ -199,7 +202,7 @@
-
+
@@ -208,12 +211,12 @@
-
+
-
+
@@ -247,19 +250,19 @@
+
+
+
-
-
-
-
-
+
+
@@ -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
-[](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.
-[](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
-[](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.
-[](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.
-[](https://github.com/Intergration-Automation-Testing/AutoControl/actions/workflows/stable_python3_10.yml)
-
-[](https://github.com/Intergration-Automation-Testing/AutoControl/actions/workflows/stable_python3_11.yml)
-
-### Documentation
-[](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
-
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"""
+
+
| Test Report |
-
-
+
+
| function_name |
{function_name} |
@@ -106,61 +91,73 @@
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))