diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 00000000..954a4345
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,26 @@
+name: Lint
+
+on:
+ push:
+ branches-ignore:
+ - main
+
+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 check .
diff --git a/.github/workflows/windows-build-release.yml b/.github/workflows/windows-build-release.yml
new file mode 100644
index 00000000..308b1a50
--- /dev/null
+++ b/.github/workflows/windows-build-release.yml
@@ -0,0 +1,69 @@
+on:
+ push:
+ tags:
+ - 'v*.*.*'
+
+permissions:
+ contents: write
+
+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 check .
+
+ # 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
+ 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
+
+ - 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-Portable-${{github.ref_name}}.zip
+
+ - name: release
+ uses: softprops/action-gh-release@v1
+ with:
+ files: |
+ 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/.gitignore b/.gitignore
index 9029d3bc..8fdaee6f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -74,6 +74,12 @@ docs/_build/
# PyBuilder
target/
+# VsCode
+.vscode/
+
+# PyCharm
+.idea/
+
# Jupyter Notebook
.ipynb_checkpoints
@@ -129,3 +135,10 @@ dmypy.json
.pyre/
*.bat
log.txt
+
+# Ruff
+.ruff/
+.ruff_cache/
+
+# Output directory
+output/
\ No newline at end of file
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
deleted file mode 100644
index 9d7656be..00000000
--- a/CONTRIBUTING
+++ /dev/null
@@ -1,29 +0,0 @@
-# How to Contribute
-
-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
-use GitHub pull requests for this purpose. Consult
-[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
-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
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..51ac0f52
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,15 @@
+# How to Contribute
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Code Reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
+
+## Community Guidelines
+
+Be a decent human being, bot, or whatever.
\ No newline at end of file
diff --git a/README.md b/README.md
index 29d86a7a..d09fccc5 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,21 @@
-# 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.
-# Download
+# Download
+
+## Single portable directory
+
1. Download the program from [Release section](../../releases/)
-2. Run `run_app.exe`
+2. Run `grimassist.exe`
+
+
+## Installer
+1. Download the Grimassist-Installer.exe from [Release section](../../releases/)
+2. Install it
+3. Run from your Windows shortcuts/desktop
# Model used
@@ -31,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
```
@@ -39,7 +48,7 @@ pip install -r requirements.txt
## Quick start
1. Run main application
```
- python run_app.py
+ python grimassist.py
```
@@ -52,20 +61,22 @@ 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) |
-## Keybinds configs
+## Keybinding configs
>[mouse_bindings.json](configs/default/mouse_bindings.json)
>[keyboard_bindings.json](configs/default/keyboard_bindings.json)
@@ -75,20 +86,30 @@ 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" |
# 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
+
+# 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 00000000..6a14e9f6
Binary files /dev/null and b/assets/images/dropdowns/Eye blink left.png differ
diff --git a/assets/images/dropdowns/Eye blink left.xcf b/assets/images/dropdowns/Eye blink left.xcf
new file mode 100644
index 00000000..7659b94c
Binary files /dev/null and b/assets/images/dropdowns/Eye blink left.xcf differ
diff --git a/assets/images/dropdowns/Eye blink right.png b/assets/images/dropdowns/Eye blink right.png
new file mode 100644
index 00000000..9b39d56d
Binary files /dev/null and b/assets/images/dropdowns/Eye blink right.png differ
diff --git a/assets/images/dropdowns/Eye blink right.xcf b/assets/images/dropdowns/Eye blink right.xcf
new file mode 100644
index 00000000..4ac9324d
Binary files /dev/null and b/assets/images/dropdowns/Eye blink right.xcf differ
diff --git a/assets/images/dropdowns/eye.xcf b/assets/images/dropdowns/eye.xcf
new file mode 100644
index 00000000..a7d54838
Binary files /dev/null and b/assets/images/dropdowns/eye.xcf differ
diff --git a/assets/images/menu_btn_about.png b/assets/images/menu_btn_about.png
new file mode 100644
index 00000000..49b655fd
Binary files /dev/null and b/assets/images/menu_btn_about.png differ
diff --git a/assets/images/menu_btn_about.xcf b/assets/images/menu_btn_about.xcf
new file mode 100644
index 00000000..ecd540ce
Binary files /dev/null and b/assets/images/menu_btn_about.xcf differ
diff --git a/assets/images/menu_btn_about_selected.png b/assets/images/menu_btn_about_selected.png
new file mode 100644
index 00000000..d14f2b36
Binary files /dev/null and b/assets/images/menu_btn_about_selected.png differ
diff --git a/assets/images/menu_btn_about_selected.xcf b/assets/images/menu_btn_about_selected.xcf
new file mode 100644
index 00000000..94b1c2eb
Binary files /dev/null and b/assets/images/menu_btn_about_selected.xcf differ
diff --git a/assets/themes/google_theme.json b/assets/themes/google_theme.json
index 6f6afb20..25fcd8c2 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-portable.spec b/build-portable.spec
new file mode 100644
index 00000000..c72a6b51
--- /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(
+ ['grimassist.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='grimassist',
+ 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/build.spec b/build.spec
index f9e3c58d..95529eba 100644
--- a/build.spec
+++ b/build.spec
@@ -15,7 +15,7 @@ ctk_modules = Path(ctk_init.parent,"modules")
app = Analysis(
- ['run_app.py'],
+ ['grimassist.py'],
pathex=[],
binaries=[],
datas=[(mp_modules.as_posix(), 'mediapipe/modules'),
@@ -39,7 +39,7 @@ exe_app = EXE(
app.scripts,
[],
exclude_binaries=True,
- name='run_app',
+ name='grimassist',
debug=False,
bootloader_ignore_signals=False,
strip=False,
@@ -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,
)
@@ -61,5 +62,5 @@ coll = COLLECT(
strip=False,
upx=True,
upx_exclude=[],
- name='project_gameface',
+ name='grimassist',
)
diff --git a/configs/default/cursor.json b/configs/default/cursor.json
index e7ebe6ec..098890a3 100644
--- a/configs/default/cursor.json
+++ b/configs/default/cursor.json
@@ -12,8 +12,10 @@
"pointer_smooth": 6,
"shape_smooth": 10,
"tick_interval_ms": 16,
- "hold_trigger_ms": 500,
+ "hold_trigger_ms": 500,
+ "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/configs/profile_1/cursor.json b/configs/profile_1/cursor.json
index 2e9a023b..0a8bb740 100644
--- a/configs/profile_1/cursor.json
+++ b/configs/profile_1/cursor.json
@@ -5,15 +5,17 @@
"tracking_vert_idxs": [
8
],
- "spd_up": 41,
- "spd_down": 41,
- "spd_left": 41,
- "spd_right": 41,
- "pointer_smooth": 15,
- "shape_smooth": 10,
- "tick_interval_ms": 16,
- "hold_trigger_ms": 500,
- "auto_play": false,
- "mouse_acceleration": false,
+ "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_interval_ms": 100,
+ "auto_play": 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..0a8bb740 100644
--- a/configs/profile_2/cursor.json
+++ b/configs/profile_2/cursor.json
@@ -5,15 +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,
- "hold_trigger_ms": 500,
- "auto_play": false,
- "mouse_acceleration": false,
+ "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_interval_ms": 100,
+ "auto_play": false,
+ "enable": 1,
+ "mouse_acceleration": false,
"use_transformation_matrix": false
}
\ No newline at end of file
diff --git a/developer.md b/developer.md
new file mode 100644
index 00000000..f038fd49
--- /dev/null
+++ b/developer.md
@@ -0,0 +1,104 @@
+# 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.
+
+# 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/run_app.py b/grimassist.py
similarity index 53%
rename from run_app.py
rename to grimassist.py
index 93637bca..1bdf3f31 100644
--- a/run_app.py
+++ b/grimassist.py
@@ -1,37 +1,29 @@
-# 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 sys
-
+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
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)
- ])
+log_path = os.environ["USERPROFILE"] + "\Grimassist"
+if not os.path.isdir(log_path):
+ os.mkdir(log_path)
+
+logging.basicConfig(
+ format=FORMAT,
+ level=logging.INFO,
+ handlers=[
+ logging.FileHandler(log_path + "\log.txt", mode="w"),
+ logging.StreamHandler(sys.stdout),
+ ],
+)
-class MainApp(gui.MainGui, Pipeline):
+class MainApp(MainGui, Pipeline):
def __init__(self, tk_root):
super().__init__(tk_root)
# Wait for window drawing.
@@ -54,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/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/installer.iss b/installer.iss
new file mode 100644
index 00000000..06d2749f
--- /dev/null
+++ b/installer.iss
@@ -0,0 +1,46 @@
+; Script generated by the Inno Setup Script Wizard.
+; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
+
+#define MyAppName "Grimassist"
+#define MyAppVersion "0.5.1"
+#define MyAppExeName "grimassist.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=Grimassist-Installer-v{#MyAppVersion}
+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}";
+Name: "autostarticon"; Description: "{cm:AutoStartProgram,{#MyAppName}}"; GroupDescription: "{cm:AdditionalIcons}";
+
+
+[Files]
+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]
+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;
+
diff --git a/requirements.txt b/requirements.txt
index aba282b7..4a97dcda 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,9 +1,13 @@
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.1.2
-PyDirectInput==1.0.4
-pywin32==306
-mediapipe==0.9.3.0
\ No newline at end of file
+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/accel_graph.py b/src/accel_graph.py
index c60748a4..90e10fab 100644
--- a/src/accel_graph.py
+++ b/src/accel_graph.py
@@ -1,23 +1,8 @@
-# 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
class AccelGraph(metaclass=abc.ABCMeta):
-
def __init__(self):
pass
@@ -27,8 +12,8 @@ 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 cdf3fcc4..170555b3 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
@@ -25,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
@@ -34,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("Intialize CameraManager singleton")
+ 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
@@ -81,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
@@ -104,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):
@@ -116,24 +102,29 @@ 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
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)
+ self.frame_buffers["debug"], self.overlay_disabled, 0, 0, 640, 108
+ )
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)
+ self.frame_buffers["debug"],
+ self.overlay_face_not_detected,
+ 0,
+ 0,
+ 640,
+ 108,
+ )
return
@@ -142,17 +133,30 @@ def draw_overlay(self, track_loc):
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(track_loc[0]), int(track_loc[1])), (0, 255, 0), 3)
-
- cv2.circle(self.frame_buffers["debug"],
- (int(track_loc[0]), int(track_loc[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(track_loc[0]), int(track_loc[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,
+ )
# ---------------------------------------------------------------------------- #
@@ -160,10 +164,9 @@ def draw_overlay(self, track_loc):
# ---------------------------------------------------------------------------- #
-class ThreadCameras():
-
+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()
@@ -171,37 +174,37 @@ 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,
- 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.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,),
- 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_caps is done
- """
- logger.info(f"Assign cameras completed. Found {self.caps}")
+ """Set default camera after assign_cameras is done"""
+ 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:
@@ -213,34 +216,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.")
+ 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
@@ -248,9 +251,10 @@ 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")
@@ -264,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)
@@ -284,7 +294,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()
@@ -292,6 +302,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")
+ logger.info("ThreadCamera destroyed")
diff --git a/src/config_manager.py b/src/config_manager.py
index cbfd5a84..e8004537 100644
--- a/src/config_manager.py
+++ b/src/config_manager.py
@@ -1,40 +1,51 @@
-# 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
+from src.utils.Trigger import Trigger
-VERSION = "0.3.33"
+VERSION = "0.5.1"
-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")
-class ConfigManager(metaclass=Singleton):
+# 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)
+
+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()}/Grimassist/configs/default"):
+ os.mkdir(f"C:/Users/{os.getlogin()}/Grimassist/configs/default")
+
+
+class ConfigManager(metaclass=Singleton):
def __init__(self):
- logger.info("Intialize ConfigManager singleton")
+ 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
self.unsave_mouse_bindings = False
@@ -42,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()
@@ -84,24 +95,23 @@ 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.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}")
@@ -109,9 +119,11 @@ def load_profile(self, profile_name: str) -> list[bool, Path]:
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..."
)
@@ -133,8 +145,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}")
@@ -150,10 +162,10 @@ 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=(', ', ': '))
+ with open(cursor_config_file, "w") as f:
+ json.dump(self.config, f, indent=4, separators=(", ", ": "))
def apply_config(self):
logger.info("Applying config")
@@ -163,88 +175,104 @@ def apply_config(self):
# ------------------------------ MOUSE BINDINGS CONFIG ----------------------------- #
- def set_temp_mouse_binding(self, gesture, device: str, action: str,
- threshold: float, trigger_type: str):
-
+ 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_type: %s",
- gesture, device, action, threshold, trigger_type)
+ "setting keybind for gesture: %s, device: %s, key: %s, threshold: %s, trigger: %s",
+ gesture,
+ device,
+ action,
+ threshold,
+ trigger.value,
+ )
- # Remove duplicate keybinds
+ # 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.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}")
- out_keybinds = {}
+ 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]):
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
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}")
+ 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_type: str):
+ 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_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.value,
+ )
- # Remove duplicate keybinds
+ # Remove duplicate keybindings
self.remove_temp_keyboard_binding(device, key_action, gesture)
# Assign
self.temp_keyboard_bindings[gesture] = [
- device, key_action,
- float(threshold), trigger_type
+ 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}"
)
- out_keybinds = {}
+ 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_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
@@ -257,13 +285,14 @@ 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.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):
@@ -272,4 +301,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 4f5d8c10..e9725379 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 *
+__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 e3a6c38d..71e5c93f 100644
--- a/src/controllers/keybinder.py
+++ b/src/controllers/keybinder.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 logging
import math
@@ -19,11 +5,12 @@
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
+from src.utils.Trigger import Trigger
logger = logging.getLogger("Keybinder")
@@ -33,15 +20,21 @@
class Keybinder(metaclass=Singleton):
-
def __init__(self) -> None:
- logger.info("Intialize Keybinder singleton")
+ self.delay_count = None
+ self.key_states = None
+ 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.triggered = False
- self.start_hold_ts = math.inf
- self.holding = False
+ self.start_hold_ts = {}
+ self.holding = {}
self.is_started = False
- self.last_know_keybinds = {}
+ self.last_know_keybindings = {}
+ self.is_active = None
def start(self):
if not self.is_started:
@@ -51,19 +44,29 @@ 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.
+ """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_keybinds = copy.deepcopy(
- (ConfigManager().mouse_bindings |
- ConfigManager().keyboard_bindings))
+ 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_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
+
+ self.last_know_keybindings = copy.deepcopy(
+ (ConfigManager().mouse_bindings | ConfigManager().keyboard_bindings)
+ )
def get_monitors(self) -> list[dict]:
out_list = []
@@ -81,68 +84,221 @@ 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"]:
+ 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:
- state_name = "mouse_" + action
+ def meta_action(self, val, action, threshold, is_active: bool) -> None:
+ state_name = "meta_" + action
- mode = "hold" if self.key_states["holding"] else "single"
+ 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
- if mode == "hold":
- if (val > thres) and (self.key_states[state_name] is False):
- pydirectinput.mouseDown(action)
+ self.toggle_active()
self.key_states[state_name] = True
-
- elif (val < thres) and (self.key_states[state_name] is True):
- pydirectinput.mouseUp(action)
+ elif (val < threshold) and (self.key_states[state_name] is True):
self.key_states[state_name] = False
- elif mode == "single":
- if (val > thres):
- if not self.key_states[state_name]:
+ 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
+
+ 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
+ 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(button=action)
self.key_states[state_name] = True
- if not self.holding and (
- ((time.time() - self.start_hold_ts) * 1000) >=
- ConfigManager().config["hold_trigger_ms"]):
+ elif (val < threshold) and (self.key_states[state_name] is True):
+ pydirectinput.mouseUp(button=action)
+ self.key_states[state_name] = False
- pydirectinput.mouseDown(button=action)
- self.holding = True
+ elif mode == Trigger.DYNAMIC:
+ if val > threshold:
+ if self.key_states[state_name] is False:
+ pydirectinput.click(button=action)
+ self.start_hold_ts[state_name] = time.time()
+ self.key_states[state_name] = True
- elif (val < thres) and (self.key_states[state_name] is 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:
+ if self.holding[state_name]:
pydirectinput.mouseUp(button=action)
- self.holding = False
- self.start_hold_ts = math.inf
-
- def keyboard_action(self, val, keysym, thres, mode):
+ self.holding[state_name] = False
+ self.start_hold_ts[state_name] = math.inf
+
+ elif mode == Trigger.TOGGLE:
+ 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 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:
+ 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_interval_ms"]:
+ 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):
state_name = "keyboard_" + keysym
- if (self.key_states[state_name] is False) and (val > thres):
- pydirectinput.keyDown(keysym)
- self.key_states[state_name] = True
+ if mode == Trigger.SINGLE:
+ if val > threshold:
+ if self.key_states[state_name] is False:
+ pydirectinput.press(keys=keysym)
+ self.key_states[state_name] = True
+ if val < threshold:
+ self.key_states[state_name] = False
- elif (self.key_states[state_name] is True) and (val < thres):
- pydirectinput.keyUp(keysym)
- self.key_states[state_name] = False
+ elif mode == Trigger.HOLD:
+ if (val > threshold) and (self.key_states[state_name] is False):
+ pydirectinput.keyDown(key=keysym)
+ self.key_states[state_name] = True
+
+ elif (val < threshold) and (self.key_states[state_name] is True):
+ 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(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(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]:
+ pydirectinput.keyUp(key=keysym)
+ self.holding[state_name] = False
+ self.start_hold_ts[state_name] = math.inf
+
+ elif mode == Trigger.TOGGLE:
+ 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:
+ 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:
+ if self.key_states[state_name] is False:
+ 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_interval_ms"]:
+ pydirectinput.press(keys=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) -> dict:
+ def act(self, blendshape_values) -> None:
"""Trigger devices action base on blendshape values
Args:
@@ -155,74 +311,56 @@ def act(self, blendshape_values) -> dict:
if blendshape_values is None:
return
- if (ConfigManager().mouse_bindings |
- ConfigManager().keyboard_bindings) != self.last_know_keybinds:
+ 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
- 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 (val > thres) and (self.key_states[state_name] is False):
- mon_id = self.get_curr_monitor()
- if mon_id is None:
- return
-
- MouseController().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():
+ if device == "meta":
+ self.meta_action(val, action, threshold, 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_curr_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_curr_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)
+ 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"""
+ 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)
+ elif device == "meta":
+ pass
+
return
diff --git a/src/controllers/mouse_controller.py b/src/controllers/mouse_controller.py
index 25bafd15..25c8822d 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
@@ -36,32 +23,37 @@
class MouseController(metaclass=Singleton):
-
def __init__(self):
- logger.info("Intialize MouseController singleton")
+ 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
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:
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)
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)
@@ -78,7 +70,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:
@@ -91,27 +83,27 @@ 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
- """
+ """Separate thread for mouse controller"""
if self.is_destroyed:
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
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(
- self.buffer, self.smooth_kernel)
+ self.buffer, self.smooth_kernel
+ )
vel_x = smooth_px - self.prev_x
vel_y = smooth_py - self.prev_y
@@ -136,6 +128,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:
@@ -143,8 +140,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/__init__.py b/src/detectors/__init__.py
index 688689dc..ec18a08e 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 *
+__all__ = ["FaceMesh"]
+from .facemesh import FaceMesh
diff --git a/src/detectors/facemesh.py b/src/detectors/facemesh.py
index c974a767..23732158 100644
--- a/src/detectors/facemesh.py
+++ b/src/detectors/facemesh.py
@@ -1,25 +1,16 @@
-# 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
+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
@@ -35,11 +26,11 @@
class FaceMesh(metaclass=Singleton):
-
def __init__(self):
- logger.info("Intialize FaceMesh singleton")
+ 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
@@ -59,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 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]
@@ -101,30 +96,35 @@ 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):
- 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.track_loc = self.calc_track_loc(
+ 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.track_loc = 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
@@ -133,13 +133,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/__init__.py b/src/gui/__init__.py
index 3d280881..a574fa27 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 *
+__all__ = ["MainGui"]
+from .main_gui import MainGui
diff --git a/src/gui/balloon.py b/src/gui/balloon.py
index a2c7da15..654e8e2e 100644
--- a/src/gui/balloon.py
+++ b/src/gui/balloon.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.
-
from functools import partial
import customtkinter
@@ -20,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()
@@ -32,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")
@@ -61,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
@@ -73,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 691b46f7..33381f68 100644
--- a/src/gui/dropdown.py
+++ b/src/gui/dropdown.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.
-
from functools import partial
import customtkinter
@@ -32,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()
@@ -50,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())
@@ -75,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():
@@ -119,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
@@ -163,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
@@ -176,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()
@@ -188,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 3fc7740c..05d43e18 100644
--- a/src/gui/frames/__init__.py
+++ b/src/gui/frames/__init__.py
@@ -1,19 +1,12 @@
-# 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 *
-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/frames/frame_cam_preview.py b/src/gui/frames/frame_cam_preview.py
index 6fb38be0..54499074 100644
--- a/src/gui/frames/frame_cam_preview.py
+++ b/src/gui/frames/frame_cam_preview.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 tkinter
import customtkinter
@@ -19,7 +5,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
@@ -29,7 +15,6 @@
class FrameCamPreview(SafeDisposableFrame):
-
def __init__(self, master, master_callback: callable, **kwargs):
super().__init__(master, **kwargs)
@@ -40,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(
@@ -72,40 +58,33 @@ 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()}),
+ "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 label
+ # 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)
@@ -118,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 404d3e49..348eb93e 100644
--- a/src/gui/frames/frame_menu.py
+++ b/src/gui/frames/frame_menu.py
@@ -1,23 +1,16 @@
-# 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
import customtkinter
from PIL import Image
from src.config_manager import ConfigManager
+from src.gui.pages import (
+ PageSelectCamera,
+ PageCursor,
+ PageSelectGestures,
+ PageKeyboard,
+ PageAbout,
+)
from src.gui.frames.safe_disposable_frame import SafeDisposableFrame
LIGHT_BLUE = "#F9FBFE"
@@ -26,7 +19,6 @@
class FrameMenu(SafeDisposableFrame):
-
def __init__(self, master, master_callback: callable, **kwargs):
super().__init__(master, **kwargs)
@@ -38,106 +30,120 @@ def __init__(self, master, master_callback: callable, **kwargs):
self.master_callback = master_callback
self.menu_btn_images = {
- "page_home": [
+ PageSelectCamera.__name__: [
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"),
- 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,
+ ),
],
- "page_cursor": [
+ 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,
+ ),
],
- "page_gestures": [
+ 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,
+ ),
],
- "page_keyboard": [
+ 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().curr_profile_name,
+ textvariable=ConfigManager().current_profile_name,
image=prof_drop,
height=42,
compound="center",
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.btns = {}
- self.btns = self.create_tab_btn(self.menu_btn_images, offset=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
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 a5285b89..c2f84d27 100644
--- a/src/gui/frames/frame_profile.py
+++ b/src/gui/frames/frame_profile.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 time
import logging
import tkinter as tk
@@ -23,8 +9,11 @@
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.safe_disposable_frame import SafeDisposableFrame
+from src.gui.frames.safe_disposable_scrollable_frame import (
+ SafeDisposableScrollableFrame,
+)
+
logger = logging.getLogger("FrameProfile")
@@ -41,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
@@ -66,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().curr_profile_name.get())
+ 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 = {}
@@ -98,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"]:
@@ -114,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"]:
@@ -126,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
@@ -136,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
@@ -161,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
@@ -184,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()
@@ -196,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")
@@ -210,7 +183,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()]
@@ -220,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:
@@ -236,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
@@ -256,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
@@ -270,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()
@@ -286,7 +253,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])
@@ -294,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,
@@ -381,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]:
@@ -397,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_id = 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
@@ -420,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
@@ -428,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]}"
)
@@ -443,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)
-
- # Add butotn
- 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)
+ 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.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
@@ -517,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
@@ -540,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
@@ -567,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 2ab26b67..357a09b4 100644
--- a/src/gui/frames/frame_profile_editor.py
+++ b/src/gui/frames/frame_profile_editor.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 logging
import re
import time
@@ -22,7 +8,9 @@
from PIL import Image
from src.config_manager import ConfigManager
-from src.gui.frames.safe_disposable_frame import SafeDisposableScrollableFrame
+from src.gui.frames.safe_disposable_scrollable_frame import (
+ SafeDisposableScrollableFrame,
+)
from src.task_killer import TaskKiller
logger = logging.getLogger("FrameProfileEditor")
@@ -48,7 +36,6 @@ def random_name(row):
class ItemProfileEditor(SafeDisposableScrollableFrame):
-
def __init__(
self,
owner_frame,
@@ -56,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
@@ -66,19 +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()
- div_id = self.get_div_id(ConfigManager().curr_profile_name.get())
-
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 = {}
@@ -93,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
@@ -107,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()
@@ -119,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...")
@@ -136,16 +117,14 @@ 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()]
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}")
@@ -157,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
@@ -178,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
@@ -187,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()
@@ -203,7 +178,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
@@ -213,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 = {
@@ -301,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_id = 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
@@ -328,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]}"
)
@@ -353,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)
-
- # Add butotn
+ 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
@@ -431,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
@@ -451,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
@@ -482,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 7fa7fbe8..48fdfc47 100644
--- a/src/gui/frames/frame_profile_switcher.py
+++ b/src/gui/frames/frame_profile_switcher.py
@@ -1,19 +1,4 @@
-# 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
import time
import tkinter as tk
from functools import partial
@@ -41,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):
@@ -53,7 +34,6 @@ def random_name(row):
class ItemProfileSwitcher(SafeDisposableFrame):
-
def __init__(
self,
owner_frame,
@@ -61,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
@@ -72,20 +51,18 @@ 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
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 = {}
@@ -100,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
@@ -148,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")
@@ -182,7 +151,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()]
@@ -207,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]:
@@ -265,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]:
@@ -310,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]:
@@ -353,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 corder
- self.float_window.config(background='#000000')
+ # Rounded corner
+ self.float_window.config(background="#000000")
self.float_window.attributes("-transparentcolor", "#000000")
# Custom border
@@ -394,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()
@@ -410,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()
@@ -418,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()
@@ -443,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 8e2e5b00..7238bc25 100644
--- a/src/gui/frames/safe_disposable_frame.py
+++ b/src/gui/frames/safe_disposable_frame.py
@@ -1,24 +1,9 @@
-# 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 customtkinter
class SafeDisposableFrame(customtkinter.CTkFrame):
-
def __init__(self, master, logger_name: str = "", **kwargs):
super().__init__(master, **kwargs)
self.is_active = False
@@ -43,40 +28,3 @@ def destroy(self):
self.canvas_im = None
self.new_photo = None
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..c25f0c76
--- /dev/null
+++ b/src/gui/frames/safe_disposable_scrollable_frame.py
@@ -0,0 +1,39 @@
+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/main_gui.py b/src/gui/main_gui.py
index ff052b7c..344a3ef4 100644
--- a/src/gui/main_gui.py
+++ b/src/gui/main_gui.py
@@ -1,27 +1,16 @@
-# 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
-
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 MouseController
+from src.controllers import Keybinder, MouseController
+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")
@@ -29,103 +18,75 @@
logger = logging.getLogger("MainGUi")
-class MainGui():
-
+class MainGui:
def __init__(self, tk_root):
logger.info("Init MainGui")
super().__init__()
self.tk_root = tk_root
- self.tk_root.geometry("1024x658")
- self.tk_root.title(f"Project Gameface {ConfigManager().version}")
+ 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)
# 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 = {
- "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,
- logger_name="page_camera",
- ),
- "page_cursor":
- pages.PageCursor(
- master=self.tk_root,
- logger_name="page_cursor",
- ),
- "page_gestures":
- pages.PageSelectGestures(
- master=self.tk_root,
- logger_name="page_gestures",
- ),
- "page_keyboard":
- pages.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 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)
-
- self.change_page("page_home")
+ self.pages = [
+ 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
+ )
+
+ 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}")
@@ -143,10 +104,8 @@ 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()
+ 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}")
@@ -156,20 +115,21 @@ def cam_preview_callback(self, function_name, args: dict, **kwargs):
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):
-
- if self.curr_page_name == target_page_name:
+ if self.current_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.__class__.__name__ == target_page_name:
page.grid()
- self.pages[target_page_name].enter()
- self.curr_page_name = target_page_name
+ page.enter()
+ self.current_page_name = page.__class__.__name__
else:
page.grid_remove()
@@ -182,7 +142,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()
diff --git a/src/gui/pages/__init__.py b/src/gui/pages/__init__.py
index 7b578760..78ac973d 100644
--- a/src/gui/pages/__init__.py
+++ b/src/gui/pages/__init__.py
@@ -1,19 +1,14 @@
-# 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 *
-from .page_keyboard import *
-from .page_select_camera import *
-from .page_select_gestures import *
+__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 20c4466c..4d3cf29b 100644
--- a/src/gui/pages/page_cursor.py
+++ b/src/gui/pages/page_cursor.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 logging
import tkinter
from functools import partial
@@ -32,7 +18,6 @@
class FrameSelectGesture(SafeDisposableFrame):
-
def __init__(
self,
master,
@@ -45,100 +30,136 @@ 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
- ]
- })
-
+ 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")
+
+ # 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
- """
+ """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, 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, 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,
- 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,
@@ -146,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
@@ -166,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 entery 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]
@@ -180,7 +201,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
@@ -197,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]
@@ -218,40 +238,75 @@ 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()
-class PageCursor(SafeDisposableFrame):
+ 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):
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 = {}
# 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 = "Mouse cursor moves with your head movement. Use this settings to adjust how fast your mouse moves in each direction."
- des_label = customtkinter.CTkLabel(master=self,
- text=des_txt,
- wraplength=300,
- justify=tkinter.LEFT)
+ 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.cget("font").configure(size=14)
des_label.grid(row=1, column=0, padx=20, pady=5, sticky="nw")
# 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()
diff --git a/src/gui/pages/page_home.py b/src/gui/pages/page_home.py
index 11b071f8..b0de4f83 100644
--- a/src/gui/pages/page_home.py
+++ b/src/gui/pages/page_home.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 logging
import tkinter
from functools import partial
@@ -26,7 +12,6 @@
class PageHome(SafeDisposableFrame):
-
def __init__(self, master, root_callback: callable, **kwargs):
super().__init__(master, **kwargs)
logging.info("Create PageHome")
@@ -39,106 +24,112 @@ 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,
- 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 = "Project Gameface 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_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.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: Project Gameface is not intended for medical use."
- disc_label = customtkinter.CTkLabel(master=self,
- text=disc_txt,
- wraplength=700,
- text_color="gray60",
- justify=tkinter.CENTER)
+ 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.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 464d2c48..2236e99d 100644
--- a/src/gui/pages/page_keyboard.py
+++ b/src/gui/pages/page_keyboard.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 logging
import tkinter as tk
import uuid
@@ -25,7 +11,11 @@
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.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")
@@ -46,7 +36,6 @@
class FrameSelectKeyboard(SafeDisposableScrollableFrame):
-
def __init__(
self,
master,
@@ -59,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
@@ -95,17 +89,16 @@ def __init__(
# Divs
self.divs = {}
- self.load_initial_keybinds()
+ self.load_initial_keybindings()
- def load_initial_keybinds(self):
- """Load default from config and set the UI
- """
- for gesture_name, bind_info in ConfigManager().keyboard_bindings.items(
- ):
+ def load_initial_keybindings(self):
+ """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,6 +110,7 @@ def load_initial_keybinds(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
@@ -128,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
@@ -140,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)
@@ -161,77 +156,76 @@ 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)
# Volume bar
@@ -240,52 +234,55 @@ 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")
-
- # Hide element related to gesture
- drop.grid_remove()
- tips_label.grid_remove()
- slider.grid_remove()
- volume_bar.grid_remove()
+ 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.grid_remove()
+
return {
"entry_field": entry_field,
"combobox": drop,
@@ -295,34 +292,37 @@ 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):
-
# 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
# 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_type=DEFAULT_TRIGGER_TYPE)
+ 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:
@@ -330,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()
@@ -350,6 +349,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_remove()
self.set_new_keyboard_binding(div)
# Valid key
@@ -370,6 +370,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)
@@ -378,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
- """
- # Cancel old waiting funciontion
+ """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="")
@@ -395,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
@@ -409,18 +409,19 @@ 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()
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:
@@ -432,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
@@ -467,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():
@@ -476,7 +473,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()
@@ -488,7 +485,6 @@ def leave(self):
class PageKeyboard(SafeDisposableFrame):
-
def __init__(self, master, **kwargs):
super().__init__(master, **kwargs)
@@ -499,42 +495,31 @@ 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 butotn
+ # Add binding button
self.add_binding_button = customtkinter.CTkButton(
master=self,
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()
@@ -542,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 e718fe8c..04950fb1 100644
--- a/src/gui/pages/page_select_camera.py
+++ b/src/gui/pages/page_select_camera.py
@@ -1,20 +1,7 @@
-# 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
+from src import utils
import customtkinter
from PIL import Image, ImageTk
@@ -31,7 +18,6 @@
class PageSelectCamera(SafeDisposableFrame):
-
def __init__(self, master, **kwargs):
super().__init__(master, **kwargs)
@@ -41,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")
@@ -54,70 +35,75 @@ 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(self, 0)
+ self.radio_var = tkinter.IntVar(value=0)
self.prev_radio_value = None
- self.radios = []
+ 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 load_initial_config(self):
- """ Update radio buttons to match CameraManager
- """
- logger.info("Refresh radio buttons")
- for old_radio in self.radios:
- old_radio.destroy()
-
+ def update_radio_buttons(self):
+ """Update radio_buttons to match CameraManager"""
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")
+ old_radios = self.radio_buttons
+
+ logger.info(f"Get camera list {new_camera_list}")
+ radio_buttons = []
+ 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_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)
+
+ # Set selected radio_button
+ target_id = ConfigManager().config["camera_id"]
+ 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
+ for old_radio in old_radios:
+ old_radio.destroy()
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()
@@ -128,24 +114,24 @@ 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()
- self.after(ConfigManager().config["tick_interval_ms"],
- self.page_loop)
+ CameraManager().thread_cameras.assign_done_flag.wait()
+ 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)
diff --git a/src/gui/pages/page_select_gestures.py b/src/gui/pages/page_select_gestures.py
index 2a070987..fef800d9 100644
--- a/src/gui/pages/page_select_gestures.py
+++ b/src/gui/pages/page_select_gestures.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 tkinter as tk
from functools import partial
@@ -24,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"
@@ -36,7 +23,6 @@
class FrameSelectGesture(SafeDisposableFrame):
-
def __init__(
self,
master,
@@ -49,22 +35,24 @@ 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.load_initial_keybinds()
+ self.divs = self.create_divs(
+ shape_list.available_actions_keys, shape_list.available_gestures_keys
+ )
+ self.load_initial_keybindings()
self.slider_dragging = False
def set_div_inactive(self, div):
@@ -76,35 +64,41 @@ 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_keybinds(self):
- """Load default from config and set the UI
- """
+ def load_initial_keybindings(self):
+ """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)
+ self.set_div_active(div, gesture_name, thres, trigger_type)
self.shared_dropdown.disable_item(gesture_name)
self.shared_dropdown.refresh_items()
@@ -116,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=175,
- 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)
@@ -163,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
@@ -196,15 +184,27 @@ 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"
+ )
+
out_dict[action_name] = {
"label": label,
"combobox": drop,
@@ -213,6 +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,
}
return out_dict
@@ -231,12 +232,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 = Trigger(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,
+ )
ConfigManager().apply_mouse_bindings()
def dropdown_callback(self, caller_name: str, target_gesture: str):
@@ -258,13 +262,17 @@ 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 = Trigger(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,
+ )
# Remove keybind if "None"
else:
@@ -273,17 +281,17 @@ 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()
- ConfigManager().remove_temp_mouse_binding(device=target_device,
- action=target_action)
+ div["trigger_dropdown"].grid_remove()
+ 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
@@ -309,11 +317,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):
@@ -321,7 +329,6 @@ def leave(self):
class PageSelectGestures(SafeDisposableFrame):
-
def __init__(self, master, **kwargs):
super().__init__(master, **kwargs)
@@ -332,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):
@@ -362,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 97b0f6b4..bda80da4 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
@@ -19,30 +6,28 @@
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 it's buffer
+ # Detect landmarks (async) and save in its buffer
FaceMesh().detect_frame(frame_rgb)
# Get facial landmarks
landmarks = FaceMesh().get_landmarks()
- if (landmarks is None):
- CameraManager().draw_overlay(track_loc=None)
+ if landmarks is 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/shape_list.py b/src/shape_list.py
index 3a89ca7f..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",
@@ -73,24 +73,31 @@
"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())
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 7b27edfc..520bbdb0 100644
--- a/src/singleton_meta.py
+++ b/src/singleton_meta.py
@@ -1,24 +1,8 @@
-# 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):
_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 57407c30..e52b6a63 100644
--- a/src/task_killer.py
+++ b/src/task_killer.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 logging
import os
import signal
@@ -25,11 +11,10 @@
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):
@@ -39,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
@@ -72,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/Trigger.py b/src/utils/Trigger.py
new file mode 100644
index 00000000..695cd08d
--- /dev/null
+++ b/src/utils/Trigger.py
@@ -0,0 +1,9 @@
+from enum import Enum
+
+
+class Trigger(Enum):
+ DYNAMIC = "dynamic"
+ RAPID = "rapid"
+ SINGLE = "single"
+ HOLD = "hold"
+ TOGGLE = "toggle"
diff --git a/src/utils/__init__.py b/src/utils/__init__.py
index 07b7fe3d..b1d798fc 100644
--- a/src/utils/__init__.py
+++ b/src/utils/__init__.py
@@ -1,17 +1,18 @@
-# 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 *
-from .smoothing import *
+__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 .smoothing import calc_smooth_kernel, apply_smoothing
diff --git a/src/utils/install_font.py b/src/utils/install_font.py
index 4c7a737a..93b22b5d 100644
--- a/src/utils/install_font.py
+++ b/src/utils/install_font.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 ctypes
import logging
from pathlib import Path
@@ -19,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()}")
@@ -28,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 193dc465..cfad32d8 100644
--- a/src/utils/list_cameras.py
+++ b/src/utils/list_cameras.py
@@ -1,40 +1,30 @@
-# 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 platform
import cv2
+if platform.system() == "Windows":
+ import pygrabber.dshow_graph
+
logger = logging.getLogger("ListCamera")
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)
+ 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:
@@ -42,42 +32,46 @@ 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):
- """For swapping camera
- """
+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:
+ if platform.system() == "Windows":
+ return str(pygrabber.dshow_graph.FilterGraph().get_input_devices()[i])
+ else:
+ return None
diff --git a/src/utils/smoothing.py b/src/utils/smoothing.py
index 35a2e6b0..257f0bcd 100644
--- a/src/utils/smoothing.py
+++ b/src/utils/smoothing.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 numpy as np
import numpy.typing as npt
@@ -23,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:])