From 771739cda9525cfff5f2a87974f594922ca62202 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sat, 3 Jun 2023 21:24:42 +0200 Subject: [PATCH 001/123] add gitignore for PyCharm --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 9029d3bc..dc9770b7 100644 --- a/.gitignore +++ b/.gitignore @@ -74,6 +74,9 @@ docs/_build/ # PyBuilder target/ +# PyCharm +.idea/ + # Jupyter Notebook .ipynb_checkpoints From e0217c23cc9fdb6cc9f8743515fbb8b3c7b46b16 Mon Sep 17 00:00:00 2001 From: will wade Date: Tue, 27 Jun 2023 16:30:16 +0100 Subject: [PATCH 002/123] initial attempt working but needs to run as admin --- installer.iss | 42 +++++++++++++++++++++++++++++ run_app.py | 20 +++++++++----- src/gui/pages/page_select_camera.py | 2 +- 3 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 installer.iss diff --git a/installer.iss b/installer.iss new file mode 100644 index 00000000..bdd887a3 --- /dev/null +++ b/installer.iss @@ -0,0 +1,42 @@ +; Script generated by the Inno Setup Script Wizard. +; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! + +#define MyAppName "Gameface" +#define MyAppVersion "1" +#define MyAppExeName "run_app.exe" + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. +; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) +AppId={{B266B614-4113-4DB7-9A30-4250FECA5009} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +;AppVerName={#MyAppName} {#MyAppVersion} +DefaultDirName={autopf}\{#MyAppName} +DisableProgramGroupPage=yes +; Uncomment the following line to run in non administrative install mode (install for current user only.) +;PrivilegesRequired=lowest +OutputBaseFilename=mysetup +SetupIconFile=assets\images\icon.ico +Compression=lzma +SolidCompression=yes +WizardStyle=modern + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked + +[Files] +Source: "dist\project_gameface\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion +Source: "dist\project_gameface\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +[Icons] +Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon + +[Run] +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent + diff --git a/run_app.py b/run_app.py index 2ddb7640..c532e7d7 100644 --- a/run_app.py +++ b/run_app.py @@ -14,6 +14,7 @@ import logging import sys +import os import customtkinter import src.gui as gui @@ -21,16 +22,21 @@ from src.pipeline import Pipeline FORMAT = "%(asctime)s %(levelname)s %(name)s: %(funcName)s: %(message)s" -logging.basicConfig(format=FORMAT, - level=logging.INFO, - handlers=[ - logging.FileHandler("log.txt", mode='w'), - logging.StreamHandler(sys.stdout) - ]) +if not os.path.isdir("C:\\Temp\\"): + os.mkdir("C:\\Temp\\") + +logging.basicConfig( + format=FORMAT, + level=logging.INFO, + handlers=[ + logging.FileHandler("C:\Temp\log.txt", mode="w"), + logging.StreamHandler(sys.stdout), + ], +) -class MainApp(gui.MainGui, Pipeline): +class MainApp(gui.MainGui, Pipeline): def __init__(self, tk_root): super().__init__(tk_root) # Wait for window drawing. diff --git a/src/gui/pages/page_select_camera.py b/src/gui/pages/page_select_camera.py index e540903a..7bf12c04 100644 --- a/src/gui/pages/page_select_camera.py +++ b/src/gui/pages/page_select_camera.py @@ -54,7 +54,7 @@ def __init__(self, master, **kwargs): self.label.grid(row=1, column=0, padx=10, pady=(20, 10), sticky="nw") # Empty radio buttons - self.radio_var = tkinter.IntVar(0) + self.radio_var = tkinter.IntVar(value=0) self.prev_radio_value = None self.radios = [] From cd57d88ecc8f63d69196590c771069a9ea89eb52 Mon Sep 17 00:00:00 2001 From: will wade Date: Tue, 27 Jun 2023 19:41:05 +0100 Subject: [PATCH 003/123] runas admin version --- installer.iss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer.iss b/installer.iss index bdd887a3..95419747 100644 --- a/installer.iss +++ b/installer.iss @@ -38,5 +38,5 @@ Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon [Run] -Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent shellexec; Verb: runas From 7f5d322334dc129682a41991aa0990bd94c8b22e Mon Sep 17 00:00:00 2001 From: will wade Date: Tue, 27 Jun 2023 19:48:39 +0100 Subject: [PATCH 004/123] creating inno6 version of our build gh action --- .github/workflows/windows-build-release.yml | 32 +++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/windows-build-release.yml diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml new file mode 100644 index 00000000..397bfe9d --- /dev/null +++ b/.github/workflows/windows-build-release.yml @@ -0,0 +1,32 @@ +on: + workflow_dispatch: + push: + branches: + - master + - inno6-installer + pull_request: + +jobs: + build: + runs-on: windows-latest + 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 + pip install -r requirements.txt + - name: Build + run: | + pyinstaller build.spec + - name: Build Installer + run: | + iscc installer.iss + - name: Upload exe + uses: actions/upload-artifact@v3 + with: + name: 'Windows Release' + path: 'GameFace Setup.exe' \ No newline at end of file From 40705fbb60d9506dc60035102484ccd1618070f9 Mon Sep 17 00:00:00 2001 From: will wade Date: Tue, 27 Jun 2023 19:55:09 +0100 Subject: [PATCH 005/123] updating requirements --- requirements.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0c89912b..5b365d1b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ opencv-contrib-python==4.7.0.72 psutil==5.9.4 pyautogui==0.9.53 customtkinter==5.1.2 -PyDirectInput==1.0.4 -pywin32==306 -mediapipe==0.9.3.0 \ No newline at end of file +mediapipe==0.9.3.0 +PyDirectInput==1.0.4; sys_platform == 'win32' +pywin32==306; sys_platform == 'win32' +pyinstaller==5.11.0 From 4c0a6711bec81473c44245fa6ea0dd1baf90ffef Mon Sep 17 00:00:00 2001 From: will wade Date: Tue, 27 Jun 2023 20:03:55 +0100 Subject: [PATCH 006/123] mysetup -> projectGameface Installer --- installer.iss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer.iss b/installer.iss index 95419747..4fd190b5 100644 --- a/installer.iss +++ b/installer.iss @@ -16,7 +16,7 @@ DefaultDirName={autopf}\{#MyAppName} DisableProgramGroupPage=yes ; Uncomment the following line to run in non administrative install mode (install for current user only.) ;PrivilegesRequired=lowest -OutputBaseFilename=mysetup +OutputBaseFilename=ProjectGameFace-Installer SetupIconFile=assets\images\icon.ico Compression=lzma SolidCompression=yes From 2dc590f9a03e31a66582c09d2ee268a2d1bd8ca1 Mon Sep 17 00:00:00 2001 From: will wade Date: Tue, 27 Jun 2023 20:07:20 +0100 Subject: [PATCH 007/123] adding readme details --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 29d86a7a..fcf741ca 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,20 @@ Project Gameface helps gamers control their mouse cursor using their head moveme -# Download +# Download + +## Single portable directory + 1. Download the program from [Release section](../../releases/) 2. Run `run_app.exe` +## Installer + +1. Download the Gameface-Installer.exe from [Release section](../../releases/) +2. Install it +3. Run from your Windows shortucts/desktop + # Model used MediaPipe Face Landmark Detection API [Task Guide](https://developers.google.com/mediapipe/solutions/vision/face_landmarker) @@ -88,7 +97,13 @@ gesture_name: [device_name, action_name, threshold, trigger_type] # Build + +## Pyinstaller / Frozen app ``` pyinstaller build.spec ``` +# Build Installer + +1. Install [inno6](https://jrsoftware.org/isdl.php#stable) +2. Build using the `installer.iss` file \ No newline at end of file From 7bb3107d6f598adcdd58e7fff2276edac965f011 Mon Sep 17 00:00:00 2001 From: will wade Date: Tue, 27 Jun 2023 21:46:43 +0100 Subject: [PATCH 008/123] Update windows-build-release.yml --- .github/workflows/windows-build-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 397bfe9d..b1308b85 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -29,4 +29,4 @@ jobs: uses: actions/upload-artifact@v3 with: name: 'Windows Release' - path: 'GameFace Setup.exe' \ No newline at end of file + path: 'ProjectGameFace-Installer.exe' From 417da76934e04b1a7db0dafb7723e9af68fef895 Mon Sep 17 00:00:00 2001 From: will wade Date: Tue, 27 Jun 2023 21:56:34 +0100 Subject: [PATCH 009/123] reverting to installer name --- installer.iss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer.iss b/installer.iss index 4fd190b5..a2863427 100644 --- a/installer.iss +++ b/installer.iss @@ -16,7 +16,7 @@ DefaultDirName={autopf}\{#MyAppName} DisableProgramGroupPage=yes ; Uncomment the following line to run in non administrative install mode (install for current user only.) ;PrivilegesRequired=lowest -OutputBaseFilename=ProjectGameFace-Installer +OutputBaseFilename='GameFace Installer.exe' SetupIconFile=assets\images\icon.ico Compression=lzma SolidCompression=yes From 8c2f71e64ff82cb8e7d992d7dfe53e95f22aacfb Mon Sep 17 00:00:00 2001 From: will wade Date: Tue, 27 Jun 2023 21:56:59 +0100 Subject: [PATCH 010/123] reverting binary name --- .github/workflows/windows-build-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index b1308b85..288048bb 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -29,4 +29,4 @@ jobs: uses: actions/upload-artifact@v3 with: name: 'Windows Release' - path: 'ProjectGameFace-Installer.exe' + path: 'GameFace Installer.exe' From 9392ad030947ce5eb1f14f6d1b3e5000a135defc Mon Sep 17 00:00:00 2001 From: will wade Date: Tue, 27 Jun 2023 22:08:53 +0100 Subject: [PATCH 011/123] trying diff way of doing name --- installer.iss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer.iss b/installer.iss index a2863427..5d573a96 100644 --- a/installer.iss +++ b/installer.iss @@ -16,7 +16,7 @@ DefaultDirName={autopf}\{#MyAppName} DisableProgramGroupPage=yes ; Uncomment the following line to run in non administrative install mode (install for current user only.) ;PrivilegesRequired=lowest -OutputBaseFilename='GameFace Installer.exe' +OutputBaseFilename=GameFace Installer SetupIconFile=assets\images\icon.ico Compression=lzma SolidCompression=yes From fa17bd6f3c1b2b040a0341cfbae802c18bb24ddf Mon Sep 17 00:00:00 2001 From: will wade Date: Tue, 27 Jun 2023 22:31:37 +0100 Subject: [PATCH 012/123] a fix for #7 Amazing this works in the current build. https://github.com/google/project-gameface/issues/7 --- assets/themes/google_theme.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/themes/google_theme.json b/assets/themes/google_theme.json index b9fe9d4e..7ff52a30 100644 --- a/assets/themes/google_theme.json +++ b/assets/themes/google_theme.json @@ -48,7 +48,7 @@ "corner_radius": 500, "border_width": 0, "button_length": 0, - "fg_Color": ["#444746", "#4A4D50"], + "fg_color": ["#444746", "#4A4D50"], "progress_color": ["#64DD17", "#1f538d"], "button_color": ["#8F8F8F", "#D5D9DE"], "button_hover_color": ["gray20", "gray100"], From 9225d13fd4ea9832f7f0e2682c5fedd9b80cb7e6 Mon Sep 17 00:00:00 2001 From: will wade Date: Wed, 28 Jun 2023 14:29:04 +0100 Subject: [PATCH 013/123] migrating config saving to C:/Users//Gameface/ --- src/config_manager.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/config_manager.py b/src/config_manager.py index ad7a8bbc..bb69358c 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -18,6 +18,7 @@ import tkinter as tk import time import shutil +import os from pathlib import Path from src.singleton_meta import Singleton @@ -25,12 +26,20 @@ VERSION = "0.3.30" -DEFAULT_JSON = Path("configs/default.json") -BACKUP_PROFILE = Path("configs/default") +DEFAULT_JSON = Path(f"C:/Users/{os.getlogin()}/Gameface/configs/default.json") +BACKUP_PROFILE = Path(f"C:/Users/{os.getlogin()}/Gameface/configs/default") logger = logging.getLogger("ConfigManager") +if not os.path.isdir(f"C:/Users/{os.getlogin()}/Gameface/configs/"): + shutil.copytree("configs", f"C:/Users/{os.getlogin()}/Gameface/configs/") + os.mkdir(f"C:/Users/{os.getlogin()}/configs/") + +if not os.path.isdir(f"C:/Users/{os.getlogin()}/Gameface/configs/default"): + os.mkdir(f"C:/Users/{os.getlogin()}/Gameface/configs/default") + + class ConfigManager(metaclass=Singleton): def __init__(self): From 0c4dcc74b5e201db062b4b15f0b5fa9a2e2c1770 Mon Sep 17 00:00:00 2001 From: will wade Date: Wed, 28 Jun 2023 14:44:12 +0100 Subject: [PATCH 014/123] fixing location of path for binary --- .github/workflows/windows-build-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 288048bb..2c6bf2fe 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -29,4 +29,4 @@ jobs: uses: actions/upload-artifact@v3 with: name: 'Windows Release' - path: 'GameFace Installer.exe' + path: '\a\project-gameface\project-gameface\Output\GameFace Installer.exe' From 716ca9790dda5b9ef3afbdd3f0f9d1fa383c316c Mon Sep 17 00:00:00 2001 From: will wade Date: Wed, 28 Jun 2023 15:52:24 +0100 Subject: [PATCH 015/123] fixing color? --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5b365d1b..d6db6ffb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ matplotlib==3.7.1 opencv-contrib-python==4.7.0.72 psutil==5.9.4 pyautogui==0.9.53 -customtkinter==5.1.2 +customtkinter mediapipe==0.9.3.0 PyDirectInput==1.0.4; sys_platform == 'win32' pywin32==306; sys_platform == 'win32' From 70e95e124c5fad2a3b65fbad75ad115c1b15e3c3 Mon Sep 17 00:00:00 2001 From: will wade Date: Wed, 28 Jun 2023 15:53:15 +0100 Subject: [PATCH 016/123] fixing verrsion to latest for customtkinter --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d6db6ffb..1bad4ace 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ matplotlib==3.7.1 opencv-contrib-python==4.7.0.72 psutil==5.9.4 pyautogui==0.9.53 -customtkinter +customtkinter==5.2.0 mediapipe==0.9.3.0 PyDirectInput==1.0.4; sys_platform == 'win32' pywin32==306; sys_platform == 'win32' From 2b196a1dd6c8e4aacef448817008d3ec1d4e55f6 Mon Sep 17 00:00:00 2001 From: will wade Date: Wed, 28 Jun 2023 16:14:03 +0100 Subject: [PATCH 017/123] add icon for app --- build.spec | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.spec b/build.spec index f9e3c58d..05966603 100644 --- a/build.spec +++ b/build.spec @@ -48,6 +48,7 @@ exe_app = EXE( disable_windowed_traceback=False, argv_emulation=False, target_arch=None, + icon='assets/images/icon.ico', codesign_identity=None, entitlements_file=None, ) @@ -62,4 +63,4 @@ coll = COLLECT( upx=True, upx_exclude=[], name='project_gameface', -) +) \ No newline at end of file From c23b1a1895b0d646c732c8d643e141a0055b61eb Mon Sep 17 00:00:00 2001 From: will wade Date: Wed, 28 Jun 2023 22:38:32 +0100 Subject: [PATCH 018/123] fiximg issues with fgcolor --- assets/themes/google_theme.json | 2 +- build.spec | 2 +- installer.iss | 4 ++-- src/shape_list.py | 2 -- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/assets/themes/google_theme.json b/assets/themes/google_theme.json index 7ff52a30..b9fe9d4e 100644 --- a/assets/themes/google_theme.json +++ b/assets/themes/google_theme.json @@ -48,7 +48,7 @@ "corner_radius": 500, "border_width": 0, "button_length": 0, - "fg_color": ["#444746", "#4A4D50"], + "fg_Color": ["#444746", "#4A4D50"], "progress_color": ["#64DD17", "#1f538d"], "button_color": ["#8F8F8F", "#D5D9DE"], "button_hover_color": ["gray20", "gray100"], diff --git a/build.spec b/build.spec index 05966603..ed216f80 100644 --- a/build.spec +++ b/build.spec @@ -63,4 +63,4 @@ coll = COLLECT( upx=True, upx_exclude=[], name='project_gameface', -) \ No newline at end of file +) diff --git a/installer.iss b/installer.iss index 5d573a96..eb6e9c99 100644 --- a/installer.iss +++ b/installer.iss @@ -16,7 +16,7 @@ DefaultDirName={autopf}\{#MyAppName} DisableProgramGroupPage=yes ; Uncomment the following line to run in non administrative install mode (install for current user only.) ;PrivilegesRequired=lowest -OutputBaseFilename=GameFace Installer +OutputBaseFilename=mysetup SetupIconFile=assets\images\icon.ico Compression=lzma SolidCompression=yes @@ -38,5 +38,5 @@ Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon [Run] -Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent shellexec; Verb: runas +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Verb: runas; Flags: postinstall skipifsilent shellexec runascurrentuser waituntilterminated; diff --git a/src/shape_list.py b/src/shape_list.py index 3a89ca7f..45f483de 100644 --- a/src/shape_list.py +++ b/src/shape_list.py @@ -85,9 +85,7 @@ "Open mouth": "assets/images/dropdowns/Open mouth.png", "Mouth left": "assets/images/dropdowns/Mouth left.png", "Mouth right": "assets/images/dropdowns/Mouth right.png", - "Roll lower mouth": "assets/images/dropdowns/Roll lower mouth.png", "Raise left eyebrow": "assets/images/dropdowns/Raise left eyebrow.png", - "Lower left eyebrow": "assets/images/dropdowns/Lower left eyebrow.png", "Raise right eyebrow": "assets/images/dropdowns/Raise right eyebrow.png", "Lower right eyebrow": "assets/images/dropdowns/Lower right eyebrow.png", } From 970bd16057d7f5a43ca619f4a6a628407d5c61f4 Mon Sep 17 00:00:00 2001 From: will wade Date: Wed, 28 Jun 2023 22:48:31 +0100 Subject: [PATCH 019/123] reputting in correct name of app --- installer.iss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer.iss b/installer.iss index eb6e9c99..24751962 100644 --- a/installer.iss +++ b/installer.iss @@ -16,7 +16,7 @@ DefaultDirName={autopf}\{#MyAppName} DisableProgramGroupPage=yes ; Uncomment the following line to run in non administrative install mode (install for current user only.) ;PrivilegesRequired=lowest -OutputBaseFilename=mysetup +OutputBaseFilename=GameFace Installer SetupIconFile=assets\images\icon.ico Compression=lzma SolidCompression=yes From 552879bbb08dc33ead5ffbf2751292688f428770 Mon Sep 17 00:00:00 2001 From: will wade Date: Wed, 28 Jun 2023 22:53:51 +0100 Subject: [PATCH 020/123] testing log path to Gameface user dir --- run_app.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/run_app.py b/run_app.py index c532e7d7..142ded1c 100644 --- a/run_app.py +++ b/run_app.py @@ -23,14 +23,15 @@ FORMAT = "%(asctime)s %(levelname)s %(name)s: %(funcName)s: %(message)s" -if not os.path.isdir("C:\\Temp\\"): - os.mkdir("C:\\Temp\\") +log_path = os.environ['USERPROFILE']+'\Gameface' +if not os.path.isdir(log_path): + os.mkdir(log_path) logging.basicConfig( format=FORMAT, level=logging.INFO, handlers=[ - logging.FileHandler("C:\Temp\log.txt", mode="w"), + logging.FileHandler(log_path+'\log.txt", mode="w"), logging.StreamHandler(sys.stdout), ], ) From 13f73b1ef2231cb5be7f92fc71a2efc286650dab Mon Sep 17 00:00:00 2001 From: will wade Date: Wed, 28 Jun 2023 22:58:58 +0100 Subject: [PATCH 021/123] stupid typo --- run_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run_app.py b/run_app.py index 142ded1c..f2807bc5 100644 --- a/run_app.py +++ b/run_app.py @@ -31,7 +31,7 @@ format=FORMAT, level=logging.INFO, handlers=[ - logging.FileHandler(log_path+'\log.txt", mode="w"), + logging.FileHandler(log_path+'\log.txt', mode="w"), logging.StreamHandler(sys.stdout), ], ) From 9212f7d96588ee363dcd16c971e97e4a0044b755 Mon Sep 17 00:00:00 2001 From: will wade Date: Wed, 28 Jun 2023 23:09:37 +0100 Subject: [PATCH 022/123] gah had reverted the switch color bug --- assets/themes/google_theme.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/themes/google_theme.json b/assets/themes/google_theme.json index b9fe9d4e..7ff52a30 100644 --- a/assets/themes/google_theme.json +++ b/assets/themes/google_theme.json @@ -48,7 +48,7 @@ "corner_radius": 500, "border_width": 0, "button_length": 0, - "fg_Color": ["#444746", "#4A4D50"], + "fg_color": ["#444746", "#4A4D50"], "progress_color": ["#64DD17", "#1f538d"], "button_color": ["#8F8F8F", "#D5D9DE"], "button_hover_color": ["gray20", "gray100"], From 22a1490fc920c1589b6b31e87b6df7f29b83f9c3 Mon Sep 17 00:00:00 2001 From: will wade Date: Thu, 29 Jun 2023 13:34:55 +0100 Subject: [PATCH 023/123] build portable and autostart re https://github.com/google/project-gameface/issues/33 --- .github/workflows/windows-build-release.yml | 2 +- build-portable.spec | 57 +++++++++++++++++++++ installer.iss | 4 ++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 build-portable.spec diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 2c6bf2fe..e0103304 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -21,7 +21,7 @@ jobs: pip install -r requirements.txt - name: Build run: | - pyinstaller build.spec + pyinstaller build-portable.spec - name: Build Installer run: | iscc installer.iss diff --git a/build-portable.spec b/build-portable.spec new file mode 100644 index 00000000..0f259e03 --- /dev/null +++ b/build-portable.spec @@ -0,0 +1,57 @@ +# -*- mode: python ; coding: utf-8 -*- + +from pathlib import Path +import mediapipe +import customtkinter + +block_cipher = None + +mp_init = Path(mediapipe.__file__) +mp_modules = Path(mp_init.parent,"modules") + +ctk_init = Path(customtkinter.__file__) +ctk_modules = Path(ctk_init.parent,"modules") + + + +app = Analysis( + ['run_app.py'], + pathex=[], + binaries=[], + datas=[(mp_modules.as_posix(), 'mediapipe/modules'), + ('assets','assets'), + ('configs','configs'), + (ctk_init.parent.as_posix(), 'customtkinter')], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) +pyz_app = PYZ(app.pure, app.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz_app, + app.scripts, + app.binaries, + app.zipfiles, + app.datas, + [], + name='run_app', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon='assets/images/icon.ico', +) diff --git a/installer.iss b/installer.iss index 24751962..6717248d 100644 --- a/installer.iss +++ b/installer.iss @@ -27,6 +27,8 @@ Name: "english"; MessagesFile: "compiler:Default.isl" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked +Name: "autostarticon"; Description: "{cm:AutoStartProgram,{#MyAppName}}"; GroupDescription: "{cm:AdditionalIcons}"; + [Files] Source: "dist\project_gameface\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion @@ -36,6 +38,8 @@ Source: "dist\project_gameface\*"; DestDir: "{app}"; Flags: ignoreversion recurs [Icons] Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon +Name: "{userstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Parameters: "/auto"; Tasks: autostarticon +; Name: "{commonstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Parameters: "/auto"; Tasks: autostarticon [Run] Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Verb: runas; Flags: postinstall skipifsilent shellexec runascurrentuser waituntilterminated; From 11612e00e0fcb49dbfdafaee737f29e02e10558c Mon Sep 17 00:00:00 2001 From: will wade Date: Thu, 29 Jun 2023 14:19:27 +0100 Subject: [PATCH 024/123] adding portable build --- .github/workflows/windows-build-release.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index e0103304..20ec1d75 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -21,12 +21,21 @@ jobs: pip install -r requirements.txt - name: Build run: | - pyinstaller build-portable.spec + pyinstaller --distpath dist-portable build-portable.spec + Copy-Item -Path assets -Destination dist-portable\ -Recurse + Copy-Item -Path configs -Destination dist-portable\ -Recurse + Compress-Archive -Path dist-portable -DestinationPath GameFaceInstaller.zip + pyinstaller build.spec - name: Build Installer run: | iscc installer.iss - - name: Upload exe + - name: Upload installer uses: actions/upload-artifact@v3 with: name: 'Windows Release' path: '\a\project-gameface\project-gameface\Output\GameFace Installer.exe' + - name: Upload portable + uses: actions/upload-artifact@v3 + with: + name: 'Windows Release' + path: '\a\project-gameface\project-gameface\GameFacePortable.exe' From fdb222756da67e862f67b7043448951c3380a64f Mon Sep 17 00:00:00 2001 From: will wade Date: Thu, 29 Jun 2023 14:20:55 +0100 Subject: [PATCH 025/123] stupid typo --- .github/workflows/windows-build-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 20ec1d75..228567db 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -24,7 +24,7 @@ jobs: pyinstaller --distpath dist-portable build-portable.spec Copy-Item -Path assets -Destination dist-portable\ -Recurse Copy-Item -Path configs -Destination dist-portable\ -Recurse - Compress-Archive -Path dist-portable -DestinationPath GameFaceInstaller.zip + Compress-Archive -Path dist-portable -DestinationPath GameFacePortable.zip pyinstaller build.spec - name: Build Installer run: | From 701dff23b0496917500fa95069c5d5f424a13834 Mon Sep 17 00:00:00 2001 From: will wade Date: Thu, 29 Jun 2023 18:15:36 +0100 Subject: [PATCH 026/123] clearer sections for zipping --- .github/workflows/windows-build-release.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 228567db..7de6c2bf 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -22,10 +22,12 @@ jobs: - name: Build run: | pyinstaller --distpath dist-portable build-portable.spec + pyinstaller build.spec + - name: Zip Portable + run: | Copy-Item -Path assets -Destination dist-portable\ -Recurse Copy-Item -Path configs -Destination dist-portable\ -Recurse Compress-Archive -Path dist-portable -DestinationPath GameFacePortable.zip - pyinstaller build.spec - name: Build Installer run: | iscc installer.iss From 7cb0bfdd8ec2ef740d25b989d7eb58f29a03e28f Mon Sep 17 00:00:00 2001 From: will wade Date: Thu, 29 Jun 2023 22:03:15 +0100 Subject: [PATCH 027/123] see if shell: pwsh fixes things --- .github/workflows/windows-build-release.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 7de6c2bf..98c91720 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -19,18 +19,21 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - - name: Build + - name: Freeze Installer run: | pyinstaller --distpath dist-portable build-portable.spec - pyinstaller build.spec + - name: Build Installer + run: | + iscc installer.iss + - name: Freeze portable + run: | + pyinstaller build.spec - name: Zip Portable + shell: pwsh run: | Copy-Item -Path assets -Destination dist-portable\ -Recurse Copy-Item -Path configs -Destination dist-portable\ -Recurse Compress-Archive -Path dist-portable -DestinationPath GameFacePortable.zip - - name: Build Installer - run: | - iscc installer.iss - name: Upload installer uses: actions/upload-artifact@v3 with: From 4a136f4352b4db40a672e8dc9f02309f5fec8540 Mon Sep 17 00:00:00 2001 From: will wade Date: Thu, 29 Jun 2023 22:09:24 +0100 Subject: [PATCH 028/123] swap installer and portable --- .github/workflows/windows-build-release.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 98c91720..84856f59 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -21,13 +21,13 @@ jobs: pip install -r requirements.txt - name: Freeze Installer run: | - pyinstaller --distpath dist-portable build-portable.spec + pyinstaller build.spec - name: Build Installer run: | - iscc installer.iss - - name: Freeze portable + iscc installer.iss + - name: Freeze Portable run: | - pyinstaller build.spec + pyinstaller --distpath dist-portable build-portable.spec - name: Zip Portable shell: pwsh run: | From a60eba5cd3a48b726ed799169ed30f332083983d Mon Sep 17 00:00:00 2001 From: will wade Date: Thu, 29 Jun 2023 22:17:45 +0100 Subject: [PATCH 029/123] d'oh! its a zip not a exe --- .github/workflows/windows-build-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 84856f59..04f71a0b 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -43,4 +43,4 @@ jobs: uses: actions/upload-artifact@v3 with: name: 'Windows Release' - path: '\a\project-gameface\project-gameface\GameFacePortable.exe' + path: '\a\project-gameface\project-gameface\GameFacePortable.zip' From 58463472f5922170cc598bf27ebb57eda257bfa3 Mon Sep 17 00:00:00 2001 From: will wade Date: Thu, 29 Jun 2023 22:27:11 +0100 Subject: [PATCH 030/123] one last thing --- .github/workflows/windows-build-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 04f71a0b..318a29b6 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -37,10 +37,10 @@ jobs: - name: Upload installer uses: actions/upload-artifact@v3 with: - name: 'Windows Release' + name: 'Windows Installer Release' path: '\a\project-gameface\project-gameface\Output\GameFace Installer.exe' - name: Upload portable uses: actions/upload-artifact@v3 with: - name: 'Windows Release' + name: 'Windows Portable Release' path: '\a\project-gameface\project-gameface\GameFacePortable.zip' From 6ad745260de9ebf68ee49ab56a2d6c0640a416ed Mon Sep 17 00:00:00 2001 From: will wade Date: Fri, 30 Jun 2023 13:04:52 +0100 Subject: [PATCH 031/123] unchecking desktop defaults --- installer.iss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer.iss b/installer.iss index 6717248d..5cd53243 100644 --- a/installer.iss +++ b/installer.iss @@ -26,7 +26,7 @@ WizardStyle=modern Name: "english"; MessagesFile: "compiler:Default.isl" [Tasks] -Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Name: "autostarticon"; Description: "{cm:AutoStartProgram,{#MyAppName}}"; GroupDescription: "{cm:AdditionalIcons}"; From f5bb7a849622ae47ac697e7a05d898e1d228b542 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Thu, 9 Nov 2023 22:15:42 +0100 Subject: [PATCH 032/123] rename to Grimassist --- README.md | 4 ++-- src/gui/main_gui.py | 2 +- src/gui/pages/page_home.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 29d86a7a..e643ab39 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# Project Gameface -Project Gameface helps gamers control their mouse cursor using their head movement and facial gestures. +# Grimassist +Grimassist helps gamers control their mouse cursor using their head movement and facial gestures. diff --git a/src/gui/main_gui.py b/src/gui/main_gui.py index ff052b7c..90ba081b 100644 --- a/src/gui/main_gui.py +++ b/src/gui/main_gui.py @@ -37,7 +37,7 @@ def __init__(self, tk_root): self.tk_root = tk_root self.tk_root.geometry("1024x658") - self.tk_root.title(f"Project Gameface {ConfigManager().version}") + self.tk_root.title(f"Grimassist {ConfigManager().version}") self.tk_root.iconbitmap("assets/images/icon.ico") self.tk_root.resizable(width=False, height=False) diff --git a/src/gui/pages/page_home.py b/src/gui/pages/page_home.py index 11b071f8..2d2ad768 100644 --- a/src/gui/pages/page_home.py +++ b/src/gui/pages/page_home.py @@ -39,7 +39,7 @@ def __init__(self, master, root_callback: callable, **kwargs): # Top text top_label = customtkinter.CTkLabel( - master=self, text="Project Gameface Gesture Settings") + master=self, text="Grimassist Gesture Settings") top_label.cget("font").configure(size=24) top_label.grid(row=0, column=0, @@ -49,7 +49,7 @@ def __init__(self, master, root_callback: callable, **kwargs): columnspan=2) # Description - des_txt = "Project Gameface helps gamers control their mouse cursor using their head movement and facial gestures." + des_txt = "Grimassist helps gamers control their mouse cursor using their head movement and facial gestures." des_label = customtkinter.CTkLabel(master=self, text=des_txt, wraplength=400, @@ -63,7 +63,7 @@ def __init__(self, master, root_callback: callable, **kwargs): columnspan=2) # Disclaimer - disc_txt = "Disclaimer: Project Gameface is not intended for medical use." + disc_txt = "Disclaimer: Grimassist is not intended for medical use." disc_label = customtkinter.CTkLabel(master=self, text=disc_txt, wraplength=700, From 2e507428d3217cd09bd67a81d7c038907729fa94 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Fri, 10 Nov 2023 15:50:13 +0100 Subject: [PATCH 033/123] remove redundant license comments --- CODE_OF_CONDUCT.md | 93 ------------------------ CONTRIBUTING | 16 +--- src/accel_graph.py | 14 ---- src/camera_manager.py | 14 ---- src/config_manager.py | 14 ---- src/controllers/__init__.py | 13 ---- src/controllers/keybinder.py | 13 ---- src/controllers/mouse_controller.py | 13 ---- src/detectors/__init__.py | 13 ---- src/detectors/facemesh.py | 13 ---- src/gui/__init__.py | 13 ---- src/gui/balloon.py | 13 ---- src/gui/dropdown.py | 13 ---- src/gui/frames/__init__.py | 13 ---- src/gui/frames/frame_cam_preview.py | 13 ---- src/gui/frames/frame_menu.py | 13 ---- src/gui/frames/frame_profile.py | 13 ---- src/gui/frames/frame_profile_editor.py | 13 ---- src/gui/frames/frame_profile_switcher.py | 13 ---- src/gui/frames/safe_disposable_frame.py | 13 ---- src/gui/main_gui.py | 13 ---- src/gui/pages/__init__.py | 13 ---- src/gui/pages/page_cursor.py | 13 ---- src/gui/pages/page_home.py | 13 ---- src/gui/pages/page_keyboard.py | 13 ---- src/gui/pages/page_select_camera.py | 13 ---- src/gui/pages/page_select_gestures.py | 13 ---- src/pipeline.py | 13 ---- src/singleton_meta.py | 13 ---- src/task_killer.py | 13 ---- src/utils/__init__.py | 13 ---- src/utils/install_font.py | 13 ---- src/utils/list_cameras.py | 13 ---- src/utils/smoothing.py | 13 ---- 34 files changed, 1 insertion(+), 527 deletions(-) delete mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 8ba5767e..00000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,93 +0,0 @@ -# Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of -experience, education, socio-economic status, nationality, personal appearance, -race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, or to ban temporarily or permanently any -contributor for other behaviors that they deem inappropriate, threatening, -offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -This Code of Conduct also applies outside the project spaces when the Project -Steward has a reasonable belief that an individual's behavior may have a -negative impact on the project or its community. - -## Conflict Resolution - -We do not believe that all conflict is bad; healthy debate and disagreement -often yield positive results. However, it is never okay to be disrespectful or -to engage in behavior that violates the project’s code of conduct. - -If you see someone violating the code of conduct, you are encouraged to address -the behavior directly with those involved. Many issues can be resolved quickly -and easily, and this gives people more control over the outcome of their -dispute. If you are unable to resolve the matter for any reason, or if the -behavior is threatening or harassing, report it. We are dedicated to providing -an environment where participants feel welcome and safe. - -Reports should be directed to *[PROJECT STEWARD NAME(s) AND EMAIL(s)]*, the -Project Steward(s) for *[PROJECT NAME]*. It is the Project Steward’s duty to -receive and address reported violations of the code of conduct. They will then -work with a committee consisting of representatives from the Open Source -Programs Office and the Google Open Source Strategy team. If for any reason you -are uncomfortable reaching out to the Project Steward, please email -opensource@google.com. - -We will investigate every complaint, but you may not receive a direct response. -We will use our discretion in determining when and how to follow up on reported -incidents, which may range from not taking action to permanent expulsion from -the project and project-sponsored spaces. We will notify the accused of the -report and provide them an opportunity to discuss it before any action is taken. -The identity of the reporter will be omitted from the details of the report -supplied to the accused. In potentially harmful situations, such as ongoing -harassment or threats to anyone's safety, we may take action without notice. - -## Attribution - -This Code of Conduct is adapted from the Contributor Covenant, version 1.4, -available at -https://www.contributor-covenant.org/version/1/4/code-of-conduct.html \ No newline at end of file diff --git a/CONTRIBUTING b/CONTRIBUTING index 9d7656be..51ac0f52 100644 --- a/CONTRIBUTING +++ b/CONTRIBUTING @@ -3,19 +3,6 @@ We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. -## Contributor License Agreement - -Contributions to this project must be accompanied by a Contributor License -Agreement (CLA). You (or your employer) retain the copyright to your -contribution; this simply gives us permission to use and redistribute your -contributions as part of the project. Head over to - to see your current agreements on file or -to sign a new one. - -You generally only need to submit a CLA once, so if you've already submitted one -(even if it was for a different project), you probably don't need to do it -again. - ## Code Reviews All submissions, including submissions by project members, require review. We @@ -25,5 +12,4 @@ information on using pull requests. ## Community Guidelines -This project follows -[Google's Open Source Community Guidelines](https://opensource.google/conduct/). \ No newline at end of file +Be a decent human being, bot, or whatever. \ No newline at end of file diff --git a/src/accel_graph.py b/src/accel_graph.py index c60748a4..8709ec94 100644 --- a/src/accel_graph.py +++ b/src/accel_graph.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import abc import math diff --git a/src/camera_manager.py b/src/camera_manager.py index cdf3fcc4..0b7d72da 100644 --- a/src/camera_manager.py +++ b/src/camera_manager.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import concurrent.futures as futures import logging import threading diff --git a/src/config_manager.py b/src/config_manager.py index cbfd5a84..d6f6247d 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import copy import json import logging diff --git a/src/controllers/__init__.py b/src/controllers/__init__.py index 4f5d8c10..28537d01 100644 --- a/src/controllers/__init__.py +++ b/src/controllers/__init__.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. from .keybinder import * from .mouse_controller import * diff --git a/src/controllers/keybinder.py b/src/controllers/keybinder.py index e3a6c38d..56dc9318 100644 --- a/src/controllers/keybinder.py +++ b/src/controllers/keybinder.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. import copy import logging diff --git a/src/controllers/mouse_controller.py b/src/controllers/mouse_controller.py index 25bafd15..ba1a93b5 100644 --- a/src/controllers/mouse_controller.py +++ b/src/controllers/mouse_controller.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. import concurrent.futures as futures import logging import threading diff --git a/src/detectors/__init__.py b/src/detectors/__init__.py index 688689dc..d573beb2 100644 --- a/src/detectors/__init__.py +++ b/src/detectors/__init__.py @@ -1,15 +1,2 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. from .facemesh import * diff --git a/src/detectors/facemesh.py b/src/detectors/facemesh.py index c974a767..5f61f329 100644 --- a/src/detectors/facemesh.py +++ b/src/detectors/facemesh.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. import logging import time diff --git a/src/gui/__init__.py b/src/gui/__init__.py index 3d280881..0fb4001e 100644 --- a/src/gui/__init__.py +++ b/src/gui/__init__.py @@ -1,15 +1,2 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. from .main_gui import * diff --git a/src/gui/balloon.py b/src/gui/balloon.py index a2c7da15..8fa70e3d 100644 --- a/src/gui/balloon.py +++ b/src/gui/balloon.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. from functools import partial diff --git a/src/gui/dropdown.py b/src/gui/dropdown.py index 691b46f7..bf17fa4f 100644 --- a/src/gui/dropdown.py +++ b/src/gui/dropdown.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. from functools import partial diff --git a/src/gui/frames/__init__.py b/src/gui/frames/__init__.py index 3fc7740c..dfcf2080 100644 --- a/src/gui/frames/__init__.py +++ b/src/gui/frames/__init__.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. from .frame_cam_preview import * from .frame_menu import * diff --git a/src/gui/frames/frame_cam_preview.py b/src/gui/frames/frame_cam_preview.py index 6fb38be0..d109ca7e 100644 --- a/src/gui/frames/frame_cam_preview.py +++ b/src/gui/frames/frame_cam_preview.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. import tkinter diff --git a/src/gui/frames/frame_menu.py b/src/gui/frames/frame_menu.py index 404d3e49..a3df5ede 100644 --- a/src/gui/frames/frame_menu.py +++ b/src/gui/frames/frame_menu.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. from functools import partial diff --git a/src/gui/frames/frame_profile.py b/src/gui/frames/frame_profile.py index a5285b89..7bc2131f 100644 --- a/src/gui/frames/frame_profile.py +++ b/src/gui/frames/frame_profile.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. import time import logging diff --git a/src/gui/frames/frame_profile_editor.py b/src/gui/frames/frame_profile_editor.py index 2ab26b67..cbce285c 100644 --- a/src/gui/frames/frame_profile_editor.py +++ b/src/gui/frames/frame_profile_editor.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. import logging import re diff --git a/src/gui/frames/frame_profile_switcher.py b/src/gui/frames/frame_profile_switcher.py index 7fa7fbe8..a8da1b8c 100644 --- a/src/gui/frames/frame_profile_switcher.py +++ b/src/gui/frames/frame_profile_switcher.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. import logging import re diff --git a/src/gui/frames/safe_disposable_frame.py b/src/gui/frames/safe_disposable_frame.py index 8e2e5b00..cc464ad0 100644 --- a/src/gui/frames/safe_disposable_frame.py +++ b/src/gui/frames/safe_disposable_frame.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. import logging diff --git a/src/gui/main_gui.py b/src/gui/main_gui.py index 90ba081b..b9ba5416 100644 --- a/src/gui/main_gui.py +++ b/src/gui/main_gui.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. import logging import tkinter as tk diff --git a/src/gui/pages/__init__.py b/src/gui/pages/__init__.py index 7b578760..0a10c0e0 100644 --- a/src/gui/pages/__init__.py +++ b/src/gui/pages/__init__.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. from .page_cursor import * from .page_home import * diff --git a/src/gui/pages/page_cursor.py b/src/gui/pages/page_cursor.py index 20c4466c..a7f813b0 100644 --- a/src/gui/pages/page_cursor.py +++ b/src/gui/pages/page_cursor.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. import logging import tkinter diff --git a/src/gui/pages/page_home.py b/src/gui/pages/page_home.py index 2d2ad768..20935347 100644 --- a/src/gui/pages/page_home.py +++ b/src/gui/pages/page_home.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. import logging import tkinter diff --git a/src/gui/pages/page_keyboard.py b/src/gui/pages/page_keyboard.py index 464d2c48..7526f103 100644 --- a/src/gui/pages/page_keyboard.py +++ b/src/gui/pages/page_keyboard.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. import logging import tkinter as tk diff --git a/src/gui/pages/page_select_camera.py b/src/gui/pages/page_select_camera.py index e718fe8c..31d92f96 100644 --- a/src/gui/pages/page_select_camera.py +++ b/src/gui/pages/page_select_camera.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. import logging import tkinter diff --git a/src/gui/pages/page_select_gestures.py b/src/gui/pages/page_select_gestures.py index 2a070987..f7b5f125 100644 --- a/src/gui/pages/page_select_gestures.py +++ b/src/gui/pages/page_select_gestures.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. import tkinter as tk from functools import partial diff --git a/src/pipeline.py b/src/pipeline.py index 97b0f6b4..582573e0 100644 --- a/src/pipeline.py +++ b/src/pipeline.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. import logging from src.camera_manager import CameraManager diff --git a/src/singleton_meta.py b/src/singleton_meta.py index 7b27edfc..7ecf26d6 100644 --- a/src/singleton_meta.py +++ b/src/singleton_meta.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. class Singleton(type): diff --git a/src/task_killer.py b/src/task_killer.py index 57407c30..48b12e4b 100644 --- a/src/task_killer.py +++ b/src/task_killer.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. import logging import os diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 07b7fe3d..4e3640e3 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. from .install_font import * from .list_cameras import * diff --git a/src/utils/install_font.py b/src/utils/install_font.py index 4c7a737a..9fe998d0 100644 --- a/src/utils/install_font.py +++ b/src/utils/install_font.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. import ctypes import logging diff --git a/src/utils/list_cameras.py b/src/utils/list_cameras.py index 193dc465..f9ec06cc 100644 --- a/src/utils/list_cameras.py +++ b/src/utils/list_cameras.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. import concurrent.futures as futures import logging diff --git a/src/utils/smoothing.py b/src/utils/smoothing.py index 35a2e6b0..613522b4 100644 --- a/src/utils/smoothing.py +++ b/src/utils/smoothing.py @@ -1,16 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. import numpy as np import numpy.typing as npt From 763a67ca590369dc2c547a4a601ddff8961a39ba Mon Sep 17 00:00:00 2001 From: acidcoke Date: Fri, 10 Nov 2023 15:53:19 +0100 Subject: [PATCH 034/123] add file ending to contributing file --- CONTRIBUTING => CONTRIBUTING.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename CONTRIBUTING => CONTRIBUTING.md (100%) diff --git a/CONTRIBUTING b/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING rename to CONTRIBUTING.md From d94dbbd4470a9c858220b41c3c957b3e94a1f6d5 Mon Sep 17 00:00:00 2001 From: will wade Date: Mon, 13 Nov 2023 09:32:22 +0000 Subject: [PATCH 035/123] Update src/config_manager.py Co-authored-by: acidcoke --- src/config_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config_manager.py b/src/config_manager.py index bb69358c..ab6d60d4 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -34,7 +34,7 @@ if not os.path.isdir(f"C:/Users/{os.getlogin()}/Gameface/configs/"): shutil.copytree("configs", f"C:/Users/{os.getlogin()}/Gameface/configs/") - os.mkdir(f"C:/Users/{os.getlogin()}/configs/") + os.mkdir(f"C:/Users/{os.getlogin()}/Gameface/configs/") if not os.path.isdir(f"C:/Users/{os.getlogin()}/Gameface/configs/default"): os.mkdir(f"C:/Users/{os.getlogin()}/Gameface/configs/default") From dbeda438856ef13985d0a67a3e3d412ef08e9cb6 Mon Sep 17 00:00:00 2001 From: will wade Date: Mon, 13 Nov 2023 09:34:20 +0000 Subject: [PATCH 036/123] Update README.md updating 3.10 to follow GH Action --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fcf741ca..ca0b6192 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ MediaPipe Face Landmark Detection API [Task Guide](https://developers.google.com ## Installation > Environment >- Windows ->- Python 3.9 +>- Python 3.10 ``` pip install -r requirements.txt ``` @@ -106,4 +106,4 @@ gesture_name: [device_name, action_name, threshold, trigger_type] # Build Installer 1. Install [inno6](https://jrsoftware.org/isdl.php#stable) -2. Build using the `installer.iss` file \ No newline at end of file +2. Build using the `installer.iss` file From 4054ea86205322bfaea741b294740222ee34ab7d Mon Sep 17 00:00:00 2001 From: will wade Date: Mon, 13 Nov 2023 09:36:03 +0000 Subject: [PATCH 037/123] putting back in missing actions --- src/shape_list.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/shape_list.py b/src/shape_list.py index 45f483de..3a89ca7f 100644 --- a/src/shape_list.py +++ b/src/shape_list.py @@ -85,7 +85,9 @@ "Open mouth": "assets/images/dropdowns/Open mouth.png", "Mouth left": "assets/images/dropdowns/Mouth left.png", "Mouth right": "assets/images/dropdowns/Mouth right.png", + "Roll lower mouth": "assets/images/dropdowns/Roll lower mouth.png", "Raise left eyebrow": "assets/images/dropdowns/Raise left eyebrow.png", + "Lower left eyebrow": "assets/images/dropdowns/Lower left eyebrow.png", "Raise right eyebrow": "assets/images/dropdowns/Raise right eyebrow.png", "Lower right eyebrow": "assets/images/dropdowns/Lower right eyebrow.png", } From d404a2ff722a374e0e3d6e7a7997a879afba8b0a Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 13 Nov 2023 21:01:23 +0100 Subject: [PATCH 038/123] rename to grimassist --- .github/workflows/windows-build-release.yml | 6 +++--- README.md | 2 +- build.spec | 2 +- installer.iss | 8 ++++---- run_app.py | 2 +- src/config_manager.py | 14 +++++++------- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 318a29b6..6feb1435 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -33,14 +33,14 @@ jobs: run: | Copy-Item -Path assets -Destination dist-portable\ -Recurse Copy-Item -Path configs -Destination dist-portable\ -Recurse - Compress-Archive -Path dist-portable -DestinationPath GameFacePortable.zip + Compress-Archive -Path dist-portable -DestinationPath GrimassistPortable.zip - name: Upload installer uses: actions/upload-artifact@v3 with: name: 'Windows Installer Release' - path: '\a\project-gameface\project-gameface\Output\GameFace Installer.exe' + path: '\a\grimassist\grimassist\Output\Grimassist Installer.exe' - name: Upload portable uses: actions/upload-artifact@v3 with: name: 'Windows Portable Release' - path: '\a\project-gameface\project-gameface\GameFacePortable.zip' + path: '\a\grimassist\grimassist\GrimassistPortable.zip' diff --git a/README.md b/README.md index ed7a2323..121a90d9 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Grimassist helps gamers control their mouse cursor using their head movement and ## Installer -1. Download the Gameface-Installer.exe from [Release section](../../releases/) +1. Download the Grimassist-Installer.exe from [Release section](../../releases/) 2. Install it 3. Run from your Windows shortucts/desktop diff --git a/build.spec b/build.spec index ed216f80..cd32a6da 100644 --- a/build.spec +++ b/build.spec @@ -62,5 +62,5 @@ coll = COLLECT( strip=False, upx=True, upx_exclude=[], - name='project_gameface', + name='grimassist', ) diff --git a/installer.iss b/installer.iss index 5cd53243..f0f2907b 100644 --- a/installer.iss +++ b/installer.iss @@ -1,7 +1,7 @@ ; Script generated by the Inno Setup Script Wizard. ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! -#define MyAppName "Gameface" +#define MyAppName "Grimassist" #define MyAppVersion "1" #define MyAppExeName "run_app.exe" @@ -16,7 +16,7 @@ DefaultDirName={autopf}\{#MyAppName} DisableProgramGroupPage=yes ; Uncomment the following line to run in non administrative install mode (install for current user only.) ;PrivilegesRequired=lowest -OutputBaseFilename=GameFace Installer +OutputBaseFilename=Grimassist Installer SetupIconFile=assets\images\icon.ico Compression=lzma SolidCompression=yes @@ -31,8 +31,8 @@ Name: "autostarticon"; Description: "{cm:AutoStartProgram,{#MyAppName}}"; GroupD [Files] -Source: "dist\project_gameface\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion -Source: "dist\project_gameface\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "dist\grimassist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion +Source: "dist\grimassist\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs ; NOTE: Don't use "Flags: ignoreversion" on any shared system files [Icons] diff --git a/run_app.py b/run_app.py index 3dd5b7b3..a6d1df03 100644 --- a/run_app.py +++ b/run_app.py @@ -23,7 +23,7 @@ FORMAT = "%(asctime)s %(levelname)s %(name)s: %(funcName)s: %(message)s" -log_path = os.environ['USERPROFILE']+'\Gameface' +log_path = os.environ['USERPROFILE']+'\Grimassist' if not os.path.isdir(log_path): os.mkdir(log_path) diff --git a/src/config_manager.py b/src/config_manager.py index 56b238dd..89f65536 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -12,18 +12,18 @@ VERSION = "0.3.33" -DEFAULT_JSON = Path(f"C:/Users/{os.getlogin()}/Gameface/configs/default.json") -BACKUP_PROFILE = Path(f"C:/Users/{os.getlogin()}/Gameface/configs/default") +DEFAULT_JSON = Path(f"C:/Users/{os.getlogin()}/Grimassist/configs/default.json") +BACKUP_PROFILE = Path(f"C:/Users/{os.getlogin()}/Grimassist/configs/default") logger = logging.getLogger("ConfigManager") -if not os.path.isdir(f"C:/Users/{os.getlogin()}/Gameface/configs/"): - shutil.copytree("configs", f"C:/Users/{os.getlogin()}/Gameface/configs/") - os.mkdir(f"C:/Users/{os.getlogin()}/Gameface/configs/") +if not os.path.isdir(f"C:/Users/{os.getlogin()}/Grimassist/configs/"): + shutil.copytree("configs", f"C:/Users/{os.getlogin()}/Grimassist/configs/") + os.mkdir(f"C:/Users/{os.getlogin()}/Grimassist/configs/") -if not os.path.isdir(f"C:/Users/{os.getlogin()}/Gameface/configs/default"): - os.mkdir(f"C:/Users/{os.getlogin()}/Gameface/configs/default") +if not os.path.isdir(f"C:/Users/{os.getlogin()}/Grimassist/configs/default"): + os.mkdir(f"C:/Users/{os.getlogin()}/Grimassist/configs/default") class ConfigManager(metaclass=Singleton): From e2952aa328a361eeac99cd20bb645a4e21327d2a Mon Sep 17 00:00:00 2001 From: will wade Date: Mon, 13 Nov 2023 22:16:20 +0000 Subject: [PATCH 039/123] Update windows-build-release.yml fixing for new repo layout. master -> main --- .github/workflows/windows-build-release.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 6feb1435..c33b2c37 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -2,8 +2,7 @@ on: workflow_dispatch: push: branches: - - master - - inno6-installer + - main pull_request: jobs: From 4434cb6df6766e5eab8b60d2ef3ebf0b1e8baa1a Mon Sep 17 00:00:00 2001 From: acidcoke Date: Tue, 14 Nov 2023 16:50:50 +0100 Subject: [PATCH 040/123] Merge pull request #2 from acidcoke/add-cursor-toggle Squashed commit of the following: commit d7f98bf11d4a8ef561b9f206692f26715a8cb190 Merge: b6e7bd5 57f5af9 Author: acidcoke Date: Tue Nov 14 16:13:20 2023 +0100 Merge branch 'add-cursor-toggle' commit 57f5af90740ed74cc325648f7f8686b8574ad1bb Merge: 260afdf 763a67c Author: acidcoke Date: Sun Nov 12 21:46:14 2023 +0100 Merge branch 'main' into add-cursor-toggle commit 260afdf30d2a66d858d82f8048cac828eaec7ca5 Author: acidcoke Date: Mon Jun 5 22:27:11 2023 +0200 fix profile_1 default commit 6826ca78d1f91b0cdf6be526472233fb0a3f84b4 Author: acidcoke Date: Sat Jun 3 17:24:14 2023 +0200 fix bug were inactive keybinder was still recognizing commit 27dc7b6b5c0f7ec1ae7ca9754f339aad4da33ce9 Author: acidcoke Date: Mon May 29 21:41:30 2023 +0200 make cursor setting saveable commit 9f2b4a484a90fb36638e228569adc9273dc46828 Author: acidcoke Date: Mon May 29 21:08:31 2023 +0200 disabling cursor also disables sliders commit e2c5ba41f4fd765595dff86523767152ae5b6275 Author: acidcoke Date: Mon May 29 21:07:33 2023 +0200 fix slider ui commit 55c00f5577eda6932bb1ef46412c96d28fc85b20 Author: acidcoke Date: Sun May 28 13:32:56 2023 +0200 roughly implement cursor toggle --- configs/default/cursor.json | 1 + configs/profile_1/cursor.json | 13 ++--- configs/profile_2/cursor.json | 1 + src/camera_manager.py | 4 +- src/controllers/keybinder.py | 20 ++++++-- src/controllers/mouse_controller.py | 14 +++-- src/gui/frames/frame_cam_preview.py | 6 +-- src/gui/main_gui.py | 5 +- src/gui/pages/page_cursor.py | 80 ++++++++++++++++++++++++++--- 9 files changed, 118 insertions(+), 26 deletions(-) diff --git a/configs/default/cursor.json b/configs/default/cursor.json index e7ebe6ec..89c21650 100644 --- a/configs/default/cursor.json +++ b/configs/default/cursor.json @@ -14,6 +14,7 @@ "tick_interval_ms": 16, "hold_trigger_ms": 500, "auto_play": false, + "enable": 1, "mouse_acceleration": false, "use_transformation_matrix": false } \ No newline at end of file diff --git a/configs/profile_1/cursor.json b/configs/profile_1/cursor.json index 2e9a023b..52c368b6 100644 --- a/configs/profile_1/cursor.json +++ b/configs/profile_1/cursor.json @@ -5,15 +5,16 @@ "tracking_vert_idxs": [ 8 ], - "spd_up": 41, - "spd_down": 41, - "spd_left": 41, - "spd_right": 41, - "pointer_smooth": 15, + "spd_up": 40, + "spd_down": 40, + "spd_left": 40, + "spd_right": 40, + "pointer_smooth": 6, "shape_smooth": 10, "tick_interval_ms": 16, "hold_trigger_ms": 500, "auto_play": false, - "mouse_acceleration": false, + "enable": 1, + "mouse_acceleration": false, "use_transformation_matrix": false } \ No newline at end of file diff --git a/configs/profile_2/cursor.json b/configs/profile_2/cursor.json index 69bd284f..e53fadec 100644 --- a/configs/profile_2/cursor.json +++ b/configs/profile_2/cursor.json @@ -14,6 +14,7 @@ "tick_interval_ms": 16, "hold_trigger_ms": 500, "auto_play": false, + "enable": 1, "mouse_acceleration": false, "use_transformation_matrix": false } \ No newline at end of file diff --git a/src/camera_manager.py b/src/camera_manager.py index 0b7d72da..e9ea8534 100644 --- a/src/camera_manager.py +++ b/src/camera_manager.py @@ -11,7 +11,7 @@ import src.utils as utils from src.config_manager import ConfigManager -from src.controllers import MouseController +from src.controllers import Keybinder from src.singleton_meta import Singleton MAX_SEARCH_CAMS = 5 @@ -109,7 +109,7 @@ def draw_overlay(self, track_loc): self.frame_buffers["debug"] = self.frame_buffers["raw"].copy() # Disabled - if not MouseController().is_active.get(): + if not Keybinder().is_active.get(): self.frame_buffers["debug"] = add_overlay( self.frame_buffers["debug"], self.overlay_disabled, 0, 0, 640, 108) diff --git a/src/controllers/keybinder.py b/src/controllers/keybinder.py index 56dc9318..54a904a7 100644 --- a/src/controllers/keybinder.py +++ b/src/controllers/keybinder.py @@ -6,10 +6,10 @@ import pydirectinput import win32api +import tkinter as tk import src.shape_list as shape_list from src.config_manager import ConfigManager -from src.controllers.mouse_controller import MouseController from src.singleton_meta import Singleton logger = logging.getLogger("Keybinder") @@ -29,6 +29,7 @@ def __init__(self) -> None: self.holding = False self.is_started = False self.last_know_keybinds = {} + self.is_active = None def start(self): if not self.is_started: @@ -38,6 +39,9 @@ def start(self): self.monitors = self.get_monitors() self.is_started = True + self.is_active = tk.BooleanVar() + self.is_active.set(ConfigManager().config["auto_play"]) + def init_states(self) -> None: """Re initializes the state of the keybinder. If new keybinds are added. @@ -164,13 +168,13 @@ def act(self, blendshape_values) -> dict: if mon_id is None: return - MouseController().toggle_active() + self.toggle_active() self.key_states[state_name] = True elif (val < thres) and (self.key_states[state_name] is True): self.key_states[state_name] = False - elif MouseController().is_active.get(): + elif self.is_active.get(): if device == "mouse": @@ -210,6 +214,16 @@ def act(self, blendshape_values) -> dict: elif device == "keyboard": self.keyboard_action(val, action, thres, mode) + def set_active(self, flag: bool) -> None: + self.is_active.set(flag) + if flag: + self.delay_count = 0 + + def toggle_active(self): + logging.info("Toggle active") + curr_state = self.is_active.get() + self.set_active(not curr_state) + def destroy(self): """Destroy the keybinder""" return diff --git a/src/controllers/mouse_controller.py b/src/controllers/mouse_controller.py index ba1a93b5..22c608b4 100644 --- a/src/controllers/mouse_controller.py +++ b/src/controllers/mouse_controller.py @@ -35,7 +35,8 @@ def __init__(self): self.is_started = False self.is_destroyed = False self.stop_flag = None - self.is_active = None + self.is_active = tk.BooleanVar() + self.is_enabled = None def start(self): if not self.is_started: @@ -47,8 +48,8 @@ def start(self): self.screen_w, self.screen_h = pyautogui.size() self.calc_smooth_kernel() - self.is_active = tk.BooleanVar() - self.is_active.set(ConfigManager().config["auto_play"]) + self.is_enabled = tk.BooleanVar() + self.is_enabled.set(ConfigManager().config["enable"]) self.stop_flag = threading.Event() self.pool.submit(self.main_loop) @@ -89,7 +90,7 @@ def main_loop(self) -> None: return while not self.stop_flag.is_set(): - if not self.is_active.get(): + if not self.is_active.get() or not self.is_enabled.get(): time.sleep(0.001) continue @@ -123,6 +124,11 @@ def main_loop(self) -> None: time.sleep(ConfigManager().config["tick_interval_ms"] / 1000) + def set_enabled(self, flag: bool) -> None: + self.is_enabled.set(flag) + if flag: + self.delay_count = 0 + def set_active(self, flag: bool) -> None: self.is_active.set(flag) if flag: diff --git a/src/gui/frames/frame_cam_preview.py b/src/gui/frames/frame_cam_preview.py index d109ca7e..dba86134 100644 --- a/src/gui/frames/frame_cam_preview.py +++ b/src/gui/frames/frame_cam_preview.py @@ -6,7 +6,7 @@ from src.camera_manager import CameraManager from src.config_manager import ConfigManager -from src.controllers import MouseController +from src.controllers import Keybinder from src.gui.frames.safe_disposable_frame import SafeDisposableFrame CANVAS_WIDTH = 216 @@ -59,7 +59,7 @@ def __init__(self, master, master_callback: callable, **kwargs): border_color="transparent", switch_height=18, switch_width=32, - variable=MouseController().is_active, + variable=Keybinder().is_active, command=lambda: master_callback( "toggle_switch", {"switch_status": self.toggle_switch.get()}), onvalue=1, @@ -74,7 +74,7 @@ def __init__(self, master, master_callback: callable, **kwargs): pady=5, sticky="nw") - # Toggle label + # Toggle description label self.toggle_label = customtkinter.CTkLabel( master=self, compound='right', diff --git a/src/gui/main_gui.py b/src/gui/main_gui.py index b9ba5416..3acccb9f 100644 --- a/src/gui/main_gui.py +++ b/src/gui/main_gui.py @@ -8,7 +8,7 @@ import src.gui.frames as frames import src.gui.pages as pages from src.config_manager import ConfigManager -from src.controllers import MouseController +from src.controllers import Keybinder, MouseController customtkinter.set_appearance_mode("light") customtkinter.set_default_color_theme("assets/themes/google_theme.json") @@ -141,10 +141,13 @@ def cam_preview_callback(self, function_name, args: dict, **kwargs): if function_name == "toggle_switch": self.set_mediapipe_mouse_enable(new_state=args["switch_status"]) + def set_mediapipe_mouse_enable(self, new_state: bool): if new_state: + Keybinder().set_active(True) MouseController().set_active(True) else: + Keybinder().set_active(False) MouseController().set_active(False) def change_page(self, target_page_name: str): diff --git a/src/gui/pages/page_cursor.py b/src/gui/pages/page_cursor.py index a7f813b0..27a7c847 100644 --- a/src/gui/pages/page_cursor.py +++ b/src/gui/pages/page_cursor.py @@ -8,7 +8,7 @@ from PIL import Image from src.config_manager import ConfigManager -from src.controllers import MouseController +from src.controllers import MouseController, Keybinder from src.gui.balloon import Balloon from src.gui.frames.safe_disposable_frame import SafeDisposableFrame @@ -58,9 +58,44 @@ def __init__( 1, MAX_HOLD_TRIG ] }) - + # Toggle label + self.toggle_label = customtkinter.CTkLabel(master=self, + compound='right', + text="Cursor control", + justify=tkinter.RIGHT) + self.toggle_label.cget("font").configure(weight='bold') + self.toggle_label.grid(row=0, + column=0, + padx=(20, 0), + pady=5, + sticky="nw") + + # Toggle switch + self.toggle_switch = customtkinter.CTkSwitch( + master=self, + text="", + width=20, + border_color="transparent", + switch_height=18, + switch_width=32, + command=lambda: self.cursor_toggle_callback( + "toggle_switch", {"switch_status": self.toggle_switch.get()}), + variable=MouseController().is_enabled, + onvalue=1, + offvalue=0, + ) + if ConfigManager().config["enable"]: + self.toggle_switch.select() + + self.toggle_switch.grid(row=0, + column=0, + padx=(150, 0), + pady=5, + sticky="nw") self.load_initial_config() + + def load_initial_config(self): """Load default from config and set the UI """ @@ -92,7 +127,7 @@ def create_divs(self, directions: dict): text=show_name, justify=tkinter.LEFT) label.cget("font").configure(weight='bold') - label.grid(row=idx, column=0, padx=20, pady=(10, 10), sticky="nw") + label.grid(row=idx+2, column=0, padx=20, pady=(10, 10), sticky="nw") self.shared_info_balloon.register_widget(label, balloon_text) # Slider @@ -108,7 +143,7 @@ def create_divs(self, directions: dict): partial(self.slider_mouse_down_callback, cfg_name)) slider.bind("", partial(self.slider_mouse_up_callback, cfg_name)) - slider.grid(row=idx, column=0, padx=30, pady=(40, 10), sticky="nw") + slider.grid(row=idx+2, column=0, padx=30, pady=(40, 10), sticky="nw") # Number entry entry_var = tkinter.StringVar() @@ -121,7 +156,7 @@ def create_divs(self, directions: dict): textvariable=entry_var, #validatecommand=vcmd, width=62) - entry.grid(row=idx, + entry.grid(row=idx+2, column=0, padx=(300, 5), pady=(34, 10), @@ -205,6 +240,37 @@ def slider_mouse_up_callback(self, div_name: str, event): def inner_refresh_profile(self): self.load_initial_config() + def enable_cursor(self, new_state: bool): + new={} + if new_state: + for cfg_name, div in self.divs.items(): + slider = div["slider"] + slider.configure(state="normal", fg_color="#D2E3FC", progress_color="#1A73E8", button_color="#1A73E8") + div["slider"] = slider + new.update({cfg_name: div}) + else: + for cfg_name, div in self.divs.items(): + slider = div["slider"] + slider.configure(state="disabled", fg_color="lightgray", progress_color="gray", button_color="gray") + div["slider"] = slider + new.update({cfg_name: div}) + self.divs=new + ConfigManager().set_temp_config(field="enable", value=new_state) + ConfigManager().apply_config() + + def cursor_toggle_callback(self, command, args: dict): + logger.info(f"cursor_toggle_callback {command} with {args}") + + if command == "toggle_switch": + self.enable_cursor(new_state=args["switch_status"]) + self.set_mediapipe_mouse_enable(new_state=args["switch_status"]) + + def set_mediapipe_mouse_enable(self, new_state: bool): + if new_state: + MouseController().set_enabled(True) + else: + MouseController().set_enabled(False) + class PageCursor(SafeDisposableFrame): @@ -228,7 +294,7 @@ def __init__(self, master, **kwargs): columnspan=1) # Description. - des_txt = "Mouse cursor moves with your head movement. Use this settings to adjust how fast your mouse moves in each direction." + des_txt = "Adjust how the mouse cursor responds to your head movements." des_label = customtkinter.CTkLabel(master=self, text=des_txt, wraplength=300, @@ -238,7 +304,7 @@ def __init__(self, master, **kwargs): # Inner frame self.inner_frame = FrameSelectGesture(self) - self.inner_frame.grid(row=2, column=0, padx=5, pady=5, sticky="nw") + self.inner_frame.grid(row=4, column=0, padx=5, pady=5, sticky="nw") def refresh_profile(self): self.inner_frame.inner_refresh_profile() From 10685d5fcf657a8c1883e9f349ddfe6eba459a21 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Tue, 14 Nov 2023 17:56:10 +0100 Subject: [PATCH 041/123] Add cursor toggle (#2) * roughly implement cursor toggle * fix slider ui * disabling cursor also disables sliders * make cursor setting saveable * fix bug were inactive keybinder was still recognizing * fix profile_1 default From 366ad291978942c1689857599aa03d530a54c818 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Tue, 14 Nov 2023 18:31:15 +0100 Subject: [PATCH 042/123] 0.3.34 bump (#4) * add rule to build workflow * bump version to 0.3.34 --- .github/workflows/windows-build-release.yml | 2 ++ installer.iss | 2 +- src/config_manager.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index c33b2c37..ee97ab0b 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -3,6 +3,8 @@ on: push: branches: - main + tags: + - v[0-9]+.[0-9]+.[0-9]+ pull_request: jobs: diff --git a/installer.iss b/installer.iss index b4bda4f3..1a923a16 100644 --- a/installer.iss +++ b/installer.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Grimassist" -#define MyAppVersion "1" +#define MyAppVersion "0.3.34" #define MyAppExeName "grimassist.exe" [Setup] diff --git a/src/config_manager.py b/src/config_manager.py index 89f65536..8c1f4085 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -10,7 +10,7 @@ from src.singleton_meta import Singleton from src.task_killer import TaskKiller -VERSION = "0.3.33" +VERSION = "0.3.34" DEFAULT_JSON = Path(f"C:/Users/{os.getlogin()}/Grimassist/configs/default.json") BACKUP_PROFILE = Path(f"C:/Users/{os.getlogin()}/Grimassist/configs/default") From dd221970918d72ecc5a68be7224d3529d1b6d843 Mon Sep 17 00:00:00 2001 From: will wade Date: Wed, 15 Nov 2023 12:10:56 +0000 Subject: [PATCH 043/123] fix for paths --- src/config_manager.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/config_manager.py b/src/config_manager.py index cbfd5a84..d3dd83ae 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -1,35 +1,33 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import copy import json import logging import shutil import time import tkinter as tk +import os from pathlib import Path from src.singleton_meta import Singleton from src.task_killer import TaskKiller -VERSION = "0.3.33" +VERSION = "0.3.34" -DEFAULT_JSON = Path("configs/default.json") -BACKUP_PROFILE = Path("configs/default") +DEFAULT_JSON = Path(f"C:/Users/{os.getlogin()}/Grimassist/configs/default.json") +BACKUP_PROFILE = Path(f"C:/Users/{os.getlogin()}/Grimassist/configs/default") logger = logging.getLogger("ConfigManager") +config_dir = f"C:/Users/{os.getlogin()}/Grimassist/configs/" +default_dir = os.path.join(config_dir, "default") + +# Create the main config directory if it doesn't exist +if not os.path.isdir(config_dir): + os.makedirs(config_dir, exist_ok=True) + shutil.copytree("configs", config_dir, dirs_exist_ok=True) + +# Create the default directory inside the config directory if it doesn't exist +if not os.path.isdir(default_dir): + os.mkdir(default_dir) class ConfigManager(metaclass=Singleton): From 814ec8db2c403ea072f66ff7e10d9f72e53f6416 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Fri, 17 Nov 2023 23:02:21 +0100 Subject: [PATCH 044/123] skip loading of page_home --- src/gui/main_gui.py | 2 +- src/gui/pages/page_select_camera.py | 58 +++++++++++++++-------------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/gui/main_gui.py b/src/gui/main_gui.py index 3acccb9f..197b9b06 100644 --- a/src/gui/main_gui.py +++ b/src/gui/main_gui.py @@ -106,7 +106,7 @@ def __init__(self, tk_root): rowspan=2, columnspan=1) - self.change_page("page_home") + self.change_page("page_camera") # Profile UI self.frame_profile_switcher = frames.FrameProfileSwitcher( diff --git a/src/gui/pages/page_select_camera.py b/src/gui/pages/page_select_camera.py index c81b6d7c..92aaaf36 100644 --- a/src/gui/pages/page_select_camera.py +++ b/src/gui/pages/page_select_camera.py @@ -71,40 +71,42 @@ def __init__(self, master, **kwargs): def load_initial_config(self): """ Update radio buttons to match CameraManager """ - logger.info("Refresh radio buttons") - for old_radio in self.radios: - old_radio.destroy() - new_camera_list = CameraManager().get_camera_list() - logger.info(f"Get camera list {new_camera_list}") - radios = [] - for row_i, cam_id in enumerate(new_camera_list): - - radio = customtkinter.CTkRadioButton(master=self, - text=f"Camera {cam_id}", - command=self.radiobutton_event, - variable=self.radio_var, - value=cam_id) - - radio.grid(row=row_i + 2, column=0, padx=50, pady=10, sticky="w") - radios.append(radio) - - # Set radio select - target_id = ConfigManager().config["camera_id"] - self.radios = radios - for radio in self.radios: - if f"Camera {target_id}" == radio.cget("text"): - radio.select() - self.prev_radio_value = self.radio_var.get() - logger.info(f"Set initial camera to {target_id}") - break + if len(self.latest_camera_list) != len(new_camera_list): + self.latest_camera_list = new_camera_list + logger.info("Refresh radio buttons") + for old_radio in self.radios: + old_radio.destroy() + + logger.info(f"Get camera list {new_camera_list}") + radios = [] + for row_i, cam_id in enumerate(new_camera_list): + + radio = customtkinter.CTkRadioButton(master=self, + text=f"Camera {cam_id}", + command=self.radiobutton_event, + variable=self.radio_var, + value=cam_id) + + radio.grid(row=row_i + 2, column=0, padx=50, pady=10, sticky="w") + radios.append(radio) + + # Set radio select + target_id = ConfigManager().config["camera_id"] + self.radios = radios + for radio in self.radios: + if f"Camera {target_id}" == radio.cget("text"): + radio.select() + self.prev_radio_value = self.radio_var.get() + logger.info(f"Set initial camera to {target_id}") + break def radiobutton_event(self): # Open new camera. new_radio_value = self.radio_var.get() if new_radio_value == self.prev_radio_value: return - logger.info(f"Change cameara: {new_radio_value}") + logger.info(f"Change camera: {new_radio_value}") CameraManager().pick_camera(new_radio_value) ConfigManager().set_temp_config("camera_id", new_radio_value) ConfigManager().apply_config() @@ -123,7 +125,7 @@ def page_loop(self): CANVAS_HEIGHT))) self.canvas.itemconfig(self.canvas_im, image=self.new_photo) self.canvas.update() - + self.load_initial_config() self.after(ConfigManager().config["tick_interval_ms"], self.page_loop) From b33b706a7ac5c7513a808944fde57fdf1d2c30e4 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sat, 18 Nov 2023 16:08:31 +0100 Subject: [PATCH 045/123] remove page_home from menu --- src/gui/frames/frame_menu.py | 9 +-------- src/gui/main_gui.py | 30 +++++++----------------------- 2 files changed, 8 insertions(+), 31 deletions(-) diff --git a/src/gui/frames/frame_menu.py b/src/gui/frames/frame_menu.py index a3df5ede..354ed3bf 100644 --- a/src/gui/frames/frame_menu.py +++ b/src/gui/frames/frame_menu.py @@ -25,14 +25,6 @@ def __init__(self, master, master_callback: callable, **kwargs): self.master_callback = master_callback self.menu_btn_images = { - "page_home": [ - customtkinter.CTkImage( - Image.open("assets/images/menu_btn_home.png"), - size=BTN_SIZE), - customtkinter.CTkImage( - Image.open("assets/images/menu_btn_home_selected.png"), - size=BTN_SIZE) - ], "page_camera": [ customtkinter.CTkImage( Image.open("assets/images/menu_btn_camera.png"), @@ -94,6 +86,7 @@ def __init__(self, master, master_callback: callable, **kwargs): self.btns = {} self.btns = self.create_tab_btn(self.menu_btn_images, offset=1) + self.set_tab_active("page_camera") def create_tab_btn(self, btns: dict, offset): diff --git a/src/gui/main_gui.py b/src/gui/main_gui.py index 197b9b06..c81b42de 100644 --- a/src/gui/main_gui.py +++ b/src/gui/main_gui.py @@ -1,4 +1,3 @@ - import logging import tkinter as tk @@ -59,10 +58,6 @@ def __init__(self, tk_root): # Create all wizard pages and grid them. self.pages = { - "page_home": - pages.PageHome(master=self.tk_root, - logger_name="page_home", - root_callback=self.root_function_callback), "page_camera": pages.PageSelectCamera( master=self.tk_root, @@ -88,23 +83,13 @@ def __init__(self, tk_root): self.page_names = list(self.pages.keys()) self.curr_page_name = None for name, page in self.pages.items(): - # Page home extended full window - if name == "page_home": - page.grid(row=0, - column=0, - padx=5, - pady=5, - sticky="nsew", - rowspan=2, - columnspan=2) - else: - page.grid(row=0, - column=1, - padx=5, - pady=5, - sticky="nsew", - rowspan=2, - columnspan=1) + page.grid(row=0, + column=1, + padx=5, + pady=5, + sticky="nsew", + rowspan=2, + columnspan=1) self.change_page("page_camera") @@ -141,7 +126,6 @@ def cam_preview_callback(self, function_name, args: dict, **kwargs): if function_name == "toggle_switch": self.set_mediapipe_mouse_enable(new_state=args["switch_status"]) - def set_mediapipe_mouse_enable(self, new_state: bool): if new_state: Keybinder().set_active(True) From e6e2559f439cdbeedd2759f631610e46bcfc389b Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sun, 19 Nov 2023 17:41:08 +0100 Subject: [PATCH 046/123] Create pylint.yml --- .github/workflows/pylint.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/pylint.yml diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml new file mode 100644 index 00000000..c85efa03 --- /dev/null +++ b/.github/workflows/pylint.yml @@ -0,0 +1,24 @@ +name: Pylint + +on: + push: + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + - name: Analysing the code with pylint + run: | + pylint $(git ls-files '*.py') From 6938fb93ad23acaadee4df8ec815f40fd2936237 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sun, 19 Nov 2023 22:19:14 +0100 Subject: [PATCH 047/123] fix typos --- README.md | 4 ++-- grimassist.py | 2 +- src/camera_manager.py | 10 +++++----- src/config_manager.py | 24 ++++++++++++------------ src/controllers/__init__.py | 1 - src/controllers/keybinder.py | 12 ++++++------ src/controllers/mouse_controller.py | 4 ++-- src/detectors/facemesh.py | 2 +- src/gui/frames/frame_profile.py | 2 +- src/gui/frames/frame_profile_editor.py | 2 +- src/gui/frames/frame_profile_switcher.py | 2 +- src/gui/pages/page_cursor.py | 2 +- src/gui/pages/page_keyboard.py | 10 +++++----- src/gui/pages/page_select_gestures.py | 15 +++++++-------- src/pipeline.py | 2 +- src/task_killer.py | 4 ++-- src/utils/list_cameras.py | 2 +- 17 files changed, 49 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 33bf5650..971624e1 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Grimassist helps gamers control their mouse cursor using their head movement and 1. Download the Grimassist-Installer.exe from [Release section](../../releases/) 2. Install it -3. Run from your Windows shortucts/desktop +3. Run from your Windows shortcuts/desktop # Model used @@ -74,7 +74,7 @@ pip install -r requirements.txt | use_transformation_matrix | Control cursor using head direction (tracking_vert_idxs will be ignored) | -## Keybinds configs +## Keybinding configs >[mouse_bindings.json](configs/default/mouse_bindings.json) >[keyboard_bindings.json](configs/default/keyboard_bindings.json) diff --git a/grimassist.py b/grimassist.py index 9968734d..dddda007 100644 --- a/grimassist.py +++ b/grimassist.py @@ -46,7 +46,7 @@ def anim_loop(self): def close_all(self): logging.info("Close all") self.is_active = False - # Completely clost this process + # Completely close this process TaskKiller().exit() diff --git a/src/camera_manager.py b/src/camera_manager.py index e9ea8534..0ddf68a8 100644 --- a/src/camera_manager.py +++ b/src/camera_manager.py @@ -32,7 +32,7 @@ def add_overlay(background, overlay, x, y, width, height): class CameraManager(metaclass=Singleton): def __init__(self): - logger.info("Intialize CameraManager singleton") + logger.info("Initialize CameraManager singleton") self.thread_cameras = None # Load placeholder image @@ -149,7 +149,7 @@ def draw_overlay(self, track_loc): class ThreadCameras(): def __init__(self, frame_buffers: dict): - logger.info("Intializing Threadcamera") + logger.info("Initializing ThreadCamera") self.lock = threading.Lock() self.pool = futures.ThreadPoolExecutor(max_workers=8) self.stop_flag = threading.Event() @@ -223,7 +223,7 @@ def release_all_cameras(self): self.caps[cam_id] = None def read_camera_loop(self, stop_flag) -> None: - logger.info("Threadcamera main_loop started.") + logger.info("ThreadCamera main_loop started.") while not stop_flag.is_set(): if self.curr_id is None: @@ -270,7 +270,7 @@ def leave(self): pass def destroy(self): - logger.info("Destroying Threadcamera") + logger.info("Destroying ThreadCamera") self.stop_flag.set() self.assign_exe.join() self.loop_exe.join() @@ -280,4 +280,4 @@ def destroy(self): self.frame_buffers = None self.caps = None - logger.info("Threadcamera destroyed") + logger.info("ThreadCamera destroyed") diff --git a/src/config_manager.py b/src/config_manager.py index 9b9ef30a..3964a190 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -40,7 +40,7 @@ class ConfigManager(metaclass=Singleton): def __init__(self): - logger.info("Intialize ConfigManager singleton") + logger.info("Initialize ConfigManager singleton") self.version = VERSION self.unsave_configs = False self.unsave_mouse_bindings = False @@ -176,7 +176,7 @@ def set_temp_mouse_binding(self, gesture, device: str, action: str, "setting keybind for gesture: %s, device: %s, key: %s, threshold: %s, trigger_type: %s", gesture, device, action, threshold, trigger_type) - # Remove duplicate keybinds + # Remove duplicate keybindings self.remove_temp_mouse_binding(device, action) # Assign @@ -188,16 +188,16 @@ def set_temp_mouse_binding(self, gesture, device: str, action: str, def remove_temp_mouse_binding(self, device: str, action: str): logger.info( f"remove_temp_mouse_binding for device: {device}, key: {action}") - out_keybinds = {} + out_keybindings = {} for key, vals in self.temp_mouse_bindings.items(): if (device == vals[0]) and (action == vals[1]): continue - out_keybinds[key] = vals - self.temp_mouse_bindings = out_keybinds + out_keybindings[key] = vals + self.temp_mouse_bindings = out_keybindings self.unsave_mouse_bindings = True def apply_mouse_bindings(self): - logger.info("Applying keybinds") + logger.info("Applying keybindings") self.mouse_bindings = copy.deepcopy(self.temp_mouse_bindings) self.write_mouse_bindings_file() self.unsave_mouse_bindings = False @@ -205,7 +205,7 @@ def apply_mouse_bindings(self): def write_mouse_bindings_file(self): mouse_bindings_file = Path(self.curr_profile_path, "mouse_bindings.json") - logger.info(f"Writing keybinds file {mouse_bindings_file}") + logger.info(f"Writing keybindings file {mouse_bindings_file}") with open(mouse_bindings_file, 'w') as f: out_json = dict(sorted(self.mouse_bindings.items())) @@ -220,7 +220,7 @@ def set_temp_keyboard_binding(self, device: str, key_action: str, "setting keybind for gesture: %s, device: %s, key: %s, threshold: %s, trigger_type: %s", gesture, device, key_action, threshold, trigger_type) - # Remove duplicate keybinds + # Remove duplicate keybindings self.remove_temp_keyboard_binding(device, key_action, gesture) # Assign @@ -241,16 +241,16 @@ def remove_temp_keyboard_binding(self, f"remove_temp_keyboard_binding for device: {device}, key: {key_action} or gesture {gesture}" ) - out_keybinds = {} + out_keybindings = {} for ges, vals in self.temp_keyboard_bindings.items(): if (gesture == ges): continue if (key_action == vals[1]): continue - out_keybinds[ges] = vals + out_keybindings[ges] = vals - self.temp_keyboard_bindings = out_keybinds + self.temp_keyboard_bindings = out_keybindings self.unsave_keyboard_bindings = True return @@ -278,4 +278,4 @@ def apply_all(self): self.apply_keyboard_bindings() def destroy(self): - logger.info("Destory") + logger.info("Destroy") diff --git a/src/controllers/__init__.py b/src/controllers/__init__.py index 28537d01..efd7873e 100644 --- a/src/controllers/__init__.py +++ b/src/controllers/__init__.py @@ -1,3 +1,2 @@ - from .keybinder import * from .mouse_controller import * diff --git a/src/controllers/keybinder.py b/src/controllers/keybinder.py index 54a904a7..20113758 100644 --- a/src/controllers/keybinder.py +++ b/src/controllers/keybinder.py @@ -22,13 +22,13 @@ class Keybinder(metaclass=Singleton): def __init__(self) -> None: - logger.info("Intialize Keybinder singleton") + logger.info("Initialize Keybinder singleton") self.top_count = 0 self.triggered = False self.start_hold_ts = math.inf self.holding = False self.is_started = False - self.last_know_keybinds = {} + self.last_know_keybindings = {} self.is_active = None def start(self): @@ -43,8 +43,8 @@ def start(self): self.is_active.set(ConfigManager().config["auto_play"]) def init_states(self) -> None: - """Re initializes the state of the keybinder. - If new keybinds are added. + """Re-initializes the state of the keybinder. + If new keybindings are added. """ # keep states for all registered keys. self.key_states = {} @@ -52,7 +52,7 @@ def init_states(self) -> None: ConfigManager().keyboard_bindings).items(): self.key_states[v[0] + "_" + v[1]] = False self.key_states["holding"] = False - self.last_know_keybinds = copy.deepcopy( + self.last_know_keybindings = copy.deepcopy( (ConfigManager().mouse_bindings | ConfigManager().keyboard_bindings)) @@ -147,7 +147,7 @@ def act(self, blendshape_values) -> dict: return if (ConfigManager().mouse_bindings | - ConfigManager().keyboard_bindings) != self.last_know_keybinds: + ConfigManager().keyboard_bindings) != self.last_know_keybindings: self.init_states() for shape_name, v in (ConfigManager().mouse_bindings | diff --git a/src/controllers/mouse_controller.py b/src/controllers/mouse_controller.py index 22c608b4..6435562b 100644 --- a/src/controllers/mouse_controller.py +++ b/src/controllers/mouse_controller.py @@ -25,7 +25,7 @@ class MouseController(metaclass=Singleton): def __init__(self): - logger.info("Intialize MouseController singleton") + logger.info("Initialize MouseController singleton") self.prev_x = 0 self.prev_y = 0 self.curr_track_loc = None @@ -41,7 +41,7 @@ def __init__(self): def start(self): if not self.is_started: logger.info("Start MouseController singleton") - # Trackpoint buffer x, y + # Track-point buffer x, y self.buffer = np.zeros([N_BUFFER, 2]) self.accel = SigmoidAccel() self.pool = futures.ThreadPoolExecutor(max_workers=1) diff --git a/src/detectors/facemesh.py b/src/detectors/facemesh.py index 5f61f329..53ae358e 100644 --- a/src/detectors/facemesh.py +++ b/src/detectors/facemesh.py @@ -24,7 +24,7 @@ class FaceMesh(metaclass=Singleton): def __init__(self): - logger.info("Intialize FaceMesh singleton") + logger.info("Initialize FaceMesh singleton") self.mp_landmarks = None self.track_loc = None self.blendshapes_buffer = np.zeros([BLENDS_MAX_BUFFER, N_SHAPES]) diff --git a/src/gui/frames/frame_profile.py b/src/gui/frames/frame_profile.py index 7bc2131f..03eba50e 100644 --- a/src/gui/frames/frame_profile.py +++ b/src/gui/frames/frame_profile.py @@ -478,7 +478,7 @@ def __init__(self, master, refresh_master_fn: callable, **kwargs): columnspan=1, rowspan=1) - # Add butotn + # Add button add_button = customtkinter.CTkButton(master=self.float_window, text="+ Add profile", fg_color="white", diff --git a/src/gui/frames/frame_profile_editor.py b/src/gui/frames/frame_profile_editor.py index cbce285c..66789df7 100644 --- a/src/gui/frames/frame_profile_editor.py +++ b/src/gui/frames/frame_profile_editor.py @@ -389,7 +389,7 @@ def __init__(self, root_window, main_gui_callback: callable, **kwargs): columnspan=1, rowspan=1) - # Add butotn + # Add button add_prof_image = customtkinter.CTkImage( Image.open("assets/images/add_prof.png"), size=(16, 12)) add_button = customtkinter.CTkButton(master=self.float_window, diff --git a/src/gui/frames/frame_profile_switcher.py b/src/gui/frames/frame_profile_switcher.py index a8da1b8c..85e665fd 100644 --- a/src/gui/frames/frame_profile_switcher.py +++ b/src/gui/frames/frame_profile_switcher.py @@ -365,7 +365,7 @@ def __init__(self, root_window, main_gui_callback: callable, **kwargs): self.float_window.grid_columnconfigure(0, weight=1) self.float_window.configure(fg_color="white") self._displayed = True - # Rounded corder + # Rounded corner self.float_window.config(background='#000000') self.float_window.attributes("-transparentcolor", "#000000") diff --git a/src/gui/pages/page_cursor.py b/src/gui/pages/page_cursor.py index 27a7c847..baaf6097 100644 --- a/src/gui/pages/page_cursor.py +++ b/src/gui/pages/page_cursor.py @@ -190,7 +190,7 @@ def validate_entry_input(self, P, slider_min, slider_max): def entry_changed_callback(self, div_name, slider_min, slider_max, var, index, mode): - """Update value with entery text + """Update value with entry text """ is_valid_input = True div = self.divs[div_name] diff --git a/src/gui/pages/page_keyboard.py b/src/gui/pages/page_keyboard.py index 7526f103..37d0d632 100644 --- a/src/gui/pages/page_keyboard.py +++ b/src/gui/pages/page_keyboard.py @@ -82,9 +82,9 @@ def __init__( # Divs self.divs = {} - self.load_initial_keybinds() + self.load_initial_keybindings() - def load_initial_keybinds(self): + def load_initial_keybindings(self): """Load default from config and set the UI """ for gesture_name, bind_info in ConfigManager().keyboard_bindings.items( @@ -367,7 +367,7 @@ def wait_for_key(self, div_name: str, entry_button, keydown: tk.Event): def button_click_callback(self, div_name, entry_button, event): """Start wait_for_key after clicked the button """ - # Cancel old waiting funciontion + # Cancel old waiting function if self.waiting_div is not None: self.wait_for_key(self.waiting_div, self.waiting_button, "cancel") @@ -463,7 +463,7 @@ def inner_refresh_profile(self): self.divs = {} # Create new divs form the new profile - self.load_initial_keybinds() + self.load_initial_keybindings() def enter(self): super().enter() @@ -510,7 +510,7 @@ def __init__(self, master, **kwargs): self, logger_name="FrameSelectKeyboard") self.inner_frame.grid(row=3, column=0, padx=5, pady=5, sticky="nswe") - # Add binding butotn + # Add binding button self.add_binding_button = customtkinter.CTkButton( master=self, text="+ Add binding", diff --git a/src/gui/pages/page_select_gestures.py b/src/gui/pages/page_select_gestures.py index f7b5f125..ba4de7ee 100644 --- a/src/gui/pages/page_select_gestures.py +++ b/src/gui/pages/page_select_gestures.py @@ -1,4 +1,3 @@ - import tkinter as tk from functools import partial @@ -25,9 +24,9 @@ class FrameSelectGesture(SafeDisposableFrame): def __init__( - self, - master, - **kwargs, + self, + master, + **kwargs, ): super().__init__(master, **kwargs) self.is_active = False @@ -51,7 +50,7 @@ def __init__( # Divs self.divs = self.create_divs(shape_list.available_actions_keys, shape_list.available_gestures_keys) - self.load_initial_keybinds() + self.load_initial_keybindings() self.slider_dragging = False def set_div_inactive(self, div): @@ -75,7 +74,7 @@ def set_div_active(self, div, gesture_name, thres): div["slider"].grid() div["volume_bar"].grid() - def load_initial_keybinds(self): + def load_initial_keybindings(self): """Load default from config and set the UI """ @@ -296,11 +295,11 @@ def frame_loop(self): def inner_refresh_profile(self): # Create new divs form the new profile - self.load_initial_keybinds() + self.load_initial_keybindings() def enter(self): super().enter() - #self.load_initial_keybinds() + # self.load_initial_keybindings() self.after(1, self.frame_loop) def leave(self): diff --git a/src/pipeline.py b/src/pipeline.py index 582573e0..35237a4d 100644 --- a/src/pipeline.py +++ b/src/pipeline.py @@ -14,7 +14,7 @@ def pipeline_tick(self) -> None: frame_rgb = CameraManager().get_raw_frame() - # Detect landmarks (async) and save in it's buffer + # Detect landmarks (async) and save in its buffer FaceMesh().detect_frame(frame_rgb) # Get facial landmarks diff --git a/src/task_killer.py b/src/task_killer.py index 48b12e4b..b0b7120a 100644 --- a/src/task_killer.py +++ b/src/task_killer.py @@ -12,11 +12,11 @@ class TaskKiller(metaclass=Singleton): - """Singleton class for saftly killing the process and free the memory + """Singleton class for softly killing the process and freeing the memory """ def __init__(self): - logger.info("Intialize TaskKiller singleton") + logger.info("Initialize TaskKiller singleton") self.is_started = False def start(self): diff --git a/src/utils/list_cameras.py b/src/utils/list_cameras.py index f9ec06cc..d2940eee 100644 --- a/src/utils/list_cameras.py +++ b/src/utils/list_cameras.py @@ -8,7 +8,7 @@ def __open_camera_task(i): - logger.info(f"Try openning camera: {i}") + logger.info(f"Try opening camera: {i}") try: cap = cv2.VideoCapture(cv2.CAP_DSHOW + i) From b09ad3b7d63f7b8861703f307a6455247dc0a6a4 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sun, 19 Nov 2023 22:33:22 +0100 Subject: [PATCH 048/123] remove empty lines at beginning of files --- src/controllers/keybinder.py | 1 - src/detectors/__init__.py | 1 - src/detectors/facemesh.py | 1 - src/gui/__init__.py | 1 - src/gui/balloon.py | 1 - src/gui/dropdown.py | 1 - src/gui/frames/__init__.py | 1 - src/gui/frames/frame_cam_preview.py | 1 - src/gui/frames/frame_menu.py | 1 - src/gui/frames/frame_profile.py | 1 - src/gui/frames/frame_profile_editor.py | 1 - src/gui/frames/frame_profile_switcher.py | 1 - src/gui/frames/safe_disposable_frame.py | 1 - src/gui/pages/__init__.py | 1 - src/gui/pages/page_cursor.py | 1 - src/gui/pages/page_home.py | 1 - src/gui/pages/page_keyboard.py | 1 - src/gui/pages/page_select_camera.py | 1 - src/task_killer.py | 1 - src/utils/__init__.py | 1 - src/utils/install_font.py | 1 - src/utils/smoothing.py | 1 - 22 files changed, 22 deletions(-) diff --git a/src/controllers/keybinder.py b/src/controllers/keybinder.py index 20113758..75f13827 100644 --- a/src/controllers/keybinder.py +++ b/src/controllers/keybinder.py @@ -1,4 +1,3 @@ - import copy import logging import math diff --git a/src/detectors/__init__.py b/src/detectors/__init__.py index d573beb2..68abfa32 100644 --- a/src/detectors/__init__.py +++ b/src/detectors/__init__.py @@ -1,2 +1 @@ - from .facemesh import * diff --git a/src/detectors/facemesh.py b/src/detectors/facemesh.py index 53ae358e..9e9f7248 100644 --- a/src/detectors/facemesh.py +++ b/src/detectors/facemesh.py @@ -1,4 +1,3 @@ - import logging import time diff --git a/src/gui/__init__.py b/src/gui/__init__.py index 0fb4001e..a825cac8 100644 --- a/src/gui/__init__.py +++ b/src/gui/__init__.py @@ -1,2 +1 @@ - from .main_gui import * diff --git a/src/gui/balloon.py b/src/gui/balloon.py index 8fa70e3d..9607b1e8 100644 --- a/src/gui/balloon.py +++ b/src/gui/balloon.py @@ -1,4 +1,3 @@ - from functools import partial import customtkinter diff --git a/src/gui/dropdown.py b/src/gui/dropdown.py index bf17fa4f..da5defad 100644 --- a/src/gui/dropdown.py +++ b/src/gui/dropdown.py @@ -1,4 +1,3 @@ - from functools import partial import customtkinter diff --git a/src/gui/frames/__init__.py b/src/gui/frames/__init__.py index dfcf2080..c8ea3c27 100644 --- a/src/gui/frames/__init__.py +++ b/src/gui/frames/__init__.py @@ -1,4 +1,3 @@ - from .frame_cam_preview import * from .frame_menu import * from .frame_profile_editor import * diff --git a/src/gui/frames/frame_cam_preview.py b/src/gui/frames/frame_cam_preview.py index dba86134..40c7669d 100644 --- a/src/gui/frames/frame_cam_preview.py +++ b/src/gui/frames/frame_cam_preview.py @@ -1,4 +1,3 @@ - import tkinter import customtkinter diff --git a/src/gui/frames/frame_menu.py b/src/gui/frames/frame_menu.py index 354ed3bf..2088bc68 100644 --- a/src/gui/frames/frame_menu.py +++ b/src/gui/frames/frame_menu.py @@ -1,4 +1,3 @@ - from functools import partial import customtkinter diff --git a/src/gui/frames/frame_profile.py b/src/gui/frames/frame_profile.py index 03eba50e..5333e91f 100644 --- a/src/gui/frames/frame_profile.py +++ b/src/gui/frames/frame_profile.py @@ -1,4 +1,3 @@ - import time import logging import tkinter as tk diff --git a/src/gui/frames/frame_profile_editor.py b/src/gui/frames/frame_profile_editor.py index 66789df7..953f898f 100644 --- a/src/gui/frames/frame_profile_editor.py +++ b/src/gui/frames/frame_profile_editor.py @@ -1,4 +1,3 @@ - import logging import re import time diff --git a/src/gui/frames/frame_profile_switcher.py b/src/gui/frames/frame_profile_switcher.py index 85e665fd..4e55f4be 100644 --- a/src/gui/frames/frame_profile_switcher.py +++ b/src/gui/frames/frame_profile_switcher.py @@ -1,4 +1,3 @@ - import logging import re import time diff --git a/src/gui/frames/safe_disposable_frame.py b/src/gui/frames/safe_disposable_frame.py index cc464ad0..766e0a32 100644 --- a/src/gui/frames/safe_disposable_frame.py +++ b/src/gui/frames/safe_disposable_frame.py @@ -1,4 +1,3 @@ - import logging import customtkinter diff --git a/src/gui/pages/__init__.py b/src/gui/pages/__init__.py index 0a10c0e0..15e19875 100644 --- a/src/gui/pages/__init__.py +++ b/src/gui/pages/__init__.py @@ -1,4 +1,3 @@ - from .page_cursor import * from .page_home import * from .page_keyboard import * diff --git a/src/gui/pages/page_cursor.py b/src/gui/pages/page_cursor.py index baaf6097..3a5ce503 100644 --- a/src/gui/pages/page_cursor.py +++ b/src/gui/pages/page_cursor.py @@ -1,4 +1,3 @@ - import logging import tkinter from functools import partial diff --git a/src/gui/pages/page_home.py b/src/gui/pages/page_home.py index 20935347..080e0150 100644 --- a/src/gui/pages/page_home.py +++ b/src/gui/pages/page_home.py @@ -1,4 +1,3 @@ - import logging import tkinter from functools import partial diff --git a/src/gui/pages/page_keyboard.py b/src/gui/pages/page_keyboard.py index 37d0d632..5af01e5a 100644 --- a/src/gui/pages/page_keyboard.py +++ b/src/gui/pages/page_keyboard.py @@ -1,4 +1,3 @@ - import logging import tkinter as tk import uuid diff --git a/src/gui/pages/page_select_camera.py b/src/gui/pages/page_select_camera.py index 92aaaf36..7badf8bf 100644 --- a/src/gui/pages/page_select_camera.py +++ b/src/gui/pages/page_select_camera.py @@ -1,4 +1,3 @@ - import logging import tkinter diff --git a/src/task_killer.py b/src/task_killer.py index b0b7120a..2f16ad70 100644 --- a/src/task_killer.py +++ b/src/task_killer.py @@ -1,4 +1,3 @@ - import logging import os import signal diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 4e3640e3..38348062 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -1,4 +1,3 @@ - from .install_font import * from .list_cameras import * from .smoothing import * diff --git a/src/utils/install_font.py b/src/utils/install_font.py index 9fe998d0..36306c7a 100644 --- a/src/utils/install_font.py +++ b/src/utils/install_font.py @@ -1,4 +1,3 @@ - import ctypes import logging from pathlib import Path diff --git a/src/utils/smoothing.py b/src/utils/smoothing.py index 613522b4..50b0dfed 100644 --- a/src/utils/smoothing.py +++ b/src/utils/smoothing.py @@ -1,4 +1,3 @@ - import numpy as np import numpy.typing as npt From 5f4c6d2cf05ff12af31462ebf719be947a3866dc Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 20 Nov 2023 21:32:32 +0100 Subject: [PATCH 049/123] replace strings with static variables --- src/gui/main_gui.py | 26 +++++++++++++------------- src/gui/pages/page_cursor.py | 3 ++- src/gui/pages/page_home.py | 2 +- src/gui/pages/page_select_camera.py | 2 +- src/gui/pages/page_select_gestures.py | 1 + 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/gui/main_gui.py b/src/gui/main_gui.py index c81b42de..e628f2f9 100644 --- a/src/gui/main_gui.py +++ b/src/gui/main_gui.py @@ -8,6 +8,7 @@ import src.gui.pages as pages from src.config_manager import ConfigManager from src.controllers import Keybinder, MouseController +from src.gui.pages import PageSelectCamera, PageCursor, PageSelectGestures, PageKeyboard customtkinter.set_appearance_mode("light") customtkinter.set_default_color_theme("assets/themes/google_theme.json") @@ -58,29 +59,28 @@ def __init__(self, tk_root): # Create all wizard pages and grid them. self.pages = { - "page_camera": - pages.PageSelectCamera( + PageSelectCamera.name: + PageSelectCamera( master=self.tk_root, logger_name="page_camera", ), - "page_cursor": + PageCursor.name: pages.PageCursor( master=self.tk_root, logger_name="page_cursor", ), - "page_gestures": - pages.PageSelectGestures( + PageSelectGestures.name: + PageSelectGestures( master=self.tk_root, logger_name="page_gestures", ), - "page_keyboard": - pages.PageKeyboard( + PageKeyboard.name: + PageKeyboard( master=self.tk_root, logger_name="page_keyboard", ) } - self.page_names = list(self.pages.keys()) self.curr_page_name = None for name, page in self.pages.items(): page.grid(row=0, @@ -91,7 +91,7 @@ def __init__(self, tk_root): rowspan=2, columnspan=1) - self.change_page("page_camera") + self.change_page(PageSelectCamera.name) # Profile UI self.frame_profile_switcher = frames.FrameProfileSwitcher( @@ -115,10 +115,10 @@ def root_function_callback(self, function_name, args: dict = {}, **kwargs): elif function_name == "refresh_profiles": logger.info("refresh_profile") - self.pages["page_gestures"].refresh_profile() - self.pages["page_camera"].refresh_profile() - self.pages["page_cursor"].refresh_profile() - self.pages["page_keyboard"].refresh_profile() + self.pages[PageSelectGestures.name].refresh_profile() + self.pages[PageSelectCamera.name].refresh_profile() + self.pages[PageCursor.name].refresh_profile() + self.pages[PageKeyboard.name].refresh_profile() def cam_preview_callback(self, function_name, args: dict, **kwargs): logger.info(f"cam_preview_callback {function_name} with {args}") diff --git a/src/gui/pages/page_cursor.py b/src/gui/pages/page_cursor.py index 3a5ce503..7410a688 100644 --- a/src/gui/pages/page_cursor.py +++ b/src/gui/pages/page_cursor.py @@ -68,7 +68,7 @@ def __init__( padx=(20, 0), pady=5, sticky="nw") - + # Toggle switch self.toggle_switch = customtkinter.CTkSwitch( master=self, @@ -272,6 +272,7 @@ def set_mediapipe_mouse_enable(self, new_state: bool): class PageCursor(SafeDisposableFrame): + name = "cursor" def __init__(self, master, **kwargs): super().__init__(master, **kwargs) diff --git a/src/gui/pages/page_home.py b/src/gui/pages/page_home.py index 080e0150..e68a1471 100644 --- a/src/gui/pages/page_home.py +++ b/src/gui/pages/page_home.py @@ -12,7 +12,7 @@ class PageHome(SafeDisposableFrame): - + name = "home" def __init__(self, master, root_callback: callable, **kwargs): super().__init__(master, **kwargs) logging.info("Create PageHome") diff --git a/src/gui/pages/page_select_camera.py b/src/gui/pages/page_select_camera.py index 7badf8bf..5bcffa8c 100644 --- a/src/gui/pages/page_select_camera.py +++ b/src/gui/pages/page_select_camera.py @@ -17,7 +17,7 @@ class PageSelectCamera(SafeDisposableFrame): - + name = "camera" def __init__(self, master, **kwargs): super().__init__(master, **kwargs) diff --git a/src/gui/pages/page_select_gestures.py b/src/gui/pages/page_select_gestures.py index ba4de7ee..e1c0d22c 100644 --- a/src/gui/pages/page_select_gestures.py +++ b/src/gui/pages/page_select_gestures.py @@ -307,6 +307,7 @@ def leave(self): class PageSelectGestures(SafeDisposableFrame): + name = "gestures" def __init__(self, master, **kwargs): super().__init__(master, **kwargs) From 992a59f0dc1bc5f58d1199aca436087519b61a60 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Tue, 21 Nov 2023 21:09:24 +0100 Subject: [PATCH 050/123] replace last string for page_names --- src/gui/main_gui.py | 8 ++++---- src/gui/pages/page_cursor.py | 2 +- src/gui/pages/page_home.py | 3 ++- src/gui/pages/page_keyboard.py | 1 + src/gui/pages/page_select_camera.py | 3 ++- src/gui/pages/page_select_gestures.py | 2 +- 6 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/gui/main_gui.py b/src/gui/main_gui.py index e628f2f9..76bd3910 100644 --- a/src/gui/main_gui.py +++ b/src/gui/main_gui.py @@ -62,22 +62,22 @@ def __init__(self, tk_root): PageSelectCamera.name: PageSelectCamera( master=self.tk_root, - logger_name="page_camera", + logger_name=PageSelectCamera.name, ), PageCursor.name: pages.PageCursor( master=self.tk_root, - logger_name="page_cursor", + logger_name=PageCursor.name, ), PageSelectGestures.name: PageSelectGestures( master=self.tk_root, - logger_name="page_gestures", + logger_name=PageSelectGestures.name, ), PageKeyboard.name: PageKeyboard( master=self.tk_root, - logger_name="page_keyboard", + logger_name=PageKeyboard.name, ) } diff --git a/src/gui/pages/page_cursor.py b/src/gui/pages/page_cursor.py index 7410a688..c52cb149 100644 --- a/src/gui/pages/page_cursor.py +++ b/src/gui/pages/page_cursor.py @@ -272,7 +272,7 @@ def set_mediapipe_mouse_enable(self, new_state: bool): class PageCursor(SafeDisposableFrame): - name = "cursor" + name = "page_cursor" def __init__(self, master, **kwargs): super().__init__(master, **kwargs) diff --git a/src/gui/pages/page_home.py b/src/gui/pages/page_home.py index e68a1471..4b1ff544 100644 --- a/src/gui/pages/page_home.py +++ b/src/gui/pages/page_home.py @@ -12,7 +12,8 @@ class PageHome(SafeDisposableFrame): - name = "home" + name = "page_home" + def __init__(self, master, root_callback: callable, **kwargs): super().__init__(master, **kwargs) logging.info("Create PageHome") diff --git a/src/gui/pages/page_keyboard.py b/src/gui/pages/page_keyboard.py index 5af01e5a..d734f5ef 100644 --- a/src/gui/pages/page_keyboard.py +++ b/src/gui/pages/page_keyboard.py @@ -474,6 +474,7 @@ def leave(self): class PageKeyboard(SafeDisposableFrame): + name = "page_keyboard" def __init__(self, master, **kwargs): super().__init__(master, **kwargs) diff --git a/src/gui/pages/page_select_camera.py b/src/gui/pages/page_select_camera.py index 5bcffa8c..90491f02 100644 --- a/src/gui/pages/page_select_camera.py +++ b/src/gui/pages/page_select_camera.py @@ -17,7 +17,8 @@ class PageSelectCamera(SafeDisposableFrame): - name = "camera" + name = "page_camera" + def __init__(self, master, **kwargs): super().__init__(master, **kwargs) diff --git a/src/gui/pages/page_select_gestures.py b/src/gui/pages/page_select_gestures.py index e1c0d22c..17d13d8c 100644 --- a/src/gui/pages/page_select_gestures.py +++ b/src/gui/pages/page_select_gestures.py @@ -307,7 +307,7 @@ def leave(self): class PageSelectGestures(SafeDisposableFrame): - name = "gestures" + name = "page_gestures" def __init__(self, master, **kwargs): super().__init__(master, **kwargs) From aa1fafdb2e4e344829a7b87b090bc5ee52705d0d Mon Sep 17 00:00:00 2001 From: acidcoke Date: Tue, 21 Nov 2023 21:54:37 +0100 Subject: [PATCH 051/123] refactor pages from dicct to list --- src/gui/main_gui.py | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/gui/main_gui.py b/src/gui/main_gui.py index 76bd3910..7b0a71a5 100644 --- a/src/gui/main_gui.py +++ b/src/gui/main_gui.py @@ -58,31 +58,27 @@ def __init__(self, tk_root): self.frame_preview.enter() # Create all wizard pages and grid them. - self.pages = { - PageSelectCamera.name: + self.pages = [ PageSelectCamera( master=self.tk_root, logger_name=PageSelectCamera.name, ), - PageCursor.name: - pages.PageCursor( + PageCursor( master=self.tk_root, logger_name=PageCursor.name, ), - PageSelectGestures.name: PageSelectGestures( master=self.tk_root, logger_name=PageSelectGestures.name, ), - PageKeyboard.name: PageKeyboard( master=self.tk_root, logger_name=PageKeyboard.name, ) - } + ] self.curr_page_name = None - for name, page in self.pages.items(): + for page in self.pages: page.grid(row=0, column=1, padx=5, @@ -115,10 +111,8 @@ def root_function_callback(self, function_name, args: dict = {}, **kwargs): elif function_name == "refresh_profiles": logger.info("refresh_profile") - self.pages[PageSelectGestures.name].refresh_profile() - self.pages[PageSelectCamera.name].refresh_profile() - self.pages[PageCursor.name].refresh_profile() - self.pages[PageKeyboard.name].refresh_profile() + for page in self.pages: + page.refresh_profile() def cam_preview_callback(self, function_name, args: dict, **kwargs): logger.info(f"cam_preview_callback {function_name} with {args}") @@ -139,11 +133,11 @@ def change_page(self, target_page_name: str): if self.curr_page_name == target_page_name: return - for name, page in self.pages.items(): - if name == target_page_name: + for page in self.pages: + if page.name == target_page_name: page.grid() - self.pages[target_page_name].enter() - self.curr_page_name = target_page_name + page.enter() + self.curr_page_name = page.name else: page.grid_remove() @@ -156,7 +150,7 @@ def del_main_gui(self): self.frame_preview.destroy() self.frame_menu.leave() self.frame_menu.destroy() - for page in self.pages.values(): + for page in self.pages: page.leave() page.destroy() From 93653414052688f14b9f919b2065523980379992 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Wed, 22 Nov 2023 20:34:52 +0100 Subject: [PATCH 052/123] replace page_name Strings with classes through reflection --- src/gui/frames/frame_menu.py | 9 +++++---- src/gui/main_gui.py | 16 ++++------------ src/gui/pages/page_cursor.py | 1 - src/gui/pages/page_home.py | 1 - src/gui/pages/page_keyboard.py | 1 - src/gui/pages/page_select_camera.py | 1 - src/gui/pages/page_select_gestures.py | 1 - 7 files changed, 9 insertions(+), 21 deletions(-) diff --git a/src/gui/frames/frame_menu.py b/src/gui/frames/frame_menu.py index 2088bc68..c4c50975 100644 --- a/src/gui/frames/frame_menu.py +++ b/src/gui/frames/frame_menu.py @@ -4,6 +4,7 @@ from PIL import Image from src.config_manager import ConfigManager +from src.gui.pages import PageSelectCamera, PageCursor, PageSelectGestures, PageKeyboard from src.gui.frames.safe_disposable_frame import SafeDisposableFrame LIGHT_BLUE = "#F9FBFE" @@ -24,7 +25,7 @@ def __init__(self, master, master_callback: callable, **kwargs): self.master_callback = master_callback self.menu_btn_images = { - "page_camera": [ + PageSelectCamera.__name__: [ customtkinter.CTkImage( Image.open("assets/images/menu_btn_camera.png"), size=BTN_SIZE), @@ -32,7 +33,7 @@ def __init__(self, master, master_callback: callable, **kwargs): Image.open("assets/images/menu_btn_camera_selected.png"), size=BTN_SIZE) ], - "page_cursor": [ + PageCursor.__name__: [ customtkinter.CTkImage( Image.open("assets/images/menu_btn_cursor.png"), size=BTN_SIZE), @@ -40,7 +41,7 @@ def __init__(self, master, master_callback: callable, **kwargs): Image.open("assets/images/menu_btn_cursor_selected.png"), size=BTN_SIZE) ], - "page_gestures": [ + PageSelectGestures.__name__: [ customtkinter.CTkImage( Image.open("assets/images/menu_btn_gestures.png"), size=BTN_SIZE), @@ -48,7 +49,7 @@ def __init__(self, master, master_callback: callable, **kwargs): Image.open("assets/images/menu_btn_gestures_selected.png"), size=BTN_SIZE) ], - "page_keyboard": [ + PageKeyboard.__name__: [ customtkinter.CTkImage( Image.open("assets/images/menu_btn_keyboard.png"), size=BTN_SIZE), diff --git a/src/gui/main_gui.py b/src/gui/main_gui.py index 7b0a71a5..9e9a1a60 100644 --- a/src/gui/main_gui.py +++ b/src/gui/main_gui.py @@ -1,11 +1,7 @@ import logging -import tkinter as tk - import customtkinter -from PIL import Image -import src.gui.frames as frames -import src.gui.pages as pages +from src.gui import frames from src.config_manager import ConfigManager from src.controllers import Keybinder, MouseController from src.gui.pages import PageSelectCamera, PageCursor, PageSelectGestures, PageKeyboard @@ -61,19 +57,15 @@ def __init__(self, tk_root): self.pages = [ PageSelectCamera( master=self.tk_root, - logger_name=PageSelectCamera.name, ), PageCursor( master=self.tk_root, - logger_name=PageCursor.name, ), PageSelectGestures( master=self.tk_root, - logger_name=PageSelectGestures.name, ), PageKeyboard( master=self.tk_root, - logger_name=PageKeyboard.name, ) ] @@ -87,7 +79,7 @@ def __init__(self, tk_root): rowspan=2, columnspan=1) - self.change_page(PageSelectCamera.name) + self.change_page(PageSelectCamera.__name__) # Profile UI self.frame_profile_switcher = frames.FrameProfileSwitcher( @@ -134,10 +126,10 @@ def change_page(self, target_page_name: str): return for page in self.pages: - if page.name == target_page_name: + if page.__class__.__name__ == target_page_name: page.grid() page.enter() - self.curr_page_name = page.name + self.curr_page_name = page.__class__.__name__ else: page.grid_remove() diff --git a/src/gui/pages/page_cursor.py b/src/gui/pages/page_cursor.py index c52cb149..b5151c8b 100644 --- a/src/gui/pages/page_cursor.py +++ b/src/gui/pages/page_cursor.py @@ -272,7 +272,6 @@ def set_mediapipe_mouse_enable(self, new_state: bool): class PageCursor(SafeDisposableFrame): - name = "page_cursor" def __init__(self, master, **kwargs): super().__init__(master, **kwargs) diff --git a/src/gui/pages/page_home.py b/src/gui/pages/page_home.py index 4b1ff544..080e0150 100644 --- a/src/gui/pages/page_home.py +++ b/src/gui/pages/page_home.py @@ -12,7 +12,6 @@ class PageHome(SafeDisposableFrame): - name = "page_home" def __init__(self, master, root_callback: callable, **kwargs): super().__init__(master, **kwargs) diff --git a/src/gui/pages/page_keyboard.py b/src/gui/pages/page_keyboard.py index d734f5ef..5af01e5a 100644 --- a/src/gui/pages/page_keyboard.py +++ b/src/gui/pages/page_keyboard.py @@ -474,7 +474,6 @@ def leave(self): class PageKeyboard(SafeDisposableFrame): - name = "page_keyboard" def __init__(self, master, **kwargs): super().__init__(master, **kwargs) diff --git a/src/gui/pages/page_select_camera.py b/src/gui/pages/page_select_camera.py index 90491f02..7badf8bf 100644 --- a/src/gui/pages/page_select_camera.py +++ b/src/gui/pages/page_select_camera.py @@ -17,7 +17,6 @@ class PageSelectCamera(SafeDisposableFrame): - name = "page_camera" def __init__(self, master, **kwargs): super().__init__(master, **kwargs) diff --git a/src/gui/pages/page_select_gestures.py b/src/gui/pages/page_select_gestures.py index 17d13d8c..ba4de7ee 100644 --- a/src/gui/pages/page_select_gestures.py +++ b/src/gui/pages/page_select_gestures.py @@ -307,7 +307,6 @@ def leave(self): class PageSelectGestures(SafeDisposableFrame): - name = "page_gestures" def __init__(self, master, **kwargs): super().__init__(master, **kwargs) From cf22345b639f052300346ca5e38bd81f4a4cc251 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Wed, 22 Nov 2023 20:46:29 +0100 Subject: [PATCH 053/123] extract SafeDisposableFrame to its own file --- src/gui/frames/__init__.py | 1 + src/gui/frames/frame_profile.py | 3 +- src/gui/frames/frame_profile_editor.py | 2 +- src/gui/frames/safe_disposable_frame.py | 35 ---------------- .../safe_disposable_scrollable_frame.py | 40 +++++++++++++++++++ src/gui/pages/page_keyboard.py | 2 +- 6 files changed, 44 insertions(+), 39 deletions(-) create mode 100644 src/gui/frames/safe_disposable_scrollable_frame.py diff --git a/src/gui/frames/__init__.py b/src/gui/frames/__init__.py index c8ea3c27..d2e03766 100644 --- a/src/gui/frames/__init__.py +++ b/src/gui/frames/__init__.py @@ -3,3 +3,4 @@ from .frame_profile_editor import * from .frame_profile_switcher import * from .safe_disposable_frame import * +from .safe_disposable_scrollable_frame import * diff --git a/src/gui/frames/frame_profile.py b/src/gui/frames/frame_profile.py index 5333e91f..408a19cc 100644 --- a/src/gui/frames/frame_profile.py +++ b/src/gui/frames/frame_profile.py @@ -9,8 +9,7 @@ from src.config_manager import ConfigManager from src.task_killer import TaskKiller -from src.gui.frames.safe_disposable_frame import (SafeDisposableFrame, - SafeDisposableScrollableFrame) +from src.gui.frames import SafeDisposableFrame, SafeDisposableScrollableFrame logger = logging.getLogger("FrameProfile") diff --git a/src/gui/frames/frame_profile_editor.py b/src/gui/frames/frame_profile_editor.py index 953f898f..0acf6ed4 100644 --- a/src/gui/frames/frame_profile_editor.py +++ b/src/gui/frames/frame_profile_editor.py @@ -8,7 +8,7 @@ from PIL import Image from src.config_manager import ConfigManager -from src.gui.frames.safe_disposable_frame import SafeDisposableScrollableFrame +from src.gui.frames import SafeDisposableScrollableFrame from src.task_killer import TaskKiller logger = logging.getLogger("FrameProfileEditor") diff --git a/src/gui/frames/safe_disposable_frame.py b/src/gui/frames/safe_disposable_frame.py index 766e0a32..a401f63e 100644 --- a/src/gui/frames/safe_disposable_frame.py +++ b/src/gui/frames/safe_disposable_frame.py @@ -31,38 +31,3 @@ def destroy(self): self.placeholder_im = None -class SafeDisposableScrollableFrame(customtkinter.CTkScrollableFrame): - - def __init__(self, master, logger_name: str = "", **kwargs): - super().__init__(master, **kwargs) - self.is_active = False - self.is_destroyed = False - self.canvas_im = None - self.new_photo = None - self.placeholder_im = None - self.logger = logging.getLogger(logger_name) - - self.refresh_scrollbar() - - def refresh_scrollbar(self): - bar_start, bar_end = self._scrollbar.get() - if (bar_end - bar_start) < 1.0: - self._scrollbar.grid() - else: - self._scrollbar.grid_remove() - - def enter(self): - self.logger.info("enter") - self.is_active = True - - def leave(self): - self.logger.info("leave") - self.is_active = False - - def destroy(self): - self.logger.info("destroy") - self.is_active = False - self.is_destroyed = True - self.canvas_im = None - self.new_photo = None - self.placeholder_im = None diff --git a/src/gui/frames/safe_disposable_scrollable_frame.py b/src/gui/frames/safe_disposable_scrollable_frame.py new file mode 100644 index 00000000..167bcd19 --- /dev/null +++ b/src/gui/frames/safe_disposable_scrollable_frame.py @@ -0,0 +1,40 @@ +import logging + +import customtkinter + + +class SafeDisposableScrollableFrame(customtkinter.CTkScrollableFrame): + + def __init__(self, master, logger_name: str = "", **kwargs): + super().__init__(master, **kwargs) + self.is_active = False + self.is_destroyed = False + self.canvas_im = None + self.new_photo = None + self.placeholder_im = None + self.logger = logging.getLogger(logger_name) + + self.refresh_scrollbar() + + def refresh_scrollbar(self): + bar_start, bar_end = self._scrollbar.get() + if (bar_end - bar_start) < 1.0: + self._scrollbar.grid() + else: + self._scrollbar.grid_remove() + + def enter(self): + self.logger.info("enter") + self.is_active = True + + def leave(self): + self.logger.info("leave") + self.is_active = False + + def destroy(self): + self.logger.info("destroy") + self.is_active = False + self.is_destroyed = True + self.canvas_im = None + self.new_photo = None + self.placeholder_im = None diff --git a/src/gui/pages/page_keyboard.py b/src/gui/pages/page_keyboard.py index 5af01e5a..3277ec74 100644 --- a/src/gui/pages/page_keyboard.py +++ b/src/gui/pages/page_keyboard.py @@ -11,7 +11,7 @@ from src.detectors import FaceMesh from src.gui.balloon import Balloon from src.gui.dropdown import Dropdown -from src.gui.frames.safe_disposable_frame import SafeDisposableFrame, SafeDisposableScrollableFrame +from src.gui.frames import SafeDisposableFrame, SafeDisposableScrollableFrame logger = logging.getLogger("PageKeyboard") From 1e3f970ea5aa9f642bde4ad87ac87e304c6a454c Mon Sep 17 00:00:00 2001 From: acidcoke Date: Thu, 23 Nov 2023 19:30:25 +0100 Subject: [PATCH 054/123] fix frames imports --- grimassist.py | 4 ++-- src/gui/frames/frame_profile.py | 4 +++- src/gui/frames/frame_profile_editor.py | 2 +- src/gui/pages/page_keyboard.py | 4 +++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/grimassist.py b/grimassist.py index dddda007..c1bc8dd9 100644 --- a/grimassist.py +++ b/grimassist.py @@ -3,7 +3,7 @@ import os import customtkinter -import src.gui as gui +from src.gui import MainGui from src.pipeline import Pipeline from src.task_killer import TaskKiller @@ -23,7 +23,7 @@ ) -class MainApp(gui.MainGui, Pipeline): +class MainApp(MainGui, Pipeline): def __init__(self, tk_root): super().__init__(tk_root) # Wait for window drawing. diff --git a/src/gui/frames/frame_profile.py b/src/gui/frames/frame_profile.py index 408a19cc..2a87a91a 100644 --- a/src/gui/frames/frame_profile.py +++ b/src/gui/frames/frame_profile.py @@ -9,7 +9,9 @@ from src.config_manager import ConfigManager from src.task_killer import TaskKiller -from src.gui.frames import SafeDisposableFrame, SafeDisposableScrollableFrame +from src.gui.frames.safe_disposable_frame import SafeDisposableFrame +from src.gui.frames.safe_disposable_scrollable_frame import SafeDisposableScrollableFrame + logger = logging.getLogger("FrameProfile") diff --git a/src/gui/frames/frame_profile_editor.py b/src/gui/frames/frame_profile_editor.py index 0acf6ed4..7f4559ff 100644 --- a/src/gui/frames/frame_profile_editor.py +++ b/src/gui/frames/frame_profile_editor.py @@ -8,7 +8,7 @@ from PIL import Image from src.config_manager import ConfigManager -from src.gui.frames import SafeDisposableScrollableFrame +from src.gui.frames.safe_disposable_scrollable_frame import SafeDisposableScrollableFrame from src.task_killer import TaskKiller logger = logging.getLogger("FrameProfileEditor") diff --git a/src/gui/pages/page_keyboard.py b/src/gui/pages/page_keyboard.py index 3277ec74..a3e1bf19 100644 --- a/src/gui/pages/page_keyboard.py +++ b/src/gui/pages/page_keyboard.py @@ -11,7 +11,9 @@ from src.detectors import FaceMesh from src.gui.balloon import Balloon from src.gui.dropdown import Dropdown -from src.gui.frames import SafeDisposableFrame, SafeDisposableScrollableFrame +from src.gui.frames.safe_disposable_frame import SafeDisposableFrame +from src.gui.frames.safe_disposable_scrollable_frame import SafeDisposableScrollableFrame + logger = logging.getLogger("PageKeyboard") From d50601301521a19be01923f97d4d54a44253594f Mon Sep 17 00:00:00 2001 From: acidcoke Date: Thu, 23 Nov 2023 19:36:14 +0100 Subject: [PATCH 055/123] add missing requirements --- requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 17d3215d..ee638a2a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,6 @@ customtkinter==5.2.0 mediapipe==0.9.3.0 PyDirectInput==1.0.4; sys_platform == 'win32' pywin32==306; sys_platform == 'win32' -pyinstaller==5.11.0 +pyinstaller==5.11.0 +Pillow==10.1.0 +numpy==1.26.2 \ No newline at end of file From 01b65626972d0f75fa013f1c600e03dd2ec7ec26 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Fri, 24 Nov 2023 23:24:26 +0100 Subject: [PATCH 056/123] display camera name on windows devices --- requirements.txt | 1 + src/gui/pages/page_select_camera.py | 13 ++++++++++--- src/utils/list_cameras.py | 11 +++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index ee638a2a..f5ce73cb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ customtkinter==5.2.0 mediapipe==0.9.3.0 PyDirectInput==1.0.4; sys_platform == 'win32' pywin32==306; sys_platform == 'win32' +pygrabber; sys_platform == 'win32' pyinstaller==5.11.0 Pillow==10.1.0 numpy==1.26.2 \ No newline at end of file diff --git a/src/gui/pages/page_select_camera.py b/src/gui/pages/page_select_camera.py index 7badf8bf..fd4df208 100644 --- a/src/gui/pages/page_select_camera.py +++ b/src/gui/pages/page_select_camera.py @@ -1,6 +1,7 @@ import logging import tkinter +from src import utils import customtkinter from PIL import Image, ImageTk @@ -74,15 +75,19 @@ def load_initial_config(self): if len(self.latest_camera_list) != len(new_camera_list): self.latest_camera_list = new_camera_list logger.info("Refresh radio buttons") - for old_radio in self.radios: - old_radio.destroy() + old_radios = self.radios logger.info(f"Get camera list {new_camera_list}") radios = [] for row_i, cam_id in enumerate(new_camera_list): + radio_text = f"Camera {cam_id}" + + cam_name = utils.get_camera_name(cam_id) + if cam_name is not None: + radio_text = f"{radio_text}: {cam_name}" radio = customtkinter.CTkRadioButton(master=self, - text=f"Camera {cam_id}", + text=radio_text, command=self.radiobutton_event, variable=self.radio_var, value=cam_id) @@ -99,6 +104,8 @@ def load_initial_config(self): self.prev_radio_value = self.radio_var.get() logger.info(f"Set initial camera to {target_id}") break + for old_radio in old_radios: + old_radio.destroy() def radiobutton_event(self): # Open new camera. diff --git a/src/utils/list_cameras.py b/src/utils/list_cameras.py index d2940eee..0c819145 100644 --- a/src/utils/list_cameras.py +++ b/src/utils/list_cameras.py @@ -1,8 +1,12 @@ import concurrent.futures as futures import logging +import platform import cv2 +if platform.system() == "Windows": + import pygrabber.dshow_graph + logger = logging.getLogger("ListCamera") @@ -68,3 +72,10 @@ def open_camera(caps, i): """ pool = futures.ThreadPoolExecutor(max_workers=1) pool.submit(assign_caps_unblock, caps, i) + + +def get_camera_name(i: int) -> str | None: + if platform.system() == "Windows": + return str(pygrabber.dshow_graph.FilterGraph().get_input_devices()[i]) + else: + return None From 9146851d68a8f438550566b195fb1b5fcdc4d8e7 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Fri, 24 Nov 2023 23:28:42 +0100 Subject: [PATCH 057/123] rename function for updating radio buttons --- src/gui/pages/page_select_camera.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/gui/pages/page_select_camera.py b/src/gui/pages/page_select_camera.py index fd4df208..342c2330 100644 --- a/src/gui/pages/page_select_camera.py +++ b/src/gui/pages/page_select_camera.py @@ -68,7 +68,7 @@ def __init__(self, master, **kwargs): self.new_photo = None self.latest_camera_list = [] - def load_initial_config(self): + def update_radio_buttons(self): """ Update radio buttons to match CameraManager """ new_camera_list = CameraManager().get_camera_list() @@ -131,16 +131,16 @@ def page_loop(self): CANVAS_HEIGHT))) self.canvas.itemconfig(self.canvas_im, image=self.new_photo) self.canvas.update() - self.load_initial_config() + self.update_radio_buttons() self.after(ConfigManager().config["tick_interval_ms"], self.page_loop) def enter(self): super().enter() - self.load_initial_config() + self.update_radio_buttons() self.after(1, self.page_loop) def refresh_profile(self): - self.load_initial_config() + self.update_radio_buttons() new_camera_id = ConfigManager().config["camera_id"] CameraManager().pick_camera(new_camera_id) From 522a391105a48b1f0f3acf73b8da6d930df08aa1 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Fri, 24 Nov 2023 23:35:18 +0100 Subject: [PATCH 058/123] update requirements --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f5ce73cb..4a97dcda 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ flatbuffers==2.0.0 matplotlib==3.7.1 -opencv-contrib-python==4.7.0.72 +opencv-contrib-python==4.8.1.78 psutil==5.9.4 pyautogui==0.9.53 customtkinter==5.2.0 From 1530f5d48680ada61206acaa24597b22131b7cf0 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sat, 25 Nov 2023 23:59:24 +0100 Subject: [PATCH 059/123] refactoring for legibility --- src/accel_graph.py | 1 + src/camera_manager.py | 82 ++++++++++++------------ src/config_manager.py | 29 +++++---- src/controllers/keybinder.py | 37 ++++++----- src/controllers/mouse_controller.py | 19 ++++-- src/detectors/facemesh.py | 24 ++++--- src/gui/frames/frame_menu.py | 10 +-- src/gui/frames/frame_profile.py | 6 +- src/gui/frames/frame_profile_editor.py | 6 +- src/gui/frames/frame_profile_switcher.py | 4 +- src/gui/main_gui.py | 6 +- src/gui/pages/page_select_camera.py | 26 ++++---- src/pipeline.py | 8 +-- src/utils/list_cameras.py | 38 +++++------ 14 files changed, 159 insertions(+), 137 deletions(-) diff --git a/src/accel_graph.py b/src/accel_graph.py index 8709ec94..718dd20d 100644 --- a/src/accel_graph.py +++ b/src/accel_graph.py @@ -15,6 +15,7 @@ def __call__(self, x: float) -> float: class SigmoidAccel(AccelGraph): def __init__(self, shift_x=5, slope=0.3, multiply=1.2): + super().__init__() self.shift_x = shift_x self.slope = slope self.multiply = multiply diff --git a/src/camera_manager.py b/src/camera_manager.py index 0ddf68a8..9a4b51c8 100644 --- a/src/camera_manager.py +++ b/src/camera_manager.py @@ -67,17 +67,17 @@ def start(self): def get_camera_list(self) -> list[int]: if not self.is_active: return [] - ret_caps = list(self.thread_cameras.caps.keys()) + cameras = list(self.thread_cameras.cameras.keys()) - return sorted(ret_caps) + return sorted(cameras) - def get_current_camera_id(self) -> int: + def get_current_camera_id(self) -> int | None: if not self.is_active: return None else: - return self.thread_cameras.curr_id + return self.thread_cameras.current_id - def pick_camera(self, camera_id: int) -> None: + def pick_camera(self, camera_id: int): logger.info(f"Swapping to camera id: {camera_id}") # Assign to ref self.frame_buffers["raw"] = self.placeholder_im @@ -90,7 +90,7 @@ def get_raw_frame(self): def get_debug_frame(self): return self.frame_buffers["debug"] - def put_debug_frame(self, frame_debug: npt.ArrayLike) -> None: + def put_debug_frame(self, frame_debug: npt.ArrayLike): self.frame_buffers["debug"] = frame_debug def leave(self): @@ -102,7 +102,7 @@ def destroy(self): if self.thread_cameras is not None: self.thread_cameras.destroy() - def draw_overlay(self, track_loc): + def draw_overlay(self, tracking_location): if not self.is_active: return @@ -116,7 +116,7 @@ def draw_overlay(self, track_loc): return # Face not detected - if (track_loc is None): + if (tracking_location is None): self.frame_buffers["debug"] = add_overlay( self.frame_buffers["debug"], self.overlay_face_not_detected, 0, 0, 640, 108) @@ -129,15 +129,15 @@ def draw_overlay(self, track_loc): cx = ConfigManager().config["fix_width"] // 2 cy = ConfigManager().config["fix_height"] // 2 cv2.line(self.frame_buffers["debug"], (cx, cy), - (int(track_loc[0]), int(track_loc[1])), (0, 255, 0), 3) + (int(tracking_location[0]), int(tracking_location[1])), (0, 255, 0), 3) cv2.circle(self.frame_buffers["debug"], - (int(track_loc[0]), int(track_loc[1])), 6, (255, 0, 0), + (int(tracking_location[0]), int(tracking_location[1])), 6, (255, 0, 0), -1) else: cv2.circle(self.frame_buffers["debug"], - (int(track_loc[0]), int(track_loc[1])), 4, + (int(tracking_location[0]), int(tracking_location[1])), 4, (255, 255, 255), -1) @@ -157,16 +157,16 @@ def __init__(self, frame_buffers: dict): self.frame_buffers = frame_buffers # Open all cameras - self.caps = {} + self.cameras = {} - self.assign_exe = Thread(target=utils.assign_caps_queue, - args=(self.caps, self.assign_done, + self.assign_exe = Thread(target=utils.assign_cameras_queue, + args=(self.cameras, self.assign_done, MAX_SEARCH_CAMS), daemon=True) self.assign_exe.start() # Decide what camera to use - self.curr_id = None #ConfigManager().config["camera_id"] - logger.info(f"Found default camera_id {self.curr_id}") + self.current_id = None #ConfigManager().config["camera_id"] + logger.info(f"Found default camera_id {self.current_id}") self.loop_exe = Thread(target=self.read_camera_loop, args=(self.stop_flag,), @@ -174,20 +174,20 @@ def __init__(self, frame_buffers: dict): self.loop_exe.start() def assign_done(self): - """Set default camera after assign_caps is done + """Set default camera after assign_cameras is done """ - logger.info(f"Assign cameras completed. Found {self.caps}") + logger.info(f"Assign cameras completed. Found {self.cameras}") init_id = ConfigManager().config["camera_id"] # pick first camera available if camera in config not found - if init_id not in self.caps: - self.curr_id = list(self.caps.keys())[0] + if init_id not in self.cameras: + self.current_id = list(self.cameras.keys())[0] else: - self.curr_id = init_id + self.current_id = init_id - logger.info(f"Try to use camera {self.curr_id}") - self.pick_camera(self.curr_id) + logger.info(f"Try to use camera {self.current_id}") + self.pick_camera(self.current_id) self.assign_done_flag.set() def pick_camera(self, new_id: int) -> None: @@ -199,34 +199,34 @@ def pick_camera(self, new_id: int) -> None: logger.info(f"Pick camera {new_id}, Releasing others...") - if new_id not in self.caps: + if new_id not in self.cameras: logger.error(f"Camera {new_id} not found") return - for cam_id, _ in self.caps.items(): + for cam_id, _ in self.cameras.items(): if cam_id == new_id: - if self.caps[new_id] is not None: + if self.cameras[new_id] is not None: continue - utils.open_camera(self.caps, cam_id) + utils.open_camera(self.cameras, cam_id) else: - if self.caps[cam_id] is not None: - self.caps[cam_id].release() - self.caps[cam_id] = None + if self.cameras[cam_id] is not None: + self.cameras[cam_id].release() + self.cameras[cam_id] = None - self.curr_id = new_id + self.current_id = new_id def release_all_cameras(self): - if self.caps is not None: - for cam_id, _ in self.caps.items(): - if self.caps[cam_id] is not None: - self.caps[cam_id].release() - self.caps[cam_id] = None + if self.cameras is not None: + for cam_id, _ in self.cameras.items(): + if self.cameras[cam_id] is not None: + self.cameras[cam_id].release() + self.cameras[cam_id] = None def read_camera_loop(self, stop_flag) -> None: logger.info("ThreadCamera main_loop started.") while not stop_flag.is_set(): - if self.curr_id is None: + if self.current_id is None: time.sleep(1) continue @@ -234,9 +234,9 @@ def read_camera_loop(self, stop_flag) -> None: time.sleep(1) continue - if (self.curr_id in self.caps) and (self.caps[self.curr_id] - is not None): - ret, frame = self.caps[self.curr_id].read() + if (self.current_id in self.cameras) and (self.cameras[self.current_id] + is not None): + ret, frame = self.cameras[self.current_id].read() cv2.waitKey(1) if not ret: logger.error("No frame returned") @@ -278,6 +278,6 @@ def destroy(self): # Release all cameras self.release_all_cameras() self.frame_buffers = None - self.caps = None + self.cameras = None logger.info("ThreadCamera destroyed") diff --git a/src/config_manager.py b/src/config_manager.py index 3964a190..be9c5357 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -40,6 +40,11 @@ class ConfigManager(metaclass=Singleton): def __init__(self): + self.temp_keyboard_bindings = None + self.temp_mouse_bindings = None + self.temp_config = None + self.keyboard_bindings = None + self.mouse_bindings = None logger.info("Initialize ConfigManager singleton") self.version = VERSION self.unsave_configs = False @@ -48,8 +53,8 @@ def __init__(self): self.config = None # Load config - self.curr_profile_path = None - self.curr_profile_name = tk.StringVar() + self.current_profile_path = None + self.current_profile_name = tk.StringVar() self.is_started = False self.profiles = self.list_profile() @@ -102,12 +107,12 @@ def rename_profile(self, old_profile_name, new_profile_name): self.profiles.remove(old_profile_name) self.profiles.append(new_profile_name) - if self.curr_profile_name.get() == old_profile_name: - self.curr_profile_name.set(new_profile_name) + if self.current_profile_name.get() == old_profile_name: + self.current_profile_name.set(new_profile_name) - def load_profile(self, profile_name: str) -> list[bool, Path]: + def load_profile(self, profile_name: str): profile_path = Path(DEFAULT_JSON.parent, profile_name) logger.info(f"Loading profile: {profile_path}") @@ -139,8 +144,8 @@ def load_profile(self, profile_name: str) -> list[bool, Path]: self.temp_mouse_bindings = copy.deepcopy(self.mouse_bindings) self.temp_keyboard_bindings = copy.deepcopy(self.keyboard_bindings) - self.curr_profile_path = profile_path - self.curr_profile_name.set(profile_name) + self.current_profile_path = profile_path + self.current_profile_name.set(profile_name) def switch_profile(self, profile_name: str): logger.info(f"Switching to profile: {profile_name}") @@ -156,7 +161,7 @@ def set_temp_config(self, field: str, value): self.unsave_configs = True def write_config_file(self): - cursor_config_file = Path(self.curr_profile_path, "cursor.json") + cursor_config_file = Path(self.current_profile_path, "cursor.json") logger.info(f"Writing config file {cursor_config_file}") with open(cursor_config_file, 'w') as f: json.dump(self.config, f, indent=4, separators=(', ', ': ')) @@ -203,7 +208,7 @@ def apply_mouse_bindings(self): self.unsave_mouse_bindings = False def write_mouse_bindings_file(self): - mouse_bindings_file = Path(self.curr_profile_path, + mouse_bindings_file = Path(self.current_profile_path, "mouse_bindings.json") logger.info(f"Writing keybindings file {mouse_bindings_file}") @@ -243,9 +248,9 @@ def remove_temp_keyboard_binding(self, out_keybindings = {} for ges, vals in self.temp_keyboard_bindings.items(): - if (gesture == ges): + if gesture == ges: continue - if (key_action == vals[1]): + if key_action == vals[1]: continue out_keybindings[ges] = vals @@ -263,7 +268,7 @@ def apply_keyboard_bindings(self): self.unsave_keyboard_bindings = False def write_keyboard_bindings_file(self): - keyboard_bindings_file = Path(self.curr_profile_path, + keyboard_bindings_file = Path(self.current_profile_path, "keyboard_bindings.json") logger.info(f"Writing keyboard bindings file {keyboard_bindings_file}") diff --git a/src/controllers/keybinder.py b/src/controllers/keybinder.py index 75f13827..8f801d32 100644 --- a/src/controllers/keybinder.py +++ b/src/controllers/keybinder.py @@ -21,6 +21,11 @@ class Keybinder(metaclass=Singleton): def __init__(self) -> None: + self.delay_count = None + self.key_states = None + self.monitors = None + self.screen_h = None + self.screen_w = None logger.info("Initialize Keybinder singleton") self.top_count = 0 self.triggered = False @@ -71,33 +76,33 @@ def get_monitors(self) -> list[dict]: return out_list - def get_curr_monitor(self): + def get_current_monitor(self) -> int: x, y = pydirectinput.position() for mon_id, mon in enumerate(self.monitors): if x >= mon["x1"] and x <= mon["x2"] and y >= mon[ "y1"] and y <= mon["y2"]: return mon_id - #raise Exception("Monitor not found") + # raise Exception("Monitor not found") return 0 - def mouse_action(self, val, action, thres, mode) -> None: + def mouse_action(self, val, action, threshold, mode) -> None: state_name = "mouse_" + action mode = "hold" if self.key_states["holding"] else "single" if mode == "hold": - if (val > thres) and (self.key_states[state_name] is False): + if (val > threshold) and (self.key_states[state_name] is False): pydirectinput.mouseDown(action) self.key_states[state_name] = True - elif (val < thres) and (self.key_states[state_name] is True): + elif (val < threshold) and (self.key_states[state_name] is True): pydirectinput.mouseUp(action) self.key_states[state_name] = False elif mode == "single": - if (val > thres): + if val > threshold: if not self.key_states[state_name]: pydirectinput.click(button=action) self.start_hold_ts = time.time() @@ -111,7 +116,7 @@ def mouse_action(self, val, action, thres, mode) -> None: pydirectinput.mouseDown(button=action) self.holding = True - elif (val < thres) and (self.key_states[state_name] is True): + elif (val < threshold) and (self.key_states[state_name] is True): self.key_states[state_name] = False @@ -120,19 +125,19 @@ def mouse_action(self, val, action, thres, mode) -> None: self.holding = False self.start_hold_ts = math.inf - def keyboard_action(self, val, keysym, thres, mode): + def keyboard_action(self, val, keysym, threshold, mode): state_name = "keyboard_" + keysym - if (self.key_states[state_name] is False) and (val > thres): + if (self.key_states[state_name] is False) and (val > threshold): pydirectinput.keyDown(keysym) self.key_states[state_name] = True - elif (self.key_states[state_name] is True) and (val < thres): + elif (self.key_states[state_name] is True) and (val < threshold): pydirectinput.keyUp(keysym) self.key_states[state_name] = False - def act(self, blendshape_values) -> dict: + def act(self, blendshape_values) -> None: """Trigger devices action base on blendshape values Args: @@ -163,7 +168,7 @@ def act(self, blendshape_values) -> dict: state_name = "mouse_" + action if (val > thres) and (self.key_states[state_name] is False): - mon_id = self.get_curr_monitor() + mon_id = self.get_current_monitor() if mon_id is None: return @@ -181,7 +186,7 @@ def act(self, blendshape_values) -> dict: state_name = "mouse_" + action if (val > thres) and (self.key_states[state_name] is False): - mon_id = self.get_curr_monitor() + mon_id = self.get_current_monitor() if mon_id is None: return @@ -197,7 +202,7 @@ def act(self, blendshape_values) -> dict: state_name = "mouse_" + action if (val > thres) and (self.key_states[state_name] is False): - mon_id = self.get_curr_monitor() + mon_id = self.get_current_monitor() next_mon_id = (mon_id + 1) % len(self.monitors) pydirectinput.moveTo( self.monitors[next_mon_id]["center_x"], @@ -220,8 +225,8 @@ def set_active(self, flag: bool) -> None: def toggle_active(self): logging.info("Toggle active") - curr_state = self.is_active.get() - self.set_active(not curr_state) + current_state = self.is_active.get() + self.set_active(not current_state) def destroy(self): """Destroy the keybinder""" diff --git a/src/controllers/mouse_controller.py b/src/controllers/mouse_controller.py index 6435562b..64330225 100644 --- a/src/controllers/mouse_controller.py +++ b/src/controllers/mouse_controller.py @@ -25,10 +25,15 @@ class MouseController(metaclass=Singleton): def __init__(self): + self.screen_h = None + self.screen_w = None + self.pool = None + self.accel = None + self.buffer = None logger.info("Initialize MouseController singleton") self.prev_x = 0 self.prev_y = 0 - self.curr_track_loc = None + self.current_tracking_location = None self.smooth_kernel = None self.delay_count = 0 self.top_count = 0 @@ -66,7 +71,7 @@ def calc_smooth_kernel(self): else: pass - def asymmetry_scale(self, vel_x, vel_y): + def asymmetry_scale(self, vel_x, vel_y) -> tuple[int, int]: if vel_x > 0: vel_x *= ConfigManager().config["spd_right"] else: @@ -79,8 +84,8 @@ def asymmetry_scale(self, vel_x, vel_y): return vel_x, vel_y - def act(self, track_loc: npt.ArrayLike): - self.curr_track_loc = track_loc + def act(self, tracking_location: npt.ArrayLike): + self.current_tracking_location = tracking_location def main_loop(self) -> None: """ Separate thread for mouse controller @@ -95,7 +100,7 @@ def main_loop(self) -> None: continue self.buffer = np.roll(self.buffer, shift=-1, axis=0) - self.buffer[-1] = self.curr_track_loc + self.buffer[-1] = self.current_tracking_location # Get latest x, y and smooth. smooth_px, smooth_py = utils.apply_smoothing( @@ -136,8 +141,8 @@ def set_active(self, flag: bool) -> None: def toggle_active(self): logging.info("Toggle active") - curr_state = self.is_active.get() - self.set_active(not curr_state) + current_state = self.is_active.get() + self.set_active(not current_state) def destroy(self): if self.is_active is not None: diff --git a/src/detectors/facemesh.py b/src/detectors/facemesh.py index 9e9f7248..028211a5 100644 --- a/src/detectors/facemesh.py +++ b/src/detectors/facemesh.py @@ -1,11 +1,16 @@ import logging import time +from typing import Any import mediapipe as mp import numpy as np import numpy.typing as npt +from mediapipe.framework.formats.landmark_pb2 import NormalizedLandmark +from mediapipe.python._framework_bindings import image as mediapipe_image from mediapipe.tasks import python from mediapipe.tasks.python import vision +from mediapipe.tasks.python.vision import FaceLandmarkerResult +from numpy import ndarray, dtype import src.utils as utils from src.config_manager import ConfigManager @@ -23,9 +28,10 @@ class FaceMesh(metaclass=Singleton): def __init__(self): + self.smooth_kernel = None logger.info("Initialize FaceMesh singleton") self.mp_landmarks = None - self.track_loc = None + self.tracking_location = None self.blendshapes_buffer = np.zeros([BLENDS_MAX_BUFFER, N_SHAPES]) self.smooth_blendshapes = None self.model = None @@ -54,7 +60,7 @@ def calc_smooth_kernel(self): self.smooth_kernel = utils.calc_smooth_kernel( ConfigManager().config["shape_smooth"]) - def calc_track_loc(self, mp_result, use_transformation_matrix=False): + def calculate_tracking_location(self, mp_result, use_transformation_matrix=False) -> ndarray[Any, dtype[Any]]: screen_w = ConfigManager().config["fix_width"] screen_h = ConfigManager().config["fix_height"] landmarks = mp_result.face_landmarks[0] @@ -87,12 +93,12 @@ def calc_track_loc(self, mp_result, use_transformation_matrix=False): return np.array([x_pixel, y_pixel], np.float32) - def mp_callback(self, mp_result, output_image, timestamp_ms: int): + def mp_callback(self, mp_result: FaceLandmarkerResult, output_image: mediapipe_image.Image, timestamp_ms: int) -> None: if len(mp_result.face_landmarks) >= 1 and len( mp_result.face_blendshapes) >= 1: self.mp_landmarks = mp_result.face_landmarks[0] # Point for moving pointer - self.track_loc = self.calc_track_loc( + self.tracking_location = self.calculate_tracking_location( mp_result, use_transformation_matrix=ConfigManager( ).config["use_transformation_matrix"]) @@ -107,7 +113,7 @@ def mp_callback(self, mp_result, output_image, timestamp_ms: int): else: self.mp_landmarks = None - self.track_loc = None + self.tracking_location = None def detect_frame(self, frame_np: npt.ArrayLike): @@ -119,13 +125,13 @@ def detect_frame(self, frame_np: npt.ArrayLike): self.model.detect_async(frame_mp, t_ms) self.latest_time_ms = t_ms - def get_landmarks(self): + def get_landmarks(self) -> list[NormalizedLandmark]: return self.mp_landmarks - def get_track_loc(self): - return self.track_loc + def get_tracking_location(self) -> ndarray: + return self.tracking_location - def get_blendshapes(self): + def get_blendshapes(self) -> npt.ArrayLike: return self.smooth_blendshapes def destroy(self): diff --git a/src/gui/frames/frame_menu.py b/src/gui/frames/frame_menu.py index c4c50975..f5733194 100644 --- a/src/gui/frames/frame_menu.py +++ b/src/gui/frames/frame_menu.py @@ -64,7 +64,7 @@ def __init__(self, master, master_callback: callable, **kwargs): Image.open("assets/images/prof_drop_head.png"), size=PROF_DROP_SIZE) profile_btn = customtkinter.CTkLabel( master=self, - textvariable=ConfigManager().curr_profile_name, + textvariable=ConfigManager().current_profile_name, image=prof_drop, height=42, compound="center", @@ -84,9 +84,9 @@ def __init__(self, master, master_callback: callable, **kwargs): columnspan=1, rowspan=1) - self.btns = {} - self.btns = self.create_tab_btn(self.menu_btn_images, offset=1) - self.set_tab_active("page_camera") + self.buttons = {} + self.buttons = self.create_tab_btn(self.menu_btn_images, offset=1) + self.set_tab_active(PageSelectCamera.__name__) def create_tab_btn(self, btns: dict, offset): @@ -117,7 +117,7 @@ def create_tab_btn(self, btns: dict, offset): return out_dict def set_tab_active(self, tab_name: str): - for k, btn in self.btns.items(): + for k, btn in self.buttons.items(): im_normal, im_active = self.menu_btn_images[k] if k == tab_name: btn.configure(image=im_active) diff --git a/src/gui/frames/frame_profile.py b/src/gui/frames/frame_profile.py index 2a87a91a..68280797 100644 --- a/src/gui/frames/frame_profile.py +++ b/src/gui/frames/frame_profile.py @@ -61,7 +61,7 @@ def __init__( self.divs = self.load_initial_profiles() - div_id = self.get_div_id(ConfigManager().curr_profile_name.get()) + div_id = self.get_div_id(ConfigManager().current_profile_name.get()) self.set_div_selected(self.divs[div_id]) @@ -197,7 +197,7 @@ def refresh_frame(self): # Delete all divs and re-create self.clear_divs() self.divs = self.load_initial_profiles() - current_profile = ConfigManager().curr_profile_name.get() + current_profile = ConfigManager().current_profile_name.get() # Check if selected profile exist new_name_list = [div["profile_name"] for _, div in self.divs.items()] @@ -273,7 +273,7 @@ def finish_rename(self, div, event): def remove_button_callback(self, div): ConfigManager().remove_profile(div["profile_name"]) - if div["profile_name"] == ConfigManager().curr_profile_name.get(): + if div["profile_name"] == ConfigManager().current_profile_name.get(): default_div_id = self.get_div_id(BACKUP_PROFILE_NAME) self.set_div_selected(self.divs[default_div_id]) diff --git a/src/gui/frames/frame_profile_editor.py b/src/gui/frames/frame_profile_editor.py index 7f4559ff..cde407a9 100644 --- a/src/gui/frames/frame_profile_editor.py +++ b/src/gui/frames/frame_profile_editor.py @@ -60,7 +60,7 @@ def __init__( self.divs = self.load_initial_profiles() - div_id = self.get_div_id(ConfigManager().curr_profile_name.get()) + div_id = self.get_div_id(ConfigManager().current_profile_name.get()) def load_initial_profiles(self): """Create div according to profiles in config @@ -122,7 +122,7 @@ def refresh_frame(self): # Delete all divs and re-create self.clear_divs() self.divs = self.load_initial_profiles() - current_profile = ConfigManager().curr_profile_name.get() + current_profile = ConfigManager().current_profile_name.get() # Check if selected profile exist new_name_list = [div["profile_name"] for _, div in self.divs.items()] @@ -188,7 +188,7 @@ def remove_button_callback(self, div): ConfigManager().remove_profile(div["profile_name"]) # If user remove an active profile, roll back to default - if div["profile_name"] == ConfigManager().curr_profile_name.get(): + if div["profile_name"] == ConfigManager().current_profile_name.get(): logger.warning(f"Removing active profile, rollback to default") ConfigManager().switch_profile(BACKUP_PROFILE_NAME) diff --git a/src/gui/frames/frame_profile_switcher.py b/src/gui/frames/frame_profile_switcher.py index 4e55f4be..2dd81b48 100644 --- a/src/gui/frames/frame_profile_switcher.py +++ b/src/gui/frames/frame_profile_switcher.py @@ -58,7 +58,7 @@ def __init__( self.divs = self.load_initial_profiles() - div_id = self.get_div_id(ConfigManager().curr_profile_name.get()) + div_id = self.get_div_id(ConfigManager().current_profile_name.get()) # self.set_div_selected(self.divs[div_id]) # Custom border @@ -168,7 +168,7 @@ def refresh_frame(self): # Delete all divs and re-create self.clear_divs() self.divs = self.load_initial_profiles() - current_profile = ConfigManager().curr_profile_name.get() + current_profile = ConfigManager().current_profile_name.get() # Check if selected profile exist new_name_list = [div["profile_name"] for _, div in self.divs.items()] diff --git a/src/gui/main_gui.py b/src/gui/main_gui.py index 9e9a1a60..7c3cc112 100644 --- a/src/gui/main_gui.py +++ b/src/gui/main_gui.py @@ -69,7 +69,7 @@ def __init__(self, tk_root): ) ] - self.curr_page_name = None + self.current_page_name = None for page in self.pages: page.grid(row=0, column=1, @@ -122,14 +122,14 @@ def set_mediapipe_mouse_enable(self, new_state: bool): def change_page(self, target_page_name: str): - if self.curr_page_name == target_page_name: + if self.current_page_name == target_page_name: return for page in self.pages: if page.__class__.__name__ == target_page_name: page.grid() page.enter() - self.curr_page_name = page.__class__.__name__ + self.current_page_name = page.__class__.__name__ else: page.grid_remove() diff --git a/src/gui/pages/page_select_camera.py b/src/gui/pages/page_select_camera.py index 342c2330..62a4dea8 100644 --- a/src/gui/pages/page_select_camera.py +++ b/src/gui/pages/page_select_camera.py @@ -43,7 +43,7 @@ def __init__(self, master, **kwargs): # Empty radio buttons self.radio_var = tkinter.IntVar(value=0) self.prev_radio_value = None - self.radios = [] + self.radio_buttons = [] # Camera canvas self.placeholder_im = Image.open( @@ -69,16 +69,16 @@ def __init__(self, master, **kwargs): self.latest_camera_list = [] def update_radio_buttons(self): - """ Update radio buttons to match CameraManager + """ Update radio_buttons to match CameraManager """ new_camera_list = CameraManager().get_camera_list() if len(self.latest_camera_list) != len(new_camera_list): self.latest_camera_list = new_camera_list - logger.info("Refresh radio buttons") - old_radios = self.radios + logger.info("Refresh radio_buttons") + old_radios = self.radio_buttons logger.info(f"Get camera list {new_camera_list}") - radios = [] + radio_buttons = [] for row_i, cam_id in enumerate(new_camera_list): radio_text = f"Camera {cam_id}" @@ -86,21 +86,21 @@ def update_radio_buttons(self): if cam_name is not None: radio_text = f"{radio_text}: {cam_name}" - radio = customtkinter.CTkRadioButton(master=self, + radio_button = customtkinter.CTkRadioButton(master=self, text=radio_text, command=self.radiobutton_event, variable=self.radio_var, value=cam_id) - radio.grid(row=row_i + 2, column=0, padx=50, pady=10, sticky="w") - radios.append(radio) + radio_button.grid(row=row_i + 2, column=0, padx=50, pady=10, sticky="w") + radio_buttons.append(radio_button) - # Set radio select + # Set selected radio_button target_id = ConfigManager().config["camera_id"] - self.radios = radios - for radio in self.radios: - if f"Camera {target_id}" == radio.cget("text"): - radio.select() + self.radio_buttons = radio_buttons + for radio_button in self.radio_buttons: + if f"Camera {target_id}" == radio_button.cget("text"): + radio_button.select() self.prev_radio_value = self.radio_var.get() logger.info(f"Set initial camera to {target_id}") break diff --git a/src/pipeline.py b/src/pipeline.py index 35237a4d..280767ba 100644 --- a/src/pipeline.py +++ b/src/pipeline.py @@ -20,16 +20,16 @@ def pipeline_tick(self) -> None: # Get facial landmarks landmarks = FaceMesh().get_landmarks() if (landmarks is None): - CameraManager().draw_overlay(track_loc=None) + CameraManager().draw_overlay(tracking_location=None) return # Control mouse position - track_loc = FaceMesh().get_track_loc() - MouseController().act(track_loc) + tracking_location = FaceMesh().get_tracking_location() + MouseController().act(tracking_location) # Control keyboard blendshape_values = FaceMesh().get_blendshapes() Keybinder().act(blendshape_values) # Draw frame overlay - CameraManager().draw_overlay(track_loc) + CameraManager().draw_overlay(tracking_location) diff --git a/src/utils/list_cameras.py b/src/utils/list_cameras.py index 0c819145..cb19ae55 100644 --- a/src/utils/list_cameras.py +++ b/src/utils/list_cameras.py @@ -15,17 +15,17 @@ def __open_camera_task(i): logger.info(f"Try opening camera: {i}") try: - cap = cv2.VideoCapture(cv2.CAP_DSHOW + i) + camera = cv2.VideoCapture(cv2.CAP_DSHOW + i) - if cap.getBackendName() != "DSHOW": - logger.info(f"Camera {i}: {cap.getBackendName()} is not supported") + if camera.getBackendName() != "DSHOW": + logger.info(f"Camera {i}: {camera.getBackendName()} is not supported") return (False, i, None) - if cap.get(cv2.CAP_PROP_FRAME_WIDTH) <= 0: + if camera.get(cv2.CAP_PROP_FRAME_WIDTH) <= 0: logger.info(f"Camera {i}: frame size error.") return False, i, None - ret, frame = cap.read() + ret, frame = camera.read() cv2.waitKey(1) if not ret: @@ -33,45 +33,45 @@ def __open_camera_task(i): return (False, i, None) h, w, _ = frame.shape - logger.info(f"Camera {i}: {cap} height: {h} width: {w}") + logger.info(f"Camera {i}: {camera} height: {h} width: {w}") - return (True, i, cap) + return (True, i, camera) except Exception as e: logger.warning(f"Camera {i}: not found {e}") return (False, i, None) -def assign_caps_unblock(caps, i): - ret, _, cap = __open_camera_task(i) +def assign_cameras_unblock(cameras, i): + ret, _, camera = __open_camera_task(i) if not ret: logger.info(f"Camera {i}: Failed to open") - if cap is not None: - caps[i] = cap + if camera is not None: + cameras[i] = camera else: - if i in caps: - del caps[i] + if i in cameras: + del cameras[i] -def assign_caps_queue(caps, done_callback: callable, max_search: int): +def assign_cameras_queue(cameras, done_callback: callable, max_search: int): for i in range(max_search): # block - ret, _, cap = __open_camera_task(i) + ret, _, camera = __open_camera_task(i) if not ret: logger.info(f"Camera {i}: Failed to open") - if cap is not None: - caps[i] = cap + if camera is not None: + cameras[i] = camera done_callback() -def open_camera(caps, i): +def open_camera(cameras, i): """For swapping camera """ pool = futures.ThreadPoolExecutor(max_workers=1) - pool.submit(assign_caps_unblock, caps, i) + pool.submit(assign_cameras_unblock, cameras, i) def get_camera_name(i: int) -> str | None: From bf260341ffa6f4d4f220c2c33814b38754cc6c6f Mon Sep 17 00:00:00 2001 From: acidcoke Date: Tue, 5 Dec 2023 20:51:19 +0100 Subject: [PATCH 060/123] Fix camera update issue --- src/gui/pages/page_select_camera.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/gui/pages/page_select_camera.py b/src/gui/pages/page_select_camera.py index 62a4dea8..b76f3fc5 100644 --- a/src/gui/pages/page_select_camera.py +++ b/src/gui/pages/page_select_camera.py @@ -131,6 +131,8 @@ def page_loop(self): CANVAS_HEIGHT))) self.canvas.itemconfig(self.canvas_im, image=self.new_photo) self.canvas.update() + + CameraManager().thread_cameras.assign_done_flag.wait() self.update_radio_buttons() self.after(ConfigManager().config["tick_interval_ms"], self.page_loop) From e9c4b7bc14574374814da38565a16b7d24e1efe2 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sun, 31 Dec 2023 23:17:36 +0100 Subject: [PATCH 061/123] Refactor Pylint to ruff workflow and update Windows build workflow --- .github/workflows/{pylint.yml => lint.yml} | 10 +-- .github/workflows/windows-build-release.yml | 68 +++++++++++++++------ 2 files changed, 56 insertions(+), 22 deletions(-) rename .github/workflows/{pylint.yml => lint.yml} (75%) diff --git a/.github/workflows/pylint.yml b/.github/workflows/lint.yml similarity index 75% rename from .github/workflows/pylint.yml rename to .github/workflows/lint.yml index c85efa03..4cc26b0d 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/lint.yml @@ -1,10 +1,10 @@ -name: Pylint +name: Lint on: push: jobs: - build: + check: runs-on: ubuntu-latest strategy: matrix: @@ -18,7 +18,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pylint - - name: Analysing the code with pylint + pip install ruff + - name: Analysing the code with ruff run: | - pylint $(git ls-files '*.py') + ruff $(git ls-files '*.py') check diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index ee97ab0b..36543e33 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -1,15 +1,42 @@ on: workflow_dispatch: - push: - branches: - - main - tags: - - v[0-9]+.[0-9]+.[0-9]+ - pull_request: + workflow_run: + workflows: ["Lint"] + branches: [main] + types: + - completed jobs: + + guard: + if: ${{ github.event.workflow_run.conclusion == 'success' && startsWith(github.ref, 'refs/tags/v') }} + runs-on: ubuntu-latest + steps: + - name: Check Tag + id: check-tag + run: | + if [[ ${{ github.event.ref }} =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "match=true" >> $GITHUB_OUTPUT + fi + - name: guard + if: steps.check-tag.outputs.match == 'true' + run: | + echo "Tag matches" + + variables: + runs-on: ubuntu-latest + steps: + - name: Get version from tag regex + id: get-version + run: | + if [[ ${{ github.event.ref }} =~ ^refs/tags/v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then + echo "version=${BASH_REMATCH[1]}" >> $GITHUB_ENV + fi + + build: - runs-on: windows-latest + runs-on: windows-latest + needs: [guard, variables] steps: - uses: actions/checkout@v3 - name: Set up Python 3.10 @@ -34,14 +61,21 @@ jobs: run: | Copy-Item -Path assets -Destination dist-portable\ -Recurse Copy-Item -Path configs -Destination dist-portable\ -Recurse - Compress-Archive -Path dist-portable -DestinationPath GrimassistPortable.zip - - name: Upload installer - uses: actions/upload-artifact@v3 - with: - name: 'Windows Installer Release' - path: '\a\grimassist\grimassist\Output\Grimassist Installer.exe' - - name: Upload portable - uses: actions/upload-artifact@v3 + Compress-Archive -Path dist-portable -DestinationPath Grimassist-${{env.version}}-Portable.zip + - name: rename installer + run: | + Move-Item -Path Output\GrimassistInstaller.exe -Destination Output\Grimassist-${{env.version}}-Installer.exe + + - name: release + uses: softprops/action-gh-release@v1 with: - name: 'Windows Portable Release' - path: '\a\grimassist\grimassist\GrimassistPortable.zip' + files: | + Output\Grimassist-${{env.version}}-Installer.exe + Grimassist-${{env.version}}-Portable.zip + tag_name: github.ref_name + body: | + Grimassist ${{env.version}} + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From e119eb1b1815da96b5fd71cb600041f7e613ff31 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sun, 31 Dec 2023 23:35:29 +0100 Subject: [PATCH 062/123] Fix warnings and remove unused imports --- src/gui/frames/frame_profile_editor.py | 2 +- src/gui/frames/frame_profile_switcher.py | 1 - src/gui/main_gui.py | 2 -- src/gui/pages/page_cursor.py | 4 ++-- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/gui/frames/frame_profile_editor.py b/src/gui/frames/frame_profile_editor.py index cbce285c..d89114f7 100644 --- a/src/gui/frames/frame_profile_editor.py +++ b/src/gui/frames/frame_profile_editor.py @@ -190,7 +190,7 @@ def remove_button_callback(self, div): # If user remove an active profile, roll back to default if div["profile_name"] == ConfigManager().curr_profile_name.get(): - logger.warning(f"Removing active profile, rollback to default") + logger.warning("Removing active profile, rollback to default") ConfigManager().switch_profile(BACKUP_PROFILE_NAME) # Refresh values in each page diff --git a/src/gui/frames/frame_profile_switcher.py b/src/gui/frames/frame_profile_switcher.py index a8da1b8c..d1a25b69 100644 --- a/src/gui/frames/frame_profile_switcher.py +++ b/src/gui/frames/frame_profile_switcher.py @@ -1,6 +1,5 @@ import logging -import re import time import tkinter as tk from functools import partial diff --git a/src/gui/main_gui.py b/src/gui/main_gui.py index 3acccb9f..da5009e1 100644 --- a/src/gui/main_gui.py +++ b/src/gui/main_gui.py @@ -1,9 +1,7 @@ import logging -import tkinter as tk import customtkinter -from PIL import Image import src.gui.frames as frames import src.gui.pages as pages diff --git a/src/gui/pages/page_cursor.py b/src/gui/pages/page_cursor.py index 27a7c847..a667268e 100644 --- a/src/gui/pages/page_cursor.py +++ b/src/gui/pages/page_cursor.py @@ -8,7 +8,7 @@ from PIL import Image from src.config_manager import ConfigManager -from src.controllers import MouseController, Keybinder +from src.controllers import MouseController from src.gui.balloon import Balloon from src.gui.frames.safe_disposable_frame import SafeDisposableFrame @@ -202,7 +202,7 @@ def entry_changed_callback(self, div_name, slider_min, slider_max, var, is_valid_input = False else: new_value = int(entry_value) - if not new_value in range(slider_min, slider_max): + if new_value not in range(slider_min, slider_max): is_valid_input = False # Update slider and config From bf3436a984120fd63aa1cf8e3185e3fddb3858cd Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 14:58:57 +0100 Subject: [PATCH 063/123] Refactor import statements in controllers, detectors, gui, and utils packages --- src/controllers/__init__.py | 6 +++--- src/detectors/__init__.py | 4 ++-- src/gui/__init__.py | 4 ++-- src/gui/frames/__init__.py | 12 ++++++------ src/gui/pages/__init__.py | 12 ++++++------ src/utils/__init__.py | 8 ++++---- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/controllers/__init__.py b/src/controllers/__init__.py index 28537d01..f2de65fb 100644 --- a/src/controllers/__init__.py +++ b/src/controllers/__init__.py @@ -1,3 +1,3 @@ - -from .keybinder import * -from .mouse_controller import * +__all__ = ['Keybinder', 'MouseController'] +from .keybinder import Keybinder +from .mouse_controller import MouseController diff --git a/src/detectors/__init__.py b/src/detectors/__init__.py index d573beb2..f25e950b 100644 --- a/src/detectors/__init__.py +++ b/src/detectors/__init__.py @@ -1,2 +1,2 @@ - -from .facemesh import * +__all__ = ['FaceMesh'] +from .facemesh import FaceMesh \ No newline at end of file diff --git a/src/gui/__init__.py b/src/gui/__init__.py index 0fb4001e..36252c85 100644 --- a/src/gui/__init__.py +++ b/src/gui/__init__.py @@ -1,2 +1,2 @@ - -from .main_gui import * +__all__ = ['MainGui'] +from .main_gui import MainGui diff --git a/src/gui/frames/__init__.py b/src/gui/frames/__init__.py index dfcf2080..20a2fdd6 100644 --- a/src/gui/frames/__init__.py +++ b/src/gui/frames/__init__.py @@ -1,6 +1,6 @@ - -from .frame_cam_preview import * -from .frame_menu import * -from .frame_profile_editor import * -from .frame_profile_switcher import * -from .safe_disposable_frame import * +__all__ = ['FrameCamPreview', 'FrameMenu', 'FrameProfileEditor', 'FrameProfileSwitcher', 'SafeDisposableFrame'] +from .frame_cam_preview import FrameCamPreview +from .frame_menu import FrameMenu +from .frame_profile_editor import FrameProfileEditor +from .frame_profile_switcher import FrameProfileSwitcher +from .safe_disposable_frame import SafeDisposableFrame diff --git a/src/gui/pages/__init__.py b/src/gui/pages/__init__.py index 0a10c0e0..dcf38cd9 100644 --- a/src/gui/pages/__init__.py +++ b/src/gui/pages/__init__.py @@ -1,6 +1,6 @@ - -from .page_cursor import * -from .page_home import * -from .page_keyboard import * -from .page_select_camera import * -from .page_select_gestures import * +__all__ = ['PageCursor', 'PageHome', 'PageKeyboard', 'PageSelectCamera', 'PageSelectGestures'] +from .page_cursor import PageCursor +from .page_home import PageHome +from .page_keyboard import PageKeyboard +from .page_select_camera import PageSelectCamera +from .page_select_gestures import PageSelectGestures diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 4e3640e3..5c0c4029 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -1,4 +1,4 @@ - -from .install_font import * -from .list_cameras import * -from .smoothing import * +__all__ = ['calc_smooth_kernel', 'apply_smoothing', 'open_camera', 'assign_caps_queue', 'assign_caps_unblock', 'install_fonts', 'remove_fonts'] +from .install_font import install_fonts, remove_fonts +from .list_cameras import assign_caps_queue, assign_caps_unblock, open_camera +from .smoothing import calc_smooth_kernel, apply_smoothing From e50be162b9c005dc01959f89d5fd5698fd5f325f Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 14:59:13 +0100 Subject: [PATCH 064/123] Fix profile name validation in profile frames --- src/gui/frames/frame_profile.py | 2 +- src/gui/frames/frame_profile_editor.py | 3 +-- src/gui/frames/frame_profile_switcher.py | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/gui/frames/frame_profile.py b/src/gui/frames/frame_profile.py index 7bc2131f..adb25a26 100644 --- a/src/gui/frames/frame_profile.py +++ b/src/gui/frames/frame_profile.py @@ -391,7 +391,7 @@ def create_div(self, row: int, div_id: str, profile_name) -> dict: if edit_button is not None: edit_button.configure( command=partial(self.rename_button_callback, div)) - entry_var_trace_id = entry_var.trace( + entry_var.trace( "w", partial(self.check_profile_name_valid, div)) entry.bind('', command=partial(self.finish_rename, div)) diff --git a/src/gui/frames/frame_profile_editor.py b/src/gui/frames/frame_profile_editor.py index d89114f7..fd6a795a 100644 --- a/src/gui/frames/frame_profile_editor.py +++ b/src/gui/frames/frame_profile_editor.py @@ -61,7 +61,6 @@ def __init__( self.divs = self.load_initial_profiles() - div_id = self.get_div_id(ConfigManager().curr_profile_name.get()) def load_initial_profiles(self): """Create div according to profiles in config @@ -300,7 +299,7 @@ def create_div(self, row: int, div_id: str, profile_name) -> dict: if edit_button is not None: edit_button.configure( command=partial(self.rename_button_callback, div)) - entry_var_trace_id = entry_var.trace( + entry_var.trace( "w", partial(self.check_profile_name_valid, div)) entry.bind('', command=partial(self.finish_rename, div)) diff --git a/src/gui/frames/frame_profile_switcher.py b/src/gui/frames/frame_profile_switcher.py index d1a25b69..dcd1860d 100644 --- a/src/gui/frames/frame_profile_switcher.py +++ b/src/gui/frames/frame_profile_switcher.py @@ -58,7 +58,6 @@ def __init__( self.divs = self.load_initial_profiles() - div_id = self.get_div_id(ConfigManager().curr_profile_name.get()) # self.set_div_selected(self.divs[div_id]) # Custom border From c9a67b5fa3218095c92e7b9662fe8f1bc277c89f Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 15:03:04 +0100 Subject: [PATCH 065/123] Add ignore rules for VsCode, Ruff, and output directory --- .gitignore | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.gitignore b/.gitignore index dc9770b7..8fdaee6f 100644 --- a/.gitignore +++ b/.gitignore @@ -74,6 +74,9 @@ docs/_build/ # PyBuilder target/ +# VsCode +.vscode/ + # PyCharm .idea/ @@ -132,3 +135,10 @@ dmypy.json .pyre/ *.bat log.txt + +# Ruff +.ruff/ +.ruff_cache/ + +# Output directory +output/ \ No newline at end of file From b3d03e8d8aa5eacb21d37b32514ad9db8c90f561 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 15:04:40 +0100 Subject: [PATCH 066/123] Update lint.yml workflow --- .github/workflows/lint.yml | 48 +++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4cc26b0d..736f8776 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,24 +1,24 @@ -name: Lint - -on: - push: - -jobs: - check: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.10"] - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install ruff - - name: Analysing the code with ruff - run: | - ruff $(git ls-files '*.py') check +name: Lint + +on: + push: + +jobs: + check: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ruff + - name: Analysing the code with ruff + run: | + ruff $(git ls-files '*.py') check From d65260c0eb24187f7c17324277f5f28c20485ed3 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 17:23:42 +0100 Subject: [PATCH 067/123] Update ruff command in lint.yml --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 736f8776..a44d2dbc 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,4 +21,4 @@ jobs: pip install ruff - name: Analysing the code with ruff run: | - ruff $(git ls-files '*.py') check + ruff check . From f59657c85b69bdda3e22c9d474cf302acd8bb298 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 18:53:36 +0100 Subject: [PATCH 068/123] Add guard job and build job documentation to windows-build-release.yml --- .github/workflows/windows-build-release.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 36543e33..53bfd809 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -8,6 +8,8 @@ on: jobs: + # The guard job checks if the workflow_run event was successful and if the branch starts with 'refs/tags/v'. + # If the conditions are met, it sets a flag indicating a match. guard: if: ${{ github.event.workflow_run.conclusion == 'success' && startsWith(github.ref, 'refs/tags/v') }} runs-on: ubuntu-latest @@ -24,6 +26,7 @@ jobs: echo "Tag matches" variables: + needs: [guard] runs-on: ubuntu-latest steps: - name: Get version from tag regex @@ -33,7 +36,7 @@ jobs: echo "version=${BASH_REMATCH[1]}" >> $GITHUB_ENV fi - + # The build job runs on a Windows machine and performs various build steps. build: runs-on: windows-latest needs: [guard, variables] From 84391e15d8719d466bd84bc2617151677cfc25ff Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 18:57:13 +0100 Subject: [PATCH 069/123] Bump version number to 0.4.0 --- installer.iss | 2 +- src/config_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/installer.iss b/installer.iss index 1a923a16..4b39f5d0 100644 --- a/installer.iss +++ b/installer.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Grimassist" -#define MyAppVersion "0.3.34" +#define MyAppVersion "0.4.0" #define MyAppExeName "grimassist.exe" [Setup] diff --git a/src/config_manager.py b/src/config_manager.py index be9c5357..49024f14 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -10,7 +10,7 @@ from src.singleton_meta import Singleton from src.task_killer import TaskKiller -VERSION = "0.3.34" +VERSION = "0.4.0" DEFAULT_JSON = Path(f"C:/Users/{os.getlogin()}/Grimassist/configs/default.json") BACKUP_PROFILE = Path(f"C:/Users/{os.getlogin()}/Grimassist/configs/default") From 20716735361b81d4810ac96dc939587b0263e44d Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 19:13:44 +0100 Subject: [PATCH 070/123] Update guard job condition in windows-build-release.yml --- .github/workflows/windows-build-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 53bfd809..043aa60d 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -11,7 +11,7 @@ jobs: # The guard job checks if the workflow_run event was successful and if the branch starts with 'refs/tags/v'. # If the conditions are met, it sets a flag indicating a match. guard: - if: ${{ github.event.workflow_run.conclusion == 'success' && startsWith(github.ref, 'refs/tags/v') }} + if: ${{ github.event.workflow_run.conclusion == 'completed' && startsWith(github.ref, 'refs/tags/v') }} runs-on: ubuntu-latest steps: - name: Check Tag From fb1248a40c121bda0770931d29062ac5d67d3b86 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 19:18:00 +0100 Subject: [PATCH 071/123] Update guard job condition in windows-build-release.yml --- .github/workflows/windows-build-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 043aa60d..210456c0 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -11,7 +11,7 @@ jobs: # The guard job checks if the workflow_run event was successful and if the branch starts with 'refs/tags/v'. # If the conditions are met, it sets a flag indicating a match. guard: - if: ${{ github.event.workflow_run.conclusion == 'completed' && startsWith(github.ref, 'refs/tags/v') }} + if: ${{ github.event.workflow_run.conclusion == 'completed'}} runs-on: ubuntu-latest steps: - name: Check Tag From 5d06198b702f70342ead3856d142323873408e91 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 19:25:19 +0100 Subject: [PATCH 072/123] Update windows-build-release.yml workflow This commit updates the windows-build-release.yml workflow file. It includes changes to the on event triggers and adds a new step to lookup the conclusion of the workflow_run event. The guard job has also been modified to check for a successful workflow_run conclusion and a branch starting with 'refs/tags/v'. --- .github/workflows/windows-build-release.yml | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 210456c0..9bd99a8a 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -1,17 +1,30 @@ on: workflow_dispatch: workflow_run: - workflows: ["Lint"] + workflows: [Lint] branches: [main] - types: - - completed + types: [completed] jobs: + get_workflow_conclusion: + name: Lookup Conclusion of Workflow_Run Event + runs-on: ubuntu-latest + outputs: + conclusion: ${{ fromJson(steps.get_conclusion.outputs.data).conclusion }} + steps: + - name: Get Workflow Run + uses: octokit/request-action@v2.1.0 + id: get_conclusion + with: + route: GET /repos/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # The guard job checks if the workflow_run event was successful and if the branch starts with 'refs/tags/v'. # If the conditions are met, it sets a flag indicating a match. guard: - if: ${{ github.event.workflow_run.conclusion == 'completed'}} + if: ${{ github.event.workflow_run.conclusion == 'success' && startsWith(github.event.ref, 'refs/tags/v') }} runs-on: ubuntu-latest steps: - name: Check Tag From ec68a976899f6c6bafdfbf1631def1fca40582a0 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 19:26:31 +0100 Subject: [PATCH 073/123] Add dependency on get_workflow_conclusion job --- .github/workflows/windows-build-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 9bd99a8a..187517cc 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -26,6 +26,7 @@ jobs: guard: if: ${{ github.event.workflow_run.conclusion == 'success' && startsWith(github.event.ref, 'refs/tags/v') }} runs-on: ubuntu-latest + needs: get_workflow_conclusion steps: - name: Check Tag id: check-tag From 05c1f90607c052096cbcd59c6986735a08017021 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 19:34:17 +0100 Subject: [PATCH 074/123] Update GitHub workflows for linting and Windows build --- .github/workflows/lint.yml | 4 +- .github/workflows/windows-build-release.yml | 54 ++++++++------------- 2 files changed, 22 insertions(+), 36 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a44d2dbc..954a4345 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,7 +2,9 @@ name: Lint on: push: - + branches-ignore: + - main + jobs: check: runs-on: ubuntu-latest diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 187517cc..a101cfb6 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -1,46 +1,30 @@ on: - workflow_dispatch: - workflow_run: - workflows: [Lint] - branches: [main] - types: [completed] + push: + tags: + - 'v*.*.*' jobs: - get_workflow_conclusion: - name: Lookup Conclusion of Workflow_Run Event + check: runs-on: ubuntu-latest - outputs: - conclusion: ${{ fromJson(steps.get_conclusion.outputs.data).conclusion }} + strategy: + matrix: + python-version: ["3.10"] steps: - - name: Get Workflow Run - uses: octokit/request-action@v2.1.0 - id: get_conclusion - with: - route: GET /repos/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # The guard job checks if the workflow_run event was successful and if the branch starts with 'refs/tags/v'. - # If the conditions are met, it sets a flag indicating a match. - guard: - if: ${{ github.event.workflow_run.conclusion == 'success' && startsWith(github.event.ref, 'refs/tags/v') }} - runs-on: ubuntu-latest - needs: get_workflow_conclusion - steps: - - name: Check Tag - id: check-tag + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies run: | - if [[ ${{ github.event.ref }} =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "match=true" >> $GITHUB_OUTPUT - fi - - name: guard - if: steps.check-tag.outputs.match == 'true' + python -m pip install --upgrade pip + pip install ruff + - name: Analysing the code with ruff run: | - echo "Tag matches" + ruff check . - variables: - needs: [guard] + set-variables: runs-on: ubuntu-latest steps: - name: Get version from tag regex @@ -53,7 +37,7 @@ jobs: # The build job runs on a Windows machine and performs various build steps. build: runs-on: windows-latest - needs: [guard, variables] + needs: [set-variables] steps: - uses: actions/checkout@v3 - name: Set up Python 3.10 From 104802f2fda50415a6e24b6587930f3001a7a213 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 19:43:29 +0100 Subject: [PATCH 075/123] Update version extraction in windows-build-release.yml --- .github/workflows/windows-build-release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index a101cfb6..883dfe9f 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -30,8 +30,9 @@ jobs: - name: Get version from tag regex id: get-version run: | - if [[ ${{ github.event.ref }} =~ ^refs/tags/v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then + if [[ ${{ github.ref }} =~ ^refs/tags/v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then echo "version=${BASH_REMATCH[1]}" >> $GITHUB_ENV + echo "version=${BASH_REMATCH[1]}" fi # The build job runs on a Windows machine and performs various build steps. From f12ebb5c8a6ae0f493e217548c39ddb6ad357f63 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 19:55:31 +0100 Subject: [PATCH 076/123] Update file paths and variables in windows-build-release.yml --- .github/workflows/windows-build-release.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 883dfe9f..3180abc3 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -63,20 +63,18 @@ jobs: run: | Copy-Item -Path assets -Destination dist-portable\ -Recurse Copy-Item -Path configs -Destination dist-portable\ -Recurse - Compress-Archive -Path dist-portable -DestinationPath Grimassist-${{env.version}}-Portable.zip + Compress-Archive -Path dist-portable -DestinationPath Grimassist-$version-Portable.zip - name: rename installer run: | - Move-Item -Path Output\GrimassistInstaller.exe -Destination Output\Grimassist-${{env.version}}-Installer.exe + Move-Item -Path Output\GrimassistInstaller.exe -Destination Output\Grimassist-$version-Installer.exe - name: release uses: softprops/action-gh-release@v1 with: files: | - Output\Grimassist-${{env.version}}-Installer.exe - Grimassist-${{env.version}}-Portable.zip + Output\Grimassist-$version-Installer.exe + Grimassist-$version-Portable.zip tag_name: github.ref_name - body: | - Grimassist ${{env.version}} draft: false prerelease: false env: From fa6fe721018cfa6ddd331751f8a5d9781ba0b427 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 20:08:01 +0100 Subject: [PATCH 077/123] Refactor GitHub Actions workflow and test variable --- .github/workflows/windows-build-release.yml | 49 +++------------------ 1 file changed, 6 insertions(+), 43 deletions(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 3180abc3..08265d45 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -31,51 +31,14 @@ jobs: id: get-version run: | if [[ ${{ github.ref }} =~ ^refs/tags/v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then - echo "version=${BASH_REMATCH[1]}" >> $GITHUB_ENV echo "version=${BASH_REMATCH[1]}" + echo "version=${BASH_REMATCH[1]}" >> $GITHUB_ENV fi - # The build job runs on a Windows machine and performs various build steps. - build: - runs-on: windows-latest - needs: [set-variables] + test-variable: + runs-on: ubuntu-latest + needs: set-variables steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.10 - uses: actions/setup-python@v3 - with: - python-version: "3.10" - - name: Install dependencies + - name: Test variable run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - - name: Freeze Installer - run: | - pyinstaller build.spec - - name: Build Installer - run: | - iscc installer.iss - - name: Freeze Portable - run: | - pyinstaller --distpath dist-portable build-portable.spec - - name: Zip Portable - shell: pwsh - run: | - Copy-Item -Path assets -Destination dist-portable\ -Recurse - Copy-Item -Path configs -Destination dist-portable\ -Recurse - Compress-Archive -Path dist-portable -DestinationPath Grimassist-$version-Portable.zip - - name: rename installer - run: | - Move-Item -Path Output\GrimassistInstaller.exe -Destination Output\Grimassist-$version-Installer.exe - - - name: release - uses: softprops/action-gh-release@v1 - with: - files: | - Output\Grimassist-$version-Installer.exe - Grimassist-$version-Portable.zip - tag_name: github.ref_name - draft: false - prerelease: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + echo "version=$version" From 7eb17ef1589740ba0f735c3c5d9f745c95c2bf04 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 20:11:08 +0100 Subject: [PATCH 078/123] Update version extraction regex in windows-build-release.yml --- .github/workflows/windows-build-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 08265d45..709b0141 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -30,7 +30,7 @@ jobs: - name: Get version from tag regex id: get-version run: | - if [[ ${{ github.ref }} =~ ^refs/tags/v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then + if [[ ${{ github.ref_name }} =~ v([0-9]+\.[0-9]+\.[0-9]+) ]]; then echo "version=${BASH_REMATCH[1]}" echo "version=${BASH_REMATCH[1]}" >> $GITHUB_ENV fi From f36c3cea9cdb39b0d83442530bcc597c373bf8c0 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 20:16:59 +0100 Subject: [PATCH 079/123] Update version handling in windows-build-release.yml --- .github/workflows/windows-build-release.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 709b0141..3cde45bf 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -31,8 +31,7 @@ jobs: id: get-version run: | if [[ ${{ github.ref_name }} =~ v([0-9]+\.[0-9]+\.[0-9]+) ]]; then - echo "version=${BASH_REMATCH[1]}" - echo "version=${BASH_REMATCH[1]}" >> $GITHUB_ENV + echo "version=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT fi test-variable: @@ -41,4 +40,4 @@ jobs: steps: - name: Test variable run: | - echo "version=$version" + echo "version=${{needs.set-variables.outputs.version}}" From 0c4d9fe88c39520f8f12a0b038144b9e726e6785 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 20:22:09 +0100 Subject: [PATCH 080/123] Add Windows build workflow for releasing Grimassist --- .github/workflows/windows-build-release.yml | 54 +++++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 3cde45bf..aef82e73 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -34,10 +34,54 @@ jobs: echo "version=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT fi - test-variable: - runs-on: ubuntu-latest - needs: set-variables + # The build job runs on a Windows machine and performs various build steps. + build: + runs-on: windows-latest + needs: [set-variables] steps: - - name: Test variable + - 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 + pip install -r requirements.txt + - name: Freeze Installer + run: | + pyinstaller build.spec + - name: Build Installer + run: | + iscc installer.iss + - name: Freeze Portable run: | - echo "version=${{needs.set-variables.outputs.version}}" + pyinstaller --distpath dist-portable build-portable.spec + + - name: Get version from tag regex + id: get-version + run: | + if [[ ${{ github.ref_name }} =~ v([0-9]+\.[0-9]+\.[0-9]+) ]]; then + echo "version=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT + fi + - name: Zip Portable + shell: pwsh + run: | + Copy-Item -Path assets -Destination dist-portable\ -Recurse + Copy-Item -Path configs -Destination dist-portable\ -Recurse + Compress-Archive -Path dist-portable -DestinationPath Grimassist-${{steps.get-version.outputs.version}}-Portable.zip + - name: rename installer + run: | + Move-Item -Path Output\GrimassistInstaller.exe -Destination Output\Grimassist-${{steps.get-version.outputs.version}}-Installer.exe + + - name: release + uses: softprops/action-gh-release@v1 + with: + files: | + Output\Grimassist-$version-Installer.exe + Grimassist-$version-Portable.zip + tag_name: github.ref_name + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 17236b7bec5d9296cd1583c21bb25acbb30fba5c Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 20:31:56 +0100 Subject: [PATCH 081/123] Update version extraction in windows-build-release.yml --- .github/workflows/windows-build-release.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index aef82e73..4e8b542a 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -61,9 +61,10 @@ jobs: - name: Get version from tag regex id: get-version run: | - if [[ ${{ github.ref_name }} =~ v([0-9]+\.[0-9]+\.[0-9]+) ]]; then - echo "version=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT - fi + if ($env:github_ref_name -match 'v([0-9]+\.[0-9]+\.[0-9]+)') { + $version = $Matches[1] + Write-Host "version=$version" >> $env:GITHUB_OUTPUT + } - name: Zip Portable shell: pwsh run: | From d84e7a2e98d914d0ddc75a3b645fda1bbbfe7927 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 20:38:47 +0100 Subject: [PATCH 082/123] Fix version extraction in Windows build workflow --- .github/workflows/windows-build-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 4e8b542a..a5ad4212 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -61,7 +61,7 @@ jobs: - name: Get version from tag regex id: get-version run: | - if ($env:github_ref_name -match 'v([0-9]+\.[0-9]+\.[0-9]+)') { + if (${{github.ref_name}} -match 'v([0-9]+\.[0-9]+\.[0-9]+)') { $version = $Matches[1] Write-Host "version=$version" >> $env:GITHUB_OUTPUT } From e56f7e2373a80d2aaf6af7dfe48570c2717c67b6 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 20:47:23 +0100 Subject: [PATCH 083/123] Update Windows build workflow --- .github/workflows/windows-build-release.yml | 23 ++------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index a5ad4212..2f1c25ea 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -38,30 +38,11 @@ jobs: build: runs-on: windows-latest needs: [set-variables] - 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 - pip install -r requirements.txt - - name: Freeze Installer - run: | - pyinstaller build.spec - - name: Build Installer - run: | - iscc installer.iss - - name: Freeze Portable - run: | - pyinstaller --distpath dist-portable build-portable.spec - + steps: - name: Get version from tag regex id: get-version run: | - if (${{github.ref_name}} -match 'v([0-9]+\.[0-9]+\.[0-9]+)') { + if ("${{github.ref_name}}" -match 'v([0-9]+\.[0-9]+\.[0-9]+)') { $version = $Matches[1] Write-Host "version=$version" >> $env:GITHUB_OUTPUT } From 8c98b522e1f2dbe26b65d7b4da4907caf93f07cb Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 20:56:02 +0100 Subject: [PATCH 084/123] Update Windows build workflow to use Python 3.10 and freeze portable version --- .github/workflows/windows-build-release.yml | 41 +++++++++++---------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 2f1c25ea..23c367fe 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -24,37 +24,38 @@ jobs: run: | ruff check . - set-variables: - runs-on: ubuntu-latest - steps: - - name: Get version from tag regex - id: get-version - run: | - if [[ ${{ github.ref_name }} =~ v([0-9]+\.[0-9]+\.[0-9]+) ]]; then - echo "version=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT - fi - # The build job runs on a Windows machine and performs various build steps. build: runs-on: windows-latest - needs: [set-variables] - steps: - - name: Get version from tag regex - id: get-version + 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 + pip install -r requirements.txt + - name: Freeze Installer + run: | + pyinstaller build.spec + - name: Build Installer + run: | + iscc installer.iss + - name: Freeze Portable run: | - if ("${{github.ref_name}}" -match 'v([0-9]+\.[0-9]+\.[0-9]+)') { - $version = $Matches[1] - Write-Host "version=$version" >> $env:GITHUB_OUTPUT - } + pyinstaller --distpath dist-portable build-portable.spec + - name: Zip Portable shell: pwsh run: | Copy-Item -Path assets -Destination dist-portable\ -Recurse Copy-Item -Path configs -Destination dist-portable\ -Recurse - Compress-Archive -Path dist-portable -DestinationPath Grimassist-${{steps.get-version.outputs.version}}-Portable.zip + Compress-Archive -Path dist-portable -DestinationPath Grimassist-${{github.ref_name}}-Portable.zip - name: rename installer run: | - Move-Item -Path Output\GrimassistInstaller.exe -Destination Output\Grimassist-${{steps.get-version.outputs.version}}-Installer.exe + Move-Item -Path Output\GrimassistInstaller.exe -Destination Output\Grimassist-Installer-${{github.ref_name}}.exe - name: release uses: softprops/action-gh-release@v1 From 39697c7c151e790c1a31b713875416b9ad0f06b2 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 21:01:56 +0100 Subject: [PATCH 085/123] Update file paths in windows-build-release.yml --- .github/workflows/windows-build-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 23c367fe..30065108 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -55,7 +55,7 @@ jobs: Compress-Archive -Path dist-portable -DestinationPath Grimassist-${{github.ref_name}}-Portable.zip - name: rename installer run: | - Move-Item -Path Output\GrimassistInstaller.exe -Destination Output\Grimassist-Installer-${{github.ref_name}}.exe + Move-Item -Path Output\Grimassist Installer.exe -Destination Output\Grimassist-Installer-${{github.ref_name}}.exe - name: release uses: softprops/action-gh-release@v1 From 266bc7a2c725b1519eeb7d90a22a25489e1a9923 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 21:15:17 +0100 Subject: [PATCH 086/123] Update file paths and naming conventions --- .github/workflows/windows-build-release.yml | 9 +++------ installer.iss | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 30065108..f3ea262f 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -52,17 +52,14 @@ jobs: run: | Copy-Item -Path assets -Destination dist-portable\ -Recurse Copy-Item -Path configs -Destination dist-portable\ -Recurse - Compress-Archive -Path dist-portable -DestinationPath Grimassist-${{github.ref_name}}-Portable.zip - - name: rename installer - run: | - Move-Item -Path Output\Grimassist Installer.exe -Destination Output\Grimassist-Installer-${{github.ref_name}}.exe + Compress-Archive -Path dist-portable -DestinationPath Grimassist-Portable-${{github.ref_name}}.zip - name: release uses: softprops/action-gh-release@v1 with: files: | - Output\Grimassist-$version-Installer.exe - Grimassist-$version-Portable.zip + Output\Grimassist-Installer-${{github.ref_name}}.exe + Grimassist-Portable-${{github.ref_name}}.zip tag_name: github.ref_name draft: false prerelease: false diff --git a/installer.iss b/installer.iss index 4b39f5d0..1243b56f 100644 --- a/installer.iss +++ b/installer.iss @@ -16,7 +16,7 @@ DefaultDirName={autopf}\{#MyAppName} DisableProgramGroupPage=yes ; Uncomment the following line to run in non administrative install mode (install for current user only.) ;PrivilegesRequired=lowest -OutputBaseFilename=Grimassist Installer +OutputBaseFilename=Grimassist-Installer-v{MyAppVersion} SetupIconFile=assets\images\icon.ico Compression=lzma SolidCompression=yes From de1e857158d58ab56e45d921226bf943251c520b Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 21:23:52 +0100 Subject: [PATCH 087/123] Update installer output filename in installer.iss --- installer.iss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer.iss b/installer.iss index 1243b56f..ccd1843b 100644 --- a/installer.iss +++ b/installer.iss @@ -16,7 +16,7 @@ DefaultDirName={autopf}\{#MyAppName} DisableProgramGroupPage=yes ; Uncomment the following line to run in non administrative install mode (install for current user only.) ;PrivilegesRequired=lowest -OutputBaseFilename=Grimassist-Installer-v{MyAppVersion} +OutputBaseFilename=Grimassist-Installer-v{#MyAppVersion} SetupIconFile=assets\images\icon.ico Compression=lzma SolidCompression=yes From f718c4fe6e1637ae7fc3c79732e52ed56abca879 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 21:33:23 +0100 Subject: [PATCH 088/123] Fix file path in windows-build-release.yml --- .github/workflows/windows-build-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index f3ea262f..190fd262 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -58,7 +58,7 @@ jobs: uses: softprops/action-gh-release@v1 with: files: | - Output\Grimassist-Installer-${{github.ref_name}}.exe + Output/Grimassist-Installer-${{github.ref_name}}.exe Grimassist-Portable-${{github.ref_name}}.zip tag_name: github.ref_name draft: false From e379bda86aaf2d6a2f873bdcbb3d969b6b9a2453 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 21:41:44 +0100 Subject: [PATCH 089/123] Add dependency on check job in build workflow --- .github/workflows/windows-build-release.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index 190fd262..e24141f0 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -27,6 +27,7 @@ jobs: # The build job runs on a Windows machine and performs various build steps. build: runs-on: windows-latest + needs: check steps: - uses: actions/checkout@v3 - name: Set up Python 3.10 @@ -63,5 +64,3 @@ jobs: tag_name: github.ref_name draft: false prerelease: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 04b0c2d447f7055e9fca68233f02db70b6f8b514 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 21:50:18 +0100 Subject: [PATCH 090/123] Add write permissions for contents in windows-build-release.yml --- .github/workflows/windows-build-release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index e24141f0..aef248fb 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -3,6 +3,9 @@ on: tags: - 'v*.*.*' +permissions: + contents: write + jobs: check: From e5d271a9a0d30385d77d1d93bff6bbe6bc2a8eae Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 21:56:50 +0100 Subject: [PATCH 091/123] Fix tag_name variable in release action --- .github/workflows/windows-build-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml index aef248fb..308b1a50 100644 --- a/.github/workflows/windows-build-release.yml +++ b/.github/workflows/windows-build-release.yml @@ -5,7 +5,7 @@ on: permissions: contents: write - + jobs: check: @@ -64,6 +64,6 @@ jobs: files: | Output/Grimassist-Installer-${{github.ref_name}}.exe Grimassist-Portable-${{github.ref_name}}.zip - tag_name: github.ref_name + tag_name: ${{github.ref_name}} draft: false prerelease: false From fda897da8fdd2956e3c8afd0c3e121a8bd2693be Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 1 Jan 2024 22:22:56 +0100 Subject: [PATCH 092/123] Update __init__.py with new camera functions --- src/utils/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 5c0c4029..8d87be52 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -1,4 +1,4 @@ -__all__ = ['calc_smooth_kernel', 'apply_smoothing', 'open_camera', 'assign_caps_queue', 'assign_caps_unblock', 'install_fonts', 'remove_fonts'] +__all__ = ['calc_smooth_kernel', 'apply_smoothing', 'open_camera', 'get_camera_name','assign_cameras_queue', 'assign_cameras_unblock', 'install_fonts', 'remove_fonts'] from .install_font import install_fonts, remove_fonts -from .list_cameras import assign_caps_queue, assign_caps_unblock, open_camera +from .list_cameras import assign_cameras_queue, assign_cameras_unblock, open_camera, get_camera_name from .smoothing import calc_smooth_kernel, apply_smoothing From a46db23744ddddb917a3a245bfcc50eb8aacb443 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sun, 28 Jan 2024 22:26:27 +0100 Subject: [PATCH 093/123] introduce enum for trigger modes --- src/controllers/keybinder.py | 481 ++++++++++++++++++----------------- src/utils/Trigger.py | 7 + 2 files changed, 255 insertions(+), 233 deletions(-) create mode 100644 src/utils/Trigger.py diff --git a/src/controllers/keybinder.py b/src/controllers/keybinder.py index 8f801d32..45999444 100644 --- a/src/controllers/keybinder.py +++ b/src/controllers/keybinder.py @@ -1,233 +1,248 @@ -import copy -import logging -import math -import time - -import pydirectinput -import win32api -import tkinter as tk - -import src.shape_list as shape_list -from src.config_manager import ConfigManager -from src.singleton_meta import Singleton - -logger = logging.getLogger("Keybinder") - -# disable lag -pydirectinput.PAUSE = 0 -pydirectinput.FAILSAFE = False - - -class Keybinder(metaclass=Singleton): - - def __init__(self) -> None: - self.delay_count = None - self.key_states = None - self.monitors = None - self.screen_h = None - self.screen_w = None - logger.info("Initialize Keybinder singleton") - self.top_count = 0 - self.triggered = False - self.start_hold_ts = math.inf - self.holding = False - self.is_started = False - self.last_know_keybindings = {} - self.is_active = None - - def start(self): - if not self.is_started: - logger.info("Start Keybinder singleton") - self.init_states() - self.screen_w, self.screen_h = pydirectinput.size() - self.monitors = self.get_monitors() - self.is_started = True - - self.is_active = tk.BooleanVar() - self.is_active.set(ConfigManager().config["auto_play"]) - - def init_states(self) -> None: - """Re-initializes the state of the keybinder. - If new keybindings are added. - """ - # keep states for all registered keys. - self.key_states = {} - for _, v in (ConfigManager().mouse_bindings | - ConfigManager().keyboard_bindings).items(): - self.key_states[v[0] + "_" + v[1]] = False - self.key_states["holding"] = False - self.last_know_keybindings = copy.deepcopy( - (ConfigManager().mouse_bindings | - ConfigManager().keyboard_bindings)) - - def get_monitors(self) -> list[dict]: - out_list = [] - monitors = win32api.EnumDisplayMonitors() - for i, (_, _, loc) in enumerate(monitors): - mon_info = {} - mon_info["id"] = i - mon_info["x1"] = loc[0] - mon_info["y1"] = loc[1] - mon_info["x2"] = loc[2] - mon_info["y2"] = loc[3] - mon_info["center_x"] = (loc[0] + loc[2]) // 2 - mon_info["center_y"] = (loc[1] + loc[3]) // 2 - out_list.append(mon_info) - - return out_list - - def get_current_monitor(self) -> int: - - x, y = pydirectinput.position() - for mon_id, mon in enumerate(self.monitors): - if x >= mon["x1"] and x <= mon["x2"] and y >= mon[ - "y1"] and y <= mon["y2"]: - return mon_id - # raise Exception("Monitor not found") - return 0 - - def mouse_action(self, val, action, threshold, mode) -> None: - state_name = "mouse_" + action - - mode = "hold" if self.key_states["holding"] else "single" - - if mode == "hold": - if (val > threshold) and (self.key_states[state_name] is False): - pydirectinput.mouseDown(action) - - self.key_states[state_name] = True - - elif (val < threshold) and (self.key_states[state_name] is True): - pydirectinput.mouseUp(action) - self.key_states[state_name] = False - - elif mode == "single": - if val > threshold: - if not self.key_states[state_name]: - pydirectinput.click(button=action) - self.start_hold_ts = time.time() - - self.key_states[state_name] = True - - if not self.holding and ( - ((time.time() - self.start_hold_ts) * 1000) >= - ConfigManager().config["hold_trigger_ms"]): - - pydirectinput.mouseDown(button=action) - self.holding = True - - elif (val < threshold) and (self.key_states[state_name] is True): - - self.key_states[state_name] = False - - if self.holding: - pydirectinput.mouseUp(button=action) - self.holding = False - self.start_hold_ts = math.inf - - def keyboard_action(self, val, keysym, threshold, mode): - - state_name = "keyboard_" + keysym - - if (self.key_states[state_name] is False) and (val > threshold): - pydirectinput.keyDown(keysym) - self.key_states[state_name] = True - - elif (self.key_states[state_name] is True) and (val < threshold): - pydirectinput.keyUp(keysym) - self.key_states[state_name] = False - - def act(self, blendshape_values) -> None: - """Trigger devices action base on blendshape values - - Args: - blendshape_values (npt.ArrayLike): blendshape values from tflite model - - Returns: - dict: debug states - """ - - if blendshape_values is None: - return - - if (ConfigManager().mouse_bindings | - ConfigManager().keyboard_bindings) != self.last_know_keybindings: - self.init_states() - - for shape_name, v in (ConfigManager().mouse_bindings | - ConfigManager().keyboard_bindings).items(): - if shape_name not in shape_list.blendshape_names: - continue - device, action, thres, mode = v - - # Get blendshape value - idx = shape_list.blendshape_indices[shape_name] - val = blendshape_values[idx] - - if (device == "mouse") and (action == "pause"): - state_name = "mouse_" + action - - if (val > thres) and (self.key_states[state_name] is False): - mon_id = self.get_current_monitor() - if mon_id is None: - return - - self.toggle_active() - - self.key_states[state_name] = True - elif (val < thres) and (self.key_states[state_name] is True): - self.key_states[state_name] = False - - elif self.is_active.get(): - - if device == "mouse": - - if action == "reset": - state_name = "mouse_" + action - if (val > thres) and (self.key_states[state_name] is - False): - mon_id = self.get_current_monitor() - if mon_id is None: - return - - pydirectinput.moveTo( - self.monitors[mon_id]["center_x"], - self.monitors[mon_id]["center_y"]) - self.key_states[state_name] = True - elif (val < thres) and (self.key_states[state_name] is - True): - self.key_states[state_name] = False - - elif action == "cycle": - state_name = "mouse_" + action - if (val > thres) and (self.key_states[state_name] is - False): - mon_id = self.get_current_monitor() - next_mon_id = (mon_id + 1) % len(self.monitors) - pydirectinput.moveTo( - self.monitors[next_mon_id]["center_x"], - self.monitors[next_mon_id]["center_y"]) - self.key_states[state_name] = True - elif (val < thres) and (self.key_states[state_name] is - True): - self.key_states[state_name] = False - - else: - self.mouse_action(val, action, thres, mode) - - elif device == "keyboard": - self.keyboard_action(val, action, thres, mode) - - def set_active(self, flag: bool) -> None: - self.is_active.set(flag) - if flag: - self.delay_count = 0 - - def toggle_active(self): - logging.info("Toggle active") - current_state = self.is_active.get() - self.set_active(not current_state) - - def destroy(self): - """Destroy the keybinder""" - return +import copy +import logging +import math +import time + +import pydirectinput +import win32api +import tkinter as tk + +import src.shape_list as shape_list +from src.config_manager import ConfigManager +from src.singleton_meta import Singleton +from src.utils.Trigger import Trigger + +logger = logging.getLogger("Keybinder") + +# disable lag +pydirectinput.PAUSE = 0 +pydirectinput.FAILSAFE = False + + +class Keybinder(metaclass=Singleton): + + def __init__(self) -> None: + self.delay_count = None + self.key_states = None + self.schedule_state_change = None + self.monitors = None + self.screen_h = None + self.screen_w = None + logger.info("Initialize Keybinder singleton") + self.top_count = 0 + self.triggered = False + self.start_hold_ts = math.inf + self.holding = False + self.is_started = False + self.last_know_keybindings = {} + self.is_active = None + + def start(self): + if not self.is_started: + logger.info("Start Keybinder singleton") + self.init_states() + self.screen_w, self.screen_h = pydirectinput.size() + self.monitors = self.get_monitors() + self.is_started = True + + self.is_active = tk.BooleanVar() + self.is_active.set(ConfigManager().config["auto_play"]) + + def init_states(self) -> None: + """Re-initializes the state of the keybinder. + If new keybindings are added. + """ + # keep states for all registered keys. + self.key_states = {} + for _, v in (ConfigManager().mouse_bindings | + ConfigManager().keyboard_bindings).items(): + self.key_states[v[0] + "_" + v[1]] = False + self.schedule_state_change[v[0] + "_" + v[1]] = False + self.key_states["holding"] = False + self.last_know_keybindings = copy.deepcopy( + (ConfigManager().mouse_bindings | + ConfigManager().keyboard_bindings)) + + def get_monitors(self) -> list[dict]: + out_list = [] + monitors = win32api.EnumDisplayMonitors() + for i, (_, _, loc) in enumerate(monitors): + mon_info = {} + mon_info["id"] = i + mon_info["x1"] = loc[0] + mon_info["y1"] = loc[1] + mon_info["x2"] = loc[2] + mon_info["y2"] = loc[3] + mon_info["center_x"] = (loc[0] + loc[2]) // 2 + mon_info["center_y"] = (loc[1] + loc[3]) // 2 + out_list.append(mon_info) + + return out_list + + def get_current_monitor(self) -> int: + + x, y = pydirectinput.position() + for mon_id, mon in enumerate(self.monitors): + if x >= mon["x1"] and x <= mon["x2"] and y >= mon[ + "y1"] and y <= mon["y2"]: + return mon_id + # raise Exception("Monitor not found") + return 0 + + def mouse_action(self, val, action, threshold, mode) -> None: + state_name = "mouse_" + action + + mode = "hold" if self.key_states["holding"] else "single" + + if mode == Trigger.TOGGLE.value: + if (val > threshold) and (self.key_states[state_name] is False): + pydirectinput.mouseDown(action) + self.key_states[state_name] = True + if (val > threshold) and (self.key_states[state_name] is True): + if self.schedule_state_change[state_name] is True: + pydirectinput.mouseUp(action) + self.schedule_state_change[state_name] = False + self.key_states[state_name] = False + + if (val < threshold) and (self.key_states[state_name] is True): + self.schedule_state_change[state_name] = True + + if mode == Trigger.HOLD.value: + if (val > threshold) and (self.key_states[state_name] is False): + pydirectinput.mouseDown(action) + self.key_states[state_name] = True + + elif (val < threshold) and (self.key_states[state_name] is True): + pydirectinput.mouseUp(action) + self.key_states[state_name] = False + + elif mode == Trigger.SINGLE.value: + if val > threshold: + if self.key_states[state_name] is False: + pydirectinput.click(button=action) + self.start_hold_ts = time.time() + + self.key_states[state_name] = True + + if not self.holding and ( + ((time.time() - self.start_hold_ts) * 1000) >= + ConfigManager().config["hold_trigger_ms"]): + + pydirectinput.mouseDown(button=action) + self.holding = True + + elif (val < threshold) and (self.key_states[state_name] is True): + + self.key_states[state_name] = False + + if self.holding: + pydirectinput.mouseUp(button=action) + self.holding = False + self.start_hold_ts = math.inf + + def keyboard_action(self, val, keysym, threshold, mode): + + state_name = "keyboard_" + keysym + + if (self.key_states[state_name] is False) and (val > threshold): + pydirectinput.keyDown(keysym) + self.key_states[state_name] = True + + elif (self.key_states[state_name] is True) and (val < threshold): + pydirectinput.keyUp(keysym) + self.key_states[state_name] = False + + def act(self, blendshape_values) -> None: + """Trigger devices action base on blendshape values + + Args: + blendshape_values (npt.ArrayLike): blendshape values from tflite model + + Returns: + dict: debug states + """ + + if blendshape_values is None: + return + + if (ConfigManager().mouse_bindings | + ConfigManager().keyboard_bindings) != self.last_know_keybindings: + self.init_states() + + for shape_name, v in (ConfigManager().mouse_bindings | + ConfigManager().keyboard_bindings).items(): + if shape_name not in shape_list.blendshape_names: + continue + device, action, thres, mode = v + + # Get blendshape value + idx = shape_list.blendshape_indices[shape_name] + val = blendshape_values[idx] + + if (device == "mouse") and (action == "pause"): + state_name = "mouse_" + action + + if (val > thres) and (self.key_states[state_name] is False): + mon_id = self.get_current_monitor() + if mon_id is None: + return + + self.toggle_active() + + self.key_states[state_name] = True + elif (val < thres) and (self.key_states[state_name] is True): + self.key_states[state_name] = False + + elif self.is_active.get(): + + if device == "mouse": + + if action == "reset": + state_name = "mouse_" + action + if (val > thres) and (self.key_states[state_name] is + False): + mon_id = self.get_current_monitor() + if mon_id is None: + return + + pydirectinput.moveTo( + self.monitors[mon_id]["center_x"], + self.monitors[mon_id]["center_y"]) + self.key_states[state_name] = True + elif (val < thres) and (self.key_states[state_name] is + True): + self.key_states[state_name] = False + + elif action == "cycle": + state_name = "mouse_" + action + if (val > thres) and (self.key_states[state_name] is + False): + mon_id = self.get_current_monitor() + next_mon_id = (mon_id + 1) % len(self.monitors) + pydirectinput.moveTo( + self.monitors[next_mon_id]["center_x"], + self.monitors[next_mon_id]["center_y"]) + self.key_states[state_name] = True + elif (val < thres) and (self.key_states[state_name] is + True): + self.key_states[state_name] = False + + else: + self.mouse_action(val, action, thres, mode) + + elif device == "keyboard": + self.keyboard_action(val, action, thres, mode) + + def set_active(self, flag: bool) -> None: + self.is_active.set(flag) + if flag: + self.delay_count = 0 + + def toggle_active(self): + logging.info("Toggle active") + current_state = self.is_active.get() + self.set_active(not current_state) + + def destroy(self): + """Destroy the keybinder""" + return diff --git a/src/utils/Trigger.py b/src/utils/Trigger.py new file mode 100644 index 00000000..19f6a571 --- /dev/null +++ b/src/utils/Trigger.py @@ -0,0 +1,7 @@ +from enum import Enum + + +class Trigger(Enum): + SINGLE = "single" + HOLD = "hold" + TOGGLE = "toggle" From bceb94b34998153dcd165e47b0639a790185a826 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 29 Jan 2024 22:27:41 +0100 Subject: [PATCH 094/123] refactor to use enum --- src/controllers/keybinder.py | 44 +++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/controllers/keybinder.py b/src/controllers/keybinder.py index 45999444..9e9b0a4f 100644 --- a/src/controllers/keybinder.py +++ b/src/controllers/keybinder.py @@ -84,7 +84,7 @@ def get_current_monitor(self) -> int: x, y = pydirectinput.position() for mon_id, mon in enumerate(self.monitors): if x >= mon["x1"] and x <= mon["x2"] and y >= mon[ - "y1"] and y <= mon["y2"]: + "y1"] and y <= mon["y2"]: return mon_id # raise Exception("Monitor not found") return 0 @@ -92,22 +92,10 @@ def get_current_monitor(self) -> int: def mouse_action(self, val, action, threshold, mode) -> None: state_name = "mouse_" + action - mode = "hold" if self.key_states["holding"] else "single" + # TODO: Figure out why this is always set to single + mode = Trigger.HOLD if self.key_states["holding"] else Trigger.SINGLE - if mode == Trigger.TOGGLE.value: - if (val > threshold) and (self.key_states[state_name] is False): - pydirectinput.mouseDown(action) - self.key_states[state_name] = True - if (val > threshold) and (self.key_states[state_name] is True): - if self.schedule_state_change[state_name] is True: - pydirectinput.mouseUp(action) - self.schedule_state_change[state_name] = False - self.key_states[state_name] = False - - if (val < threshold) and (self.key_states[state_name] is True): - self.schedule_state_change[state_name] = True - - if mode == Trigger.HOLD.value: + if mode == Trigger.HOLD: if (val > threshold) and (self.key_states[state_name] is False): pydirectinput.mouseDown(action) self.key_states[state_name] = True @@ -116,7 +104,7 @@ def mouse_action(self, val, action, threshold, mode) -> None: pydirectinput.mouseUp(action) self.key_states[state_name] = False - elif mode == Trigger.SINGLE.value: + elif mode == Trigger.SINGLE: if val > threshold: if self.key_states[state_name] is False: pydirectinput.click(button=action) @@ -125,9 +113,8 @@ def mouse_action(self, val, action, threshold, mode) -> None: self.key_states[state_name] = True if not self.holding and ( - ((time.time() - self.start_hold_ts) * 1000) >= + ((time.time() - self.start_hold_ts) * 1000) >= ConfigManager().config["hold_trigger_ms"]): - pydirectinput.mouseDown(button=action) self.holding = True @@ -140,6 +127,20 @@ def mouse_action(self, val, action, threshold, mode) -> None: self.holding = False self.start_hold_ts = math.inf + elif mode == Trigger.TOGGLE: + if (val > threshold) and (self.key_states[state_name] is False): + pydirectinput.mouseDown(action) + self.key_states[state_name] = True + if (val > threshold) and (self.key_states[state_name] is True): + if self.schedule_state_change[state_name] is True: + pydirectinput.mouseUp(action) + self.schedule_state_change[state_name] = False + self.key_states[state_name] = False + + if (val < threshold) and (self.key_states[state_name] is True): + self.schedule_state_change[state_name] = True + + def keyboard_action(self, val, keysym, threshold, mode): state_name = "keyboard_" + keysym @@ -166,15 +167,16 @@ def act(self, blendshape_values) -> None: return if (ConfigManager().mouse_bindings | - ConfigManager().keyboard_bindings) != self.last_know_keybindings: + ConfigManager().keyboard_bindings) != self.last_know_keybindings: self.init_states() for shape_name, v in (ConfigManager().mouse_bindings | ConfigManager().keyboard_bindings).items(): if shape_name not in shape_list.blendshape_names: continue - device, action, thres, mode = v + device, action, thres, mode = v + mode = Trigger(mode.lower()) # Get blendshape value idx = shape_list.blendshape_indices[shape_name] val = blendshape_values[idx] From 6cec59f83511d04a7ec6b096e4f5be0c183a0907 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 29 Jan 2024 22:28:51 +0100 Subject: [PATCH 095/123] add stub for new trigger type --- src/controllers/keybinder.py | 3 +++ src/utils/Trigger.py | 1 + 2 files changed, 4 insertions(+) diff --git a/src/controllers/keybinder.py b/src/controllers/keybinder.py index 9e9b0a4f..81734736 100644 --- a/src/controllers/keybinder.py +++ b/src/controllers/keybinder.py @@ -140,6 +140,9 @@ def mouse_action(self, val, action, threshold, mode) -> None: if (val < threshold) and (self.key_states[state_name] is True): self.schedule_state_change[state_name] = True + elif mode == Trigger.RAPID: + pass + def keyboard_action(self, val, keysym, threshold, mode): diff --git a/src/utils/Trigger.py b/src/utils/Trigger.py index 19f6a571..c3148f79 100644 --- a/src/utils/Trigger.py +++ b/src/utils/Trigger.py @@ -2,6 +2,7 @@ class Trigger(Enum): + RAPID = "rapid" SINGLE = "single" HOLD = "hold" TOGGLE = "toggle" From 74ebd8f1cd539cc6644672dfa03eee72cca7bc53 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Thu, 15 Feb 2024 16:34:19 +0100 Subject: [PATCH 096/123] Make single trigger work properly. Rename old single trigger to Dynamic, since it would switch between single click and holding it --- src/controllers/keybinder.py | 18 ++++++++++++------ src/utils/Trigger.py | 1 + 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/controllers/keybinder.py b/src/controllers/keybinder.py index 81734736..74e5d8f2 100644 --- a/src/controllers/keybinder.py +++ b/src/controllers/keybinder.py @@ -92,10 +92,17 @@ def get_current_monitor(self) -> int: def mouse_action(self, val, action, threshold, mode) -> None: state_name = "mouse_" + action - # TODO: Figure out why this is always set to single - mode = Trigger.HOLD if self.key_states["holding"] else Trigger.SINGLE + # TODO: Figure out why this is always set to single. + mode = Trigger.HOLD if self.key_states["holding"] else Trigger.DYNAMIC - if mode == Trigger.HOLD: + if mode == Trigger.SINGLE: + if val > threshold: + if self.key_states[state_name] is False: + pydirectinput.click(button=action) + self.start_hold_ts = time.time() + self.key_states[state_name] = True + + elif mode == Trigger.HOLD: if (val > threshold) and (self.key_states[state_name] is False): pydirectinput.mouseDown(action) self.key_states[state_name] = True @@ -104,13 +111,12 @@ def mouse_action(self, val, action, threshold, mode) -> None: pydirectinput.mouseUp(action) self.key_states[state_name] = False - elif mode == Trigger.SINGLE: + elif mode == Trigger.DYNAMIC: if val > threshold: if self.key_states[state_name] is False: pydirectinput.click(button=action) self.start_hold_ts = time.time() - - self.key_states[state_name] = True + self.key_states[state_name] = True if not self.holding and ( ((time.time() - self.start_hold_ts) * 1000) >= diff --git a/src/utils/Trigger.py b/src/utils/Trigger.py index c3148f79..8479d07f 100644 --- a/src/utils/Trigger.py +++ b/src/utils/Trigger.py @@ -6,3 +6,4 @@ class Trigger(Enum): SINGLE = "single" HOLD = "hold" TOGGLE = "toggle" + DYNAMIC = "dynamic" From 2099d4be16d955b7f4faca6f56255aa863ba01cc Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sat, 17 Feb 2024 17:13:53 +0100 Subject: [PATCH 097/123] add config entry for new Triggertype --- configs/default/cursor.json | 3 ++- configs/profile_1/cursor.json | 3 ++- configs/profile_2/cursor.json | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/configs/default/cursor.json b/configs/default/cursor.json index 89c21650..8104dbe7 100644 --- a/configs/default/cursor.json +++ b/configs/default/cursor.json @@ -12,7 +12,8 @@ "pointer_smooth": 6, "shape_smooth": 10, "tick_interval_ms": 16, - "hold_trigger_ms": 500, + "hold_trigger_ms": 500, + "rapid_fire_delay": 500, "auto_play": false, "enable": 1, "mouse_acceleration": false, diff --git a/configs/profile_1/cursor.json b/configs/profile_1/cursor.json index 52c368b6..938f42a1 100644 --- a/configs/profile_1/cursor.json +++ b/configs/profile_1/cursor.json @@ -12,7 +12,8 @@ "pointer_smooth": 6, "shape_smooth": 10, "tick_interval_ms": 16, - "hold_trigger_ms": 500, + "hold_trigger_ms": 500, + "rapid_fire_delay": 500, "auto_play": false, "enable": 1, "mouse_acceleration": false, diff --git a/configs/profile_2/cursor.json b/configs/profile_2/cursor.json index e53fadec..f045cc3e 100644 --- a/configs/profile_2/cursor.json +++ b/configs/profile_2/cursor.json @@ -12,7 +12,8 @@ "pointer_smooth": 15, "shape_smooth": 10, "tick_interval_ms": 16, - "hold_trigger_ms": 500, + "hold_trigger_ms": 500, + "rapid_fire_delay": 500, "auto_play": false, "enable": 1, "mouse_acceleration": false, From 69553e57e493a8e92d7ed5630f8e4e88575875ca Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sat, 17 Feb 2024 17:17:48 +0100 Subject: [PATCH 098/123] minor refactoring --- src/controllers/keybinder.py | 523 ++++++++++++++++++----------------- 1 file changed, 264 insertions(+), 259 deletions(-) diff --git a/src/controllers/keybinder.py b/src/controllers/keybinder.py index 74e5d8f2..7cabaacc 100644 --- a/src/controllers/keybinder.py +++ b/src/controllers/keybinder.py @@ -1,259 +1,264 @@ -import copy -import logging -import math -import time - -import pydirectinput -import win32api -import tkinter as tk - -import src.shape_list as shape_list -from src.config_manager import ConfigManager -from src.singleton_meta import Singleton -from src.utils.Trigger import Trigger - -logger = logging.getLogger("Keybinder") - -# disable lag -pydirectinput.PAUSE = 0 -pydirectinput.FAILSAFE = False - - -class Keybinder(metaclass=Singleton): - - def __init__(self) -> None: - self.delay_count = None - self.key_states = None - self.schedule_state_change = None - self.monitors = None - self.screen_h = None - self.screen_w = None - logger.info("Initialize Keybinder singleton") - self.top_count = 0 - self.triggered = False - self.start_hold_ts = math.inf - self.holding = False - self.is_started = False - self.last_know_keybindings = {} - self.is_active = None - - def start(self): - if not self.is_started: - logger.info("Start Keybinder singleton") - self.init_states() - self.screen_w, self.screen_h = pydirectinput.size() - self.monitors = self.get_monitors() - self.is_started = True - - self.is_active = tk.BooleanVar() - self.is_active.set(ConfigManager().config["auto_play"]) - - def init_states(self) -> None: - """Re-initializes the state of the keybinder. - If new keybindings are added. - """ - # keep states for all registered keys. - self.key_states = {} - for _, v in (ConfigManager().mouse_bindings | - ConfigManager().keyboard_bindings).items(): - self.key_states[v[0] + "_" + v[1]] = False - self.schedule_state_change[v[0] + "_" + v[1]] = False - self.key_states["holding"] = False - self.last_know_keybindings = copy.deepcopy( - (ConfigManager().mouse_bindings | - ConfigManager().keyboard_bindings)) - - def get_monitors(self) -> list[dict]: - out_list = [] - monitors = win32api.EnumDisplayMonitors() - for i, (_, _, loc) in enumerate(monitors): - mon_info = {} - mon_info["id"] = i - mon_info["x1"] = loc[0] - mon_info["y1"] = loc[1] - mon_info["x2"] = loc[2] - mon_info["y2"] = loc[3] - mon_info["center_x"] = (loc[0] + loc[2]) // 2 - mon_info["center_y"] = (loc[1] + loc[3]) // 2 - out_list.append(mon_info) - - return out_list - - def get_current_monitor(self) -> int: - - x, y = pydirectinput.position() - for mon_id, mon in enumerate(self.monitors): - if x >= mon["x1"] and x <= mon["x2"] and y >= mon[ - "y1"] and y <= mon["y2"]: - return mon_id - # raise Exception("Monitor not found") - return 0 - - def mouse_action(self, val, action, threshold, mode) -> None: - state_name = "mouse_" + action - - # TODO: Figure out why this is always set to single. - mode = Trigger.HOLD if self.key_states["holding"] else Trigger.DYNAMIC - - if mode == Trigger.SINGLE: - if val > threshold: - if self.key_states[state_name] is False: - pydirectinput.click(button=action) - self.start_hold_ts = time.time() - self.key_states[state_name] = True - - elif mode == Trigger.HOLD: - if (val > threshold) and (self.key_states[state_name] is False): - pydirectinput.mouseDown(action) - self.key_states[state_name] = True - - elif (val < threshold) and (self.key_states[state_name] is True): - pydirectinput.mouseUp(action) - self.key_states[state_name] = False - - elif mode == Trigger.DYNAMIC: - if val > threshold: - if self.key_states[state_name] is False: - pydirectinput.click(button=action) - self.start_hold_ts = time.time() - self.key_states[state_name] = True - - if not self.holding and ( - ((time.time() - self.start_hold_ts) * 1000) >= - ConfigManager().config["hold_trigger_ms"]): - pydirectinput.mouseDown(button=action) - self.holding = True - - elif (val < threshold) and (self.key_states[state_name] is True): - - self.key_states[state_name] = False - - if self.holding: - pydirectinput.mouseUp(button=action) - self.holding = False - self.start_hold_ts = math.inf - - elif mode == Trigger.TOGGLE: - if (val > threshold) and (self.key_states[state_name] is False): - pydirectinput.mouseDown(action) - self.key_states[state_name] = True - if (val > threshold) and (self.key_states[state_name] is True): - if self.schedule_state_change[state_name] is True: - pydirectinput.mouseUp(action) - self.schedule_state_change[state_name] = False - self.key_states[state_name] = False - - if (val < threshold) and (self.key_states[state_name] is True): - self.schedule_state_change[state_name] = True - - elif mode == Trigger.RAPID: - pass - - - def keyboard_action(self, val, keysym, threshold, mode): - - state_name = "keyboard_" + keysym - - if (self.key_states[state_name] is False) and (val > threshold): - pydirectinput.keyDown(keysym) - self.key_states[state_name] = True - - elif (self.key_states[state_name] is True) and (val < threshold): - pydirectinput.keyUp(keysym) - self.key_states[state_name] = False - - def act(self, blendshape_values) -> None: - """Trigger devices action base on blendshape values - - Args: - blendshape_values (npt.ArrayLike): blendshape values from tflite model - - Returns: - dict: debug states - """ - - if blendshape_values is None: - return - - if (ConfigManager().mouse_bindings | - ConfigManager().keyboard_bindings) != self.last_know_keybindings: - self.init_states() - - for shape_name, v in (ConfigManager().mouse_bindings | - ConfigManager().keyboard_bindings).items(): - if shape_name not in shape_list.blendshape_names: - continue - - device, action, thres, mode = v - mode = Trigger(mode.lower()) - # Get blendshape value - idx = shape_list.blendshape_indices[shape_name] - val = blendshape_values[idx] - - if (device == "mouse") and (action == "pause"): - state_name = "mouse_" + action - - if (val > thres) and (self.key_states[state_name] is False): - mon_id = self.get_current_monitor() - if mon_id is None: - return - - self.toggle_active() - - self.key_states[state_name] = True - elif (val < thres) and (self.key_states[state_name] is True): - self.key_states[state_name] = False - - elif self.is_active.get(): - - if device == "mouse": - - if action == "reset": - state_name = "mouse_" + action - if (val > thres) and (self.key_states[state_name] is - False): - mon_id = self.get_current_monitor() - if mon_id is None: - return - - pydirectinput.moveTo( - self.monitors[mon_id]["center_x"], - self.monitors[mon_id]["center_y"]) - self.key_states[state_name] = True - elif (val < thres) and (self.key_states[state_name] is - True): - self.key_states[state_name] = False - - elif action == "cycle": - state_name = "mouse_" + action - if (val > thres) and (self.key_states[state_name] is - False): - mon_id = self.get_current_monitor() - next_mon_id = (mon_id + 1) % len(self.monitors) - pydirectinput.moveTo( - self.monitors[next_mon_id]["center_x"], - self.monitors[next_mon_id]["center_y"]) - self.key_states[state_name] = True - elif (val < thres) and (self.key_states[state_name] is - True): - self.key_states[state_name] = False - - else: - self.mouse_action(val, action, thres, mode) - - elif device == "keyboard": - self.keyboard_action(val, action, thres, mode) - - def set_active(self, flag: bool) -> None: - self.is_active.set(flag) - if flag: - self.delay_count = 0 - - def toggle_active(self): - logging.info("Toggle active") - current_state = self.is_active.get() - self.set_active(not current_state) - - def destroy(self): - """Destroy the keybinder""" - return +import copy +import logging +import math +import time + +import pydirectinput +import win32api +import tkinter as tk + +import src.shape_list as shape_list +from src.config_manager import ConfigManager +from src.singleton_meta import Singleton +from src.utils.Trigger import Trigger + +logger = logging.getLogger("Keybinder") + +# disable lag +pydirectinput.PAUSE = 0 +pydirectinput.FAILSAFE = False + + +class Keybinder(metaclass=Singleton): + + def __init__(self) -> None: + self.delay_count = None + self.key_states = None + self.schedule_state_change = {} + self.monitors = None + self.screen_h = None + self.screen_w = None + logger.info("Initialize Keybinder singleton") + self.top_count = 0 + self.start_hold_ts ={} + self.holding = {} + self.is_started = False + self.last_know_keybindings = {} + self.is_active = None + + def start(self): + if not self.is_started: + logger.info("Start Keybinder singleton") + self.init_states() + self.screen_w, self.screen_h = pydirectinput.size() + self.monitors = self.get_monitors() + self.is_started = True + + self.is_active = tk.BooleanVar() + self.is_active.set(ConfigManager().config["auto_play"]) + + def init_states(self) -> None: + """Re-initializes the state of the keybinder. + If new keybindings are added. + """ + # keep states for all registered keys. + self.key_states = {} + self.start_hold_ts = {} + for _, v in (ConfigManager().mouse_bindings | + ConfigManager().keyboard_bindings).items(): + state_name = v[0]+"_"+v[1] + self.key_states[state_name] = False + self.schedule_state_change[state_name] = False + self.start_hold_ts[state_name] = math.inf + self.holding[state_name] = False + + self.key_states["holding"] = False + self.last_know_keybindings = copy.deepcopy( + (ConfigManager().mouse_bindings | + ConfigManager().keyboard_bindings)) + + def get_monitors(self) -> list[dict]: + out_list = [] + monitors = win32api.EnumDisplayMonitors() + for i, (_, _, loc) in enumerate(monitors): + mon_info = {} + mon_info["id"] = i + mon_info["x1"] = loc[0] + mon_info["y1"] = loc[1] + mon_info["x2"] = loc[2] + mon_info["y2"] = loc[3] + mon_info["center_x"] = (loc[0] + loc[2]) // 2 + mon_info["center_y"] = (loc[1] + loc[3]) // 2 + out_list.append(mon_info) + + return out_list + + def get_current_monitor(self) -> int: + + x, y = pydirectinput.position() + for mon_id, mon in enumerate(self.monitors): + if x >= mon["x1"] and x <= mon["x2"] and y >= mon[ + "y1"] and y <= mon["y2"]: + return mon_id + # raise Exception("Monitor not found") + return 0 + + def mouse_action(self, val, action, threshold, mode) -> None: + state_name = "mouse_" + action + + # TODO: Figure out why this is always set to single. + mode = Trigger.HOLD if self.key_states["holding"] else Trigger.DYNAMIC + + if mode == Trigger.SINGLE: + if val > threshold: + if self.key_states[state_name] is False: + pydirectinput.click(button=action) + self.key_states[state_name] = True + if val < threshold: + self.key_states[state_name] = False + + elif mode == Trigger.HOLD: + if (val > threshold) and (self.key_states[state_name] is False): + pydirectinput.mouseDown(action) + self.key_states[state_name] = True + + elif (val < threshold) and (self.key_states[state_name] is True): + pydirectinput.mouseUp(action) + self.key_states[state_name] = False + + elif mode == Trigger.DYNAMIC: + if val > threshold: + if self.key_states[state_name] is False: + pydirectinput.click(button=action) + self.start_hold_ts = time.time() + self.key_states[state_name] = True + + if self.holding[state_name] is False and ( + ((time.time() - self.start_hold_ts[state_name]) * 1000) >= + ConfigManager().config["hold_trigger_ms"]): + pydirectinput.mouseDown(button=action) + self.holding[state_name] = True + + elif (val < threshold) and (self.key_states[state_name] is True): + + self.key_states[state_name] = False + + if self.holding[state_name]: + pydirectinput.mouseUp(button=action) + self.holding[state_name] = False + self.start_hold_ts[state_name] = math.inf + + elif mode == Trigger.TOGGLE: + if (val > threshold) and (self.key_states[state_name] is False): + pydirectinput.mouseDown(action) + self.key_states[state_name] = True + if (val > threshold) and (self.key_states[state_name] is True): + if self.schedule_state_change[state_name] is True: + pydirectinput.mouseUp(action) + self.schedule_state_change[state_name] = False + self.key_states[state_name] = False + + if (val < threshold) and (self.key_states[state_name] is True): + self.schedule_state_change[state_name] = True + + elif mode == Trigger.RAPID: + pass + + + def keyboard_action(self, val, keysym, threshold, mode): + + state_name = "keyboard_" + keysym + + if (self.key_states[state_name] is False) and (val > threshold): + pydirectinput.keyDown(keysym) + self.key_states[state_name] = True + + elif (self.key_states[state_name] is True) and (val < threshold): + pydirectinput.keyUp(keysym) + self.key_states[state_name] = False + + def act(self, blendshape_values) -> None: + """Trigger devices action base on blendshape values + + Args: + blendshape_values (npt.ArrayLike): blendshape values from tflite model + + Returns: + dict: debug states + """ + + if blendshape_values is None: + return + + if (ConfigManager().mouse_bindings | + ConfigManager().keyboard_bindings) != self.last_know_keybindings: + self.init_states() + + for shape_name, v in (ConfigManager().mouse_bindings | + ConfigManager().keyboard_bindings).items(): + if shape_name not in shape_list.blendshape_names: + continue + + device, action, thres, mode = v + mode = Trigger(mode.lower()) + # Get blendshape value + idx = shape_list.blendshape_indices[shape_name] + val = blendshape_values[idx] + + if (device == "mouse") and (action == "pause"): + state_name = "mouse_" + action + + if (val > thres) and (self.key_states[state_name] is False): + mon_id = self.get_current_monitor() + if mon_id is None: + return + + self.toggle_active() + + self.key_states[state_name] = True + elif (val < thres) and (self.key_states[state_name] is True): + self.key_states[state_name] = False + + elif self.is_active.get(): + + if device == "mouse": + + if action == "reset": + state_name = "mouse_" + action + if (val > thres) and (self.key_states[state_name] is + False): + mon_id = self.get_current_monitor() + if mon_id is None: + return + + pydirectinput.moveTo( + self.monitors[mon_id]["center_x"], + self.monitors[mon_id]["center_y"]) + self.key_states[state_name] = True + elif (val < thres) and (self.key_states[state_name] is + True): + self.key_states[state_name] = False + + elif action == "cycle": + state_name = "mouse_" + action + if (val > thres) and (self.key_states[state_name] is + False): + mon_id = self.get_current_monitor() + next_mon_id = (mon_id + 1) % len(self.monitors) + pydirectinput.moveTo( + self.monitors[next_mon_id]["center_x"], + self.monitors[next_mon_id]["center_y"]) + self.key_states[state_name] = True + elif (val < thres) and (self.key_states[state_name] is + True): + self.key_states[state_name] = False + + else: + self.mouse_action(val, action, thres, mode) + + elif device == "keyboard": + self.keyboard_action(val, action, thres, mode) + + def set_active(self, flag: bool) -> None: + self.is_active.set(flag) + if flag: + self.delay_count = 0 + + def toggle_active(self): + logging.info("Toggle active") + current_state = self.is_active.get() + self.set_active(not current_state) + + def destroy(self): + """Destroy the keybinder""" + return From 45360c9d8ad73d53dc17ceb20dc2b2f4262538ba Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sat, 17 Feb 2024 17:18:52 +0100 Subject: [PATCH 099/123] implement rapid fire mode --- src/controllers/keybinder.py | 17 ++++++++++++++++- src/gui/pages/page_cursor.py | 5 +++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/controllers/keybinder.py b/src/controllers/keybinder.py index 7cabaacc..6589642a 100644 --- a/src/controllers/keybinder.py +++ b/src/controllers/keybinder.py @@ -152,8 +152,23 @@ def mouse_action(self, val, action, threshold, mode) -> None: self.schedule_state_change[state_name] = True elif mode == Trigger.RAPID: - pass + if val > threshold: + if self.key_states[state_name] is False: + pydirectinput.click(button=action) + self.key_states[state_name] = True + self.start_hold_ts[state_name] = time.time() + if self.key_states[state_name] is True: + if (((time.time() - self.start_hold_ts[state_name]) * 1000) + >= ConfigManager().config["rapid_fire_delay"]): + pydirectinput.click(button=action) + self.holding[state_name] = True + self.start_hold_ts[state_name] = time.time() + + if val < threshold: + if self.key_states[state_name] is True: + self.key_states[state_name] = False + self.start_hold_ts[state_name] = math.inf def keyboard_action(self, val, keysym, threshold, mode): diff --git a/src/gui/pages/page_cursor.py b/src/gui/pages/page_cursor.py index 776c8cc0..3dbc2f8b 100644 --- a/src/gui/pages/page_cursor.py +++ b/src/gui/pages/page_cursor.py @@ -55,6 +55,11 @@ def __init__( "hold_trigger_ms", "Controls how long the user should\nhold a gesture in milliseconds\nfor an action to trigger", 1, MAX_HOLD_TRIG + ], + "(Advanced) Rapid fire trigger delay(ms)": [ + "rapid_fire_delay", + "Controls how much time should pass\nbetween each individual\ntriggering of the action", + 1, MAX_HOLD_TRIG ] }) # Toggle label From b0260f19eb09291e60afeea8f17a921e4ebd90c5 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sun, 18 Feb 2024 18:55:03 +0100 Subject: [PATCH 100/123] Properly hard code trigger value which was hidden in a convoluted if-clause --- src/controllers/keybinder.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/controllers/keybinder.py b/src/controllers/keybinder.py index 6589642a..64b79d39 100644 --- a/src/controllers/keybinder.py +++ b/src/controllers/keybinder.py @@ -22,6 +22,7 @@ class Keybinder(metaclass=Singleton): def __init__(self) -> None: + self.forced_mode = None self.delay_count = None self.key_states = None self.schedule_state_change = {} @@ -62,7 +63,7 @@ def init_states(self) -> None: self.start_hold_ts[state_name] = math.inf self.holding[state_name] = False - self.key_states["holding"] = False + self.forced_mode = Trigger.DYNAMIC self.last_know_keybindings = copy.deepcopy( (ConfigManager().mouse_bindings | ConfigManager().keyboard_bindings)) @@ -96,8 +97,8 @@ def get_current_monitor(self) -> int: def mouse_action(self, val, action, threshold, mode) -> None: state_name = "mouse_" + action - # TODO: Figure out why this is always set to single. - mode = Trigger.HOLD if self.key_states["holding"] else Trigger.DYNAMIC + # TODO: Figure out why this is always set to dynamic. + mode = self.forced_mode if mode == Trigger.SINGLE: if val > threshold: @@ -120,7 +121,7 @@ def mouse_action(self, val, action, threshold, mode) -> None: if val > threshold: if self.key_states[state_name] is False: pydirectinput.click(button=action) - self.start_hold_ts = time.time() + self.start_hold_ts[state_name] = time.time() self.key_states[state_name] = True if self.holding[state_name] is False and ( From b0bbcf6083203868f409953345ec19e2bdb3ba60 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sun, 18 Feb 2024 21:43:35 +0100 Subject: [PATCH 101/123] implement trigger modes for keyboard actions --- src/controllers/keybinder.py | 80 ++++++++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 7 deletions(-) diff --git a/src/controllers/keybinder.py b/src/controllers/keybinder.py index 64b79d39..b7d4567e 100644 --- a/src/controllers/keybinder.py +++ b/src/controllers/keybinder.py @@ -97,7 +97,7 @@ def get_current_monitor(self) -> int: def mouse_action(self, val, action, threshold, mode) -> None: state_name = "mouse_" + action - # TODO: Figure out why this is always set to dynamic. + # TODO: un-hardcode this mode = self.forced_mode if mode == Trigger.SINGLE: @@ -175,13 +175,79 @@ def keyboard_action(self, val, keysym, threshold, mode): state_name = "keyboard_" + keysym - if (self.key_states[state_name] is False) and (val > threshold): - pydirectinput.keyDown(keysym) - self.key_states[state_name] = True + # TODO: un-hardcode this + mode = self.forced_mode - elif (self.key_states[state_name] is True) and (val < threshold): - pydirectinput.keyUp(keysym) - self.key_states[state_name] = False + if mode == Trigger.SINGLE: + if val > threshold: + if self.key_states[state_name] is False: + pydirectinput.press(button=keysym) + self.key_states[state_name] = True + if val < threshold: + self.key_states[state_name] = False + + elif mode == Trigger.HOLD: + if (val > threshold) and (self.key_states[state_name] is False): + pydirectinput.keyDown(keysym) + self.key_states[state_name] = True + + elif (val < threshold) and (self.key_states[state_name] is True): + pydirectinput.keyUp(keysym) + self.key_states[state_name] = False + + elif mode == Trigger.DYNAMIC: + if val > threshold: + if self.key_states[state_name] is False: + pydirectinput.press(button=keysym) + self.start_hold_ts[state_name] = time.time() + self.key_states[state_name] = True + + if self.holding[state_name] is False and ( + ((time.time() - self.start_hold_ts[state_name]) * 1000) >= + ConfigManager().config["hold_trigger_ms"]): + pydirectinput.keyDown(button=keysym) + self.holding[state_name] = True + + elif (val < threshold) and (self.key_states[state_name] is True): + + self.key_states[state_name] = False + + if self.holding[state_name]: + pydirectinput.keyUp(button=keysym) + self.holding[state_name] = False + self.start_hold_ts[state_name] = math.inf + + elif mode == Trigger.TOGGLE: + if (val > threshold) and (self.key_states[state_name] is False): + pydirectinput.keyDown(keysym) + self.key_states[state_name] = True + if (val > threshold) and (self.key_states[state_name] is True): + if self.schedule_state_change[state_name] is True: + pydirectinput.keyUp(keysym) + self.schedule_state_change[state_name] = False + self.key_states[state_name] = False + + if (val < threshold) and (self.key_states[state_name] is True): + self.schedule_state_change[state_name] = True + + elif mode == Trigger.RAPID: + if val > threshold: + if self.key_states[state_name] is False: + pydirectinput.press(button=keysym) + self.key_states[state_name] = True + self.start_hold_ts[state_name] = time.time() + + if self.key_states[state_name] is True: + if (((time.time() - self.start_hold_ts[state_name]) * 1000) + >= ConfigManager().config["rapid_fire_delay"]): + pydirectinput.press(button=keysym) + self.holding[state_name] = True + self.start_hold_ts[state_name] = time.time() + + if val < threshold: + if self.key_states[state_name] is True: + self.key_states[state_name] = False + self.start_hold_ts[state_name] = math.inf def act(self, blendshape_values) -> None: """Trigger devices action base on blendshape values From 6ea4ed0175af7393cb9ccbd5e600cb5ecef09369 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Thu, 22 Feb 2024 20:42:17 +0100 Subject: [PATCH 102/123] implement toggle for mouse buttons --- src/controllers/keybinder.py | 74 ++++++++++++++++++++---------------- src/gui/pages/page_cursor.py | 4 +- 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/src/controllers/keybinder.py b/src/controllers/keybinder.py index b7d4567e..9a7513cd 100644 --- a/src/controllers/keybinder.py +++ b/src/controllers/keybinder.py @@ -25,13 +25,14 @@ def __init__(self) -> None: self.forced_mode = None self.delay_count = None self.key_states = None - self.schedule_state_change = {} + self.schedule_toggle_off = {} + self.schedule_toggle_on = {} self.monitors = None self.screen_h = None self.screen_w = None logger.info("Initialize Keybinder singleton") self.top_count = 0 - self.start_hold_ts ={} + self.start_hold_ts = {} self.holding = {} self.is_started = False self.last_know_keybindings = {} @@ -59,7 +60,8 @@ def init_states(self) -> None: ConfigManager().keyboard_bindings).items(): state_name = v[0]+"_"+v[1] self.key_states[state_name] = False - self.schedule_state_change[state_name] = False + self.schedule_toggle_off[state_name] = False + self.schedule_toggle_on[state_name] = True self.start_hold_ts[state_name] = math.inf self.holding[state_name] = False @@ -98,7 +100,7 @@ def mouse_action(self, val, action, threshold, mode) -> None: state_name = "mouse_" + action # TODO: un-hardcode this - mode = self.forced_mode + #mode = self.forced_mode if mode == Trigger.SINGLE: if val > threshold: @@ -110,11 +112,11 @@ def mouse_action(self, val, action, threshold, mode) -> None: elif mode == Trigger.HOLD: if (val > threshold) and (self.key_states[state_name] is False): - pydirectinput.mouseDown(action) + pydirectinput.mouseDown(button=action) self.key_states[state_name] = True elif (val < threshold) and (self.key_states[state_name] is True): - pydirectinput.mouseUp(action) + pydirectinput.mouseUp(button=action) self.key_states[state_name] = False elif mode == Trigger.DYNAMIC: @@ -140,17 +142,25 @@ def mouse_action(self, val, action, threshold, mode) -> None: self.start_hold_ts[state_name] = math.inf elif mode == Trigger.TOGGLE: - if (val > threshold) and (self.key_states[state_name] is False): - pydirectinput.mouseDown(action) - self.key_states[state_name] = True - if (val > threshold) and (self.key_states[state_name] is True): - if self.schedule_state_change[state_name] is True: - pydirectinput.mouseUp(action) - self.schedule_state_change[state_name] = False - self.key_states[state_name] = False + if (val > threshold): + if self.key_states[state_name] is False: + if self.schedule_toggle_on[state_name] is True: + pydirectinput.mouseDown(button=action) + self.key_states[state_name] = True - if (val < threshold) and (self.key_states[state_name] is True): - self.schedule_state_change[state_name] = True + if self.key_states[state_name] is True: + if self.schedule_toggle_off[state_name] is True: + pydirectinput.mouseUp(button=action) + self.key_states[state_name] = False + + + if (val < threshold): + if self.key_states[state_name] is True: + self.schedule_toggle_off[state_name] = True + self.schedule_toggle_on[state_name] = False + if self.key_states[state_name] is False: + self.schedule_toggle_on[state_name] = True + self.schedule_toggle_off[state_name] = False elif mode == Trigger.RAPID: if val > threshold: @@ -161,7 +171,7 @@ def mouse_action(self, val, action, threshold, mode) -> None: if self.key_states[state_name] is True: if (((time.time() - self.start_hold_ts[state_name]) * 1000) - >= ConfigManager().config["rapid_fire_delay"]): + >= ConfigManager().config["rapid_fire_interval"]): pydirectinput.click(button=action) self.holding[state_name] = True self.start_hold_ts[state_name] = time.time() @@ -176,36 +186,36 @@ def keyboard_action(self, val, keysym, threshold, mode): state_name = "keyboard_" + keysym # TODO: un-hardcode this - mode = self.forced_mode + # mode = self.forced_mode if mode == Trigger.SINGLE: if val > threshold: if self.key_states[state_name] is False: - pydirectinput.press(button=keysym) + pydirectinput.press(keys=keysym) self.key_states[state_name] = True if val < threshold: self.key_states[state_name] = False elif mode == Trigger.HOLD: if (val > threshold) and (self.key_states[state_name] is False): - pydirectinput.keyDown(keysym) + pydirectinput.keyDown(key=keysym) self.key_states[state_name] = True elif (val < threshold) and (self.key_states[state_name] is True): - pydirectinput.keyUp(keysym) + pydirectinput.keyUp(key=keysym) self.key_states[state_name] = False elif mode == Trigger.DYNAMIC: if val > threshold: if self.key_states[state_name] is False: - pydirectinput.press(button=keysym) + pydirectinput.press(keys=keysym) self.start_hold_ts[state_name] = time.time() self.key_states[state_name] = True if self.holding[state_name] is False and ( ((time.time() - self.start_hold_ts[state_name]) * 1000) >= ConfigManager().config["hold_trigger_ms"]): - pydirectinput.keyDown(button=keysym) + pydirectinput.keyDown(key=keysym) self.holding[state_name] = True elif (val < threshold) and (self.key_states[state_name] is True): @@ -213,34 +223,34 @@ def keyboard_action(self, val, keysym, threshold, mode): self.key_states[state_name] = False if self.holding[state_name]: - pydirectinput.keyUp(button=keysym) + pydirectinput.keyUp(key=keysym) self.holding[state_name] = False self.start_hold_ts[state_name] = math.inf elif mode == Trigger.TOGGLE: if (val > threshold) and (self.key_states[state_name] is False): - pydirectinput.keyDown(keysym) + pydirectinput.keyDown(key=keysym) self.key_states[state_name] = True if (val > threshold) and (self.key_states[state_name] is True): - if self.schedule_state_change[state_name] is True: - pydirectinput.keyUp(keysym) - self.schedule_state_change[state_name] = False + if self.schedule_toggle_off[state_name] is True: + pydirectinput.keyUp(key=keysym) + self.schedule_toggle_off[state_name] = False self.key_states[state_name] = False if (val < threshold) and (self.key_states[state_name] is True): - self.schedule_state_change[state_name] = True + self.schedule_toggle_off[state_name] = True elif mode == Trigger.RAPID: if val > threshold: if self.key_states[state_name] is False: - pydirectinput.press(button=keysym) + pydirectinput.press(keys=keysym) self.key_states[state_name] = True self.start_hold_ts[state_name] = time.time() if self.key_states[state_name] is True: if (((time.time() - self.start_hold_ts[state_name]) * 1000) - >= ConfigManager().config["rapid_fire_delay"]): - pydirectinput.press(button=keysym) + >= ConfigManager().config["rapid_fire_interval"]): + pydirectinput.press(keys=keysym) self.holding[state_name] = True self.start_hold_ts[state_name] = time.time() diff --git a/src/gui/pages/page_cursor.py b/src/gui/pages/page_cursor.py index 3dbc2f8b..9b4229e1 100644 --- a/src/gui/pages/page_cursor.py +++ b/src/gui/pages/page_cursor.py @@ -56,8 +56,8 @@ def __init__( "Controls how long the user should\nhold a gesture in milliseconds\nfor an action to trigger", 1, MAX_HOLD_TRIG ], - "(Advanced) Rapid fire trigger delay(ms)": [ - "rapid_fire_delay", + "(Advanced) Rapid fire trigger interval(ms)": [ + "rapid_fire_interval", "Controls how much time should pass\nbetween each individual\ntriggering of the action", 1, MAX_HOLD_TRIG ] From 69f242516c353288e49402e9cea302760b9b9e63 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Thu, 22 Feb 2024 20:46:00 +0100 Subject: [PATCH 103/123] implement toggle for keyboard keys --- src/controllers/keybinder.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/controllers/keybinder.py b/src/controllers/keybinder.py index 9a7513cd..c0e0aafd 100644 --- a/src/controllers/keybinder.py +++ b/src/controllers/keybinder.py @@ -142,7 +142,7 @@ def mouse_action(self, val, action, threshold, mode) -> None: self.start_hold_ts[state_name] = math.inf elif mode == Trigger.TOGGLE: - if (val > threshold): + if val > threshold: if self.key_states[state_name] is False: if self.schedule_toggle_on[state_name] is True: pydirectinput.mouseDown(button=action) @@ -153,8 +153,7 @@ def mouse_action(self, val, action, threshold, mode) -> None: pydirectinput.mouseUp(button=action) self.key_states[state_name] = False - - if (val < threshold): + if val < threshold: if self.key_states[state_name] is True: self.schedule_toggle_off[state_name] = True self.schedule_toggle_on[state_name] = False @@ -228,17 +227,24 @@ def keyboard_action(self, val, keysym, threshold, mode): self.start_hold_ts[state_name] = math.inf elif mode == Trigger.TOGGLE: - if (val > threshold) and (self.key_states[state_name] is False): - pydirectinput.keyDown(key=keysym) - self.key_states[state_name] = True - if (val > threshold) and (self.key_states[state_name] is True): - if self.schedule_toggle_off[state_name] is True: - pydirectinput.keyUp(key=keysym) - self.schedule_toggle_off[state_name] = False - self.key_states[state_name] = False + if val > threshold: + if self.key_states[state_name] is False: + if self.schedule_toggle_on[state_name] is True: + pydirectinput.keyDown(key=keysym) + self.key_states[state_name] = True + + if self.key_states[state_name] is True: + if self.schedule_toggle_off[state_name] is True: + pydirectinput.keyUp(key=keysym) + self.key_states[state_name] = False - if (val < threshold) and (self.key_states[state_name] is True): - self.schedule_toggle_off[state_name] = True + if val < threshold: + if self.key_states[state_name] is True: + self.schedule_toggle_off[state_name] = True + self.schedule_toggle_on[state_name] = False + if self.key_states[state_name] is False: + self.schedule_toggle_on[state_name] = True + self.schedule_toggle_off[state_name] = False elif mode == Trigger.RAPID: if val > threshold: From 9b0757bdf398c5c87c744e5ff9a7772bce6f4c54 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Thu, 22 Feb 2024 21:26:21 +0100 Subject: [PATCH 104/123] release all keys when exiting --- src/controllers/keybinder.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/controllers/keybinder.py b/src/controllers/keybinder.py index c0e0aafd..2c98f805 100644 --- a/src/controllers/keybinder.py +++ b/src/controllers/keybinder.py @@ -359,4 +359,15 @@ def toggle_active(self): def destroy(self): """Destroy the keybinder""" + logger.info("releasing all keys...") + for state_name in self.key_states: + # TODO: too many python shenanigans. Might break if you look wrong at it + device, action = state_name.split("_") + if device == "mouse": + logger.info(f"releasing {state_name}") + pydirectinput.mouseUp(button=action) + if device == "keyboard": + logger.info(f"releasing {state_name}") + pydirectinput.keyUp(key=action) + return From fef53de97266b90934d18219848046c1dac0c25f Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sat, 24 Feb 2024 15:09:23 +0100 Subject: [PATCH 105/123] crudely implement gui for mouse trigger selection --- src/config_manager.py | 7 +++-- src/controllers/keybinder.py | 2 -- src/gui/pages/page_select_gestures.py | 40 +++++++++++++++++++++++---- src/utils/Trigger.py | 2 +- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/config_manager.py b/src/config_manager.py index 49024f14..0680bc44 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -9,6 +9,7 @@ from src.singleton_meta import Singleton from src.task_killer import TaskKiller +from src.utils.Trigger import Trigger VERSION = "0.4.0" @@ -175,18 +176,18 @@ def apply_config(self): # ------------------------------ MOUSE BINDINGS CONFIG ----------------------------- # def set_temp_mouse_binding(self, gesture, device: str, action: str, - threshold: float, trigger_type: str): + threshold: float, trigger): logger.info( "setting keybind for gesture: %s, device: %s, key: %s, threshold: %s, trigger_type: %s", - gesture, device, action, threshold, trigger_type) + gesture, device, action, threshold, trigger) # Remove duplicate keybindings self.remove_temp_mouse_binding(device, action) # Assign self.temp_mouse_bindings[gesture] = [ - device, action, float(threshold), trigger_type + device, action, float(threshold), trigger ] self.unsave_mouse_bindings = True diff --git a/src/controllers/keybinder.py b/src/controllers/keybinder.py index 2c98f805..89d6f453 100644 --- a/src/controllers/keybinder.py +++ b/src/controllers/keybinder.py @@ -22,7 +22,6 @@ class Keybinder(metaclass=Singleton): def __init__(self) -> None: - self.forced_mode = None self.delay_count = None self.key_states = None self.schedule_toggle_off = {} @@ -65,7 +64,6 @@ def init_states(self) -> None: self.start_hold_ts[state_name] = math.inf self.holding[state_name] = False - self.forced_mode = Trigger.DYNAMIC self.last_know_keybindings = copy.deepcopy( (ConfigManager().mouse_bindings | ConfigManager().keyboard_bindings)) diff --git a/src/gui/pages/page_select_gestures.py b/src/gui/pages/page_select_gestures.py index ba4de7ee..40663c36 100644 --- a/src/gui/pages/page_select_gestures.py +++ b/src/gui/pages/page_select_gestures.py @@ -10,11 +10,12 @@ from src.gui.balloon import Balloon from src.gui.dropdown import Dropdown from src.gui.frames.safe_disposable_frame import SafeDisposableFrame +from src.utils.Trigger import Trigger MAX_ROWS = 2 HELP_ICON_SIZE = (18, 18) DIV_WIDTH = 240 -DEFAULT_TRIGGER_TYPE = "single" +DEFAULT_TRIGGER_TYPE = "dynamic" GREEN = "#34A853" YELLOW = "#FABB05" @@ -62,17 +63,22 @@ def set_div_inactive(self, div): div["subtle_label"].grid_remove() div["slider"].grid_remove() div["volume_bar"].grid_remove() + div["trigger_dropdown"].set(DEFAULT_TRIGGER_TYPE) + div["trigger_dropdown"].grid_remove() - def set_div_active(self, div, gesture_name, thres): + def set_div_active(self, div, gesture_name, thres, trigger): div["selected_gesture"] = gesture_name div["combobox"].set(gesture_name) div["slider"].set(int(thres * 100)) div["slider"].configure(state="normal") + div["trigger_dropdown"].configure(state="normal") + div["trigger_dropdown"].set(trigger) div["tips_label"].grid() div["subtle_label"].grid() div["slider"].grid() div["volume_bar"].grid() + div["trigger_dropdown"].grid() def load_initial_keybindings(self): """Load default from config and set the UI @@ -90,7 +96,7 @@ def load_initial_keybindings(self): [device, action_key]) target_action_name = shape_list.available_actions_keys[action_idx] div = self.divs[target_action_name] - self.set_div_active(div, gesture_name, thres) + self.set_div_active(div, gesture_name, thres, trigger_type) self.shared_dropdown.disable_item(gesture_name) self.shared_dropdown.refresh_items() @@ -191,6 +197,22 @@ def create_divs(self, action_list: list, gesture_list: list): sticky="nw") subtle_label.grid_remove() + + # Trigger dropdown + trigger_list=[t.value for t in Trigger] + trigger_dropdown = customtkinter.CTkOptionMenu(master=self, + values=trigger_list, + width=240, + dynamic_resizing=False, + state="disabled" + ) + trigger_dropdown.grid(row=row, + column=column, + padx=(20, 20), + pady=(142, 10), + sticky="nw") + # self.shared_dropdown.register_widget(drop, action_name) + out_dict[action_name] = { "label": label, "combobox": drop, @@ -199,6 +221,7 @@ def create_divs(self, action_list: list, gesture_list: list): "volume_bar": volume_bar, "subtle_label": subtle_label, "selected_gesture": gesture_list[0], + "trigger_dropdown": trigger_dropdown } return out_dict @@ -217,12 +240,15 @@ def slider_mouse_up_callback(self, caller_name: str, event): # change int [0,100] to float [0,1] thres_value = div["slider"].get() / 100 + trigger_value = div["trigger_dropdown"].get() + + ConfigManager().set_temp_mouse_binding( div["selected_gesture"], device=target_device, action=target_action, threshold=thres_value, - trigger_type=DEFAULT_TRIGGER_TYPE) + trigger=trigger_value) ConfigManager().apply_mouse_bindings() def dropdown_callback(self, caller_name: str, target_gesture: str): @@ -244,13 +270,16 @@ def dropdown_callback(self, caller_name: str, target_gesture: str): div["volume_bar"].grid() div["tips_label"].grid() div["subtle_label"].grid() + div["trigger_dropdown"].grid() thres_value = div["slider"].get() / 100 + trigger_value = div["trigger_dropdown"].get() + ConfigManager().set_temp_mouse_binding( target_gesture, device=target_device, action=target_action, threshold=thres_value, - trigger_type=DEFAULT_TRIGGER_TYPE) + trigger=trigger_value) # Remove keybind if "None" else: @@ -259,6 +288,7 @@ def dropdown_callback(self, caller_name: str, target_gesture: str): div["volume_bar"].grid_remove() div["tips_label"].grid_remove() div["subtle_label"].grid_remove() + div["trigger_dropdown"].grid_remove() ConfigManager().remove_temp_mouse_binding(device=target_device, action=target_action) diff --git a/src/utils/Trigger.py b/src/utils/Trigger.py index 8479d07f..695cd08d 100644 --- a/src/utils/Trigger.py +++ b/src/utils/Trigger.py @@ -2,8 +2,8 @@ class Trigger(Enum): + DYNAMIC = "dynamic" RAPID = "rapid" SINGLE = "single" HOLD = "hold" TOGGLE = "toggle" - DYNAMIC = "dynamic" From d75b832335b721699b2e27ea2a38f58a2a24754e Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sun, 25 Feb 2024 17:21:35 +0100 Subject: [PATCH 106/123] refactor trigger names --- src/config_manager.py | 8 ++++---- src/gui/pages/page_keyboard.py | 2 +- src/gui/pages/page_select_gestures.py | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/config_manager.py b/src/config_manager.py index 0680bc44..dc2da131 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -221,10 +221,10 @@ def write_mouse_bindings_file(self): def set_temp_keyboard_binding(self, device: str, key_action: str, gesture: str, threshold: float, - trigger_type: str): + trigger: str): logger.info( - "setting keybind for gesture: %s, device: %s, key: %s, threshold: %s, trigger_type: %s", - gesture, device, key_action, threshold, trigger_type) + "setting keybind for gesture: %s, device: %s, key: %s, threshold: %s, trigger: %s", + gesture, device, key_action, threshold, trigger) # Remove duplicate keybindings self.remove_temp_keyboard_binding(device, key_action, gesture) @@ -232,7 +232,7 @@ def set_temp_keyboard_binding(self, device: str, key_action: str, # Assign self.temp_keyboard_bindings[gesture] = [ device, key_action, - float(threshold), trigger_type + float(threshold), trigger ] self.unsave_keyboard_bindings = True diff --git a/src/gui/pages/page_keyboard.py b/src/gui/pages/page_keyboard.py index a3e1bf19..4fc4a0eb 100644 --- a/src/gui/pages/page_keyboard.py +++ b/src/gui/pages/page_keyboard.py @@ -305,7 +305,7 @@ def set_new_keyboard_binding(self, div): key_action=div["selected_key_action"], gesture=div["selected_gesture"], threshold=thres_value, - trigger_type=DEFAULT_TRIGGER_TYPE) + trigger=DEFAULT_TRIGGER_TYPE) ConfigManager().apply_keyboard_bindings() def wait_for_key(self, div_name: str, entry_button, keydown: tk.Event): diff --git a/src/gui/pages/page_select_gestures.py b/src/gui/pages/page_select_gestures.py index 40663c36..af55750b 100644 --- a/src/gui/pages/page_select_gestures.py +++ b/src/gui/pages/page_select_gestures.py @@ -240,7 +240,7 @@ def slider_mouse_up_callback(self, caller_name: str, event): # change int [0,100] to float [0,1] thres_value = div["slider"].get() / 100 - trigger_value = div["trigger_dropdown"].get() + trigger = div["trigger_dropdown"].get() ConfigManager().set_temp_mouse_binding( @@ -248,7 +248,7 @@ def slider_mouse_up_callback(self, caller_name: str, event): device=target_device, action=target_action, threshold=thres_value, - trigger=trigger_value) + trigger=trigger) ConfigManager().apply_mouse_bindings() def dropdown_callback(self, caller_name: str, target_gesture: str): @@ -272,14 +272,14 @@ def dropdown_callback(self, caller_name: str, target_gesture: str): div["subtle_label"].grid() div["trigger_dropdown"].grid() thres_value = div["slider"].get() / 100 - trigger_value = div["trigger_dropdown"].get() + trigger = div["trigger_dropdown"].get() ConfigManager().set_temp_mouse_binding( target_gesture, device=target_device, action=target_action, threshold=thres_value, - trigger=trigger_value) + trigger=trigger) # Remove keybind if "None" else: From 51636cfbcba14f66bd4ef06d9e68fc26bbb087b2 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sun, 25 Feb 2024 17:27:46 +0100 Subject: [PATCH 107/123] use trigger enums for mouse actions --- src/config_manager.py | 8 ++++---- src/gui/pages/page_select_gestures.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/config_manager.py b/src/config_manager.py index dc2da131..2e7fda21 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -176,18 +176,18 @@ def apply_config(self): # ------------------------------ MOUSE BINDINGS CONFIG ----------------------------- # def set_temp_mouse_binding(self, gesture, device: str, action: str, - threshold: float, trigger): + threshold: float, trigger: Trigger): logger.info( - "setting keybind for gesture: %s, device: %s, key: %s, threshold: %s, trigger_type: %s", - gesture, device, action, threshold, trigger) + "setting keybind for gesture: %s, device: %s, key: %s, threshold: %s, trigger: %s", + gesture, device, action, threshold, trigger.value) # Remove duplicate keybindings self.remove_temp_mouse_binding(device, action) # Assign self.temp_mouse_bindings[gesture] = [ - device, action, float(threshold), trigger + device, action, float(threshold), trigger.value ] self.unsave_mouse_bindings = True diff --git a/src/gui/pages/page_select_gestures.py b/src/gui/pages/page_select_gestures.py index af55750b..ac15b0ef 100644 --- a/src/gui/pages/page_select_gestures.py +++ b/src/gui/pages/page_select_gestures.py @@ -240,7 +240,7 @@ def slider_mouse_up_callback(self, caller_name: str, event): # change int [0,100] to float [0,1] thres_value = div["slider"].get() / 100 - trigger = div["trigger_dropdown"].get() + trigger = Trigger(div["trigger_dropdown"].get()) ConfigManager().set_temp_mouse_binding( @@ -272,7 +272,7 @@ def dropdown_callback(self, caller_name: str, target_gesture: str): div["subtle_label"].grid() div["trigger_dropdown"].grid() thres_value = div["slider"].get() / 100 - trigger = div["trigger_dropdown"].get() + trigger = Trigger(div["trigger_dropdown"].get()) ConfigManager().set_temp_mouse_binding( target_gesture, From 9a06c7b80df80185aa83526d2f987006069c59e8 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sun, 25 Feb 2024 17:29:41 +0100 Subject: [PATCH 108/123] fix layout in page_select_gestures.py --- src/gui/pages/page_select_gestures.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui/pages/page_select_gestures.py b/src/gui/pages/page_select_gestures.py index ac15b0ef..37e2147c 100644 --- a/src/gui/pages/page_select_gestures.py +++ b/src/gui/pages/page_select_gestures.py @@ -110,7 +110,7 @@ def create_divs(self, action_list: list, gesture_list: list): # Action label label = customtkinter.CTkLabel(master=self, text=action_name, - height=175, + height=200, width=300, anchor='nw', justify=tk.LEFT) @@ -209,7 +209,7 @@ def create_divs(self, action_list: list, gesture_list: list): trigger_dropdown.grid(row=row, column=column, padx=(20, 20), - pady=(142, 10), + pady=(156, 10), sticky="nw") # self.shared_dropdown.register_widget(drop, action_name) From 3a276bc0008aea0864777a5fef8118fff4ceb824 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sun, 25 Feb 2024 19:34:04 +0100 Subject: [PATCH 109/123] add trigger selection for keyboard actions --- src/config_manager.py | 6 +++--- src/gui/pages/page_keyboard.py | 28 +++++++++++++++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/config_manager.py b/src/config_manager.py index 2e7fda21..19f0c226 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -221,10 +221,10 @@ def write_mouse_bindings_file(self): def set_temp_keyboard_binding(self, device: str, key_action: str, gesture: str, threshold: float, - trigger: str): + trigger: Trigger): logger.info( "setting keybind for gesture: %s, device: %s, key: %s, threshold: %s, trigger: %s", - gesture, device, key_action, threshold, trigger) + gesture, device, key_action, threshold, trigger.value) # Remove duplicate keybindings self.remove_temp_keyboard_binding(device, key_action, gesture) @@ -232,7 +232,7 @@ def set_temp_keyboard_binding(self, device: str, key_action: str, # Assign self.temp_keyboard_bindings[gesture] = [ device, key_action, - float(threshold), trigger + float(threshold), trigger.value ] self.unsave_keyboard_bindings = True diff --git a/src/gui/pages/page_keyboard.py b/src/gui/pages/page_keyboard.py index 4fc4a0eb..2559f5ea 100644 --- a/src/gui/pages/page_keyboard.py +++ b/src/gui/pages/page_keyboard.py @@ -13,7 +13,7 @@ from src.gui.dropdown import Dropdown from src.gui.frames.safe_disposable_frame import SafeDisposableFrame from src.gui.frames.safe_disposable_scrollable_frame import SafeDisposableScrollableFrame - +from src.utils.Trigger import Trigger logger = logging.getLogger("PageKeyboard") @@ -105,6 +105,7 @@ def load_initial_keybindings(self): div["subtle_label"].grid() div["slider"].grid() div["volume_bar"].grid() + div["trigger_dropdown"].grid() self.shared_dropdown.disable_item(gesture_name) self.divs[div_name] = div self.next_empty_row += 1 @@ -267,12 +268,27 @@ def create_div(self, row: int, div_name: str, gesture_name: str, pady=(158, 10), sticky="nw") + # Trigger dropdown + trigger_list = [t.value for t in Trigger] + trigger_dropdown = customtkinter.CTkOptionMenu(master=self, + values=trigger_list, + width=240, + dynamic_resizing=False, + state="normal" + ) + trigger_dropdown.grid(row=row, + column=0, + padx=PAD_X, + pady=(186, 10), + sticky="nw") + # Hide element related to gesture drop.grid_remove() tips_label.grid_remove() slider.grid_remove() volume_bar.grid_remove() subtle_label.grid_remove() + trigger_dropdown.grid_remove() return { "entry_field": entry_field, @@ -283,7 +299,8 @@ def create_div(self, row: int, div_name: str, gesture_name: str, "subtle_label": subtle_label, "selected_gesture": gesture_name, "selected_key_action": key_action, - "remove_button": remove_button + "remove_button": remove_button, + "trigger_dropdown": trigger_dropdown } def set_new_keyboard_binding(self, div): @@ -300,12 +317,13 @@ def set_new_keyboard_binding(self, div): # Set the keybinding thres_value = div["slider"].get() / 100 + trigger = Trigger(div["trigger_dropdown"].get()) ConfigManager().set_temp_keyboard_binding( device="keyboard", key_action=div["selected_key_action"], gesture=div["selected_gesture"], threshold=thres_value, - trigger=DEFAULT_TRIGGER_TYPE) + trigger=trigger) ConfigManager().apply_keyboard_bindings() def wait_for_key(self, div_name: str, entry_button, keydown: tk.Event): @@ -338,6 +356,7 @@ def wait_for_key(self, div_name: str, entry_button, keydown: tk.Event): div["volume_bar"].grid_remove() div["tips_label"].grid_remove() div["subtle_label"].grid_remove() + div["trigger_dropdown"].grid() self.set_new_keyboard_binding(div) # Valid key @@ -358,6 +377,7 @@ def wait_for_key(self, div_name: str, entry_button, keydown: tk.Event): div["volume_bar"].grid() div["tips_label"].grid() div["subtle_label"].grid() + div["trigger_dropdown"].grid() if self.wait_for_key_bind_id is not None: self.waiting_button.unbind("", self.wait_for_key_bind_id) @@ -397,11 +417,13 @@ def dropdown_callback(self, div_name: str, target_gesture: str): div["volume_bar"].grid() div["tips_label"].grid() div["subtle_label"].grid() + div["trigger_dropdown"].grid() else: div["slider"].grid_remove() div["volume_bar"].grid_remove() div["tips_label"].grid_remove() div["subtle_label"].grid_remove() + div["trigger_dropdown"].grid_remove() self.set_new_keyboard_binding(div) self.refresh_scrollbar() From 687361dbed7dec039d50d66370421c818218366f Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sun, 25 Feb 2024 19:34:56 +0100 Subject: [PATCH 110/123] some formatting --- src/gui/pages/page_select_gestures.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/gui/pages/page_select_gestures.py b/src/gui/pages/page_select_gestures.py index 37e2147c..10f57a23 100644 --- a/src/gui/pages/page_select_gestures.py +++ b/src/gui/pages/page_select_gestures.py @@ -197,9 +197,8 @@ def create_divs(self, action_list: list, gesture_list: list): sticky="nw") subtle_label.grid_remove() - # Trigger dropdown - trigger_list=[t.value for t in Trigger] + trigger_list = [t.value for t in Trigger] trigger_dropdown = customtkinter.CTkOptionMenu(master=self, values=trigger_list, width=240, @@ -211,7 +210,6 @@ def create_divs(self, action_list: list, gesture_list: list): padx=(20, 20), pady=(156, 10), sticky="nw") - # self.shared_dropdown.register_widget(drop, action_name) out_dict[action_name] = { "label": label, From e9d5f885d9476e175cce68799e3c80f060cf63f7 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sun, 25 Feb 2024 19:37:09 +0100 Subject: [PATCH 111/123] move meta actions to separate function --- src/controllers/keybinder.py | 103 ++++++++++++++++++----------------- 1 file changed, 54 insertions(+), 49 deletions(-) diff --git a/src/controllers/keybinder.py b/src/controllers/keybinder.py index 89d6f453..badcf958 100644 --- a/src/controllers/keybinder.py +++ b/src/controllers/keybinder.py @@ -94,6 +94,52 @@ def get_current_monitor(self) -> int: # raise Exception("Monitor not found") return 0 + def meta_action(self, val, action, threshold, is_active: bool) -> None: + state_name = "meta_" + action + + if action == "pause": + + if (val > threshold) and (self.key_states[state_name] is False): + mon_id = self.get_current_monitor() + if mon_id is None: + return + + self.toggle_active() + + self.key_states[state_name] = True + elif (val < threshold) and (self.key_states[state_name] is True): + self.key_states[state_name] = False + + if is_active: + + if action == "reset": + if (val > threshold) and (self.key_states[state_name] is + False): + mon_id = self.get_current_monitor() + if mon_id is None: + return + + pydirectinput.moveTo( + self.monitors[mon_id]["center_x"], + self.monitors[mon_id]["center_y"]) + self.key_states[state_name] = True + elif (val < threshold) and (self.key_states[state_name] is + True): + self.key_states[state_name] = False + + elif action == "cycle": + if (val > threshold) and (self.key_states[state_name] is + False): + mon_id = self.get_current_monitor() + next_mon_id = (mon_id + 1) % len(self.monitors) + pydirectinput.moveTo( + self.monitors[next_mon_id]["center_x"], + self.monitors[next_mon_id]["center_y"]) + self.key_states[state_name] = True + elif (val < threshold) and (self.key_states[state_name] is + True): + self.key_states[state_name] = False + def mouse_action(self, val, action, threshold, mode) -> None: state_name = "mouse_" + action @@ -285,65 +331,22 @@ def act(self, blendshape_values) -> None: if shape_name not in shape_list.blendshape_names: continue - device, action, thres, mode = v + device, action, threshold, mode = v mode = Trigger(mode.lower()) # Get blendshape value idx = shape_list.blendshape_indices[shape_name] val = blendshape_values[idx] - if (device == "mouse") and (action == "pause"): - state_name = "mouse_" + action + if device == "meta": + self.meta_action(val, action, threshold, self.is_active.get()) - if (val > thres) and (self.key_states[state_name] is False): - mon_id = self.get_current_monitor() - if mon_id is None: - return - - self.toggle_active() - - self.key_states[state_name] = True - elif (val < thres) and (self.key_states[state_name] is True): - self.key_states[state_name] = False - - elif self.is_active.get(): + if self.is_active.get(): if device == "mouse": - - if action == "reset": - state_name = "mouse_" + action - if (val > thres) and (self.key_states[state_name] is - False): - mon_id = self.get_current_monitor() - if mon_id is None: - return - - pydirectinput.moveTo( - self.monitors[mon_id]["center_x"], - self.monitors[mon_id]["center_y"]) - self.key_states[state_name] = True - elif (val < thres) and (self.key_states[state_name] is - True): - self.key_states[state_name] = False - - elif action == "cycle": - state_name = "mouse_" + action - if (val > thres) and (self.key_states[state_name] is - False): - mon_id = self.get_current_monitor() - next_mon_id = (mon_id + 1) % len(self.monitors) - pydirectinput.moveTo( - self.monitors[next_mon_id]["center_x"], - self.monitors[next_mon_id]["center_y"]) - self.key_states[state_name] = True - elif (val < thres) and (self.key_states[state_name] is - True): - self.key_states[state_name] = False - - else: - self.mouse_action(val, action, thres, mode) + self.mouse_action(val, action, threshold, mode) elif device == "keyboard": - self.keyboard_action(val, action, thres, mode) + self.keyboard_action(val, action, threshold, mode) def set_active(self, flag: bool) -> None: self.is_active.set(flag) @@ -367,5 +370,7 @@ def destroy(self): if device == "keyboard": logger.info(f"releasing {state_name}") pydirectinput.keyUp(key=action) + elif device == "meta": + pass return From f03445b4688d6c702d2bee718882623e73735829 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sun, 25 Feb 2024 19:40:16 +0100 Subject: [PATCH 112/123] increase default window size to accommodate bigger action divs --- src/gui/main_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/main_gui.py b/src/gui/main_gui.py index 7c3cc112..cb18e25c 100644 --- a/src/gui/main_gui.py +++ b/src/gui/main_gui.py @@ -19,7 +19,7 @@ def __init__(self, tk_root): super().__init__() self.tk_root = tk_root - self.tk_root.geometry("1024x658") + self.tk_root.geometry("1024x800") self.tk_root.title(f"Grimassist {ConfigManager().version}") self.tk_root.iconbitmap("assets/images/icon.ico") self.tk_root.resizable(width=False, height=False) From ee2c9c32befa728c17147b08a4a4dc0ebabfafc6 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sun, 25 Feb 2024 19:40:49 +0100 Subject: [PATCH 113/123] allow resizing of window --- src/gui/main_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/main_gui.py b/src/gui/main_gui.py index cb18e25c..228f1f2b 100644 --- a/src/gui/main_gui.py +++ b/src/gui/main_gui.py @@ -22,7 +22,7 @@ def __init__(self, tk_root): self.tk_root.geometry("1024x800") self.tk_root.title(f"Grimassist {ConfigManager().version}") self.tk_root.iconbitmap("assets/images/icon.ico") - self.tk_root.resizable(width=False, height=False) + self.tk_root.resizable(width=True, height=True) self.tk_root.grid_rowconfigure(1, weight=1) self.tk_root.grid_columnconfigure(1, weight=1) From 542f7d1936401b3f648e2374361c15864ff49c1f Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 26 Feb 2024 19:45:44 +0100 Subject: [PATCH 114/123] update configs --- configs/default/cursor.json | 2 +- configs/profile_1/cursor.json | 10 +++++----- configs/profile_2/cursor.json | 22 +++++++++++----------- src/controllers/keybinder.py | 4 ++-- src/gui/pages/page_cursor.py | 4 ++-- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/configs/default/cursor.json b/configs/default/cursor.json index 8104dbe7..098890a3 100644 --- a/configs/default/cursor.json +++ b/configs/default/cursor.json @@ -13,7 +13,7 @@ "shape_smooth": 10, "tick_interval_ms": 16, "hold_trigger_ms": 500, - "rapid_fire_delay": 500, + "rapid_fire_interval_ms": 100, "auto_play": false, "enable": 1, "mouse_acceleration": false, diff --git a/configs/profile_1/cursor.json b/configs/profile_1/cursor.json index 938f42a1..0a8bb740 100644 --- a/configs/profile_1/cursor.json +++ b/configs/profile_1/cursor.json @@ -9,12 +9,12 @@ "spd_down": 40, "spd_left": 40, "spd_right": 40, - "pointer_smooth": 6, - "shape_smooth": 10, - "tick_interval_ms": 16, + "pointer_smooth": 6, + "shape_smooth": 10, + "tick_interval_ms": 16, "hold_trigger_ms": 500, - "rapid_fire_delay": 500, - "auto_play": false, + "rapid_fire_interval_ms": 100, + "auto_play": false, "enable": 1, "mouse_acceleration": false, "use_transformation_matrix": false diff --git a/configs/profile_2/cursor.json b/configs/profile_2/cursor.json index f045cc3e..0a8bb740 100644 --- a/configs/profile_2/cursor.json +++ b/configs/profile_2/cursor.json @@ -5,17 +5,17 @@ "tracking_vert_idxs": [ 8 ], - "spd_up": 42, - "spd_down": 42, - "spd_left": 42, - "spd_right": 42, - "pointer_smooth": 15, - "shape_smooth": 10, - "tick_interval_ms": 16, + "spd_up": 40, + "spd_down": 40, + "spd_left": 40, + "spd_right": 40, + "pointer_smooth": 6, + "shape_smooth": 10, + "tick_interval_ms": 16, "hold_trigger_ms": 500, - "rapid_fire_delay": 500, - "auto_play": false, - "enable": 1, - "mouse_acceleration": false, + "rapid_fire_interval_ms": 100, + "auto_play": false, + "enable": 1, + "mouse_acceleration": false, "use_transformation_matrix": false } \ No newline at end of file diff --git a/src/controllers/keybinder.py b/src/controllers/keybinder.py index badcf958..fcdbe9e5 100644 --- a/src/controllers/keybinder.py +++ b/src/controllers/keybinder.py @@ -214,7 +214,7 @@ def mouse_action(self, val, action, threshold, mode) -> None: if self.key_states[state_name] is True: if (((time.time() - self.start_hold_ts[state_name]) * 1000) - >= ConfigManager().config["rapid_fire_interval"]): + >= ConfigManager().config["rapid_fire_interval_ms"]): pydirectinput.click(button=action) self.holding[state_name] = True self.start_hold_ts[state_name] = time.time() @@ -299,7 +299,7 @@ def keyboard_action(self, val, keysym, threshold, mode): if self.key_states[state_name] is True: if (((time.time() - self.start_hold_ts[state_name]) * 1000) - >= ConfigManager().config["rapid_fire_interval"]): + >= ConfigManager().config["rapid_fire_interval_ms"]): pydirectinput.press(keys=keysym) self.holding[state_name] = True self.start_hold_ts[state_name] = time.time() diff --git a/src/gui/pages/page_cursor.py b/src/gui/pages/page_cursor.py index 9b4229e1..0b738e9f 100644 --- a/src/gui/pages/page_cursor.py +++ b/src/gui/pages/page_cursor.py @@ -56,8 +56,8 @@ def __init__( "Controls how long the user should\nhold a gesture in milliseconds\nfor an action to trigger", 1, MAX_HOLD_TRIG ], - "(Advanced) Rapid fire trigger interval(ms)": [ - "rapid_fire_interval", + "(Advanced) Rapid fire interval(ms)": [ + "rapid_fire_interval_ms", "Controls how much time should pass\nbetween each individual\ntriggering of the action", 1, MAX_HOLD_TRIG ] From 8e41dfc25e9341abc1feceddcdd9647df3bc4b52 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 26 Feb 2024 20:39:41 +0100 Subject: [PATCH 115/123] fix cursor settings layout --- src/gui/pages/page_cursor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gui/pages/page_cursor.py b/src/gui/pages/page_cursor.py index 0b738e9f..33bcccd9 100644 --- a/src/gui/pages/page_cursor.py +++ b/src/gui/pages/page_cursor.py @@ -66,7 +66,7 @@ def __init__( self.toggle_label = customtkinter.CTkLabel(master=self, compound='right', text="Cursor control", - justify=tkinter.RIGHT) + justify=tkinter.LEFT) self.toggle_label.cget("font").configure(weight='bold') self.toggle_label.grid(row=0, column=0, @@ -281,8 +281,8 @@ class PageCursor(SafeDisposableFrame): def __init__(self, master, **kwargs): super().__init__(master, **kwargs) - self.grid_rowconfigure(2, weight=1) - self.grid_columnconfigure(0, weight=1) + self.grid_rowconfigure(2, weight=0) + self.grid_columnconfigure(0, weight=0) self.is_active = False self.task = {} From 1a4e0c2c6e7e176fd9fde43fd23e688e6ae252b0 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Mon, 26 Feb 2024 21:09:01 +0100 Subject: [PATCH 116/123] fix glitch in page_keyboard.py --- src/gui/pages/page_keyboard.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/gui/pages/page_keyboard.py b/src/gui/pages/page_keyboard.py index 2559f5ea..426f9ef2 100644 --- a/src/gui/pages/page_keyboard.py +++ b/src/gui/pages/page_keyboard.py @@ -206,6 +206,7 @@ def create_div(self, row: int, div_name: str, gesture_name: str, dynamic_resizing=False, state="disabled") drop.grid(row=row, column=0, padx=PAD_X, pady=(64, 10), sticky="nw") + drop.grid_remove() self.shared_dropdown.register_widget(drop, div_name) # Label ? @@ -221,6 +222,7 @@ def create_div(self, row: int, div_name: str, gesture_name: str, padx=PAD_X, pady=(92, 10), sticky="nw") + tips_label.grid_remove() self.shared_info_balloon.register_widget(tips_label, BALLOON_TXT) # Volume bar @@ -235,6 +237,8 @@ def create_div(self, row: int, div_name: str, gesture_name: str, pady=(122, 10), sticky="nw") + volume_bar.grid_remove() + # Slider slider = customtkinter.CTkSlider(master=self, from_=1, @@ -256,6 +260,8 @@ def create_div(self, row: int, div_name: str, gesture_name: str, pady=(142, 10), sticky="nw") + slider.grid_remove() + # Subtle, Exaggerated subtle_label = customtkinter.CTkLabel(master=self, text="Subtle\t\t\t Exaggerated", @@ -267,6 +273,7 @@ def create_div(self, row: int, div_name: str, gesture_name: str, padx=PAD_X, pady=(158, 10), sticky="nw") + subtle_label.grid_remove() # Trigger dropdown trigger_list = [t.value for t in Trigger] @@ -274,7 +281,7 @@ def create_div(self, row: int, div_name: str, gesture_name: str, values=trigger_list, width=240, dynamic_resizing=False, - state="normal" + state="normal", ) trigger_dropdown.grid(row=row, column=0, @@ -282,12 +289,6 @@ def create_div(self, row: int, div_name: str, gesture_name: str, pady=(186, 10), sticky="nw") - # Hide element related to gesture - drop.grid_remove() - tips_label.grid_remove() - slider.grid_remove() - volume_bar.grid_remove() - subtle_label.grid_remove() trigger_dropdown.grid_remove() return { @@ -356,7 +357,7 @@ def wait_for_key(self, div_name: str, entry_button, keydown: tk.Event): div["volume_bar"].grid_remove() div["tips_label"].grid_remove() div["subtle_label"].grid_remove() - div["trigger_dropdown"].grid() + div["trigger_dropdown"].grid_remove() self.set_new_keyboard_binding(div) # Valid key From 647eb32576ae05c4099ddec9601b77692ab67a6b Mon Sep 17 00:00:00 2001 From: acidcoke Date: Tue, 27 Feb 2024 20:50:35 +0100 Subject: [PATCH 117/123] some formatting --- src/controllers/keybinder.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/controllers/keybinder.py b/src/controllers/keybinder.py index fcdbe9e5..db80c83b 100644 --- a/src/controllers/keybinder.py +++ b/src/controllers/keybinder.py @@ -143,9 +143,6 @@ def meta_action(self, val, action, threshold, is_active: bool) -> None: def mouse_action(self, val, action, threshold, mode) -> None: state_name = "mouse_" + action - # TODO: un-hardcode this - #mode = self.forced_mode - if mode == Trigger.SINGLE: if val > threshold: if self.key_states[state_name] is False: @@ -228,9 +225,6 @@ def keyboard_action(self, val, keysym, threshold, mode): state_name = "keyboard_" + keysym - # TODO: un-hardcode this - # mode = self.forced_mode - if mode == Trigger.SINGLE: if val > threshold: if self.key_states[state_name] is False: From 251f6abcde959ff5c613d80543387c932caba6cc Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sun, 24 Mar 2024 18:29:02 +0100 Subject: [PATCH 118/123] improve readme --- README.md | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 971624e1..a9be3bf5 100644 --- a/README.md +++ b/README.md @@ -61,17 +61,19 @@ pip install -r requirements.txt |-----------|---------------------------------------| | camera_id | Default camera index on your machine. | | tracking_vert_idxs | Tracking points for controlling cursor ([see](assets/images/uv_unwrap_full.png)) | -| camera_id | Default camera index on your machine. | | spd_up | Cursor speed in the upward direction | | spd_down | Cursor speed in downward direction | | spd_left | Cursor speed in left direction | | spd_right | Cursor speed in right direction | -| pointer_smooth | Amount of cursor smoothness | -| shape_smooth | Reduces the flickering of the action | -| hold_trigger_ms | Hold action trigger delay in milliseconds | -| auto_play | Automatically begin playing when you launch the program | -| mouse_acceleration | Make the cursor move faster when the head moves quickly | -| use_transformation_matrix | Control cursor using head direction (tracking_vert_idxs will be ignored) | +| pointer_smooth | Amount of cursor smoothness | +| shape_smooth | Reduces the flickering of the action | +| tick_interval_ms | interval between each tick of the pipeline in milliseconds | +| hold_trigger_ms | Hold action trigger delay in milliseconds | +| rapid_fire_interval_ms | interval between each activation of the action in milliseconds | +| auto_play | Automatically begin playing when you launch the program | +| enable | Enable cursor control | +| mouse_acceleration | Make the cursor move faster when the head moves quickly | +| use_transformation_matrix | Control cursor using head direction (tracking_vert_idxs will be ignored) | ## Keybinding configs @@ -84,13 +86,13 @@ gesture_name: [device_name, action_name, threshold, trigger_type] ``` -| | | -|--------------|-------------------------------------------------------------------------------------------| -| gesture_name | Face expression name, see the [list](src/shape_list.py#L16) | -| device_name | "mouse" or "keyboard" | -| action_name | "left", "right" and "middle" for mouse. "" for keyboard, for instance, "w" for the W key. | -| threshold | The action trigger threshold has values ranging from 0.0 to 1.0. | -| trigger_type | Action trigger type, use "single" for a single trigger, "hold" for ongoing action. | +| | | +|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| gesture_name | Face expression name, see the [list](src/shape_list.py#L16) | +| device_name | "meta", "mouse", or "keyboard" | +| action_name | name of the action e.g. "left" for mouse.
e.g. "ctrl" for keyboard
e.g. "pause" for meta | +| threshold | The action trigger threshold has values ranging from 0.0 to 1.0. | +| trigger_type | "single" for a single trigger
"hold" for ongoing action.
"dynamic" for a mixture of single and hold. It first acts like single and after passing the amount of miliseconds from hold_trigger_ms like hold. Note: this is the default behaviour for mouse buttons
"toggle" to switch an action on and off
"rapid" trigger an action every "rapid_fire_interval_ms" | From 1f34e5e73c1f165c63e3566c5341c6d410a6f6cd Mon Sep 17 00:00:00 2001 From: acidcoke Date: Thu, 4 Apr 2024 19:41:12 +0200 Subject: [PATCH 119/123] change actions to meta --- src/shape_list.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shape_list.py b/src/shape_list.py index 3a89ca7f..aa3a2e34 100644 --- a/src/shape_list.py +++ b/src/shape_list.py @@ -73,9 +73,9 @@ "Mouse left click": ["mouse", "left"], "Mouse right click": ["mouse", "right"], "Mouse middle click": ["mouse", "middle"], - "Mouse pause / unpause": ["mouse", "pause"], - "Reset cursor to center": ["mouse", "reset"], - "Switch focus between monitors": ["mouse", "cycle"] + "Mouse pause / unpause": ["meta", "pause"], + "Reset cursor to center": ["meta", "reset"], + "Switch focus between monitors": ["meta", "cycle"] } available_actions_keys = list(available_actions.keys()) available_actions_values = list(available_actions.values()) From e4761b8cdcea2d79d0b6f8bd2f11be67b9a9a725 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Sat, 6 Apr 2024 17:21:26 +0200 Subject: [PATCH 120/123] bump version to 0.5.0 --- installer.iss | 2 +- src/config_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/installer.iss b/installer.iss index ccd1843b..f6c8da92 100644 --- a/installer.iss +++ b/installer.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Grimassist" -#define MyAppVersion "0.4.0" +#define MyAppVersion "0.5.0" #define MyAppExeName "grimassist.exe" [Setup] diff --git a/src/config_manager.py b/src/config_manager.py index 19f0c226..0324182a 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -11,7 +11,7 @@ from src.task_killer import TaskKiller from src.utils.Trigger import Trigger -VERSION = "0.4.0" +VERSION = "0.5.0" DEFAULT_JSON = Path(f"C:/Users/{os.getlogin()}/Grimassist/configs/default.json") BACKUP_PROFILE = Path(f"C:/Users/{os.getlogin()}/Grimassist/configs/default") From bdbe595473adc87adf77b408c63a2fc7228fc190 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Fri, 12 Apr 2024 17:46:05 +0200 Subject: [PATCH 121/123] Merge pull request #13 from AceCentre/12-no-developer-getting-started-guide (#13) Add a basic developer getting-started guide --- developer.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 developer.md diff --git a/developer.md b/developer.md new file mode 100644 index 00000000..61c2b79e --- /dev/null +++ b/developer.md @@ -0,0 +1,51 @@ +# Developer getting started guide +Follow these instructions to get started as a developer for this project. + +1. Install Python 3.10 for Windows. + + You can download an installer from here for example. + [python.org/downloads/release/python-31011/](https://www.python.org/downloads/release/python-31011/) + + There isn't a mediapipe for 3.12 Python, which is the latest at time of + writing. + +2. Check the Python version is installed, on the path, and so on. + + If you run this command you should get this output. + + > python --version + Python 3.10.11 + + If you don't then restart Powershell or VSCode or VSCodium, or the whole + machine. Or check the PATH environment variable. + +3. Create a Python virtual environment (venv). + + Python venv is now the best practice for programs that require PIP modules. + + Run commands like these to create the venv in a sub-directory also named + `venv`. The repository is already configured to ignore that sub-directory. + + cd /path/where/you/cloned/FaceCommander + python -m venv venv + + Going forwards, you will run Python like this. + `.\venv\Scripts\python.exe `... other command line options ... + +4. Install the required PIP modules. + + Run commands like these to update PIP and then install the required modules. + + cd /path/where/you/cloned/FaceCommander + .\venv\Scripts\python.exe -m pip install --upgrade pip + .\venv\Scripts\python.exe -m pip install -r .\requirements.txt + +5. Run the program. + + Run commands like this. + + cd /path/where/you/cloned/FaceCommander + .\venv\Scripts\python.exe grimassist.py + +The program should start. Its print logging should appear in the terminal or +Powershell session. From f0e8daebb56bb208b09f294a0a5de10bd64b38a6 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Fri, 12 Apr 2024 18:19:57 +0200 Subject: [PATCH 122/123] Merge pull request #16 from AceCentre/2-add-blink-detection (#14) * Merge pull request #16 from AceCentre/2-add-blink-detection 2 add blink detection * run formatter * add changes for grimassist * satisfy linter --- README.md | 4 + assets/images/dropdowns/Eye blink left.png | Bin 0 -> 5019 bytes assets/images/dropdowns/Eye blink left.xcf | Bin 0 -> 4940 bytes assets/images/dropdowns/Eye blink right.png | Bin 0 -> 5012 bytes assets/images/dropdowns/Eye blink right.xcf | Bin 0 -> 4654 bytes assets/images/dropdowns/eye.xcf | Bin 0 -> 15849 bytes assets/images/menu_btn_about.png | Bin 0 -> 4935 bytes assets/images/menu_btn_about.xcf | Bin 0 -> 6937 bytes assets/images/menu_btn_about_selected.png | Bin 0 -> 5962 bytes assets/images/menu_btn_about_selected.xcf | Bin 0 -> 5813 bytes developer.md | 53 +++ grimassist.py | 4 +- here.code-workspace | 30 ++ src/accel_graph.py | 2 - src/camera_manager.py | 126 ++++--- src/config_manager.py | 95 +++--- src/controllers/__init__.py | 2 +- src/controllers/keybinder.py | 72 ++-- src/controllers/mouse_controller.py | 7 +- src/detectors/__init__.py | 2 +- src/detectors/facemesh.py | 38 ++- src/gui/__init__.py | 2 +- src/gui/balloon.py | 21 +- src/gui/dropdown.py | 83 ++--- src/gui/frames/__init__.py | 8 +- src/gui/frames/frame_cam_preview.py | 74 ++--- src/gui/frames/frame_menu.py | 122 ++++--- src/gui/frames/frame_profile.py | 313 ++++++++---------- src/gui/frames/frame_profile_editor.py | 294 ++++++++-------- src/gui/frames/frame_profile_switcher.py | 218 ++++++------ src/gui/frames/safe_disposable_frame.py | 3 - .../safe_disposable_scrollable_frame.py | 1 - src/gui/main_gui.py | 92 ++--- src/gui/pages/__init__.py | 10 +- src/gui/pages/page_about.py | 240 ++++++++++++++ src/gui/pages/page_cursor.py | 222 ++++++------- src/gui/pages/page_home.py | 107 +++--- src/gui/pages/page_keyboard.py | 305 ++++++++--------- src/gui/pages/page_select_camera.py | 61 ++-- src/gui/pages/page_select_gestures.py | 210 ++++++------ src/pipeline.py | 4 +- src/shape_list.py | 38 ++- src/singleton_meta.py | 5 +- src/task_killer.py | 8 +- src/utils/__init__.py | 18 +- src/utils/install_font.py | 4 +- src/utils/list_cameras.py | 6 +- src/utils/smoothing.py | 3 +- 48 files changed, 1602 insertions(+), 1305 deletions(-) create mode 100644 assets/images/dropdowns/Eye blink left.png create mode 100644 assets/images/dropdowns/Eye blink left.xcf create mode 100644 assets/images/dropdowns/Eye blink right.png create mode 100644 assets/images/dropdowns/Eye blink right.xcf create mode 100644 assets/images/dropdowns/eye.xcf create mode 100644 assets/images/menu_btn_about.png create mode 100644 assets/images/menu_btn_about.xcf create mode 100644 assets/images/menu_btn_about_selected.png create mode 100644 assets/images/menu_btn_about_selected.xcf create mode 100644 here.code-workspace create mode 100644 src/gui/pages/page_about.py diff --git a/README.md b/README.md index a9be3bf5..d09fccc5 100644 --- a/README.md +++ b/README.md @@ -109,3 +109,7 @@ gesture_name: [device_name, action_name, threshold, trigger_type] 1. Install [inno6](https://jrsoftware.org/isdl.php#stable) 2. Build using the `installer.iss` file + +# Attribution +Blink graphics in the user interface are based on +[Eye icons created by Kiranshastry - Flaticon](https://www.flaticon.com/free-icons/eye). diff --git a/assets/images/dropdowns/Eye blink left.png b/assets/images/dropdowns/Eye blink left.png new file mode 100644 index 0000000000000000000000000000000000000000..6a14e9f6e41dafa057e05c2fa01a4abf71cff47b GIT binary patch literal 5019 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 literal 0 HcmV?d00001 diff --git a/assets/images/dropdowns/Eye blink right.png b/assets/images/dropdowns/Eye blink right.png new file mode 100644 index 0000000000000000000000000000000000000000..9b39d56d08154349acfd63cfdc2f787668d85033 GIT binary patch literal 5012 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 W@s>0=FE0YkMH2^y2@eOxZ~6lhU3jDb literal 0 HcmV?d00001 diff --git a/assets/images/dropdowns/Eye blink right.xcf b/assets/images/dropdowns/Eye blink right.xcf new file mode 100644 index 0000000000000000000000000000000000000000..4ac9324dd74f6e70db2a18709f6487c6c444994e GIT binary patch literal 4654 zcmc&%eQXrh5ud#~`{FNbzLM0YX1!2sL+*TMe8x_Xv)=(Gp)|obB!p7IV(*=|xew=V zO>Lys_eY}?0V$zX5)u-mD!sIbs1lJX)GrgMQu(7&sG6vXl$JuEU>i7Ww}!^P+s?eb zg(aeLixfJi*_k)*H}huS$Gp9;8toGYxLh$ zL$cvxKJo*>gN`G_s4qZvAU}lsU|uKOmjG;Uqa;USvN_j8;z^|kHF$s?7BXREB4{D9 zz+UTonsufhBcnwZ*vW7|d-?5N^7lMm_{2F^_Y8{$|VC%V7mR7fL%=$I++4Daf<93TqeMoo4 z?W?BbjJSqw#GMx`L9-Ee;WtJ(hGO--evI2KI<=80ZWZsVUG)ar(7Fh92ri!-V(ZWN zxZSEhvec2cL-4jP1YOFjZ2id>Io|!#`RuVj@repHeCT8we%8V5p3{tNto<9;y}x^p zJ@MNMs6(*vV1#vU`;^;PMjmHRt@x^;PkPwX|9F^Vn)bcVww)N{_EjJEviP@+#qoX8 z&R#fkgJV3A4QyZQ%N*k#uV?#D{DAAOjc>679RZG6am>dK_>Fv)zyA_D&}__0{kvss z|1e+IVBH7X*?!kY+;@reUG{zLd9I6Ug6%%GmdBBP`7GPHH_Poy*%a#>(@aN< zB8;cQrAFnj;tD5JzENqekJlx&!H6Oz;}Nx|zPK(F?v6*|39*g~S0cPi7Crput;g<#$&5%y>3sfsKmPC zJ(&3F+AU9YxLRu40>ysQF;FaueoYRGF*&NNmeviZA@}1nk?x?XB{wOFE;-s4QKUeN zh(CV=LjSW1cigDN!dkBsX!@_v&!{~FZk`SNWI|P9nylelNdfOHd2Cj2#rp}n1=m9g ztegeE^|pf)@HMY$X!OkD@$CQLv8gw%#ocRY=XK!@!nwR_{#&DcZcPn^+;@Fdf5Yt+ zqYYxALMm-YMYL&i`8D%NeSFyz-H!C5+QE4KgW9dMq+FV0=TMC~> z?I$~pQ-o~lQeH;^RL;mwMB|vcXwj);ns!Ro)XOY7-C%UrEJ3EL%ZW@livN|ojsljF zt|Or&tf`yVn>iWSMda)>B!8N7kssd~y-OeYykOex;7k=lQOXXbDI3^QIusz2w50?P z@@&|Qw$W$e1oU&De<8-P14c_@v@}LbW3)7)kV)Fo=$&W7X0(ky6Nk~#7!6}MF$PA< zV6+TI%V4w&qL4}2GU%OW!)CONJ`;!0G8heGI5CDDZrZz!{N~gf&)5rLYU8<6p-k>|l-p?uXn)Y>3hh7JW_-YGJC!izP9^<|+^Mun>D%jGcPdU1 zU1-8Gl*WaoDFd1-A`dDFG|^iN#dT3R@mznODESQ~;K!RMCMMNA%IfWsx237Eb-NVU z>~A1*0E-5P2PC|VLh+a;N?md+=}IaIHDuac>0#+NbSy>_-%SCi(VDe!0VUiKr>>Yl6p(S+$Z^SU6QYl9AWlhqvV^W z&ycVJOnw4%zfGUcO`Hzf?Wh;(I-ag6zn%hN?$)dN3L9jxVTbHNebX)dZN0^2fC?7v z$$sLnp=W`fyXK=14m~&WpaX7F3^<~{;(!}mb)3}qU>wwKFjMw{zQYW6qw@S`>jALC z0W*(WpKM%7nNqo;4Nu#Dj`2lqDc-Xkh5K2w=@`&Kz1O2a7leh%=-o^*XCc41Wpt!X zGW)Py_NZyhL8=6DDX^uAkg*)5oX9q?;~Ls|Id(z;${eH-m&W3YI9`MihlSZr#g!LY z#G%YV25}iItBB*}6>(T{?Nl~-2}K;r9F&&8bSZrKg1ZR*C0(e2u8YhG+4_$|xZEA* z4llRE%z}ZDomlhAzjywl6F)h9?agIQ_{`Su^=&O>{QcJAOL&lJ@#Y2P?&rdZcz>)% zR9Zvc5Z2DwE1-4R3I9b_K#EUvugh*T+|B14`rJ3koQeFhm)95_^Q_BnVz8^6<@;$X zavBm2LH5$3~hRpY`$HPnnFCQ~!6XV7;7bc<^ zm}+XVuBQ;Tvqsn3@#Th54p)Js(^N0f?+@!42}d%=*4r_M9hlP)<~D?rymPox&t`_( Sc>Zt-hAfP`oDKv3|Nd{})HOQ* literal 0 HcmV?d00001 diff --git a/assets/images/dropdowns/eye.xcf b/assets/images/dropdowns/eye.xcf new file mode 100644 index 0000000000000000000000000000000000000000..a7d54838a7e55812a30e725f3a4a9aa94e9e9ae6 GIT binary patch literal 15849 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 ROL6>}hdE93>8DT1e*$t^IsO0u literal 0 HcmV?d00001 diff --git a/assets/images/menu_btn_about.png b/assets/images/menu_btn_about.png new file mode 100644 index 0000000000000000000000000000000000000000..49b655fdd627034b78928fb0859aadd16ff17bfa GIT binary patch literal 4935 zcmeHKc~nzp7JmT|2|`&E0r424gIdYTgDgV;AwVJqP!Vveyu9}ko@66=gn-JTxFJ{- zx8nkcTdgCdwjxLixZ;jl7jOlwTEwc1Rm&(iUjhowc+MQpnf^!4+rInV`}^+wefQpz zZ$(U0gag%+3PF&AR3e@V-Ywu}4;l!bk1maP4qnGIViU-zNE%J2*D44VP9xKGI1M)u z3J5aYP{&Mkv>r9+r#E?l6w1)z`$qNUk$-fISg)?$5=D2k_E>XqZbL++gYHVn-h?uN zYsvHpJf|H?{LwMnQcB+xyxnP=vd#UnWOn(J&ShPG3GvpELE*;NM~%$b)U0avj!>?* zeD&>uZ-?7`7eX$ss0pi@+0l9QTI=9hWAkZvM%LuADmRrj+G$BuwDZAUF@9%Hjd7LNlz;iEmJS7lY!b)H zcNd*uK5Q1$JZ-t(HYZ=1Ui`+8yRP&!R4nmQ9!8wCP7Ruw!bc15vG+-+?ShmmCH5{s z`Z>mq0z(s$qg+}rTT#TH?6YBh!c!!U6MN6&-ty9U>sY_eiQ{Wi;*uS%RgKL;%D>r< zRYe&FOMONyfyJkiO7c8?jwfA}aMo<^%w3jsrG3xozZLGIhOe@@vOPrqwmIv0^{gLY z(F1QA*WkM=)mi!0oz=nB#Lc_)6}JN^ZyV~?Uwe6dR^6?!t>??!1XfEca#psLc|W6U z{CnN0LZ-*s(H#VJm&^0a@Vl|lQfVzy- zT3Q-2&6lawD_IfjwG}ini&(3Yg0)P zoet({@BLHjWU>$N8bc2YfDe`l(Xlv8HcPE$^|dgNVMYMbY#u83nM}YYU+>f;1$8J#uS~8hXtVL7J?n%WWx_NyerbP)%X!+#gY>bWYxC|ag zjxt~z?1|hL4EY$$72sS9@k5b5P*RP7L^LRFh63PB0^lGRjPQ9l%)sUTTn5bL`7z`e zo5Ntk0zWPnSMUX}e;CQyc?upwj$(*_$3_uf_J5!ovPkTT&_uv;A#=wS%9JwBA+;L012iiYLaJZv5U9xx4`Js&D`9Z2m;1{UM}2W>VSr z!EPHGSKpj>OFHA5CW+gSV=63kl0?$FdI>hlJ}geNYpK&2JV%kceG1y%75#J3s?%2l z2Jf2UjA73nY<(np6j1x(ZSf-;>Dn=$yjqL>-5aSxva9Q4&209*VK<8GNXoKFTPHyC zJJVx{&V9GK7Br79r&Kz44T!Qztz$I1$4oo&if0v_9iAAxzGg8|7hPTY?a;c0y4{U) zZGNQivS%iS-Hz)Gw>_x$bGPjXUOyuwC3sSwuf!p0Y9* zR6K`qiP%VCP0)RDU9Xl6%!GEVEhnM#b^H=a<)DC7Q`U_uo|R)$CrK1McTV_avD1ZB zYs#6xdaJc@Z2fNQEjg*D+jFK=M2$z^+|P+hls|18k*IXp(000QUMV?;GMG|Bd9gOR z0{SAuDdg;>ce4+=DIQ1WYa+8PaNCiW$Ff;CbJ>>C0Zfgr{KsvD>+;eMa^$tpo$xB*=qg3r zIp>wd29rK>?d_mu6?;~!xYV?4!`|OJIPZqfxHo$g&t+KlvpVNQ)vfxg zn_M09k~bR+N1dw#Esy!NBTTmE3OXLPm>NXV=@)H$CZ9p~mU`EZd#=xnh6vh=^*CM(D-OIE-t-vV6ZG|`(hhI87d$9B&puan8x#a24T}<&g(NQeH#r_S A-2eap literal 0 HcmV?d00001 diff --git a/assets/images/menu_btn_about.xcf b/assets/images/menu_btn_about.xcf new file mode 100644 index 0000000000000000000000000000000000000000..ecd540ce1239addbed50ac792abbe2dc5ac1ee8f GIT binary patch literal 6937 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 literal 0 HcmV?d00001 diff --git a/developer.md b/developer.md index 61c2b79e..f038fd49 100644 --- a/developer.md +++ b/developer.md @@ -49,3 +49,56 @@ Follow these instructions to get started as a developer for this project. The program should start. Its print logging should appear in the terminal or Powershell session. + +# Tips for Git on Windows +Git for Windows can be installed with winget as described here. +[git-scm.com/download/win](https://git-scm.com/download/win) + +You can activate OpenSSH in Windows 10 as described here. +[stackoverflow.com/a/40720527/7657675](https://stackoverflow.com/a/40720527/7657675) + +You can then set up a private key for GitHub authentication and configure SSH in +the usual way, by creating a `.ssh` sub-directory under your `users` directory, +for example `C:\Users\Jim\.ssh`. For example, you could create a `config` file +there with these settings. + + Host github.com + IdentityFile ~/.ssh/ + User + + Host * + AddKeysToAgent yes + +That will cause the SSH identity you use for GitHub to be loaded in the agent so +you don't have to retype the private key passcode every time. + +You can discover the OpenSSH executable path by running a Powershell command +like this. + + gcm ssh + +The output could be like this (spaces have been compressed). + + CommandType Name Version Source + ----------- ---- ------- ------ + Application ssh-add.exe 8.1.0.1 C:\Windows\System32\OpenSSH\ssh.exe + +You can configure Git to use OpenSSH in the current Powershell session by +setting an environment variable, like this. + + $env:GIT_SSH = "C:\Windows\System32\OpenSSH\ssh.exe" + +You can configure Git to use OpenSSH in all your future Powershell sessions by +configuring a permanent environment variable, like this. + + [Environment]::SetEnvironmentVariable( + "GIT_SSH", "C:\Windows\System32\OpenSSH\ssh.exe", "User") + +TOTH [stackoverflow.com/a/55389713/7657675](https://stackoverflow.com/a/55389713/7657675) +Looks like you have to exit the Powershell session in which you ran that +command for it to take effect. + +You can check the value has been set by printing all environment variables. To +do that run a command like this. + + get-childitem env: diff --git a/grimassist.py b/grimassist.py index c1bc8dd9..1bdf3f31 100644 --- a/grimassist.py +++ b/grimassist.py @@ -9,7 +9,7 @@ FORMAT = "%(asctime)s %(levelname)s %(name)s: %(funcName)s: %(message)s" -log_path = os.environ['USERPROFILE']+'\Grimassist' +log_path = os.environ["USERPROFILE"] + "\Grimassist" if not os.path.isdir(log_path): os.mkdir(log_path) @@ -17,7 +17,7 @@ format=FORMAT, level=logging.INFO, handlers=[ - logging.FileHandler(log_path+'\log.txt', mode="w"), + logging.FileHandler(log_path + "\log.txt", mode="w"), logging.StreamHandler(sys.stdout), ], ) diff --git a/here.code-workspace b/here.code-workspace new file mode 100644 index 00000000..d9daf06e --- /dev/null +++ b/here.code-workspace @@ -0,0 +1,30 @@ +// This is a workspace declaration to facilitate use of VSCode and VSCodium to +// edit the contents of this directory. +// See https://code.visualstudio.com/docs/editor/multi-root-workspaces +{ + "folders": [ + { "path": ".", "name": "Face Commander"} + ], + "settings": { + // Variable names are documented in the VSCod[e|ium] Settings. + // Search for "window title". + "window.title": "${activeEditorShort}${separator}${folderName}" + }, + "extensions": { + "recommendations": [ + // Plugin to open diagrams.net aka draw.io diagrams in VSCod[e|ium]. + "hediet.vscode-drawio", + + // Word wrapping for Markdown and comments. + "stkb.rewrap", + + // Plugin to open PDF files in VSCod[e|ium]. + "tomoki1207.pdf", + + // Plugin to count words in the file or in the selection. + "witulski.selection-word-count" + ] + } +} +// (c) 2024 The ACE Centre-North, UK registered charity 1089313. +// MIT licensed, see https://opensource.org/licenses/MIT \ No newline at end of file diff --git a/src/accel_graph.py b/src/accel_graph.py index 718dd20d..90e10fab 100644 --- a/src/accel_graph.py +++ b/src/accel_graph.py @@ -3,7 +3,6 @@ class AccelGraph(metaclass=abc.ABCMeta): - def __init__(self): pass @@ -13,7 +12,6 @@ def __call__(self, x: float) -> float: class SigmoidAccel(AccelGraph): - def __init__(self, shift_x=5, slope=0.3, multiply=1.2): super().__init__() self.shift_x = shift_x diff --git a/src/camera_manager.py b/src/camera_manager.py index 9a4b51c8..170555b3 100644 --- a/src/camera_manager.py +++ b/src/camera_manager.py @@ -20,41 +20,41 @@ def add_overlay(background, overlay, x, y, width, height): - - background_section = background[y:y + height, x:x + width] - overlay_section = overlay[y:y + height, x:x + width] - background[y:y + height, - x:x + width] = cv2.addWeighted(background_section, 0.3, - overlay_section, 0.7, 0) + background_section = background[y : y + height, x : x + width] + overlay_section = overlay[y : y + height, x : x + width] + background[y : y + height, x : x + width] = cv2.addWeighted( + background_section, 0.3, overlay_section, 0.7, 0 + ) return background class CameraManager(metaclass=Singleton): - def __init__(self): logger.info("Initialize CameraManager singleton") self.thread_cameras = None # Load placeholder image self.placeholder_im = Image.open("assets/images/placeholder.png") - self.placeholder_im = np.array(self.placeholder_im.convert('RGB')) + self.placeholder_im = np.array(self.placeholder_im.convert("RGB")) # Overlays self.overlay_active = cv2.cvtColor( - cv2.imread("assets/images/overlays/active.png", - cv2.IMREAD_UNCHANGED), cv2.COLOR_BGRA2RGB) + cv2.imread("assets/images/overlays/active.png", cv2.IMREAD_UNCHANGED), + cv2.COLOR_BGRA2RGB, + ) self.overlay_disabled = cv2.cvtColor( - cv2.imread("assets/images/overlays/disabled.png", - cv2.IMREAD_UNCHANGED), cv2.COLOR_BGRA2RGB) + cv2.imread("assets/images/overlays/disabled.png", cv2.IMREAD_UNCHANGED), + cv2.COLOR_BGRA2RGB, + ) self.overlay_face_not_detected = cv2.cvtColor( - cv2.imread("assets/images/overlays/face_not_detected.png", - cv2.IMREAD_UNCHANGED), cv2.COLOR_BGRA2RGB) + cv2.imread( + "assets/images/overlays/face_not_detected.png", cv2.IMREAD_UNCHANGED + ), + cv2.COLOR_BGRA2RGB, + ) # Use dict for pass as reference - self.frame_buffers = { - "raw": self.placeholder_im, - "debug": self.placeholder_im - } + self.frame_buffers = {"raw": self.placeholder_im, "debug": self.placeholder_im} self.is_active = False self.is_destroyed = False @@ -111,15 +111,20 @@ def draw_overlay(self, tracking_location): # Disabled if not Keybinder().is_active.get(): self.frame_buffers["debug"] = add_overlay( - self.frame_buffers["debug"], self.overlay_disabled, 0, 0, 640, - 108) + self.frame_buffers["debug"], self.overlay_disabled, 0, 0, 640, 108 + ) return # Face not detected - if (tracking_location is None): + if tracking_location is None: self.frame_buffers["debug"] = add_overlay( - self.frame_buffers["debug"], self.overlay_face_not_detected, 0, - 0, 640, 108) + self.frame_buffers["debug"], + self.overlay_face_not_detected, + 0, + 0, + 640, + 108, + ) return @@ -128,17 +133,30 @@ def draw_overlay(self, tracking_location): if ConfigManager().config["use_transformation_matrix"]: cx = ConfigManager().config["fix_width"] // 2 cy = ConfigManager().config["fix_height"] // 2 - cv2.line(self.frame_buffers["debug"], (cx, cy), - (int(tracking_location[0]), int(tracking_location[1])), (0, 255, 0), 3) - - cv2.circle(self.frame_buffers["debug"], - (int(tracking_location[0]), int(tracking_location[1])), 6, (255, 0, 0), - -1) + cv2.line( + self.frame_buffers["debug"], + (cx, cy), + (int(tracking_location[0]), int(tracking_location[1])), + (0, 255, 0), + 3, + ) + + cv2.circle( + self.frame_buffers["debug"], + (int(tracking_location[0]), int(tracking_location[1])), + 6, + (255, 0, 0), + -1, + ) else: - cv2.circle(self.frame_buffers["debug"], - (int(tracking_location[0]), int(tracking_location[1])), 4, - (255, 255, 255), -1) + cv2.circle( + self.frame_buffers["debug"], + (int(tracking_location[0]), int(tracking_location[1])), + 4, + (255, 255, 255), + -1, + ) # ---------------------------------------------------------------------------- # @@ -146,8 +164,7 @@ def draw_overlay(self, tracking_location): # ---------------------------------------------------------------------------- # -class ThreadCameras(): - +class ThreadCameras: def __init__(self, frame_buffers: dict): logger.info("Initializing ThreadCamera") self.lock = threading.Lock() @@ -159,23 +176,23 @@ def __init__(self, frame_buffers: dict): # Open all cameras self.cameras = {} - self.assign_exe = Thread(target=utils.assign_cameras_queue, - args=(self.cameras, self.assign_done, - MAX_SEARCH_CAMS), - daemon=True) + self.assign_exe = Thread( + target=utils.assign_cameras_queue, + args=(self.cameras, self.assign_done, MAX_SEARCH_CAMS), + daemon=True, + ) self.assign_exe.start() # Decide what camera to use - self.current_id = None #ConfigManager().config["camera_id"] + self.current_id = None # ConfigManager().config["camera_id"] logger.info(f"Found default camera_id {self.current_id}") - self.loop_exe = Thread(target=self.read_camera_loop, - args=(self.stop_flag,), - daemon=True) + self.loop_exe = Thread( + target=self.read_camera_loop, args=(self.stop_flag,), daemon=True + ) self.loop_exe.start() def assign_done(self): - """Set default camera after assign_cameras is done - """ + """Set default camera after assign_cameras is done""" logger.info(f"Assign cameras completed. Found {self.cameras}") init_id = ConfigManager().config["camera_id"] @@ -234,8 +251,9 @@ def read_camera_loop(self, stop_flag) -> None: time.sleep(1) continue - if (self.current_id in self.cameras) and (self.cameras[self.current_id] - is not None): + if (self.current_id in self.cameras) and ( + self.cameras[self.current_id] is not None + ): ret, frame = self.cameras[self.current_id].read() cv2.waitKey(1) if not ret: @@ -250,17 +268,23 @@ def read_camera_loop(self, stop_flag) -> None: h, w, _ = frame.shape # Trim image - if h != ConfigManager().config["fix_height"] or w != ConfigManager( - ).config["fix_width"]: + if ( + h != ConfigManager().config["fix_height"] + or w != ConfigManager().config["fix_width"] + ): target_width = int(h * 4 / 3) if w > target_width: trim_width = w - target_width trim_left = trim_width // 2 trim_right = trim_width - trim_left frame = frame[:, trim_left:-trim_right, :] - frame = cv2.resize(frame, - (ConfigManager().config["fix_width"], - ConfigManager().config["fix_height"])) + frame = cv2.resize( + frame, + ( + ConfigManager().config["fix_width"], + ConfigManager().config["fix_height"], + ), + ) frame = cv2.flip(frame, 1) self.frame_buffers["raw"] = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) diff --git a/src/config_manager.py b/src/config_manager.py index 0324182a..aac2de33 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -39,7 +39,6 @@ class ConfigManager(metaclass=Singleton): - def __init__(self): self.temp_keyboard_bindings = None self.temp_mouse_bindings = None @@ -96,23 +95,22 @@ def add_profile(self): # Random name base on local timestamp new_profile_name = "profile_z" + str(hex(int(time.time() * 1000)))[2:] logger.info(f"Add profile {new_profile_name}") - shutil.copytree(BACKUP_PROFILE, - Path(DEFAULT_JSON.parent, new_profile_name)) + shutil.copytree(BACKUP_PROFILE, Path(DEFAULT_JSON.parent, new_profile_name)) self.profiles.append(new_profile_name) logger.info(f"Current profiles: {self.profiles}") def rename_profile(self, old_profile_name, new_profile_name): logger.info(f"Rename profile {old_profile_name} to {new_profile_name}") - shutil.move(Path(DEFAULT_JSON.parent, old_profile_name), - Path(DEFAULT_JSON.parent, new_profile_name)) + shutil.move( + Path(DEFAULT_JSON.parent, old_profile_name), + Path(DEFAULT_JSON.parent, new_profile_name), + ) self.profiles.remove(old_profile_name) self.profiles.append(new_profile_name) if self.current_profile_name.get() == old_profile_name: self.current_profile_name.set(new_profile_name) - - def load_profile(self, profile_name: str): profile_path = Path(DEFAULT_JSON.parent, profile_name) logger.info(f"Loading profile: {profile_path}") @@ -121,9 +119,11 @@ def load_profile(self, profile_name: str): mouse_bindings_file = Path(profile_path, "mouse_bindings.json") keyboard_bindings_file = Path(profile_path, "keyboard_bindings.json") - if (not cursor_config_file.is_file()) or ( - not mouse_bindings_file.is_file()) or ( - not keyboard_bindings_file.is_file()): + if ( + (not cursor_config_file.is_file()) + or (not mouse_bindings_file.is_file()) + or (not keyboard_bindings_file.is_file()) + ): logger.critical( f"{profile_path.as_posix()} Invalid configuration files or missing files, exit program..." ) @@ -164,8 +164,8 @@ def set_temp_config(self, field: str, value): def write_config_file(self): cursor_config_file = Path(self.current_profile_path, "cursor.json") logger.info(f"Writing config file {cursor_config_file}") - with open(cursor_config_file, 'w') as f: - json.dump(self.config, f, indent=4, separators=(', ', ': ')) + with open(cursor_config_file, "w") as f: + json.dump(self.config, f, indent=4, separators=(", ", ": ")) def apply_config(self): logger.info("Applying config") @@ -175,25 +175,32 @@ def apply_config(self): # ------------------------------ MOUSE BINDINGS CONFIG ----------------------------- # - def set_temp_mouse_binding(self, gesture, device: str, action: str, - threshold: float, trigger: Trigger): - + def set_temp_mouse_binding( + self, gesture, device: str, action: str, threshold: float, trigger: Trigger + ): logger.info( "setting keybind for gesture: %s, device: %s, key: %s, threshold: %s, trigger: %s", - gesture, device, action, threshold, trigger.value) + gesture, + device, + action, + threshold, + trigger.value, + ) # Remove duplicate keybindings self.remove_temp_mouse_binding(device, action) # Assign self.temp_mouse_bindings[gesture] = [ - device, action, float(threshold), trigger.value + device, + action, + float(threshold), + trigger.value, ] self.unsave_mouse_bindings = True def remove_temp_mouse_binding(self, device: str, action: str): - logger.info( - f"remove_temp_mouse_binding for device: {device}, key: {action}") + logger.info(f"remove_temp_mouse_binding for device: {device}, key: {action}") out_keybindings = {} for key, vals in self.temp_mouse_bindings.items(): if (device == vals[0]) and (action == vals[1]): @@ -209,39 +216,48 @@ def apply_mouse_bindings(self): self.unsave_mouse_bindings = False def write_mouse_bindings_file(self): - mouse_bindings_file = Path(self.current_profile_path, - "mouse_bindings.json") + mouse_bindings_file = Path(self.current_profile_path, "mouse_bindings.json") logger.info(f"Writing keybindings file {mouse_bindings_file}") - with open(mouse_bindings_file, 'w') as f: + with open(mouse_bindings_file, "w") as f: out_json = dict(sorted(self.mouse_bindings.items())) - json.dump(out_json, f, indent=4, separators=(', ', ': ')) + json.dump(out_json, f, indent=4, separators=(", ", ": ")) # ------------------------------ KEYBOARD BINDINGS CONFIG ----------------------------- # - def set_temp_keyboard_binding(self, device: str, key_action: str, - gesture: str, threshold: float, - trigger: Trigger): + def set_temp_keyboard_binding( + self, + device: str, + key_action: str, + gesture: str, + threshold: float, + trigger: Trigger, + ): logger.info( "setting keybind for gesture: %s, device: %s, key: %s, threshold: %s, trigger: %s", - gesture, device, key_action, threshold, trigger.value) + gesture, + device, + key_action, + threshold, + trigger.value, + ) # Remove duplicate keybindings self.remove_temp_keyboard_binding(device, key_action, gesture) # Assign self.temp_keyboard_bindings[gesture] = [ - device, key_action, - float(threshold), trigger.value + device, + key_action, + float(threshold), + trigger.value, ] self.unsave_keyboard_bindings = True - def remove_temp_keyboard_binding(self, - device: str, - key_action: str = "None", - gesture: str = "None"): - """Remove binding from config by providing either key_action or gesture. - """ + def remove_temp_keyboard_binding( + self, device: str, key_action: str = "None", gesture: str = "None" + ): + """Remove binding from config by providing either key_action or gesture.""" logger.info( f"remove_temp_keyboard_binding for device: {device}, key: {key_action} or gesture {gesture}" @@ -269,13 +285,14 @@ def apply_keyboard_bindings(self): self.unsave_keyboard_bindings = False def write_keyboard_bindings_file(self): - keyboard_bindings_file = Path(self.current_profile_path, - "keyboard_bindings.json") + keyboard_bindings_file = Path( + self.current_profile_path, "keyboard_bindings.json" + ) logger.info(f"Writing keyboard bindings file {keyboard_bindings_file}") - with open(keyboard_bindings_file, 'w') as f: + with open(keyboard_bindings_file, "w") as f: out_json = dict(sorted(self.keyboard_bindings.items())) - json.dump(out_json, f, indent=4, separators=(', ', ': ')) + json.dump(out_json, f, indent=4, separators=(", ", ": ")) # ---------------------------------------------------------------------------- # def apply_all(self): diff --git a/src/controllers/__init__.py b/src/controllers/__init__.py index f2de65fb..e9725379 100644 --- a/src/controllers/__init__.py +++ b/src/controllers/__init__.py @@ -1,3 +1,3 @@ -__all__ = ['Keybinder', 'MouseController'] +__all__ = ["Keybinder", "MouseController"] from .keybinder import Keybinder from .mouse_controller import MouseController diff --git a/src/controllers/keybinder.py b/src/controllers/keybinder.py index db80c83b..71e5c93f 100644 --- a/src/controllers/keybinder.py +++ b/src/controllers/keybinder.py @@ -20,7 +20,6 @@ class Keybinder(metaclass=Singleton): - def __init__(self) -> None: self.delay_count = None self.key_states = None @@ -50,14 +49,15 @@ def start(self): def init_states(self) -> None: """Re-initializes the state of the keybinder. - If new keybindings are added. + If new keybindings are added. """ # keep states for all registered keys. self.key_states = {} self.start_hold_ts = {} - for _, v in (ConfigManager().mouse_bindings | - ConfigManager().keyboard_bindings).items(): - state_name = v[0]+"_"+v[1] + for _, v in ( + ConfigManager().mouse_bindings | ConfigManager().keyboard_bindings + ).items(): + state_name = v[0] + "_" + v[1] self.key_states[state_name] = False self.schedule_toggle_off[state_name] = False self.schedule_toggle_on[state_name] = True @@ -65,8 +65,8 @@ def init_states(self) -> None: self.holding[state_name] = False self.last_know_keybindings = copy.deepcopy( - (ConfigManager().mouse_bindings | - ConfigManager().keyboard_bindings)) + (ConfigManager().mouse_bindings | ConfigManager().keyboard_bindings) + ) def get_monitors(self) -> list[dict]: out_list = [] @@ -85,11 +85,9 @@ def get_monitors(self) -> list[dict]: return out_list def get_current_monitor(self) -> int: - x, y = pydirectinput.position() for mon_id, mon in enumerate(self.monitors): - if x >= mon["x1"] and x <= mon["x2"] and y >= mon[ - "y1"] and y <= mon["y2"]: + if x >= mon["x1"] and x <= mon["x2"] and y >= mon["y1"] and y <= mon["y2"]: return mon_id # raise Exception("Monitor not found") return 0 @@ -98,7 +96,6 @@ def meta_action(self, val, action, threshold, is_active: bool) -> None: state_name = "meta_" + action if action == "pause": - if (val > threshold) and (self.key_states[state_name] is False): mon_id = self.get_current_monitor() if mon_id is None: @@ -111,33 +108,30 @@ def meta_action(self, val, action, threshold, is_active: bool) -> None: self.key_states[state_name] = False if is_active: - if action == "reset": - if (val > threshold) and (self.key_states[state_name] is - False): + if (val > threshold) and (self.key_states[state_name] is False): mon_id = self.get_current_monitor() if mon_id is None: return pydirectinput.moveTo( self.monitors[mon_id]["center_x"], - self.monitors[mon_id]["center_y"]) + self.monitors[mon_id]["center_y"], + ) self.key_states[state_name] = True - elif (val < threshold) and (self.key_states[state_name] is - True): + elif (val < threshold) and (self.key_states[state_name] is True): self.key_states[state_name] = False elif action == "cycle": - if (val > threshold) and (self.key_states[state_name] is - False): + if (val > threshold) and (self.key_states[state_name] is False): mon_id = self.get_current_monitor() next_mon_id = (mon_id + 1) % len(self.monitors) pydirectinput.moveTo( self.monitors[next_mon_id]["center_x"], - self.monitors[next_mon_id]["center_y"]) + self.monitors[next_mon_id]["center_y"], + ) self.key_states[state_name] = True - elif (val < threshold) and (self.key_states[state_name] is - True): + elif (val < threshold) and (self.key_states[state_name] is True): self.key_states[state_name] = False def mouse_action(self, val, action, threshold, mode) -> None: @@ -168,13 +162,13 @@ def mouse_action(self, val, action, threshold, mode) -> None: self.key_states[state_name] = True if self.holding[state_name] is False and ( - ((time.time() - self.start_hold_ts[state_name]) * 1000) >= - ConfigManager().config["hold_trigger_ms"]): + ((time.time() - self.start_hold_ts[state_name]) * 1000) + >= ConfigManager().config["hold_trigger_ms"] + ): pydirectinput.mouseDown(button=action) self.holding[state_name] = True elif (val < threshold) and (self.key_states[state_name] is True): - self.key_states[state_name] = False if self.holding[state_name]: @@ -210,8 +204,9 @@ def mouse_action(self, val, action, threshold, mode) -> None: self.start_hold_ts[state_name] = time.time() if self.key_states[state_name] is True: - if (((time.time() - self.start_hold_ts[state_name]) * 1000) - >= ConfigManager().config["rapid_fire_interval_ms"]): + if ( + (time.time() - self.start_hold_ts[state_name]) * 1000 + ) >= ConfigManager().config["rapid_fire_interval_ms"]: pydirectinput.click(button=action) self.holding[state_name] = True self.start_hold_ts[state_name] = time.time() @@ -222,7 +217,6 @@ def mouse_action(self, val, action, threshold, mode) -> None: self.start_hold_ts[state_name] = math.inf def keyboard_action(self, val, keysym, threshold, mode): - state_name = "keyboard_" + keysym if mode == Trigger.SINGLE: @@ -250,13 +244,13 @@ def keyboard_action(self, val, keysym, threshold, mode): self.key_states[state_name] = True if self.holding[state_name] is False and ( - ((time.time() - self.start_hold_ts[state_name]) * 1000) >= - ConfigManager().config["hold_trigger_ms"]): + ((time.time() - self.start_hold_ts[state_name]) * 1000) + >= ConfigManager().config["hold_trigger_ms"] + ): pydirectinput.keyDown(key=keysym) self.holding[state_name] = True elif (val < threshold) and (self.key_states[state_name] is True): - self.key_states[state_name] = False if self.holding[state_name]: @@ -292,8 +286,9 @@ def keyboard_action(self, val, keysym, threshold, mode): self.start_hold_ts[state_name] = time.time() if self.key_states[state_name] is True: - if (((time.time() - self.start_hold_ts[state_name]) * 1000) - >= ConfigManager().config["rapid_fire_interval_ms"]): + if ( + (time.time() - self.start_hold_ts[state_name]) * 1000 + ) >= ConfigManager().config["rapid_fire_interval_ms"]: pydirectinput.press(keys=keysym) self.holding[state_name] = True self.start_hold_ts[state_name] = time.time() @@ -316,12 +311,14 @@ def act(self, blendshape_values) -> None: if blendshape_values is None: return - if (ConfigManager().mouse_bindings | - ConfigManager().keyboard_bindings) != self.last_know_keybindings: + if ( + ConfigManager().mouse_bindings | ConfigManager().keyboard_bindings + ) != self.last_know_keybindings: self.init_states() - for shape_name, v in (ConfigManager().mouse_bindings | - ConfigManager().keyboard_bindings).items(): + for shape_name, v in ( + ConfigManager().mouse_bindings | ConfigManager().keyboard_bindings + ).items(): if shape_name not in shape_list.blendshape_names: continue @@ -335,7 +332,6 @@ def act(self, blendshape_values) -> None: self.meta_action(val, action, threshold, self.is_active.get()) if self.is_active.get(): - if device == "mouse": self.mouse_action(val, action, threshold, mode) diff --git a/src/controllers/mouse_controller.py b/src/controllers/mouse_controller.py index 64330225..25c8822d 100644 --- a/src/controllers/mouse_controller.py +++ b/src/controllers/mouse_controller.py @@ -23,7 +23,6 @@ class MouseController(metaclass=Singleton): - def __init__(self): self.screen_h = None self.screen_w = None @@ -88,8 +87,7 @@ def act(self, tracking_location: npt.ArrayLike): self.current_tracking_location = tracking_location def main_loop(self) -> None: - """ Separate thread for mouse controller - """ + """Separate thread for mouse controller""" if self.is_destroyed: return @@ -104,7 +102,8 @@ def main_loop(self) -> None: # Get latest x, y and smooth. smooth_px, smooth_py = utils.apply_smoothing( - self.buffer, self.smooth_kernel) + self.buffer, self.smooth_kernel + ) vel_x = smooth_px - self.prev_x vel_y = smooth_py - self.prev_y diff --git a/src/detectors/__init__.py b/src/detectors/__init__.py index acc4f230..ec18a08e 100644 --- a/src/detectors/__init__.py +++ b/src/detectors/__init__.py @@ -1,2 +1,2 @@ -__all__ = ['FaceMesh'] +__all__ = ["FaceMesh"] from .facemesh import FaceMesh diff --git a/src/detectors/facemesh.py b/src/detectors/facemesh.py index 028211a5..23732158 100644 --- a/src/detectors/facemesh.py +++ b/src/detectors/facemesh.py @@ -26,7 +26,6 @@ class FaceMesh(metaclass=Singleton): - def __init__(self): self.smooth_kernel = None logger.info("Initialize FaceMesh singleton") @@ -51,16 +50,20 @@ def start(self): output_facial_transformation_matrixes=True, running_mode=mp.tasks.vision.RunningMode.LIVE_STREAM, num_faces=1, - result_callback=self.mp_callback) + result_callback=self.mp_callback, + ) self.model = vision.FaceLandmarker.create_from_options(options) self.calc_smooth_kernel() def calc_smooth_kernel(self): self.smooth_kernel = utils.calc_smooth_kernel( - ConfigManager().config["shape_smooth"]) + ConfigManager().config["shape_smooth"] + ) - def calculate_tracking_location(self, mp_result, use_transformation_matrix=False) -> ndarray[Any, dtype[Any]]: + def calculate_tracking_location( + self, mp_result, use_transformation_matrix=False + ) -> ndarray[Any, dtype[Any]]: screen_w = ConfigManager().config["fix_width"] screen_h = ConfigManager().config["fix_height"] landmarks = mp_result.face_landmarks[0] @@ -93,30 +96,35 @@ def calculate_tracking_location(self, mp_result, use_transformation_matrix=False return np.array([x_pixel, y_pixel], np.float32) - def mp_callback(self, mp_result: FaceLandmarkerResult, output_image: mediapipe_image.Image, timestamp_ms: int) -> None: - if len(mp_result.face_landmarks) >= 1 and len( - mp_result.face_blendshapes) >= 1: + def mp_callback( + self, + mp_result: FaceLandmarkerResult, + output_image: mediapipe_image.Image, + timestamp_ms: int, + ) -> None: + if len(mp_result.face_landmarks) >= 1 and len(mp_result.face_blendshapes) >= 1: self.mp_landmarks = mp_result.face_landmarks[0] # Point for moving pointer self.tracking_location = self.calculate_tracking_location( mp_result, - use_transformation_matrix=ConfigManager( - ).config["use_transformation_matrix"]) - self.blendshapes_buffer = np.roll(self.blendshapes_buffer, - shift=-1, - axis=0) + use_transformation_matrix=ConfigManager().config[ + "use_transformation_matrix" + ], + ) + self.blendshapes_buffer = np.roll(self.blendshapes_buffer, shift=-1, axis=0) self.blendshapes_buffer[-1] = np.array( - [b.score for b in mp_result.face_blendshapes[0]]) + [b.score for b in mp_result.face_blendshapes[0]] + ) self.smooth_blendshapes = utils.apply_smoothing( - self.blendshapes_buffer, self.smooth_kernel) + self.blendshapes_buffer, self.smooth_kernel + ) else: self.mp_landmarks = None self.tracking_location = None def detect_frame(self, frame_np: npt.ArrayLike): - t_ms = int(time.time() * 1000) if t_ms <= self.latest_time_ms: return diff --git a/src/gui/__init__.py b/src/gui/__init__.py index 36252c85..a574fa27 100644 --- a/src/gui/__init__.py +++ b/src/gui/__init__.py @@ -1,2 +1,2 @@ -__all__ = ['MainGui'] +__all__ = ["MainGui"] from .main_gui import MainGui diff --git a/src/gui/balloon.py b/src/gui/balloon.py index 9607b1e8..654e8e2e 100644 --- a/src/gui/balloon.py +++ b/src/gui/balloon.py @@ -6,10 +6,8 @@ BALLOON_SIZE = (305, 80) -class Balloon(): - +class Balloon: def __init__(self, master, image_path: str): - self.float_window = customtkinter.CTkToplevel(master) self.float_window.wm_overrideredirect(True) self.float_window.lift() @@ -18,21 +16,23 @@ def __init__(self, master, image_path: str): self.float_window.wm_attributes("-transparentcolor", "white") # Hide icon in taskbar - self.float_window.wm_attributes('-toolwindow', 'True') + self.float_window.wm_attributes("-toolwindow", "True") self.balloon_image = customtkinter.CTkImage( - Image.open(image_path).resize(BALLOON_SIZE), size=BALLOON_SIZE) + Image.open(image_path).resize(BALLOON_SIZE), size=BALLOON_SIZE + ) self.label = customtkinter.CTkLabel( self.float_window, text="", - compound='center', - #anchor="nw", - justify='left', + compound="center", + # anchor="nw", + justify="left", width=BALLOON_SIZE[0], height=BALLOON_SIZE[1], text_color="#3C4043", - image=self.balloon_image) + image=self.balloon_image, + ) self.label.cget("font").configure(size=16) self.label.grid(row=0, column=0, sticky="nsew") @@ -47,7 +47,6 @@ def register_widget(self, widget, text: str): widget.bind("", partial(self.hide_balloon, widget)) def show_balloon(self, widget, text, event): - if not self._displayed: self.label.configure(text=text) self._displayed = True @@ -59,8 +58,6 @@ def show_balloon(self, widget, text, event): self.float_window.deiconify() def hide_balloon(self, widget, event): - if self._displayed: - self._displayed = False self.float_window.withdraw() diff --git a/src/gui/dropdown.py b/src/gui/dropdown.py index da5defad..33381f68 100644 --- a/src/gui/dropdown.py +++ b/src/gui/dropdown.py @@ -18,14 +18,18 @@ def mouse_in_widget(mouse_x, mouse_y, widget, expand_x=(0, 0), expand_y=(0, 0)): widget_y1 = widget.winfo_rooty() - expand_y[0] widget_x2 = widget_x1 + widget.winfo_width() + expand_x[0] + expand_x[1] widget_y2 = widget_y1 + widget.winfo_height() + expand_y[0] + expand_y[1] - if mouse_x >= widget_x1 and mouse_x <= widget_x2 and mouse_y >= widget_y1 and mouse_y <= widget_y2: + if ( + mouse_x >= widget_x1 + and mouse_x <= widget_x2 + and mouse_y >= widget_y1 + and mouse_y <= widget_y2 + ): return True else: return False -class Dropdown(): - +class Dropdown: def __init__(self, master, dropdown_items: dict, width, callback: callable): self.master_toplevel = master.winfo_toplevel() @@ -36,10 +40,10 @@ def __init__(self, master, dropdown_items: dict, width, callback: callable): self.float_window.wm_attributes("-topmost", True) # Hide icon in taskbar - self.float_window.wm_attributes('-toolwindow', 'True') + self.float_window.wm_attributes("-toolwindow", "True") self.float_window.grid_rowconfigure(MAX_ROWS, weight=1) self.float_window.grid_columnconfigure(1, weight=1) - #self.float_window.group(master) + # self.float_window.group(master) self._displayed = True self.dropdown_keys = list(dropdown_items.keys()) @@ -61,36 +65,34 @@ def create_divs(self, master, ges_images: dict, width: int) -> dict: divs = {} for row, (gesture, image_path) in enumerate(ges_images.items()): image = customtkinter.CTkImage( - Image.open(image_path).resize(ICON_SIZE), size=ICON_SIZE) + Image.open(image_path).resize(ICON_SIZE), size=ICON_SIZE + ) # Label ? - row_btn = customtkinter.CTkButton(master=master, - width=width, - height=ITEM_HEIGHT, - text=gesture, - border_width=0, - corner_radius=0, - image=image, - hover=True, - fg_color=LIGHT_BLUE, - hover_color="gray90", - text_color_disabled="gray80", - compound="left", - anchor="nw") - - row_btn.grid(row=row, - column=0, - padx=(0, 0), - pady=(0, 0), - sticky="nsew") + row_btn = customtkinter.CTkButton( + master=master, + width=width, + height=ITEM_HEIGHT, + text=gesture, + border_width=0, + corner_radius=0, + image=image, + hover=True, + fg_color=LIGHT_BLUE, + hover_color="gray90", + text_color_disabled="gray80", + compound="left", + anchor="nw", + ) + + row_btn.grid(row=row, column=0, padx=(0, 0), pady=(0, 0), sticky="nsew") divs[gesture] = {"button": row_btn, "image": image} return divs def mouse_release(self, event): - """Release mouse and trigger button - """ + """Release mouse and trigger button""" # Check if release which button for gesture, div in self.divs.items(): @@ -105,10 +107,9 @@ def mouse_release(self, event): return def mouse_motion(self, event): - if not mouse_in_widget(event.x_root, - event.y_root, - self.float_window, - expand_y=(Y_OFFSET, 0)): + if not mouse_in_widget( + event.x_root, event.y_root, self.float_window, expand_y=(Y_OFFSET, 0) + ): self.hide_dropdown() return @@ -149,12 +150,12 @@ def enable_all_except(self, target_gestures: list): def refresh_items(self): self.enable_all_except( - list(ConfigManager().mouse_bindings.keys()) + - list(ConfigManager().keyboard_bindings.keys())) + list(ConfigManager().mouse_bindings.keys()) + + list(ConfigManager().keyboard_bindings.keys()) + ) def register_widget(self, widget, name): - widget.bind("", partial(self.show_dropdown, widget, - name)) + widget.bind("", partial(self.show_dropdown, widget, name)) def show_dropdown(self, widget, name, event): # Close the opening dropdown first @@ -162,7 +163,6 @@ def show_dropdown(self, widget, name, event): self.hide_dropdown() if not self._displayed: - self.refresh_items() draw_x = widget.winfo_rootx() @@ -174,22 +174,23 @@ def show_dropdown(self, widget, name, event): # Use bind_all in case hold over label, canvas self.bind_id_release = self.float_window.bind_all( - "", self.mouse_release) + "", self.mouse_release + ) self.bind_id_motion = self.float_window.bind_all( - "", self.mouse_motion) - self.float_window.wm_attributes('-disabled', False) + "", self.mouse_motion + ) + self.float_window.wm_attributes("-disabled", False) # Set current user self.current_user = name self._displayed = True def hide_dropdown(self, event=None): - if self._displayed: # Remove bindings and completely disable the window self.float_window.unbind("", self.bind_id_release) self.float_window.unbind("", self.bind_id_motion) - self.float_window.wm_attributes('-disabled', True) + self.float_window.wm_attributes("-disabled", True) self._displayed = False diff --git a/src/gui/frames/__init__.py b/src/gui/frames/__init__.py index 20a2fdd6..05d43e18 100644 --- a/src/gui/frames/__init__.py +++ b/src/gui/frames/__init__.py @@ -1,4 +1,10 @@ -__all__ = ['FrameCamPreview', 'FrameMenu', 'FrameProfileEditor', 'FrameProfileSwitcher', 'SafeDisposableFrame'] +__all__ = [ + "FrameCamPreview", + "FrameMenu", + "FrameProfileEditor", + "FrameProfileSwitcher", + "SafeDisposableFrame", +] from .frame_cam_preview import FrameCamPreview from .frame_menu import FrameMenu from .frame_profile_editor import FrameProfileEditor diff --git a/src/gui/frames/frame_cam_preview.py b/src/gui/frames/frame_cam_preview.py index 40c7669d..54499074 100644 --- a/src/gui/frames/frame_cam_preview.py +++ b/src/gui/frames/frame_cam_preview.py @@ -15,7 +15,6 @@ class FrameCamPreview(SafeDisposableFrame): - def __init__(self, master, master_callback: callable, **kwargs): super().__init__(master, **kwargs) @@ -26,29 +25,30 @@ def __init__(self, master, master_callback: callable, **kwargs): # Canvas. self.placeholder_im = Image.open("assets/images/placeholder.png") self.placeholder_im = ImageTk.PhotoImage( - image=self.placeholder_im.resize((CANVAS_WIDTH, CANVAS_HEIGHT))) - - self.canvas = tkinter.Canvas(master=self, - width=CANVAS_WIDTH, - height=CANVAS_HEIGHT, - bg=LIGHT_BLUE, - bd=0, - relief='ridge', - highlightthickness=0) + image=self.placeholder_im.resize((CANVAS_WIDTH, CANVAS_HEIGHT)) + ) + + self.canvas = tkinter.Canvas( + master=self, + width=CANVAS_WIDTH, + height=CANVAS_HEIGHT, + bg=LIGHT_BLUE, + bd=0, + relief="ridge", + highlightthickness=0, + ) self.canvas.grid(row=0, column=0, padx=10, pady=10, sticky="sw") # Toggle label - self.toggle_label = customtkinter.CTkLabel(master=self, - compound='right', - text="Face control", - text_color="black", - justify=tkinter.LEFT) + self.toggle_label = customtkinter.CTkLabel( + master=self, + compound="right", + text="Face control", + text_color="black", + justify=tkinter.LEFT, + ) self.toggle_label.cget("font").configure(size=14) - self.toggle_label.grid(row=1, - column=0, - padx=(10, 0), - pady=5, - sticky="nw") + self.toggle_label.grid(row=1, column=0, padx=(10, 0), pady=5, sticky="nw") # Toggle switch self.toggle_switch = customtkinter.CTkSwitch( @@ -60,38 +60,31 @@ def __init__(self, master, master_callback: callable, **kwargs): switch_width=32, variable=Keybinder().is_active, command=lambda: master_callback( - "toggle_switch", {"switch_status": self.toggle_switch.get()}), + "toggle_switch", {"switch_status": self.toggle_switch.get()} + ), onvalue=1, offvalue=0, ) if ConfigManager().config["auto_play"]: self.toggle_switch.select() - self.toggle_switch.grid(row=1, - column=0, - padx=(100, 0), - pady=5, - sticky="nw") + self.toggle_switch.grid(row=1, column=0, padx=(100, 0), pady=5, sticky="nw") # Toggle description label self.toggle_label = customtkinter.CTkLabel( master=self, - compound='right', + compound="right", text="Allow facial gestures to control\nyour actions. ", text_color="#444746", - justify=tkinter.LEFT) + justify=tkinter.LEFT, + ) self.toggle_label.cget("font").configure(size=12) - self.toggle_label.grid(row=2, - column=0, - padx=(10, 0), - pady=5, - sticky="nw") + self.toggle_label.grid(row=2, column=0, padx=(10, 0), pady=5, sticky="nw") # Set first image. - self.canvas_image = self.canvas.create_image(0, - 0, - image=self.placeholder_im, - anchor=tkinter.NW) + self.canvas_image = self.canvas.create_image( + 0, 0, image=self.placeholder_im, anchor=tkinter.NW + ) self.new_photo = None self.after(1, self.camera_loop) @@ -104,13 +97,12 @@ def camera_loop(self): frame_rgb = CameraManager().get_debug_frame() # Assign ref to avoid garbage collected self.new_photo = ImageTk.PhotoImage( - image=Image.fromarray(frame_rgb).resize((CANVAS_WIDTH, - CANVAS_HEIGHT))) + image=Image.fromarray(frame_rgb).resize((CANVAS_WIDTH, CANVAS_HEIGHT)) + ) self.canvas.itemconfig(self.canvas_image, image=self.new_photo) self.canvas.update() - self.after(ConfigManager().config["tick_interval_ms"], - self.camera_loop) + self.after(ConfigManager().config["tick_interval_ms"], self.camera_loop) def enter(self): super().enter() diff --git a/src/gui/frames/frame_menu.py b/src/gui/frames/frame_menu.py index f5733194..348eb93e 100644 --- a/src/gui/frames/frame_menu.py +++ b/src/gui/frames/frame_menu.py @@ -4,7 +4,13 @@ from PIL import Image from src.config_manager import ConfigManager -from src.gui.pages import PageSelectCamera, PageCursor, PageSelectGestures, PageKeyboard +from src.gui.pages import ( + PageSelectCamera, + PageCursor, + PageSelectGestures, + PageKeyboard, + PageAbout, +) from src.gui.frames.safe_disposable_frame import SafeDisposableFrame LIGHT_BLUE = "#F9FBFE" @@ -13,7 +19,6 @@ class FrameMenu(SafeDisposableFrame): - def __init__(self, master, master_callback: callable, **kwargs): super().__init__(master, **kwargs) @@ -27,41 +32,55 @@ def __init__(self, master, master_callback: callable, **kwargs): self.menu_btn_images = { PageSelectCamera.__name__: [ customtkinter.CTkImage( - Image.open("assets/images/menu_btn_camera.png"), - size=BTN_SIZE), + Image.open("assets/images/menu_btn_camera.png"), size=BTN_SIZE + ), customtkinter.CTkImage( Image.open("assets/images/menu_btn_camera_selected.png"), - size=BTN_SIZE) + size=BTN_SIZE, + ), ], PageCursor.__name__: [ customtkinter.CTkImage( - Image.open("assets/images/menu_btn_cursor.png"), - size=BTN_SIZE), + Image.open("assets/images/menu_btn_cursor.png"), size=BTN_SIZE + ), customtkinter.CTkImage( Image.open("assets/images/menu_btn_cursor_selected.png"), - size=BTN_SIZE) + size=BTN_SIZE, + ), ], PageSelectGestures.__name__: [ customtkinter.CTkImage( - Image.open("assets/images/menu_btn_gestures.png"), - size=BTN_SIZE), + Image.open("assets/images/menu_btn_gestures.png"), size=BTN_SIZE + ), customtkinter.CTkImage( Image.open("assets/images/menu_btn_gestures_selected.png"), - size=BTN_SIZE) + size=BTN_SIZE, + ), ], PageKeyboard.__name__: [ customtkinter.CTkImage( - Image.open("assets/images/menu_btn_keyboard.png"), - size=BTN_SIZE), + Image.open("assets/images/menu_btn_keyboard.png"), size=BTN_SIZE + ), customtkinter.CTkImage( Image.open("assets/images/menu_btn_keyboard_selected.png"), - size=BTN_SIZE) - ] + size=BTN_SIZE, + ), + ], + PageAbout.__name__: [ + customtkinter.CTkImage( + Image.open("assets/images/menu_btn_about.png"), size=BTN_SIZE + ), + customtkinter.CTkImage( + Image.open("assets/images/menu_btn_about_selected.png"), + size=BTN_SIZE, + ), + ], } # Profile button prof_drop = customtkinter.CTkImage( - Image.open("assets/images/prof_drop_head.png"), size=PROF_DROP_SIZE) + Image.open("assets/images/prof_drop_head.png"), size=PROF_DROP_SIZE + ) profile_btn = customtkinter.CTkLabel( master=self, textvariable=ConfigManager().current_profile_name, @@ -71,47 +90,54 @@ def __init__(self, master, master_callback: callable, **kwargs): anchor="w", cursor="hand2", ) - profile_btn.bind("", - partial(self.master_callback, "show_profile_switcher")) + profile_btn.bind( + "", partial(self.master_callback, "show_profile_switcher") + ) - profile_btn.grid(row=0, - column=0, - padx=35, - pady=10, - ipadx=0, - ipady=0, - sticky="nw", - columnspan=1, - rowspan=1) + profile_btn.grid( + row=0, + column=0, + padx=35, + pady=10, + ipadx=0, + ipady=0, + sticky="nw", + columnspan=1, + rowspan=1, + ) self.buttons = {} self.buttons = self.create_tab_btn(self.menu_btn_images, offset=1) self.set_tab_active(PageSelectCamera.__name__) def create_tab_btn(self, btns: dict, offset): - out_dict = {} for idx, (k, im_paths) in enumerate(btns.items()): - btn = customtkinter.CTkButton(master=self, - image=im_paths[0], - anchor="nw", - border_spacing=0, - border_width=0, - hover=False, - corner_radius=0, - text="", - command=partial( - self.master_callback, - function_name="change_page", - args={"target": k})) - - btn.grid(row=idx + offset, - column=0, - padx=(0, 0), - pady=0, - ipadx=0, - ipady=0, - sticky="nw") + btn = customtkinter.CTkButton( + master=self, + image=im_paths[0], + anchor="nw", + border_spacing=0, + border_width=0, + hover=False, + corner_radius=0, + text="", + command=partial( + self.master_callback, + function_name="change_page", + args={"target": k}, + ), + ) + + btn.grid( + row=idx + offset, + column=0, + padx=(0, 0), + pady=0, + ipadx=0, + ipady=0, + sticky="nw", + ) btn.configure(fg_color=LIGHT_BLUE, hover=False) out_dict[k] = btn return out_dict diff --git a/src/gui/frames/frame_profile.py b/src/gui/frames/frame_profile.py index 5fadcbc3..c2f84d27 100644 --- a/src/gui/frames/frame_profile.py +++ b/src/gui/frames/frame_profile.py @@ -10,7 +10,9 @@ from src.config_manager import ConfigManager from src.task_killer import TaskKiller from src.gui.frames.safe_disposable_frame import SafeDisposableFrame -from src.gui.frames.safe_disposable_scrollable_frame import SafeDisposableScrollableFrame +from src.gui.frames.safe_disposable_scrollable_frame import ( + SafeDisposableScrollableFrame, +) logger = logging.getLogger("FrameProfile") @@ -28,22 +30,16 @@ DARK_BLUE = "#1A73E8" BACKUP_PROFILE_NAME = "default" -DIV_COLORS = { - "default": "white", - "hovering": LIGHT_BLUE, - "selected": MEDIUM_BLUE -} +DIV_COLORS = {"default": "white", "hovering": LIGHT_BLUE, "selected": MEDIUM_BLUE} class FrameProfileItems(SafeDisposableScrollableFrame): - def __init__( self, master, refresh_master_fn, **kwargs, ): - super().__init__(master, **kwargs) self.refresh_master_fn = refresh_master_fn self.is_active = False @@ -53,21 +49,21 @@ def __init__( self.edit_image = customtkinter.CTkImage( Image.open("assets/images/edit.png").resize(EDIT_ICON_SIZE), - size=EDIT_ICON_SIZE) + size=EDIT_ICON_SIZE, + ) self.bin_image = customtkinter.CTkImage( Image.open("assets/images/bin.png").resize(BIN_ICON_SIZE), - size=BIN_ICON_SIZE) + size=BIN_ICON_SIZE, + ) self.divs = self.load_initial_profiles() div_id = self.get_div_id(ConfigManager().current_profile_name.get()) self.set_div_selected(self.divs[div_id]) - def load_initial_profiles(self): - """Create div according to profiles in config - """ + """Create div according to profiles in config""" profile_names = ConfigManager().list_profile() divs = {} @@ -85,9 +81,7 @@ def hover_enter(self, div, event): if div["is_selected"]: return for widget_name, widget in div.items(): - target_widgets = [ - "wrap_label", "entry", "edit_button", "bin_button" - ] + target_widgets = ["wrap_label", "entry", "edit_button", "bin_button"] if widget is None: continue if div["is_editing"]: @@ -101,9 +95,7 @@ def hover_leave(self, div, event): if div["is_selected"]: return for widget_name, widget in div.items(): - target_widgets = [ - "wrap_label", "entry", "edit_button", "bin_button" - ] + target_widgets = ["wrap_label", "entry", "edit_button", "bin_button"] if widget is None: continue if div["is_editing"]: @@ -113,8 +105,7 @@ def hover_leave(self, div, event): div["is_hovering"] = False def get_div_id(self, profile_name: str): - """Get div unique id from profile name - """ + """Get div unique id from profile name""" for div_id, div in self.divs.items(): if div["profile_name"] == profile_name: return div_id @@ -123,9 +114,7 @@ def get_div_id(self, profile_name: str): def set_div_inactive(self, target_div): for widget_name, widget in target_div.items(): - target_widgets = [ - "wrap_label", "entry", "edit_button", "bin_button" - ] + target_widgets = ["wrap_label", "entry", "edit_button", "bin_button"] if widget is None: continue @@ -148,9 +137,7 @@ def set_div_selected(self, div: dict, event=None): d["is_selected"] = False for widget_name, widget in div.items(): - target_widgets = [ - "wrap_label", "entry", "edit_button", "bin_button" - ] + target_widgets = ["wrap_label", "entry", "edit_button", "bin_button"] if widget is None: continue @@ -171,8 +158,8 @@ def remove_div(self, div_name): for widget in div.values(): if isinstance( - widget, customtkinter.windows.widgets.core_widget_classes. - CTkBaseClass): + widget, customtkinter.windows.widgets.core_widget_classes.CTkBaseClass + ): widget.grid_forget() widget.destroy() self.refresh_scrollbar() @@ -183,8 +170,7 @@ def clear_divs(self): self.divs = {} def refresh_frame(self): - """Refresh the divs if profile directory has changed - """ + """Refresh the divs if profile directory has changed""" logger.info("Refresh frame_profile") @@ -207,7 +193,6 @@ def refresh_frame(self): # Set and highlight selected profile for div_id, div in self.divs.items(): - if div["profile_name"] == current_profile: self.set_div_selected(div) else: @@ -223,15 +208,14 @@ def rename_button_callback(self, div: dict): hdiv["edit_button"].grid_remove() div["is_editing"] = True - div["entry"].configure(state="normal", - border_width=2, - border_color=LIGHT_GREEN, - fg_color="white") + div["entry"].configure( + state="normal", border_width=2, border_color=LIGHT_GREEN, fg_color="white" + ) div["entry"].focus_set() div["entry"].icursor("end") def check_profile_name_valid(self, div, var, index, mode): - pattern = re.compile(r'^[a-zA-Z0-9_-]+$') + pattern = re.compile(r"^[a-zA-Z0-9_-]+$") is_valid_input = bool(pattern.match(div["entry_var"].get())) # Change border color @@ -243,7 +227,6 @@ def check_profile_name_valid(self, div, var, index, mode): return is_valid_input def finish_rename(self, div, event): - if not self.check_profile_name_valid(div, None, None, None): logger.warning("Invalid profile name") return @@ -257,11 +240,8 @@ def finish_rename(self, div, event): else: new_color = DIV_COLORS["default"] - div["entry"].configure(state="disabled", - fg_color=new_color, - border_width=0) - ConfigManager().rename_profile(div["profile_name"], - div["entry_var"].get()) + div["entry"].configure(state="disabled", fg_color=new_color, border_width=0) + ConfigManager().rename_profile(div["profile_name"], div["entry_var"].get()) ConfigManager().switch_profile(div["entry_var"].get()) div["profile_name"] = div["entry_var"].get() @@ -281,82 +261,84 @@ def remove_button_callback(self, div): def create_div(self, row: int, div_id: str, profile_name) -> dict: # Box wrapper - wrap_label = customtkinter.CTkLabel(self, - text="", - height=54, - fg_color="white", - corner_radius=10) + wrap_label = customtkinter.CTkLabel( + self, text="", height=54, fg_color="white", corner_radius=10 + ) wrap_label.grid(row=row, column=0, padx=10, pady=4, sticky="new") # Edit button if profile_name != BACKUP_PROFILE_NAME: - edit_button = customtkinter.CTkButton(self, - text="", - width=20, - border_width=0, - corner_radius=0, - image=self.edit_image, - hover=False, - compound="right", - fg_color="transparent", - anchor="e", - command=None) - - edit_button.grid(row=row, - column=0, - padx=(0, 55), - pady=(20, 0), - sticky="ne", - columnspan=10, - rowspan=10) + edit_button = customtkinter.CTkButton( + self, + text="", + width=20, + border_width=0, + corner_radius=0, + image=self.edit_image, + hover=False, + compound="right", + fg_color="transparent", + anchor="e", + command=None, + ) + + edit_button.grid( + row=row, + column=0, + padx=(0, 55), + pady=(20, 0), + sticky="ne", + columnspan=10, + rowspan=10, + ) else: edit_button = None # Bin button if profile_name != BACKUP_PROFILE_NAME: - bin_button = customtkinter.CTkButton(self, - text="", - width=20, - border_width=0, - corner_radius=0, - image=self.bin_image, - hover=False, - compound="right", - fg_color="transparent", - anchor="e", - command=None) - - bin_button.grid(row=row, - column=0, - padx=(0, 20), - pady=(20, 0), - sticky="ne", - columnspan=10, - rowspan=10) + bin_button = customtkinter.CTkButton( + self, + text="", + width=20, + border_width=0, + corner_radius=0, + image=self.bin_image, + hover=False, + compound="right", + fg_color="transparent", + anchor="e", + command=None, + ) + + bin_button.grid( + row=row, + column=0, + padx=(0, 20), + pady=(20, 0), + sticky="ne", + columnspan=10, + rowspan=10, + ) else: bin_button = None # Entry entry_var = tk.StringVar() entry_var.set(profile_name) - entry = customtkinter.CTkEntry(self, - textvariable=entry_var, - placeholder_text="", - width=170, - height=30, - corner_radius=0, - state="disabled", - border_width=0, - insertborderwidth=0, - fg_color="white") + entry = customtkinter.CTkEntry( + self, + textvariable=entry_var, + placeholder_text="", + width=170, + height=30, + corner_radius=0, + state="disabled", + border_width=0, + insertborderwidth=0, + fg_color="white", + ) entry.cget("font").configure(size=16) - entry.grid(row=row, - column=0, - padx=20, - pady=20, - ipadx=10, - ipady=0, - sticky="nw") + entry.grid(row=row, column=0, padx=20, pady=20, ipadx=10, ipady=0, sticky="nw") div = { "div_id": div_id, @@ -368,15 +350,15 @@ def create_div(self, row: int, div_id: str, profile_name) -> dict: "bin_button": bin_button, "is_hovering": False, "is_editing": False, - "is_selected": False + "is_selected": False, } # Hover effect for widget in [wrap_label, entry, edit_button, bin_button]: if widget is None: continue - widget.bind('', partial(self.hover_enter, div)) - widget.bind('', partial(self.hover_leave, div)) + widget.bind("", partial(self.hover_enter, div)) + widget.bind("", partial(self.hover_leave, div)) # Click label : swap profile function for widget in [wrap_label, entry]: @@ -384,16 +366,13 @@ def create_div(self, row: int, div_id: str, profile_name) -> dict: # Bin button : remove div function if bin_button is not None: - bin_button.configure( - command=partial(self.remove_button_callback, div)) + bin_button.configure(command=partial(self.remove_button_callback, div)) # Edit button : rename profile function if edit_button is not None: - edit_button.configure( - command=partial(self.rename_button_callback, div)) - entry_var.trace( - "w", partial(self.check_profile_name_valid, div)) - entry.bind('', command=partial(self.finish_rename, div)) + edit_button.configure(command=partial(self.rename_button_callback, div)) + entry_var.trace("w", partial(self.check_profile_name_valid, div)) + entry.bind("", command=partial(self.finish_rename, div)) return div @@ -407,7 +386,6 @@ def leave(self): class FrameProfile(SafeDisposableFrame): - def __init__(self, master, refresh_master_fn: callable, **kwargs): super().__init__(master, **kwargs) self.master_window = master @@ -415,11 +393,11 @@ def __init__(self, master, refresh_master_fn: callable, **kwargs): self.float_window.wm_overrideredirect(True) self.float_window.lift() self.float_window.wm_attributes("-disabled", True) - self.float_window.wm_attributes('-toolwindow', 'True') + self.float_window.wm_attributes("-toolwindow", "True") self.float_window.grid_rowconfigure(3, weight=1) self.float_window.grid_columnconfigure(0, weight=1) self.float_window.configure(fg_color="white") - #self.float_window.attributes('-topmost', True) + # self.float_window.attributes('-topmost', True) self.float_window.geometry( f"{POPUP_SIZE[0]}x{POPUP_SIZE[1]}+{POPUP_OFFSET[0]}+{POPUP_OFFSET[1]}" ) @@ -430,66 +408,63 @@ def __init__(self, master, refresh_master_fn: callable, **kwargs): self.shadow_window.wm_attributes("-alpha", 0.7) self.shadow_window.wm_overrideredirect(True) self.shadow_window.lift() - self.shadow_window.wm_attributes('-toolwindow', 'True') - #self.shadow_window.attributes('-topmost', True) + self.shadow_window.wm_attributes("-toolwindow", "True") + # self.shadow_window.attributes('-topmost', True) self.shadow_window.geometry( f"{self.master_window.winfo_width()}x{self.master_window.winfo_height()}" ) # Label - top_label = customtkinter.CTkLabel(master=self.float_window, - text="User profiles") + top_label = customtkinter.CTkLabel( + master=self.float_window, text="User profiles" + ) top_label.cget("font").configure(size=24) - top_label.grid(row=0, - column=0, - padx=20, - pady=20, - sticky="nw", - columnspan=1) - - # Description label - des_label = customtkinter.CTkLabel(master=self.float_window, - text="With profile manager you can create and manage multiple profiles for each usage, so that you can easily switch between them.", - wraplength=300, - justify=tk.LEFT) + top_label.grid(row=0, column=0, padx=20, pady=20, sticky="nw", columnspan=1) + + # Description label + des_label = customtkinter.CTkLabel( + master=self.float_window, + text="With profile manager you can create and manage multiple profiles for each usage, so that you can easily switch between them.", + wraplength=300, + justify=tk.LEFT, + ) des_label.cget("font").configure(size=14) des_label.grid(row=1, column=0, padx=20, pady=10, sticky="nw") - # Close button self.close_icon = customtkinter.CTkImage( Image.open("assets/images/close.png").resize(CLOSE_ICON_SIZE), - size=CLOSE_ICON_SIZE) - - close_btn = customtkinter.CTkButton(master=self.float_window, - text="", - image=self.close_icon, - fg_color="white", - hover_color="white", - border_width=0, - corner_radius=4, - width=24, - command=self.hide_window) - close_btn.grid(row=0, - column=0, - padx=10, - pady=10, - sticky="ne", - columnspan=1, - rowspan=1) + size=CLOSE_ICON_SIZE, + ) + + close_btn = customtkinter.CTkButton( + master=self.float_window, + text="", + image=self.close_icon, + fg_color="white", + hover_color="white", + border_width=0, + corner_radius=4, + width=24, + command=self.hide_window, + ) + close_btn.grid( + row=0, column=0, padx=10, pady=10, sticky="ne", columnspan=1, rowspan=1 + ) # Add button - add_button = customtkinter.CTkButton(master=self.float_window, - text="+ Add profile", - fg_color="white", - width=100, - text_color=DARK_BLUE, - command=self.add_button_callback) + add_button = customtkinter.CTkButton( + master=self.float_window, + text="+ Add profile", + fg_color="white", + width=100, + text_color=DARK_BLUE, + command=self.add_button_callback, + ) add_button.grid(row=2, column=0, padx=15, pady=5, sticky="nw") # Inner scrollable frame - self.inner_frame = FrameProfileItems(self.float_window, - refresh_master_fn) + self.inner_frame = FrameProfileItems(self.float_window, refresh_master_fn) self.inner_frame.grid(row=3, column=0, padx=5, pady=5, sticky="nswe") self._displayed = True @@ -504,17 +479,14 @@ def __init__(self, master, refresh_master_fn: callable, **kwargs): def add_button_callback(self): ConfigManager().add_profile() self.inner_frame.refresh_frame() - def lift_window(self, event): - """Lift windows when root window get focus - """ + """Lift windows when root window get focus""" self.shadow_window.lift() self.float_window.lift() def follow_window(self, event): - """Move profile window when root window is moved - """ + """Move profile window when root window is moved""" if self.prev_event is None: self.prev_event = event @@ -527,7 +499,8 @@ def follow_window(self, event): shift_x = self.master_window.winfo_rootx() shift_y = self.master_window.winfo_rooty() self.float_window.geometry( - f"+{POPUP_OFFSET[0]+shift_x}+{POPUP_OFFSET[1]+shift_y}") + f"+{POPUP_OFFSET[0]+shift_x}+{POPUP_OFFSET[1]+shift_y}" + ) self.shadow_window.geometry(f"+{shift_x}+{shift_y}") self.prev_event = event @@ -554,21 +527,21 @@ def show_window(self): self.shadow_window.geometry(f"+{shift_x}+{shift_y}") self.shadow_window.deiconify() self.shadow_window.lift() - self.shadow_window.wm_attributes('-disabled', True) + self.shadow_window.wm_attributes("-disabled", True) # Popup self.float_window.geometry( - f"+{POPUP_OFFSET[0]+shift_x}+{POPUP_OFFSET[1]+shift_y}") + f"+{POPUP_OFFSET[0]+shift_x}+{POPUP_OFFSET[1]+shift_y}" + ) self.float_window.deiconify() self.float_window.lift() - self.float_window.wm_attributes('-disabled', False) + self.float_window.wm_attributes("-disabled", False) self._displayed = True def hide_window(self, event=None): - if self._displayed: logger.info("hide") - self.float_window.wm_attributes('-disabled', True) + self.float_window.wm_attributes("-disabled", True) self._displayed = False self.float_window.withdraw() diff --git a/src/gui/frames/frame_profile_editor.py b/src/gui/frames/frame_profile_editor.py index 6c596603..357a09b4 100644 --- a/src/gui/frames/frame_profile_editor.py +++ b/src/gui/frames/frame_profile_editor.py @@ -8,7 +8,9 @@ from PIL import Image from src.config_manager import ConfigManager -from src.gui.frames.safe_disposable_scrollable_frame import SafeDisposableScrollableFrame +from src.gui.frames.safe_disposable_scrollable_frame import ( + SafeDisposableScrollableFrame, +) from src.task_killer import TaskKiller logger = logging.getLogger("FrameProfileEditor") @@ -34,7 +36,6 @@ def random_name(row): class ItemProfileEditor(SafeDisposableScrollableFrame): - def __init__( self, owner_frame, @@ -42,7 +43,6 @@ def __init__( main_gui_callback, **kwargs, ): - super().__init__(top_level, **kwargs) self.main_gui_callback = main_gui_callback self.is_active = False @@ -52,18 +52,18 @@ def __init__( self.edit_image = customtkinter.CTkImage( Image.open("assets/images/rename.png").resize(EDIT_ICON_SIZE), - size=EDIT_ICON_SIZE) + size=EDIT_ICON_SIZE, + ) self.bin_image = customtkinter.CTkImage( Image.open("assets/images/bin.png").resize(BIN_ICON_SIZE), - size=BIN_ICON_SIZE) + size=BIN_ICON_SIZE, + ) self.divs = self.load_initial_profiles() - def load_initial_profiles(self): - """Create div according to profiles in config - """ + """Create div according to profiles in config""" profile_names = ConfigManager().list_profile() divs = {} @@ -78,8 +78,7 @@ def load_initial_profiles(self): return divs def get_div_id(self, profile_name: str): - """Get div unique id from profile name - """ + """Get div unique id from profile name""" for div_id, div in self.divs.items(): if div["profile_name"] == profile_name: return div_id @@ -92,8 +91,8 @@ def remove_div(self, div_name): for widget in div.values(): if isinstance( - widget, customtkinter.windows.widgets.core_widget_classes. - CTkBaseClass): + widget, customtkinter.windows.widgets.core_widget_classes.CTkBaseClass + ): widget.grid_forget() widget.destroy() self.refresh_scrollbar() @@ -104,16 +103,13 @@ def clear_divs(self): self.divs = {} def refresh_frame(self): - """Refresh the divs if profile directory has changed - """ + """Refresh the divs if profile directory has changed""" logger.info("Refresh frame_profile") # Check if folders same as divs name_list = [div["profile_name"] for _, div in self.divs.items()] - - if set(ConfigManager().list_profile()) == set(name_list): return logger.info("Profile directory changed, reload...") @@ -126,11 +122,9 @@ def refresh_frame(self): # Check if selected profile exist new_name_list = [div["profile_name"] for _, div in self.divs.items()] if current_profile not in new_name_list: - logger.critical( - f"Profile {current_profile} not found in {new_name_list}") + logger.critical(f"Profile {current_profile} not found in {new_name_list}") TaskKiller().exit() - self.refresh_scrollbar() logger.info(f"Current selected profile {current_profile}") @@ -142,16 +136,14 @@ def rename_button_callback(self, div: dict): hdiv["edit_button"].grid_remove() div["is_editing"] = True - div["entry"].configure(state="normal", - border_width=2, - border_color=LIGHT_GREEN, - fg_color="white") + div["entry"].configure( + state="normal", border_width=2, border_color=LIGHT_GREEN, fg_color="white" + ) div["entry"].focus_set() div["entry"].icursor("end") - def check_profile_name_valid(self, div, var, index, mode): - pattern = re.compile(r'^[a-zA-Z0-9_-]+$') + pattern = re.compile(r"^[a-zA-Z0-9_-]+$") is_valid_input = bool(pattern.match(div["entry_var"].get())) # Change border color @@ -163,7 +155,6 @@ def check_profile_name_valid(self, div, var, index, mode): return is_valid_input def finish_rename(self, div, event): - if not self.check_profile_name_valid(div, None, None, None): logger.warning("Invalid profile name") return @@ -172,8 +163,7 @@ def finish_rename(self, div, event): div["entry"].configure(state="disabled", border_width=0) - ConfigManager().rename_profile(div["profile_name"], - div["entry_var"].get()) + ConfigManager().rename_profile(div["profile_name"], div["entry_var"].get()) div["profile_name"] = div["entry_var"].get() @@ -198,84 +188,86 @@ def remove_button_callback(self, div): def create_div(self, row: int, div_id: str, profile_name) -> dict: # Box wrapper - wrap_label = customtkinter.CTkLabel(self, - text="", - height=54, - fg_color="white", - corner_radius=10) + wrap_label = customtkinter.CTkLabel( + self, text="", height=54, fg_color="white", corner_radius=10 + ) wrap_label.grid(row=row, column=0, padx=10, pady=4, sticky="new") # Edit button if profile_name != BACKUP_PROFILE_NAME: - edit_button = customtkinter.CTkButton(self, - text="", - width=20, - border_width=0, - corner_radius=0, - image=self.edit_image, - hover=False, - compound="right", - fg_color="transparent", - anchor="e", - command=None) - - edit_button.grid(row=row, - column=0, - padx=(0, 55), - pady=(20, 0), - sticky="ne", - columnspan=10, - rowspan=10) + edit_button = customtkinter.CTkButton( + self, + text="", + width=20, + border_width=0, + corner_radius=0, + image=self.edit_image, + hover=False, + compound="right", + fg_color="transparent", + anchor="e", + command=None, + ) + + edit_button.grid( + row=row, + column=0, + padx=(0, 55), + pady=(20, 0), + sticky="ne", + columnspan=10, + rowspan=10, + ) else: edit_button = None # Bin button if profile_name != BACKUP_PROFILE_NAME: - bin_button = customtkinter.CTkButton(self, - text="", - width=20, - border_width=0, - corner_radius=0, - image=self.bin_image, - hover=False, - compound="right", - fg_color="transparent", - anchor="e", - command=None) - - bin_button.grid(row=row, - column=0, - padx=(0, 20), - pady=(20, 0), - sticky="ne", - columnspan=10, - rowspan=10) + bin_button = customtkinter.CTkButton( + self, + text="", + width=20, + border_width=0, + corner_radius=0, + image=self.bin_image, + hover=False, + compound="right", + fg_color="transparent", + anchor="e", + command=None, + ) + + bin_button.grid( + row=row, + column=0, + padx=(0, 20), + pady=(20, 0), + sticky="ne", + columnspan=10, + rowspan=10, + ) else: bin_button = None # Entry entry_var = tk.StringVar() entry_var.set(profile_name) - entry = customtkinter.CTkEntry(self, - textvariable=entry_var, - placeholder_text="", - width=170, - height=30, - corner_radius=0, - state="disabled", - border_width=0, - insertborderwidth=0, - fg_color="white") + entry = customtkinter.CTkEntry( + self, + textvariable=entry_var, + placeholder_text="", + width=170, + height=30, + corner_radius=0, + state="disabled", + border_width=0, + insertborderwidth=0, + fg_color="white", + ) entry.cget("font").configure(size=16) - entry.grid(row=row, - column=0, - padx=20, - pady=20, - ipadx=10, - ipady=0, - sticky="nw") - - sep = tk.ttk.Separator(wrap_label, orient='horizontal') + entry.grid(row=row, column=0, padx=20, pady=20, ipadx=10, ipady=0, sticky="nw") + + sep = tk.ttk.Separator(wrap_label, orient="horizontal") sep.grid(row=row, column=0, padx=0, pady=0, sticky="sew") div = { @@ -286,21 +278,18 @@ def create_div(self, row: int, div_id: str, profile_name) -> dict: "entry_var": entry_var, "edit_button": edit_button, "bin_button": bin_button, - "is_editing": False + "is_editing": False, } # Bin button : remove div function if bin_button is not None: - bin_button.configure( - command=partial(self.remove_button_callback, div)) + bin_button.configure(command=partial(self.remove_button_callback, div)) # Edit button : rename profile function if edit_button is not None: - edit_button.configure( - command=partial(self.rename_button_callback, div)) - entry_var.trace( - "w", partial(self.check_profile_name_valid, div)) - entry.bind('', command=partial(self.finish_rename, div)) + edit_button.configure(command=partial(self.rename_button_callback, div)) + entry_var.trace("w", partial(self.check_profile_name_valid, div)) + entry.bind("", command=partial(self.finish_rename, div)) return div @@ -313,21 +302,19 @@ def leave(self): super().leave() -class FrameProfileEditor(): - +class FrameProfileEditor: def __init__(self, root_window, main_gui_callback: callable, **kwargs): - self.root_window = root_window self.main_gui_callback = main_gui_callback self.float_window = customtkinter.CTkToplevel(root_window) self.float_window.wm_overrideredirect(True) self.float_window.lift() self.float_window.wm_attributes("-disabled", True) - self.float_window.wm_attributes('-toolwindow', 'True') + self.float_window.wm_attributes("-toolwindow", "True") self.float_window.grid_rowconfigure(3, weight=1) self.float_window.grid_columnconfigure(0, weight=1) self.float_window.configure(fg_color="white") - #self.float_window.attributes('-topmost', True) + # self.float_window.attributes('-topmost', True) self.float_window.geometry( f"{POPUP_SIZE[0]}x{POPUP_SIZE[1]}+{POPUP_OFFSET[0]}+{POPUP_OFFSET[1]}" ) @@ -338,72 +325,71 @@ def __init__(self, root_window, main_gui_callback: callable, **kwargs): self.shadow_window.wm_attributes("-alpha", 0.7) self.shadow_window.wm_overrideredirect(True) self.shadow_window.lift() - self.shadow_window.wm_attributes('-toolwindow', 'True') - #self.shadow_window.attributes('-topmost', True) + self.shadow_window.wm_attributes("-toolwindow", "True") + # self.shadow_window.attributes('-topmost', True) self.shadow_window.geometry( f"{self.root_window.winfo_width()}x{self.root_window.winfo_height()}" ) # Label - top_label = customtkinter.CTkLabel(master=self.float_window, - text="User profiles") + top_label = customtkinter.CTkLabel( + master=self.float_window, text="User profiles" + ) top_label.cget("font").configure(size=24) - top_label.grid(row=0, - column=0, - padx=20, - pady=20, - sticky="nw", - columnspan=1) + top_label.grid(row=0, column=0, padx=20, pady=20, sticky="nw", columnspan=1) # Description label des_label = customtkinter.CTkLabel( master=self.float_window, - text= - "With profile manager you can create and manage multiple profiles for each usage, so that you can easily switch between them.", + text="With profile manager you can create and manage multiple profiles for each usage, so that you can easily switch between them.", wraplength=300, - justify=tk.LEFT) + justify=tk.LEFT, + ) des_label.cget("font").configure(size=14) des_label.grid(row=1, column=0, padx=20, pady=10, sticky="nw") # Close button self.close_icon = customtkinter.CTkImage( Image.open("assets/images/close.png").resize(CLOSE_ICON_SIZE), - size=CLOSE_ICON_SIZE) - - close_btn = customtkinter.CTkButton(master=self.float_window, - text="", - image=self.close_icon, - fg_color="white", - hover_color="white", - border_width=0, - corner_radius=4, - width=24, - command=self.hide_window) - close_btn.grid(row=0, - column=0, - padx=10, - pady=10, - sticky="ne", - columnspan=1, - rowspan=1) + size=CLOSE_ICON_SIZE, + ) + + close_btn = customtkinter.CTkButton( + master=self.float_window, + text="", + image=self.close_icon, + fg_color="white", + hover_color="white", + border_width=0, + corner_radius=4, + width=24, + command=self.hide_window, + ) + close_btn.grid( + row=0, column=0, padx=10, pady=10, sticky="ne", columnspan=1, rowspan=1 + ) # Add button add_prof_image = customtkinter.CTkImage( - Image.open("assets/images/add_prof.png"), size=(16, 12)) - add_button = customtkinter.CTkButton(master=self.float_window, - text="Add profile", - image=add_prof_image, - fg_color="white", - width=100, - text_color=DARK_BLUE, - command=self.add_button_callback) + Image.open("assets/images/add_prof.png"), size=(16, 12) + ) + add_button = customtkinter.CTkButton( + master=self.float_window, + text="Add profile", + image=add_prof_image, + fg_color="white", + width=100, + text_color=DARK_BLUE, + command=self.add_button_callback, + ) add_button.grid(row=2, column=0, padx=15, pady=5, sticky="nw") # Inner scrollable frame self.inner_frame = ItemProfileEditor( owner_frame=self, top_level=self.float_window, - main_gui_callback=main_gui_callback) + main_gui_callback=main_gui_callback, + ) self.inner_frame.grid(row=3, column=0, padx=5, pady=5, sticky="nswe") self._displayed = True @@ -416,14 +402,12 @@ def add_button_callback(self): self.inner_frame.refresh_frame() def lift_window(self, event): - """Lift windows when root window get focus - """ + """Lift windows when root window get focus""" self.shadow_window.lift() self.float_window.lift() def follow_window(self, event): - """Move profile window when root window is moved - """ + """Move profile window when root window is moved""" if self.prev_event is None: self.prev_event = event @@ -436,7 +420,8 @@ def follow_window(self, event): shift_x = self.root_window.winfo_rootx() shift_y = self.root_window.winfo_rooty() self.float_window.geometry( - f"+{POPUP_OFFSET[0]+shift_x}+{POPUP_OFFSET[1]+shift_y}") + f"+{POPUP_OFFSET[0]+shift_x}+{POPUP_OFFSET[1]+shift_y}" + ) self.shadow_window.geometry(f"+{shift_x}+{shift_y}") self.prev_event = event @@ -467,31 +452,30 @@ def show_window(self): self.shadow_window.geometry(f"+{shift_x}+{shift_y}") self.shadow_window.deiconify() self.shadow_window.lift() - self.shadow_window.wm_attributes('-disabled', True) + self.shadow_window.wm_attributes("-disabled", True) # Popup self.float_window.geometry( - f"+{POPUP_OFFSET[0]+shift_x}+{POPUP_OFFSET[1]+shift_y}") + f"+{POPUP_OFFSET[0]+shift_x}+{POPUP_OFFSET[1]+shift_y}" + ) self.float_window.deiconify() self.float_window.lift() - self.float_window.wm_attributes('-disabled', False) + self.float_window.wm_attributes("-disabled", False) self._displayed = True def hide_window(self, event=None): - if self._displayed: - self.root_window.unbind_all("") self.root_window.unbind_all("") logger.info("hide") - self.float_window.wm_attributes('-disabled', True) + self.float_window.wm_attributes("-disabled", True) self._displayed = False self.float_window.withdraw() self.shadow_window.withdraw() - def enter(self): + def enter(self): self.inner_frame.enter() self.show_window() logger.info("enter") diff --git a/src/gui/frames/frame_profile_switcher.py b/src/gui/frames/frame_profile_switcher.py index ecc03c76..48fdfc47 100644 --- a/src/gui/frames/frame_profile_switcher.py +++ b/src/gui/frames/frame_profile_switcher.py @@ -26,11 +26,7 @@ DARK_BLUE = "#1A73E8" BACKUP_PROFILE_NAME = "default" -DIV_COLORS = { - "default": "white", - "hovering": LIGHT_BLUE, - "selected": MEDIUM_BLUE -} +DIV_COLORS = {"default": "white", "hovering": LIGHT_BLUE, "selected": MEDIUM_BLUE} def random_name(row): @@ -38,7 +34,6 @@ def random_name(row): class ItemProfileSwitcher(SafeDisposableFrame): - def __init__( self, owner_frame, @@ -46,7 +41,6 @@ def __init__( main_gui_callback, **kwargs, ): - super().__init__(top_level, **kwargs) self.owner_frame = owner_frame self.main_gui_callback = main_gui_callback @@ -63,13 +57,12 @@ def __init__( self.configure(border_color="gray60") self.configure(fg_color="transparent") self.configure(bg_color="transparent") - self.configure(background_corner_colors=[ - "#000000", "#000000", "#000000", "#000000" - ]) + self.configure( + background_corner_colors=["#000000", "#000000", "#000000", "#000000"] + ) def load_initial_profiles(self): - """Create div according to profiles in config - """ + """Create div according to profiles in config""" profile_names = ConfigManager().list_profile() divs = {} @@ -84,26 +77,22 @@ def load_initial_profiles(self): row += 1 # Create add profile button - drop_add_div_id = random_name(row ) + drop_add_div_id = random_name(row) addp_div = self.create_add_profiles_div(row, drop_add_div_id) addp_div["wrap_label"].grid() divs[drop_add_div_id] = addp_div - row += 1 + row += 1 # Create edit profile button edit_div_id = random_name(row) edit_div = self.create_edit_profiles_div(row, edit_div_id) edit_div["wrap_label"].grid() divs[edit_div_id] = edit_div - return divs - - def get_div_id(self, profile_name: str): - """Get div unique id from profile name - """ + """Get div unique id from profile name""" for div_id, div in self.divs.items(): if div["profile_name"] == profile_name: return div_id @@ -132,23 +121,19 @@ def hover_leave(self, div, event): div["is_hovering"] = False - - - def remove_div(self, div_name): logger.info(f"Remove {div_name}") div = self.divs[div_name] for widget in div.values(): if isinstance( - widget, customtkinter.windows.widgets.core_widget_classes. - CTkBaseClass): + widget, customtkinter.windows.widgets.core_widget_classes.CTkBaseClass + ): widget.grid_forget() widget.destroy() def refresh_frame(self): - """Refresh the divs if profile directory has changed - """ + """Refresh the divs if profile directory has changed""" logger.info("Refresh frame_profile") @@ -191,55 +176,53 @@ def switch_div_profile(self, div, event): def create_div(self, row: int, div_id: str, profile_name) -> dict: prefix_icon = customtkinter.CTkImage( - Image.open("assets/images/proj_icon_blank.png"), - size=PREFIX_ICON_SIZE) + Image.open("assets/images/proj_icon_blank.png"), size=PREFIX_ICON_SIZE + ) # Box entry_var = tk.StringVar() entry_var.set(profile_name) - wrap_label = customtkinter.CTkLabel(self, - text="", - textvariable=entry_var, - height=40, - image=prefix_icon, - compound="left", - anchor="w", - cursor="hand2", - fg_color="white", - corner_radius=0) + wrap_label = customtkinter.CTkLabel( + self, + text="", + textvariable=entry_var, + height=40, + image=prefix_icon, + compound="left", + anchor="w", + cursor="hand2", + fg_color="white", + corner_radius=0, + ) top_pad = TOP_PAD if row == 0 else 0 - wrap_label.grid(row=row, - column=0, - padx=(1, 2), - pady=(top_pad, 0), - ipadx=0, - ipady=0, - sticky="new") - - sep = tk.ttk.Separator(wrap_label, orient='horizontal') - sep.grid(row=row, - column=0, - padx=0, - pady=0, - ipadx=0, - ipady=0, - sticky="sew") + wrap_label.grid( + row=row, + column=0, + padx=(1, 2), + pady=(top_pad, 0), + ipadx=0, + ipady=0, + sticky="new", + ) + + sep = tk.ttk.Separator(wrap_label, orient="horizontal") + sep.grid(row=row, column=0, padx=0, pady=0, ipadx=0, ipady=0, sticky="sew") div = { "div_id": div_id, "profile_name": profile_name, "wrap_label": wrap_label, "entry_var": entry_var, - "is_hovering": False + "is_hovering": False, } # Hover effect for widget in [wrap_label]: if widget is None: continue - widget.bind('', partial(self.hover_enter, div)) - widget.bind('', partial(self.hover_leave, div)) + widget.bind("", partial(self.hover_enter, div)) + widget.bind("", partial(self.hover_leave, div)) # Click label : swap profile function for widget in [wrap_label]: @@ -249,42 +232,47 @@ def create_div(self, row: int, div_id: str, profile_name) -> dict: def create_edit_profiles_div(self, row: int, div_id: str) -> dict: prefix_icon = customtkinter.CTkImage( - Image.open("assets/images/edit.png"), size=PREFIX_ICON_SIZE) + Image.open("assets/images/edit.png"), size=PREFIX_ICON_SIZE + ) # Box - wrap_label = customtkinter.CTkLabel(self, - text="Manage Profiles", - height=40, - image=prefix_icon, - compound="left", - justify="left", - anchor="w", - cursor="hand2", - fg_color="white", - corner_radius=0) + wrap_label = customtkinter.CTkLabel( + self, + text="Manage Profiles", + height=40, + image=prefix_icon, + compound="left", + justify="left", + anchor="w", + cursor="hand2", + fg_color="white", + corner_radius=0, + ) top_pad = TOP_PAD if row == 0 else 0 - wrap_label.grid(row=row, - column=0, - padx=(1, 2), - pady=(top_pad, 0), - ipadx=0, - ipady=0, - sticky="new") + wrap_label.grid( + row=row, + column=0, + padx=(1, 2), + pady=(top_pad, 0), + ipadx=0, + ipady=0, + sticky="new", + ) div = { "div_id": div_id, "profile_name": "Manage Profiles", "wrap_label": wrap_label, - "is_hovering": False + "is_hovering": False, } # Hover effect for widget in [wrap_label]: if widget is None: continue - widget.bind('', partial(self.hover_enter, div)) - widget.bind('', partial(self.hover_leave, div)) + widget.bind("", partial(self.hover_enter, div)) + widget.bind("", partial(self.hover_leave, div)) # Click label : swap profile function for widget in [wrap_label]: @@ -294,42 +282,47 @@ def create_edit_profiles_div(self, row: int, div_id: str) -> dict: def create_add_profiles_div(self, row: int, div_id: str) -> dict: prefix_icon = customtkinter.CTkImage( - Image.open("assets/images/add_drop.png"), size=PREFIX_ICON_SIZE) + Image.open("assets/images/add_drop.png"), size=PREFIX_ICON_SIZE + ) # Box - wrap_label = customtkinter.CTkLabel(self, - text="Add Profile", - height=40, - image=prefix_icon, - compound="left", - justify="left", - anchor="w", - cursor="hand2", - fg_color="white", - corner_radius=0) + wrap_label = customtkinter.CTkLabel( + self, + text="Add Profile", + height=40, + image=prefix_icon, + compound="left", + justify="left", + anchor="w", + cursor="hand2", + fg_color="white", + corner_radius=0, + ) top_pad = TOP_PAD if row == 0 else 0 - wrap_label.grid(row=row, - column=0, - padx=(1, 2), - pady=(top_pad, 0), - ipadx=0, - ipady=0, - sticky="new") + wrap_label.grid( + row=row, + column=0, + padx=(1, 2), + pady=(top_pad, 0), + ipadx=0, + ipady=0, + sticky="new", + ) div = { "div_id": div_id, "profile_name": "Add Profile", "wrap_label": wrap_label, - "is_hovering": False + "is_hovering": False, } # Hover effect for widget in [wrap_label]: if widget is None: continue - widget.bind('', partial(self.hover_enter, div)) - widget.bind('', partial(self.hover_leave, div)) + widget.bind("", partial(self.hover_enter, div)) + widget.bind("", partial(self.hover_leave, div)) # Click label : swap profile function for widget in [wrap_label]: @@ -337,33 +330,29 @@ def create_add_profiles_div(self, row: int, div_id: str) -> dict: return div - def enter(self): - super().enter() + super().enter() self.refresh_frame() - def leave(self): super().leave() -class FrameProfileSwitcher(): - +class FrameProfileSwitcher: def __init__(self, root_window, main_gui_callback: callable, **kwargs): - self.root_window = root_window self.main_gui_callback = main_gui_callback self.float_window = customtkinter.CTkToplevel(root_window) self.float_window.wm_overrideredirect(True) self.float_window.lift() self.float_window.wm_attributes("-disabled", True) - self.float_window.wm_attributes('-toolwindow', 'True') + self.float_window.wm_attributes("-toolwindow", "True") self.float_window.grid_rowconfigure(3, weight=1) self.float_window.grid_columnconfigure(0, weight=1) self.float_window.configure(fg_color="white") self._displayed = True # Rounded corner - self.float_window.config(background='#000000') + self.float_window.config(background="#000000") self.float_window.attributes("-transparentcolor", "#000000") # Custom border @@ -378,7 +367,8 @@ def __init__(self, root_window, main_gui_callback: callable, **kwargs): self.inner_frame = ItemProfileSwitcher( owner_frame=self, top_level=self.float_window, - main_gui_callback=main_gui_callback) + main_gui_callback=main_gui_callback, + ) self.inner_frame.grid(row=3, column=0, padx=5, pady=5, sticky="nswe") self.hide_window() @@ -394,7 +384,6 @@ def change_profile(self, target): self.pages["page_keyboard"].refresh_profile() def show_window(self): - self.root_window.bind("", self.hide_window) shift_x = self.root_window.winfo_rootx() shift_y = self.root_window.winfo_rooty() @@ -402,23 +391,21 @@ def show_window(self): # Popup n_rows = len(ConfigManager().profiles) + 2 - - self.float_window.geometry( f"{PROFILE_ITEM_SIZE[0]}x{PROFILE_ITEM_SIZE[1]*n_rows+EXTEND_PAD}+{POPUP_OFFSET[0]+shift_x}+{POPUP_OFFSET[1]+shift_y}" ) self.float_window.deiconify() self.float_window.lift() - self.float_window.wm_attributes('-disabled', False) + self.float_window.wm_attributes("-disabled", False) self._displayed = True - def hide_window(self, event=None): + def hide_window(self, event=None): if self._displayed: logger.info("hide") self.root_window.unbind_all("") - self.float_window.wm_attributes('-disabled', True) + self.float_window.wm_attributes("-disabled", True) self._displayed = False self.float_window.withdraw() @@ -427,15 +414,12 @@ def show_profile_editor(self, event, **kwargs): self.hide_window() self.main_gui_callback("show_profile_editor") - def dropdown_add_profile(self, event, **kwargs): ConfigManager().add_profile() self.inner_frame.refresh_frame() self.show_window() - def enter(self): - # refresh UI self.inner_frame.enter() self.show_window() diff --git a/src/gui/frames/safe_disposable_frame.py b/src/gui/frames/safe_disposable_frame.py index a401f63e..7238bc25 100644 --- a/src/gui/frames/safe_disposable_frame.py +++ b/src/gui/frames/safe_disposable_frame.py @@ -4,7 +4,6 @@ class SafeDisposableFrame(customtkinter.CTkFrame): - def __init__(self, master, logger_name: str = "", **kwargs): super().__init__(master, **kwargs) self.is_active = False @@ -29,5 +28,3 @@ def destroy(self): self.canvas_im = None self.new_photo = None self.placeholder_im = None - - diff --git a/src/gui/frames/safe_disposable_scrollable_frame.py b/src/gui/frames/safe_disposable_scrollable_frame.py index 167bcd19..c25f0c76 100644 --- a/src/gui/frames/safe_disposable_scrollable_frame.py +++ b/src/gui/frames/safe_disposable_scrollable_frame.py @@ -4,7 +4,6 @@ class SafeDisposableScrollableFrame(customtkinter.CTkScrollableFrame): - def __init__(self, master, logger_name: str = "", **kwargs): super().__init__(master, **kwargs) self.is_active = False diff --git a/src/gui/main_gui.py b/src/gui/main_gui.py index 228f1f2b..344a3ef4 100644 --- a/src/gui/main_gui.py +++ b/src/gui/main_gui.py @@ -4,7 +4,13 @@ from src.gui import frames from src.config_manager import ConfigManager from src.controllers import Keybinder, MouseController -from src.gui.pages import PageSelectCamera, PageCursor, PageSelectGestures, PageKeyboard +from src.gui.pages import ( + PageSelectCamera, + PageCursor, + PageSelectGestures, + PageKeyboard, + PageAbout, +) customtkinter.set_appearance_mode("light") customtkinter.set_default_color_theme("assets/themes/google_theme.json") @@ -12,8 +18,7 @@ logger = logging.getLogger("MainGUi") -class MainGui(): - +class MainGui: def __init__(self, tk_root): logger.info("Init MainGui") super().__init__() @@ -28,64 +33,60 @@ def __init__(self, tk_root): self.tk_root.grid_columnconfigure(1, weight=1) # Create menu frame and assign callbacks - self.frame_menu = frames.FrameMenu(self.tk_root, - self.root_function_callback, - height=360, - width=260, - logger_name="frame_menu") - self.frame_menu.grid(row=0, - column=0, - padx=0, - pady=0, - sticky="nsew", - columnspan=1, - rowspan=3) + self.frame_menu = frames.FrameMenu( + self.tk_root, + self.root_function_callback, + height=360, + width=260, + logger_name="frame_menu", + ) + self.frame_menu.grid( + row=0, column=0, padx=0, pady=0, sticky="nsew", columnspan=1, rowspan=3 + ) # Create Preview frame - self.frame_preview = frames.FrameCamPreview(self.tk_root, - self.cam_preview_callback, - logger_name="frame_preview") - self.frame_preview.grid(row=1, - column=0, - padx=0, - pady=0, - sticky="sew", - columnspan=1) + self.frame_preview = frames.FrameCamPreview( + self.tk_root, self.cam_preview_callback, logger_name="frame_preview" + ) + self.frame_preview.grid( + row=1, column=0, padx=0, pady=0, sticky="sew", columnspan=1 + ) self.frame_preview.enter() # Create all wizard pages and grid them. self.pages = [ - PageSelectCamera( - master=self.tk_root, - ), - PageCursor( - master=self.tk_root, - ), - PageSelectGestures( - master=self.tk_root, - ), - PageKeyboard( - master=self.tk_root, - ) + PageSelectCamera( + master=self.tk_root, + ), + PageCursor( + master=self.tk_root, + ), + PageSelectGestures( + master=self.tk_root, + ), + PageKeyboard( + master=self.tk_root, + ), + PageAbout( + master=self.tk_root, + ), ] self.current_page_name = None for page in self.pages: - page.grid(row=0, - column=1, - padx=5, - pady=5, - sticky="nsew", - rowspan=2, - columnspan=1) + page.grid( + row=0, column=1, padx=5, pady=5, sticky="nsew", rowspan=2, columnspan=1 + ) self.change_page(PageSelectCamera.__name__) # Profile UI self.frame_profile_switcher = frames.FrameProfileSwitcher( - self.tk_root, main_gui_callback=self.root_function_callback) + self.tk_root, main_gui_callback=self.root_function_callback + ) self.frame_profile_editor = frames.FrameProfileEditor( - self.tk_root, main_gui_callback=self.root_function_callback) + self.tk_root, main_gui_callback=self.root_function_callback + ) def root_function_callback(self, function_name, args: dict = {}, **kwargs): logger.info(f"root_function_callback {function_name} with {args}") @@ -121,7 +122,6 @@ def set_mediapipe_mouse_enable(self, new_state: bool): MouseController().set_active(False) def change_page(self, target_page_name: str): - if self.current_page_name == target_page_name: return diff --git a/src/gui/pages/__init__.py b/src/gui/pages/__init__.py index dcf38cd9..78ac973d 100644 --- a/src/gui/pages/__init__.py +++ b/src/gui/pages/__init__.py @@ -1,6 +1,14 @@ -__all__ = ['PageCursor', 'PageHome', 'PageKeyboard', 'PageSelectCamera', 'PageSelectGestures'] +__all__ = [ + "PageCursor", + "PageHome", + "PageKeyboard", + "PageSelectCamera", + "PageSelectGestures", + "PageAbout", +] from .page_cursor import PageCursor from .page_home import PageHome from .page_keyboard import PageKeyboard from .page_select_camera import PageSelectCamera from .page_select_gestures import PageSelectGestures +from .page_about import PageAbout diff --git a/src/gui/pages/page_about.py b/src/gui/pages/page_about.py new file mode 100644 index 00000000..1400cc01 --- /dev/null +++ b/src/gui/pages/page_about.py @@ -0,0 +1,240 @@ +# Standard library imports. +# +# Logging module. +# https://docs.python.org/3.8/library/logging.html +import logging + +# +# Object oriented path handling. +# https://docs.python.org/3/library/pathlib.html + +# +# Tcl/Tk user interface module. +# https://docs.python.org/3/library/tkinter.html +# https://tkdocs.com/tutorial/text.html +from tkinter import Text +from tkinter.ttk import Label +from tkinter.font import Font + +# +# Browser launcher module. +# https://docs.python.org/3/library/webbrowser.html +import webbrowser + +# +# PIP modules. +# +# +# Local imports. +# +from src.gui.frames.safe_disposable_frame import SafeDisposableFrame +from src.config_manager import ConfigManager + +logger = logging.getLogger("PageAbout") + + +def log_path(description, path): + logger.info( + " ".join( + ( + description, + "".join(('"', str(path), '"')), + "Exists." if path.exists() else "Doesn't exist.", + ) + ) + ) + + +# Jim initially was using Python lambda expressions for the event handlers. That +# seemed to result in all tag_bind() calls being overridden to whichever was the +# last one called. So now the tags are set up here, outside the class, and +# lambda expressions aren't used. +addressTags = {} + + +def add_address_tag(pageAbout, address): + def _enter(event): + pageAbout.hover_enter(address, event) + + def _leave(event): + pageAbout.hover_leave(address, event) + + def _click(event): + pageAbout.open_in_browser(address, event) + + addressTag = { + "tag": f"address{len(addressTags)}", + "enter": _enter, + "leave": _leave, + "click": _click, + } + tagText = addressTag["tag"] + + pageAbout.text.tag_configure(tagText) + + # TOTH using tag_bind. https://stackoverflow.com/a/65733556/7657675 + # TOTH list of events that makes clear they have to be in angle brackets. + # https://stackoverflow.com/a/32289245/7657675 + # + # - is triggered when the pointer hovers here. + # - is triggered when the pointer stops hovering here. + # - <1> is triggered when this is clicked. It seems to be a shorthand for + # button-1. + pageAbout.text.tag_bind(tagText, "", addressTag["enter"]) + pageAbout.text.tag_bind(tagText, "", addressTag["leave"]) + pageAbout.text.tag_bind(tagText, "<1>", addressTag["click"]) + + addressTags[address] = addressTag + return addressTag + + +def add_address_tags(pageAbout, addresses): + # TOTH Underlining https://stackoverflow.com/a/44890599/7657675 + pageAbout.text.tag_configure("link", underline=True, foreground="#0000EE") + + for address in addresses: + add_address_tag(pageAbout, address) + + return addressTags + + +def tags_for(address): + try: + return ("link", addressTags[address]["tag"]) + except KeyError as keyError: + raise ValueError( + "Address hasn't been registered." + f' Add add_address_tag(pageAbout,"{address}")' + ) from keyError + + +class PageAbout(SafeDisposableFrame): + hoverCursor = "hand2" + + def __init__(self, master, **kwargs): + super().__init__(master, **kwargs) + + self.grid_rowconfigure(0, weight=1) + self.grid_rowconfigure(1, weight=0) + self.grid_columnconfigure(0, weight=1) + self.is_active = False + self.grid_propagate(False) + + # Create font objects for the fonts used on this page. + font24 = Font(family="Google Sans", size=24) + font18 = Font(family="Google Sans", size=18) + font12 = Font(family="Google Sans", size=12) + + # Handy code to log all font families. + # logger.info(font_families()) + + # Create the about page as a Text widget. The text won't be editable but + # that can't be set here because the widget will ignore the + # text.insert() method. Instead editing is disabled after the insert(). + self.text = Text( + self, + wrap="word", + borderwidth=0, + font=font12, + spacing1=20, + height=10, # height value has to be guessed it seems. + ) + + # Create tags for styling the page content. + self.text.tag_configure("h1", font=font24) + self.text.tag_configure("h2", font=font18) + + # Create a tag for every address in the about page. That seems to be the + # only way that event handlers can be bound. + add_address_tags( + self, + ( + "https://www.flaticon.com/free-icons/eye", + "https://github.com/acidcoke/Grimassist/", + "https://github.com/google/project-gameface", + ), + ) + logger.info(f"addressTags{addressTags}") + + # At time of coding, the app windows seems to be fixed size and the + # about page content fits in it. So there's no need for a scroll bar. If + # that changes then uncomment this code to add a scroll bar. + # + # from tkinter.ttk import Scrollbar + # self.scrollY = Scrollbar( + # self, orient = 'vertical', command = self.text.yview) + # self.text['yscrollcommand'] = self.scrollY.set + # self.scrollY.grid(column = 1, row = 0, sticky = 'ns') + + self.text.grid(row=0, column=0, padx=0, pady=5, sticky="new") + + # The argument tail is an alternating sequence of texts and tag lists. + # If the text is to be set in the default style then an empty tag list + # () is given. + self.text.insert( + "1.0", + "About Grimassist\n", + "h1", + f"Version {ConfigManager().version}\n" + "Control and move the pointer using head movements and facial" + " gestures.\nDisclaimer: This software isn't intended for medical" + " use.\nGrimassist is an Open Source project\n", + (), + "Attribution\n", + "h2", + "Blink graphics in the user interface are based on ", + (), + "Eye icons created by Kiranshastry - Flaticon", + tags_for("https://www.flaticon.com/free-icons/eye"), + ".\nThis software is based on ", + (), + "Google GameFace", + tags_for("https://github.com/google/project-gameface"), + ".", + (), + ) + self.text.configure(state="disabled") + + # When the pointer hovers over a link, change it to a hand. It might + # seem like that could be done by adding a configuration to the tag, + # which is how links are underlined. However, it seems like that doesn't + # work and the cursor cannot be configured at the tag level. The + # solution is to configure and reconfigure it dynmically, at the widget + # level, in the hover handlers. + # + # Discover the default cursor configuration here and store it. The value + # is used in the hover handlers. + self.initialCursor = self.text["cursor"] + + # Label to display the address of a link when it's hovered over. + # + # TBD make it transparent. For now it takes the background colour of the + # text control, above. + self.hoverLabel = Label( + self, text="", font=font12, background=self.text["background"] + ) + self.hoverLabel.grid(row=1, column=0, sticky="sw") + + def hover_enter(self, address, event): + logger.info(f"hover({address}, {event}) {event.type}") + # TOTH how to set the text of a label. + # https://stackoverflow.com/a/17126015/7657675 + self.hoverLabel.configure(text=address) + self.text.configure(cursor=self.hoverCursor) + + def hover_leave(self, address, event): + logger.info(f"hover({address}, {event}) {event.type}") + # TOTH how to set the text of a label. + # https://stackoverflow.com/a/17126015/7657675 + self.hoverLabel.configure(text="") + self.text.configure(cursor=self.initialCursor) + + def open_in_browser(self, address, event): + logger.info(f"open_in_browser({address}, {event})") + webbrowser.open(address) + + def enter(self): + super().enter() + + # Next line would opens the About file in the browser. + # open_in_browser(aboutHTML.as_uri(), None) diff --git a/src/gui/pages/page_cursor.py b/src/gui/pages/page_cursor.py index 33bcccd9..4d3cf29b 100644 --- a/src/gui/pages/page_cursor.py +++ b/src/gui/pages/page_cursor.py @@ -18,7 +18,6 @@ class FrameSelectGesture(SafeDisposableFrame): - def __init__( self, master, @@ -31,48 +30,50 @@ def __init__( self.slider_dragging = False self.help_icon = customtkinter.CTkImage( Image.open("assets/images/help.png").resize(HELP_ICON_SIZE), - size=HELP_ICON_SIZE) + size=HELP_ICON_SIZE, + ) - self.shared_info_balloon = Balloon( - self, image_path="assets/images/balloon.png") + self.shared_info_balloon = Balloon(self, image_path="assets/images/balloon.png") # Slider divs - self.divs = self.create_divs({ - "Move up": ["spd_up", "", 1, 100], - "Move down": ["spd_down", "", 1, 100], - "Move right": ["spd_right", "", 1, 100], - "Move left": ["spd_left", "", 1, 100], - "(Advanced) Smooth pointer": [ - "pointer_smooth", - "Controls the smoothness of the\nmouse cursor. Enables the user\nto reduce jitteriness", - 1, 100 - ], - "(Advanced) Smooth blendshapes": [ - "shape_smooth", "Reduces the flickering of the action\ntrigger", - 1, 100 - ], - "(Advanced) Hold trigger delay(ms)": [ - "hold_trigger_ms", - "Controls how long the user should\nhold a gesture in milliseconds\nfor an action to trigger", - 1, MAX_HOLD_TRIG - ], - "(Advanced) Rapid fire interval(ms)": [ - "rapid_fire_interval_ms", - "Controls how much time should pass\nbetween each individual\ntriggering of the action", - 1, MAX_HOLD_TRIG - ] - }) + self.divs = self.create_divs( + { + "Move up": ["spd_up", "", 1, 100], + "Move down": ["spd_down", "", 1, 100], + "Move right": ["spd_right", "", 1, 100], + "Move left": ["spd_left", "", 1, 100], + "(Advanced) Smooth pointer": [ + "pointer_smooth", + "Controls the smoothness of the\nmouse cursor. Enables the user\nto reduce jitteriness", + 1, + 100, + ], + "(Advanced) Smooth blendshapes": [ + "shape_smooth", + "Reduces the flickering of the action\ntrigger", + 1, + 100, + ], + "(Advanced) Hold trigger delay(ms)": [ + "hold_trigger_ms", + "Controls how long the user should\nhold a gesture in milliseconds\nfor an action to trigger", + 1, + MAX_HOLD_TRIG, + ], + "(Advanced) Rapid fire interval(ms)": [ + "rapid_fire_interval_ms", + "Controls how much time should pass\nbetween each individual\ntriggering of the action", + 1, + MAX_HOLD_TRIG, + ], + } + ) # Toggle label - self.toggle_label = customtkinter.CTkLabel(master=self, - compound='right', - text="Cursor control", - justify=tkinter.LEFT) - self.toggle_label.cget("font").configure(weight='bold') - self.toggle_label.grid(row=0, - column=0, - padx=(20, 0), - pady=5, - sticky="nw") + self.toggle_label = customtkinter.CTkLabel( + master=self, compound="right", text="Cursor control", justify=tkinter.LEFT + ) + self.toggle_label.cget("font").configure(weight="bold") + self.toggle_label.grid(row=0, column=0, padx=(20, 0), pady=5, sticky="nw") # Toggle switch self.toggle_switch = customtkinter.CTkSwitch( @@ -83,7 +84,8 @@ def __init__( switch_height=18, switch_width=32, command=lambda: self.cursor_toggle_callback( - "toggle_switch", {"switch_status": self.toggle_switch.get()}), + "toggle_switch", {"switch_status": self.toggle_switch.get()} + ), variable=MouseController().is_enabled, onvalue=1, offvalue=0, @@ -91,80 +93,73 @@ def __init__( if ConfigManager().config["enable"]: self.toggle_switch.select() - self.toggle_switch.grid(row=0, - column=0, - padx=(150, 0), - pady=5, - sticky="nw") + self.toggle_switch.grid(row=0, column=0, padx=(150, 0), pady=5, sticky="nw") self.load_initial_config() - - def load_initial_config(self): - """Load default from config and set the UI - """ + """Load default from config and set the UI""" for cfg_name, div in self.divs.items(): - cfg_value = int( - np.clip(ConfigManager().config[cfg_name], - a_min=1, - a_max=MAX_HOLD_TRIG)) + np.clip(ConfigManager().config[cfg_name], a_min=1, a_max=MAX_HOLD_TRIG) + ) div["slider"].set(cfg_value) # Temporary remove trace, adjust the value and put it back div["entry_var"].trace_vdelete("w", div["entry_trace_id"]) div["entry_var"].set(cfg_value) - div["entry_trace_id"] = div["entry_var"].trace( - "w", div["entry_trace_fn"]) + div["entry_trace_id"] = div["entry_var"].trace("w", div["entry_trace_fn"]) def create_divs(self, directions: dict): out_dict = {} - for idx, (show_name, (cfg_name, balloon_text, slider_min, - slider_max)) in enumerate(directions.items()): - + for idx, ( + show_name, + (cfg_name, balloon_text, slider_min, slider_max), + ) in enumerate(directions.items()): help_image = self.help_icon if balloon_text != "" else None # Label - label = customtkinter.CTkLabel(master=self, - image=help_image, - compound='right', - text=show_name, - justify=tkinter.LEFT) - label.cget("font").configure(weight='bold') - label.grid(row=idx+2, column=0, padx=20, pady=(10, 10), sticky="nw") + label = customtkinter.CTkLabel( + master=self, + image=help_image, + compound="right", + text=show_name, + justify=tkinter.LEFT, + ) + label.cget("font").configure(weight="bold") + label.grid(row=idx + 2, column=0, padx=20, pady=(10, 10), sticky="nw") self.shared_info_balloon.register_widget(label, balloon_text) # Slider - slider = customtkinter.CTkSlider(master=self, - from_=slider_min, - to=slider_max, - width=250, - number_of_steps=99, - command=partial( - self.slider_drag_callback, - cfg_name)) - slider.bind("", - partial(self.slider_mouse_down_callback, cfg_name)) - slider.bind("", - partial(self.slider_mouse_up_callback, cfg_name)) - slider.grid(row=idx+2, column=0, padx=30, pady=(40, 10), sticky="nw") + slider = customtkinter.CTkSlider( + master=self, + from_=slider_min, + to=slider_max, + width=250, + number_of_steps=99, + command=partial(self.slider_drag_callback, cfg_name), + ) + slider.bind( + "", partial(self.slider_mouse_down_callback, cfg_name) + ) + slider.bind( + "", partial(self.slider_mouse_up_callback, cfg_name) + ) + slider.grid(row=idx + 2, column=0, padx=30, pady=(40, 10), sticky="nw") # Number entry entry_var = tkinter.StringVar() - entry_trace_fn = partial(self.entry_changed_callback, cfg_name, - slider_min, slider_max) + entry_trace_fn = partial( + self.entry_changed_callback, cfg_name, slider_min, slider_max + ) entry_var_trace_id = entry_var.trace("w", entry_trace_fn) entry = customtkinter.CTkEntry( master=self, - validate='all', + validate="all", textvariable=entry_var, - #validatecommand=vcmd, - width=62) - entry.grid(row=idx+2, - column=0, - padx=(300, 5), - pady=(34, 10), - sticky="nw") + # validatecommand=vcmd, + width=62, + ) + entry.grid(row=idx + 2, column=0, padx=(300, 5), pady=(34, 10), sticky="nw") out_dict[cfg_name] = { "label": label, @@ -172,7 +167,7 @@ def create_divs(self, directions: dict): "entry": entry, "entry_var": entry_var, "entry_trace_id": entry_var_trace_id, - "entry_trace_fn": entry_trace_fn + "entry_trace_fn": entry_trace_fn, } return out_dict @@ -192,10 +187,10 @@ def validate_entry_input(self, P, slider_min, slider_max): else: return False - def entry_changed_callback(self, div_name, slider_min, slider_max, var, - index, mode): - """Update value with entry text - """ + def entry_changed_callback( + self, div_name, slider_min, slider_max, var, index, mode + ): + """Update value with entry text""" is_valid_input = True div = self.divs[div_name] @@ -223,8 +218,7 @@ def entry_changed_callback(self, div_name, slider_min, slider_max, var, div["entry"].configure(fg_color="#ee9e9d") def slider_drag_callback(self, div_name: str, new_value: str): - """Update value when slider being drag - """ + """Update value when slider being drag""" self.slider_dragging = True new_value = int(new_value) div = self.divs[div_name] @@ -245,20 +239,30 @@ def inner_refresh_profile(self): self.load_initial_config() def enable_cursor(self, new_state: bool): - new={} + new = {} if new_state: for cfg_name, div in self.divs.items(): slider = div["slider"] - slider.configure(state="normal", fg_color="#D2E3FC", progress_color="#1A73E8", button_color="#1A73E8") + slider.configure( + state="normal", + fg_color="#D2E3FC", + progress_color="#1A73E8", + button_color="#1A73E8", + ) div["slider"] = slider new.update({cfg_name: div}) else: for cfg_name, div in self.divs.items(): slider = div["slider"] - slider.configure(state="disabled", fg_color="lightgray", progress_color="gray", button_color="gray") + slider.configure( + state="disabled", + fg_color="lightgray", + progress_color="gray", + button_color="gray", + ) div["slider"] = slider new.update({cfg_name: div}) - self.divs=new + self.divs = new ConfigManager().set_temp_config(field="enable", value=new_state) ConfigManager().apply_config() @@ -277,7 +281,6 @@ def set_mediapipe_mouse_enable(self, new_state: bool): class PageCursor(SafeDisposableFrame): - def __init__(self, master, **kwargs): super().__init__(master, **kwargs) @@ -287,22 +290,17 @@ def __init__(self, master, **kwargs): self.task = {} # Top label. - self.top_label = customtkinter.CTkLabel(master=self, - text="Cursor speed") + self.top_label = customtkinter.CTkLabel(master=self, text="Cursor speed") self.top_label.cget("font").configure(size=24) - self.top_label.grid(row=0, - column=0, - padx=20, - pady=10, - sticky="nw", - columnspan=1) + self.top_label.grid( + row=0, column=0, padx=20, pady=10, sticky="nw", columnspan=1 + ) # Description. des_txt = "Adjust how the mouse cursor responds to your head movements." - des_label = customtkinter.CTkLabel(master=self, - text=des_txt, - wraplength=300, - justify=tkinter.LEFT) + des_label = customtkinter.CTkLabel( + master=self, text=des_txt, wraplength=300, justify=tkinter.LEFT + ) des_label.cget("font").configure(size=14) des_label.grid(row=1, column=0, padx=20, pady=5, sticky="nw") diff --git a/src/gui/pages/page_home.py b/src/gui/pages/page_home.py index 080e0150..b0de4f83 100644 --- a/src/gui/pages/page_home.py +++ b/src/gui/pages/page_home.py @@ -12,7 +12,6 @@ class PageHome(SafeDisposableFrame): - def __init__(self, master, root_callback: callable, **kwargs): super().__init__(master, **kwargs) logging.info("Create PageHome") @@ -25,106 +24,112 @@ def __init__(self, master, root_callback: callable, **kwargs): # Top text top_label = customtkinter.CTkLabel( - master=self, text="Grimassist Gesture Settings") + master=self, text="Grimassist Gesture Settings" + ) top_label.cget("font").configure(size=24) - top_label.grid(row=0, - column=0, - padx=20, - pady=20, - sticky="new", - columnspan=2) + top_label.grid(row=0, column=0, padx=20, pady=20, sticky="new", columnspan=2) # Description des_txt = "Grimassist helps gamers control their mouse cursor using their head movement and facial gestures." - des_label = customtkinter.CTkLabel(master=self, - text=des_txt, - wraplength=400, - justify=tkinter.CENTER) + des_label = customtkinter.CTkLabel( + master=self, text=des_txt, wraplength=400, justify=tkinter.CENTER + ) des_label.cget("font").configure(size=14) - des_label.grid(row=1, - column=0, - padx=20, - pady=(5, 5), - sticky="new", - columnspan=2) + des_label.grid( + row=1, column=0, padx=20, pady=(5, 5), sticky="new", columnspan=2 + ) # Disclaimer disc_txt = "Disclaimer: Grimassist is not intended for medical use." - disc_label = customtkinter.CTkLabel(master=self, - text=disc_txt, - wraplength=700, - text_color="gray60", - justify=tkinter.CENTER) + disc_label = customtkinter.CTkLabel( + master=self, + text=disc_txt, + wraplength=700, + text_color="gray60", + justify=tkinter.CENTER, + ) disc_label.cget("font").configure(size=14) - disc_label.grid(row=2, - column=0, - padx=20, - pady=(5, 10), - sticky="new", - columnspan=2) + disc_label.grid( + row=2, column=0, padx=20, pady=(5, 10), sticky="new", columnspan=2 + ) # Page camera btn page_camera_btn_im = customtkinter.CTkImage( - Image.open("assets/images/page_camera_btn.png"), size=BTN_SIZE) + Image.open("assets/images/page_camera_btn.png"), size=BTN_SIZE + ) page_camera_btn = customtkinter.CTkButton( master=self, text="", border_width=0, corner_radius=12, image=page_camera_btn_im, - command=partial(root_callback, - function_name="change_page", - args={"target": "page_camera"})) + command=partial( + root_callback, + function_name="change_page", + args={"target": "page_camera"}, + ), + ) page_camera_btn.grid(row=3, column=0, padx=80, pady=10, sticky="nw") # Page cursor btn page_cursor_btn_im = customtkinter.CTkImage( - Image.open("assets/images/page_cursor_btn.png"), size=BTN_SIZE) + Image.open("assets/images/page_cursor_btn.png"), size=BTN_SIZE + ) page_cursor_btn = customtkinter.CTkButton( master=self, text="", border_width=0, corner_radius=12, image=page_cursor_btn_im, - command=partial(root_callback, - function_name="change_page", - args={"target": "page_cursor"})) + command=partial( + root_callback, + function_name="change_page", + args={"target": "page_cursor"}, + ), + ) page_cursor_btn.grid(row=4, column=0, padx=80, pady=10, sticky="nw") # Page gestures btn page_gestures_btn_im = customtkinter.CTkImage( - Image.open("assets/images/page_gestures_btn.png"), size=BTN_SIZE) + Image.open("assets/images/page_gestures_btn.png"), size=BTN_SIZE + ) page_gestures_btn = customtkinter.CTkButton( master=self, text="", border_width=0, corner_radius=12, image=page_gestures_btn_im, - command=partial(root_callback, - function_name="change_page", - args={"target": "page_gestures"})) + command=partial( + root_callback, + function_name="change_page", + args={"target": "page_gestures"}, + ), + ) page_gestures_btn.grid(row=5, column=0, padx=80, pady=10, sticky="nw") # Page keyboard btn page_keyboard_btn_im = customtkinter.CTkImage( - Image.open("assets/images/page_keyboard_btn.png"), size=BTN_SIZE) + Image.open("assets/images/page_keyboard_btn.png"), size=BTN_SIZE + ) page_keyboard_btn = customtkinter.CTkButton( master=self, text="", border_width=0, corner_radius=12, image=page_keyboard_btn_im, - command=partial(root_callback, - function_name="change_page", - args={"target": "page_keyboard"})) + command=partial( + root_callback, + function_name="change_page", + args={"target": "page_keyboard"}, + ), + ) page_keyboard_btn.grid(row=6, column=0, padx=80, pady=10, sticky="nw") # home image home_im = customtkinter.CTkImage( - Image.open("assets/images/home_im.png"), size=HOME_IM_SIZE) - label = customtkinter.CTkLabel(self, - image=home_im, - width=HOME_IM_SIZE[0], - height=HOME_IM_SIZE[1], - text="") + Image.open("assets/images/home_im.png"), size=HOME_IM_SIZE + ) + label = customtkinter.CTkLabel( + self, image=home_im, width=HOME_IM_SIZE[0], height=HOME_IM_SIZE[1], text="" + ) label.grid(row=3, column=1, padx=20, pady=20, rowspan=3, sticky="we") diff --git a/src/gui/pages/page_keyboard.py b/src/gui/pages/page_keyboard.py index 426f9ef2..2236e99d 100644 --- a/src/gui/pages/page_keyboard.py +++ b/src/gui/pages/page_keyboard.py @@ -12,7 +12,9 @@ from src.gui.balloon import Balloon from src.gui.dropdown import Dropdown from src.gui.frames.safe_disposable_frame import SafeDisposableFrame -from src.gui.frames.safe_disposable_scrollable_frame import SafeDisposableScrollableFrame +from src.gui.frames.safe_disposable_scrollable_frame import ( + SafeDisposableScrollableFrame, +) from src.utils.Trigger import Trigger logger = logging.getLogger("PageKeyboard") @@ -34,7 +36,6 @@ class FrameSelectKeyboard(SafeDisposableScrollableFrame): - def __init__( self, master, @@ -47,33 +48,38 @@ def __init__( self.grid_columnconfigure(1, weight=1) # Float UIs - self.shared_info_balloon = Balloon( - self, image_path="assets/images/balloon.png") + self.shared_info_balloon = Balloon(self, image_path="assets/images/balloon.png") self.shared_dropdown = Dropdown( self, dropdown_items=shape_list.available_gestures, width=DIV_WIDTH, - callback=self.dropdown_callback) + callback=self.dropdown_callback, + ) self.help_icon = customtkinter.CTkImage( Image.open("assets/images/help.png").resize(HELP_ICON_SIZE), - size=HELP_ICON_SIZE) + size=HELP_ICON_SIZE, + ) self.a_button_image = customtkinter.CTkImage( Image.open("assets/images/a_button.png").resize(A_BUTTON_SIZE), - size=A_BUTTON_SIZE) + size=A_BUTTON_SIZE, + ) - self.blank_a_button_image = customtkinter.CTkImage(Image.open( - "assets/images/blank_a_button.png").resize(A_BUTTON_SIZE), - size=A_BUTTON_SIZE) + self.blank_a_button_image = customtkinter.CTkImage( + Image.open("assets/images/blank_a_button.png").resize(A_BUTTON_SIZE), + size=A_BUTTON_SIZE, + ) - self.a_button_active_image = customtkinter.CTkImage(Image.open( - "assets/images/a_button_active.png").resize(A_BUTTON_SIZE), - size=A_BUTTON_SIZE) + self.a_button_active_image = customtkinter.CTkImage( + Image.open("assets/images/a_button_active.png").resize(A_BUTTON_SIZE), + size=A_BUTTON_SIZE, + ) self.bin_image = customtkinter.CTkImage( Image.open("assets/images/bin.png").resize(BIN_ICON_SIZE), - size=BIN_ICON_SIZE) + size=BIN_ICON_SIZE, + ) self.wait_for_key_bind_id = None self.next_empty_row = 0 @@ -86,14 +92,13 @@ def __init__( self.load_initial_keybindings() def load_initial_keybindings(self): - """Load default from config and set the UI - """ - for gesture_name, bind_info in ConfigManager().keyboard_bindings.items( - ): + """Load default from config and set the UI""" + for gesture_name, bind_info in ConfigManager().keyboard_bindings.items(): div_name = f"div_{self.next_empty_row}" - div = self.create_div(self.next_empty_row, div_name, gesture_name, - bind_info) + div = self.create_div( + self.next_empty_row, div_name, gesture_name, bind_info + ) # Show elements related to gesture div["selected_gesture"] = gesture_name @@ -117,10 +122,12 @@ def add_blank_div(self): new_uuid = uuid.uuid1() div_name = f"div_{new_uuid}" logger.info(f"Add {div_name}") - div = self.create_div(row=self.next_empty_row, - div_name=div_name, - gesture_name="None", - bind_info=["keyboard", "None", 0.5, "hold"]) + div = self.create_div( + row=self.next_empty_row, + div_name=div_name, + gesture_name="None", + bind_info=["keyboard", "None", 0.5, "hold"], + ) self.divs[div_name] = div self.next_empty_row += 1 @@ -129,9 +136,8 @@ def add_blank_div(self): def remove_keybind(self, selected_key_action, selected_gesture): logger.info(f"Remove keyboard binding {selected_key_action}") ConfigManager().remove_temp_keyboard_binding( - device="keyboard", - key_action=selected_key_action, - gesture=selected_gesture) + device="keyboard", key_action=selected_key_action, gesture=selected_gesture + ) ConfigManager().apply_keyboard_bindings() self.shared_dropdown.hide_dropdown() self.shared_dropdown.enable_item(selected_gesture) @@ -150,78 +156,75 @@ def remove_div(self, div_name): for widget in div.values(): if isinstance( - widget, customtkinter.windows.widgets.core_widget_classes. - CTkBaseClass): + widget, customtkinter.windows.widgets.core_widget_classes.CTkBaseClass + ): widget.grid_forget() widget.destroy() - def create_div(self, row: int, div_name: str, gesture_name: str, - bind_info: list): + def create_div(self, row: int, div_name: str, gesture_name: str, bind_info: list): _, key_action, thres, _ = bind_info # Bin button - remove_button = customtkinter.CTkButton(master=self, - text="", - hover=False, - image=self.bin_image, - fg_color="white", - anchor="e", - cursor="hand2", - width=25) + remove_button = customtkinter.CTkButton( + master=self, + text="", + hover=False, + image=self.bin_image, + fg_color="white", + anchor="e", + cursor="hand2", + width=25, + ) remove_button.cget("font").configure(size=18) - remove_button.bind("", - partial(self.bin_button_callback, div_name)) + remove_button.bind( + "", partial(self.bin_button_callback, div_name) + ) - remove_button.grid(row=row, - column=0, - padx=(142, 0), - pady=(18, 10), - sticky="nw") + remove_button.grid(row=row, column=0, padx=(142, 0), pady=(18, 10), sticky="nw") # Key entry field_txt = "" if key_action == "None" else key_action - entry_field = customtkinter.CTkLabel(master=self, - text=field_txt, - image=self.a_button_image, - width=A_BUTTON_SIZE[0], - height=A_BUTTON_SIZE[1], - cursor="hand2") + entry_field = customtkinter.CTkLabel( + master=self, + text=field_txt, + image=self.a_button_image, + width=A_BUTTON_SIZE[0], + height=A_BUTTON_SIZE[1], + cursor="hand2", + ) entry_field.cget("font").configure(size=17) entry_field.bind( "", - partial(self.button_click_callback, div_name, entry_field)) + partial(self.button_click_callback, div_name, entry_field), + ) - entry_field.grid(row=row, - column=0, - padx=PAD_X, - pady=(10, 10), - sticky="nw") + entry_field.grid(row=row, column=0, padx=PAD_X, pady=(10, 10), sticky="nw") # Combobox - drop = customtkinter.CTkOptionMenu(master=self, - values=[gesture_name], - width=DIV_WIDTH, - dynamic_resizing=False, - state="disabled") + drop = customtkinter.CTkOptionMenu( + master=self, + values=[gesture_name], + width=DIV_WIDTH, + dynamic_resizing=False, + state="disabled", + ) drop.grid(row=row, column=0, padx=PAD_X, pady=(64, 10), sticky="nw") drop.grid_remove() self.shared_dropdown.register_widget(drop, div_name) # Label ? - tips_label = customtkinter.CTkLabel(master=self, - image=self.help_icon, - compound='right', - text="Gesture size", - text_color="#5E5E5E", - justify='left') + tips_label = customtkinter.CTkLabel( + master=self, + image=self.help_icon, + compound="right", + text="Gesture size", + text_color="#5E5E5E", + justify="left", + ) tips_label.cget("font").configure(size=12) - tips_label.grid(row=row, - column=0, - padx=PAD_X, - pady=(92, 10), - sticky="nw") + tips_label.grid(row=row, column=0, padx=PAD_X, pady=(92, 10), sticky="nw") tips_label.grid_remove() self.shared_info_balloon.register_widget(tips_label, BALLOON_TXT) @@ -231,63 +234,52 @@ def create_div(self, row: int, div_name: str, gesture_name: str, width=DIV_WIDTH, ) - volume_bar.grid(row=row, - column=0, - padx=PAD_X, - pady=(122, 10), - sticky="nw") + volume_bar.grid(row=row, column=0, padx=PAD_X, pady=(122, 10), sticky="nw") volume_bar.grid_remove() # Slider - slider = customtkinter.CTkSlider(master=self, - from_=1, - to=100, - width=DIV_WIDTH + 10, - number_of_steps=100, - command=partial( - self.slider_drag_callback, - div_name)) + slider = customtkinter.CTkSlider( + master=self, + from_=1, + to=100, + width=DIV_WIDTH + 10, + number_of_steps=100, + command=partial(self.slider_drag_callback, div_name), + ) slider.set(thres * 100) - slider.bind("", - partial(self.slider_mouse_down_callback, div_name)) - slider.bind("", - partial(self.slider_mouse_up_callback, div_name)) + slider.bind("", partial(self.slider_mouse_down_callback, div_name)) + slider.bind( + "", partial(self.slider_mouse_up_callback, div_name) + ) - slider.grid(row=row, - column=0, - padx=PAD_X - 5, - pady=(142, 10), - sticky="nw") + slider.grid(row=row, column=0, padx=PAD_X - 5, pady=(142, 10), sticky="nw") slider.grid_remove() # Subtle, Exaggerated - subtle_label = customtkinter.CTkLabel(master=self, - text="Subtle\t\t\t Exaggerated", - text_color="#868686", - justify=tk.LEFT) + subtle_label = customtkinter.CTkLabel( + master=self, + text="Subtle\t\t\t Exaggerated", + text_color="#868686", + justify=tk.LEFT, + ) subtle_label.cget("font").configure(size=11) - subtle_label.grid(row=row, - column=0, - padx=PAD_X, - pady=(158, 10), - sticky="nw") + subtle_label.grid(row=row, column=0, padx=PAD_X, pady=(158, 10), sticky="nw") subtle_label.grid_remove() # Trigger dropdown trigger_list = [t.value for t in Trigger] - trigger_dropdown = customtkinter.CTkOptionMenu(master=self, - values=trigger_list, - width=240, - dynamic_resizing=False, - state="normal", - ) - trigger_dropdown.grid(row=row, - column=0, - padx=PAD_X, - pady=(186, 10), - sticky="nw") + trigger_dropdown = customtkinter.CTkOptionMenu( + master=self, + values=trigger_list, + width=240, + dynamic_resizing=False, + state="normal", + ) + trigger_dropdown.grid( + row=row, column=0, padx=PAD_X, pady=(186, 10), sticky="nw" + ) trigger_dropdown.grid_remove() @@ -301,17 +293,18 @@ def create_div(self, row: int, div_name: str, gesture_name: str, "selected_gesture": gesture_name, "selected_key_action": key_action, "remove_button": remove_button, - "trigger_dropdown": trigger_dropdown + "trigger_dropdown": trigger_dropdown, } def set_new_keyboard_binding(self, div): - # Remove keybind if set to invalid key - if (div["selected_gesture"] == "None") or (div["selected_key_action"] - == "None"): + if (div["selected_gesture"] == "None") or ( + div["selected_key_action"] == "None" + ): logger.info(f"Remove keyboard binding {div['selected_key_action']}") ConfigManager().remove_temp_keyboard_binding( - device="keyboard", gesture=div["selected_gesture"]) + device="keyboard", gesture=div["selected_gesture"] + ) ConfigManager().apply_keyboard_bindings() return @@ -324,12 +317,12 @@ def set_new_keyboard_binding(self, div): key_action=div["selected_key_action"], gesture=div["selected_gesture"], threshold=thres_value, - trigger=trigger) + trigger=trigger, + ) ConfigManager().apply_keyboard_bindings() def wait_for_key(self, div_name: str, entry_button, keydown: tk.Event): - """Wait for user to press any key then set the config - """ + """Wait for user to press any key then set the config""" if div_name not in self.divs: return if self.waiting_div is None: @@ -337,19 +330,18 @@ def wait_for_key(self, div_name: str, entry_button, keydown: tk.Event): div = self.divs[div_name] - keydown_txt = keydown.keysym.lower() if isinstance( - keydown, tk.Event) else keydown + keydown_txt = ( + keydown.keysym.lower() if isinstance(keydown, tk.Event) else keydown + ) logger.info(f"Key press: <{div_name}> {keydown_txt}") - occupied_keys = [ - div["entry_field"].cget("text") for div in self.divs.values() - ] + occupied_keys = [div["entry_field"].cget("text") for div in self.divs.values()] # Not valid key if (keydown_txt in occupied_keys) or ( - keydown_txt not in shape_list.available_keyboard_keys): - logger.info( - f"Key action <{keydown_txt}> not found in available list") + keydown_txt not in shape_list.available_keyboard_keys + ): + logger.info(f"Key action <{keydown_txt}> not found in available list") entry_button.configure(image=self.a_button_image) div["selected_key_action"] = "None" div["slider"].grid_remove() @@ -387,15 +379,15 @@ def wait_for_key(self, div_name: str, entry_button, keydown: tk.Event): self.wait_for_key_bind_id = None def button_click_callback(self, div_name, entry_button, event): - """Start wait_for_key after clicked the button - """ + """Start wait_for_key after clicked the button""" # Cancel old waiting function if self.waiting_div is not None: self.wait_for_key(self.waiting_div, self.waiting_button, "cancel") # Start waiting for key press self.wait_for_key_bind_id = entry_button.bind( - "", partial(self.wait_for_key, div_name, entry_button)) + "", partial(self.wait_for_key, div_name, entry_button) + ) entry_button.focus_set() entry_button.configure(text="") @@ -404,7 +396,6 @@ def button_click_callback(self, div_name, entry_button, event): self.waiting_button = entry_button def dropdown_callback(self, div_name: str, target_gesture: str): - div = self.divs[div_name] # Release old item @@ -430,8 +421,7 @@ def dropdown_callback(self, div_name: str, target_gesture: str): self.refresh_scrollbar() def slider_drag_callback(self, div_name: str, new_value: str): - """Update value when slider being drag - """ + """Update value when slider being drag""" self.slider_dragging = True new_value = int(new_value) if div_name in self.divs: @@ -443,17 +433,14 @@ def slider_mouse_down_callback(self, div_name: str, event): self.slider_dragging = True def slider_mouse_up_callback(self, div_name: str, event): - self.slider_dragging = False div = self.divs[div_name] self.set_new_keyboard_binding(div) def update_volume_preview(self): - bs = FaceMesh().get_blendshapes() for div in self.divs.values(): - if div["selected_gesture"] == "None": continue @@ -478,8 +465,7 @@ def frame_loop(self): return def inner_refresh_profile(self): - """Refresh the page divs to match the new profile - """ + """Refresh the page divs to match the new profile""" # Remove old divs self.next_empty_row = 0 for div_name, div in self.divs.items(): @@ -499,7 +485,6 @@ def leave(self): class PageKeyboard(SafeDisposableFrame): - def __init__(self, master, **kwargs): super().__init__(master, **kwargs) @@ -510,28 +495,20 @@ def __init__(self, master, **kwargs): self.bind_id_leave = None # Top label. - self.top_label = customtkinter.CTkLabel(master=self, - text="Keyboard binding") + self.top_label = customtkinter.CTkLabel(master=self, text="Keyboard binding") self.top_label.cget("font").configure(size=24) - self.top_label.grid(row=0, - column=0, - padx=20, - pady=5, - sticky="nw", - columnspan=1) + self.top_label.grid(row=0, column=0, padx=20, pady=5, sticky="nw", columnspan=1) # Description. des_txt = "Select a facial gesture that you would like to bind to a specific keyboard key. Sensitivity allows you to control the extent to which you need to gesture to trigger the keyboard key press" - des_label = customtkinter.CTkLabel(master=self, - text=des_txt, - wraplength=300, - justify=tk.LEFT) # + des_label = customtkinter.CTkLabel( + master=self, text=des_txt, wraplength=300, justify=tk.LEFT + ) # des_label.cget("font").configure(size=14) des_label.grid(row=1, column=0, padx=20, pady=(10, 40), sticky="nw") # Inner frame - self.inner_frame = FrameSelectKeyboard( - self, logger_name="FrameSelectKeyboard") + self.inner_frame = FrameSelectKeyboard(self, logger_name="FrameSelectKeyboard") self.inner_frame.grid(row=3, column=0, padx=5, pady=5, sticky="nswe") # Add binding button @@ -540,12 +517,9 @@ def __init__(self, master, **kwargs): text="+ Add binding", fg_color="white", text_color=BLUE, - command=self.inner_frame.add_blank_div) - self.add_binding_button.grid(row=2, - column=0, - padx=5, - pady=5, - sticky="nw") + command=self.inner_frame.add_blank_div, + ) + self.add_binding_button.grid(row=2, column=0, padx=5, pady=5, sticky="nw") def enter(self): super().enter() @@ -553,7 +527,8 @@ def enter(self): # Hide dropdown when mouse leave the frame self.bind_id_leave = self.bind( - "", self.inner_frame.shared_dropdown.hide_dropdown) + "", self.inner_frame.shared_dropdown.hide_dropdown + ) def refresh_profile(self): self.inner_frame.inner_refresh_profile() diff --git a/src/gui/pages/page_select_camera.py b/src/gui/pages/page_select_camera.py index b76f3fc5..04950fb1 100644 --- a/src/gui/pages/page_select_camera.py +++ b/src/gui/pages/page_select_camera.py @@ -18,7 +18,6 @@ class PageSelectCamera(SafeDisposableFrame): - def __init__(self, master, **kwargs): super().__init__(master, **kwargs) @@ -28,12 +27,7 @@ def __init__(self, master, **kwargs): # Top text top_label = customtkinter.CTkLabel(master=self, text="Camera") top_label.cget("font").configure(size=24) - top_label.grid(row=0, - column=0, - padx=20, - pady=20, - sticky="nw", - columnspan=2) + top_label.grid(row=0, column=0, padx=20, pady=20, sticky="nw", columnspan=2) # Label self.label = customtkinter.CTkLabel(master=self, text="Select a Camera") @@ -46,31 +40,26 @@ def __init__(self, master, **kwargs): self.radio_buttons = [] # Camera canvas - self.placeholder_im = Image.open( - "assets/images/placeholder.png").resize( - (CANVAS_WIDTH, CANVAS_HEIGHT)) + self.placeholder_im = Image.open("assets/images/placeholder.png").resize( + (CANVAS_WIDTH, CANVAS_HEIGHT) + ) self.placeholder_im = ImageTk.PhotoImage(self.placeholder_im) - self.canvas = tkinter.Canvas(master=self, - width=CANVAS_WIDTH, - height=CANVAS_HEIGHT) - self.canvas.grid(row=1, - column=1, - padx=(10, 50), - pady=10, - sticky="e", - rowspan=MAX_ROWS) + self.canvas = tkinter.Canvas( + master=self, width=CANVAS_WIDTH, height=CANVAS_HEIGHT + ) + self.canvas.grid( + row=1, column=1, padx=(10, 50), pady=10, sticky="e", rowspan=MAX_ROWS + ) # Set first image. - self.canvas_im = self.canvas.create_image(0, - 0, - image=self.placeholder_im, - anchor=tkinter.NW) + self.canvas_im = self.canvas.create_image( + 0, 0, image=self.placeholder_im, anchor=tkinter.NW + ) self.new_photo = None self.latest_camera_list = [] def update_radio_buttons(self): - """ Update radio_buttons to match CameraManager - """ + """Update radio_buttons to match CameraManager""" new_camera_list = CameraManager().get_camera_list() if len(self.latest_camera_list) != len(new_camera_list): self.latest_camera_list = new_camera_list @@ -86,11 +75,13 @@ def update_radio_buttons(self): if cam_name is not None: radio_text = f"{radio_text}: {cam_name}" - radio_button = customtkinter.CTkRadioButton(master=self, - text=radio_text, - command=self.radiobutton_event, - variable=self.radio_var, - value=cam_id) + radio_button = customtkinter.CTkRadioButton( + master=self, + text=radio_text, + command=self.radiobutton_event, + variable=self.radio_var, + value=cam_id, + ) radio_button.grid(row=row_i + 2, column=0, padx=50, pady=10, sticky="w") radio_buttons.append(radio_button) @@ -123,19 +114,17 @@ def page_loop(self): return if self.is_active: - frame_rgb = CameraManager().get_raw_frame() # Assign ref to avoid garbage collected self.new_photo = ImageTk.PhotoImage( - image=Image.fromarray(frame_rgb).resize((CANVAS_WIDTH, - CANVAS_HEIGHT))) + image=Image.fromarray(frame_rgb).resize((CANVAS_WIDTH, CANVAS_HEIGHT)) + ) self.canvas.itemconfig(self.canvas_im, image=self.new_photo) self.canvas.update() - + CameraManager().thread_cameras.assign_done_flag.wait() self.update_radio_buttons() - self.after(ConfigManager().config["tick_interval_ms"], - self.page_loop) + self.after(ConfigManager().config["tick_interval_ms"], self.page_loop) def enter(self): super().enter() diff --git a/src/gui/pages/page_select_gestures.py b/src/gui/pages/page_select_gestures.py index 10f57a23..fef800d9 100644 --- a/src/gui/pages/page_select_gestures.py +++ b/src/gui/pages/page_select_gestures.py @@ -23,11 +23,10 @@ class FrameSelectGesture(SafeDisposableFrame): - def __init__( - self, - master, - **kwargs, + self, + master, + **kwargs, ): super().__init__(master, **kwargs) self.is_active = False @@ -36,21 +35,23 @@ def __init__( self.grid_columnconfigure(1, weight=1) # Float UIs - self.shared_info_balloon = Balloon( - self, image_path="assets/images/balloon.png") + self.shared_info_balloon = Balloon(self, image_path="assets/images/balloon.png") self.shared_dropdown = Dropdown( self, dropdown_items=shape_list.available_gestures, width=DIV_WIDTH, - callback=self.dropdown_callback) + callback=self.dropdown_callback, + ) self.help_icon = customtkinter.CTkImage( Image.open("assets/images/help.png").resize(HELP_ICON_SIZE), - size=HELP_ICON_SIZE) + size=HELP_ICON_SIZE, + ) # Divs - self.divs = self.create_divs(shape_list.available_actions_keys, - shape_list.available_gestures_keys) + self.divs = self.create_divs( + shape_list.available_actions_keys, shape_list.available_gestures_keys + ) self.load_initial_keybindings() self.slider_dragging = False @@ -81,19 +82,20 @@ def set_div_active(self, div, gesture_name, thres, trigger): div["trigger_dropdown"].grid() def load_initial_keybindings(self): - """Load default from config and set the UI - """ + """Load default from config and set the UI""" for div_name, div in self.divs.items(): self.set_div_inactive(div) for gesture_name, ( - device, action_key, thres, - trigger_type) in ConfigManager().mouse_bindings.items(): + device, + action_key, + thres, + trigger_type, + ) in ConfigManager().mouse_bindings.items(): if [device, action_key] not in shape_list.available_actions_values: continue - action_idx = shape_list.available_actions_values.index( - [device, action_key]) + action_idx = shape_list.available_actions_values.index([device, action_key]) target_action_name = shape_list.available_actions_keys[action_idx] div = self.divs[target_action_name] self.set_div_active(div, gesture_name, thres, trigger_type) @@ -108,45 +110,41 @@ def create_divs(self, action_list: list, gesture_list: list): column = idx // (MAX_ROWS + 1) # Action label - label = customtkinter.CTkLabel(master=self, - text=action_name, - height=200, - width=300, - anchor='nw', - justify=tk.LEFT) - label.cget("font").configure(weight='bold') - label.grid(row=row, - column=column, - padx=(20, 20), - pady=(0, 0), - sticky="nw") + label = customtkinter.CTkLabel( + master=self, + text=action_name, + height=200, + width=300, + anchor="nw", + justify=tk.LEFT, + ) + label.cget("font").configure(weight="bold") + label.grid(row=row, column=column, padx=(20, 20), pady=(0, 0), sticky="nw") # Combobox - drop = customtkinter.CTkOptionMenu(master=self, - values=[gesture_list[0]], - width=240, - dynamic_resizing=False, - state="disabled") - drop.grid(row=row, - column=column, - padx=(20, 20), - pady=(28, 10), - sticky="nw") + drop = customtkinter.CTkOptionMenu( + master=self, + values=[gesture_list[0]], + width=240, + dynamic_resizing=False, + state="disabled", + ) + drop.grid(row=row, column=column, padx=(20, 20), pady=(28, 10), sticky="nw") self.shared_dropdown.register_widget(drop, action_name) # Label ? - tips_label = customtkinter.CTkLabel(master=self, - image=self.help_icon, - compound='right', - text="Gesture size", - text_color="#5E5E5E", - justify='left') + tips_label = customtkinter.CTkLabel( + master=self, + image=self.help_icon, + compound="right", + text="Gesture size", + text_color="#5E5E5E", + justify="left", + ) tips_label.cget("font").configure(size=12) - tips_label.grid(row=row, - column=column, - padx=(20, 20), - pady=(62, 10), - sticky="nw") + tips_label.grid( + row=row, column=column, padx=(20, 20), pady=(62, 10), sticky="nw" + ) tips_label.grid_remove() self.shared_info_balloon.register_widget(tips_label, BALLOON_TXT) @@ -155,32 +153,30 @@ def create_divs(self, action_list: list, gesture_list: list): master=self, width=240, ) - volume_bar.grid(row=row, - column=column, - padx=(20, 20), - pady=(92, 10), - sticky="nw") + volume_bar.grid( + row=row, column=column, padx=(20, 20), pady=(92, 10), sticky="nw" + ) volume_bar.grid_remove() # Slider - slider = customtkinter.CTkSlider(master=self, - from_=1, - to=100, - width=250, - number_of_steps=100, - command=partial( - self.slider_drag_callback, - action_name)) - slider.bind("", - partial(self.slider_mouse_down_callback, action_name)) - slider.bind("", - partial(self.slider_mouse_up_callback, action_name)) + slider = customtkinter.CTkSlider( + master=self, + from_=1, + to=100, + width=250, + number_of_steps=100, + command=partial(self.slider_drag_callback, action_name), + ) + slider.bind( + "", partial(self.slider_mouse_down_callback, action_name) + ) + slider.bind( + "", partial(self.slider_mouse_up_callback, action_name) + ) slider.configure(state="disabled", hover=False) - slider.grid(row=row, - column=column, - padx=(15, 20), - pady=(112, 10), - sticky="nw") + slider.grid( + row=row, column=column, padx=(15, 20), pady=(112, 10), sticky="nw" + ) slider.grid_remove() # Subtle, Exaggerated @@ -188,28 +184,26 @@ def create_divs(self, action_list: list, gesture_list: list): master=self, text="Subtle\t\t\t Exaggerated", text_color="#868686", - justify=tk.LEFT) + justify=tk.LEFT, + ) subtle_label.cget("font").configure(size=11) - subtle_label.grid(row=row, - column=column, - padx=(20, 20), - pady=(128, 10), - sticky="nw") + subtle_label.grid( + row=row, column=column, padx=(20, 20), pady=(128, 10), sticky="nw" + ) subtle_label.grid_remove() # Trigger dropdown trigger_list = [t.value for t in Trigger] - trigger_dropdown = customtkinter.CTkOptionMenu(master=self, - values=trigger_list, - width=240, - dynamic_resizing=False, - state="disabled" - ) - trigger_dropdown.grid(row=row, - column=column, - padx=(20, 20), - pady=(156, 10), - sticky="nw") + trigger_dropdown = customtkinter.CTkOptionMenu( + master=self, + values=trigger_list, + width=240, + dynamic_resizing=False, + state="disabled", + ) + trigger_dropdown.grid( + row=row, column=column, padx=(20, 20), pady=(156, 10), sticky="nw" + ) out_dict[action_name] = { "label": label, @@ -219,7 +213,7 @@ def create_divs(self, action_list: list, gesture_list: list): "volume_bar": volume_bar, "subtle_label": subtle_label, "selected_gesture": gesture_list[0], - "trigger_dropdown": trigger_dropdown + "trigger_dropdown": trigger_dropdown, } return out_dict @@ -240,13 +234,13 @@ def slider_mouse_up_callback(self, caller_name: str, event): trigger = Trigger(div["trigger_dropdown"].get()) - ConfigManager().set_temp_mouse_binding( div["selected_gesture"], device=target_device, action=target_action, threshold=thres_value, - trigger=trigger) + trigger=trigger, + ) ConfigManager().apply_mouse_bindings() def dropdown_callback(self, caller_name: str, target_gesture: str): @@ -277,7 +271,8 @@ def dropdown_callback(self, caller_name: str, target_gesture: str): device=target_device, action=target_action, threshold=thres_value, - trigger=trigger) + trigger=trigger, + ) # Remove keybind if "None" else: @@ -287,17 +282,16 @@ def dropdown_callback(self, caller_name: str, target_gesture: str): div["tips_label"].grid_remove() div["subtle_label"].grid_remove() div["trigger_dropdown"].grid_remove() - ConfigManager().remove_temp_mouse_binding(device=target_device, - action=target_action) + ConfigManager().remove_temp_mouse_binding( + device=target_device, action=target_action + ) ConfigManager().apply_mouse_bindings() def update_volume_preview(self): - bs = FaceMesh().get_blendshapes() for div_name, div in self.divs.items(): - if div["selected_gesture"] == "None": continue @@ -335,7 +329,6 @@ def leave(self): class PageSelectGestures(SafeDisposableFrame): - def __init__(self, master, **kwargs): super().__init__(master, **kwargs) @@ -346,28 +339,20 @@ def __init__(self, master, **kwargs): self.bind_id_leave = None # Top label. - self.top_label = customtkinter.CTkLabel(master=self, - text="Mouse binding") + self.top_label = customtkinter.CTkLabel(master=self, text="Mouse binding") self.top_label.cget("font").configure(size=24) - self.top_label.grid(row=0, - column=0, - padx=20, - pady=5, - sticky="nw", - columnspan=1) + self.top_label.grid(row=0, column=0, padx=20, pady=5, sticky="nw", columnspan=1) # Description. des_txt = "Select a facial gesture that you would like to bind to a specific mouse action. Sensitivity allows you to control the extent to which you need to gesture to trigger the mouse action" - des_label = customtkinter.CTkLabel(master=self, - text=des_txt, - wraplength=300, - justify=tk.LEFT) # + des_label = customtkinter.CTkLabel( + master=self, text=des_txt, wraplength=300, justify=tk.LEFT + ) # des_label.cget("font").configure(size=14) des_label.grid(row=1, column=0, padx=20, pady=10, sticky="nw") # Inner frame - self.inner_frame = FrameSelectGesture(self, - logger_name="FrameSelectGesture") + self.inner_frame = FrameSelectGesture(self, logger_name="FrameSelectGesture") self.inner_frame.grid(row=2, column=0, padx=5, pady=5, sticky="nw") def enter(self): @@ -376,7 +361,8 @@ def enter(self): # Hide dropdown when mouse leave the frame self.bind_id_leave = self.bind( - "", self.inner_frame.shared_dropdown.hide_dropdown) + "", self.inner_frame.shared_dropdown.hide_dropdown + ) def refresh_profile(self): self.inner_frame.inner_refresh_profile() diff --git a/src/pipeline.py b/src/pipeline.py index 280767ba..bda80da4 100644 --- a/src/pipeline.py +++ b/src/pipeline.py @@ -6,12 +6,10 @@ class Pipeline: - def __init__(self): logging.info("Init Pipeline") def pipeline_tick(self) -> None: - frame_rgb = CameraManager().get_raw_frame() # Detect landmarks (async) and save in its buffer @@ -19,7 +17,7 @@ def pipeline_tick(self) -> None: # Get facial landmarks landmarks = FaceMesh().get_landmarks() - if (landmarks is None): + if landmarks is None: CameraManager().draw_overlay(tracking_location=None) return diff --git a/src/shape_list.py b/src/shape_list.py index aa3a2e34..3263bf59 100644 --- a/src/shape_list.py +++ b/src/shape_list.py @@ -23,8 +23,8 @@ "cheekPuff", "cheekSquintRight", "cheekSquintLeft", - "eyeBlinkRight", - "eyeBlinkLeft", + "Eye blink right", + "Eye blink left", "eyeLookDownRight", "eyeLookDownLeft", "eyeLookInRight", @@ -75,22 +75,29 @@ "Mouse middle click": ["mouse", "middle"], "Mouse pause / unpause": ["meta", "pause"], "Reset cursor to center": ["meta", "reset"], - "Switch focus between monitors": ["meta", "cycle"] + "Switch focus between monitors": ["meta", "cycle"], } available_actions_keys = list(available_actions.keys()) available_actions_values = list(available_actions.values()) available_gestures = { - "None": "assets/images/dropdowns/None.png", - "Open mouth": "assets/images/dropdowns/Open mouth.png", - "Mouth left": "assets/images/dropdowns/Mouth left.png", - "Mouth right": "assets/images/dropdowns/Mouth right.png", - "Roll lower mouth": "assets/images/dropdowns/Roll lower mouth.png", - "Raise left eyebrow": "assets/images/dropdowns/Raise left eyebrow.png", - "Lower left eyebrow": "assets/images/dropdowns/Lower left eyebrow.png", - "Raise right eyebrow": "assets/images/dropdowns/Raise right eyebrow.png", - "Lower right eyebrow": "assets/images/dropdowns/Lower right eyebrow.png", + name: "assets/images/dropdowns/" + name + ".png" + for name in ( + "None", + "Eye blink right", + "Eye blink left", + "Open mouth", + "Mouth left", + "Mouth right", + "Roll lower mouth", + "Raise left eyebrow", + "Lower left eyebrow", + "Raise right eyebrow", + "Lower right eyebrow", + ) } + + for k, v in available_gestures.items(): assert k in blendshape_names, f"{k} not in blendshape_names" available_gestures_keys = list(available_gestures.keys()) @@ -108,7 +115,6 @@ "7": "7", "8": "8", "9": "9", - # Functions "f1": "f1", "f2": "f2", @@ -122,7 +128,6 @@ "f10": "f10", "f11": "f11", "f12": "f12", - # Letters "a": "a", "b": "b", @@ -150,7 +155,6 @@ "x": "x", "y": "y", "z": "z", - # Special characters "exclam": "!", "at": "@", @@ -174,7 +178,7 @@ "semicolon": ";", "colon": ":", "apostrophe": "'", - "quotedbl": "\"", + "quotedbl": '"', "grave": "`", "comma": ",", "less": "<", @@ -184,7 +188,6 @@ "asciitilde": "~", "bar": "|", "period": ".", - # Miscellaneous "return": "enter", "backspace": "backspace", @@ -204,7 +207,6 @@ "alt_l": "altleft", "alt_r": "altright", "num_lock": "numlock", - # Directions "up": "up", "down": "down", diff --git a/src/singleton_meta.py b/src/singleton_meta.py index 7ecf26d6..520bbdb0 100644 --- a/src/singleton_meta.py +++ b/src/singleton_meta.py @@ -1,11 +1,8 @@ - - class Singleton(type): _instances = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: - cls._instances[cls] = super(Singleton, - cls).__call__(*args, **kwargs) + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) return cls._instances[cls] diff --git a/src/task_killer.py b/src/task_killer.py index 2f16ad70..e52b6a63 100644 --- a/src/task_killer.py +++ b/src/task_killer.py @@ -11,8 +11,7 @@ class TaskKiller(metaclass=Singleton): - """Singleton class for softly killing the process and freeing the memory - """ + """Singleton class for softly killing the process and freeing the memory""" def __init__(self): logger.info("Initialize TaskKiller singleton") @@ -25,16 +24,20 @@ def start(self): # Start singletons from src.config_manager import ConfigManager + ConfigManager().start() from src.camera_manager import CameraManager + CameraManager().start() from src.controllers import Keybinder, MouseController + MouseController().start() Keybinder().start() from src.detectors import FaceMesh + FaceMesh().start() self.is_started = True @@ -58,7 +61,6 @@ def exit(self): logging.info(f"Kill {parent}, {children}") for c in children: try: - c.send_signal(signal.SIGTERM) except psutil.NoSuchProcess: pass diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 8d87be52..b1d798fc 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -1,4 +1,18 @@ -__all__ = ['calc_smooth_kernel', 'apply_smoothing', 'open_camera', 'get_camera_name','assign_cameras_queue', 'assign_cameras_unblock', 'install_fonts', 'remove_fonts'] +__all__ = [ + "calc_smooth_kernel", + "apply_smoothing", + "open_camera", + "get_camera_name", + "assign_cameras_queue", + "assign_cameras_unblock", + "install_fonts", + "remove_fonts", +] from .install_font import install_fonts, remove_fonts -from .list_cameras import assign_cameras_queue, assign_cameras_unblock, open_camera, get_camera_name +from .list_cameras import ( + assign_cameras_queue, + assign_cameras_unblock, + open_camera, + get_camera_name, +) from .smoothing import calc_smooth_kernel, apply_smoothing diff --git a/src/utils/install_font.py b/src/utils/install_font.py index 36306c7a..93b22b5d 100644 --- a/src/utils/install_font.py +++ b/src/utils/install_font.py @@ -5,7 +5,7 @@ def install_fonts(font_dir: str) -> None: font_dir = Path(font_dir) - gdi32 = ctypes.WinDLL('gdi32') + gdi32 = ctypes.WinDLL("gdi32") for font_file in font_dir.glob("*.ttf"): logging.info(f"Installing font {font_file.as_posix()}") @@ -14,7 +14,7 @@ def install_fonts(font_dir: str) -> None: def remove_fonts(font_dir: str) -> None: font_dir = Path(font_dir) - gdi32 = ctypes.WinDLL('gdi32') + gdi32 = ctypes.WinDLL("gdi32") for font_file in font_dir.glob("*.ttf"): logging.info(f"Removing font {font_file.as_posix()}") gdi32.RemoveFontResourceW(font_file.as_posix()) diff --git a/src/utils/list_cameras.py b/src/utils/list_cameras.py index cb19ae55..cfad32d8 100644 --- a/src/utils/list_cameras.py +++ b/src/utils/list_cameras.py @@ -11,7 +11,6 @@ def __open_camera_task(i): - logger.info(f"Try opening camera: {i}") try: @@ -54,9 +53,7 @@ def assign_cameras_unblock(cameras, i): def assign_cameras_queue(cameras, done_callback: callable, max_search: int): - for i in range(max_search): - # block ret, _, camera = __open_camera_task(i) if not ret: @@ -68,8 +65,7 @@ def assign_cameras_queue(cameras, done_callback: callable, max_search: int): def open_camera(cameras, i): - """For swapping camera - """ + """For swapping camera""" pool = futures.ThreadPoolExecutor(max_workers=1) pool.submit(assign_cameras_unblock, cameras, i) diff --git a/src/utils/smoothing.py b/src/utils/smoothing.py index 50b0dfed..257f0bcd 100644 --- a/src/utils/smoothing.py +++ b/src/utils/smoothing.py @@ -9,7 +9,6 @@ def calc_smooth_kernel(n: int) -> npt.ArrayLike: return kernel.reshape(n, 1) -def apply_smoothing(data: npt.ArrayLike, - kernel: npt.ArrayLike) -> npt.ArrayLike: +def apply_smoothing(data: npt.ArrayLike, kernel: npt.ArrayLike) -> npt.ArrayLike: smooth_n = len(kernel) return sum(kernel * data[-smooth_n:]) From de26c2ead3aa10bf4500d21af6fc3552c6947f12 Mon Sep 17 00:00:00 2001 From: acidcoke Date: Fri, 12 Apr 2024 18:26:07 +0200 Subject: [PATCH 123/123] bump to v0.5.1 --- installer.iss | 2 +- src/config_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/installer.iss b/installer.iss index f6c8da92..06d2749f 100644 --- a/installer.iss +++ b/installer.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Grimassist" -#define MyAppVersion "0.5.0" +#define MyAppVersion "0.5.1" #define MyAppExeName "grimassist.exe" [Setup] diff --git a/src/config_manager.py b/src/config_manager.py index aac2de33..e8004537 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -11,7 +11,7 @@ from src.task_killer import TaskKiller from src.utils.Trigger import Trigger -VERSION = "0.5.0" +VERSION = "0.5.1" DEFAULT_JSON = Path(f"C:/Users/{os.getlogin()}/Grimassist/configs/default.json") BACKUP_PROFILE = Path(f"C:/Users/{os.getlogin()}/Grimassist/configs/default")