From dbaf92a3c5f82b58206f15af69b16ec088ba1ae5 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Sat, 4 Apr 2026 13:36:40 +0800 Subject: [PATCH 1/4] Update dev version * Refactor * Update GUI * Update GitHub Actions --- .claude/settings.local.json | 15 + .../workflows/{dev_python3_10.yml => dev.yml} | 77 +- .github/workflows/dev_python3_11.yml | 95 -- .github/workflows/dev_python3_12.yml | 95 -- .../{stable_python3_11.yml => stable.yml} | 77 +- .github/workflows/stable_python3_10.yml | 95 -- .github/workflows/stable_python3_12.yml | 95 -- docs/source/API/special/keyboard_keys.rst | 2 +- je_auto_control/__init__.py | 10 +- je_auto_control/__main__.py | 10 +- je_auto_control/gui/__init__.py | 12 + .../gui/language_wrapper/english.py | 103 +- .../multi_language_wrapper.py | 2 +- .../language_wrapper/traditional_chinese.py | 101 +- je_auto_control/gui/main_widget.py | 1016 ++++++++++++++--- je_auto_control/gui/main_window.py | 63 +- .../core/utils/x11_linux_display.py | 2 +- .../linux_with_x11/core/utils/x11_linux_vk.py | 6 +- .../listener/x11_linux_listener.py | 8 +- .../mouse/x11_linux_mouse_control.py | 6 +- je_auto_control/osx/core/utils/osx_vk.py | 2 +- je_auto_control/osx/listener/osx_listener.py | 1 - je_auto_control/osx/pid/pid_control.py | 6 +- je_auto_control/osx/screen/osx_screen.py | 54 +- .../callback/callback_function_executor.py | 4 +- .../{critcal_exit.py => critical_exit.py} | 0 .../utils/cv2_utils/screen_record.py | 25 +- .../utils/cv2_utils/video_recording.py | 2 +- .../utils/executor/action_executor.py | 6 +- .../generate_report/generate_html_report.py | 2 +- .../generate_report/generate_json_report.py | 2 +- .../generate_report/generate_xml_report.py | 2 +- ...loggin_instance.py => logging_instance.py} | 0 .../package_manager/package_manager_class.py | 2 +- .../utils/project/create_project_structure.py | 12 +- .../utils/shell_process/shell_exec.py | 2 +- .../auto_control_socket_server.py | 18 +- .../utils/start_exe/start_another_process.py | 4 +- .../utils/test_record/record_test_class.py | 2 +- .../change_xml_structure.py | 12 +- .../windows/core/utils/win32_vk.py | 2 +- .../listener/win32_keyboard_listener.py | 4 +- .../windows/listener/win32_mouse_listener.py | 4 +- .../windows/screen/win32_screen.py | 4 +- je_auto_control/wrapper/auto_control_image.py | 2 +- .../wrapper/auto_control_keyboard.py | 2 +- je_auto_control/wrapper/auto_control_mouse.py | 8 +- .../wrapper/auto_control_record.py | 4 +- .../wrapper/auto_control_screen.py | 65 +- je_auto_control/wrapper/platform_wrapper.py | 6 +- pyproject.toml | 10 +- dev.toml => stable.toml | 10 +- test/unit_test/argparse/argparse_test.py | 9 +- .../exception/auto_control_exception_test.py | 25 +- .../unit_test/keyboard/keyboard_write_test.py | 6 +- test/unit_test/mouse/mouse_scroll_test.py | 4 +- test/unit_test/mouse/mouse_test.py | 37 +- test/unit_test/screen/screen_test.py | 6 +- .../socket_server_test/socket_client_test.py | 2 +- .../total_record/total_record_test.py | 34 +- 60 files changed, 1450 insertions(+), 842 deletions(-) create mode 100644 .claude/settings.local.json rename .github/workflows/{dev_python3_10.yml => dev.yml} (57%) delete mode 100644 .github/workflows/dev_python3_11.yml delete mode 100644 .github/workflows/dev_python3_12.yml rename .github/workflows/{stable_python3_11.yml => stable.yml} (57%) delete mode 100644 .github/workflows/stable_python3_10.yml delete mode 100644 .github/workflows/stable_python3_12.yml rename je_auto_control/utils/critical_exit/{critcal_exit.py => critical_exit.py} (100%) rename je_auto_control/utils/logging/{loggin_instance.py => logging_instance.py} (100%) rename dev.toml => stable.toml (89%) diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..581d686 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,15 @@ +{ + "permissions": { + "allow": [ + "Bash(python -c \"from je_auto_control.gui import start_autocontrol_gui; print\\('Import OK'\\)\")", + "Bash(py -c \"from je_auto_control.gui import start_autocontrol_gui; print\\('Import OK'\\)\")", + "Bash(py -m pip install -r dev_requirements.txt)", + "Bash(py -c \"from je_auto_control import start_autocontrol_gui; print\\('Entry point import OK'\\)\")", + "Bash(py -c \"import je_auto_control; print\\(type\\(je_auto_control.record\\)\\); print\\(type\\(je_auto_control.stop_record\\)\\); print\\(type\\(je_auto_control.create_project_dir\\)\\); print\\(type\\(je_auto_control.start_autocontrol_gui\\)\\)\")", + "Bash(py -c ':*)", + "Bash(py -m je_auto_control --execute_str '{\"auto_control\": []}')", + "Bash(py -m je_auto_control --execute_str \"{\\\\\"auto_control\\\\\": [{\\\\\"AC_screen_size\\\\\": {}}]}\")", + "Bash(py -m je_auto_control --execute_str \"{\\\\\"auto_control\\\\\": [[\\\\\"AC_screen_size\\\\\"]]}\")" + ] + } +} diff --git a/.github/workflows/dev_python3_10.yml b/.github/workflows/dev.yml similarity index 57% rename from .github/workflows/dev_python3_10.yml rename to .github/workflows/dev.yml index 271caac..691f3a5 100644 --- a/.github/workflows/dev_python3_10.yml +++ b/.github/workflows/dev.yml @@ -1,4 +1,4 @@ -name: AutoControl_Dev_Python3.10 +name: AutoControl Dev CI on: push: @@ -12,84 +12,101 @@ permissions: contents: read jobs: - build_dev_version: + test: runs-on: windows-2022 + strategy: + fail-fast: false + matrix: + python-version: [ "3.10", "3.11", "3.12" ] steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.10 - uses: actions/setup-python@v3 + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: ${{ matrix.python-version }} + - name: Install dependencies run: | python -m pip install --upgrade pip wheel pip install -r dev_requirements.txt - - name: Test Screen Module + # Screen tests + - name: Test Screen Size run: python ./test/unit_test/screen/screen_test.py - name: Test Screenshot run: python ./test/unit_test/screen/screenshot_test.py - name: Test Screen Get Pixel run: python ./test/unit_test/screen/get_pixel_test.py - - name: Save Screenshot Image + - name: Upload Screenshot Artifact uses: actions/upload-artifact@v4 + if: always() with: - name: screenshot_png + name: screenshot_png_${{ matrix.python-version }} path: test.png + if-no-files-found: ignore - - name: Test Keyboard type Function + # Keyboard tests + - name: Test Keyboard Type run: python ./test/unit_test/keyboard/keyboard_type_test.py - - name: Test Keyboard write Function - run: | - python ./test/unit_test/keyboard/keyboard_write_test.py - - name: Test Keyboard is_press Function + - name: Test Keyboard Write + run: python ./test/unit_test/keyboard/keyboard_write_test.py + - name: Test Keyboard Is Press run: python ./test/unit_test/keyboard/keyboard_is_press_test.py - - name: Test Keyboard hotkey Function + - name: Test Keyboard Hotkey run: python ./test/unit_test/keyboard/hotkey_test.py + + # Mouse tests - name: Test Mouse Module - run: | - python ./test/unit_test/mouse/mouse_test.py - exit 0 - - name: Test Scroll Module - run: python ./test/unit_test/exception/auto_control_exception_test.py + run: python ./test/unit_test/mouse/mouse_test.py + continue-on-error: true + + # Exception tests - name: Test Exceptions run: python ./test/unit_test/exception/auto_control_exception_test.py + # Critical exit tests - name: Test Critical Exit - run: | - python ./test/unit_test/critical_exit/critical_exit_test.py - exit 0 + run: python ./test/unit_test/critical_exit/critical_exit_test.py + continue-on-error: true - name: Test Real Critical Situation - run: | - python ./test/unit_test/critical_exit/real_critical_test.py - exit 0 + run: python ./test/unit_test/critical_exit/real_critical_test.py + continue-on-error: true + # Record tests - name: Test Record Module run: python ./test/unit_test/record/record_test.py - name: Test Total Record run: python ./test/unit_test/total_record/total_record_test.py + # Executor tests - name: Test Execute Action run: python ./test/unit_test/execute_action/execute_action_test.py - - name: Test Json Module + # JSON tests + - name: Test JSON Module run: python ./test/unit_test/json/json_test.py - - name: Test Generate Json Report + + # Report generation tests + - name: Test Generate JSON Report run: python ./test/unit_test/generate_report/json_report.py - name: Test Generate HTML Report run: python ./test/unit_test/generate_report/html_report_test.py + # Argparse test - name: Test Argparse run: python ./test/unit_test/argparse/argparse_test.py + # Callback test - name: Test Callback Module run: python ./test/unit_test/callback/callback_test.py - - name: Test Create Project Function + # Project creation test + - name: Test Create Project run: python ./test/unit_test/create_project_file/create_project_test.py + # Info tests - name: Test Get Mouse Info run: python ./test/unit_test/get_info/mouse_info.py - name: Test Get Keyboard Info - run: python ./test/unit_test/get_info/keyboard_info.py \ No newline at end of file + 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 deleted file mode 100644 index fb2fce6..0000000 --- a/.github/workflows/dev_python3_11.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: AutoControl_Dev_Python3.11 - -on: - push: - branches: [ "dev" ] - pull_request: - branches: [ "dev" ] - schedule: - - cron: "0 1 * * *" - -permissions: - contents: read - -jobs: - build_dev_version: - runs-on: windows-2022 - - steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.11 - uses: actions/setup-python@v3 - with: - python-version: "3.11" - - name: Install dependencies - run: | - python -m pip install --upgrade pip wheel - pip install -r dev_requirements.txt - - - name: Test Screen Module - run: python ./test/unit_test/screen/screen_test.py - - name: Test Screenshot - run: python ./test/unit_test/screen/screenshot_test.py - - name: Test Screen Get Pixel - run: python ./test/unit_test/screen/get_pixel_test.py - - name: Save Screenshot Image - uses: actions/upload-artifact@v4 - with: - name: screenshot_png - path: test.png - - - name: Test Keyboard type Function - run: python ./test/unit_test/keyboard/keyboard_type_test.py - - name: Test Keyboard write Function - run: | - python ./test/unit_test/keyboard/keyboard_write_test.py - - name: Test Keyboard is_press Function - run: python ./test/unit_test/keyboard/keyboard_is_press_test.py - - name: Test Keyboard hotkey Function - run: python ./test/unit_test/keyboard/hotkey_test.py - - name: Test Mouse Module - run: | - python ./test/unit_test/mouse/mouse_test.py - exit 0 - - name: Test Scroll Module - run: python ./test/unit_test/exception/auto_control_exception_test.py - - name: Test Exceptions - run: python ./test/unit_test/exception/auto_control_exception_test.py - - - name: Test Critical Exit - run: | - python ./test/unit_test/critical_exit/critical_exit_test.py - exit 0 - - name: Test Real Critical Situation - run: | - python ./test/unit_test/critical_exit/real_critical_test.py - exit 0 - - - name: Test Record Module - run: python ./test/unit_test/record/record_test.py - - name: Test Total Record - run: python ./test/unit_test/total_record/total_record_test.py - - - name: Test Execute Action - run: python ./test/unit_test/execute_action/execute_action_test.py - - - name: Test Json Module - 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 Generate HTML Report - run: python ./test/unit_test/generate_report/html_report_test.py - - - name: Test Argparse - run: python ./test/unit_test/argparse/argparse_test.py - - - name: Test Callback Module - run: python ./test/unit_test/callback/callback_test.py - - - name: Test Create Project Function - run: python ./test/unit_test/create_project_file/create_project_test.py - - - name: Test Get Mouse Info - run: python ./test/unit_test/get_info/mouse_info.py - - name: Test Get Keyboard Info - run: python ./test/unit_test/get_info/keyboard_info.py \ No newline at end of file diff --git a/.github/workflows/dev_python3_12.yml b/.github/workflows/dev_python3_12.yml deleted file mode 100644 index c0b5561..0000000 --- a/.github/workflows/dev_python3_12.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: AutoControl_Dev_Python3.12 - -on: - push: - branches: [ "dev" ] - pull_request: - branches: [ "dev" ] - schedule: - - cron: "0 1 * * *" - -permissions: - contents: read - -jobs: - build_dev_version: - runs-on: windows-2022 - - steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.12 - uses: actions/setup-python@v3 - with: - python-version: "3.12" - - name: Install dependencies - run: | - python -m pip install --upgrade pip wheel - pip install -r dev_requirements.txt - - - name: Test Screen Module - run: python ./test/unit_test/screen/screen_test.py - - name: Test Screenshot - run: python ./test/unit_test/screen/screenshot_test.py - - name: Test Screen Get Pixel - run: python ./test/unit_test/screen/get_pixel_test.py - - name: Save Screenshot Image - uses: actions/upload-artifact@v4 - with: - name: screenshot_png - path: test.png - - - name: Test Keyboard type Function - run: python ./test/unit_test/keyboard/keyboard_type_test.py - - name: Test Keyboard write Function - run: | - python ./test/unit_test/keyboard/keyboard_write_test.py - - name: Test Keyboard is_press Function - run: python ./test/unit_test/keyboard/keyboard_is_press_test.py - - name: Test Keyboard hotkey Function - run: python ./test/unit_test/keyboard/hotkey_test.py - - name: Test Mouse Module - run: | - python ./test/unit_test/mouse/mouse_test.py - exit 0 - - name: Test Scroll Module - run: python ./test/unit_test/exception/auto_control_exception_test.py - - name: Test Exceptions - run: python ./test/unit_test/exception/auto_control_exception_test.py - - - name: Test Critical Exit - run: | - python ./test/unit_test/critical_exit/critical_exit_test.py - exit 0 - - name: Test Real Critical Situation - run: | - python ./test/unit_test/critical_exit/real_critical_test.py - exit 0 - - - name: Test Record Module - run: python ./test/unit_test/record/record_test.py - - name: Test Total Record - run: python ./test/unit_test/total_record/total_record_test.py - - - name: Test Execute Action - run: python ./test/unit_test/execute_action/execute_action_test.py - - - name: Test Json Module - 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 Generate HTML Report - run: python ./test/unit_test/generate_report/html_report_test.py - - - name: Test Argparse - run: python ./test/unit_test/argparse/argparse_test.py - - - name: Test Callback Module - run: python ./test/unit_test/callback/callback_test.py - - - name: Test Create Project Function - run: python ./test/unit_test/create_project_file/create_project_test.py - - - name: Test Get Mouse Info - run: python ./test/unit_test/get_info/mouse_info.py - - name: Test Get Keyboard Info - run: python ./test/unit_test/get_info/keyboard_info.py \ No newline at end of file diff --git a/.github/workflows/stable_python3_11.yml b/.github/workflows/stable.yml similarity index 57% rename from .github/workflows/stable_python3_11.yml rename to .github/workflows/stable.yml index f94045a..29c2de3 100644 --- a/.github/workflows/stable_python3_11.yml +++ b/.github/workflows/stable.yml @@ -1,4 +1,4 @@ -name: AutoControl_Stable_Python3.11 +name: AutoControl Stable CI on: push: @@ -12,84 +12,101 @@ permissions: contents: read jobs: - build_dev_version: + test: runs-on: windows-2022 + strategy: + fail-fast: false + matrix: + python-version: [ "3.10", "3.11", "3.12" ] steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.11 - uses: actions/setup-python@v3 + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: ${{ matrix.python-version }} + - name: Install dependencies run: | python -m pip install --upgrade pip wheel pip install -r dev_requirements.txt - - name: Test Screen Module + # Screen tests + - name: Test Screen Size run: python ./test/unit_test/screen/screen_test.py - name: Test Screenshot run: python ./test/unit_test/screen/screenshot_test.py - name: Test Screen Get Pixel run: python ./test/unit_test/screen/get_pixel_test.py - - name: Save Screenshot Image + - name: Upload Screenshot Artifact uses: actions/upload-artifact@v4 + if: always() with: - name: screenshot_png + name: screenshot_png_${{ matrix.python-version }} path: test.png + if-no-files-found: ignore - - name: Test Keyboard type Function + # Keyboard tests + - name: Test Keyboard Type run: python ./test/unit_test/keyboard/keyboard_type_test.py - - name: Test Keyboard write Function - run: | - python ./test/unit_test/keyboard/keyboard_write_test.py - - name: Test Keyboard is_press Function + - name: Test Keyboard Write + run: python ./test/unit_test/keyboard/keyboard_write_test.py + - name: Test Keyboard Is Press run: python ./test/unit_test/keyboard/keyboard_is_press_test.py - - name: Test Keyboard hotkey Function + - name: Test Keyboard Hotkey run: python ./test/unit_test/keyboard/hotkey_test.py + + # Mouse tests - name: Test Mouse Module - run: | - python ./test/unit_test/mouse/mouse_test.py - exit 0 - - name: Test Scroll Module - run: python ./test/unit_test/exception/auto_control_exception_test.py + run: python ./test/unit_test/mouse/mouse_test.py + continue-on-error: true + + # Exception tests - name: Test Exceptions run: python ./test/unit_test/exception/auto_control_exception_test.py + # Critical exit tests - name: Test Critical Exit - run: | - python ./test/unit_test/critical_exit/critical_exit_test.py - exit 0 + run: python ./test/unit_test/critical_exit/critical_exit_test.py + continue-on-error: true - name: Test Real Critical Situation - run: | - python ./test/unit_test/critical_exit/real_critical_test.py - exit 0 + run: python ./test/unit_test/critical_exit/real_critical_test.py + continue-on-error: true + # Record tests - name: Test Record Module run: python ./test/unit_test/record/record_test.py - name: Test Total Record run: python ./test/unit_test/total_record/total_record_test.py + # Executor tests - name: Test Execute Action run: python ./test/unit_test/execute_action/execute_action_test.py - - name: Test Json Module + # JSON tests + - name: Test JSON Module run: python ./test/unit_test/json/json_test.py - - name: Test Generate Json Report + + # Report generation tests + - name: Test Generate JSON Report run: python ./test/unit_test/generate_report/json_report.py - name: Test Generate HTML Report run: python ./test/unit_test/generate_report/html_report_test.py + # Argparse test - name: Test Argparse run: python ./test/unit_test/argparse/argparse_test.py + # Callback test - name: Test Callback Module run: python ./test/unit_test/callback/callback_test.py - - name: Test Create Project Function + # Project creation test + - name: Test Create Project run: python ./test/unit_test/create_project_file/create_project_test.py + # Info tests - name: Test Get Mouse Info run: python ./test/unit_test/get_info/mouse_info.py - name: Test Get Keyboard Info - run: python ./test/unit_test/get_info/keyboard_info.py \ No newline at end of file + 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 deleted file mode 100644 index aba9ba7..0000000 --- a/.github/workflows/stable_python3_10.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: AutoControl_Stable_Python3.10 - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - schedule: - - cron: "0 1 * * *" - -permissions: - contents: read - -jobs: - build_dev_version: - runs-on: windows-2022 - - steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.10 - uses: actions/setup-python@v3 - with: - python-version: "3.10" - - name: Install dependencies - run: | - python -m pip install --upgrade pip wheel - pip install -r dev_requirements.txt - - - name: Test Screen Module - run: python ./test/unit_test/screen/screen_test.py - - name: Test Screenshot - run: python ./test/unit_test/screen/screenshot_test.py - - name: Test Screen Get Pixel - run: python ./test/unit_test/screen/get_pixel_test.py - - name: Save Screenshot Image - uses: actions/upload-artifact@v4 - with: - name: screenshot_png - path: test.png - - - name: Test Keyboard type Function - run: python ./test/unit_test/keyboard/keyboard_type_test.py - - name: Test Keyboard write Function - run: | - python ./test/unit_test/keyboard/keyboard_write_test.py - - name: Test Keyboard is_press Function - run: python ./test/unit_test/keyboard/keyboard_is_press_test.py - - name: Test Keyboard hotkey Function - run: python ./test/unit_test/keyboard/hotkey_test.py - - name: Test Mouse Module - run: | - python ./test/unit_test/mouse/mouse_test.py - exit 0 - - name: Test Scroll Module - run: python ./test/unit_test/exception/auto_control_exception_test.py - - name: Test Exceptions - run: python ./test/unit_test/exception/auto_control_exception_test.py - - - name: Test Critical Exit - run: | - python ./test/unit_test/critical_exit/critical_exit_test.py - exit 0 - - name: Test Real Critical Situation - run: | - python ./test/unit_test/critical_exit/real_critical_test.py - exit 0 - - - name: Test Record Module - run: python ./test/unit_test/record/record_test.py - - name: Test Total Record - run: python ./test/unit_test/total_record/total_record_test.py - - - name: Test Execute Action - run: python ./test/unit_test/execute_action/execute_action_test.py - - - name: Test Json Module - 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 Generate HTML Report - run: python ./test/unit_test/generate_report/html_report_test.py - - - name: Test Argparse - run: python ./test/unit_test/argparse/argparse_test.py - - - name: Test Callback Module - run: python ./test/unit_test/callback/callback_test.py - - - name: Test Create Project Function - run: python ./test/unit_test/create_project_file/create_project_test.py - - - name: Test Get Mouse Info - run: python ./test/unit_test/get_info/mouse_info.py - - name: Test Get Keyboard Info - run: python ./test/unit_test/get_info/keyboard_info.py \ No newline at end of file diff --git a/.github/workflows/stable_python3_12.yml b/.github/workflows/stable_python3_12.yml deleted file mode 100644 index e9a146f..0000000 --- a/.github/workflows/stable_python3_12.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: AutoControl_Stable_Python3.12 - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - schedule: - - cron: "0 1 * * *" - -permissions: - contents: read - -jobs: - build_dev_version: - runs-on: windows-2022 - - steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.12 - uses: actions/setup-python@v3 - with: - python-version: "3.12" - - name: Install dependencies - run: | - python -m pip install --upgrade pip wheel - pip install -r dev_requirements.txt - - - name: Test Screen Module - run: python ./test/unit_test/screen/screen_test.py - - name: Test Screenshot - run: python ./test/unit_test/screen/screenshot_test.py - - name: Test Screen Get Pixel - run: python ./test/unit_test/screen/get_pixel_test.py - - name: Save Screenshot Image - uses: actions/upload-artifact@v4 - with: - name: screenshot_png - path: test.png - - - name: Test Keyboard type Function - run: python ./test/unit_test/keyboard/keyboard_type_test.py - - name: Test Keyboard write Function - run: | - python ./test/unit_test/keyboard/keyboard_write_test.py - - name: Test Keyboard is_press Function - run: python ./test/unit_test/keyboard/keyboard_is_press_test.py - - name: Test Keyboard hotkey Function - run: python ./test/unit_test/keyboard/hotkey_test.py - - name: Test Mouse Module - run: | - python ./test/unit_test/mouse/mouse_test.py - exit 0 - - name: Test Scroll Module - run: python ./test/unit_test/exception/auto_control_exception_test.py - - name: Test Exceptions - run: python ./test/unit_test/exception/auto_control_exception_test.py - - - name: Test Critical Exit - run: | - python ./test/unit_test/critical_exit/critical_exit_test.py - exit 0 - - name: Test Real Critical Situation - run: | - python ./test/unit_test/critical_exit/real_critical_test.py - exit 0 - - - name: Test Record Module - run: python ./test/unit_test/record/record_test.py - - name: Test Total Record - run: python ./test/unit_test/total_record/total_record_test.py - - - name: Test Execute Action - run: python ./test/unit_test/execute_action/execute_action_test.py - - - name: Test Json Module - 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 Generate HTML Report - run: python ./test/unit_test/generate_report/html_report_test.py - - - name: Test Argparse - run: python ./test/unit_test/argparse/argparse_test.py - - - name: Test Callback Module - run: python ./test/unit_test/callback/callback_test.py - - - name: Test Create Project Function - run: python ./test/unit_test/create_project_file/create_project_test.py - - - name: Test Get Mouse Info - run: python ./test/unit_test/get_info/mouse_info.py - - name: Test Get Keyboard Info - run: python ./test/unit_test/get_info/keyboard_info.py \ No newline at end of file diff --git a/docs/source/API/special/keyboard_keys.rst b/docs/source/API/special/keyboard_keys.rst index 179f1ab..cb1d6e4 100644 --- a/docs/source/API/special/keyboard_keys.rst +++ b/docs/source/API/special/keyboard_keys.rst @@ -481,7 +481,7 @@ Keyboard keys API "|": osx_key_bar, ",": osx_key_comma, "<": osx_key_less, - "/": osx_key_salsh, + "/": osx_key_slash, "?": osx_key_question, ".": osx_key_period, ">": osx_key_greater, diff --git a/je_auto_control/__init__.py b/je_auto_control/__init__.py index 8064411..c25e439 100644 --- a/je_auto_control/__init__.py +++ b/je_auto_control/__init__.py @@ -6,7 +6,7 @@ from je_auto_control.utils.callback.callback_function_executor import \ callback_executor # Critical -from je_auto_control.utils.critical_exit.critcal_exit import CriticalExit +from je_auto_control.utils.critical_exit.critical_exit import CriticalExit from je_auto_control.utils.cv2_utils.screen_record import ScreenRecorder # utils cv2_utils from je_auto_control.utils.cv2_utils.screenshot import pil_screenshot @@ -92,6 +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 @@ -105,10 +106,12 @@ from je_auto_control.wrapper.auto_control_screen import screen_size from je_auto_control.wrapper.auto_control_screen import screenshot from je_auto_control.wrapper.auto_control_screen import get_pixel +# GUI +from je_auto_control.gui import start_autocontrol_gui __all__ = [ "click_mouse", "mouse_keys_table", "get_mouse_position", "press_mouse", "release_mouse", - "mouse_scroll_error_message", "set_mouse_position", "special_mouse_keys_table", + "mouse_scroll", "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", @@ -122,5 +125,6 @@ "generate_xml_report", "get_dir_files_as_list", "create_project_dir", "start_autocontrol_socket_server", "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" + "ScreenRecorder", "get_pixel", + "start_autocontrol_gui" ] diff --git a/je_auto_control/__main__.py b/je_auto_control/__main__.py index 330d0c0..6cb24d7 100644 --- a/je_auto_control/__main__.py +++ b/je_auto_control/__main__.py @@ -25,12 +25,10 @@ def preprocess_execute_files(file_path: str): def preprocess_read_str_execute_action(execute_str: str): - if sys.platform in ["win32", "cygwin", "msys"]: - json_data = json.loads(execute_str) - execute_str = json.loads(json_data) - else: - execute_str = json.loads(execute_str) - execute_action(execute_str) + json_data = json.loads(execute_str) + if isinstance(json_data, str): + json_data = json.loads(json_data) + execute_action(json_data) argparse_event_dict = { diff --git a/je_auto_control/gui/__init__.py b/je_auto_control/gui/__init__.py index e69de29..350707f 100644 --- a/je_auto_control/gui/__init__.py +++ b/je_auto_control/gui/__init__.py @@ -0,0 +1,12 @@ +import sys + +from PySide6.QtWidgets import QApplication + +from je_auto_control.gui.main_window import AutoControlGUIUI + + +def start_autocontrol_gui(): + app = QApplication(sys.argv) + window = AutoControlGUIUI() + window.show() + sys.exit(app.exec()) diff --git a/je_auto_control/gui/language_wrapper/english.py b/je_auto_control/gui/language_wrapper/english.py index 2d2563f..0d6c3ee 100644 --- a/je_auto_control/gui/language_wrapper/english.py +++ b/je_auto_control/gui/language_wrapper/english.py @@ -1,13 +1,26 @@ english_word_dict = { # Main "application_name": "AutoControlGUI", - # Widget - "interval_time": "Interval Time (s):", - "cursor_x": "Cursor X Position:", - "cursor_y": "Cursor Y Position:", + + # Tabs + "tab_auto_click": "Auto Click", + "tab_screenshot": "Screenshot", + "tab_image_detect": "Image Detection", + "tab_record": "Record / Playback", + "tab_script": "Script Executor", + "tab_screen_record": "Screen Recording", + "tab_shell": "Shell Command", + "tab_report": "Report", + + # Auto Click Tab + "interval_time": "Interval (ms):", + "cursor_x": "Cursor X:", + "cursor_y": "Cursor Y:", "mouse_button": "Mouse Button:", - "keyboard_button": "Keyboard Button:", + "keyboard_button": "Keyboard Key:", "click_type": "Click Type:", + "single_click": "Single Click", + "double_click": "Double Click", "input_method": "Input Method:", "mouse_radio": "Mouse", "keyboard_radio": "Keyboard", @@ -16,4 +29,84 @@ "times": "Times", "start": "Start", "stop": "Stop", + "hotkey_label": "Hotkey (comma separated):", + "hotkey_send": "Send Hotkey", + "write_label": "Write Text:", + "write_send": "Write", + "mouse_scroll_label": "Scroll Value:", + "scroll_direction_label": "Scroll Direction:", + "scroll_send": "Scroll", + "get_position": "Get Mouse Position", + "current_position": "Current Position:", + + # Screenshot Tab + "take_screenshot": "Take Screenshot", + "save_screenshot": "Save Screenshot", + "file_path_label": "File Path:", + "browse": "Browse", + "region_label": "Region (x1, y1, x2, y2):", + "get_pixel_label": "Get Pixel Color", + "pixel_x": "X:", + "pixel_y": "Y:", + "pixel_result": "Pixel Color:", + "screen_size_label": "Screen Size:", + "get_screen_size": "Get Screen Size", + + # Image Detection Tab + "template_image": "Template Image:", + "threshold_label": "Threshold (0.0~1.0):", + "locate_image": "Locate Image", + "locate_all": "Locate All", + "locate_click": "Locate & Click", + "detection_result": "Detection Result:", + "draw_image_check": "Draw Detection", + + # Record / Playback Tab + "start_record": "Start Record", + "stop_record": "Stop Record", + "playback": "Playback", + "save_record": "Save to JSON", + "load_record": "Load JSON", + "record_status": "Status:", + "record_idle": "Idle", + "record_recording": "Recording...", + "record_list_label": "Recorded Actions:", + + # Script Executor Tab + "load_script": "Load Script (JSON)", + "execute_script": "Execute", + "execute_dir_label": "Execute Directory:", + "execute_dir": "Execute All", + "script_content": "Script Content:", + "execution_result": "Execution Result:", + + # Screen Recording Tab + "recorder_name": "Recorder Name:", + "output_file": "Output File:", + "codec_label": "Codec:", + "fps_label": "FPS:", + "resolution_label": "Resolution (WxH):", + "start_screen_record": "Start Recording", + "stop_screen_record": "Stop Recording", + "screen_record_status": "Status:", + + # Shell Tab + "shell_command_label": "Shell Command:", + "execute_shell": "Execute", + "shell_output": "Output:", + "start_exe_label": "Executable Path:", + "start_exe": "Start Executable", + + # Report Tab + "report_name": "Report Name:", + "generate_html_report": "Generate HTML Report", + "generate_json_report": "Generate JSON Report", + "generate_xml_report": "Generate XML Report", + "enable_test_record": "Enable Test Record", + "disable_test_record": "Disable Test Record", + "test_record_status": "Test Record:", + "report_result": "Result:", + + # Language + "language_label": "Language:", } diff --git a/je_auto_control/gui/language_wrapper/multi_language_wrapper.py b/je_auto_control/gui/language_wrapper/multi_language_wrapper.py index 4842d7d..bc8a1df 100644 --- a/je_auto_control/gui/language_wrapper/multi_language_wrapper.py +++ b/je_auto_control/gui/language_wrapper/multi_language_wrapper.py @@ -1,6 +1,6 @@ from je_auto_control.gui.language_wrapper.english import english_word_dict from je_auto_control.gui.language_wrapper.traditional_chinese import traditional_chinese_word_dict -from je_auto_control.utils.logging.loggin_instance import autocontrol_logger +from je_auto_control.utils.logging.logging_instance import autocontrol_logger diff --git a/je_auto_control/gui/language_wrapper/traditional_chinese.py b/je_auto_control/gui/language_wrapper/traditional_chinese.py index 66e20bb..866674b 100644 --- a/je_auto_control/gui/language_wrapper/traditional_chinese.py +++ b/je_auto_control/gui/language_wrapper/traditional_chinese.py @@ -1,13 +1,26 @@ traditional_chinese_word_dict = { # Main "application_name": "AutoControlGUI", - # Widget - "interval_time": "間隔時間(秒):", - "cursor_x": "滑鼠游標 X 軸位置:", - "cursor_y": "滑鼠游標 Y 軸位置:", + + # Tabs + "tab_auto_click": "自動點擊", + "tab_screenshot": "螢幕截圖", + "tab_image_detect": "影像偵測", + "tab_record": "錄製 / 回放", + "tab_script": "腳本執行", + "tab_screen_record": "螢幕錄影", + "tab_shell": "Shell 命令", + "tab_report": "報告產生", + + # Auto Click Tab + "interval_time": "間隔時間 (ms):", + "cursor_x": "游標 X:", + "cursor_y": "游標 Y:", "mouse_button": "滑鼠按鍵:", "keyboard_button": "鍵盤按鍵:", "click_type": "點擊類型:", + "single_click": "單擊", + "double_click": "雙擊", "input_method": "輸入方式:", "mouse_radio": "滑鼠", "keyboard_radio": "鍵盤", @@ -16,4 +29,84 @@ "times": "次數", "start": "開始", "stop": "停止", + "hotkey_label": "組合鍵(逗號分隔):", + "hotkey_send": "送出組合鍵", + "write_label": "輸入文字:", + "write_send": "輸入", + "mouse_scroll_label": "滾動值:", + "scroll_direction_label": "滾動方向:", + "scroll_send": "滾動", + "get_position": "取得滑鼠位置", + "current_position": "目前位置:", + + # Screenshot Tab + "take_screenshot": "截取螢���", + "save_screenshot": "儲存截圖", + "file_path_label": "檔案路徑:", + "browse": "瀏覽", + "region_label": "區域 (x1, y1, x2, y2):", + "get_pixel_label": "取得像素顏色", + "pixel_x": "X:", + "pixel_y": "Y:", + "pixel_result": "像素顏色:", + "screen_size_label": "螢幕大小:", + "get_screen_size": "取得螢幕大小", + + # Image Detection Tab + "template_image": "模板影像:", + "threshold_label": "精確度 (0.0~1.0):", + "locate_image": "尋找影像", + "locate_all": "尋找全部", + "locate_click": "尋找並點擊", + "detection_result": "偵測結果:", + "draw_image_check": "標記偵測結果", + + # Record / Playback Tab + "start_record": "開始錄製", + "stop_record": "停止錄製", + "playback": "回放", + "save_record": "儲存為 JSON", + "load_record": "載入 JSON", + "record_status": "狀態:", + "record_idle": "閒置", + "record_recording": "錄製中...", + "record_list_label": "已錄製動作:", + + # Script Executor Tab + "load_script": "載入腳本 (JSON)", + "execute_script": "執行", + "execute_dir_label": "執行目錄:", + "execute_dir": "執行全部", + "script_content": "腳本內容:", + "execution_result": "執行結果:", + + # Screen Recording Tab + "recorder_name": "錄影器名稱:", + "output_file": "輸出檔案:", + "codec_label": "編碼器:", + "fps_label": "FPS:", + "resolution_label": "解析度 (寬x高):", + "start_screen_record": "開始錄影", + "stop_screen_record": "停止錄影", + "screen_record_status": "狀態:", + + # Shell Tab + "shell_command_label": "Shell 命令:", + "execute_shell": "執行", + "shell_output": "輸出:", + "start_exe_label": "可執行檔路徑:", + "start_exe": "啟動可執行檔", + + # Report Tab + "report_name": "報告名稱:", + "generate_html_report": "產生 HTML 報告", + "generate_json_report": "產生 JSON 報告", + "generate_xml_report": "產生 XML 報告", + "enable_test_record": "啟用測試紀錄", + "disable_test_record": "停用測試紀錄", + "test_record_status": "測試紀錄:", + "report_result": "結果:", + + # Language + "language_label": "語言:", } diff --git a/je_auto_control/gui/main_widget.py b/je_auto_control/gui/main_widget.py index 9e54e3d..7843d4d 100644 --- a/je_auto_control/gui/main_widget.py +++ b/je_auto_control/gui/main_widget.py @@ -1,215 +1,915 @@ -from PySide6.QtCore import QTimer -from PySide6.QtGui import QIntValidator, QKeyEvent, Qt +import json +import sys +from threading import Thread + +from PySide6.QtCore import QTimer, Signal, QObject +from PySide6.QtGui import QIntValidator, QDoubleValidator, QKeyEvent, Qt from PySide6.QtWidgets import ( QWidget, QLineEdit, QComboBox, QPushButton, QVBoxLayout, QLabel, - QGridLayout, QHBoxLayout, QRadioButton, QButtonGroup, QMessageBox + QGridLayout, QHBoxLayout, QRadioButton, QButtonGroup, QMessageBox, + QTabWidget, QTextEdit, QFileDialog, QCheckBox, QGroupBox, QSplitter, + QListWidget ) from je_auto_control.gui.language_wrapper.multi_language_wrapper import language_wrapper -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.platform_wrapper import keyboard_keys_table, mouse_keys_table +from je_auto_control.wrapper.auto_control_keyboard import ( + type_keyboard, press_keyboard_key, release_keyboard_key, hotkey, write, check_key_is_press +) +from je_auto_control.wrapper.auto_control_mouse import ( + click_mouse, get_mouse_position, set_mouse_position, press_mouse, release_mouse, mouse_scroll +) +from je_auto_control.wrapper.auto_control_screen import screen_size, screenshot, get_pixel +from je_auto_control.wrapper.auto_control_image import locate_all_image, locate_image_center, locate_and_click +from je_auto_control.wrapper.auto_control_record import record, stop_record +from je_auto_control.wrapper.auto_control_keyboard import get_keyboard_keys_table +from je_auto_control.wrapper.auto_control_mouse import mouse_keys_table, special_mouse_keys_table +from je_auto_control.utils.executor.action_executor import execute_action, execute_files +from je_auto_control.utils.json.json_file import read_action_json, write_action_json +from je_auto_control.utils.file_process.get_dir_file_list import get_dir_files_as_list +from je_auto_control.utils.cv2_utils.screen_record import ScreenRecorder +from je_auto_control.utils.shell_process.shell_exec import ShellManager +from je_auto_control.utils.start_exe.start_another_process import start_exe +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_report +from je_auto_control.utils.generate_report.generate_xml_report import generate_xml_report +from je_auto_control.utils.test_record.record_test_class import test_record_instance + + +def _t(key: str) -> str: + """language_wrapper shorthand""" + return language_wrapper.language_word_dict.get(key, key) +class _WorkerSignals(QObject): + finished = Signal(str) + error = Signal(str) + + +# ============================================================================= +# Main Widget +# ============================================================================= class AutoControlGUIWidget(QWidget): - """ - AutoControl GUI Widget - 自動控制 GUI 元件 - 提供滑鼠與鍵盤操作的自動化設定介面 - """ def __init__(self, parent=None): super().__init__(parent) + layout = QVBoxLayout() + + self.tabs = QTabWidget() + self.tabs.addTab(self._build_auto_click_tab(), _t("tab_auto_click")) + self.tabs.addTab(self._build_screenshot_tab(), _t("tab_screenshot")) + self.tabs.addTab(self._build_image_detect_tab(), _t("tab_image_detect")) + self.tabs.addTab(self._build_record_tab(), _t("tab_record")) + self.tabs.addTab(self._build_script_tab(), _t("tab_script")) + self.tabs.addTab(self._build_screen_record_tab(), _t("tab_screen_record")) + self.tabs.addTab(self._build_shell_tab(), _t("tab_shell")) + self.tabs.addTab(self._build_report_tab(), _t("tab_report")) + layout.addWidget(self.tabs) - main_layout = QVBoxLayout() + self.setLayout(layout) - # === Grid for input fields 輸入欄位區塊 === + # shared state + self.timer = QTimer() + self.repeat_count = 0 + self.repeat_max = 0 + self.screen_recorder = ScreenRecorder() + self._record_data = [] + + # ========================================================================= + # Tab 1: Auto Click + # ========================================================================= + def _build_auto_click_tab(self) -> QWidget: + tab = QWidget() + outer = QVBoxLayout() + + # --- Mouse / Keyboard click group --- + click_group = QGroupBox(_t("tab_auto_click")) grid = QGridLayout() + row = 0 - # 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) + grid.addWidget(QLabel(_t("input_method")), row, 0) + self.mouse_radio = QRadioButton(_t("mouse_radio")) + self.keyboard_radio = QRadioButton(_t("keyboard_radio")) + self.mouse_radio.setChecked(True) + self._input_group = QButtonGroup() + self._input_group.addButton(self.mouse_radio) + self._input_group.addButton(self.keyboard_radio) + h = QHBoxLayout() + h.addWidget(self.mouse_radio) + h.addWidget(self.keyboard_radio) + grid.addLayout(h, row, 1) - # Cursor X/Y 滑鼠座標 - grid.addWidget(QLabel(language_wrapper.language_word_dict.get("cursor_x")), 2, 0) + row += 1 + grid.addWidget(QLabel(_t("interval_time")), row, 0) + self.interval_input = QLineEdit("1000") + self.interval_input.setValidator(QIntValidator(1, 999999999)) + grid.addWidget(self.interval_input, row, 1) + + row += 1 + grid.addWidget(QLabel(_t("cursor_x")), row, 0) self.cursor_x_input = QLineEdit() self.cursor_x_input.setValidator(QIntValidator()) - grid.addWidget(self.cursor_x_input, 2, 1) + grid.addWidget(self.cursor_x_input, row, 1) - grid.addWidget(QLabel(language_wrapper.language_word_dict.get("cursor_y")), 3, 0) + row += 1 + grid.addWidget(QLabel(_t("cursor_y")), row, 0) self.cursor_y_input = QLineEdit() self.cursor_y_input.setValidator(QIntValidator()) - grid.addWidget(self.cursor_y_input, 3, 1) + grid.addWidget(self.cursor_y_input, row, 1) - # Mouse button 滑鼠按鍵 - grid.addWidget(QLabel(language_wrapper.language_word_dict.get("mouse_button")), 4, 0) + row += 1 + grid.addWidget(QLabel(_t("mouse_button")), row, 0) self.mouse_button_combo = QComboBox() - self.mouse_button_combo.addItems(mouse_keys_table) - grid.addWidget(self.mouse_button_combo, 4, 1) + self.mouse_button_combo.addItems(list(mouse_keys_table.keys()) if isinstance(mouse_keys_table, dict) else list(mouse_keys_table)) + grid.addWidget(self.mouse_button_combo, row, 1) - # Keyboard button 鍵盤按鍵 - grid.addWidget(QLabel(language_wrapper.language_word_dict.get("keyboard_button")), 5, 0) + row += 1 + grid.addWidget(QLabel(_t("keyboard_button")), row, 0) self.keyboard_button_combo = QComboBox() - self.keyboard_button_combo.addItems(keyboard_keys_table.keys()) - grid.addWidget(self.keyboard_button_combo, 5, 1) + self.keyboard_button_combo.addItems(list(get_keyboard_keys_table().keys())) + grid.addWidget(self.keyboard_button_combo, row, 1) - # Click type 點擊類型 - grid.addWidget(QLabel(language_wrapper.language_word_dict.get("click_type")), 6, 0) + row += 1 + grid.addWidget(QLabel(_t("click_type")), row, 0) self.click_type_combo = QComboBox() - self.click_type_combo.addItems(["Single Click", "Double Click"]) - grid.addWidget(self.click_type_combo, 6, 1) + self.click_type_combo.addItems([_t("single_click"), _t("double_click")]) + grid.addWidget(self.click_type_combo, row, 1) - # 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")) - self.mouse_radio.setChecked(True) - self.input_method_group = QButtonGroup() - self.input_method_group.addButton(self.mouse_radio) - self.input_method_group.addButton(self.keyboard_radio) - grid.addWidget(self.mouse_radio, 7, 1) - grid.addWidget(self.keyboard_radio, 7, 2) - - main_layout.addLayout(grid) - - # === 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")) + row += 1 + self.repeat_until_stopped = QRadioButton(_t("repeat_until_stopped_radio")) + self.repeat_count_times = QRadioButton(_t("repeat_radio")) self.repeat_count_input = QLineEdit() - self.repeat_count_input.setValidator(QIntValidator()) - self.repeat_count_input.setPlaceholderText(language_wrapper.language_word_dict.get("times")) - repeat_group = QButtonGroup() - repeat_group.addButton(self.repeat_until_stopped) - repeat_group.addButton(self.repeat_count_times) + self.repeat_count_input.setValidator(QIntValidator(1, 999999999)) + self.repeat_count_input.setPlaceholderText(_t("times")) + rg = QButtonGroup(tab) + rg.addButton(self.repeat_until_stopped) + rg.addButton(self.repeat_count_times) self.repeat_until_stopped.setChecked(True) - self.repeat_count = 0 - self.repeat_max = 0 + rh = QHBoxLayout() + rh.addWidget(self.repeat_until_stopped) + rh.addWidget(self.repeat_count_times) + rh.addWidget(self.repeat_count_input) + grid.addLayout(rh, row, 0, 1, 2) + + row += 1 + btn_h = QHBoxLayout() + self.start_button = QPushButton(_t("start")) + self.start_button.clicked.connect(self._start_auto_click) + self.stop_button = QPushButton(_t("stop")) + self.stop_button.clicked.connect(self._stop_auto_click) + btn_h.addWidget(self.start_button) + btn_h.addWidget(self.stop_button) + grid.addLayout(btn_h, row, 0, 1, 2) + + click_group.setLayout(grid) + outer.addWidget(click_group) - repeat_layout.addWidget(self.repeat_until_stopped) - repeat_layout.addWidget(self.repeat_count_times) - repeat_layout.addWidget(self.repeat_count_input) - main_layout.addLayout(repeat_layout) - - # === 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) - self.stop_button = QPushButton(language_wrapper.language_word_dict.get("stop")) - self.stop_button.clicked.connect(self.stop_autocontrol) - button_layout.addWidget(self.start_button) - button_layout.addWidget(self.stop_button) - main_layout.addLayout(button_layout) - - # Timer 計時器 - self.start_autocontrol_timer = QTimer() - - # 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 - 根據選擇的輸入方式啟用/停用相關欄位 - """ + # --- Mouse position --- + pos_group = QGroupBox(_t("get_position")) + pos_layout = QHBoxLayout() + self.pos_btn = QPushButton(_t("get_position")) + self.pos_btn.clicked.connect(self._get_mouse_pos) + self.pos_label = QLabel(_t("current_position") + " --") + pos_layout.addWidget(self.pos_btn) + pos_layout.addWidget(self.pos_label) + pos_group.setLayout(pos_layout) + outer.addWidget(pos_group) + + # --- Hotkey --- + hotkey_group = QGroupBox(_t("hotkey_label")) + hk_layout = QHBoxLayout() + self.hotkey_input = QLineEdit() + self.hotkey_input.setPlaceholderText("ctrl,a") + self.hotkey_btn = QPushButton(_t("hotkey_send")) + self.hotkey_btn.clicked.connect(self._send_hotkey) + hk_layout.addWidget(self.hotkey_input) + hk_layout.addWidget(self.hotkey_btn) + hotkey_group.setLayout(hk_layout) + outer.addWidget(hotkey_group) + + # --- Write text --- + write_group = QGroupBox(_t("write_label")) + wr_layout = QHBoxLayout() + self.write_input = QLineEdit() + self.write_btn = QPushButton(_t("write_send")) + self.write_btn.clicked.connect(self._send_write) + wr_layout.addWidget(self.write_input) + wr_layout.addWidget(self.write_btn) + write_group.setLayout(wr_layout) + outer.addWidget(write_group) + + # --- Scroll --- + scroll_group = QGroupBox(_t("mouse_scroll_label")) + sc_layout = QHBoxLayout() + self.scroll_value_input = QLineEdit("3") + self.scroll_value_input.setValidator(QIntValidator()) + sc_layout.addWidget(QLabel(_t("mouse_scroll_label"))) + sc_layout.addWidget(self.scroll_value_input) + if special_mouse_keys_table: + self.scroll_dir_combo = QComboBox() + self.scroll_dir_combo.addItems(list(special_mouse_keys_table.keys())) + sc_layout.addWidget(self.scroll_dir_combo) + else: + self.scroll_dir_combo = None + self.scroll_btn = QPushButton(_t("scroll_send")) + self.scroll_btn.clicked.connect(self._send_scroll) + sc_layout.addWidget(self.scroll_btn) + scroll_group.setLayout(sc_layout) + outer.addWidget(scroll_group) + + outer.addStretch() + + # toggle handler + self.mouse_radio.toggled.connect(self._update_click_mode) + self._update_click_mode() + + tab.setLayout(outer) + return tab + + def _update_click_mode(self): 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): - """ - Start auto control with timer - 啟動計時器開始自動控制 - """ + def _start_auto_click(self): try: interval = int(self.interval_input.text()) except ValueError: - QMessageBox.warning(self, "Warning", "Interval must be a number\n間隔必須是數字") + QMessageBox.warning(self, "Warning", "Interval must be a number") return - - self.start_autocontrol_timer.setInterval(interval) - self.start_autocontrol_timer.timeout.connect(self.start_timer_function) - self.start_autocontrol_timer.start() - + self.repeat_count = 0 try: self.repeat_max = int(self.repeat_count_input.text()) except ValueError: self.repeat_max = 0 + self.timer.setInterval(interval) + try: + self.timer.timeout.disconnect(self._timer_tick) + except RuntimeError: + pass + self.timer.timeout.connect(self._timer_tick) + self.timer.start() - # === 計時器觸發函式 === - def start_timer_function(self): - """ - Timer callback function - 計時器回呼函式 - """ + def _stop_auto_click(self): + self.timer.stop() + + def _timer_tick(self): if self.repeat_until_stopped.isChecked(): - self.trigger_autocontrol_function() + self._do_click() elif self.repeat_count_times.isChecked(): self.repeat_count += 1 - if self.repeat_count < self.repeat_max: - self.trigger_autocontrol_function() + if self.repeat_count <= self.repeat_max: + self._do_click() else: self.repeat_count = 0 - 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(): - button = self.mouse_button_combo.currentText() + self.timer.stop() + + def _do_click(self): + try: + is_double = self.click_type_combo.currentIndex() == 1 + if self.mouse_radio.isChecked(): + btn = self.mouse_button_combo.currentText() + x = int(self.cursor_x_input.text() or "0") + y = int(self.cursor_y_input.text() or "0") + click_mouse(btn, x, y) + if is_double: + click_mouse(btn, x, y) + else: + key = self.keyboard_button_combo.currentText() + type_keyboard(key) + if is_double: + type_keyboard(key) + except Exception as e: + self.timer.stop() + QMessageBox.warning(self, "Error", str(e)) + + def _get_mouse_pos(self): + try: + x, y = get_mouse_position() + self.pos_label.setText(_t("current_position") + f" ({x}, {y})") + self.cursor_x_input.setText(str(x)) + self.cursor_y_input.setText(str(y)) + except Exception as e: + QMessageBox.warning(self, "Error", str(e)) + + def _send_hotkey(self): + try: + keys = [k.strip() for k in self.hotkey_input.text().split(",") if k.strip()] + if keys: + hotkey(keys) + except Exception as e: + QMessageBox.warning(self, "Error", str(e)) + + def _send_write(self): + try: + text = self.write_input.text() + if text: + write(text) + except Exception as e: + QMessageBox.warning(self, "Error", str(e)) + + def _send_scroll(self): + try: + val = int(self.scroll_value_input.text() or "3") + direction = self.scroll_dir_combo.currentText() if self.scroll_dir_combo else "scroll_down" + mouse_scroll(val, scroll_direction=direction) + except Exception as e: + QMessageBox.warning(self, "Error", str(e)) + + # ========================================================================= + # Tab 2: Screenshot + # ========================================================================= + def _build_screenshot_tab(self) -> QWidget: + tab = QWidget() + layout = QVBoxLayout() + + # Screen size + size_group = QGroupBox(_t("screen_size_label")) + sg = QHBoxLayout() + self.screen_size_label = QLabel("--") + self.screen_size_btn = QPushButton(_t("get_screen_size")) + self.screen_size_btn.clicked.connect(self._get_screen_size) + sg.addWidget(self.screen_size_label) + sg.addWidget(self.screen_size_btn) + size_group.setLayout(sg) + layout.addWidget(size_group) + + # Screenshot + ss_group = QGroupBox(_t("take_screenshot")) + ss_grid = QGridLayout() + ss_grid.addWidget(QLabel(_t("file_path_label")), 0, 0) + self.ss_path_input = QLineEdit() + ss_grid.addWidget(self.ss_path_input, 0, 1) + self.ss_browse_btn = QPushButton(_t("browse")) + self.ss_browse_btn.clicked.connect(self._browse_ss_path) + ss_grid.addWidget(self.ss_browse_btn, 0, 2) + + ss_grid.addWidget(QLabel(_t("region_label")), 1, 0) + self.ss_region_input = QLineEdit() + self.ss_region_input.setPlaceholderText("0, 0, 800, 600") + ss_grid.addWidget(self.ss_region_input, 1, 1, 1, 2) + + btn_h = QHBoxLayout() + self.ss_take_btn = QPushButton(_t("take_screenshot")) + self.ss_take_btn.clicked.connect(self._take_screenshot) + btn_h.addWidget(self.ss_take_btn) + ss_grid.addLayout(btn_h, 2, 0, 1, 3) + ss_group.setLayout(ss_grid) + layout.addWidget(ss_group) + + # Get pixel + px_group = QGroupBox(_t("get_pixel_label")) + px_grid = QGridLayout() + px_grid.addWidget(QLabel(_t("pixel_x")), 0, 0) + self.pixel_x_input = QLineEdit("0") + self.pixel_x_input.setValidator(QIntValidator()) + px_grid.addWidget(self.pixel_x_input, 0, 1) + px_grid.addWidget(QLabel(_t("pixel_y")), 0, 2) + self.pixel_y_input = QLineEdit("0") + self.pixel_y_input.setValidator(QIntValidator()) + px_grid.addWidget(self.pixel_y_input, 0, 3) + self.pixel_btn = QPushButton(_t("get_pixel_label")) + self.pixel_btn.clicked.connect(self._get_pixel_color) + px_grid.addWidget(self.pixel_btn, 1, 0, 1, 2) + self.pixel_result_label = QLabel(_t("pixel_result") + " --") + px_grid.addWidget(self.pixel_result_label, 1, 2, 1, 2) + px_group.setLayout(px_grid) + layout.addWidget(px_group) + + self.ss_result_text = QTextEdit() + self.ss_result_text.setReadOnly(True) + self.ss_result_text.setMaximumHeight(100) + layout.addWidget(self.ss_result_text) + layout.addStretch() + tab.setLayout(layout) + return tab + + def _get_screen_size(self): + try: + w, h = screen_size() + self.screen_size_label.setText(f"{w} x {h}") + except Exception as e: + QMessageBox.warning(self, "Error", str(e)) + + def _browse_ss_path(self): + path, _ = QFileDialog.getSaveFileName(self, _t("save_screenshot"), "", "PNG (*.png);;All (*)") + if path: + self.ss_path_input.setText(path) + + def _take_screenshot(self): + try: + path = self.ss_path_input.text() or None + region_text = self.ss_region_input.text().strip() + region = None + if region_text: + region = [int(x.strip()) for x in region_text.split(",")] + screenshot(file_path=path, screen_region=region) + self.ss_result_text.setText(f"Screenshot saved: {path or '(not saved)'}") + except Exception as e: + self.ss_result_text.setText(f"Error: {e}") + + def _get_pixel_color(self): + try: + x = int(self.pixel_x_input.text()) + y = int(self.pixel_y_input.text()) + color = get_pixel(x, y) + self.pixel_result_label.setText(_t("pixel_result") + f" {color}") + except Exception as e: + self.pixel_result_label.setText(f"Error: {e}") + + # ========================================================================= + # Tab 3: Image Detection + # ========================================================================= + def _build_image_detect_tab(self) -> QWidget: + tab = QWidget() + layout = QVBoxLayout() + + grid = QGridLayout() + grid.addWidget(QLabel(_t("template_image")), 0, 0) + self.img_path_input = QLineEdit() + grid.addWidget(self.img_path_input, 0, 1) + self.img_browse_btn = QPushButton(_t("browse")) + self.img_browse_btn.clicked.connect(self._browse_img) + grid.addWidget(self.img_browse_btn, 0, 2) + + grid.addWidget(QLabel(_t("threshold_label")), 1, 0) + self.threshold_input = QLineEdit("0.8") + self.threshold_input.setValidator(QDoubleValidator(0.0, 1.0, 2)) + grid.addWidget(self.threshold_input, 1, 1) + self.draw_check = QCheckBox(_t("draw_image_check")) + grid.addWidget(self.draw_check, 1, 2) + + layout.addLayout(grid) + + btn_h = QHBoxLayout() + self.locate_btn = QPushButton(_t("locate_image")) + self.locate_btn.clicked.connect(self._locate_image) + self.locate_all_btn = QPushButton(_t("locate_all")) + self.locate_all_btn.clicked.connect(self._locate_all) + self.locate_click_btn = QPushButton(_t("locate_click")) + self.locate_click_btn.clicked.connect(self._locate_click) + btn_h.addWidget(self.locate_btn) + btn_h.addWidget(self.locate_all_btn) + btn_h.addWidget(self.locate_click_btn) + layout.addLayout(btn_h) + + layout.addWidget(QLabel(_t("detection_result"))) + self.detect_result_text = QTextEdit() + self.detect_result_text.setReadOnly(True) + layout.addWidget(self.detect_result_text) + tab.setLayout(layout) + return tab + + def _browse_img(self): + path, _ = QFileDialog.getOpenFileName(self, _t("template_image"), "", "Images (*.png *.jpg *.bmp);;All (*)") + if path: + self.img_path_input.setText(path) + + def _get_detect_params(self): + path = self.img_path_input.text() + if not path: + raise ValueError("Template image path is empty") + threshold = float(self.threshold_input.text() or "0.8") + draw = self.draw_check.isChecked() + return path, threshold, draw + + def _locate_image(self): + try: + path, th, draw = self._get_detect_params() + result = locate_image_center(path, th, draw) + self.detect_result_text.setText(f"Center: {result}") + except Exception as e: + self.detect_result_text.setText(f"Error: {e}") + + def _locate_all(self): + try: + path, th, draw = self._get_detect_params() + result = locate_all_image(path, th, draw) + self.detect_result_text.setText(f"Found {len(result)} matches:\n{result}") + except Exception as e: + self.detect_result_text.setText(f"Error: {e}") + + def _locate_click(self): + try: + path, th, draw = self._get_detect_params() + btn = self.mouse_button_combo.currentText() if hasattr(self, "mouse_button_combo") else "mouse_left" + result = locate_and_click(path, btn, th, draw) + self.detect_result_text.setText(f"Clicked at: {result}") + except Exception as e: + self.detect_result_text.setText(f"Error: {e}") + + # ========================================================================= + # Tab 4: Record / Playback + # ========================================================================= + def _build_record_tab(self) -> QWidget: + tab = QWidget() + layout = QVBoxLayout() + + self.record_status_label = QLabel(_t("record_status") + " " + _t("record_idle")) + layout.addWidget(self.record_status_label) + + btn_h = QHBoxLayout() + self.rec_start_btn = QPushButton(_t("start_record")) + self.rec_start_btn.clicked.connect(self._start_record) + self.rec_stop_btn = QPushButton(_t("stop_record")) + self.rec_stop_btn.clicked.connect(self._stop_record) + self.rec_play_btn = QPushButton(_t("playback")) + self.rec_play_btn.clicked.connect(self._playback_record) + btn_h.addWidget(self.rec_start_btn) + btn_h.addWidget(self.rec_stop_btn) + btn_h.addWidget(self.rec_play_btn) + layout.addLayout(btn_h) + + btn_h2 = QHBoxLayout() + self.rec_save_btn = QPushButton(_t("save_record")) + self.rec_save_btn.clicked.connect(self._save_record) + self.rec_load_btn = QPushButton(_t("load_record")) + self.rec_load_btn.clicked.connect(self._load_record) + btn_h2.addWidget(self.rec_save_btn) + btn_h2.addWidget(self.rec_load_btn) + layout.addLayout(btn_h2) + + layout.addWidget(QLabel(_t("record_list_label"))) + self.record_list_text = QTextEdit() + self.record_list_text.setReadOnly(True) + layout.addWidget(self.record_list_text) + tab.setLayout(layout) + return tab + + def _start_record(self): + try: + record() + self.record_status_label.setText(_t("record_status") + " " + _t("record_recording")) + except Exception as e: + QMessageBox.warning(self, "Error", str(e)) + + def _stop_record(self): + try: + self._record_data = stop_record() or [] + self.record_status_label.setText(_t("record_status") + " " + _t("record_idle")) + self.record_list_text.setText(json.dumps(self._record_data, indent=2, ensure_ascii=False)) + except Exception as e: + QMessageBox.warning(self, "Error", str(e)) + + def _playback_record(self): + try: + if not self._record_data: + QMessageBox.warning(self, "Warning", "No recorded data") + return + execute_action(self._record_data) + except Exception as e: + QMessageBox.warning(self, "Error", str(e)) + + def _save_record(self): + try: + if not self._record_data: + QMessageBox.warning(self, "Warning", "No recorded data") + return + path, _ = QFileDialog.getSaveFileName(self, _t("save_record"), "", "JSON (*.json)") + if path: + write_action_json(path, self._record_data) + except Exception as e: + QMessageBox.warning(self, "Error", str(e)) + + def _load_record(self): + try: + path, _ = QFileDialog.getOpenFileName(self, _t("load_record"), "", "JSON (*.json)") + if path: + self._record_data = read_action_json(path) + self.record_list_text.setText(json.dumps(self._record_data, indent=2, ensure_ascii=False)) + except Exception as e: + QMessageBox.warning(self, "Error", str(e)) + + # ========================================================================= + # Tab 5: Script Executor + # ========================================================================= + def _build_script_tab(self) -> QWidget: + tab = QWidget() + layout = QVBoxLayout() + + # Load / execute single file + file_h = QHBoxLayout() + self.script_path_input = QLineEdit() + self.script_browse_btn = QPushButton(_t("load_script")) + self.script_browse_btn.clicked.connect(self._browse_script) + self.script_exec_btn = QPushButton(_t("execute_script")) + self.script_exec_btn.clicked.connect(self._execute_script) + file_h.addWidget(self.script_path_input) + file_h.addWidget(self.script_browse_btn) + file_h.addWidget(self.script_exec_btn) + layout.addLayout(file_h) + + # Execute directory + dir_h = QHBoxLayout() + self.script_dir_input = QLineEdit() + self.script_dir_browse_btn = QPushButton(_t("execute_dir_label")) + self.script_dir_browse_btn.clicked.connect(self._browse_script_dir) + self.script_dir_exec_btn = QPushButton(_t("execute_dir")) + self.script_dir_exec_btn.clicked.connect(self._execute_dir) + dir_h.addWidget(self.script_dir_input) + dir_h.addWidget(self.script_dir_browse_btn) + dir_h.addWidget(self.script_dir_exec_btn) + layout.addLayout(dir_h) + + # Manual JSON input + layout.addWidget(QLabel(_t("script_content"))) + self.script_editor = QTextEdit() + self.script_editor.setPlaceholderText('[["AC_type_keyboard", {"keycode": "a"}]]') + layout.addWidget(self.script_editor) + + exec_btn = QPushButton(_t("execute_script")) + exec_btn.clicked.connect(self._execute_manual_script) + layout.addWidget(exec_btn) + + layout.addWidget(QLabel(_t("execution_result"))) + self.script_result_text = QTextEdit() + self.script_result_text.setReadOnly(True) + layout.addWidget(self.script_result_text) + tab.setLayout(layout) + return tab + + def _browse_script(self): + path, _ = QFileDialog.getOpenFileName(self, _t("load_script"), "", "JSON (*.json)") + if path: + self.script_path_input.setText(path) 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座標必須是數字") + data = read_action_json(path) + self.script_editor.setText(json.dumps(data, indent=2, ensure_ascii=False)) + except Exception as e: + self.script_result_text.setText(f"Error loading: {e}") + + def _execute_script(self): + try: + path = self.script_path_input.text() + if not path: + return + data = read_action_json(path) + result = execute_action(data) + self.script_result_text.setText(json.dumps(result, indent=2, default=str, ensure_ascii=False)) + except Exception as e: + self.script_result_text.setText(f"Error: {e}") + + def _browse_script_dir(self): + path = QFileDialog.getExistingDirectory(self, _t("execute_dir_label")) + if path: + self.script_dir_input.setText(path) + + def _execute_dir(self): + try: + path = self.script_dir_input.text() + if not path: return - self._execute_click(click_mouse, click_type, button, x, y) - - elif self.keyboard_radio.isChecked(): - button = self.keyboard_button_combo.currentText() - 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() - - # === 鍵盤快捷鍵事件 === + files = get_dir_files_as_list(path) + result = execute_files(files) + self.script_result_text.setText(json.dumps(result, indent=2, default=str, ensure_ascii=False)) + except Exception as e: + self.script_result_text.setText(f"Error: {e}") + + def _execute_manual_script(self): + try: + text = self.script_editor.toPlainText().strip() + if not text: + return + data = json.loads(text) + result = execute_action(data) + self.script_result_text.setText(json.dumps(result, indent=2, default=str, ensure_ascii=False)) + except Exception as e: + self.script_result_text.setText(f"Error: {e}") + + # ========================================================================= + # Tab 6: Screen Recording + # ========================================================================= + def _build_screen_record_tab(self) -> QWidget: + tab = QWidget() + layout = QVBoxLayout() + + grid = QGridLayout() + row = 0 + + grid.addWidget(QLabel(_t("recorder_name")), row, 0) + self.sr_name_input = QLineEdit("default") + grid.addWidget(self.sr_name_input, row, 1) + + row += 1 + grid.addWidget(QLabel(_t("output_file")), row, 0) + self.sr_file_input = QLineEdit("output.avi") + grid.addWidget(self.sr_file_input, row, 1) + self.sr_file_browse_btn = QPushButton(_t("browse")) + self.sr_file_browse_btn.clicked.connect(self._browse_sr_file) + grid.addWidget(self.sr_file_browse_btn, row, 2) + + row += 1 + grid.addWidget(QLabel(_t("codec_label")), row, 0) + self.sr_codec_input = QLineEdit("XVID") + grid.addWidget(self.sr_codec_input, row, 1) + + row += 1 + grid.addWidget(QLabel(_t("fps_label")), row, 0) + self.sr_fps_input = QLineEdit("30") + self.sr_fps_input.setValidator(QIntValidator(1, 120)) + grid.addWidget(self.sr_fps_input, row, 1) + + row += 1 + grid.addWidget(QLabel(_t("resolution_label")), row, 0) + self.sr_res_input = QLineEdit("1920x1080") + grid.addWidget(self.sr_res_input, row, 1) + + layout.addLayout(grid) + + btn_h = QHBoxLayout() + self.sr_start_btn = QPushButton(_t("start_screen_record")) + self.sr_start_btn.clicked.connect(self._start_screen_record) + self.sr_stop_btn = QPushButton(_t("stop_screen_record")) + self.sr_stop_btn.clicked.connect(self._stop_screen_record) + btn_h.addWidget(self.sr_start_btn) + btn_h.addWidget(self.sr_stop_btn) + layout.addLayout(btn_h) + + self.sr_status_label = QLabel(_t("screen_record_status") + " " + _t("record_idle")) + layout.addWidget(self.sr_status_label) + + layout.addStretch() + tab.setLayout(layout) + return tab + + def _browse_sr_file(self): + path, _ = QFileDialog.getSaveFileName(self, _t("output_file"), "", "AVI (*.avi);;MP4 (*.mp4);;All (*)") + if path: + self.sr_file_input.setText(path) + + def _start_screen_record(self): + try: + name = self.sr_name_input.text() or "default" + output = self.sr_file_input.text() or "output.avi" + codec = self.sr_codec_input.text() or "XVID" + fps = int(self.sr_fps_input.text() or "30") + res_text = self.sr_res_input.text() or "1920x1080" + w, h = res_text.lower().split("x") + resolution = (int(w), int(h)) + self.screen_recorder.start_new_record(name, output, codec, fps, resolution) + self.sr_status_label.setText(_t("screen_record_status") + " " + _t("record_recording")) + except Exception as e: + QMessageBox.warning(self, "Error", str(e)) + + def _stop_screen_record(self): + try: + name = self.sr_name_input.text() or "default" + self.screen_recorder.stop_record(name) + self.sr_status_label.setText(_t("screen_record_status") + " " + _t("record_idle")) + except Exception as e: + QMessageBox.warning(self, "Error", str(e)) + + # ========================================================================= + # Tab 7: Shell Command + # ========================================================================= + def _build_shell_tab(self) -> QWidget: + tab = QWidget() + layout = QVBoxLayout() + + # Shell command + shell_group = QGroupBox(_t("shell_command_label")) + sg = QVBoxLayout() + self.shell_input = QLineEdit() + self.shell_input.setPlaceholderText("echo hello") + self.shell_exec_btn = QPushButton(_t("execute_shell")) + self.shell_exec_btn.clicked.connect(self._execute_shell) + sh = QHBoxLayout() + sh.addWidget(self.shell_input) + sh.addWidget(self.shell_exec_btn) + sg.addLayout(sh) + shell_group.setLayout(sg) + layout.addWidget(shell_group) + + # Start exe + exe_group = QGroupBox(_t("start_exe_label")) + eg = QHBoxLayout() + self.exe_path_input = QLineEdit() + self.exe_browse_btn = QPushButton(_t("browse")) + self.exe_browse_btn.clicked.connect(self._browse_exe) + self.exe_start_btn = QPushButton(_t("start_exe")) + self.exe_start_btn.clicked.connect(self._start_exe) + eg.addWidget(self.exe_path_input) + eg.addWidget(self.exe_browse_btn) + eg.addWidget(self.exe_start_btn) + exe_group.setLayout(eg) + layout.addWidget(exe_group) + + layout.addWidget(QLabel(_t("shell_output"))) + self.shell_output_text = QTextEdit() + self.shell_output_text.setReadOnly(True) + layout.addWidget(self.shell_output_text) + tab.setLayout(layout) + return tab + + def _execute_shell(self): + try: + cmd = self.shell_input.text() + if not cmd: + return + mgr = ShellManager() + mgr.exec_shell(cmd) + self.shell_output_text.setText(f"Executed: {cmd}\n(Check console for output)") + except Exception as e: + self.shell_output_text.setText(f"Error: {e}") + + def _browse_exe(self): + path, _ = QFileDialog.getOpenFileName(self, _t("start_exe_label"), "", "Executable (*.exe);;All (*)") + if path: + self.exe_path_input.setText(path) + + def _start_exe(self): + try: + path = self.exe_path_input.text() + if not path: + return + start_exe(path) + self.shell_output_text.setText(f"Started: {path}") + except Exception as e: + self.shell_output_text.setText(f"Error: {e}") + + # ========================================================================= + # Tab 8: Report + # ========================================================================= + def _build_report_tab(self) -> QWidget: + tab = QWidget() + layout = QVBoxLayout() + + # Test record toggle + tr_group = QGroupBox(_t("test_record_status")) + tr_h = QHBoxLayout() + self.tr_enable_btn = QPushButton(_t("enable_test_record")) + self.tr_enable_btn.clicked.connect(lambda: self._set_test_record(True)) + self.tr_disable_btn = QPushButton(_t("disable_test_record")) + self.tr_disable_btn.clicked.connect(lambda: self._set_test_record(False)) + self.tr_status_label = QLabel("OFF") + tr_h.addWidget(self.tr_enable_btn) + tr_h.addWidget(self.tr_disable_btn) + tr_h.addWidget(self.tr_status_label) + tr_group.setLayout(tr_h) + layout.addWidget(tr_group) + + # Report name + name_h = QHBoxLayout() + name_h.addWidget(QLabel(_t("report_name"))) + self.report_name_input = QLineEdit("autocontrol_report") + name_h.addWidget(self.report_name_input) + layout.addLayout(name_h) + + # Generate buttons + btn_h = QHBoxLayout() + self.html_report_btn = QPushButton(_t("generate_html_report")) + self.html_report_btn.clicked.connect(self._gen_html) + self.json_report_btn = QPushButton(_t("generate_json_report")) + self.json_report_btn.clicked.connect(self._gen_json) + self.xml_report_btn = QPushButton(_t("generate_xml_report")) + self.xml_report_btn.clicked.connect(self._gen_xml) + btn_h.addWidget(self.html_report_btn) + btn_h.addWidget(self.json_report_btn) + btn_h.addWidget(self.xml_report_btn) + layout.addLayout(btn_h) + + layout.addWidget(QLabel(_t("report_result"))) + self.report_result_text = QTextEdit() + self.report_result_text.setReadOnly(True) + layout.addWidget(self.report_result_text) + layout.addStretch() + tab.setLayout(layout) + return tab + + def _set_test_record(self, enable: bool): + test_record_instance.set_record_enable(enable) + self.tr_status_label.setText("ON" if enable else "OFF") + + def _gen_html(self): + try: + name = self.report_name_input.text() or "autocontrol_report" + generate_html_report(name) + self.report_result_text.setText(f"HTML report generated: {name}") + except Exception as e: + self.report_result_text.setText(f"Error: {e}") + + def _gen_json(self): + try: + name = self.report_name_input.text() or "autocontrol_report" + generate_json_report(name) + self.report_result_text.setText(f"JSON report generated: {name}") + except Exception as e: + self.report_result_text.setText(f"Error: {e}") + + def _gen_xml(self): + try: + name = self.report_name_input.text() or "autocontrol_report" + generate_xml_report(name) + self.report_result_text.setText(f"XML report generated: {name}") + except Exception as e: + self.report_result_text.setText(f"Error: {e}") + + # ========================================================================= + # Global keyboard shortcut: Ctrl+4 to 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() + self._stop_auto_click() else: super().keyPressEvent(event) diff --git a/je_auto_control/gui/main_window.py b/je_auto_control/gui/main_window.py index 8f7d3ed..3d8751c 100644 --- a/je_auto_control/gui/main_window.py +++ b/je_auto_control/gui/main_window.py @@ -1,6 +1,7 @@ import sys -from PySide6.QtWidgets import QMainWindow, QApplication +from PySide6.QtWidgets import QMainWindow, QApplication, QComboBox, QLabel, QHBoxLayout, QWidget +from PySide6.QtGui import QAction from qt_material import QtStyleTools from je_auto_control.gui.language_wrapper.multi_language_wrapper import language_wrapper @@ -8,34 +9,64 @@ class AutoControlGUIUI(QMainWindow, QtStyleTools): - """ - AutoControl GUI Main Window - 自動控制 GUI 主視窗 - - 提供應用程式主要介面 - - 套用 Qt Material 樣式 - """ def __init__(self): super().__init__() - # === Application ID 應用程式 ID === - # 用於 Windows 工作列顯示正確的應用程式名稱 + # Application ID for Windows taskbar 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.app_id) - # === Style 設定字型與樣式 === + # Style self.setStyleSheet( "font-size: 12pt;" "font-family: 'Lato';" ) - - # 套用 Qt Material 樣式 (可替換不同主題檔案) self.apply_stylesheet(self, "dark_amber.xml") - # === Central Widget 主控元件 === - # 將 AutoControlGUIWidget 作為主視窗中央元件 + # Window title and size + self.setWindowTitle(language_wrapper.language_word_dict.get("application_name", "AutoControlGUI")) + self.resize(900, 700) + + # Central widget self.auto_control_gui_widget = AutoControlGUIWidget() - self.setCentralWidget(self.auto_control_gui_widget) \ No newline at end of file + self.setCentralWidget(self.auto_control_gui_widget) + + # Menu bar with language switcher + self._build_menu_bar() + + def _build_menu_bar(self): + menu_bar = self.menuBar() + + # Language selector in menu bar + lang_widget = QWidget() + lang_layout = QHBoxLayout(lang_widget) + lang_layout.setContentsMargins(4, 0, 4, 0) + + lang_label = QLabel(language_wrapper.language_word_dict.get("language_label", "Language:")) + self.lang_combo = QComboBox() + self.lang_combo.addItems(["English", "Traditional_Chinese"]) + self.lang_combo.setCurrentText(language_wrapper.language) + self.lang_combo.currentTextChanged.connect(self._on_language_changed) + + lang_layout.addWidget(lang_label) + lang_layout.addWidget(self.lang_combo) + + menu_bar.setCornerWidget(lang_widget) + + def _on_language_changed(self, language: str): + language_wrapper.reset_language(language) + # Rebuild UI with new language + self.setWindowTitle(language_wrapper.language_word_dict.get("application_name", "AutoControlGUI")) + self.auto_control_gui_widget = AutoControlGUIWidget() + self.setCentralWidget(self.auto_control_gui_widget) + self._build_menu_bar() + + +if "__main__" == __name__: + app = QApplication(sys.argv) + window = AutoControlGUIUI() + window.show() + sys.exit(app.exec()) \ 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 748848c..f5ee1f8 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 @@ -10,4 +10,4 @@ from Xlib.display import Display # get x system display -display = Display(os.environ['DISPLAY']) +display = Display(os.environ.get('DISPLAY', ':0')) 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 05f44a7..6c29569 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 @@ -12,7 +12,7 @@ # x11 linux virtual keycode x11_linux_key_backspace = display.keysym_to_keycode(XK.string_to_keysym("BackSpace")) -x11_linux_key_slash_b = display.keysym_to_keycode(XK.string_to_keysym("BackSpace")) +x11_linux_key_slash_b = display.keysym_to_keycode(XK.string_to_keysym("backslash")) x11_linux_key_tab = display.keysym_to_keycode(XK.string_to_keysym("Tab")) x11_linux_key_enter = display.keysym_to_keycode(XK.string_to_keysym("Return")) x11_linux_key_return = display.keysym_to_keycode(XK.string_to_keysym("Return")) @@ -99,7 +99,7 @@ x11_linux_key_space = display.keysym_to_keycode(XK.string_to_keysym("space")) x11_linux_key_newline_n = display.keysym_to_keycode(XK.string_to_keysym("Return")) x11_linux_key_newline_r = display.keysym_to_keycode(XK.string_to_keysym("Return")) -x11_linux_key_newline_t = display.keysym_to_keycode(XK.string_to_keysym("Escape")) +x11_linux_key_newline_t = display.keysym_to_keycode(XK.string_to_keysym("Tab")) x11_linux_key_exclam = display.keysym_to_keycode(XK.string_to_keysym("exclam")) x11_linux_key_numbersign = display.keysym_to_keycode(XK.string_to_keysym("numbersign")) x11_linux_key_percent = display.keysym_to_keycode(XK.string_to_keysym("percent")) @@ -176,7 +176,7 @@ x11_linux_key_P = display.keysym_to_keycode(XK.string_to_keysym("P")) x11_linux_key_Q = display.keysym_to_keycode(XK.string_to_keysym("Q")) x11_linux_key_R = display.keysym_to_keycode(XK.string_to_keysym("R")) -x11_linux_key_S = display.keysym_to_keycode(XK.string_to_keysym("s")) +x11_linux_key_S = display.keysym_to_keycode(XK.string_to_keysym("S")) x11_linux_key_T = display.keysym_to_keycode(XK.string_to_keysym("T")) x11_linux_key_U = display.keysym_to_keycode(XK.string_to_keysym("U")) x11_linux_key_V = display.keysym_to_keycode(XK.string_to_keysym("V")) 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 013c9ec..01c6027 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 @@ -69,8 +69,8 @@ def run(self, reply) -> None: 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 Exception: - raise AutoControlException(listener_error_message) + except Exception as error: + raise AutoControlException(f"{listener_error_message}: {error}") from error def record(self, record_queue: Queue) -> None: """ @@ -142,8 +142,8 @@ def run(self) -> None: # 持續等待事件 self.root.display.next_event() - except Exception: - raise AutoControlException(listener_error_message) + except Exception as error: + raise AutoControlException(f"{listener_error_message}: {error}") from error finally: self.handler.still_listener = False self.still_listener = False 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 1d091ef..e05592a 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 @@ -1,6 +1,6 @@ import sys import time -from typing import Tuple +from typing import Optional, Tuple from je_auto_control.utils.exception.exception_tags import linux_import_error_message from je_auto_control.utils.exception.exceptions import AutoControlException @@ -70,7 +70,7 @@ def release_mouse(mouse_keycode: int) -> None: display.sync() -def click_mouse(mouse_keycode: int, x: int = None, y: int = None) -> None: +def click_mouse(mouse_keycode: int, x: Optional[int] = None, y: Optional[int] = None) -> None: """ Perform mouse click (press + release) 模擬滑鼠點擊(按下 + 釋放) @@ -102,7 +102,7 @@ def scroll(scroll_value: int, scroll_direction: int) -> None: def send_mouse_event_to_window(window_id: int, mouse_keycode: int, - x: int = None, y: int = None) -> None: + x: Optional[int] = None, y: Optional[int] = None) -> None: """ Send mouse event directly to a specific window 將滑鼠事件直接送到指定視窗 diff --git a/je_auto_control/osx/core/utils/osx_vk.py b/je_auto_control/osx/core/utils/osx_vk.py index e6c7d4a..116f0a6 100644 --- a/je_auto_control/osx/core/utils/osx_vk.py +++ b/je_auto_control/osx/core/utils/osx_vk.py @@ -50,7 +50,7 @@ osx_key_semicolon = osx_key_colon = 0x29 osx_key_backslash = osx_key_bar = 0x2a osx_key_comma = osx_key_less = 0x2b -osx_key_salsh = osx_key_question = 0x2c +osx_key_slash = osx_key_question = 0x2c osx_key_n = osx_key_N = 0x2d osx_key_m = osx_key_M = 0x2e osx_key_period = osx_key_greater = 0x2f diff --git a/je_auto_control/osx/listener/osx_listener.py b/je_auto_control/osx/listener/osx_listener.py index 37a9e2a..dd40546 100644 --- a/je_auto_control/osx/listener/osx_listener.py +++ b/je_auto_control/osx/listener/osx_listener.py @@ -70,7 +70,6 @@ def keyboard_handler(event) -> None: if keycode == 98: # 特殊情況:忽略 keycode 98 return record_queue.put(("AC_type_keyboard", keycode)) - print(event) def osx_record() -> None: diff --git a/je_auto_control/osx/pid/pid_control.py b/je_auto_control/osx/pid/pid_control.py index b9bee71..d1f4e25 100644 --- a/je_auto_control/osx/pid/pid_control.py +++ b/je_auto_control/osx/pid/pid_control.py @@ -38,9 +38,11 @@ def get_pid_by_window_title(title: str) -> int | None: :param title: Window title 視窗標題 :return: PID (int) or None 若找到則回傳 PID,否則回傳 None """ + # 轉義 AppleScript 字串中的特殊字元 Escape special characters for AppleScript + escaped_title = title.replace("\\", "\\\\").replace('"', '\\"') # AppleScript 腳本,用來搜尋視窗標題 script = f''' - set targetWindowName to "{title}" + set targetWindowName to "{escaped_title}" tell application "System Events" repeat with proc in processes repeat with win in windows of proc @@ -57,5 +59,5 @@ def get_pid_by_window_title(title: str) -> int | None: stderr=subprocess.DEVNULL ).decode().strip() return int(pid_str) if pid_str else None - except subprocess.CalledProcessError: + except (subprocess.CalledProcessError, ValueError): return None \ 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 184e354..be82b17 100644 --- a/je_auto_control/osx/screen/osx_screen.py +++ b/je_auto_control/osx/screen/osx_screen.py @@ -82,29 +82,31 @@ def get_pixel(x: int, y: int) -> Tuple[int, int, int, int]: "Unable to capture screen image. 請確認已授予螢幕錄製權限" ) - # 取得影像資料供應器 Get data provider - provider = cg.CGImageGetDataProvider(img) - - # 複製影像資料 Copy image data - cfdata = cg.CGDataProviderCopyData(provider) - - # 取得資料長度 Get data length - length = cf.CFDataGetLength(cfdata) - 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) - - # 預設像素格式為 BGRA Default pixel format is BGRA - b, g, r, a = buf[0], buf[1], buf[2], buf[3] - - # 釋放 CoreFoundation 物件 Release CF objects - cf.CFRelease(cfdata) - cf.CFRelease(provider) - cf.CFRelease(img) - - return r, g, b, a \ No newline at end of file + provider = None + cfdata = None + try: + # 取得影像資料供應器 Get data provider + provider = cg.CGImageGetDataProvider(img) + + # 複製影像資料 Copy image data + cfdata = cg.CGDataProviderCopyData(provider) + + # 取得資料長度 Get data length + length = cf.CFDataGetLength(cfdata) + if length < 4: + raise RuntimeError("Invalid pixel data. 資料不足") + + # 取得 byte pointer Get byte pointer + buf = cf.CFDataGetBytePtr(cfdata) + + # 預設像素格式為 BGRA Default pixel format is BGRA + b, g, r, a = buf[0], buf[1], buf[2], buf[3] + + return r, g, b, a + finally: + # 釋放 CoreFoundation 物件 Release CF objects + if cfdata: + cf.CFRelease(cfdata) + if provider: + cf.CFRelease(provider) + cf.CFRelease(img) \ 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 26ce439..47b4002 100644 --- a/je_auto_control/utils/callback/callback_function_executor.py +++ b/je_auto_control/utils/callback/callback_function_executor.py @@ -40,7 +40,7 @@ # 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 + mouse_scroll, press_mouse, release_mouse, set_mouse_position ) # record wrapper from je_auto_control.wrapper.auto_control_record import record, stop_record @@ -69,7 +69,7 @@ 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_error_message, + "AC_mouse_scroll": mouse_scroll, "AC_set_mouse_position": set_mouse_position, # keyboard 鍵盤相關 diff --git a/je_auto_control/utils/critical_exit/critcal_exit.py b/je_auto_control/utils/critical_exit/critical_exit.py similarity index 100% rename from je_auto_control/utils/critical_exit/critcal_exit.py rename to je_auto_control/utils/critical_exit/critical_exit.py diff --git a/je_auto_control/utils/cv2_utils/screen_record.py b/je_auto_control/utils/cv2_utils/screen_record.py index 98592ff..22cc04d 100644 --- a/je_auto_control/utils/cv2_utils/screen_record.py +++ b/je_auto_control/utils/cv2_utils/screen_record.py @@ -70,18 +70,19 @@ def __init__(self, path_and_filename, codec, frame_per_sec, resolution: Tuple[in def run(self) -> None: self.record_flag = True - while self.record_flag: - # 擷取螢幕畫面 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) - - # 錄影結束後釋放資源 Release resources after recording - self.video_writer.release() + try: + while self.record_flag: + # 擷取螢幕畫面 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) + finally: + # 錄影結束後釋放資源 Release resources after recording + self.video_writer.release() def stop(self) -> None: """ diff --git a/je_auto_control/utils/cv2_utils/video_recording.py b/je_auto_control/utils/cv2_utils/video_recording.py index 9969969..2aa4b15 100644 --- a/je_auto_control/utils/cv2_utils/video_recording.py +++ b/je_auto_control/utils/cv2_utils/video_recording.py @@ -3,7 +3,7 @@ import numpy as np from mss import mss -from je_auto_control.utils.logging.loggin_instance import autocontrol_logger +from je_auto_control.utils.logging.logging_instance import autocontrol_logger class RecordingThread(threading.Thread): diff --git a/je_auto_control/utils/executor/action_executor.py b/je_auto_control/utils/executor/action_executor.py index 33182f3..aca52d2 100644 --- a/je_auto_control/utils/executor/action_executor.py +++ b/je_auto_control/utils/executor/action_executor.py @@ -15,7 +15,7 @@ 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.logging.logging_instance import autocontrol_logger from je_auto_control.utils.package_manager.package_manager_class import package_manager from je_auto_control.utils.project.create_project_structure import create_project_dir from je_auto_control.utils.shell_process.shell_exec import ShellManager @@ -28,7 +28,7 @@ ) 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 + mouse_scroll, 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 @@ -55,7 +55,7 @@ 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_error_message, + "AC_mouse_scroll": mouse_scroll, "AC_set_mouse_position": set_mouse_position, # Keyboard 鍵盤相關 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 d245c8b..14608a2 100644 --- a/je_auto_control/utils/generate_report/generate_html_report.py +++ b/je_auto_control/utils/generate_report/generate_html_report.py @@ -2,7 +2,7 @@ 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.logging.logging_instance import autocontrol_logger from je_auto_control.utils.test_record.record_test_class import test_record_instance _lock = Lock() 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 59f3d28..1c37551 100644 --- a/je_auto_control/utils/generate_report/generate_json_report.py +++ b/je_auto_control/utils/generate_report/generate_json_report.py @@ -4,7 +4,7 @@ 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.logging.logging_instance import autocontrol_logger from je_auto_control.utils.test_record.record_test_class import test_record_instance 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 0739f89..bb54973 100644 --- a/je_auto_control/utils/generate_report/generate_xml_report.py +++ b/je_auto_control/utils/generate_report/generate_xml_report.py @@ -3,7 +3,7 @@ from xml.dom.minidom import parseString from je_auto_control.utils.generate_report.generate_json_report import generate_json -from je_auto_control.utils.logging.loggin_instance import autocontrol_logger +from je_auto_control.utils.logging.logging_instance import autocontrol_logger from je_auto_control.utils.xml.change_xml_structure.change_xml_structure import dict_to_elements_tree diff --git a/je_auto_control/utils/logging/loggin_instance.py b/je_auto_control/utils/logging/logging_instance.py similarity index 100% rename from je_auto_control/utils/logging/loggin_instance.py rename to je_auto_control/utils/logging/logging_instance.py 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 7b7d06f..361c471 100644 --- a/je_auto_control/utils/package_manager/package_manager_class.py +++ b/je_auto_control/utils/package_manager/package_manager_class.py @@ -5,7 +5,7 @@ from sys import stderr from typing import Optional -from je_auto_control.utils.logging.loggin_instance import autocontrol_logger +from je_auto_control.utils.logging.logging_instance import autocontrol_logger class PackageManager: diff --git a/je_auto_control/utils/project/create_project_structure.py b/je_auto_control/utils/project/create_project_structure.py index 585fc28..c2726ec 100644 --- a/je_auto_control/utils/project/create_project_structure.py +++ b/je_auto_control/utils/project/create_project_structure.py @@ -3,7 +3,7 @@ from threading import Lock 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.logging.logging_instance import autocontrol_logger from je_auto_control.utils.project.template.template_executor import ( executor_template_1, executor_template_2, bad_executor_template_1 ) @@ -51,9 +51,9 @@ def create_template(parent_name: str, project_path: str = None) -> None: # 建立 keyword JSON 檔案 Create keyword JSON files if keyword_dir_path.exists() and keyword_dir_path.is_dir(): - 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) + 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(): @@ -86,8 +86,8 @@ def create_project_dir(project_path: str = None, parent_name: str = "AutoControl project_path = getcwd() # 建立 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_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 c30ae5f..35531f9 100644 --- a/je_auto_control/utils/shell_process/shell_exec.py +++ b/je_auto_control/utils/shell_process/shell_exec.py @@ -5,7 +5,7 @@ from threading import Thread from typing import Union -from je_auto_control.utils.logging.loggin_instance import autocontrol_logger +from je_auto_control.utils.logging.logging_instance import autocontrol_logger class ShellManager: diff --git a/je_auto_control/utils/socket_server/auto_control_socket_server.py b/je_auto_control/utils/socket_server/auto_control_socket_server.py index c2861f3..bb51227 100644 --- a/je_auto_control/utils/socket_server/auto_control_socket_server.py +++ b/je_auto_control/utils/socket_server/auto_control_socket_server.py @@ -19,18 +19,18 @@ def handle(self) -> None: else: try: execute_str = json.loads(command_string) - for execute_function, execute_return in execute_action(execute_str).items(): - socket.sendto(str(execute_return).encode("utf-8"), self.client_address) - socket.sendto("\n".encode("utf-8"), self.client_address) - socket.sendto("Return_Data_Over_JE".encode("utf-8"), self.client_address) - socket.sendto("\n".encode("utf-8"), self.client_address) + for _, execute_return in execute_action(execute_str).items(): + socket.sendall(str(execute_return).encode("utf-8")) + socket.sendall("\n".encode("utf-8")) + socket.sendall("Return_Data_Over_JE".encode("utf-8")) + socket.sendall("\n".encode("utf-8")) except Exception as error: print(repr(error), file=sys.stderr) try: - socket.sendto(str(error).encode("utf-8"), self.client_address) - socket.sendto("\n".encode("utf-8"), self.client_address) - socket.sendto("Return_Data_Over_JE".encode("utf-8"), self.client_address) - socket.sendto("\n".encode("utf-8"), self.client_address) + socket.sendall(str(error).encode("utf-8")) + socket.sendall("\n".encode("utf-8")) + socket.sendall("Return_Data_Over_JE".encode("utf-8")) + socket.sendall("\n".encode("utf-8")) except Exception as error: print(repr(error)) 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 12002d8..a650958 100644 --- a/je_auto_control/utils/start_exe/start_another_process.py +++ b/je_auto_control/utils/start_exe/start_another_process.py @@ -2,7 +2,7 @@ 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.logging.logging_instance import autocontrol_logger from je_auto_control.utils.shell_process.shell_exec import ShellManager @@ -30,6 +30,6 @@ def start_exe(exe_path: str) -> None: raise AutoControlException(f"Failed to execute {exe_path_obj}: {repr(error)}") else: autocontrol_logger.error( - f"start_exe, exe_path: {exe_path_obj}, failed: {AutoControlException(can_not_find_file_error_message)}" + f"start_exe, exe_path: {exe_path_obj}, failed: {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 bdb9338..36169ce 100644 --- a/je_auto_control/utils/test_record/record_test_class.py +++ b/je_auto_control/utils/test_record/record_test_class.py @@ -1,5 +1,5 @@ import datetime -from je_auto_control.utils.logging.loggin_instance import autocontrol_logger +from je_auto_control.utils.logging.logging_instance import autocontrol_logger class TestRecord: 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 6b9b293..7dd4bf8 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 @@ -59,14 +59,17 @@ def _to_elements_tree(json_dict: Any, root: ElementTree.Element) -> None: root.text = json_dict elif isinstance(json_dict, dict): for key, value in json_dict.items(): - assert isinstance(key, str) + if not isinstance(key, str): + raise TypeError(f"Expected str key, got {type(key)}") if key.startswith('#'): # 處理文字節點 Handle text node - assert key == '#text' and isinstance(value, str) + if key != '#text' or not isinstance(value, str): + raise ValueError(f"Invalid text node: key={key}, value type={type(value)}") root.text = value elif key.startswith('@'): # 處理屬性 Handle attributes - assert isinstance(value, str) + if not isinstance(value, str): + raise TypeError(f"Expected str attribute value, got {type(value)}") root.set(key[1:], value) elif isinstance(value, list): # 處理多個子節點 Handle multiple children @@ -78,7 +81,8 @@ def _to_elements_tree(json_dict: Any, root: ElementTree.Element) -> None: else: raise TypeError(f"Invalid type: {type(json_dict)}") - assert isinstance(json_dict, dict) and len(json_dict) == 1 + if not isinstance(json_dict, dict) or len(json_dict) != 1: + raise TypeError(f"Expected dict with exactly 1 key, got {type(json_dict)} with {len(json_dict) if isinstance(json_dict, dict) else 'N/A'} keys") tag, body = next(iter(json_dict.items())) node = ElementTree.Element(tag) _to_elements_tree(body, node) diff --git a/je_auto_control/windows/core/utils/win32_vk.py b/je_auto_control/windows/core/utils/win32_vk.py index 81b598f..94f6390 100644 --- a/je_auto_control/windows/core/utils/win32_vk.py +++ b/je_auto_control/windows/core/utils/win32_vk.py @@ -99,7 +99,7 @@ WIN32_keyJ: int = 0x4A WIN32_keyK: int = 0x4B WIN32_keyL: int = 0x4C -WIN32_keyM: int = 0X4D +WIN32_keyM: int = 0x4D WIN32_keyN: int = 0x4E WIN32_keyO: int = 0x4F WIN32_keyP: int = 0x50 diff --git a/je_auto_control/windows/listener/win32_keyboard_listener.py b/je_auto_control/windows/listener/win32_keyboard_listener.py index 1c76f64..c0a65f2 100644 --- a/je_auto_control/windows/listener/win32_keyboard_listener.py +++ b/je_auto_control/windows/listener/win32_keyboard_listener.py @@ -84,8 +84,8 @@ def _start_listener(self) -> None: 啟動鍵盤監聽 Start keyboard listener """ - pointer = _get_function_pointer(self._win32_hook_proc) - if not self._set_win32_hook(pointer): + self._pointer = _get_function_pointer(self._win32_hook_proc) + if not self._set_win32_hook(self._pointer): raise AutoControlException("Failed to set keyboard hook") message = MSG() diff --git a/je_auto_control/windows/listener/win32_mouse_listener.py b/je_auto_control/windows/listener/win32_mouse_listener.py index 5ed3da1..ddb137f 100644 --- a/je_auto_control/windows/listener/win32_mouse_listener.py +++ b/je_auto_control/windows/listener/win32_mouse_listener.py @@ -94,8 +94,8 @@ def _start_listener(self) -> None: 啟動滑鼠監聽 Start mouse listener """ - pointer = _get_function_pointer(self._win32_hook_proc) - if not self._set_win32_hook(pointer): + self._pointer = _get_function_pointer(self._win32_hook_proc) + if not self._set_win32_hook(self._pointer): raise AutoControlException("Failed to set mouse hook") message = MSG() diff --git a/je_auto_control/windows/screen/win32_screen.py b/je_auto_control/windows/screen/win32_screen.py index 52a0d2d..88a5769 100644 --- a/je_auto_control/windows/screen/win32_screen.py +++ b/je_auto_control/windows/screen/win32_screen.py @@ -38,12 +38,12 @@ def get_pixel(x: int, y: int, hwnd: int = 0) -> Tuple[int, int, int]: """ dc = _user32.GetDC(hwnd) if not dc: - raise RuntimeError("GetDC failed") + raise AutoControlException("GetDC failed") try: pixel = _gdi32.GetPixel(dc, x, y) if pixel == 0xFFFFFFFF: # GetPixel 失敗時回傳 -1 (0xFFFFFFFF) - raise RuntimeError("GetPixel failed") + raise AutoControlException("GetPixel failed") r = pixel & 0xFF g = (pixel >> 8) & 0xFF diff --git a/je_auto_control/wrapper/auto_control_image.py b/je_auto_control/wrapper/auto_control_image.py index b9f7f1a..c9f3baa 100644 --- a/je_auto_control/wrapper/auto_control_image.py +++ b/je_auto_control/wrapper/auto_control_image.py @@ -4,7 +4,7 @@ from je_auto_control.utils.cv2_utils.screenshot import pil_screenshot 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.logging.logging_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, set_mouse_position diff --git a/je_auto_control/wrapper/auto_control_keyboard.py b/je_auto_control/wrapper/auto_control_keyboard.py index db72877..1b2d155 100644 --- a/je_auto_control/wrapper/auto_control_keyboard.py +++ b/je_auto_control/wrapper/auto_control_keyboard.py @@ -8,7 +8,7 @@ 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.logging.logging_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, keyboard_keys_table, keyboard_check diff --git a/je_auto_control/wrapper/auto_control_mouse.py b/je_auto_control/wrapper/auto_control_mouse.py index 1e15d73..b859bf5 100644 --- a/je_auto_control/wrapper/auto_control_mouse.py +++ b/je_auto_control/wrapper/auto_control_mouse.py @@ -10,7 +10,7 @@ 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.logging.logging_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, mouse_keys_table, special_mouse_keys_table @@ -69,7 +69,8 @@ def get_mouse_position() -> tuple[int, int] | None: raise AutoControlMouseException(mouse_get_position_error_message + " " + repr(error)) except Exception as error: record_action_to_list("get_mouse_position", None, repr(error)) - print(repr(error), file=sys.stderr) + autocontrol_logger.error(f"get_mouse_position failed: {repr(error)}") + raise def set_mouse_position(x: int, y: int) -> tuple[int, int] | None: @@ -96,6 +97,7 @@ def set_mouse_position(x: int, y: int) -> tuple[int, int] | None: except Exception as error: record_action_to_list("set_mouse_position", param, repr(error)) autocontrol_logger.error(f"set_mouse_position failed: {repr(error)}") + raise def press_mouse(mouse_keycode: Union[int, str], x: int = None, y: int = None) -> tuple[int, int, int] | None: @@ -121,6 +123,7 @@ def press_mouse(mouse_keycode: Union[int, str], x: int = None, y: int = None) -> except Exception as error: record_action_to_list("press_mouse", param, repr(error)) autocontrol_logger.error(f"press_mouse failed: {repr(error)}") + raise def release_mouse(mouse_keycode: Union[int, str], x: int = None, y: int = None) -> tuple[int, int, int] | None: @@ -146,6 +149,7 @@ def release_mouse(mouse_keycode: Union[int, str], x: int = None, y: int = None) except Exception as error: record_action_to_list("release_mouse", param, repr(error)) autocontrol_logger.error(f"release_mouse failed: {repr(error)}") + raise def click_mouse(mouse_keycode: Union[int, str], x: int = None, y: int = None) -> Tuple[int, int, int]: diff --git a/je_auto_control/wrapper/auto_control_record.py b/je_auto_control/wrapper/auto_control_record.py index f5c81fb..cc65716 100644 --- a/je_auto_control/wrapper/auto_control_record.py +++ b/je_auto_control/wrapper/auto_control_record.py @@ -3,7 +3,7 @@ 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 +from je_auto_control.utils.logging.logging_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 recorder @@ -40,7 +40,7 @@ def stop_record() -> list: if action[0] == "AC_type_keyboard": new_list.append([action[0], dict([["keycode", action[1]]])]) else: - new_list.append([action[0], dict(zip(["mouse_keycode", "x", "y"], [action[0], action[1], action[2]]))]) + new_list.append([action[0], {"mouse_keycode": action[0], "x": action[1], "y": action[2]}]) record_action_to_list("stop_record", None) return new_list except Exception as error: diff --git a/je_auto_control/wrapper/auto_control_screen.py b/je_auto_control/wrapper/auto_control_screen.py index fe34254..31492cb 100644 --- a/je_auto_control/wrapper/auto_control_screen.py +++ b/je_auto_control/wrapper/auto_control_screen.py @@ -7,7 +7,7 @@ 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.logging.logging_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 screen @@ -15,48 +15,54 @@ def screen_size() -> Tuple[int, int]: """ get screen size + 取得螢幕大小 """ - autocontrol_logger.info("AC_screen_size") + autocontrol_logger.info("screen_size") try: - try: - record_action_to_list("size", None) - return screen.size() - except AutoControlScreenException: - autocontrol_logger.error(f"screen_size, failed: {repr(AutoControlScreenException(screen_get_size_error_message))}") - raise AutoControlScreenException(screen_get_size_error_message) + record_action_to_list("size", None) + return screen.size() + except AutoControlScreenException: + autocontrol_logger.error(f"screen_size failed: {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)}") + autocontrol_logger.error(f"screen_size failed: {repr(error)}") + raise def screenshot(file_path: str = None, screen_region: list = None) -> List[int]: """ - use to capture current screen cv2_utils - :param file_path screenshot file save path - :param screen_region screenshot screen_region + use to capture current screen + 擷取當前螢幕畫面 + + :param file_path: screenshot file save path 截圖儲存路徑 + :param screen_region: screenshot region 截圖區域 """ - autocontrol_logger.info(f"screen_size, file_path: {file_path}, screen_region: {screen_region}") + autocontrol_logger.info(f"screenshot, file_path: {file_path}, screen_region: {screen_region}") param = locals() try: - try: - record_action_to_list("AC_screenshot", param) - return cv2.cvtColor( - np.array(pil_screenshot(file_path=file_path, screen_region=screen_region)), cv2.COLOR_RGB2BGR) - except AutoControlScreenException as error: - autocontrol_logger.info( - f"screen_size, file_path: {file_path}, screen_region: {screen_region}, " - f"failed: {AutoControlScreenException(screen_screenshot_error_message + ' ' + repr(error))}") - raise AutoControlScreenException(screen_screenshot_error_message + " " + repr(error)) + record_action_to_list("AC_screenshot", param) + return cv2.cvtColor( + np.array(pil_screenshot(file_path=file_path, screen_region=screen_region)), cv2.COLOR_RGB2BGR) + except AutoControlScreenException as error: + autocontrol_logger.error( + f"screenshot failed, file_path: {file_path}, screen_region: {screen_region}, " + f"error: {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( - f"screen_size, file_path: {file_path}, screen_region: {screen_region}, " - f"failed: {repr(error)}") + autocontrol_logger.error( + f"screenshot failed, file_path: {file_path}, screen_region: {screen_region}, " + f"error: {repr(error)}") + raise def get_pixel(x: int, y: int, hwnd=None): + """ + 取得指定座標的像素顏色 + Get pixel color at given coordinates + """ autocontrol_logger.info(f"get_pixel, x: {x}, y: {y}, hwnd: {hwnd}") - param = locals() try: if hwnd is None: return screen.get_pixel(x, y) @@ -64,6 +70,7 @@ def get_pixel(x: int, y: int, hwnd=None): return screen.get_pixel(x, y, hwnd) except Exception as error: record_action_to_list("AC_get_pixel", None, repr(error)) - autocontrol_logger.info( - f"get_pixel, x: {x}, y: {y}, hwnd: {hwnd}" - f"failed: {repr(error)}") + autocontrol_logger.error( + f"get_pixel failed, x: {x}, y: {y}, hwnd: {hwnd}, " + f"error: {repr(error)}") + raise diff --git a/je_auto_control/wrapper/platform_wrapper.py b/je_auto_control/wrapper/platform_wrapper.py index 25b0e8e..756e11a 100644 --- a/je_auto_control/wrapper/platform_wrapper.py +++ b/je_auto_control/wrapper/platform_wrapper.py @@ -1,7 +1,7 @@ import sys 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.logging.logging_instance import autocontrol_logger if sys.platform in ["win32", "cygwin", "msys"]: from je_auto_control.windows.core.utils.win32_vk import WIN32_ABSOLUTE @@ -226,7 +226,7 @@ from je_auto_control.osx.core.utils.osx_vk import osx_key_semicolon, osx_key_colon from je_auto_control.osx.core.utils.osx_vk import osx_key_backslash, osx_key_bar from je_auto_control.osx.core.utils.osx_vk import osx_key_comma, osx_key_less - from je_auto_control.osx.core.utils.osx_vk import osx_key_salsh, osx_key_question + from je_auto_control.osx.core.utils.osx_vk import osx_key_slash, osx_key_question from je_auto_control.osx.core.utils.osx_vk import osx_key_period, osx_key_greater from je_auto_control.osx.core.utils.osx_vk import osx_key_grave, osx_key_asciitilde from je_auto_control.osx.core.utils.osx_vk import osx_key_space @@ -796,7 +796,7 @@ "|": osx_key_bar, ",": osx_key_comma, "<": osx_key_less, - "/": osx_key_salsh, + "/": osx_key_slash, "?": osx_key_question, ".": osx_key_period, ">": osx_key_greater, diff --git a/pyproject.toml b/pyproject.toml index 127ce9b..9584b63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ -# Rename to build stable version -# This is stable version +# Rename to build dev version +# This is dev version [build-system] -requires = ["setuptools>=61.0"] +requires = ["setuptools"] build-backend = "setuptools.build_meta" [project] -name = "je_auto_control" -version = "0.0.178" +name = "je_auto_control_dev" +version = "0.0.134" authors = [ { name = "JE-Chen", email = "jechenmailman@gmail.com" }, ] diff --git a/dev.toml b/stable.toml similarity index 89% rename from dev.toml rename to stable.toml index dd9c979..127ce9b 100644 --- a/dev.toml +++ b/stable.toml @@ -1,12 +1,12 @@ -# Rename to build dev version -# This is dev version +# Rename to build stable version +# This is stable version [build-system] -requires = ["setuptools"] +requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] -name = "je_auto_control_dev" -version = "0.0.132" +name = "je_auto_control" +version = "0.0.178" authors = [ { name = "JE-Chen", email = "jechenmailman@gmail.com" }, ] diff --git a/test/unit_test/argparse/argparse_test.py b/test/unit_test/argparse/argparse_test.py index 5b20b4e..b5c241f 100644 --- a/test/unit_test/argparse/argparse_test.py +++ b/test/unit_test/argparse/argparse_test.py @@ -1,8 +1,9 @@ import os +import subprocess print(os.getcwd()) -os.system("cd " + os.getcwd()) -os.system("python je_auto_control --execute_file " + os.getcwd() + r"/test/unit_test/argparse/test1.json") -os.system("python je_auto_control --execute_dir " + os.getcwd() + r"/test/unit_test/argparse") -os.system("python je_auto_control --create_project " + os.getcwd()) +cwd = os.getcwd() +subprocess.run(["python", "je_auto_control", "--execute_file", os.path.join(cwd, "test/unit_test/argparse/test1.json")]) +subprocess.run(["python", "je_auto_control", "--execute_dir", os.path.join(cwd, "test/unit_test/argparse")]) +subprocess.run(["python", "je_auto_control", "--create_project", cwd]) diff --git a/test/unit_test/exception/auto_control_exception_test.py b/test/unit_test/exception/auto_control_exception_test.py index b9ecab4..35dbf8a 100644 --- a/test/unit_test/exception/auto_control_exception_test.py +++ b/test/unit_test/exception/auto_control_exception_test.py @@ -1,5 +1,3 @@ -import sys - from je_auto_control.utils.exception.exceptions import AutoControlCantFindKeyException from je_auto_control.utils.exception.exceptions import AutoControlException from je_auto_control.utils.exception.exceptions import AutoControlKeyboardException @@ -15,16 +13,13 @@ AutoControlCantFindKeyException, ImageNotFoundException ] -try: - for index, value in enumerate(exception_list): - try: - # Branch Prediction - print(value) - if exception_list[index] != ImageNotFoundException: - raise exception_list[index]() - else: - raise exception_list[index]("test.png") - except Exception as error: - print(error) -except AutoControlException: - sys.exit(0) + +for index, value in enumerate(exception_list): + try: + print(value) + if exception_list[index] != ImageNotFoundException: + raise exception_list[index]() + else: + raise exception_list[index]("test.png") + except Exception as error: + print(repr(error)) diff --git a/test/unit_test/keyboard/keyboard_write_test.py b/test/unit_test/keyboard/keyboard_write_test.py index 4614762..a120eed 100644 --- a/test/unit_test/keyboard/keyboard_write_test.py +++ b/test/unit_test/keyboard/keyboard_write_test.py @@ -13,10 +13,8 @@ release_keyboard_key("shift") print(write("abcdefghijklmnopqrstuvwxyz")) -# this writes will print one error -> keyboard write error can't find key : Ѓ and write remain string - +# this write will raise an error for unsupported character try: print(write("Ѓ123456789")) except Exception as error: - print(repr(error), file=sys.stderr) -sys.exit(0) + print(f"Expected error for unsupported character: {repr(error)}", file=sys.stderr) diff --git a/test/unit_test/mouse/mouse_scroll_test.py b/test/unit_test/mouse/mouse_scroll_test.py index 649103a..cd5c09a 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_error_message +from je_auto_control import mouse_scroll sleep(3) -mouse_scroll_error_message(100) +mouse_scroll(100) diff --git a/test/unit_test/mouse/mouse_test.py b/test/unit_test/mouse/mouse_test.py index cfac4e3..1e65dee 100644 --- a/test/unit_test/mouse/mouse_test.py +++ b/test/unit_test/mouse/mouse_test.py @@ -1,25 +1,22 @@ -try: - import sys - import time +import sys +import time - from je_auto_control import click_mouse - 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 +from je_auto_control import click_mouse +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 - time.sleep(3) +time.sleep(3) - print(get_mouse_position()) - set_mouse_position(809, 388) +print(get_mouse_position()) +set_mouse_position(809, 388) - print(mouse_keys_table.keys()) +print(mouse_keys_table.keys()) - press_mouse("mouse_right") - release_mouse("mouse_right") - press_mouse("mouse_left") - release_mouse("mouse_left") - click_mouse("mouse_left") -except Exception: - sys.exit(0) +press_mouse("mouse_right") +release_mouse("mouse_right") +press_mouse("mouse_left") +release_mouse("mouse_left") +click_mouse("mouse_left") diff --git a/test/unit_test/screen/screen_test.py b/test/unit_test/screen/screen_test.py index 0c3bd52..83c06de 100644 --- a/test/unit_test/screen/screen_test.py +++ b/test/unit_test/screen/screen_test.py @@ -1,6 +1,6 @@ from je_auto_control import screen_size -screen_size = screen_size() -assert (screen_size is not None) -print(screen_size) +result = screen_size() +assert (result is not None) +print(result) diff --git a/test/unit_test/socket_server_test/socket_client_test.py b/test/unit_test/socket_server_test/socket_client_test.py index 7cde5cd..d6ba476 100644 --- a/test/unit_test/socket_server_test/socket_client_test.py +++ b/test/unit_test/socket_server_test/socket_client_test.py @@ -6,4 +6,4 @@ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket: client_socket.connect((HOST, PORT)) client_socket.sendall(bytes(data + "\n", "utf-8")) - received = str(client_socket.recv(8192), "utf-8") + client_socket.recv(8192) diff --git a/test/unit_test/total_record/total_record_test.py b/test/unit_test/total_record/total_record_test.py index 0c31cdb..d5689da 100644 --- a/test/unit_test/total_record/total_record_test.py +++ b/test/unit_test/total_record/total_record_test.py @@ -6,24 +6,22 @@ from je_auto_control import test_record_instance from je_auto_control import write +test_record_instance.set_record_enable(True) +print(keyboard_keys_table.keys()) +press_keyboard_key("shift") +write("123456789") +assert (write("abcdefghijklmnopqrstuvwxyz") == "abcdefghijklmnopqrstuvwxyz") +release_keyboard_key("shift") + +# this write will raise an error for unsupported character try: - test_record_instance.set_record_enable(True) - print(keyboard_keys_table.keys()) - press_keyboard_key("shift") - write("123456789") - assert (write("abcdefghijklmnopqrstuvwxyz") == "abcdefghijklmnopqrstuvwxyz") - release_keyboard_key("shift") - # this writes will print one error -> keyboard write error can't find key : Ѓ and write remain string - try: - assert (write("?123456789") == "123456789") - except Exception as error: - print(repr(error), file=sys.stderr) - try: - write("!#@L@#{@#PL#{!@#L{!#{|##PO}!@#O@!O#P!)KI#O_!K") - except Exception as error: - print(repr(error), file=sys.stderr) + assert (write("?123456789") == "123456789") +except Exception as error: + print(f"Expected error for special character: {repr(error)}", file=sys.stderr) - print(test_record_instance.test_record_list) +try: + write("!#@L@#{@#PL#{!@#L{!#{|##PO}!@#O@!O#P!)KI#O_!K") except Exception as error: - print(repr(error), file=sys.stderr) -sys.exit(0) + print(f"Expected error for special characters: {repr(error)}", file=sys.stderr) + +print(test_record_instance.test_record_list) From 49a22b61dab6d6670bb3e684ed585dad81927286 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Sat, 4 Apr 2026 13:52:09 +0800 Subject: [PATCH 2/4] Update dev version * Update keyboard * Edit GitHub Actions --- .github/workflows/dev.yml | 1 + .github/workflows/stable.yml | 1 + .gitignore | 2 ++ je_auto_control/wrapper/auto_control_keyboard.py | 6 ++++-- pyproject.toml | 2 +- 5 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 691f3a5..a0d2ce3 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -30,6 +30,7 @@ jobs: run: | python -m pip install --upgrade pip wheel pip install -r dev_requirements.txt + pip install -e . # Screen tests - name: Test Screen Size diff --git a/.github/workflows/stable.yml b/.github/workflows/stable.yml index 29c2de3..4fb003f 100644 --- a/.github/workflows/stable.yml +++ b/.github/workflows/stable.yml @@ -30,6 +30,7 @@ jobs: run: | python -m pip install --upgrade pip wheel pip install -r dev_requirements.txt + pip install -e . # Screen tests - name: Test Screen Size diff --git a/.gitignore b/.gitignore index f18f9c3..9e0f3e7 100644 --- a/.gitignore +++ b/.gitignore @@ -121,3 +121,5 @@ dmypy.json .idea/AutoControl.iml .idea/misc.xml .idea/workspace.xml +.claude/settings.local.json +/.claude/ diff --git a/je_auto_control/wrapper/auto_control_keyboard.py b/je_auto_control/wrapper/auto_control_keyboard.py index 1b2d155..171c5b5 100644 --- a/je_auto_control/wrapper/auto_control_keyboard.py +++ b/je_auto_control/wrapper/auto_control_keyboard.py @@ -143,9 +143,11 @@ def write(write_string: str, is_shift: bool = False) -> Optional[str]: 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)) + type_keyboard(key, is_shift, skip_record=True) + record_write_chars.append(single_char) elif single_char.isspace(): - record_write_chars.append(type_keyboard("space", is_shift, skip_record=True)) + type_keyboard("space", is_shift, skip_record=True) + record_write_chars.append(single_char) else: autocontrol_logger.error(f"write failed: {keyboard_write_cant_find_error_message}, char={single_char}") raise AutoControlKeyboardException(keyboard_write_cant_find_error_message) diff --git a/pyproject.toml b/pyproject.toml index 9584b63..776a38a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" [project] name = "je_auto_control_dev" -version = "0.0.134" +version = "0.0.135" authors = [ { name = "JE-Chen", email = "jechenmailman@gmail.com" }, ] From ec6d6d7b2e9f688afa478e1ffd4d5cb793efb804 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Sat, 4 Apr 2026 13:53:52 +0800 Subject: [PATCH 3/4] Delete .claude directory --- .claude/settings.local.json | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 581d686..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(python -c \"from je_auto_control.gui import start_autocontrol_gui; print\\('Import OK'\\)\")", - "Bash(py -c \"from je_auto_control.gui import start_autocontrol_gui; print\\('Import OK'\\)\")", - "Bash(py -m pip install -r dev_requirements.txt)", - "Bash(py -c \"from je_auto_control import start_autocontrol_gui; print\\('Entry point import OK'\\)\")", - "Bash(py -c \"import je_auto_control; print\\(type\\(je_auto_control.record\\)\\); print\\(type\\(je_auto_control.stop_record\\)\\); print\\(type\\(je_auto_control.create_project_dir\\)\\); print\\(type\\(je_auto_control.start_autocontrol_gui\\)\\)\")", - "Bash(py -c ':*)", - "Bash(py -m je_auto_control --execute_str '{\"auto_control\": []}')", - "Bash(py -m je_auto_control --execute_str \"{\\\\\"auto_control\\\\\": [{\\\\\"AC_screen_size\\\\\": {}}]}\")", - "Bash(py -m je_auto_control --execute_str \"{\\\\\"auto_control\\\\\": [[\\\\\"AC_screen_size\\\\\"]]}\")" - ] - } -} From 044ffac0d7dcd7ab36dca62698b0decefbc9a25e Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Sat, 4 Apr 2026 14:00:10 +0800 Subject: [PATCH 4/4] Update stable version --- .gitignore | 1 + stable.toml => dev.toml | 12 ++++++------ pyproject.toml | 12 ++++++------ 3 files changed, 13 insertions(+), 12 deletions(-) rename stable.toml => dev.toml (85%) diff --git a/.gitignore b/.gitignore index 9e0f3e7..a97cc1f 100644 --- a/.gitignore +++ b/.gitignore @@ -123,3 +123,4 @@ dmypy.json .idea/workspace.xml .claude/settings.local.json /.claude/ +/.claude diff --git a/stable.toml b/dev.toml similarity index 85% rename from stable.toml rename to dev.toml index 127ce9b..72fa2d1 100644 --- a/stable.toml +++ b/dev.toml @@ -1,12 +1,12 @@ -# Rename to build stable version -# This is stable version +# Rename to build dev version +# This is dev version [build-system] -requires = ["setuptools>=61.0"] +requires = ["setuptools"] build-backend = "setuptools.build_meta" [project] -name = "je_auto_control" -version = "0.0.178" +name = "je_auto_control_dev" +version = "0.0.135" authors = [ { name = "JE-Chen", email = "jechenmailman@gmail.com" }, ] @@ -43,4 +43,4 @@ content-type = "text/markdown" find = { namespaces = false } [project.optional-dependencies] -gui = ["PySide6==6.10.1", "qt-material"] +gui = ["PySide6==6.11.0", "qt-material"] diff --git a/pyproject.toml b/pyproject.toml index 776a38a..da4cab0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ -# Rename to build dev version -# This is dev version +# Rename to build stable version +# This is stable version [build-system] -requires = ["setuptools"] +requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] -name = "je_auto_control_dev" -version = "0.0.135" +name = "je_auto_control" +version = "0.0.179" authors = [ { name = "JE-Chen", email = "jechenmailman@gmail.com" }, ] @@ -43,4 +43,4 @@ content-type = "text/markdown" find = { namespaces = false } [project.optional-dependencies] -gui = ["PySide6==6.10.1", "qt-material"] +gui = ["PySide6==6.11.0", "qt-material"]