mirror of
https://github.com/onyx-and-iris/voicemeeter-compact.git
synced 2026-03-23 18:29:09 +00:00
Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5258671a79 | |||
| 2185748435 | |||
| d82afc1973 | |||
| 6be32dcd32 | |||
| abb55d5090 | |||
| 3271a52f15 | |||
| eb2ce5360f | |||
| 45dbcae804 | |||
| 4fab6d9ad9 | |||
| fee3fa199b | |||
| cf3205a86f | |||
| c0416d5b7c | |||
| a952f64bab | |||
| 3f6172c4bf | |||
| 96f3fbbee1 | |||
| 0bc566fa00 | |||
| c9b7f89453 | |||
| bdba07694b | |||
| 462301cd4e | |||
| 768fed217b | |||
| 34299ad84e | |||
| 7a78d7233e | |||
| 971b4a4601 | |||
| b219511ef8 | |||
| 270bda2dc1 | |||
| 6d5bd673a4 | |||
| 16ac188eb4 | |||
| 737dc75cba | |||
| 03d8415f68 | |||
| bd868d4613 | |||
| a65f851403 | |||
| 9e5b5a31a8 | |||
| 7aa8091de6 | |||
| d903faecd9 | |||
| b0d7d734fb | |||
| 262dcd572b | |||
| d612d38933 | |||
| 5a693b8aaf | |||
| 9210a26de6 | |||
| b0f634f1e8 | |||
| 5e5ae33e6a | |||
| 0d04a2f33e | |||
| 81a5497a32 | |||
| edc76db88e | |||
| 3b701a074d | |||
| 66cabb68cf | |||
|
|
37d7e58704 | ||
| 293bccc5ba | |||
| 1d8ffdc756 | |||
| e4d87334cb | |||
| ad3020809e | |||
| 76c6630892 | |||
| cc46fc31f8 | |||
| 8657e8846a | |||
| 43aad156a0 | |||
| 5101ff01f2 | |||
| c437ae5843 | |||
| ae59ba30f9 | |||
| a3fa227ac1 | |||
| b1b6c66828 | |||
| cb00de36f0 | |||
| ae200068d0 | |||
| b720494c68 | |||
| 6e6308a17f | |||
| 848248d02b | |||
| e4fc68c1ad | |||
| 752d1d7dd9 | |||
| 70c225bda3 | |||
| f459cdee44 | |||
| 198c08003e | |||
| 6fa9bf7131 | |||
| c3f20ea43f | |||
| 2e868705d5 | |||
| df6f215514 | |||
|
|
0a5987631f | ||
| 3d3bc71d15 | |||
| 7b148b2614 | |||
| ef558fdde6 | |||
| c684ed9981 | |||
| 1498daf36f | |||
| e964c94d07 | |||
| 9f20225a59 | |||
| f63c36c94a |
53
.github/workflows/publish.yml
vendored
Normal file
53
.github/workflows/publish.yml
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
name: Publish to PyPI
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install Poetry
|
||||
run: |
|
||||
pip install poetry==2.3.1
|
||||
poetry --version
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
poetry install --only-root
|
||||
poetry build
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: ./dist
|
||||
|
||||
pypi-publish:
|
||||
needs: build
|
||||
name: Upload release to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/project/voicemeeter-compact/
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: ./dist
|
||||
|
||||
- name: Publish package distributions to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: ./dist
|
||||
341
.github/workflows/release.yml
vendored
Normal file
341
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,341 @@
|
||||
name: Release Voicemeeter Compact
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
push:
|
||||
tags: ['v*.*.*']
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
version: latest
|
||||
virtualenvs-create: true
|
||||
virtualenvs-in-project: true
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@v1
|
||||
with:
|
||||
version: 3.x
|
||||
|
||||
- name: Download Forest TTK Theme
|
||||
run: |
|
||||
# Clone the Forest theme repository
|
||||
git clone https://github.com/rdbende/Forest-ttk-theme.git temp-forest-theme
|
||||
|
||||
# Copy the required theme files to theme/forest
|
||||
Copy-Item "temp-forest-theme\forest-dark.tcl" "theme\forest\"
|
||||
Copy-Item "temp-forest-theme\forest-light.tcl" "theme\forest\"
|
||||
Copy-Item "temp-forest-theme\forest-dark" "theme\forest\" -Recurse
|
||||
Copy-Item "temp-forest-theme\forest-light" "theme\forest\" -Recurse
|
||||
|
||||
# Clean up
|
||||
Remove-Item temp-forest-theme -Recurse -Force
|
||||
shell: pwsh
|
||||
|
||||
- name: Download Azure TTK Theme
|
||||
run: |
|
||||
# Clone the Azure theme repository
|
||||
git clone https://github.com/rdbende/Azure-ttk-theme.git temp-azure-theme
|
||||
|
||||
# Copy the required theme files to theme/azure
|
||||
Copy-Item "temp-azure-theme\azure.tcl" "theme\azure\"
|
||||
Copy-Item "temp-azure-theme\theme" "theme\azure\" -Recurse
|
||||
|
||||
# Clean up
|
||||
Remove-Item temp-azure-theme -Recurse -Force
|
||||
shell: pwsh
|
||||
|
||||
- name: Cache Poetry dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .venv
|
||||
key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}
|
||||
|
||||
- name: Install Poetry plugins
|
||||
run: poetry self add poethepoet
|
||||
shell: bash
|
||||
|
||||
- name: Replace path dependencies with PyPI versions
|
||||
run: |
|
||||
poetry remove voicemeeter-api vban-cmd || true
|
||||
poetry add voicemeeter-api vban-cmd
|
||||
shell: bash
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
# Install project dependencies
|
||||
poetry install --with build
|
||||
|
||||
# Verify PyInstaller is available
|
||||
echo "Verifying PyInstaller installation..."
|
||||
poetry show pyinstaller
|
||||
|
||||
- name: Get Poetry executable path
|
||||
shell: bash
|
||||
run: |
|
||||
poetryPath=$(which poetry)
|
||||
echo "Poetry path: $poetryPath"
|
||||
echo "POETRY_BIN=$poetryPath" >> $GITHUB_ENV
|
||||
|
||||
- name: Build artifacts with dynamic taskfile
|
||||
run: task --taskfile Taskfile.dynamic.yml build-all
|
||||
shell: bash
|
||||
env:
|
||||
POETRY_BIN: ${{ env.POETRY_BIN }}
|
||||
|
||||
- name: Create release archives
|
||||
run: task --taskfile Taskfile.dynamic.yml compress-all
|
||||
shell: bash
|
||||
env:
|
||||
POETRY_BIN: ${{ env.POETRY_BIN }}
|
||||
|
||||
- name: Verify build outputs
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "Verifying build outputs..."
|
||||
|
||||
$expectedFiles = @(
|
||||
"dist/sunvalley-basic.zip",
|
||||
"dist/sunvalley-banana.zip",
|
||||
"dist/sunvalley-potato.zip",
|
||||
"dist/forest-dark-basic.zip",
|
||||
"dist/forest-dark-banana.zip",
|
||||
"dist/forest-dark-potato.zip",
|
||||
"dist/forest-light-basic.zip",
|
||||
"dist/forest-light-banana.zip",
|
||||
"dist/forest-light-potato.zip",
|
||||
"dist/azure-dark-basic.zip",
|
||||
"dist/azure-dark-banana.zip",
|
||||
"dist/azure-dark-potato.zip",
|
||||
"dist/azure-light-basic.zip",
|
||||
"dist/azure-light-banana.zip",
|
||||
"dist/azure-light-potato.zip"
|
||||
)
|
||||
|
||||
$missingFiles = @()
|
||||
$foundFiles = @()
|
||||
|
||||
foreach ($file in $expectedFiles) {
|
||||
if (Test-Path $file) {
|
||||
$size = [math]::Round((Get-Item $file).Length / 1MB, 2)
|
||||
$foundFiles += "$file ($size MB)"
|
||||
} else {
|
||||
$missingFiles += $file
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Found files:"
|
||||
$foundFiles | ForEach-Object { Write-Host " $_" }
|
||||
|
||||
if ($missingFiles.Count -gt 0) {
|
||||
Write-Host -ForegroundColor Red "Missing files:"
|
||||
$missingFiles | ForEach-Object { Write-Host " $_" }
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host -ForegroundColor Green "All expected files found!"
|
||||
|
||||
# Sunvalley theme variants
|
||||
- name: Upload build artifacts - Sunvalley Basic
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sunvalley-basic
|
||||
path: dist/sunvalley-basic.zip
|
||||
|
||||
- name: Upload build artifacts - Sunvalley Banana
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sunvalley-banana
|
||||
path: dist/sunvalley-banana.zip
|
||||
|
||||
- name: Upload build artifacts - Sunvalley Potato
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sunvalley-potato
|
||||
path: dist/sunvalley-potato.zip
|
||||
|
||||
# Forest theme variants (dark)
|
||||
- name: Upload build artifacts - Forest Basic Dark
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: forest-dark-basic
|
||||
path: dist/forest-dark-basic.zip
|
||||
|
||||
- name: Upload build artifacts - Forest Banana Dark
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: forest-dark-banana
|
||||
path: dist/forest-dark-banana.zip
|
||||
|
||||
- name: Upload build artifacts - Forest Potato Dark
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: forest-dark-potato
|
||||
path: dist/forest-dark-potato.zip
|
||||
|
||||
# Forest theme variants (light)
|
||||
- name: Upload build artifacts - Forest Basic Light
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: forest-light-basic
|
||||
path: dist/forest-light-basic.zip
|
||||
|
||||
- name: Upload build artifacts - Forest Banana Light
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: forest-light-banana
|
||||
path: dist/forest-light-banana.zip
|
||||
|
||||
- name: Upload build artifacts - Forest Potato Light
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: forest-light-potato
|
||||
path: dist/forest-light-potato.zip
|
||||
|
||||
# Azure theme variants (dark)
|
||||
- name: Upload build artifacts - Azure Basic Dark
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: azure-dark-basic
|
||||
path: dist/azure-dark-basic.zip
|
||||
|
||||
- name: Upload build artifacts - Azure Banana Dark
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: azure-dark-banana
|
||||
path: dist/azure-dark-banana.zip
|
||||
|
||||
- name: Upload build artifacts - Azure Potato Dark
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: azure-dark-potato
|
||||
path: dist/azure-dark-potato.zip
|
||||
|
||||
# Azure theme variants (light)
|
||||
- name: Upload build artifacts - Azure Basic Light
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: azure-light-basic
|
||||
path: dist/azure-light-basic.zip
|
||||
|
||||
- name: Upload build artifacts - Azure Banana Light
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: azure-light-banana
|
||||
path: dist/azure-light-banana.zip
|
||||
|
||||
- name: Upload build artifacts - Azure Potato Light
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: azure-light-potato
|
||||
path: dist/azure-light-potato.zip
|
||||
|
||||
- name: Build Summary
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host -ForegroundColor Green "Build completed successfully!"
|
||||
Write-Host ""
|
||||
Write-Host "Built artifacts (15 theme variants):"
|
||||
Write-Host " Sunvalley Theme:"
|
||||
Write-Host " - voicemeeter-compact-sunvalley-basic.zip"
|
||||
Write-Host " - voicemeeter-compact-sunvalley-banana.zip"
|
||||
Write-Host " - voicemeeter-compact-sunvalley-potato.zip"
|
||||
Write-Host " Forest Theme:"
|
||||
Write-Host " - voicemeeter-compact-forest-dark-basic.zip"
|
||||
Write-Host " - voicemeeter-compact-forest-dark-banana.zip"
|
||||
Write-Host " - voicemeeter-compact-forest-dark-potato.zip"
|
||||
Write-Host " - voicemeeter-compact-forest-light-basic.zip"
|
||||
Write-Host " - voicemeeter-compact-forest-light-banana.zip"
|
||||
Write-Host " - voicemeeter-compact-forest-light-potato.zip"
|
||||
Write-Host " Azure Theme:"
|
||||
Write-Host " - voicemeeter-compact-azure-dark-basic.zip"
|
||||
Write-Host " - voicemeeter-compact-azure-dark-banana.zip"
|
||||
Write-Host " - voicemeeter-compact-azure-dark-potato.zip"
|
||||
Write-Host " - voicemeeter-compact-azure-light-basic.zip"
|
||||
Write-Host " - voicemeeter-compact-azure-light-banana.zip"
|
||||
Write-Host " - voicemeeter-compact-azure-light-potato.zip"
|
||||
|
||||
release:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
- name: Create Release
|
||||
run: |
|
||||
TAG_NAME=${GITHUB_REF#refs/tags/}
|
||||
gh release create $TAG_NAME \
|
||||
--title "Voicemeeter Compact $TAG_NAME" \
|
||||
--notes "## Voicemeeter Compact Release $TAG_NAME
|
||||
|
||||
### Theme Variants
|
||||
Choose your preferred theme and Voicemeeter version:
|
||||
|
||||
**Sunvalley Theme**
|
||||
- **sunvalley-basic.zip** - For Voicemeeter Basic
|
||||
- **sunvalley-banana.zip** - For Voicemeeter Banana
|
||||
- **sunvalley-potato.zip** - For Voicemeeter Potato
|
||||
|
||||
**Forest Theme**
|
||||
- **forest-dark-basic.zip** - Dark theme for Voicemeeter Basic
|
||||
- **forest-dark-banana.zip** - Dark theme for Voicemeeter Banana
|
||||
- **forest-dark-potato.zip** - Dark theme for Voicemeeter Potato
|
||||
- **forest-light-basic.zip** - Light theme for Voicemeeter Basic
|
||||
- **forest-light-banana.zip** - Light theme for Voicemeeter Banana
|
||||
- **forest-light-potato.zip** - Light theme for Voicemeeter Potato
|
||||
|
||||
**Azure Theme**
|
||||
- **azure-dark-basic.zip** - Dark theme for Voicemeeter Basic
|
||||
- **azure-dark-banana.zip** - Dark theme for Voicemeeter Banana
|
||||
- **azure-dark-potato.zip** - Dark theme for Voicemeeter Potato
|
||||
- **azure-light-basic.zip** - Light theme for Voicemeeter Basic
|
||||
- **azure-light-banana.zip** - Light theme for Voicemeeter Banana
|
||||
- **azure-light-potato.zip** - Light theme for Voicemeeter Potato
|
||||
|
||||
### Requirements
|
||||
- Windows 10/11
|
||||
- Voicemeeter (Basic/Banana/Potato) installed
|
||||
- Python 3.10+ (if running from source)
|
||||
|
||||
### Installation
|
||||
1. Download the zip file matching your Voicemeeter version and preferred theme
|
||||
2. Extract and run the executable - no installation required
|
||||
3. The application will automatically detect your Voicemeeter installation
|
||||
|
||||
### Notes
|
||||
- Built with PyInstaller for standalone execution
|
||||
- Each variant is scaled for its specific Voicemeeter version
|
||||
- Themes provide different visual styles while maintaining full functionality"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload release assets
|
||||
run: |
|
||||
TAG_NAME=${GITHUB_REF#refs/tags/}
|
||||
find . -name "*.zip" -exec gh release upload $TAG_NAME {} \;
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
19
.github/workflows/ruff.yml
vendored
Normal file
19
.github/workflows/ruff.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
name: Ruff
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
ruff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: astral-sh/ruff-action@v3
|
||||
with:
|
||||
args: 'format --check --diff'
|
||||
112
.gitignore
vendored
112
.gitignore
vendored
@ -1,9 +1,9 @@
|
||||
# quick test
|
||||
quick.py
|
||||
# Generated by ignr: github.com/onyx-and-iris/ignr
|
||||
|
||||
## Python ##
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.py[codz]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
@ -23,7 +23,6 @@ parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
@ -50,9 +49,10 @@ htmlcov/
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
*.py.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
@ -75,6 +75,7 @@ instance/
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
@ -85,7 +86,9 @@ profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
@ -94,7 +97,37 @@ ipython_config.py
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
#poetry.toml
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
||||
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
||||
#pdm.lock
|
||||
#pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# pixi
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
||||
#pixi.lock
|
||||
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
||||
# in the .venv directory. It is recommended not to include this directory in version control.
|
||||
.pixi
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
@ -106,13 +139,13 @@ celerybeat.pid
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.envrc
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
venv_vmcompact/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
@ -132,4 +165,65 @@ dmypy.json
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
.vscode/
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Abstra
|
||||
# Abstra is an AI-powered process automation framework.
|
||||
# Ignore directories containing user credentials, local state, and settings.
|
||||
# Learn more at https://abstra.io/docs
|
||||
.abstra/
|
||||
|
||||
# Visual Studio Code
|
||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||
# you could uncomment the following to ignore the entire vscode folder
|
||||
.vscode/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Cursor
|
||||
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
||||
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
||||
# refer to https://docs.cursor.com/context/ignore-files
|
||||
.cursorignore
|
||||
.cursorindexingignore
|
||||
|
||||
# Marimo
|
||||
marimo/_static/
|
||||
marimo/_lsp/
|
||||
__marimo__/
|
||||
|
||||
# End of ignr
|
||||
|
||||
# Test files
|
||||
test-*.py
|
||||
|
||||
# Forest/Azure theme files
|
||||
theme/**/*
|
||||
!theme/*/
|
||||
!theme/**/.gitkeep
|
||||
|
||||
# Spec files
|
||||
spec/**/*
|
||||
!spec/*/
|
||||
!spec/**/.gitkeep
|
||||
|
||||
# Taskfile build files
|
||||
Taskfile.unified.yml
|
||||
SPEC_CONSOLIDATION.md
|
||||
|
||||
13
.pre-commit-config.yaml
Normal file
13
.pre-commit-config.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: https://github.com/python-poetry/poetry
|
||||
rev: '2.3.2'
|
||||
hooks:
|
||||
- id: poetry-check
|
||||
- id: poetry-lock
|
||||
36
CHANGELOG.md
36
CHANGELOG.md
@ -7,7 +7,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- [ ] Add support for forest theme (if rbende adds it to pypi)
|
||||
- [ ]
|
||||
|
||||
## [1.10.0] - 2026-03-26
|
||||
|
||||
### Added
|
||||
|
||||
- Automated builds for Releases. This is much preferred over manual releases because users can be sure the files are built directly from the source code.
|
||||
- Azure theme added to Releases.
|
||||
- vban.toml files can now use key `host` intead of `ip`.
|
||||
- `ip` is still usable for backwards compatibility.
|
||||
|
||||
### Changed
|
||||
|
||||
- Attempting a VBAN connection now uses a PING/PONG handshake to verify connection, this makes connections more reliable.
|
||||
- Navigation frame is disabled by default. You can easily enable it from the menu or with an app.toml config.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Comp, Gate sliders now receive feedback when changes are made on the Voicemeeter GUI.
|
||||
- Bus CONFIG mode button rotates through the correct modes for Basic Kind.
|
||||
- Bus CONFIG mono now rotates through *off, on, stereo reverse*.
|
||||
- Bus CONFIG mode/mono buttons are now a fixed width.
|
||||
|
||||
## [1.9.8] - 2025-01-22
|
||||
|
||||
### Changed
|
||||
|
||||
- vm-compact config dirs now override _internal/configs (if using build from releases). See [TOML Files](https://github.com/onyx-and-iris/voicemeeter-compact?tab=readme-ov-file#toml-files) section in README.
|
||||
- after disconnecting from a vban connection, vban menus are re-enabled after 500ms.
|
||||
|
||||
## [1.9.5] - 2024-07-03
|
||||
|
||||
### Changed
|
||||
|
||||
- Launching the Voicemeeter Compact app will now launch the x64 bit Voicemeeter GUI (on 64 bit systems) for all kinds.
|
||||
|
||||
## [1.9.0] - 2023-07-10
|
||||
|
||||
|
||||
39
README.md
39
README.md
@ -1,6 +1,7 @@
|
||||
[](https://badge.fury.io/py/voicemeeter-compact)
|
||||
[](https://github.com/onyx-and-iris/voicemeeter-compact/blob/main/LICENSE)
|
||||
[](https://github.com/psf/black)
|
||||
[](https://python-poetry.org/)
|
||||
[](https://github.com/astral-sh/ruff)
|
||||

|
||||
|
||||

|
||||
@ -30,12 +31,13 @@ Example `__main__.py` file:
|
||||
|
||||
```python
|
||||
import voicemeeterlib
|
||||
|
||||
import vmcompact
|
||||
|
||||
|
||||
def main():
|
||||
# choose the kind of Voicemeeter (Local connection)
|
||||
KIND_ID = "banana"
|
||||
KIND_ID = 'banana'
|
||||
|
||||
# pass the KIND_ID and the vm object to the app
|
||||
with voicemeeterlib.api(KIND_ID) as vm:
|
||||
@ -43,7 +45,7 @@ def main():
|
||||
app.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
```
|
||||
|
||||
@ -63,15 +65,18 @@ Set the kind of Voicemeeter, KIND_ID may be:
|
||||
|
||||
## TOML Files
|
||||
|
||||
This is how your files should be organised. Wherever your `__main__.py` file is located (after install this can be any location), `configs` should be in the same location.
|
||||
Directly inside of configs directory you may place an app.toml, vban.toml and a directory for each kind.
|
||||
Inside each kind directory you may place as many custom toml configurations as you wish.
|
||||
If you've downloaded the binary from [Releases][releases] you can find configs included in the `_internal/configs` directory.
|
||||
|
||||
You may override these configs by placing a directory `vm-compact` in one of the following locations:
|
||||
|
||||
- `user home directory / .config`
|
||||
- `user home directory / Documents / Voicemeeter`
|
||||
|
||||
The contents should match the following directory structure:
|
||||
|
||||
.
|
||||
|
||||
├── `__main__.py`
|
||||
|
||||
├── configs
|
||||
├── vm-compact
|
||||
|
||||
├── app.toml
|
||||
|
||||
@ -109,7 +114,7 @@ Configure certain startup states for the app.
|
||||
Configure a user config to load on app startup. Don't include the .toml extension in the config name.
|
||||
|
||||
- `theme`
|
||||
By default the app loads up the [Sun Valley light or dark theme](https://github.com/rdbende/Sun-Valley-ttk-theme) by @rdbende. You have the option to load up the app without any theme loaded. Simply set `enabled` to false and `mode` will take no effect.
|
||||
By default the app loads up the [Sun Valley light or dark theme][releases] by @rdbende. You have the option to load up the app without any theme loaded. Simply set `enabled` to false and `mode` will take no effect.
|
||||
|
||||
- `extends`
|
||||
Extending the app will show both strips and buses. In reduced mode only one or the other. This app will extend both horizontally and vertically, simply set `extends_horizontal` true or false accordingly.
|
||||
@ -134,13 +139,13 @@ A valid `vban.toml` might look like this:
|
||||
```toml
|
||||
[connection-1]
|
||||
kind = 'banana'
|
||||
ip = '192.168.1.2'
|
||||
host = '192.168.1.2'
|
||||
streamname = 'worklaptop'
|
||||
port = 6980
|
||||
|
||||
[connection-2]
|
||||
kind = 'potato'
|
||||
ip = '192.168.1.3'
|
||||
host = '192.168.1.3'
|
||||
streamname = 'streampc'
|
||||
port = 6990
|
||||
```
|
||||
@ -149,7 +154,7 @@ port = 6990
|
||||
|
||||
Three example user configs are included with the package, one for each kind of Voicemeeter. Use these to configure parameter startup states. Any parameter supported by the underlying interfaces may be used. Check the 'multiple-parameters' section for more info:
|
||||
|
||||
[Python Interface for Voicemeeter API](https://github.com/onyx-and-iris/voicemeeter-api-python#multiple-parameters)
|
||||
[Python Interface for the Voicemeeter API](https://github.com/onyx-and-iris/voicemeeter-api-python#multiple-parameters)
|
||||
|
||||
[Python Interface for VBAN CMD](https://github.com/onyx-and-iris/vban-cmd-python#multiple-parameters)
|
||||
|
||||
@ -157,6 +162,10 @@ User configs may be loaded at any time via the menu.
|
||||
|
||||
## Special Thanks
|
||||
|
||||
[Vincent Burel](https://github.com/vburel2018) for creating Voicemeeter, its SDK, the C Remote API, the RT Packet service and Streamer View app!
|
||||
[Vincent Burel](https://github.com/vburel2018) for creating Voicemeeter and its SDK.
|
||||
|
||||
[Rdbende](https://github.com/rdbende) for creating the beautiful Sun Valley Tkinter theme and adding it to Pypi!
|
||||
[Rdbende](https://github.com/rdbende) for creating the beautiful [Sun Valley theme][sv-theme].
|
||||
|
||||
|
||||
[sv-theme]: https://github.com/rdbende/Sun-Valley-ttk-theme
|
||||
[releases]: https://github.com/onyx-and-iris/voicemeeter-compact/releases
|
||||
38
Taskfile.azure.yml
Normal file
38
Taskfile.azure.yml
Normal file
@ -0,0 +1,38 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
desc: Build Azure artifacts
|
||||
deps: [rewrite]
|
||||
cmds:
|
||||
- defer: { task: restore }
|
||||
- for:
|
||||
matrix:
|
||||
KIND: [basic, banana, potato]
|
||||
THEME: [azure-light, azure-dark]
|
||||
cmd: poetry run pyinstaller --noconfirm --distpath dist/{{.ITEM.THEME}}-{{.ITEM.KIND}} spec/azure/{{.ITEM.THEME}}-{{.ITEM.KIND}}.spec
|
||||
|
||||
rewrite:
|
||||
desc: Run the source code rewriter
|
||||
cmds:
|
||||
- poetry run python tools/rewriter.py --rewrite --theme {{.THEME}}
|
||||
|
||||
restore:
|
||||
desc: Restore the backup files
|
||||
cmds:
|
||||
- poetry run python tools/rewriter.py --restore
|
||||
|
||||
compress:
|
||||
desc: Compress Azure artifacts
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
KIND: [basic, banana, potato]
|
||||
THEME: [azure-light, azure-dark]
|
||||
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/{{.ITEM.THEME}}-{{.ITEM.KIND}} -DestinationPath dist/{{.ITEM.THEME}}-{{.ITEM.KIND}}.zip -Force"'
|
||||
|
||||
clean:
|
||||
desc: Clean build and dist directories
|
||||
cmds:
|
||||
- |
|
||||
{{.SHELL}} -Command "Remove-Item -Path build/azure-*,dist/azure-* -Recurse -Force"
|
||||
84
Taskfile.dynamic.yml
Normal file
84
Taskfile.dynamic.yml
Normal file
@ -0,0 +1,84 @@
|
||||
version: '3'
|
||||
|
||||
# Dynamic build system - no spec files needed!
|
||||
# Examples:
|
||||
# - task -t Taskfile.dynamic.yml build THEMES="azure forest"
|
||||
# - task -t Taskfile.dynamic.yml build-all
|
||||
# THEMES can be specified as a space-separated list or "all" to build everything.
|
||||
#
|
||||
# Compression tasks are also dynamic and can be used like:
|
||||
# Usage examples:
|
||||
# - task -t Taskfile.dynamic.yml compress THEME=azure
|
||||
# - task -t Taskfile.dynamic.yml compress-all
|
||||
|
||||
vars:
|
||||
THEMES: '{{.THEMES | default "all"}}'
|
||||
SHELL: pwsh
|
||||
|
||||
tasks:
|
||||
build:
|
||||
desc: Build specified themes dynamically (no spec files needed)
|
||||
cmds:
|
||||
- ${POETRY_BIN:-poetry} run python tools/dynamic_builder.py {{.THEMES}}
|
||||
|
||||
build-all:
|
||||
desc: Build all themes
|
||||
cmds:
|
||||
- ${POETRY_BIN:-poetry} run python tools/dynamic_builder.py all
|
||||
|
||||
build-azure:
|
||||
desc: Build only azure theme
|
||||
cmds:
|
||||
- ${POETRY_BIN:-poetry} run python tools/dynamic_builder.py azure
|
||||
|
||||
build-forest:
|
||||
desc: Build only forest theme
|
||||
cmds:
|
||||
- ${POETRY_BIN:-poetry} run python tools/dynamic_builder.py forest
|
||||
|
||||
build-sunvalley:
|
||||
desc: Build only sunvalley theme
|
||||
cmds:
|
||||
- ${POETRY_BIN:-poetry} run python tools/dynamic_builder.py sunvalley
|
||||
|
||||
compress:
|
||||
desc: Compress artifacts for specified theme
|
||||
cmds:
|
||||
- task: compress-{{.THEME}}
|
||||
|
||||
compress-all:
|
||||
desc: Compress artifacts for all themes
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
THEME: [azure, forest, sunvalley]
|
||||
task: compress-{{.ITEM.THEME}}
|
||||
|
||||
compress-azure:
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
KIND: [basic, banana, potato]
|
||||
VARIANT: [azure-light, azure-dark]
|
||||
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/{{.ITEM.VARIANT}}-{{.ITEM.KIND}} -DestinationPath dist/{{.ITEM.VARIANT}}-{{.ITEM.KIND}}.zip -Force"'
|
||||
|
||||
compress-forest:
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
KIND: [basic, banana, potato]
|
||||
VARIANT: [forest-light, forest-dark]
|
||||
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/{{.ITEM.VARIANT}}-{{.ITEM.KIND}} -DestinationPath dist/{{.ITEM.VARIANT}}-{{.ITEM.KIND}}.zip -Force"'
|
||||
|
||||
compress-sunvalley:
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
KIND: [basic, banana, potato]
|
||||
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/sunvalley-{{.ITEM.KIND}} -DestinationPath dist/sunvalley-{{.ITEM.KIND}}.zip -Force"'
|
||||
|
||||
clean:
|
||||
desc: Clean all build artifacts
|
||||
cmds:
|
||||
- |
|
||||
{{.SHELL}} -Command "Remove-Item -Path build/*,dist/* -Recurse -Force -ErrorAction SilentlyContinue"
|
||||
38
Taskfile.forest.yml
Normal file
38
Taskfile.forest.yml
Normal file
@ -0,0 +1,38 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
desc: Build Forest artifacts
|
||||
deps: [rewrite]
|
||||
cmds:
|
||||
- defer: { task: restore }
|
||||
- for:
|
||||
matrix:
|
||||
KIND: [basic, banana, potato]
|
||||
THEME: [forest-light, forest-dark]
|
||||
cmd: poetry run pyinstaller --noconfirm --distpath dist/{{.ITEM.THEME}}-{{.ITEM.KIND}} spec/forest/{{.ITEM.THEME}}-{{.ITEM.KIND}}.spec
|
||||
|
||||
rewrite:
|
||||
desc: Run the source code rewriter
|
||||
cmds:
|
||||
- poetry run python tools/rewriter.py --rewrite --theme {{.THEME}}
|
||||
|
||||
restore:
|
||||
desc: Restore the backup files
|
||||
cmds:
|
||||
- poetry run python tools/rewriter.py --restore
|
||||
|
||||
compress:
|
||||
desc: Compress Forest artifacts
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
KIND: [basic, banana, potato]
|
||||
THEME: [forest-light, forest-dark]
|
||||
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/{{.ITEM.THEME}}-{{.ITEM.KIND}} -DestinationPath dist/{{.ITEM.THEME}}-{{.ITEM.KIND}}.zip -Force"'
|
||||
|
||||
clean:
|
||||
desc: Clean build and dist directories
|
||||
cmds:
|
||||
- |
|
||||
{{.SHELL}} -Command "Remove-Item -Path build/forest-*,dist/forest-* -Recurse -Force"
|
||||
24
Taskfile.sunvalley.yml
Normal file
24
Taskfile.sunvalley.yml
Normal file
@ -0,0 +1,24 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
desc: Build Sunvalley artifacts
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
KIND: [basic, banana, potato]
|
||||
cmd: poetry run pyinstaller --noconfirm --distpath dist/sunvalley-{{.ITEM.KIND}} spec/sunvalley/sunvalley-{{.ITEM.KIND}}.spec
|
||||
|
||||
compress:
|
||||
desc: Compress Sunvalley artifacts
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
KIND: [basic, banana, potato]
|
||||
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/sunvalley-{{.ITEM.KIND}} -DestinationPath dist/sunvalley-{{.ITEM.KIND}}.zip -Force"'
|
||||
|
||||
clean:
|
||||
desc: Clean build and dist directories
|
||||
cmds:
|
||||
- |
|
||||
{{.SHELL}} -Command "Remove-Item -Path build/sunvalley-*,dist/sunvalley-* -Recurse -Force"
|
||||
61
Taskfile.yml
Normal file
61
Taskfile.yml
Normal file
@ -0,0 +1,61 @@
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
sunvalley:
|
||||
taskfile: ./Taskfile.sunvalley.yml
|
||||
vars:
|
||||
THEME: sunvalley
|
||||
forest:
|
||||
taskfile: ./Taskfile.forest.yml
|
||||
vars:
|
||||
THEME: forest
|
||||
azure:
|
||||
taskfile: ./Taskfile.azure.yml
|
||||
vars:
|
||||
THEME: azure
|
||||
|
||||
vars:
|
||||
SHELL: pwsh
|
||||
|
||||
tasks:
|
||||
default:
|
||||
desc: Prepare artifacts for release
|
||||
cmds:
|
||||
- task: release
|
||||
|
||||
release:
|
||||
desc: Build and compress all artifacts
|
||||
cmds:
|
||||
- task: build
|
||||
- task: compress
|
||||
- echo "Release complete"
|
||||
|
||||
generate-specs:
|
||||
desc: Generate all spec files from templates
|
||||
cmds:
|
||||
- poetry run python tools/spec_generator.py --clean
|
||||
|
||||
build:
|
||||
desc: Build all artifacts
|
||||
deps: [generate-specs]
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
THEME: [sunvalley, forest, azure]
|
||||
task: '{{.ITEM.THEME}}:build'
|
||||
|
||||
compress:
|
||||
desc: Compress all artifacts
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
THEME: [sunvalley, forest, azure]
|
||||
task: '{{.ITEM.THEME}}:compress'
|
||||
|
||||
clean:
|
||||
desc: Clean up build and dist directories
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
THEME: [sunvalley, forest, azure]
|
||||
task: '{{.ITEM.THEME}}:clean'
|
||||
@ -4,12 +4,12 @@ import vmcompact
|
||||
|
||||
|
||||
def main():
|
||||
KIND_ID = "banana"
|
||||
KIND_ID = 'banana'
|
||||
|
||||
with voicemeeterlib.api(KIND_ID) as vmr:
|
||||
app = vmcompact.connect(KIND_ID, vmr)
|
||||
app.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
# load a specific profile on start (file name without .toml ext)
|
||||
# [configs]
|
||||
# config="example"
|
||||
# load with themes enabled? set the default mode
|
||||
# load with themes enabled?
|
||||
[theme]
|
||||
enabled = true
|
||||
mode = "light"
|
||||
# load in extended mode? if so which orientation
|
||||
[extends]
|
||||
extended = true
|
||||
@ -22,4 +21,4 @@ size = 3
|
||||
default = 0
|
||||
# show the navigation frame?
|
||||
[navigation]
|
||||
show = true
|
||||
show = false
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
### set the ip then uncomment
|
||||
# [connection-1]
|
||||
# kind = 'banana'
|
||||
# ip = '<ip address 1>'
|
||||
# ip = 'localhost'
|
||||
# streamname = 'Command1'
|
||||
# port = 6980
|
||||
|
||||
# [connection-2]
|
||||
# kind = 'potato'
|
||||
# ip = '<ip address 2>'
|
||||
# ip = 'gamepc.local'
|
||||
# streamname = 'Command1'
|
||||
# port = 6980
|
||||
|
||||
318
poetry.lock
generated
318
poetry.lock
generated
@ -1,169 +1,261 @@
|
||||
# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "22.12.0"
|
||||
description = "The uncompromising code formatter."
|
||||
name = "altgraph"
|
||||
version = "0.17.4"
|
||||
description = "Python graph (network) package"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = "*"
|
||||
groups = ["build"]
|
||||
files = [
|
||||
{file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"},
|
||||
{file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"},
|
||||
{file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"},
|
||||
{file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"},
|
||||
{file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"},
|
||||
{file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"},
|
||||
{file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"},
|
||||
{file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"},
|
||||
{file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"},
|
||||
{file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"},
|
||||
{file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"},
|
||||
{file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"},
|
||||
{file = "altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff"},
|
||||
{file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "macholib"
|
||||
version = "1.16.3"
|
||||
description = "Mach-O header analysis and editing"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["build"]
|
||||
markers = "sys_platform == \"darwin\""
|
||||
files = [
|
||||
{file = "macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c"},
|
||||
{file = "macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8.0.0"
|
||||
mypy-extensions = ">=0.4.3"
|
||||
pathspec = ">=0.9.0"
|
||||
platformdirs = ">=2"
|
||||
tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
|
||||
|
||||
[package.extras]
|
||||
colorama = ["colorama (>=0.4.3)"]
|
||||
d = ["aiohttp (>=3.7.4)"]
|
||||
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||
uvloop = ["uvloop (>=0.15.2)"]
|
||||
altgraph = ">=0.17"
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.4"
|
||||
description = "Composable command line interface toolkit"
|
||||
name = "packaging"
|
||||
version = "24.2"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["build"]
|
||||
files = [
|
||||
{file = "click-8.1.4-py3-none-any.whl", hash = "sha256:2739815aaa5d2c986a88f1e9230c55e17f0caad3d958a5e13ad0797c166db9e3"},
|
||||
{file = "click-8.1.4.tar.gz", hash = "sha256:b97d0c74955da062a7d4ef92fadb583806a585b2ea81958a81bd72726cbb8e37"},
|
||||
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
|
||||
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pefile"
|
||||
version = "2023.2.7"
|
||||
description = "Python PE parsing module"
|
||||
optional = false
|
||||
python-versions = ">=3.6.0"
|
||||
groups = ["build"]
|
||||
markers = "sys_platform == \"win32\""
|
||||
files = [
|
||||
{file = "pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"},
|
||||
{file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller"
|
||||
version = "6.11.1"
|
||||
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
|
||||
optional = false
|
||||
python-versions = "<3.14,>=3.8"
|
||||
groups = ["build"]
|
||||
files = [
|
||||
{file = "pyinstaller-6.11.1-py3-none-macosx_10_13_universal2.whl", hash = "sha256:44e36172de326af6d4e7663b12f71dbd34e2e3e02233e181e457394423daaf03"},
|
||||
{file = "pyinstaller-6.11.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:6d12c45a29add78039066a53fb05967afaa09a672426072b13816fe7676abfc4"},
|
||||
{file = "pyinstaller-6.11.1-py3-none-manylinux2014_i686.whl", hash = "sha256:ddc0fddd75f07f7e423da1f0822e389a42af011f9589e0269b87e0d89aa48c1f"},
|
||||
{file = "pyinstaller-6.11.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:0d6475559c4939f0735122989611d7f739ed3bf02f666ce31022928f7a7e4fda"},
|
||||
{file = "pyinstaller-6.11.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:e21c7806e34f40181e7606926a14579f848bfb1dc52cbca7eea66eccccbfe977"},
|
||||
{file = "pyinstaller-6.11.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:32c742a24fe65d0702958fadf4040f76de85859c26bec0008766e5dbabc5b68f"},
|
||||
{file = "pyinstaller-6.11.1-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:208c0ef6dab0837a0a273ea32d1a3619a208e3d1fe3fec3785eea71a77fd00ce"},
|
||||
{file = "pyinstaller-6.11.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:ad84abf465bcda363c1d54eafa76745d77b6a8a713778348377dc98d12a452f7"},
|
||||
{file = "pyinstaller-6.11.1-py3-none-win32.whl", hash = "sha256:2e8365276c5131c9bef98e358fbc305e4022db8bedc9df479629d6414021956a"},
|
||||
{file = "pyinstaller-6.11.1-py3-none-win_amd64.whl", hash = "sha256:7ac83c0dc0e04357dab98c487e74ad2adb30e7eb186b58157a8faf46f1fa796f"},
|
||||
{file = "pyinstaller-6.11.1-py3-none-win_arm64.whl", hash = "sha256:35e6b8077d240600bb309ed68bb0b1453fd2b7ab740b66d000db7abae6244423"},
|
||||
{file = "pyinstaller-6.11.1.tar.gz", hash = "sha256:491dfb4d9d5d1d9650d9507daec1ff6829527a254d8e396badd60a0affcb72ef"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
altgraph = "*"
|
||||
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
|
||||
packaging = ">=22.0"
|
||||
pefile = {version = ">=2022.5.30,<2024.8.26 || >2024.8.26", markers = "sys_platform == \"win32\""}
|
||||
pyinstaller-hooks-contrib = ">=2024.9"
|
||||
pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""}
|
||||
setuptools = ">=42.0.0"
|
||||
|
||||
[package.extras]
|
||||
completion = ["argcomplete"]
|
||||
hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
name = "pyinstaller-hooks-contrib"
|
||||
version = "2024.11"
|
||||
description = "Community maintained hooks for PyInstaller"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["build"]
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
{file = "pyinstaller_hooks_contrib-2024.11-py3-none-any.whl", hash = "sha256:2781d121a1ee961152ba7287a262c65a1078da30c9ef7621cb8c819326884fd5"},
|
||||
{file = "pyinstaller_hooks_contrib-2024.11.tar.gz", hash = "sha256:84399af6b4b902030958063df25f657abbff249d0f329c5344928355c9833ab4"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
packaging = ">=22.0"
|
||||
setuptools = ">=42.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "pywin32-ctypes"
|
||||
version = "0.2.3"
|
||||
description = "A (partial) reimplementation of pywin32 using ctypes/cffi"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["build"]
|
||||
markers = "sys_platform == \"win32\""
|
||||
files = [
|
||||
{file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"},
|
||||
{file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "isort"
|
||||
version = "5.12.0"
|
||||
description = "A Python utility / library to sort Python imports."
|
||||
name = "ruff"
|
||||
version = "0.9.1"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.8.0"
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"},
|
||||
{file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"},
|
||||
{file = "ruff-0.9.1-py3-none-linux_armv6l.whl", hash = "sha256:84330dda7abcc270e6055551aca93fdde1b0685fc4fd358f26410f9349cf1743"},
|
||||
{file = "ruff-0.9.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3cae39ba5d137054b0e5b472aee3b78a7c884e61591b100aeb544bcd1fc38d4f"},
|
||||
{file = "ruff-0.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:50c647ff96f4ba288db0ad87048257753733763b409b2faf2ea78b45c8bb7fcb"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0c8b149e9c7353cace7d698e1656ffcf1e36e50f8ea3b5d5f7f87ff9986a7ca"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:beb3298604540c884d8b282fe7625651378e1986c25df51dec5b2f60cafc31ce"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39d0174ccc45c439093971cc06ed3ac4dc545f5e8bdacf9f067adf879544d969"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:69572926c0f0c9912288915214ca9b2809525ea263603370b9e00bed2ba56dbd"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:937267afce0c9170d6d29f01fcd1f4378172dec6760a9f4dface48cdabf9610a"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:186c2313de946f2c22bdf5954b8dd083e124bcfb685732cfb0beae0c47233d9b"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f94942a3bb767675d9a051867c036655fe9f6c8a491539156a6f7e6b5f31831"},
|
||||
{file = "ruff-0.9.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:728d791b769cc28c05f12c280f99e8896932e9833fef1dd8756a6af2261fd1ab"},
|
||||
{file = "ruff-0.9.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2f312c86fb40c5c02b44a29a750ee3b21002bd813b5233facdaf63a51d9a85e1"},
|
||||
{file = "ruff-0.9.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ae017c3a29bee341ba584f3823f805abbe5fe9cd97f87ed07ecbf533c4c88366"},
|
||||
{file = "ruff-0.9.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5dc40a378a0e21b4cfe2b8a0f1812a6572fc7b230ef12cd9fac9161aa91d807f"},
|
||||
{file = "ruff-0.9.1-py3-none-win32.whl", hash = "sha256:46ebf5cc106cf7e7378ca3c28ce4293b61b449cd121b98699be727d40b79ba72"},
|
||||
{file = "ruff-0.9.1-py3-none-win_amd64.whl", hash = "sha256:342a824b46ddbcdddd3abfbb332fa7fcaac5488bf18073e841236aadf4ad5c19"},
|
||||
{file = "ruff-0.9.1-py3-none-win_arm64.whl", hash = "sha256:1cd76c7f9c679e6e8f2af8f778367dca82b95009bc7b1a85a47f1521ae524fa7"},
|
||||
{file = "ruff-0.9.1.tar.gz", hash = "sha256:fd2b25ecaf907d6458fa842675382c8597b3c746a2dde6717fe3415425df0c17"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "78.1.1"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["build"]
|
||||
files = [
|
||||
{file = "setuptools-78.1.1-py3-none-any.whl", hash = "sha256:c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561"},
|
||||
{file = "setuptools-78.1.1.tar.gz", hash = "sha256:fcc17fd9cd898242f6b4adfaca46137a9edef687f43e6f78469692a5e70d851d"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
colors = ["colorama (>=0.4.3)"]
|
||||
pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
|
||||
plugins = ["setuptools"]
|
||||
requirements-deprecated-finder = ["pip-api", "pipreqs"]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.0.0"
|
||||
description = "Type system extensions for programs checked with the mypy type checker."
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
||||
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.11.1"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"},
|
||||
{file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "3.8.1"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "platformdirs-3.8.1-py3-none-any.whl", hash = "sha256:cec7b889196b9144d088e4c57d9ceef7374f6c39694ad1577a0aab50d27ea28c"},
|
||||
{file = "platformdirs-3.8.1.tar.gz", hash = "sha256:f87ca4fcff7d2b0f81c6a748a77973d7af0f4d526f98f308477c3c436c74d528"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
|
||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"]
|
||||
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""]
|
||||
core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"]
|
||||
cover = ["pytest-cov"]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
|
||||
enabler = ["pytest-enabler (>=2.2)"]
|
||||
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
|
||||
type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"]
|
||||
|
||||
[[package]]
|
||||
name = "sv-ttk"
|
||||
version = "2.5.5"
|
||||
version = "2.6.0"
|
||||
description = "A gorgeous theme for Tkinter, based on Windows 11's UI"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "sv_ttk-2.5.5-py3-none-any.whl", hash = "sha256:49d1cd03c032728c183d1fe2318f88cdb658ef3e87157e1ca3fcf6661054965b"},
|
||||
{file = "sv_ttk-2.5.5.tar.gz", hash = "sha256:9bbfe2aba6cc6f9fdf70d79331046543c9666fcccc78bad5ff648a9987e3cedb"},
|
||||
{file = "sv_ttk-2.6.0-py3-none-any.whl", hash = "sha256:4319c52edf2e14732fe84bdc9788e26f9e9a1ad79451ec0f89f0120ffc8105d9"},
|
||||
{file = "sv_ttk-2.6.0.tar.gz", hash = "sha256:3fd440396c95e30e88f686fcf28be425480f7320d6bf346f9cea5d6f56702cc2"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.0.1"
|
||||
version = "2.2.1"
|
||||
description = "A lil' TOML parser"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version == \"3.10\""
|
||||
files = [
|
||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"},
|
||||
{file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
|
||||
{file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vban-cmd"
|
||||
version = "2.4.4"
|
||||
version = "2.10.3"
|
||||
description = "Python interface for the VBAN RT Packet Service (Sendtext)"
|
||||
optional = false
|
||||
python-versions = ">=3.10,<4.0"
|
||||
files = [
|
||||
{file = "vban_cmd-2.4.4-py3-none-any.whl", hash = "sha256:f439219a2dc6a45123bb70fc94d2aabfc643f26dffc9206da2228e2520f83e01"},
|
||||
{file = "vban_cmd-2.4.4.tar.gz", hash = "sha256:31dbf6abb681d57772c9082722b024d0798eff99b4c622bfbb539179852a935d"},
|
||||
]
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main", "dev"]
|
||||
files = []
|
||||
develop = true
|
||||
|
||||
[package.dependencies]
|
||||
tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
|
||||
tomli = {version = ">=2.0.1,<3.0", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.source]
|
||||
type = "directory"
|
||||
url = "../vban-cmd-python"
|
||||
|
||||
[[package]]
|
||||
name = "voicemeeter-api"
|
||||
version = "2.4.4"
|
||||
version = "2.7.2"
|
||||
description = "A Python wrapper for the Voiceemeter API"
|
||||
optional = false
|
||||
python-versions = ">=3.10,<4.0"
|
||||
files = [
|
||||
{file = "voicemeeter_api-2.4.4-py3-none-any.whl", hash = "sha256:2e2f0b475de7cfc0d1c397838498162f0b405ab82a6f6ca0095434b43385b43e"},
|
||||
{file = "voicemeeter_api-2.4.4.tar.gz", hash = "sha256:a4f8ecaa7f5d6b9e9a8545dcf047754711af9a120628b3a98821466c7d65c1e7"},
|
||||
]
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main", "dev"]
|
||||
files = []
|
||||
develop = true
|
||||
|
||||
[package.dependencies]
|
||||
tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
|
||||
tomli = {version = ">=2.0.1,<3.0", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.source]
|
||||
type = "directory"
|
||||
url = "../voicemeeter-api-python"
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "cde74de8c9de0895555068efea354edf6b51f2332c253f02f6c8f26fe6b4e795"
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.10,<3.14"
|
||||
content-hash = "f1e1782280c5e165fef043ca2695ea5f5c93fd00a66ace809266e0196fef6b71"
|
||||
|
||||
132
pyproject.toml
132
pyproject.toml
@ -1,26 +1,128 @@
|
||||
[tool.poetry]
|
||||
[project]
|
||||
name = "voicemeeter-compact"
|
||||
version = "1.9.2"
|
||||
version = "1.10.0"
|
||||
description = "A Compact Voicemeeter Remote App"
|
||||
authors = ["onyx-and-iris <code@onyxandiris.online>"]
|
||||
license = "MIT"
|
||||
authors = [{ name = "Onyx and Iris", email = "code@onyxandiris.online" }]
|
||||
license = { text = "MIT" }
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/onyx-and-iris/voicemeeter-compact"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"voicemeeter-api (>=2.7.2,<3.0.0)",
|
||||
"vban-cmd (>=2.10.2,<3.0.0)",
|
||||
"sv-ttk (>=2.6.0,<3.0.0)",
|
||||
"tomli (>=2.0.1,<3.0) ; python_version < '3.11'",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
voicemeeter-compact-basic = "vmcompact.gui.basic:run"
|
||||
voicemeeter-compact-banana = "vmcompact.gui.banana:run"
|
||||
voicemeeter-compact-potato = "vmcompact.gui.potato:run"
|
||||
|
||||
[tool.poetry]
|
||||
packages = [{ include = "vmcompact" }]
|
||||
include = ["vmcompact/img/cat.ico"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
sv-ttk = "^2.5.5"
|
||||
tomli = { version = "^2.0.1", python = "<3.11" }
|
||||
voicemeeter-api = "^2.4.4"
|
||||
vban-cmd = "^2.4.4"
|
||||
[tool.poetry.requires-plugins]
|
||||
poethepoet = ">=0.42.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = { version = "^22.6.0", allow-prereleases = true }
|
||||
isort = "^5.12.0"
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.9.1"
|
||||
voicemeeter-api = { path = "../voicemeeter-api-python/", develop = true }
|
||||
vban-cmd = { path = "../vban-cmd-python/", develop = true }
|
||||
|
||||
[tool.poetry.group.build.dependencies]
|
||||
pyinstaller = "^6.11.1"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poe.tasks]
|
||||
build-sunvalley = "task build-sunvalley"
|
||||
build-forest = "task build-forest"
|
||||
release = [
|
||||
{ ref = "build-sunvalley" },
|
||||
{ ref = "build-forest" },
|
||||
{ cmd = "task compress-sunvalley" },
|
||||
{ cmd = "task compress-forest" },
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
exclude = [
|
||||
".bzr",
|
||||
".direnv",
|
||||
".eggs",
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"venv",
|
||||
]
|
||||
|
||||
# Same as Black.
|
||||
line-length = 88
|
||||
indent-width = 4
|
||||
|
||||
# Assume Python 3.10
|
||||
target-version = "py310"
|
||||
|
||||
[tool.ruff.lint]
|
||||
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
|
||||
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
|
||||
# McCabe complexity (`C901`) by default.
|
||||
select = ["E4", "E7", "E9", "F"]
|
||||
ignore = []
|
||||
|
||||
# Allow fix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
|
||||
# Allow unused variables when underscore-prefixed.
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
|
||||
[tool.ruff.format]
|
||||
# Unlike Black, use single quotes for strings.
|
||||
quote-style = "single"
|
||||
|
||||
# Like Black, indent with spaces, rather than tabs.
|
||||
indent-style = "space"
|
||||
|
||||
# Like Black, respect magic trailing commas.
|
||||
skip-magic-trailing-comma = false
|
||||
|
||||
# Like Black, automatically detect the appropriate line ending.
|
||||
line-ending = "auto"
|
||||
|
||||
# Enable auto-formatting of code examples in docstrings. Markdown,
|
||||
# reStructuredText code/literal blocks and doctests are all supported.
|
||||
#
|
||||
# This is currently disabled by default, but it is planned for this
|
||||
# to be opt-out in the future.
|
||||
docstring-code-format = false
|
||||
|
||||
# Set the line length limit used when formatting code snippets in
|
||||
# docstrings.
|
||||
#
|
||||
# This only has an effect when the `docstring-code-format` setting is
|
||||
# enabled.
|
||||
docstring-code-line-length = "dynamic"
|
||||
|
||||
[tool.ruff.lint.mccabe]
|
||||
max-complexity = 10
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"__init__.py" = ["E402", "F401"]
|
||||
|
||||
0
spec/azure/.gitkeep
Normal file
0
spec/azure/.gitkeep
Normal file
0
spec/forest/.gitkeep
Normal file
0
spec/forest/.gitkeep
Normal file
0
spec/sunvalley/.gitkeep
Normal file
0
spec/sunvalley/.gitkeep
Normal file
0
theme/azure/.gitkeep
Normal file
0
theme/azure/.gitkeep
Normal file
0
theme/forest/.gitkeep
Normal file
0
theme/forest/.gitkeep
Normal file
322
tools/dynamic_builder.py
Normal file
322
tools/dynamic_builder.py
Normal file
@ -0,0 +1,322 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Dynamic build system for voicemeeter-compact.
|
||||
Generates spec files on-the-fly and builds executables without storing intermediate files.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
# Build configuration
|
||||
THEMES = {
|
||||
'azure': ['azure-light', 'azure-dark'],
|
||||
'forest': ['forest-light', 'forest-dark'],
|
||||
'sunvalley': ['sunvalley'],
|
||||
}
|
||||
|
||||
KINDS = ['basic', 'banana', 'potato']
|
||||
|
||||
# Templates (same as spec_generator.py)
|
||||
PYTHON_TEMPLATE = """import voicemeeterlib
|
||||
|
||||
import vmcompact
|
||||
|
||||
|
||||
def main():
|
||||
KIND_ID = '{kind}'
|
||||
|
||||
with voicemeeterlib.api(KIND_ID) as vmr:{theme_arg}
|
||||
app = vmcompact.connect(KIND_ID, vmr{theme_param})
|
||||
app.mainloop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
"""
|
||||
|
||||
SPEC_TEMPLATE = """# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
block_cipher = None
|
||||
|
||||
added_files = [
|
||||
( '{img_path}', 'img' ),{theme_files}
|
||||
( '{config_path}', 'configs' ),
|
||||
]
|
||||
|
||||
a = Analysis(
|
||||
['{script_path}'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=added_files,
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={{}},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='{theme_variant}-{kind}',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='{theme_variant}-{kind}',
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
class DynamicBuilder:
|
||||
def __init__(self, base_dir: Path, dist_dir: Path):
|
||||
self.base_dir = base_dir
|
||||
self.dist_dir = dist_dir
|
||||
self.temp_dir = None
|
||||
|
||||
def __enter__(self):
|
||||
self.temp_dir = Path(tempfile.mkdtemp(prefix='vmcompact_build_'))
|
||||
print(f'Using temp directory: {self.temp_dir}')
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if self.temp_dir and self.temp_dir.exists():
|
||||
shutil.rmtree(self.temp_dir)
|
||||
print(f'Cleaned up temp directory: {self.temp_dir}')
|
||||
|
||||
def create_python_file(self, theme_variant: str, kind: str) -> Path:
|
||||
"""Create a temporary Python launcher file."""
|
||||
if theme_variant == 'sunvalley':
|
||||
theme_arg = ''
|
||||
theme_param = ''
|
||||
else:
|
||||
theme_arg = f"\n theme = '{theme_variant}'"
|
||||
theme_param = ', theme=theme'
|
||||
|
||||
content = PYTHON_TEMPLATE.format(
|
||||
kind=kind, theme_arg=theme_arg, theme_param=theme_param
|
||||
)
|
||||
|
||||
py_file = self.temp_dir / f'{theme_variant}-{kind}.py'
|
||||
with open(py_file, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
return py_file
|
||||
|
||||
def create_spec_file(self, theme_variant: str, kind: str, py_file: Path) -> Path:
|
||||
"""Create a temporary PyInstaller spec file."""
|
||||
if theme_variant == 'sunvalley':
|
||||
theme_files = ''
|
||||
else:
|
||||
theme_base = theme_variant.split('-')[0]
|
||||
theme_path = (self.base_dir / 'theme' / theme_base).as_posix()
|
||||
theme_files = f"\n ( '{theme_path}', 'theme' ),"
|
||||
|
||||
content = SPEC_TEMPLATE.format(
|
||||
script_path=py_file.as_posix(),
|
||||
img_path=(self.base_dir / 'vmcompact' / 'img').as_posix(),
|
||||
config_path=(self.base_dir / 'configs').as_posix(),
|
||||
theme_files=theme_files,
|
||||
kind=kind,
|
||||
theme_variant=theme_variant,
|
||||
)
|
||||
|
||||
spec_file = self.temp_dir / f'{theme_variant}-{kind}.spec'
|
||||
with open(spec_file, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
return spec_file
|
||||
|
||||
def build_variant(self, theme_variant: str, kind: str) -> bool:
|
||||
"""Build a single theme/kind variant."""
|
||||
print(f'Building {theme_variant}-{kind}...')
|
||||
|
||||
# Create temporary files
|
||||
py_file = self.create_python_file(theme_variant, kind)
|
||||
spec_file = self.create_spec_file(theme_variant, kind, py_file)
|
||||
|
||||
# Build with PyInstaller
|
||||
dist_path = self.dist_dir / f'{theme_variant}-{kind}'
|
||||
poetry_bin = os.getenv('POETRY_BIN', 'poetry')
|
||||
cmd = [
|
||||
poetry_bin,
|
||||
'run',
|
||||
'pyinstaller',
|
||||
'--noconfirm',
|
||||
'--distpath',
|
||||
str(dist_path.parent),
|
||||
str(spec_file),
|
||||
]
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd, cwd=self.base_dir, capture_output=True, text=True
|
||||
)
|
||||
if result.returncode == 0:
|
||||
print(f'[OK] Built {theme_variant}-{kind}')
|
||||
return True
|
||||
else:
|
||||
print(f'[FAIL] Failed to build {theme_variant}-{kind}')
|
||||
print(f'Error: {result.stderr}')
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f'[ERROR] Exception building {theme_variant}-{kind}: {e}')
|
||||
return False
|
||||
|
||||
def build_theme(self, theme_family: str) -> Dict[str, bool]:
|
||||
"""Build all variants for a theme family."""
|
||||
results = {}
|
||||
|
||||
if theme_family not in THEMES:
|
||||
print(f'Unknown theme: {theme_family}')
|
||||
return results
|
||||
|
||||
variants = THEMES[theme_family]
|
||||
|
||||
for variant in variants:
|
||||
for kind in KINDS:
|
||||
success = self.build_variant(variant, kind)
|
||||
results[f'{variant}-{kind}'] = success
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def run_rewriter(theme_family: str, base_dir: Path) -> bool:
|
||||
"""Run the theme rewriter if needed."""
|
||||
if theme_family in ['azure', 'forest']:
|
||||
print(f'Running rewriter for {theme_family} theme...')
|
||||
poetry_bin = os.getenv('POETRY_BIN', 'poetry')
|
||||
cmd = [
|
||||
poetry_bin,
|
||||
'run',
|
||||
'python',
|
||||
'tools/rewriter.py',
|
||||
'--rewrite',
|
||||
'--theme',
|
||||
theme_family,
|
||||
]
|
||||
try:
|
||||
result = subprocess.run(cmd, cwd=base_dir)
|
||||
return result.returncode == 0
|
||||
except Exception as e:
|
||||
print(f'Rewriter failed: {e}')
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def restore_rewriter(base_dir: Path) -> bool:
|
||||
"""Restore files after rewriter."""
|
||||
print('Restoring rewriter changes...')
|
||||
poetry_bin = os.getenv('POETRY_BIN', 'poetry')
|
||||
cmd = [poetry_bin, 'run', 'python', 'tools/rewriter.py', '--restore']
|
||||
try:
|
||||
result = subprocess.run(cmd, cwd=base_dir)
|
||||
return result.returncode == 0
|
||||
except Exception as e:
|
||||
print(f'Restore failed: {e}')
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Dynamic build system for voicemeeter-compact'
|
||||
)
|
||||
parser.add_argument(
|
||||
'themes',
|
||||
nargs='*',
|
||||
choices=list(THEMES.keys()) + ['all'],
|
||||
help='Themes to build (default: all)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dist-dir',
|
||||
type=Path,
|
||||
default=Path('dist'),
|
||||
help='Distribution output directory',
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.themes or 'all' in args.themes:
|
||||
themes_to_build = list(THEMES.keys())
|
||||
else:
|
||||
themes_to_build = args.themes
|
||||
|
||||
base_dir = Path.cwd()
|
||||
args.dist_dir.mkdir(exist_ok=True)
|
||||
|
||||
print(f'Building themes: {", ".join(themes_to_build)}')
|
||||
|
||||
all_results = {}
|
||||
|
||||
with DynamicBuilder(base_dir, args.dist_dir) as builder:
|
||||
for theme_family in themes_to_build:
|
||||
# Run rewriter if needed
|
||||
if not run_rewriter(theme_family, base_dir):
|
||||
print(f'Skipping {theme_family} due to rewriter failure')
|
||||
continue
|
||||
|
||||
try:
|
||||
# Build theme
|
||||
results = builder.build_theme(theme_family)
|
||||
all_results.update(results)
|
||||
|
||||
finally:
|
||||
# Always restore rewriter changes
|
||||
if theme_family in ['azure', 'forest']:
|
||||
restore_rewriter(base_dir)
|
||||
|
||||
# Report results
|
||||
print('\n' + '=' * 50)
|
||||
print('BUILD SUMMARY')
|
||||
print('=' * 50)
|
||||
|
||||
success_count = 0
|
||||
total_count = 0
|
||||
|
||||
for build_name, success in all_results.items():
|
||||
status = '[OK]' if success else '[FAIL]'
|
||||
print(f'{status} {build_name}')
|
||||
if success:
|
||||
success_count += 1
|
||||
total_count += 1
|
||||
|
||||
print(f'\nSuccess: {success_count}/{total_count}')
|
||||
|
||||
if success_count == total_count:
|
||||
print('All builds completed successfully!')
|
||||
sys.exit(0)
|
||||
else:
|
||||
print('Some builds failed!')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
285
tools/rewriter.py
Normal file
285
tools/rewriter.py
Normal file
@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rewrites app.py, builders.py, menu.py, and navigation.py to remove sv_ttk dependencies and apply theme changes for the build process.
|
||||
Also provides a cleanup function to restore the original files after building.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
logger = logging.getLogger('vm-compact-rewriter')
|
||||
|
||||
PACKAGE_DIR = Path(__file__).parent.parent / 'vmcompact'
|
||||
|
||||
SRC_DIR = Path(__file__).parent / 'src'
|
||||
|
||||
|
||||
def write_outs(output, outs: tuple):
|
||||
for out in outs:
|
||||
output.write(out)
|
||||
|
||||
|
||||
def rewrite_app(theme):
|
||||
app_logger = logger.getChild('app')
|
||||
app_logger.info('rewriting app.py')
|
||||
infile = Path(SRC_DIR) / 'app.bk'
|
||||
outfile = Path(PACKAGE_DIR) / 'app.py'
|
||||
with open(infile, 'r') as input:
|
||||
with open(outfile, 'w') as output:
|
||||
for line in input:
|
||||
match line:
|
||||
case ' self._vmr = vmr\n':
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
' self._vmr = vmr\n',
|
||||
' self._theme = theme\n',
|
||||
' self._theme_name = theme.split("-")[0]\n',
|
||||
' self._theme_type = theme.split("-")[-1]\n',
|
||||
' tcldir = Path.cwd() / "theme" / self._theme_name\n',
|
||||
' if not tcldir.is_dir():\n',
|
||||
' tcldir = Path.cwd() / "_internal" / "theme"\n',
|
||||
' match self._theme_name:\n',
|
||||
' case "forest":\n',
|
||||
' self.tk.call("source", tcldir.resolve() / f"{self._theme}.tcl")\n',
|
||||
' case "azure":\n',
|
||||
' self.tk.call("source", tcldir.resolve() / f"{self._theme_name}.tcl")\n',
|
||||
),
|
||||
)
|
||||
case _:
|
||||
output.write(line)
|
||||
|
||||
|
||||
def rewrite_builders(theme):
|
||||
builders_logger = logger.getChild('builders')
|
||||
builders_logger.info('rewriting builders.py')
|
||||
infile = Path(SRC_DIR) / 'builders.bk'
|
||||
outfile = Path(PACKAGE_DIR) / 'builders.py'
|
||||
with open(infile, 'r') as input:
|
||||
with open(outfile, 'w') as output:
|
||||
ignore_next_lines = 0
|
||||
|
||||
for line in input:
|
||||
if ignore_next_lines > 0:
|
||||
builders_logger.info(f'ignoring: {line}')
|
||||
ignore_next_lines -= 1
|
||||
continue
|
||||
|
||||
match line:
|
||||
# loading themes
|
||||
case 'import sv_ttk\n':
|
||||
output.write('#import sv_ttk\n')
|
||||
case ' self.app.resizable(False, False)\n':
|
||||
if theme.startswith('forest'):
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
' self.app.resizable(False, False)\n'
|
||||
' if _configuration.themes_enabled:\n',
|
||||
' ttk.Style().theme_use(self.app._theme)\n',
|
||||
' self.logger.info(f"{self.app._theme} Theme applied")\n',
|
||||
),
|
||||
)
|
||||
elif theme.startswith('azure'):
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
' self.app.resizable(False, False)\n'
|
||||
' if _configuration.themes_enabled:\n',
|
||||
' self.app.tk.call("set_theme", self.app._theme_type)\n',
|
||||
' self.logger.info(f"Azure {self.app._theme_type} Theme applied")\n',
|
||||
),
|
||||
)
|
||||
ignore_next_lines = 6
|
||||
# setting navframe button widths
|
||||
case ' variable=self.navframe.submix,\n':
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
' variable=self.navframe.submix,\n'
|
||||
' width=8,\n',
|
||||
),
|
||||
)
|
||||
case ' variable=self.navframe.channel,\n':
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
' variable=self.navframe.channel,\n'
|
||||
' width=8,\n',
|
||||
),
|
||||
)
|
||||
case ' variable=self.navframe.extend,\n':
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
' variable=self.navframe.extend,\n'
|
||||
' width=8,\n',
|
||||
),
|
||||
)
|
||||
case ' variable=self.navframe.info,\n':
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
' variable=self.navframe.info,\n'
|
||||
' width=8,\n',
|
||||
),
|
||||
)
|
||||
# set channelframe button widths
|
||||
case ' variable=self.labelframe.mute,\n':
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
' variable=self.labelframe.mute,\n'
|
||||
' width=7,\n',
|
||||
),
|
||||
)
|
||||
case ' variable=self.labelframe.conf,\n':
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
' variable=self.labelframe.conf,\n'
|
||||
' width=7,\n',
|
||||
),
|
||||
)
|
||||
case ' variable=self.labelframe.on,\n':
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
' variable=self.labelframe.on,\n'
|
||||
' width=7,\n',
|
||||
),
|
||||
)
|
||||
# set stripconfigframe button widths
|
||||
case ' self.configframe.phys_out_params.index(param)\n':
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
' self.configframe.phys_out_params.index(param)\n',
|
||||
' ],\n',
|
||||
' width=6,\n',
|
||||
),
|
||||
)
|
||||
ignore_next_lines = 1
|
||||
case ' self.configframe.virt_out_params.index(param)\n':
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
' self.configframe.virt_out_params.index(param)\n',
|
||||
' ],\n',
|
||||
' width=6,\n',
|
||||
),
|
||||
)
|
||||
ignore_next_lines = 1
|
||||
# This does both strip and bus param vars buttons
|
||||
case ' variable=self.configframe.param_vars[i],\n':
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
' variable=self.configframe.param_vars[i],\n',
|
||||
' width=6,\n',
|
||||
),
|
||||
)
|
||||
case _:
|
||||
if 'Toggle.TButton' in line:
|
||||
if theme.startswith('forest'):
|
||||
output.write(
|
||||
line.replace('Toggle.TButton', 'ToggleButton')
|
||||
)
|
||||
elif theme.startswith('azure'):
|
||||
output.write(
|
||||
line.replace(
|
||||
'Toggle.TButton', 'Switch.TCheckbutton'
|
||||
)
|
||||
)
|
||||
else:
|
||||
output.write(line)
|
||||
|
||||
|
||||
def rewrite_menu(theme):
|
||||
menu_logger = logger.getChild('menu')
|
||||
menu_logger.info('rewriting menu.py')
|
||||
infile = Path(SRC_DIR) / 'menu.bk'
|
||||
outfile = Path(PACKAGE_DIR) / 'menu.py'
|
||||
with open(infile, 'r') as input:
|
||||
with open(outfile, 'w') as output:
|
||||
ignore_next_lines = 0
|
||||
|
||||
for line in input:
|
||||
if ignore_next_lines > 0:
|
||||
menu_logger.info(f'ignoring: {line}')
|
||||
ignore_next_lines -= 1
|
||||
continue
|
||||
match line:
|
||||
case 'import sv_ttk\n':
|
||||
output.write('#import sv_ttk\n')
|
||||
case ' # layout/themes\n':
|
||||
ignore_next_lines = 14
|
||||
case _:
|
||||
output.write(line)
|
||||
|
||||
|
||||
def rewrite_navigation(theme):
|
||||
navigation_logger = logger.getChild('navigation')
|
||||
navigation_logger.info('rewriting navigation.py')
|
||||
infile = Path(SRC_DIR) / 'navigation.bk'
|
||||
outfile = Path(PACKAGE_DIR) / 'navigation.py'
|
||||
with open(infile, 'r') as input:
|
||||
with open(outfile, 'w') as output:
|
||||
for line in input:
|
||||
match line:
|
||||
case ' self.builder.create_info_button()\n':
|
||||
if theme.startswith('azure'):
|
||||
output.write(
|
||||
' # self.builder.create_info_button()\n'
|
||||
)
|
||||
else:
|
||||
output.write(line)
|
||||
case _:
|
||||
output.write(line)
|
||||
|
||||
|
||||
def prepare_for_build(theme):
|
||||
################# MOVE FILES FROM PACKAGE DIR INTO SRC DIR #########################
|
||||
for file in (
|
||||
PACKAGE_DIR / 'app.py',
|
||||
PACKAGE_DIR / 'builders.py',
|
||||
PACKAGE_DIR / 'menu.py',
|
||||
PACKAGE_DIR / 'navigation.py',
|
||||
):
|
||||
if file.exists():
|
||||
logger.debug(f'moving {str(file)}')
|
||||
file.rename(SRC_DIR / f'{file.stem}.bk')
|
||||
|
||||
###################### RUN THE FILE REWRITER FOR EACH *.BK #########################
|
||||
for step in (rewrite_app, rewrite_builders, rewrite_menu, rewrite_navigation):
|
||||
step(theme)
|
||||
|
||||
|
||||
def cleanup():
|
||||
########################## RESTORE *.BK FILES #####################################
|
||||
for file in (
|
||||
SRC_DIR / 'app.bk',
|
||||
SRC_DIR / 'builders.bk',
|
||||
SRC_DIR / 'menu.bk',
|
||||
SRC_DIR / 'navigation.bk',
|
||||
):
|
||||
if file.exists():
|
||||
logger.debug(f'moving {str(file)}')
|
||||
file.replace(PACKAGE_DIR / f'{file.stem}.py')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--rewrite', action='store_true')
|
||||
parser.add_argument('--theme', type=str, default='forest')
|
||||
parser.add_argument('--restore', action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.rewrite:
|
||||
logger.info('preparing files for build')
|
||||
prepare_for_build(args.theme)
|
||||
elif args.restore:
|
||||
logger.info('cleaning up files')
|
||||
cleanup()
|
||||
192
tools/spec_generator.py
Normal file
192
tools/spec_generator.py
Normal file
@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Spec file generator for voicemeeter-compact builds.
|
||||
Generates Python launcher files and PyInstaller spec files from templates.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
# Build configuration
|
||||
THEMES = {
|
||||
'azure': ['azure-light', 'azure-dark'],
|
||||
'forest': ['forest-light', 'forest-dark'],
|
||||
'sunvalley': ['sunvalley'], # Single variant, no light/dark
|
||||
}
|
||||
|
||||
KINDS = ['basic', 'banana', 'potato']
|
||||
|
||||
# Templates
|
||||
PYTHON_TEMPLATE = """import voicemeeterlib
|
||||
|
||||
import vmcompact
|
||||
|
||||
|
||||
def main():
|
||||
KIND_ID = '{kind}'
|
||||
|
||||
with voicemeeterlib.api(KIND_ID) as vmr:{theme_arg}
|
||||
app = vmcompact.connect(KIND_ID, vmr{theme_param})
|
||||
app.mainloop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
"""
|
||||
|
||||
SPEC_TEMPLATE = """# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
block_cipher = None
|
||||
|
||||
added_files = [
|
||||
( '../../vmcompact/img', 'img' ),{theme_files}
|
||||
( '../../configs', 'configs' ),
|
||||
]
|
||||
|
||||
a = Analysis(
|
||||
['{script_name}'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=added_files,
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={{}},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='{kind}',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='{kind}',
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
def generate_python_file(theme_variant: str, kind: str, output_dir: Path) -> None:
|
||||
"""Generate a Python launcher file."""
|
||||
if theme_variant == 'sunvalley':
|
||||
# Sunvalley doesn't use theme parameter
|
||||
theme_arg = ''
|
||||
theme_param = ''
|
||||
else:
|
||||
theme_arg = f"\n theme = '{theme_variant}'"
|
||||
theme_param = ', theme=theme'
|
||||
|
||||
content = PYTHON_TEMPLATE.format(
|
||||
kind=kind, theme_arg=theme_arg, theme_param=theme_param
|
||||
)
|
||||
|
||||
filename = f'{theme_variant}-{kind}.py'
|
||||
output_path = output_dir / filename
|
||||
|
||||
with open(output_path, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
print(f'Generated: {output_path}')
|
||||
|
||||
|
||||
def generate_spec_file(theme_variant: str, kind: str, output_dir: Path) -> None:
|
||||
"""Generate a PyInstaller spec file."""
|
||||
script_name = f'{theme_variant}-{kind}.py'
|
||||
|
||||
if theme_variant == 'sunvalley':
|
||||
# Sunvalley doesn't include theme files
|
||||
theme_files = ''
|
||||
else:
|
||||
theme_base = theme_variant.split('-')[0] # 'azure' from 'azure-dark'
|
||||
theme_files = f"\n ( '../../theme/{theme_base}', 'theme' ),"
|
||||
|
||||
content = SPEC_TEMPLATE.format(
|
||||
script_name=script_name, theme_files=theme_files, kind=kind
|
||||
)
|
||||
|
||||
filename = f'{theme_variant}-{kind}.spec'
|
||||
output_path = output_dir / filename
|
||||
|
||||
with open(output_path, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
print(f'Generated: {output_path}')
|
||||
|
||||
|
||||
def generate_all_files(output_base_dir: Path) -> None:
|
||||
"""Generate all Python and spec files for all theme/kind combinations."""
|
||||
for theme_family, theme_variants in THEMES.items():
|
||||
theme_dir = output_base_dir / theme_family
|
||||
theme_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for theme_variant in theme_variants:
|
||||
for kind in KINDS:
|
||||
generate_python_file(theme_variant, kind, theme_dir)
|
||||
generate_spec_file(theme_variant, kind, theme_dir)
|
||||
|
||||
|
||||
def clean_existing_files(output_base_dir: Path) -> None:
|
||||
"""Remove all existing generated files."""
|
||||
for theme_family in THEMES.keys():
|
||||
theme_dir = output_base_dir / theme_family
|
||||
if theme_dir.exists():
|
||||
for file in theme_dir.glob('*.py'):
|
||||
file.unlink()
|
||||
print(f'Removed: {file}')
|
||||
for file in theme_dir.glob('*.spec'):
|
||||
file.unlink()
|
||||
print(f'Removed: {file}')
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Generate spec files for voicemeeter-compact'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--clean', action='store_true', help='Clean existing files before generating'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--output-dir',
|
||||
type=Path,
|
||||
default=Path('spec'),
|
||||
help='Output directory for spec files (default: spec)',
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.clean:
|
||||
print('Cleaning existing files...')
|
||||
clean_existing_files(args.output_dir)
|
||||
|
||||
print('Generating spec files...')
|
||||
generate_all_files(args.output_dir)
|
||||
print('Done!')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
0
tools/src/.gitkeep
Normal file
0
tools/src/.gitkeep
Normal file
@ -1,3 +1,3 @@
|
||||
from .app import connect
|
||||
|
||||
__ALL__ = ["connect"]
|
||||
__ALL__ = ['connect']
|
||||
|
||||
@ -6,10 +6,11 @@ from tkinter import messagebox, ttk
|
||||
from typing import NamedTuple
|
||||
|
||||
import voicemeeterlib
|
||||
from voicemeeterlib import kinds
|
||||
|
||||
from .builders import MainFrameBuilder
|
||||
from .configurations import loader
|
||||
from .data import _base_values, _configuration, _kinds_all, get_configuration
|
||||
from .data import _base_values, _configuration, get_configuration
|
||||
from .errors import VMCompactError
|
||||
from .menu import Menus
|
||||
from .subject import Subject
|
||||
@ -29,42 +30,46 @@ class App(tk.Tk):
|
||||
"""
|
||||
|
||||
APP_cls = type(
|
||||
f"Voicemeeter{kind}.Compact",
|
||||
f'Voicemeeter{kind}.Compact',
|
||||
(cls,),
|
||||
{
|
||||
"kind": kind,
|
||||
'kind': kind,
|
||||
},
|
||||
)
|
||||
return APP_cls
|
||||
|
||||
def __init__(self, vmr):
|
||||
def __init__(self, vmr, theme):
|
||||
super().__init__()
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
self._vmr = vmr
|
||||
self._vmr.event.add(["pdirty", "ldirty"])
|
||||
self.after(12000 if self._vmr.gui.launched_by_api else 1, self.start_updates)
|
||||
self._vmr.init_thread()
|
||||
icon_path = Path(__file__).parent.resolve() / "img" / "cat.ico"
|
||||
if icon_path.is_file():
|
||||
self.iconbitmap(str(icon_path))
|
||||
self.minsize(275, False)
|
||||
self._vmr.event.add(['pdirty', 'ldirty'])
|
||||
self.subject = Subject()
|
||||
self.start_updates()
|
||||
self._vmr.init_thread()
|
||||
for pn in (
|
||||
Path(__file__).parent.resolve() / 'img' / 'cat.ico',
|
||||
Path.cwd() / '_internal' / 'img' / 'cat.ico',
|
||||
):
|
||||
if pn.is_file():
|
||||
self.iconbitmap(str(pn))
|
||||
break
|
||||
self.minsize(275, False)
|
||||
self._configs = None
|
||||
self.protocol("WM_DELETE_WINDOW", self.on_close_window)
|
||||
self.menu = self["menu"] = Menus(self, vmr)
|
||||
self.protocol('WM_DELETE_WINDOW', self.on_close_window)
|
||||
self.menu = self['menu'] = Menus(self, vmr)
|
||||
self.styletable = ttk.Style()
|
||||
if _configuration.config:
|
||||
vmr.apply_config(_configuration.config)
|
||||
|
||||
self.build_app()
|
||||
|
||||
self.drag_id = ""
|
||||
self.bind("<Configure>", self.dragging)
|
||||
self.drag_id = ''
|
||||
self.bind('<Configure>', self.dragging)
|
||||
|
||||
self.after(1, self.healthcheck_step)
|
||||
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}App"
|
||||
return f'{type(self).__name__}App'
|
||||
|
||||
@property
|
||||
def target(self):
|
||||
@ -80,8 +85,8 @@ class App(tk.Tk):
|
||||
frame
|
||||
for frame in self.winfo_children()
|
||||
if isinstance(frame, ttk.Frame)
|
||||
and "!stripconfig" in str(frame)
|
||||
or "!busconfig" in str(frame)
|
||||
and '!stripconfig' in str(frame)
|
||||
or '!busconfig' in str(frame)
|
||||
)
|
||||
|
||||
def build_app(self, kind=None, vban=None):
|
||||
@ -97,22 +102,22 @@ class App(tk.Tk):
|
||||
self.submix_frame = None
|
||||
self.builder = MainFrameBuilder(self)
|
||||
self.builder.setup()
|
||||
self.builder.create_channelframe("strip")
|
||||
self.builder.create_channelframe('strip')
|
||||
self.builder.create_separator()
|
||||
self.builder.create_navframe()
|
||||
if _configuration.extended:
|
||||
self.nav_frame.extend.set(True)
|
||||
self.nav_frame.extend_frame()
|
||||
if self.kind.name == "potato":
|
||||
if self.kind.name == 'potato':
|
||||
self.builder.create_banner()
|
||||
|
||||
def on_pdirty(self):
|
||||
if _base_values.run_update:
|
||||
self.after(1, self.subject.notify, "pdirty")
|
||||
self.after(1, self.subject.notify, 'pdirty')
|
||||
|
||||
def on_ldirty(self):
|
||||
if not _base_values.dragging:
|
||||
self.after(1, self.subject.notify, "ldirty")
|
||||
self.after(1, self.subject.notify, 'ldirty')
|
||||
|
||||
def _destroy_top_level_frames(self):
|
||||
"""
|
||||
@ -132,14 +137,14 @@ class App(tk.Tk):
|
||||
|
||||
def dragging(self, event, *args):
|
||||
if event.widget is self:
|
||||
if self.drag_id == "":
|
||||
if self.drag_id == '':
|
||||
_base_values.dragging = True
|
||||
else:
|
||||
self.after_cancel(self.drag_id)
|
||||
self.drag_id = self.after(100, self.stop_drag)
|
||||
|
||||
def stop_drag(self):
|
||||
self.drag_id = ""
|
||||
self.drag_id = ''
|
||||
_base_values.dragging = False
|
||||
|
||||
@cached_property
|
||||
@ -148,20 +153,25 @@ class App(tk.Tk):
|
||||
return self._configs
|
||||
|
||||
def start_updates(self):
|
||||
self.logger.debug("updates started")
|
||||
_base_values.run_update = True
|
||||
def init():
|
||||
self.logger.debug('updates started')
|
||||
_base_values.run_update = True
|
||||
|
||||
if self._vmr.gui.launched_by_api:
|
||||
self.on_pdirty()
|
||||
self.subject.notify('pdirty')
|
||||
self.after(12000, init)
|
||||
else:
|
||||
init()
|
||||
|
||||
def healthcheck_step(self):
|
||||
if not _base_values.vban_connected:
|
||||
try:
|
||||
self._vmr.version
|
||||
except voicemeeterlib.error.CAPIError:
|
||||
resp = messagebox.askyesno(message="Restart Voicemeeter GUI?")
|
||||
resp = messagebox.askyesno(message='Restart Voicemeeter GUI?')
|
||||
if resp:
|
||||
self.logger.debug(
|
||||
"healthcheck failed, rebuilding the app after GUI restart."
|
||||
'healthcheck failed, rebuilding the app after GUI restart.'
|
||||
)
|
||||
self._vmr.end_thread()
|
||||
self._vmr.run_voicemeeter(self._vmr.kind.name)
|
||||
@ -170,13 +180,13 @@ class App(tk.Tk):
|
||||
self.after(8000, self.start_updates)
|
||||
self._destroy_top_level_frames()
|
||||
self.build_app(self._vmr.kind)
|
||||
vban_config = get_configuration("vban")
|
||||
vban_config = get_configuration('vban')
|
||||
for i, _ in enumerate(vban_config):
|
||||
target = getattr(self.menu, f"menu_vban_{i+1}")
|
||||
target.entryconfig(0, state="normal")
|
||||
target.entryconfig(1, state="disabled")
|
||||
target = getattr(self.menu, f'menu_vban_{i + 1}')
|
||||
target.entryconfig(0, state='normal')
|
||||
target.entryconfig(1, state='disabled')
|
||||
[
|
||||
self.menu.menu_vban.entryconfig(j, state="normal")
|
||||
self.menu.menu_vban.entryconfig(j, state='normal')
|
||||
for j, _ in enumerate(self.menu.menu_vban.winfo_children())
|
||||
]
|
||||
else:
|
||||
@ -189,14 +199,14 @@ class App(tk.Tk):
|
||||
self.destroy()
|
||||
|
||||
|
||||
_apps = {kind.name: App.make(kind) for kind in _kinds_all}
|
||||
_apps = {kind.name: App.make(kind) for kind in kinds.all}
|
||||
|
||||
|
||||
def connect(kind_id: str, vmr) -> App:
|
||||
def connect(kind_id: str, vmr, theme=None) -> App:
|
||||
"""return App of the kind requested"""
|
||||
|
||||
try:
|
||||
VMMIN_cls = _apps[kind_id]
|
||||
except KeyError:
|
||||
raise VMCompactError(f"Invalid kind: {kind_id}")
|
||||
return VMMIN_cls(vmr)
|
||||
raise VMCompactError(f'Invalid kind: {kind_id}')
|
||||
return VMMIN_cls(vmr, theme)
|
||||
|
||||
@ -17,7 +17,7 @@ class Banner(ttk.Frame):
|
||||
|
||||
self.label = ttk.Label(
|
||||
self,
|
||||
text=f"SUBMIX: {self.submix.get().upper()}",
|
||||
text=f'SUBMIX: {self.submix.get().upper()}',
|
||||
)
|
||||
self.label.grid(column=0, row=0, sticky=(tk.N, tk.S, tk.W, tk.E))
|
||||
|
||||
@ -28,8 +28,8 @@ class Banner(ttk.Frame):
|
||||
return self.parent.target
|
||||
|
||||
def on_update(self, subject):
|
||||
if subject == "submix":
|
||||
if subject == 'submix':
|
||||
if not _base_values.dragging:
|
||||
self.logger.debug("checking submix for banner")
|
||||
self.logger.debug('checking submix for banner')
|
||||
self.submix.set(self.target.bus[_configuration.submixes].label)
|
||||
self.label["text"] = f"SUBMIX: {self.submix.get().upper()}"
|
||||
self.label['text'] = f'SUBMIX: {self.submix.get().upper()}'
|
||||
|
||||
@ -6,6 +6,7 @@ from tkinter import ttk
|
||||
|
||||
import sv_ttk
|
||||
|
||||
from . import util
|
||||
from .banner import Banner
|
||||
from .channels import _make_channelframe
|
||||
from .config import BusConfig, StripConfig
|
||||
@ -41,31 +42,31 @@ class MainFrameBuilder(AbstractBuilder):
|
||||
)
|
||||
self.app.resizable(False, False)
|
||||
if _configuration.themes_enabled:
|
||||
if sv_ttk.get_theme() not in ("light", "dark"):
|
||||
if sv_ttk.get_theme() not in ('light', 'dark'):
|
||||
sv_ttk.set_theme(_configuration.theme_mode)
|
||||
self.logger.info(
|
||||
f"Sunvalley {sv_ttk.get_theme().capitalize()} Theme applied"
|
||||
f'Sunvalley {sv_ttk.get_theme().capitalize()} Theme applied'
|
||||
)
|
||||
|
||||
def create_channelframe(self, type_):
|
||||
if type_ == "strip":
|
||||
if type_ == 'strip':
|
||||
self.app.strip_frame = _make_channelframe(self.app, type_)
|
||||
else:
|
||||
self.app.bus_frame = _make_channelframe(self.app, type_)
|
||||
self.logger.info(f"Finished building channelframe type {type_}")
|
||||
self.logger.info(f'Finished building channelframe type {type_}')
|
||||
|
||||
def create_separator(self):
|
||||
self.app.sep = ttk.Separator(self.app, orient="vertical")
|
||||
self.app.sep = ttk.Separator(self.app, orient='vertical')
|
||||
self.app.sep.grid(row=0, column=1, sticky=(tk.N, tk.S))
|
||||
self.app.columnconfigure(1, minsize=15)
|
||||
self.logger.info(f"Finished building separator")
|
||||
self.logger.info('Finished building separator')
|
||||
|
||||
def create_navframe(self):
|
||||
self.app.nav_frame = Navigation(self.app)
|
||||
self.logger.info(f"Finished building navframe")
|
||||
self.logger.info('Finished building navframe')
|
||||
|
||||
def create_configframe(self, type_, index, id):
|
||||
if type_ == "strip":
|
||||
if type_ == 'strip':
|
||||
self.app.config_frame = StripConfig(self.app, index, id)
|
||||
if self.app.strip_frame:
|
||||
[
|
||||
@ -95,20 +96,20 @@ class MainFrameBuilder(AbstractBuilder):
|
||||
if self.app.strip_frame:
|
||||
[
|
||||
frame.styletable.configure(
|
||||
f"{frame.identifier}Conf{frame.index}.TButton",
|
||||
background=f"{'white' if not frame.conf.get() else 'yellow'}",
|
||||
f'{frame.identifier}Conf{frame.index}.TButton',
|
||||
background=f'{"white" if not frame.conf.get() else "yellow"}',
|
||||
)
|
||||
for _, frame in enumerate(self.app.strip_frame.labelframes)
|
||||
]
|
||||
if self.app.bus_frame:
|
||||
[
|
||||
frame.styletable.configure(
|
||||
f"{frame.identifier}Conf{frame.index}.TButton",
|
||||
background=f"{'white' if not frame.conf.get() else 'yellow'}",
|
||||
f'{frame.identifier}Conf{frame.index}.TButton',
|
||||
background=f'{"white" if not frame.conf.get() else "yellow"}',
|
||||
)
|
||||
for _, frame in enumerate(self.app.bus_frame.labelframes)
|
||||
]
|
||||
self.logger.info(f"Finished building configframe for {type_}[{index}]")
|
||||
self.logger.info(f'Finished building configframe for {type_}[{index}]')
|
||||
self.app.after(5, self.reset_config_frames)
|
||||
|
||||
def reset_config_frames(self):
|
||||
@ -121,7 +122,7 @@ class MainFrameBuilder(AbstractBuilder):
|
||||
def create_banner(self):
|
||||
self.app.banner = Banner(self.app)
|
||||
self.app.banner.grid(row=4, column=0, columnspan=3)
|
||||
self.logger.info(f"Finished building banner")
|
||||
self.logger.info('Finished building banner')
|
||||
|
||||
def teardown(self):
|
||||
pass
|
||||
@ -140,31 +141,31 @@ class NavigationFrameBuilder(AbstractBuilder):
|
||||
self.navframe.info = tk.BooleanVar()
|
||||
|
||||
self.navframe.channel_text = tk.StringVar(
|
||||
value=f"{self.navframe.parent.strip_frame.identifier.upper()}"
|
||||
value=f'{self.navframe.parent.strip_frame.identifier.upper()}'
|
||||
)
|
||||
self.navframe.extend_text = tk.StringVar(
|
||||
value=f"{'REDUCE' if self.navframe.extend.get() else 'EXTEND'}"
|
||||
value=f'{"REDUCE" if self.navframe.extend.get() else "EXTEND"}'
|
||||
)
|
||||
self.navframe.info_text = tk.StringVar()
|
||||
|
||||
def create_submix_button(self):
|
||||
self.navframe.submix_button = ttk.Checkbutton(
|
||||
self.navframe,
|
||||
text="SUBMIX",
|
||||
text='SUBMIX',
|
||||
command=self.navframe.show_submix,
|
||||
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'Submix.TButton'}",
|
||||
style=f'{"Toggle.TButton" if _configuration.themes_enabled else "Submix.TButton"}',
|
||||
variable=self.navframe.submix,
|
||||
)
|
||||
self.navframe.submix_button.grid(column=0, row=0)
|
||||
if self.navframe.parent.kind.name != "potato":
|
||||
self.navframe.submix_button["state"] = "disabled"
|
||||
if self.navframe.parent.kind.name != 'potato':
|
||||
self.navframe.submix_button['state'] = 'disabled'
|
||||
|
||||
def create_channel_button(self):
|
||||
self.navframe.channel_button = ttk.Checkbutton(
|
||||
self.navframe,
|
||||
textvariable=self.navframe.channel_text,
|
||||
command=self.navframe.switch_channel,
|
||||
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'Channel.TButton'}",
|
||||
style=f'{"Toggle.TButton" if _configuration.themes_enabled else "Channel.TButton"}',
|
||||
variable=self.navframe.channel,
|
||||
)
|
||||
self.navframe.channel_button.grid(column=0, row=1, rowspan=1)
|
||||
@ -174,7 +175,7 @@ class NavigationFrameBuilder(AbstractBuilder):
|
||||
self.navframe,
|
||||
textvariable=self.navframe.extend_text,
|
||||
command=self.navframe.extend_frame,
|
||||
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'Extend.TButton'}",
|
||||
style=f'{"Toggle.TButton" if _configuration.themes_enabled else "Extend.TButton"}',
|
||||
variable=self.navframe.extend,
|
||||
)
|
||||
self.navframe.extend_button.grid(column=0, row=2)
|
||||
@ -183,7 +184,7 @@ class NavigationFrameBuilder(AbstractBuilder):
|
||||
self.navframe.info_button = ttk.Checkbutton(
|
||||
self.navframe,
|
||||
textvariable=self.navframe.info_text,
|
||||
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'Rec.TButton'}",
|
||||
style=f'{"Toggle.TButton" if _configuration.themes_enabled else "Rec.TButton"}',
|
||||
variable=self.navframe.info,
|
||||
)
|
||||
self.navframe.info_button.grid(column=0, row=3)
|
||||
@ -227,9 +228,9 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
|
||||
"""Adds a progress bar widget to a single label frame"""
|
||||
self.labelframe.pb = ttk.Progressbar(
|
||||
self.labelframe,
|
||||
maximum=72,
|
||||
orient="vertical",
|
||||
mode="determinate",
|
||||
maximum=72, # Range: 0 = -60dB, 72 = +12dB (72dB total range)
|
||||
orient='vertical',
|
||||
mode='determinate',
|
||||
variable=self.labelframe.level,
|
||||
)
|
||||
self.labelframe.pb.grid(column=0, row=0)
|
||||
@ -240,17 +241,17 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
|
||||
self.labelframe,
|
||||
from_=12.0,
|
||||
to=-60.0,
|
||||
orient="vertical",
|
||||
orient='vertical',
|
||||
variable=self.labelframe.gain,
|
||||
command=self.labelframe.scale_callback,
|
||||
length=_configuration.channel_height,
|
||||
)
|
||||
self.scale.grid(column=1, row=0)
|
||||
self.scale.bind("<Double-Button-1>", self.labelframe.reset_gain)
|
||||
self.scale.bind("<Button-1>", self.labelframe.scale_press)
|
||||
self.scale.bind("<ButtonRelease-1>", self.labelframe.scale_release)
|
||||
self.scale.bind('<Double-Button-1>', self.labelframe.reset_gain)
|
||||
self.scale.bind('<Button-1>', self.labelframe.scale_press)
|
||||
self.scale.bind('<ButtonRelease-1>', self.labelframe.scale_release)
|
||||
self.scale.bind(
|
||||
"<MouseWheel>",
|
||||
'<MouseWheel>',
|
||||
partial(
|
||||
self.labelframe.pause_updates,
|
||||
self.labelframe._on_mousewheel,
|
||||
@ -268,9 +269,9 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
|
||||
"""Adds a mute button widget to a single label frame"""
|
||||
self.button_mute = ttk.Checkbutton(
|
||||
self.labelframe,
|
||||
text="MUTE",
|
||||
text='MUTE',
|
||||
command=partial(self.labelframe.pause_updates, self.labelframe.toggle_mute),
|
||||
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{self.identifier}Mute{self.index}.TButton'}",
|
||||
style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{self.identifier}Mute{self.index}.TButton"}',
|
||||
variable=self.labelframe.mute,
|
||||
)
|
||||
self.button_mute.grid(column=0, row=2, columnspan=2)
|
||||
@ -278,9 +279,9 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
|
||||
def add_conf_button(self):
|
||||
self.button_conf = ttk.Checkbutton(
|
||||
self.labelframe,
|
||||
text="CONFIG",
|
||||
text='CONFIG',
|
||||
command=self.labelframe.open_config,
|
||||
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{self.identifier}Conf{self.index}.TButton'}",
|
||||
style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{self.identifier}Conf{self.index}.TButton"}',
|
||||
variable=self.labelframe.conf,
|
||||
)
|
||||
self.button_conf.grid(column=0, row=3, columnspan=2)
|
||||
@ -288,9 +289,9 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
|
||||
def add_on_button(self):
|
||||
self.button_on = ttk.Checkbutton(
|
||||
self.labelframe,
|
||||
text="ON",
|
||||
text='ON',
|
||||
command=partial(self.labelframe.pause_updates, self.labelframe.set_on),
|
||||
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{self.identifier}On{self.index}.TButton'}",
|
||||
style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{self.identifier}On{self.index}.TButton"}',
|
||||
variable=self.labelframe.on,
|
||||
)
|
||||
self.button_on.grid(column=0, row=2, columnspan=2)
|
||||
@ -339,40 +340,40 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
"""Responsible for building channel configframe widgets"""
|
||||
|
||||
def setup(self):
|
||||
if self.configframe.parent.kind.name == "basic":
|
||||
self.configframe.slider_params = ("audibility",)
|
||||
if self.configframe.parent.kind.name == 'basic':
|
||||
self.configframe.slider_params = ('audibility',)
|
||||
self.configframe.slider_vars = (tk.DoubleVar(),)
|
||||
else:
|
||||
self.configframe.slider_params = ("comp.knob", "gate.knob", "limit")
|
||||
self.configframe.slider_params = ('comp.knob', 'gate.knob', 'limit')
|
||||
self.configframe.slider_vars = [
|
||||
tk.DoubleVar() for _ in self.configframe.slider_params
|
||||
]
|
||||
|
||||
self.configframe.phys_out_params = [
|
||||
f"A{i+1}" for i in range(self.configframe.phys_out)
|
||||
f'A{i + 1}' for i in range(self.configframe.phys_out)
|
||||
]
|
||||
self.configframe.phys_out_params_vars = [
|
||||
tk.BooleanVar() for _ in self.configframe.phys_out_params
|
||||
]
|
||||
|
||||
self.configframe.virt_out_params = [
|
||||
f"B{i+1}" for i in range(self.configframe.virt_out)
|
||||
f'B{i + 1}' for i in range(self.configframe.virt_out)
|
||||
]
|
||||
self.configframe.virt_out_params_vars = [
|
||||
tk.BooleanVar() for _ in self.configframe.virt_out_params
|
||||
]
|
||||
|
||||
self.configframe.params = ("mono", "solo")
|
||||
self.configframe.param_vars = list(
|
||||
tk.BooleanVar() for _ in self.configframe.params
|
||||
self.configframe.bool_params = ('mono', 'solo')
|
||||
self.configframe.bool_param_vars = list(
|
||||
tk.BooleanVar() for _ in self.configframe.bool_params
|
||||
)
|
||||
|
||||
if self.configframe.parent.kind.name in ("banana", "potato"):
|
||||
if self.configframe.parent.kind.name in ('banana', 'potato'):
|
||||
if self.configframe.index == self.configframe.phys_in:
|
||||
self.configframe.params = list(
|
||||
map(lambda x: x.replace("mono", "mc"), self.configframe.params)
|
||||
map(lambda x: x.replace('mono', 'mc'), self.configframe.bool_params)
|
||||
)
|
||||
if self.configframe.parent.kind.name == "banana":
|
||||
if self.configframe.parent.kind.name == 'banana':
|
||||
pass
|
||||
# karaoke modes not in RT Packet yet. May implement in future
|
||||
"""
|
||||
@ -388,101 +389,104 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
== self.configframe.phys_in + self.configframe.virt_in - 1
|
||||
):
|
||||
self.configframe.params = list(
|
||||
map(lambda x: x.replace("mono", "mc"), self.configframe.params)
|
||||
map(
|
||||
lambda x: x.replace('mono', 'mc'),
|
||||
self.configframe.bool_params,
|
||||
)
|
||||
)
|
||||
|
||||
def create_comp_slider(self):
|
||||
comp_label = ttk.Label(self.configframe, text="Comp")
|
||||
comp_label = ttk.Label(self.configframe, text='Comp')
|
||||
comp_scale = ttk.Scale(
|
||||
self.configframe,
|
||||
from_=0.0,
|
||||
to=10.0,
|
||||
orient="horizontal",
|
||||
orient='horizontal',
|
||||
length=_configuration.channel_width,
|
||||
variable=self.configframe.slider_vars[
|
||||
self.configframe.slider_params.index("comp.knob")
|
||||
self.configframe.slider_params.index('comp.knob')
|
||||
],
|
||||
command=partial(self.configframe.scale_callback, "comp.knob"),
|
||||
command=partial(self.configframe.scale_callback, 'comp.knob'),
|
||||
)
|
||||
comp_scale.bind(
|
||||
"<Double-Button-1>", partial(self.configframe.reset_scale, "comp.knob", 0)
|
||||
'<Double-Button-1>', partial(self.configframe.reset_scale, 'comp.knob', 0)
|
||||
)
|
||||
comp_scale.bind("<Button-1>", self.configframe.scale_press)
|
||||
comp_scale.bind("<ButtonRelease-1>", self.configframe.scale_release)
|
||||
comp_scale.bind("<Enter>", partial(self.configframe.scale_enter, "comp.knob"))
|
||||
comp_scale.bind("<Leave>", self.configframe.scale_leave)
|
||||
comp_scale.bind('<Button-1>', self.configframe.scale_press)
|
||||
comp_scale.bind('<ButtonRelease-1>', self.configframe.scale_release)
|
||||
comp_scale.bind('<Enter>', partial(self.configframe.scale_enter, 'comp.knob'))
|
||||
comp_scale.bind('<Leave>', self.configframe.scale_leave)
|
||||
|
||||
comp_label.grid(column=0, row=0)
|
||||
comp_scale.grid(column=1, row=0)
|
||||
|
||||
def create_gate_slider(self):
|
||||
gate_label = ttk.Label(self.configframe, text="Gate")
|
||||
gate_label = ttk.Label(self.configframe, text='Gate')
|
||||
gate_scale = ttk.Scale(
|
||||
self.configframe,
|
||||
from_=0.0,
|
||||
to=10.0,
|
||||
orient="horizontal",
|
||||
orient='horizontal',
|
||||
length=_configuration.channel_width,
|
||||
variable=self.configframe.slider_vars[
|
||||
self.configframe.slider_params.index("gate.knob")
|
||||
self.configframe.slider_params.index('gate.knob')
|
||||
],
|
||||
command=partial(self.configframe.scale_callback, "gate.knob"),
|
||||
command=partial(self.configframe.scale_callback, 'gate.knob'),
|
||||
)
|
||||
gate_scale.bind(
|
||||
"<Double-Button-1>", partial(self.configframe.reset_scale, "gate.knob", 0)
|
||||
'<Double-Button-1>', partial(self.configframe.reset_scale, 'gate.knob', 0)
|
||||
)
|
||||
gate_scale.bind("<Button-1>", self.configframe.scale_press)
|
||||
gate_scale.bind("<ButtonRelease-1>", self.configframe.scale_release)
|
||||
gate_scale.bind("<Enter>", partial(self.configframe.scale_enter, "gate.knob"))
|
||||
gate_scale.bind("<Leave>", self.configframe.scale_leave)
|
||||
gate_scale.bind('<Button-1>', self.configframe.scale_press)
|
||||
gate_scale.bind('<ButtonRelease-1>', self.configframe.scale_release)
|
||||
gate_scale.bind('<Enter>', partial(self.configframe.scale_enter, 'gate.knob'))
|
||||
gate_scale.bind('<Leave>', self.configframe.scale_leave)
|
||||
|
||||
gate_label.grid(column=2, row=0)
|
||||
gate_scale.grid(column=3, row=0)
|
||||
|
||||
def create_limit_slider(self):
|
||||
limit_label = ttk.Label(self.configframe, text="Limit")
|
||||
limit_label = ttk.Label(self.configframe, text='Limit')
|
||||
limit_scale = ttk.Scale(
|
||||
self.configframe,
|
||||
from_=-40,
|
||||
to=12,
|
||||
orient="horizontal",
|
||||
orient='horizontal',
|
||||
length=_configuration.channel_width,
|
||||
variable=self.configframe.slider_vars[
|
||||
self.configframe.slider_params.index("limit")
|
||||
self.configframe.slider_params.index('limit')
|
||||
],
|
||||
command=partial(self.configframe.scale_callback, "limit"),
|
||||
command=partial(self.configframe.scale_callback, 'limit'),
|
||||
)
|
||||
limit_scale.bind(
|
||||
"<Double-Button-1>", partial(self.configframe.reset_scale, "limit", 12)
|
||||
'<Double-Button-1>', partial(self.configframe.reset_scale, 'limit', 12)
|
||||
)
|
||||
limit_scale.bind("<Button-1>", self.configframe.scale_press)
|
||||
limit_scale.bind("<ButtonRelease-1>", self.configframe.scale_release)
|
||||
limit_scale.bind("<Enter>", partial(self.configframe.scale_enter, "limit"))
|
||||
limit_scale.bind("<Leave>", self.configframe.scale_leave)
|
||||
limit_scale.bind('<Button-1>', self.configframe.scale_press)
|
||||
limit_scale.bind('<ButtonRelease-1>', self.configframe.scale_release)
|
||||
limit_scale.bind('<Enter>', partial(self.configframe.scale_enter, 'limit'))
|
||||
limit_scale.bind('<Leave>', self.configframe.scale_leave)
|
||||
|
||||
limit_label.grid(column=4, row=0)
|
||||
limit_scale.grid(column=5, row=0)
|
||||
|
||||
def create_audibility_slider(self):
|
||||
aud_label = ttk.Label(self.configframe, text="Audibility")
|
||||
aud_label = ttk.Label(self.configframe, text='Audibility')
|
||||
aud_scale = ttk.Scale(
|
||||
self.configframe,
|
||||
from_=0.0,
|
||||
to=10.0,
|
||||
orient="horizontal",
|
||||
orient='horizontal',
|
||||
length=_configuration.channel_width,
|
||||
variable=self.configframe.slider_vars[
|
||||
self.configframe.slider_params.index("audibility")
|
||||
self.configframe.slider_params.index('audibility')
|
||||
],
|
||||
command=partial(self.configframe.scale_callback, "audibility"),
|
||||
command=partial(self.configframe.scale_callback, 'audibility'),
|
||||
)
|
||||
aud_scale.bind(
|
||||
"<Double-Button-1>", partial(self.configframe.reset_scale, "audibility", 0)
|
||||
'<Double-Button-1>', partial(self.configframe.reset_scale, 'audibility', 0)
|
||||
)
|
||||
aud_scale.bind("<Button-1>", self.configframe.scale_press)
|
||||
aud_scale.bind("<ButtonRelease-1>", self.configframe.scale_release)
|
||||
aud_scale.bind("<Enter>", partial(self.configframe.scale_enter, "audibility"))
|
||||
aud_scale.bind("<Leave>", self.configframe.scale_leave)
|
||||
aud_scale.bind('<Button-1>', self.configframe.scale_press)
|
||||
aud_scale.bind('<ButtonRelease-1>', self.configframe.scale_release)
|
||||
aud_scale.bind('<Enter>', partial(self.configframe.scale_enter, 'audibility'))
|
||||
aud_scale.bind('<Leave>', self.configframe.scale_leave)
|
||||
|
||||
aud_label.grid(column=0, row=0)
|
||||
aud_scale.grid(column=1, row=0)
|
||||
@ -495,7 +499,7 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
command=partial(
|
||||
self.configframe.pause_updates, self.configframe.toggle_a, param
|
||||
),
|
||||
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{param}.TButton'}",
|
||||
style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{param}.TButton"}',
|
||||
variable=self.configframe.phys_out_params_vars[
|
||||
self.configframe.phys_out_params.index(param)
|
||||
],
|
||||
@ -518,7 +522,7 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
command=partial(
|
||||
self.configframe.pause_updates, self.configframe.toggle_b, param
|
||||
),
|
||||
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{param}.TButton'}",
|
||||
style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{param}.TButton"}',
|
||||
variable=self.configframe.virt_out_params_vars[
|
||||
self.configframe.virt_out_params.index(param)
|
||||
],
|
||||
@ -541,10 +545,10 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
command=partial(
|
||||
self.configframe.pause_updates, self.configframe.toggle_p, param
|
||||
),
|
||||
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{param}.TButton'}",
|
||||
variable=self.configframe.param_vars[i],
|
||||
style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{param}.TButton"}',
|
||||
variable=self.configframe.bool_param_vars[i],
|
||||
)
|
||||
for i, param in enumerate(self.configframe.params)
|
||||
for i, param in enumerate(self.configframe.bool_params)
|
||||
]
|
||||
[
|
||||
button.grid(
|
||||
@ -558,50 +562,73 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
class BusConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
"""Responsible for building channel configframe widgets"""
|
||||
|
||||
def __init__(self, configframe, app):
|
||||
super().__init__(configframe)
|
||||
self.app = app
|
||||
|
||||
def setup(self):
|
||||
# fmt: off
|
||||
self.configframe.bus_mode_map = {
|
||||
"normal": "Normal",
|
||||
"amix": "Mix Down A",
|
||||
"bmix": "Mix Down B",
|
||||
"repeat": "Stereo Repeat",
|
||||
"composite": "Composite",
|
||||
"tvmix": "Up Mix TV",
|
||||
"upmix21": "Up Mix 2.1",
|
||||
"upmix41": "Up Mix 4.1",
|
||||
"upmix61": "Up Mix 6.1",
|
||||
"centeronly": "Center Only",
|
||||
"lfeonly": "LFE Only",
|
||||
"rearonly": "Rear Only",
|
||||
}
|
||||
self.configframe.bus_modes = list(self.configframe.bus_mode_map.keys())
|
||||
# fmt: on
|
||||
self.configframe.params = ("mono", "eq.on", "eq.ab")
|
||||
self.configframe.param_vars = [tk.BooleanVar() for _ in self.configframe.params]
|
||||
self.configframe.bus_mode_map = util.get_busmode_fullnames(self.app.kind)
|
||||
self.configframe.bus_mode_map_reverse = util.get_busmode_fullnames_reversed(
|
||||
self.app.kind
|
||||
)
|
||||
self.configframe.bus_modes = util.get_busmode_shortnames(self.app.kind)
|
||||
self.configframe.int_params = ('mono',)
|
||||
self.configframe.int_param_vars = [
|
||||
tk.IntVar(value=getattr(self.configframe.target, param))
|
||||
for param in self.configframe.int_params
|
||||
]
|
||||
self.configframe.mono_modes = util.get_busmono_modes()
|
||||
self.configframe.bus_mono_label_text = tk.StringVar(
|
||||
value=self.configframe.mono_modes[self.configframe.target.mono]
|
||||
)
|
||||
self.configframe.bool_params = ('eq.on', 'eq.ab')
|
||||
self.configframe.bool_param_vars = [
|
||||
tk.BooleanVar() for _ in self.configframe.bool_params
|
||||
]
|
||||
self.configframe.bus_mode_label_text = tk.StringVar(
|
||||
value=self.configframe.bus_mode_map[self.configframe.current_bus_mode()]
|
||||
)
|
||||
|
||||
def create_bus_mode_button(self):
|
||||
self.configframe.busmode_button = ttk.Button(
|
||||
self.configframe, textvariable=self.configframe.bus_mode_label_text
|
||||
self.configframe,
|
||||
textvariable=self.configframe.bus_mode_label_text,
|
||||
width=15,
|
||||
)
|
||||
self.configframe.busmode_button.grid(
|
||||
column=0, row=0, columnspan=2, sticky=(tk.W)
|
||||
column=0, row=0, columnspan=2, sticky=(tk.W), padx=1, pady=1
|
||||
)
|
||||
self.configframe.busmode_button.bind(
|
||||
"<Button-1>",
|
||||
'<Button-1>',
|
||||
partial(
|
||||
self.configframe.pause_updates, self.configframe.rotate_bus_modes_right
|
||||
),
|
||||
)
|
||||
self.configframe.busmode_button.bind(
|
||||
"<Button-3>",
|
||||
'<Button-3>',
|
||||
partial(
|
||||
self.configframe.pause_updates, self.configframe.rotate_bus_modes_left
|
||||
),
|
||||
)
|
||||
|
||||
def create_bus_mono_button(self):
|
||||
self.configframe.mono_button = ttk.Button(
|
||||
self.configframe,
|
||||
textvariable=self.configframe.bus_mono_label_text,
|
||||
width=15,
|
||||
)
|
||||
self.configframe.mono_button.bind(
|
||||
'<Button-1>',
|
||||
partial(self.configframe.pause_updates, self.configframe.rotate_mono_right),
|
||||
)
|
||||
self.configframe.mono_button.bind(
|
||||
'<Button-3>',
|
||||
partial(self.configframe.pause_updates, self.configframe.rotate_mono_left),
|
||||
)
|
||||
self.configframe.mono_button.grid(
|
||||
column=0, row=1, sticky=(tk.W), padx=1, pady=1
|
||||
)
|
||||
|
||||
def create_param_buttons(self):
|
||||
param_buttons = [
|
||||
ttk.Checkbutton(
|
||||
@ -610,14 +637,14 @@ class BusConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
command=partial(
|
||||
self.configframe.pause_updates, self.configframe.toggle_p, param
|
||||
),
|
||||
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{param}.TButton'}",
|
||||
variable=self.configframe.param_vars[i],
|
||||
style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{param}.TButton"}',
|
||||
variable=self.configframe.bool_param_vars[i],
|
||||
)
|
||||
for i, param in enumerate(self.configframe.params)
|
||||
for i, param in enumerate(self.configframe.bool_params)
|
||||
]
|
||||
[
|
||||
button.grid(
|
||||
column=i,
|
||||
column=i + 1,
|
||||
row=1,
|
||||
)
|
||||
for i, button in enumerate(param_buttons)
|
||||
|
||||
@ -46,7 +46,7 @@ class ChannelLabelFrame(ttk.LabelFrame):
|
||||
try:
|
||||
return getattr(self.target, param)
|
||||
except AttributeError as e:
|
||||
self.logger(f"{type(e).__name__}: {e}")
|
||||
self.logger(f'{type(e).__name__}: {e}')
|
||||
|
||||
def setter(self, param, value):
|
||||
if param in dir(self.target): # avoid calling getattr (with hasattr)
|
||||
@ -56,19 +56,19 @@ class ChannelLabelFrame(ttk.LabelFrame):
|
||||
"""callback function for scale widget"""
|
||||
|
||||
val = round(self.gain.get(), 1)
|
||||
self.setter("gain", val)
|
||||
self.setter('gain', val)
|
||||
self.gainlabel.set(val)
|
||||
|
||||
def toggle_mute(self, *args):
|
||||
self.target.mute = self.mute.get()
|
||||
if not _configuration.themes_enabled:
|
||||
self.styletable.configure(
|
||||
f"{self.identifier}Mute{self.index}.TButton",
|
||||
f'{self.identifier}Mute{self.index}.TButton',
|
||||
background=f'{"red" if self.mute.get() else "white"}',
|
||||
)
|
||||
|
||||
def reset_gain(self, *args):
|
||||
self.setter("gain", 0)
|
||||
self.setter('gain', 0)
|
||||
self.gain.set(0)
|
||||
self.gainlabel.set(self.gain.get())
|
||||
|
||||
@ -76,16 +76,16 @@ class ChannelLabelFrame(ttk.LabelFrame):
|
||||
self.after(1, self.remove_events)
|
||||
|
||||
def remove_events(self):
|
||||
self.parent.target.event.remove("pdirty")
|
||||
self.parent.target.event.remove("ldirty")
|
||||
self.parent.target.event.remove('pdirty')
|
||||
self.parent.target.event.remove('ldirty')
|
||||
|
||||
def scale_release(self, *args):
|
||||
_base_values.run_update = False
|
||||
self.after(1, self.add_events)
|
||||
|
||||
def add_events(self):
|
||||
self.parent.target.event.add("pdirty")
|
||||
self.parent.target.event.add("ldirty")
|
||||
self.parent.target.event.add('pdirty')
|
||||
self.parent.target.event.add('ldirty')
|
||||
self.after(500, self.resume_updates)
|
||||
|
||||
def pause_updates(self, func, *args):
|
||||
@ -115,7 +115,7 @@ class ChannelLabelFrame(ttk.LabelFrame):
|
||||
self.gain.set(12)
|
||||
elif self.gain.get() < -60:
|
||||
self.gain.set(-60)
|
||||
self.setter("gain", self.gain.get())
|
||||
self.setter('gain', self.gain.get())
|
||||
self.gainlabel.set(round(self.gain.get(), 1))
|
||||
|
||||
def open_config(self):
|
||||
@ -125,36 +125,36 @@ class ChannelLabelFrame(ttk.LabelFrame):
|
||||
self.parent.parent.config_frame.teardown()
|
||||
if not _configuration.themes_enabled:
|
||||
self.styletable.configure(
|
||||
f"{self.identifier}Conf{self.index}.TButton",
|
||||
f'{self.identifier}Conf{self.index}.TButton',
|
||||
background=f'{"yellow" if self.conf.get() else "white"}',
|
||||
)
|
||||
|
||||
def on_update(self, subject):
|
||||
if subject == "ldirty":
|
||||
if subject == 'ldirty':
|
||||
self.upd_levels()
|
||||
elif subject == "pdirty":
|
||||
elif subject == 'pdirty':
|
||||
self.sync_params()
|
||||
elif subject == "labelframe":
|
||||
elif subject == 'labelframe':
|
||||
self.after(5, self.sync_labels)
|
||||
|
||||
def sync_params(self):
|
||||
"""sync parameter states, update button colours"""
|
||||
self.gain.set(self.getter("gain"))
|
||||
self.gain.set(self.getter('gain'))
|
||||
self.gainlabel.set(round(self.gain.get(), 1))
|
||||
self.mute.set(self.getter("mute"))
|
||||
self.mute.set(self.getter('mute'))
|
||||
if not _configuration.themes_enabled:
|
||||
self.styletable.configure(
|
||||
f"{self.identifier}Mute{self.index}.TButton",
|
||||
background=f'{"red" if self.mute.get() else "white"}',
|
||||
f'{self.identifier}Mute{self.index}.TButton',
|
||||
background=f'{"red" if self.mute.get() else "white"}',
|
||||
)
|
||||
|
||||
def sync_labels(self):
|
||||
"""sync labelframes according to label text"""
|
||||
retval = self.getter("label")
|
||||
retval = self.getter('label')
|
||||
if self.parent.label_cache[self.id][self.index] != retval:
|
||||
self.parent.label_cache[self.id][self.index] = retval
|
||||
if len(retval) > 10:
|
||||
retval = f"{retval[:8]}.."
|
||||
retval = f'{retval[:8]}..'
|
||||
if not retval:
|
||||
self.parent.columnconfigure(self.index, minsize=0)
|
||||
self.parent.parent.subject.remove(self)
|
||||
@ -197,14 +197,22 @@ class Strip(ChannelLabelFrame):
|
||||
|
||||
def upd_levels(self):
|
||||
"""
|
||||
Updates level values.
|
||||
Updates level values using direct dB values.
|
||||
"""
|
||||
if self.index < self.parent.parent.kind.num_strip:
|
||||
if self.target.levels.is_updated:
|
||||
val = max(self.target.levels.prefader)
|
||||
self.level.set(
|
||||
(0 if self.mute.get() else 72 + val - 12 + self.gain.get())
|
||||
)
|
||||
if val < -72:
|
||||
if self.level.get() != 0:
|
||||
self.level.set(0)
|
||||
return
|
||||
# Convert dB to progressbar: -60dB=0, 0dB=60, +12dB=72
|
||||
if self.mute.get():
|
||||
level_display = 0
|
||||
else:
|
||||
level_db = val + self.gain.get()
|
||||
level_display = max(0, min(72, level_db + 60))
|
||||
self.level.set(level_display)
|
||||
|
||||
|
||||
class Bus(ChannelLabelFrame):
|
||||
@ -223,9 +231,18 @@ class Bus(ChannelLabelFrame):
|
||||
|
||||
def upd_levels(self):
|
||||
if self.index < self.parent.parent.kind.num_bus:
|
||||
if self.target.levels.is_updated or self.level.get() != -118:
|
||||
if self.target.levels.is_updated:
|
||||
val = max(self.target.levels.all)
|
||||
self.level.set((0 if self.mute.get() else 72 + val - 12))
|
||||
if val < -72:
|
||||
if self.level.get() != 0:
|
||||
self.level.set(0)
|
||||
return
|
||||
# Convert dB to progressbar: -60dB=0, 0dB=60, +12dB=72
|
||||
if self.mute.get():
|
||||
level_display = 0
|
||||
else:
|
||||
level_display = max(0, min(72, val + 60))
|
||||
self.level.set(level_display)
|
||||
|
||||
|
||||
class ChannelFrame(ttk.Frame):
|
||||
@ -236,8 +253,8 @@ class ChannelFrame(ttk.Frame):
|
||||
self.phys_in, self.virt_in = parent.kind.ins
|
||||
self.phys_out, self.virt_out = parent.kind.outs
|
||||
self.label_cache = {
|
||||
"strip": [""] * (self.phys_in + self.virt_in),
|
||||
"bus": [""] * (self.phys_out + self.virt_out),
|
||||
'strip': [''] * (self.phys_in + self.virt_in),
|
||||
'bus': [''] * (self.phys_out + self.virt_out),
|
||||
}
|
||||
self.parent.subject.add(self)
|
||||
self.update_labels()
|
||||
@ -264,10 +281,10 @@ class ChannelFrame(ttk.Frame):
|
||||
|
||||
def update_labels(self):
|
||||
for labelframe in self.labelframes:
|
||||
labelframe.on_update("labelframe")
|
||||
labelframe.on_update('labelframe')
|
||||
|
||||
def on_update(self, subject):
|
||||
if subject == "pdirty":
|
||||
if subject == 'pdirty':
|
||||
self.update_labels()
|
||||
|
||||
def grid_configure(self):
|
||||
@ -281,7 +298,7 @@ class ChannelFrame(ttk.Frame):
|
||||
[self.parent.subject.remove(frame) for frame in self.labelframes]
|
||||
self.parent.subject.remove(self)
|
||||
self.destroy()
|
||||
setattr(self.parent, f"{self.identifier}_frame", None)
|
||||
setattr(self.parent, f'{self.identifier}_frame', None)
|
||||
|
||||
|
||||
def _make_channelframe(parent, identifier):
|
||||
@ -298,7 +315,7 @@ def _make_channelframe(parent, identifier):
|
||||
"""
|
||||
|
||||
for i, labelframe in enumerate(
|
||||
getattr(self, "strips" if identifier == "strip" else "buses")
|
||||
getattr(self, 'strips' if identifier == 'strip' else 'buses')
|
||||
):
|
||||
labelframe.grid(row=0, column=i)
|
||||
label = labelframe.target.label
|
||||
@ -329,20 +346,20 @@ def _make_channelframe(parent, identifier):
|
||||
self.grid_configure()
|
||||
init_labels(self)
|
||||
|
||||
if identifier == "strip":
|
||||
if identifier == 'strip':
|
||||
CHANNELFRAME_cls = type(
|
||||
f"ChannelFrame{identifier.capitalize()}",
|
||||
f'ChannelFrame{identifier.capitalize()}',
|
||||
(ChannelFrame,),
|
||||
{
|
||||
"__init__": init_strip,
|
||||
'__init__': init_strip,
|
||||
},
|
||||
)
|
||||
else:
|
||||
CHANNELFRAME_cls = type(
|
||||
f"ChannelFrame{identifier.capitalize()}",
|
||||
f'ChannelFrame{identifier.capitalize()}',
|
||||
(ChannelFrame,),
|
||||
{
|
||||
"__init__": init_bus,
|
||||
'__init__': init_bus,
|
||||
},
|
||||
)
|
||||
return CHANNELFRAME_cls(parent)
|
||||
|
||||
@ -31,7 +31,7 @@ class Config(ttk.Frame):
|
||||
return self.parent.target
|
||||
|
||||
def getter(self, param):
|
||||
param = param.split(".")
|
||||
param = param.split('.')
|
||||
try:
|
||||
if len(param) == 2:
|
||||
target = getattr(self.target, param[0])
|
||||
@ -39,10 +39,10 @@ class Config(ttk.Frame):
|
||||
else:
|
||||
return getattr(self.target, param[0])
|
||||
except AttributeError as e:
|
||||
self.logger.error(f"{type(e).__name__}: {e}")
|
||||
self.logger.error(f'{type(e).__name__}: {e}')
|
||||
|
||||
def setter(self, param, value):
|
||||
param = param.split(".")
|
||||
param = param.split('.')
|
||||
try:
|
||||
if len(param) == 2:
|
||||
target = getattr(self.target, param[0])
|
||||
@ -50,22 +50,22 @@ class Config(ttk.Frame):
|
||||
else:
|
||||
setattr(self.target, param[0], value)
|
||||
except AttributeError as e:
|
||||
self.logger(f"{type(e).__name__}: {e}")
|
||||
self.logger(f'{type(e).__name__}: {e}')
|
||||
|
||||
def scale_press(self, *args):
|
||||
self.after(1, self.remove_events)
|
||||
|
||||
def remove_events(self):
|
||||
self.parent.target.event.remove("pdirty")
|
||||
self.parent.target.event.remove("ldirty")
|
||||
self.parent.target.event.remove('pdirty')
|
||||
self.parent.target.event.remove('ldirty')
|
||||
|
||||
def scale_release(self, *args):
|
||||
_base_values.run_update = False
|
||||
self.after(1, self.add_events)
|
||||
|
||||
def add_events(self):
|
||||
self.parent.target.event.add("pdirty")
|
||||
self.parent.target.event.add("ldirty")
|
||||
self.parent.target.event.add('pdirty')
|
||||
self.parent.target.event.add('ldirty')
|
||||
self.after(350, self.resume_updates)
|
||||
|
||||
def pause_updates(self, func, *args):
|
||||
@ -84,7 +84,7 @@ class Config(ttk.Frame):
|
||||
self.parent.nav_frame.info_text.set(round(val, 1))
|
||||
|
||||
def scale_leave(self, *args):
|
||||
self.parent.nav_frame.info_text.set("")
|
||||
self.parent.nav_frame.info_text.set('')
|
||||
|
||||
def scale_callback(self, param, *args):
|
||||
"""callback function for scale widget"""
|
||||
@ -98,16 +98,16 @@ class Config(ttk.Frame):
|
||||
self.slider_vars[self.slider_params.index(param)].set(val)
|
||||
|
||||
def toggle_p(self, param):
|
||||
val = self.param_vars[self.params.index(param)].get()
|
||||
val = self.bool_param_vars[self.bool_params.index(param)].get()
|
||||
self.setter(param, val)
|
||||
if not _configuration.themes_enabled:
|
||||
self.styletable.configure(
|
||||
f"{param}.TButton", background=f'{"green" if val else "white"}'
|
||||
f'{param}.TButton', background=f'{"green" if val else "white"}'
|
||||
)
|
||||
|
||||
def on_update(self, subject):
|
||||
"""update parameters"""
|
||||
if subject == "pdirty":
|
||||
if subject == 'pdirty':
|
||||
self.sync()
|
||||
|
||||
|
||||
@ -134,7 +134,7 @@ class StripConfig(Config):
|
||||
|
||||
def make_row_0(self):
|
||||
if self.index < self.phys_in:
|
||||
if self.parent.kind.name == "basic":
|
||||
if self.parent.kind.name == 'basic':
|
||||
self.builder.create_audibility_slider()
|
||||
else:
|
||||
self.builder.create_comp_slider()
|
||||
@ -153,7 +153,7 @@ class StripConfig(Config):
|
||||
self.setter(param, val)
|
||||
if not _configuration.themes_enabled:
|
||||
self.styletable.configure(
|
||||
f"{param}.TButton", background=f'{"green" if val else "white"}'
|
||||
f'{param}.TButton', background=f'{"green" if val else "white"}'
|
||||
)
|
||||
|
||||
def toggle_b(self, param):
|
||||
@ -161,7 +161,7 @@ class StripConfig(Config):
|
||||
self.setter(param, val)
|
||||
if not _configuration.themes_enabled:
|
||||
self.styletable.configure(
|
||||
f"{param}.TButton", background=f'{"green" if val else "white"}'
|
||||
f'{param}.TButton', background=f'{"green" if val else "white"}'
|
||||
)
|
||||
|
||||
def teardown(self):
|
||||
@ -177,37 +177,36 @@ class StripConfig(Config):
|
||||
for i, param in enumerate(self.virt_out_params)
|
||||
]
|
||||
[
|
||||
self.param_vars[i].set(self.getter(param))
|
||||
for i, param in enumerate(self.params)
|
||||
self.bool_param_vars[i].set(self.getter(param))
|
||||
for i, param in enumerate(self.bool_params)
|
||||
]
|
||||
[
|
||||
self.slider_vars[i].set(self.getter(param))
|
||||
for i, param in enumerate(self.slider_params)
|
||||
if self.index < self.phys_in
|
||||
]
|
||||
if not _base_values.vban_connected: # slider vars not defined in RT Packet
|
||||
[
|
||||
self.slider_vars[i].set(self.getter(param))
|
||||
for i, param in enumerate(self.slider_params)
|
||||
if self.index < self.phys_in
|
||||
]
|
||||
|
||||
if not _configuration.themes_enabled:
|
||||
[
|
||||
self.styletable.configure(
|
||||
f"{param}.TButton",
|
||||
f'{param}.TButton',
|
||||
background=f'{"green" if self.phys_out_params_vars[i].get() else "white"}',
|
||||
)
|
||||
for i, param in enumerate(self.phys_out_params)
|
||||
]
|
||||
[
|
||||
self.styletable.configure(
|
||||
f"{param}.TButton",
|
||||
f'{param}.TButton',
|
||||
background=f'{"green" if self.virt_out_params_vars[i].get() else "white"}',
|
||||
)
|
||||
for i, param in enumerate(self.virt_out_params)
|
||||
]
|
||||
[
|
||||
self.styletable.configure(
|
||||
f"{param}.TButton",
|
||||
f'{param}.TButton',
|
||||
background=f'{"green" if self.param_vars[i].get() else "white"}',
|
||||
)
|
||||
for i, param in enumerate(self.params)
|
||||
for i, param in enumerate(self.bool_params)
|
||||
]
|
||||
|
||||
|
||||
@ -218,7 +217,7 @@ class BusConfig(Config):
|
||||
self.grid(column=0, row=1, columnspan=4, padx=(2,))
|
||||
else:
|
||||
self.grid(column=0, row=3, columnspan=4, padx=(2,))
|
||||
self.builder = builders.BusConfigFrameBuilder(self)
|
||||
self.builder = builders.BusConfigFrameBuilder(self, parent)
|
||||
self.builder.setup()
|
||||
self.make_row_0()
|
||||
self.make_row_1()
|
||||
@ -238,55 +237,56 @@ class BusConfig(Config):
|
||||
self.builder.create_bus_mode_button()
|
||||
|
||||
def make_row_1(self):
|
||||
self.builder.create_bus_mono_button()
|
||||
self.builder.create_param_buttons()
|
||||
|
||||
def current_bus_mode(self):
|
||||
for mode in self.bus_modes:
|
||||
if getattr(self.target.mode, mode):
|
||||
return mode
|
||||
return self.target.mode.get()
|
||||
|
||||
def rotate_bus_modes_right(self, *args):
|
||||
current_mode = self.current_bus_mode()
|
||||
next = self.bus_modes.index(current_mode) + 1
|
||||
if next < len(self.bus_modes):
|
||||
setattr(
|
||||
self.target.mode,
|
||||
self.bus_modes[next],
|
||||
True,
|
||||
)
|
||||
self.bus_mode_label_text.set(self.bus_mode_map[self.bus_modes[next]])
|
||||
else:
|
||||
self.target.mode.normal = True
|
||||
self.bus_mode_label_text.set("Normal")
|
||||
current_mode = self.bus_mode_map_reverse[self.bus_mode_label_text.get()]
|
||||
current_index = self.bus_modes.index(current_mode)
|
||||
next_index = (current_index + 1) % len(self.bus_modes)
|
||||
next_mode = self.bus_modes[next_index]
|
||||
|
||||
setattr(self.target.mode, next_mode, True)
|
||||
self.bus_mode_label_text.set(self.bus_mode_map[next_mode])
|
||||
|
||||
def rotate_bus_modes_left(self, *args):
|
||||
current_mode = self.current_bus_mode()
|
||||
prev = self.bus_modes.index(current_mode) - 1
|
||||
if prev < 0:
|
||||
self.target.mode.rearonly = True
|
||||
self.bus_mode_label_text.set("Rear Only")
|
||||
else:
|
||||
setattr(
|
||||
self.target.mode,
|
||||
self.bus_modes[prev],
|
||||
True,
|
||||
)
|
||||
self.bus_mode_label_text.set(self.bus_mode_map[self.bus_modes[prev]])
|
||||
current_mode = self.bus_mode_map_reverse[self.bus_mode_label_text.get()]
|
||||
current_index = self.bus_modes.index(current_mode)
|
||||
prev_index = (current_index - 1) % len(self.bus_modes)
|
||||
prev_mode = self.bus_modes[prev_index]
|
||||
|
||||
setattr(self.target.mode, prev_mode, True)
|
||||
self.bus_mode_label_text.set(self.bus_mode_map[prev_mode])
|
||||
|
||||
def rotate_mono_right(self, *args):
|
||||
current_val = self.mono_modes.index(self.bus_mono_label_text.get())
|
||||
next_val = (current_val + 1) % 3
|
||||
self.bus_mono_label_text.set(self.mono_modes[next_val])
|
||||
self.setter('mono', next_val)
|
||||
|
||||
def rotate_mono_left(self, *args):
|
||||
current_val = self.mono_modes.index(self.bus_mono_label_text.get())
|
||||
next_val = (current_val - 1) % 3
|
||||
self.bus_mono_label_text.set(self.mono_modes[next_val])
|
||||
self.setter('mono', next_val)
|
||||
|
||||
def teardown(self):
|
||||
self.builder.teardown()
|
||||
|
||||
def sync(self):
|
||||
[
|
||||
self.param_vars[i].set(self.getter(param))
|
||||
for i, param in enumerate(self.params)
|
||||
self.bool_param_vars[i].set(self.getter(param))
|
||||
for i, param in enumerate(self.bool_params)
|
||||
]
|
||||
self.bus_mode_label_text.set(self.bus_mode_map[self.current_bus_mode()])
|
||||
if not _configuration.themes_enabled:
|
||||
[
|
||||
self.styletable.configure(
|
||||
f"{param}.TButton",
|
||||
background=f'{"green" if self.param_vars[i].get() else "white"}',
|
||||
f'{param}.TButton',
|
||||
background=f'{"green" if self.bool_param_vars[i].get() else "white"}',
|
||||
)
|
||||
for i, param in enumerate(self.params)
|
||||
for i, param in enumerate(self.bool_params)
|
||||
]
|
||||
|
||||
@ -12,66 +12,66 @@ configuration = {}
|
||||
|
||||
|
||||
def get_configpath():
|
||||
configpaths = [
|
||||
Path.cwd() / "configs",
|
||||
Path.home() / ".config" / "vm-compact" / "configs",
|
||||
Path.home() / "Documents" / "Voicemeeter" / "configs",
|
||||
]
|
||||
for configpath in configpaths:
|
||||
if configpath.exists():
|
||||
return configpath
|
||||
for pn in (
|
||||
Path.home() / '.config' / 'vm-compact',
|
||||
Path.home() / 'Documents' / 'Voicemeeter' / 'vm-compact',
|
||||
Path.cwd() / '_internal' / 'configs',
|
||||
Path.cwd() / 'configs',
|
||||
):
|
||||
if pn.exists():
|
||||
return pn
|
||||
|
||||
|
||||
if configpath := get_configpath():
|
||||
filepaths = list(configpath.glob("*.toml"))
|
||||
if any(f.stem in ("app", "vban") for f in filepaths):
|
||||
filepaths = list(configpath.glob('*.toml'))
|
||||
if any(f.stem in ('app', 'vban') for f in filepaths):
|
||||
configs = {}
|
||||
for filepath in filepaths:
|
||||
filename = filepath.with_suffix("").stem
|
||||
if filename in ("app", "vban"):
|
||||
filename = filepath.with_suffix('').stem
|
||||
if filename in ('app', 'vban'):
|
||||
try:
|
||||
with open(filepath, "rb") as f:
|
||||
with open(filepath, 'rb') as f:
|
||||
configs[filename] = tomllib.load(f)
|
||||
logger.info(f"configuration: {filename} loaded into memory")
|
||||
logger.info(f'configuration: {filename} loaded into memory')
|
||||
except tomllib.TOMLDecodeError:
|
||||
logger.error(f"Invalid TOML config: configs/{filename.stem}")
|
||||
logger.error(f'Invalid TOML config: configs/{filename.stem}')
|
||||
configuration |= configs
|
||||
|
||||
_defaults = {
|
||||
"configs": {
|
||||
"config": None,
|
||||
'configs': {
|
||||
'config': None,
|
||||
},
|
||||
"theme": {
|
||||
"enabled": True,
|
||||
"mode": "light",
|
||||
'theme': {
|
||||
'enabled': True,
|
||||
'mode': 'light',
|
||||
},
|
||||
"extends": {
|
||||
"extended": True,
|
||||
"extends_horizontal": True,
|
||||
'extends': {
|
||||
'extended': True,
|
||||
'extends_horizontal': True,
|
||||
},
|
||||
"channel": {
|
||||
"width": 80,
|
||||
"height": 130,
|
||||
"xpadding": 3,
|
||||
'channel': {
|
||||
'width': 80,
|
||||
'height': 130,
|
||||
'xpadding': 3,
|
||||
},
|
||||
"mwscroll_step": {
|
||||
"size": 3,
|
||||
'mwscroll_step': {
|
||||
'size': 3,
|
||||
},
|
||||
"submixes": {
|
||||
"default": 0,
|
||||
'submixes': {
|
||||
'default': 0,
|
||||
},
|
||||
"navigation": {"show": True},
|
||||
'navigation': {'show': False},
|
||||
}
|
||||
|
||||
|
||||
if "app" in configuration:
|
||||
if 'app' in configuration:
|
||||
for key in _defaults:
|
||||
if key in configuration["app"]:
|
||||
configuration["app"][key] = _defaults[key] | configuration["app"][key]
|
||||
if key in configuration['app']:
|
||||
configuration['app'][key] = _defaults[key] | configuration['app'][key]
|
||||
else:
|
||||
configuration["app"][key] = _defaults[key]
|
||||
configuration['app'][key] = _defaults[key]
|
||||
else:
|
||||
configuration["app"] = _defaults
|
||||
configuration['app'] = _defaults
|
||||
|
||||
|
||||
def get_configuration(key):
|
||||
@ -80,19 +80,19 @@ def get_configuration(key):
|
||||
|
||||
|
||||
def loader(kind_id, target):
|
||||
configs = {"reset": target.configs["reset"]}
|
||||
configs = {'reset': target.configs['reset']}
|
||||
if configpath := get_configpath():
|
||||
userconfigpath = configpath / kind_id
|
||||
if userconfigpath.exists():
|
||||
filepaths = list(userconfigpath.glob("*.toml"))
|
||||
filepaths = list(userconfigpath.glob('*.toml'))
|
||||
for filepath in filepaths:
|
||||
identifier = filepath.with_suffix("").stem
|
||||
identifier = filepath.with_suffix('').stem
|
||||
try:
|
||||
with open(filepath, "rb") as f:
|
||||
with open(filepath, 'rb') as f:
|
||||
configs[identifier] = tomllib.load(f)
|
||||
logger.info(f"loader: {identifier} loaded into memory")
|
||||
logger.info(f'loader: {identifier} loaded into memory')
|
||||
except tomllib.TOMLDecodeError:
|
||||
logger.error(f"Invalid TOML config: configs/{filename.stem}")
|
||||
logger.error(f'Invalid TOML config: configs/{filename.stem}')
|
||||
|
||||
target.configs = configs
|
||||
return target.configs
|
||||
|
||||
@ -4,7 +4,7 @@ from voicemeeterlib import kinds
|
||||
|
||||
from .configurations import get_configuration
|
||||
|
||||
configuration = get_configuration("app")
|
||||
configuration = get_configuration('app')
|
||||
|
||||
|
||||
class SingletonMeta(type):
|
||||
@ -20,32 +20,32 @@ class SingletonMeta(type):
|
||||
@dataclass
|
||||
class Configurations(metaclass=SingletonMeta):
|
||||
# is the gui extended
|
||||
extended: bool = configuration["extends"]["extended"]
|
||||
extended: bool = configuration['extends']['extended']
|
||||
# direction the gui extends
|
||||
extends_horizontal: bool = configuration["extends"]["extends_horizontal"]
|
||||
extends_horizontal: bool = configuration['extends']['extends_horizontal']
|
||||
# are themes enabled
|
||||
themes_enabled: bool = configuration["theme"]["enabled"]
|
||||
themes_enabled: bool = configuration['theme']['enabled']
|
||||
# light or dark
|
||||
theme_mode: str = configuration["theme"]["mode"]
|
||||
theme_mode: str = configuration['theme']['mode']
|
||||
# size of mousewheel scroll step
|
||||
mwscroll_step: int = configuration["mwscroll_step"]["size"]
|
||||
mwscroll_step: int = configuration['mwscroll_step']['size']
|
||||
# bus assigned as current submix
|
||||
submixes: int = configuration["submixes"]["default"]
|
||||
submixes: int = configuration['submixes']['default']
|
||||
|
||||
# width of a single channel labelframe
|
||||
channel_width: int = configuration["channel"]["width"]
|
||||
channel_width: int = configuration['channel']['width']
|
||||
# height of a single channel labelframe
|
||||
channel_height: int = configuration["channel"]["height"]
|
||||
channel_height: int = configuration['channel']['height']
|
||||
# xpadding for a single channel labelframe
|
||||
channel_xpadding: int = configuration["channel"]["xpadding"]
|
||||
channel_xpadding: int = configuration['channel']['xpadding']
|
||||
|
||||
# do we grid the navigation frame?
|
||||
navigation_show: bool = configuration["navigation"]["show"]
|
||||
navigation_show: bool = configuration['navigation']['show']
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
if "configs" in configuration:
|
||||
return configuration["configs"]["config"]
|
||||
if 'configs' in configuration:
|
||||
return configuration['configs']['config']
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -61,10 +61,6 @@ class BaseValues(metaclass=SingletonMeta):
|
||||
_base_values = BaseValues()
|
||||
_configuration = Configurations()
|
||||
|
||||
_kinds = {kind.name: kind for kind in kinds.kinds_all}
|
||||
|
||||
_kinds_all = _kinds.values()
|
||||
|
||||
|
||||
def kind_get(kind_id):
|
||||
return _kinds[kind_id]
|
||||
return kinds.request_kind_map(kind_id)
|
||||
|
||||
@ -19,7 +19,7 @@ class GainLayer(ttk.LabelFrame):
|
||||
else:
|
||||
self.level_offset = parent.phys_in * 2 + (index - parent.phys_in) * 8
|
||||
|
||||
self.builder = builders.ChannelLabelFrameBuilder(self, index, id="gainlayer")
|
||||
self.builder = builders.ChannelLabelFrameBuilder(self, index, id='gainlayer')
|
||||
self.builder.setup()
|
||||
self.builder.add_progressbar()
|
||||
self.builder.add_scale()
|
||||
@ -38,20 +38,20 @@ class GainLayer(ttk.LabelFrame):
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
return "gainlayer"
|
||||
return 'gainlayer'
|
||||
|
||||
def getter(self, param):
|
||||
try:
|
||||
return getattr(self.target, param)
|
||||
except AttributeError as e:
|
||||
self.logger(f"{type(e).__name__}: {e}")
|
||||
self.logger(f'{type(e).__name__}: {e}')
|
||||
|
||||
def setter(self, param, value):
|
||||
if param in dir(self.target): # avoid calling getattr (with hasattr)
|
||||
setattr(self.target, param, value)
|
||||
|
||||
def reset_gain(self, *args):
|
||||
self.setter("gain", 0)
|
||||
self.setter('gain', 0)
|
||||
self.gain.set(0)
|
||||
self.gainlabel.set(self.gain.get())
|
||||
|
||||
@ -59,23 +59,23 @@ class GainLayer(ttk.LabelFrame):
|
||||
"""callback function for scale widget"""
|
||||
|
||||
val = round(self.gain.get(), 1)
|
||||
self.setter("gain", val)
|
||||
self.setter('gain', val)
|
||||
self.gainlabel.set(val)
|
||||
|
||||
def scale_press(self, *args):
|
||||
self.after(1, self.remove_events)
|
||||
|
||||
def remove_events(self):
|
||||
self.parent.target.event.remove("pdirty")
|
||||
self.parent.target.event.remove("ldirty")
|
||||
self.parent.target.event.remove('pdirty')
|
||||
self.parent.target.event.remove('ldirty')
|
||||
|
||||
def scale_release(self, *args):
|
||||
_base_values.run_update = False
|
||||
self.after(1, self.add_events)
|
||||
|
||||
def add_events(self):
|
||||
self.parent.target.event.add("pdirty")
|
||||
self.parent.target.event.add("ldirty")
|
||||
self.parent.target.event.add('pdirty')
|
||||
self.parent.target.event.add('ldirty')
|
||||
self.after(500, self.resume_updates)
|
||||
|
||||
def pause_updates(self, func, *args):
|
||||
@ -103,7 +103,7 @@ class GainLayer(ttk.LabelFrame):
|
||||
self.gain.set(12)
|
||||
elif self.gain.get() < -60:
|
||||
self.gain.set(-60)
|
||||
self.setter("gain", self.gain.get())
|
||||
self.setter('gain', self.gain.get())
|
||||
self.after(1, self.resume_updates)
|
||||
|
||||
def set_on(self):
|
||||
@ -116,20 +116,20 @@ class GainLayer(ttk.LabelFrame):
|
||||
)
|
||||
if not _configuration.themes_enabled:
|
||||
self.styletable.configure(
|
||||
f"{self.identifier}On{self.index}.TButton",
|
||||
f'{self.identifier}On{self.index}.TButton',
|
||||
background=f'{"green" if self.on.get() else "white"}',
|
||||
)
|
||||
|
||||
def on_update(self, subject):
|
||||
if subject == "ldirty":
|
||||
if subject == 'ldirty':
|
||||
self.upd_levels()
|
||||
elif subject == "pdirty":
|
||||
elif subject == 'pdirty':
|
||||
self.sync_params()
|
||||
elif subject == "labelframe":
|
||||
elif subject == 'labelframe':
|
||||
self.after(5, self.sync_labels)
|
||||
|
||||
def sync_params(self):
|
||||
self.gain.set(self.getter("gain"))
|
||||
self.gain.set(self.getter('gain'))
|
||||
self.gainlabel.set(round(self.gain.get(), 1))
|
||||
self.on.set(
|
||||
getattr(
|
||||
@ -139,7 +139,7 @@ class GainLayer(ttk.LabelFrame):
|
||||
)
|
||||
if not _configuration.themes_enabled:
|
||||
self.styletable.configure(
|
||||
f"{self.identifier}On{self.index}.TButton",
|
||||
f'{self.identifier}On{self.index}.TButton',
|
||||
background=f'{"green" if self.on.get() else "white"}',
|
||||
)
|
||||
|
||||
@ -147,7 +147,7 @@ class GainLayer(ttk.LabelFrame):
|
||||
"""sync params with voicemeeter"""
|
||||
retval = self.parent.target.strip[self.index].label
|
||||
if len(retval) > 10:
|
||||
retval = f"{retval[:8]}.."
|
||||
retval = f'{retval[:8]}..'
|
||||
if not retval:
|
||||
self.parent.columnconfigure(self.index, minsize=0)
|
||||
self.parent.parent.subject.remove(self)
|
||||
@ -161,17 +161,18 @@ class GainLayer(ttk.LabelFrame):
|
||||
"""
|
||||
Updates level values.
|
||||
"""
|
||||
|
||||
if self.parent.target.strip[self.index].levels.is_updated:
|
||||
val = max(self.parent.target.strip[self.index].levels.prefader)
|
||||
self.level.set(
|
||||
(
|
||||
0
|
||||
if self.parent.parent.strip_frame.strips[self.index].mute.get()
|
||||
or not self.on.get()
|
||||
else 72 + val - 12 + self.gain.get()
|
||||
)
|
||||
)
|
||||
# Convert dB to progressbar: -60dB=0, 0dB=60, +12dB=72
|
||||
if (
|
||||
self.parent.parent.strip_frame.strips[self.index].mute.get()
|
||||
or not self.on.get()
|
||||
):
|
||||
level_display = 0
|
||||
else:
|
||||
level_db = val + self.gain.get()
|
||||
level_display = max(0, min(72, level_db + 60))
|
||||
self.level.set(level_display)
|
||||
|
||||
def grid_configure(self):
|
||||
self.grid(padx=_configuration.channel_xpadding, sticky=(tk.N, tk.S))
|
||||
@ -201,8 +202,8 @@ class SubMixFrame(ttk.Frame):
|
||||
self.parent = parent
|
||||
self.phys_in, self.virt_in = parent.kind.ins
|
||||
self.phys_out, self.virt_out = parent.kind.outs
|
||||
self.buses = tuple(f"A{i+1}" for i in range(self.phys_out)) + tuple(
|
||||
f"B{i+1}" for i in range(self.virt_out)
|
||||
self.buses = tuple(f'A{i + 1}' for i in range(self.phys_out)) + tuple(
|
||||
f'B{i + 1}' for i in range(self.virt_out)
|
||||
)
|
||||
|
||||
self.gainlayers = [
|
||||
@ -221,7 +222,7 @@ class SubMixFrame(ttk.Frame):
|
||||
else:
|
||||
if parent.bus_frame and parent.bus_frame.grid_info():
|
||||
self.grid(
|
||||
row=parent.bus_frame.grid_info()["row"], column=0, sticky=(tk.W)
|
||||
row=parent.bus_frame.grid_info()['row'], column=0, sticky=(tk.W)
|
||||
)
|
||||
parent.bus_frame.grid_remove()
|
||||
else:
|
||||
@ -256,9 +257,9 @@ class SubMixFrame(ttk.Frame):
|
||||
)
|
||||
|
||||
def on_update(self, subject):
|
||||
if subject == "pdirty":
|
||||
if subject == 'pdirty':
|
||||
for labelframe in self.labelframes:
|
||||
labelframe.on_update("labelframe")
|
||||
labelframe.on_update('labelframe')
|
||||
|
||||
def grid_configure(self):
|
||||
[
|
||||
|
||||
1
vmcompact/gui/banana/__init__.py
Normal file
1
vmcompact/gui/banana/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .main import run
|
||||
11
vmcompact/gui/banana/main.py
Normal file
11
vmcompact/gui/banana/main.py
Normal file
@ -0,0 +1,11 @@
|
||||
import voicemeeterlib
|
||||
|
||||
import vmcompact
|
||||
|
||||
|
||||
def run():
|
||||
KIND_ID = 'banana'
|
||||
|
||||
with voicemeeterlib.api(KIND_ID) as vmr:
|
||||
app = vmcompact.connect(KIND_ID, vmr)
|
||||
app.mainloop()
|
||||
1
vmcompact/gui/basic/__init__.py
Normal file
1
vmcompact/gui/basic/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .main import run
|
||||
11
vmcompact/gui/basic/main.py
Normal file
11
vmcompact/gui/basic/main.py
Normal file
@ -0,0 +1,11 @@
|
||||
import voicemeeterlib
|
||||
|
||||
import vmcompact
|
||||
|
||||
|
||||
def run():
|
||||
KIND_ID = 'basic'
|
||||
|
||||
with voicemeeterlib.api(KIND_ID) as vmr:
|
||||
app = vmcompact.connect(KIND_ID, vmr)
|
||||
app.mainloop()
|
||||
1
vmcompact/gui/potato/__init__.py
Normal file
1
vmcompact/gui/potato/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .main import run
|
||||
11
vmcompact/gui/potato/main.py
Normal file
11
vmcompact/gui/potato/main.py
Normal file
@ -0,0 +1,11 @@
|
||||
import voicemeeterlib
|
||||
|
||||
import vmcompact
|
||||
|
||||
|
||||
def run():
|
||||
KIND_ID = 'potato'
|
||||
|
||||
with voicemeeterlib.api(KIND_ID) as vmr:
|
||||
app = vmcompact.connect(KIND_ID, vmr)
|
||||
app.mainloop()
|
||||
@ -19,8 +19,8 @@ class Menus(tk.Menu):
|
||||
self.parent = parent
|
||||
self.vmr = vmr
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
self.vban_config = get_configuration("vban")
|
||||
self.app_config = get_configuration("app")
|
||||
self.vban_config = get_configuration('vban')
|
||||
self.app_config = get_configuration('app')
|
||||
self._is_topmost = tk.BooleanVar()
|
||||
self._lock = tk.BooleanVar()
|
||||
self._unlock = tk.BooleanVar()
|
||||
@ -30,9 +30,9 @@ class Menus(tk.Menu):
|
||||
|
||||
# voicemeeter menu
|
||||
self.menu_voicemeeter = tk.Menu(self, tearoff=0)
|
||||
self.add_cascade(menu=self.menu_voicemeeter, label="Voicemeeter")
|
||||
self.add_cascade(menu=self.menu_voicemeeter, label='Voicemeeter')
|
||||
self.menu_voicemeeter.add_checkbutton(
|
||||
label="Always On Top",
|
||||
label='Always On Top',
|
||||
onvalue=1,
|
||||
offvalue=0,
|
||||
variable=self._is_topmost,
|
||||
@ -40,51 +40,51 @@ class Menus(tk.Menu):
|
||||
)
|
||||
self.menu_voicemeeter.add_separator()
|
||||
self.menu_voicemeeter.add_command(
|
||||
label="Show",
|
||||
label='Show',
|
||||
underline=0,
|
||||
command=partial(self.action_invoke_voicemeeter, "show"),
|
||||
command=partial(self.action_invoke_voicemeeter, 'show'),
|
||||
)
|
||||
self.menu_voicemeeter.add_command(
|
||||
label="Hide",
|
||||
label='Hide',
|
||||
underline=0,
|
||||
command=partial(self.action_invoke_voicemeeter, "hide"),
|
||||
command=partial(self.action_invoke_voicemeeter, 'hide'),
|
||||
)
|
||||
self.menu_voicemeeter.add_command(
|
||||
label="Restart",
|
||||
label='Restart',
|
||||
underline=0,
|
||||
command=partial(self.action_invoke_voicemeeter, "restart"),
|
||||
command=partial(self.action_invoke_voicemeeter, 'restart'),
|
||||
)
|
||||
self.menu_voicemeeter.add_command(
|
||||
label="Shutdown",
|
||||
label='Shutdown',
|
||||
underline=0,
|
||||
command=partial(self.action_invoke_voicemeeter, "shutdown"),
|
||||
command=partial(self.action_invoke_voicemeeter, 'shutdown'),
|
||||
)
|
||||
self.menu_voicemeeter.add_separator()
|
||||
self.menu_lock = tk.Menu(self.menu_voicemeeter, tearoff=0)
|
||||
self.menu_voicemeeter.add_cascade(
|
||||
menu=self.menu_lock, label="GUI Lock", underline=0
|
||||
menu=self.menu_lock, label='GUI Lock', underline=0
|
||||
)
|
||||
self.menu_lock.add_checkbutton(
|
||||
label="Lock",
|
||||
label='Lock',
|
||||
onvalue=1,
|
||||
offvalue=0,
|
||||
variable=self._lock,
|
||||
command=partial(self.action_set_voicemeeter, "lock"),
|
||||
command=partial(self.action_set_voicemeeter, 'lock'),
|
||||
)
|
||||
self.menu_lock.add_checkbutton(
|
||||
label="Unlock",
|
||||
label='Unlock',
|
||||
onvalue=1,
|
||||
offvalue=0,
|
||||
variable=self._unlock,
|
||||
command=partial(self.action_set_voicemeeter, "lock", False),
|
||||
command=partial(self.action_set_voicemeeter, 'lock', False),
|
||||
)
|
||||
|
||||
# configs menu
|
||||
self.menu_configs = tk.Menu(self, tearoff=0)
|
||||
self.add_cascade(menu=self.menu_configs, label="Configs")
|
||||
self.add_cascade(menu=self.menu_configs, label='Configs')
|
||||
self.menu_configs_load = tk.Menu(self.menu_configs, tearoff=0)
|
||||
self.menu_configs.add_cascade(menu=self.menu_configs_load, label="Load config")
|
||||
self.config_defaults = {"reset"}
|
||||
self.menu_configs.add_cascade(menu=self.menu_configs_load, label='Load config')
|
||||
self.config_defaults = {'reset'}
|
||||
if len(self.parent.userconfigs) > len(self.config_defaults) and all(
|
||||
key in self.parent.userconfigs for key in self.config_defaults
|
||||
):
|
||||
@ -96,22 +96,24 @@ class Menus(tk.Menu):
|
||||
if profile not in self.config_defaults
|
||||
]
|
||||
else:
|
||||
self.menu_configs.entryconfig(0, state="disabled")
|
||||
self.menu_configs.entryconfig(0, state='disabled')
|
||||
self.menu_configs.add_command(
|
||||
label="Reset to defaults", command=self.load_defaults
|
||||
label='Reset to defaults', command=self.load_defaults
|
||||
)
|
||||
|
||||
# layout menu
|
||||
self.menu_layout = tk.Menu(self, tearoff=0)
|
||||
self.add_cascade(menu=self.menu_layout, label="Layout")
|
||||
self.add_cascade(menu=self.menu_layout, label='Layout')
|
||||
# layout/submixes
|
||||
# here we build menu regardless of kind but disable if not potato
|
||||
buses = tuple(f"A{i+1}" for i in range(5)) + tuple(f"B{i+1}" for i in range(3))
|
||||
buses = tuple(f'A{i + 1}' for i in range(5)) + tuple(
|
||||
f'B{i + 1}' for i in range(3)
|
||||
)
|
||||
self.menu_submixes = tk.Menu(self.menu_layout, tearoff=0)
|
||||
self.menu_layout.add_cascade(menu=self.menu_submixes, label="Submixes")
|
||||
self.menu_layout.add_cascade(menu=self.menu_submixes, label='Submixes')
|
||||
[
|
||||
self.menu_submixes.add_checkbutton(
|
||||
label=f"Bus {buses[i]}",
|
||||
label=f'Bus {buses[i]}',
|
||||
underline=0,
|
||||
onvalue=1,
|
||||
offvalue=0,
|
||||
@ -121,94 +123,94 @@ class Menus(tk.Menu):
|
||||
for i in range(8)
|
||||
]
|
||||
self._selected_bus[_configuration.submixes].set(True)
|
||||
if self.parent.kind.name != "potato":
|
||||
self.menu_layout.entryconfig(0, state="disabled")
|
||||
if self.parent.kind.name != 'potato':
|
||||
self.menu_layout.entryconfig(0, state='disabled')
|
||||
# layout/extends
|
||||
self.menu_extends = tk.Menu(self.menu_layout, tearoff=0)
|
||||
self.menu_layout.add_cascade(
|
||||
menu=self.menu_extends, label="Extends", underline=0
|
||||
menu=self.menu_extends, label='Extends', underline=0
|
||||
)
|
||||
self.menu_extends.add_command(
|
||||
label="horizontal",
|
||||
label='horizontal',
|
||||
underline=0,
|
||||
command=partial(self.switch_orientation, extends_horizontal=True),
|
||||
)
|
||||
self.menu_extends.add_command(
|
||||
label="vertical",
|
||||
label='vertical',
|
||||
underline=0,
|
||||
command=partial(self.switch_orientation, extends_horizontal=False),
|
||||
)
|
||||
self.menu_extends.entryconfig(
|
||||
0 if _configuration.extends_horizontal else 1, state="disabled"
|
||||
0 if _configuration.extends_horizontal else 1, state='disabled'
|
||||
)
|
||||
# layout/themes
|
||||
self.menu_themes = tk.Menu(self.menu_layout, tearoff=0)
|
||||
self.menu_layout.add_cascade(menu=self.menu_themes, label="Themes")
|
||||
self.menu_layout.add_cascade(menu=self.menu_themes, label='Themes')
|
||||
self.menu_themes.add_command(
|
||||
label="light", command=partial(self.load_theme, "light")
|
||||
label='light', command=partial(self.load_theme, 'light')
|
||||
)
|
||||
self.menu_themes.add_command(
|
||||
label="dark", command=partial(self.load_theme, "dark")
|
||||
label='dark', command=partial(self.load_theme, 'dark')
|
||||
)
|
||||
self.menu_themes.entryconfig(
|
||||
0 if self.app_config["theme"]["mode"] == "light" else 1,
|
||||
state="disabled",
|
||||
0 if self.app_config['theme']['mode'] == 'light' else 1,
|
||||
state='disabled',
|
||||
)
|
||||
if not _configuration.themes_enabled:
|
||||
self.menu_layout.entryconfig(2, state="disabled")
|
||||
self.menu_layout.entryconfig(2, state='disabled')
|
||||
# layout/navigation
|
||||
self.menu_navigation = tk.Menu(self.menu_layout, tearoff=0)
|
||||
self.menu_layout.add_cascade(menu=self.menu_navigation, label="Navigation")
|
||||
self.menu_layout.add_cascade(menu=self.menu_navigation, label='Navigation')
|
||||
self.menu_navigation.add_checkbutton(
|
||||
label="show",
|
||||
label='show',
|
||||
onvalue=1,
|
||||
offvalue=0,
|
||||
variable=self._navigation_show,
|
||||
command=partial(self.toggle_navigation, "show"),
|
||||
command=partial(self.toggle_navigation, 'show'),
|
||||
)
|
||||
self.menu_navigation.add_checkbutton(
|
||||
label="hide",
|
||||
label='hide',
|
||||
onvalue=1,
|
||||
offvalue=0,
|
||||
variable=self._navigation_hide,
|
||||
command=partial(self.toggle_navigation, "hide"),
|
||||
command=partial(self.toggle_navigation, 'hide'),
|
||||
)
|
||||
|
||||
# vban connect menu
|
||||
self.menu_vban = tk.Menu(self, tearoff=0)
|
||||
self.add_cascade(menu=self.menu_vban, label="VBAN")
|
||||
self.add_cascade(menu=self.menu_vban, label='VBAN')
|
||||
if self.vban_config:
|
||||
for i, _ in enumerate(self.vban_config):
|
||||
setattr(self, f"menu_vban_{i+1}", tk.Menu(self.menu_vban, tearoff=0))
|
||||
target_menu = getattr(self, f"menu_vban_{i+1}")
|
||||
setattr(self, f'menu_vban_{i + 1}', tk.Menu(self.menu_vban, tearoff=0))
|
||||
target_menu = getattr(self, f'menu_vban_{i + 1}')
|
||||
self.menu_vban.add_cascade(
|
||||
menu=target_menu,
|
||||
label=f"{self.vban_config[f'connection-{i+1}']['streamname']}",
|
||||
label=f'{self.vban_config[f"connection-{i + 1}"]["streamname"]}',
|
||||
underline=0,
|
||||
)
|
||||
target_menu.add_command(
|
||||
label="Connect", command=partial(self.vban_connect, i)
|
||||
label='Connect', command=partial(self.vban_connect, i)
|
||||
)
|
||||
target_menu.add_command(
|
||||
label="Disconnect", command=partial(self.vban_disconnect, i)
|
||||
label='Disconnect', command=partial(self.vban_disconnect, i)
|
||||
)
|
||||
target_menu.entryconfig(1, state="disabled")
|
||||
target_menu.entryconfig(1, state='disabled')
|
||||
else:
|
||||
self.entryconfig(4, state="disabled")
|
||||
self.entryconfig(4, state='disabled')
|
||||
|
||||
# Help menu
|
||||
self.menu_help = tk.Menu(self, tearoff=0)
|
||||
self.add_cascade(menu=self.menu_help, label="Help")
|
||||
self.add_cascade(menu=self.menu_help, label='Help')
|
||||
self.menu_help.add_command(
|
||||
label="Voicemeeter Site",
|
||||
label='Voicemeeter Site',
|
||||
command=self.documentation,
|
||||
)
|
||||
self.menu_help.add_command(
|
||||
label="Source Code",
|
||||
label='Source Code',
|
||||
command=self.github,
|
||||
)
|
||||
self.menu_help.add_command(
|
||||
label="App Creator",
|
||||
label='App Creator',
|
||||
command=self.onyxandiris,
|
||||
)
|
||||
|
||||
@ -219,52 +221,56 @@ class Menus(tk.Menu):
|
||||
|
||||
def enable_vban_menus(self):
|
||||
[
|
||||
self.menu_vban.entryconfig(j, state="normal")
|
||||
self.menu_vban.entryconfig(j, state='normal')
|
||||
for j, _ in enumerate(self.menu_vban.winfo_children())
|
||||
]
|
||||
|
||||
def action_invoke_voicemeeter(self, cmd):
|
||||
if fn := getattr(self.target.command, cmd):
|
||||
fn()
|
||||
if cmd == "shutdown":
|
||||
if cmd == 'shutdown':
|
||||
self.parent.on_close_window()
|
||||
|
||||
def action_set_voicemeeter(self, cmd, val=True):
|
||||
if cmd == "lock":
|
||||
if cmd == 'lock':
|
||||
self._lock.set(val)
|
||||
self._unlock.set(not self._lock.get())
|
||||
setattr(self.target.command, cmd, val)
|
||||
|
||||
def load_custom_profile(self, profile):
|
||||
self.logger.info(f"loading user profile {profile}")
|
||||
self.logger.info(f'loading user profile {profile}')
|
||||
self.target.apply(profile)
|
||||
if not _base_values.run_update:
|
||||
self.parent.subject.notify('pdirty')
|
||||
|
||||
def load_profile(self, profile):
|
||||
self.logger.info(f"loading user profile {profile}")
|
||||
self.logger.info(f'loading user profile {profile}')
|
||||
self.target.apply_config(profile)
|
||||
if not _base_values.run_update:
|
||||
self.parent.subject.notify('pdirty')
|
||||
|
||||
def load_defaults(self):
|
||||
msg = (
|
||||
"Are you sure you want to Reset values to defaults?",
|
||||
"Physical strips B1, Virtual strips A1",
|
||||
"Mono, Solo, Mute, EQ all OFF",
|
||||
"Gain sliders for Strip/Bus at 0.0",
|
||||
'Are you sure you want to Reset values to defaults?',
|
||||
'Physical strips B1, Virtual strips A1',
|
||||
'Mono, Solo, Mute, EQ all OFF',
|
||||
'Gain sliders for Strip/Bus at 0.0',
|
||||
)
|
||||
resp = messagebox.askyesno(message="\n".join(msg))
|
||||
resp = messagebox.askyesno(message='\n'.join(msg))
|
||||
if resp:
|
||||
self.load_profile("reset")
|
||||
self.load_profile('reset')
|
||||
|
||||
def always_on_top(self):
|
||||
self.parent.attributes("-topmost", self._is_topmost.get())
|
||||
self.parent.attributes('-topmost', self._is_topmost.get())
|
||||
|
||||
def switch_orientation(self, extends_horizontal: bool = True, *args):
|
||||
_configuration.extends_horizontal = extends_horizontal
|
||||
if extends_horizontal:
|
||||
self.menu_extends.entryconfig(0, state="disabled")
|
||||
self.menu_extends.entryconfig(1, state="normal")
|
||||
self.menu_extends.entryconfig(0, state='disabled')
|
||||
self.menu_extends.entryconfig(1, state='normal')
|
||||
else:
|
||||
self.menu_extends.entryconfig(1, state="disabled")
|
||||
self.menu_extends.entryconfig(0, state="normal")
|
||||
self.menu_extends.entryconfig(1, state='disabled')
|
||||
self.menu_extends.entryconfig(0, state='normal')
|
||||
|
||||
def set_submix(self, i):
|
||||
if _configuration.submixes != i:
|
||||
@ -274,38 +280,38 @@ class Menus(tk.Menu):
|
||||
self.parent.nav_frame.show_submix()
|
||||
for j, var in enumerate(self._selected_bus):
|
||||
var.set(i == j)
|
||||
self.parent.subject.notify("submix")
|
||||
self.parent.subject.notify('submix')
|
||||
|
||||
def load_theme(self, theme):
|
||||
sv_ttk.set_theme(theme)
|
||||
_configuration.theme_mode = theme
|
||||
self.menu_themes.entryconfig(
|
||||
0,
|
||||
state=f"{'disabled' if theme == 'light' else 'normal'}",
|
||||
state=f'{"disabled" if theme == "light" else "normal"}',
|
||||
)
|
||||
self.menu_themes.entryconfig(
|
||||
1,
|
||||
state=f"{'disabled' if theme == 'dark' else 'normal'}",
|
||||
state=f'{"disabled" if theme == "dark" else "normal"}',
|
||||
)
|
||||
[
|
||||
menu.config(bg=f"{'black' if theme == 'dark' else 'white'}")
|
||||
menu.config(bg=f'{"black" if theme == "dark" else "white"}')
|
||||
for menu in self.winfo_children()
|
||||
if isinstance(menu, tk.Menu)
|
||||
]
|
||||
self.menu_lock.config(bg=f"{'black' if theme == 'dark' else 'white'}")
|
||||
self.menu_configs_load.config(bg=f"{'black' if theme == 'dark' else 'white'}")
|
||||
self.menu_lock.config(bg=f'{"black" if theme == "dark" else "white"}')
|
||||
self.menu_configs_load.config(bg=f'{"black" if theme == "dark" else "white"}')
|
||||
[
|
||||
menu.config(bg=f"{'black' if theme == 'dark' else 'white'}")
|
||||
menu.config(bg=f'{"black" if theme == "dark" else "white"}')
|
||||
for menu in self.menu_vban.winfo_children()
|
||||
if isinstance(menu, tk.Menu)
|
||||
]
|
||||
[
|
||||
menu.config(bg=f"{'black' if theme == 'dark' else 'white'}")
|
||||
menu.config(bg=f'{"black" if theme == "dark" else "white"}')
|
||||
for menu in self.menu_layout.winfo_children()
|
||||
if isinstance(menu, tk.Menu)
|
||||
]
|
||||
self.logger.info(
|
||||
f"Finished loading theme Sunvalley {sv_ttk.get_theme().capitalize()} theme"
|
||||
f'Finished loading theme Sunvalley {sv_ttk.get_theme().capitalize()} theme'
|
||||
)
|
||||
|
||||
def menu_teardown(self, i):
|
||||
@ -316,10 +322,10 @@ class Menus(tk.Menu):
|
||||
try:
|
||||
self.menu_configs_load.delete(profile)
|
||||
except tk._tkinter.tclError as e:
|
||||
self.logger.warning(f"{type(e).__name__}: {e}")
|
||||
self.logger.warning(f'{type(e).__name__}: {e}')
|
||||
|
||||
[
|
||||
self.menu_vban.entryconfig(j, state="disabled")
|
||||
self.menu_vban.entryconfig(j, state='disabled')
|
||||
for j, _ in enumerate(self.menu_vban.winfo_children())
|
||||
if j != i
|
||||
]
|
||||
@ -331,44 +337,46 @@ class Menus(tk.Menu):
|
||||
self.menu_configs_load.add_command(
|
||||
label=profile, command=partial(self.load_profile, profile)
|
||||
)
|
||||
self.menu_configs.entryconfig(0, state="normal")
|
||||
self.menu_configs.entryconfig(0, state='normal')
|
||||
else:
|
||||
self.menu_configs.entryconfig(0, state="disabled")
|
||||
self.menu_configs.entryconfig(0, state='disabled')
|
||||
|
||||
def toggle_navigation(self, cmd=None):
|
||||
if cmd == "show":
|
||||
self.logger.debug("show navframe")
|
||||
if cmd == 'show':
|
||||
self.logger.debug('show navframe')
|
||||
self.parent.nav_frame.grid()
|
||||
self._navigation_show.set(True)
|
||||
self._navigation_hide.set(not self._navigation_show.get())
|
||||
else:
|
||||
self.logger.debug("hide navframe")
|
||||
self.logger.debug('hide navframe')
|
||||
self.parent.nav_frame.grid_remove()
|
||||
self._navigation_hide.set(True)
|
||||
self._navigation_show.set(not self._navigation_hide.get())
|
||||
|
||||
def vban_connect(self, i):
|
||||
opts = {}
|
||||
opts |= self.vban_config[f"connection-{i+1}"]
|
||||
kind_id = opts.pop("kind")
|
||||
opts |= self.vban_config[f'connection-{i + 1}']
|
||||
kind_id = opts.pop('kind')
|
||||
if 'ip' in opts:
|
||||
opts['host'] = opts.pop('ip')
|
||||
self.vban = vban_cmd.api(kind_id, **opts)
|
||||
# login to vban interface
|
||||
try:
|
||||
self.logger.info(f"Attempting vban connection to {opts.get('ip')}")
|
||||
self.logger.info(f'Attempting vban connection to {opts.get("host")}')
|
||||
self.vban.login()
|
||||
except VBANCMDConnectionError as e:
|
||||
self.vban.logout()
|
||||
msg = (
|
||||
f"Timeout attempting to establish connection to {opts.get('ip')}",
|
||||
f"Please check your connection settings",
|
||||
f'Timeout attempting to establish connection to {opts.get("host")}',
|
||||
'Please check your connection settings',
|
||||
)
|
||||
messagebox.showerror("Connection Error", "\n".join(msg))
|
||||
msg = (str(e), f"resuming local connection")
|
||||
self.logger.error(", ".join(msg))
|
||||
messagebox.showerror('Connection Error', '\n'.join(msg))
|
||||
msg = (str(e), 'resuming local connection')
|
||||
self.logger.error(', '.join(msg))
|
||||
self.after(1, self.enable_vban_menus)
|
||||
return
|
||||
self.menu_teardown(i)
|
||||
self.vban.event.add(["pdirty", "ldirty"])
|
||||
self.vban.event.add(['pdirty', 'ldirty'])
|
||||
# destroy the current App frames
|
||||
self.parent._destroy_top_level_frames()
|
||||
_base_values.vban_connected = True
|
||||
@ -377,17 +385,17 @@ class Menus(tk.Menu):
|
||||
# build new app frames according to a kind
|
||||
kind = kind_get(kind_id)
|
||||
self.parent.build_app(kind, self.vban)
|
||||
target_menu = getattr(self, f"menu_vban_{i+1}")
|
||||
target_menu.entryconfig(0, state="disabled")
|
||||
target_menu.entryconfig(1, state="normal")
|
||||
target_menu = getattr(self, f'menu_vban_{i + 1}')
|
||||
target_menu.entryconfig(0, state='disabled')
|
||||
target_menu.entryconfig(1, state='normal')
|
||||
self.menu_layout.entryconfig(
|
||||
0, state=f"{'normal' if kind.name == 'potato' else 'disabled'}"
|
||||
0, state=f'{"normal" if kind.name == "potato" else "disabled"}'
|
||||
)
|
||||
# ensure the configs are reloaded into memory
|
||||
if "config" in self.parent.target.__dict__:
|
||||
del self.parent.target.__dict__["config"]
|
||||
if "userconfigs" in self.parent.__dict__:
|
||||
del self.parent.__dict__["userconfigs"]
|
||||
if 'config' in self.parent.target.__dict__:
|
||||
del self.parent.target.__dict__['config']
|
||||
if 'userconfigs' in self.parent.__dict__:
|
||||
del self.parent.__dict__['userconfigs']
|
||||
self.menu_setup()
|
||||
|
||||
def vban_disconnect(self, i):
|
||||
@ -402,26 +410,26 @@ class Menus(tk.Menu):
|
||||
# build new app frames according to a kind
|
||||
kind = kind_get(self.vmr.type)
|
||||
self.parent.build_app(kind)
|
||||
target_menu = getattr(self, f"menu_vban_{i+1}")
|
||||
target_menu.entryconfig(0, state="normal")
|
||||
target_menu.entryconfig(1, state="disabled")
|
||||
target_menu = getattr(self, f'menu_vban_{i + 1}')
|
||||
target_menu.entryconfig(0, state='normal')
|
||||
target_menu.entryconfig(1, state='disabled')
|
||||
self.menu_layout.entryconfig(
|
||||
0, state=f"{'normal' if kind.name == 'potato' else 'disabled'}"
|
||||
0, state=f'{"normal" if kind.name == "potato" else "disabled"}'
|
||||
)
|
||||
# ensure the configs are reloaded into memory
|
||||
if "config" in self.parent.target.__dict__:
|
||||
del self.parent.target.__dict__["config"]
|
||||
if "userconfigs" in self.parent.__dict__:
|
||||
del self.parent.__dict__["userconfigs"]
|
||||
if 'config' in self.parent.target.__dict__:
|
||||
del self.parent.target.__dict__['config']
|
||||
if 'userconfigs' in self.parent.__dict__:
|
||||
del self.parent.__dict__['userconfigs']
|
||||
self.menu_setup()
|
||||
|
||||
self.after(15000, self.enable_vban_menus)
|
||||
self.after(50, self.enable_vban_menus)
|
||||
|
||||
def documentation(self):
|
||||
webbrowser.open_new(r"https://voicemeeter.com/")
|
||||
webbrowser.open_new(r'https://voicemeeter.com/')
|
||||
|
||||
def github(self):
|
||||
webbrowser.open_new(r"https://github.com/onyx-and-iris/voicemeeter-compact")
|
||||
webbrowser.open_new(r'https://github.com/onyx-and-iris/voicemeeter-compact')
|
||||
|
||||
def onyxandiris(self):
|
||||
webbrowser.open_new(r"https://onyxandiris.online")
|
||||
webbrowser.open_new(r'https://onyxandiris.online')
|
||||
|
||||
@ -33,7 +33,7 @@ class Navigation(ttk.Frame):
|
||||
if self.submix.get():
|
||||
self.parent.submix_frame = SubMixFrame(self.parent)
|
||||
self.logger.info(
|
||||
f"Finished building submixframe for submix {_configuration.submixes}"
|
||||
f'Finished building submixframe for submix {_configuration.submixes}'
|
||||
)
|
||||
else:
|
||||
if _configuration.extends_horizontal:
|
||||
@ -49,51 +49,51 @@ class Navigation(ttk.Frame):
|
||||
else:
|
||||
self.parent.rowconfigure(2, weight=0, minsize=0)
|
||||
self.logger.info(
|
||||
f"Finished tearing down submixframe for submix {_configuration.submixes}"
|
||||
f'Finished tearing down submixframe for submix {_configuration.submixes}'
|
||||
)
|
||||
|
||||
if not _configuration.themes_enabled:
|
||||
self.styletable.configure(
|
||||
f"Submix.TButton",
|
||||
'Submix.TButton',
|
||||
background=f'{"purple" if self.submix.get() else "white"}',
|
||||
)
|
||||
|
||||
def switch_channel(self):
|
||||
if self.channel_text.get() == "STRIP":
|
||||
self.mainframebuilder.create_channelframe("bus")
|
||||
if self.channel_text.get() == 'STRIP':
|
||||
self.mainframebuilder.create_channelframe('bus')
|
||||
self.parent.strip_frame.teardown()
|
||||
else:
|
||||
self.mainframebuilder.create_channelframe("strip")
|
||||
self.mainframebuilder.create_channelframe('strip')
|
||||
self.parent.bus_frame.teardown()
|
||||
|
||||
self.extend_button["state"] = (
|
||||
"disabled" if self.channel_text.get() == "STRIP" else "normal"
|
||||
self.extend_button['state'] = (
|
||||
'disabled' if self.channel_text.get() == 'STRIP' else 'normal'
|
||||
)
|
||||
[frame.teardown() for frame in self.parent.configframes]
|
||||
self.channel_text.set("BUS" if self.channel_text.get() == "STRIP" else "STRIP")
|
||||
self.channel_text.set('BUS' if self.channel_text.get() == 'STRIP' else 'STRIP')
|
||||
|
||||
def extend_frame(self):
|
||||
_configuration.extended = self.extend.get()
|
||||
if self.extend.get():
|
||||
self.channel_button["state"] = "disabled"
|
||||
self.mainframebuilder.create_channelframe("bus")
|
||||
self.channel_button['state'] = 'disabled'
|
||||
self.mainframebuilder.create_channelframe('bus')
|
||||
else:
|
||||
[
|
||||
frame.teardown()
|
||||
for frame in self.parent.configframes
|
||||
if "!busconfig" in str(frame)
|
||||
if '!busconfig' in str(frame)
|
||||
]
|
||||
self.parent.bus_frame.teardown()
|
||||
self.parent.bus_frame = None
|
||||
self.channel_button["state"] = "normal"
|
||||
self.channel_button['state'] = 'normal'
|
||||
|
||||
if self.parent.submix_frame:
|
||||
self.parent.submix_frame.teardown()
|
||||
self.submix.set(False)
|
||||
if not _configuration.themes_enabled:
|
||||
self.styletable.configure(
|
||||
f"Submix.TButton",
|
||||
'Submix.TButton',
|
||||
background=f'{"purple" if self.submix.get() else "white"}',
|
||||
)
|
||||
|
||||
self.extend_text.set("REDUCE" if self.extend.get() else "EXTEND")
|
||||
self.extend_text.set('REDUCE' if self.extend.get() else 'EXTEND')
|
||||
|
||||
34
vmcompact/util.py
Normal file
34
vmcompact/util.py
Normal file
@ -0,0 +1,34 @@
|
||||
def get_busmode_fullnames(kind) -> dict:
|
||||
if kind.name == 'basic':
|
||||
return {
|
||||
'normal': 'Normal',
|
||||
'amix': 'Mix Down A',
|
||||
'repeat': 'Stereo Repeat',
|
||||
'composite': 'Composite',
|
||||
}
|
||||
return {
|
||||
'normal': 'Normal',
|
||||
'amix': 'Mix Down A',
|
||||
'bmix': 'Mix Down B',
|
||||
'repeat': 'Stereo Repeat',
|
||||
'composite': 'Composite',
|
||||
'tvmix': 'Up Mix TV',
|
||||
'upmix21': 'Up Mix 2.1',
|
||||
'upmix41': 'Up Mix 4.1',
|
||||
'upmix61': 'Up Mix 6.1',
|
||||
'centeronly': 'Center Only',
|
||||
'lfeonly': 'LFE Only',
|
||||
'rearonly': 'Rear Only',
|
||||
}
|
||||
|
||||
|
||||
def get_busmode_fullnames_reversed(kind) -> dict:
|
||||
return {v: k for k, v in get_busmode_fullnames(kind).items()}
|
||||
|
||||
|
||||
def get_busmode_shortnames(kind) -> list:
|
||||
return list(get_busmode_fullnames(kind).keys())
|
||||
|
||||
|
||||
def get_busmono_modes() -> list:
|
||||
return ['Mono: off', 'Mono: on', 'Stereo Reverse']
|
||||
Loading…
x
Reference in New Issue
Block a user