Compare commits

..

No commits in common. "main" and "v1.6.0" have entirely different histories.
main ... v1.6.0

47 changed files with 694 additions and 2975 deletions

View File

@ -1,53 +0,0 @@
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

View File

@ -1,341 +0,0 @@
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 }}

View File

@ -1,19 +0,0 @@
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
View File

@ -1,9 +1,9 @@
# Generated by ignr: github.com/onyx-and-iris/ignr # quick test
quick.py
## Python ##
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[codz] *.py[cod]
*$py.class *$py.class
# C extensions # C extensions
@ -23,6 +23,7 @@ parts/
sdist/ sdist/
var/ var/
wheels/ wheels/
pip-wheel-metadata/
share/python-wheels/ share/python-wheels/
*.egg-info/ *.egg-info/
.installed.cfg .installed.cfg
@ -49,10 +50,9 @@ htmlcov/
nosetests.xml nosetests.xml
coverage.xml coverage.xml
*.cover *.cover
*.py.cover *.py,cover
.hypothesis/ .hypothesis/
.pytest_cache/ .pytest_cache/
cover/
# Translations # Translations
*.mo *.mo
@ -75,7 +75,6 @@ instance/
docs/_build/ docs/_build/
# PyBuilder # PyBuilder
.pybuilder/
target/ target/
# Jupyter Notebook # Jupyter Notebook
@ -86,9 +85,7 @@ profile_default/
ipython_config.py ipython_config.py
# pyenv # pyenv
# For a library or package, you might want to ignore these files since the code is .python-version
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv # pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
@ -97,37 +94,7 @@ ipython_config.py
# install all needed dependencies. # install all needed dependencies.
#Pipfile.lock #Pipfile.lock
# UV # PEP 582; used by e.g. github.com/David-OConnor/pyflow
# 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__/ __pypackages__/
# Celery stuff # Celery stuff
@ -139,13 +106,13 @@ celerybeat.pid
# Environments # Environments
.env .env
.envrc
.venv .venv
env/ env/
venv/ venv/
ENV/ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
venv_vmcompact/
# Spyder project settings # Spyder project settings
.spyderproject .spyderproject
@ -164,66 +131,3 @@ dmypy.json
# Pyre type checker # Pyre type checker
.pyre/ .pyre/
# 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

View File

@ -1,13 +0,0 @@
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

View File

@ -7,93 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
- [ ] - [ ] Add support for forest theme (should be coming soon)
## [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
### Added
- Should the voicemeeter-compact app lose communication with Voicemeeter GUI a popup will show asking to restart the GUI.
- If yes is selected the app's mainframe will redraw, there will be a grace period before updates start again due to Voicemeeter engine startup.
### Fixed
- From the menu, Voicemeeter->Shutdown now closes both the compact app and the main Voicemeeter GUI.
## [1.8.0] - 2023-06-29
### Added
- Ability to toggle the navigation frame. This may also be set in app.toml, check example config.
### Changed
- xpadding added to channel labelframes. This may also be configured through app.toml.
- During startup of the app there is now a twelve second grace period before parameter updates begin if the GUI was not previously launched. This is aimed at removing the stutter (due to VM engine startup) on initial launch. Be mindful of this if changing settings on the base Voicemeeter app. After the grace period all updates continue as normal.
- dependency updates:
- sv_ttk updated to v2.5.1.
- voicemeeter-api updated to v2.0.2.
## [1.7.0] - 2023-06-26
### Changed
- There are changes to how some parameters must be set in user toml configs.
- use `comp.knob` to set a strip comp slider.
- use `gate.knob` to set a strip gate slider.
- use `eq.on` to set a bus eq.on button.
- use `eq.ab` to set a bus eq.ab button.
Check example configs.
- `configs` directory may now be located in one of the following locations:
- \<current working directory>/configs/
- \<user home directory>/.configs/vm-compact/configs/
- \<user home directory>/Documents/Voicemeeter/configs/
- dependency updates:
- sv_ttk updated to v2.4.5.
- voicemeeter-api updated to v2.0.1.
- vban-cmd updated to v2.0.0.
### Fixed
- A number of changes that reduce the amount of api calls being made.
## [1.6.0] - 2022-09-29 ## [1.6.0] - 2022-09-29

View File

@ -1,7 +1,6 @@
[![PyPI version](https://badge.fury.io/py/voicemeeter-compact.svg)](https://badge.fury.io/py/voicemeeter-compact) [![PyPI version](https://badge.fury.io/py/voicemeeter-compact.svg)](https://badge.fury.io/py/voicemeeter-compact)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/onyx-and-iris/voicemeeter-compact/blob/main/LICENSE) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/onyx-and-iris/voicemeeter-compact/blob/main/LICENSE)
[![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
![OS: Windows](https://img.shields.io/badge/os-windows-red) ![OS: Windows](https://img.shields.io/badge/os-windows-red)
![Image of app/potato size comparison](./doc_imgs/potatocomparisonsmaller.png) ![Image of app/potato size comparison](./doc_imgs/potatocomparisonsmaller.png)
@ -31,21 +30,20 @@ Example `__main__.py` file:
```python ```python
import voicemeeterlib import voicemeeterlib
import vmcompact import vmcompact
def main(): def main():
# choose the kind of Voicemeeter (Local connection) # pass the kind_id and the vm object to the app
KIND_ID = 'banana' with voicemeeterlib.api(kind_id) as vm:
app = vmcompact.connect(kind_id, vm)
# pass the KIND_ID and the vm object to the app
with voicemeeterlib.api(KIND_ID) as vm:
app = vmcompact.connect(KIND_ID, vm)
app.mainloop() app.mainloop()
if __name__ == '__main__': if __name__ == "__main__":
# choose the kind of Voicemeeter (Local connection)
kind_id = "banana"
main() main()
``` ```
@ -55,9 +53,9 @@ It's important to know that only labelled strips and buses will appear in the Ch
If the GUI looks like the above when you first load it, then no channels are labelled. From the menu, `Configs->Load config` you may load an example config. Save your current Voicemeeter settings first :). If the GUI looks like the above when you first load it, then no channels are labelled. From the menu, `Configs->Load config` you may load an example config. Save your current Voicemeeter settings first :).
### KIND_ID ### kind_id
Set the kind of Voicemeeter, KIND_ID may be: Set the kind of Voicemeeter, kind_id may be:
- `basic` - `basic`
- `banana` - `banana`
@ -65,18 +63,15 @@ Set the kind of Voicemeeter, KIND_ID may be:
## TOML Files ## TOML Files
If you've downloaded the binary from [Releases][releases] you can find configs included in the `_internal/configs` directory. 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.
You may override these configs by placing a directory `vm-compact` in one of the following locations: Inside each kind directory you may place as many custom toml configurations as you wish.
- `user home directory / .config`
- `user home directory / Documents / Voicemeeter`
The contents should match the following directory structure:
. .
├── vm-compact ├── `__main__.py`
├── configs
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;├── app.toml &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;├── app.toml
@ -114,7 +109,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. Configure a user config to load on app startup. Don't include the .toml extension in the config name.
- `theme` - `theme`
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. 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.
- `extends` - `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. 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.
@ -139,13 +134,13 @@ A valid `vban.toml` might look like this:
```toml ```toml
[connection-1] [connection-1]
kind = 'banana' kind = 'banana'
host = '192.168.1.2' ip = '192.168.1.2'
streamname = 'worklaptop' streamname = 'worklaptop'
port = 6980 port = 6980
[connection-2] [connection-2]
kind = 'potato' kind = 'potato'
host = '192.168.1.3' ip = '192.168.1.3'
streamname = 'streampc' streamname = 'streampc'
port = 6990 port = 6990
``` ```
@ -154,7 +149,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: 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 the Voicemeeter API](https://github.com/onyx-and-iris/voicemeeter-api-python#multiple-parameters) [Python Interface for 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) [Python Interface for VBAN CMD](https://github.com/onyx-and-iris/vban-cmd-python#multiple-parameters)
@ -162,10 +157,6 @@ User configs may be loaded at any time via the menu.
## Special Thanks ## Special Thanks
[Vincent Burel](https://github.com/vburel2018) for creating Voicemeeter and its SDK. [Vincent Burel](https://github.com/vburel2018) for creating Voicemeeter, its SDK, the C Remote API, the RT Packet service and Streamer View app!
[Rdbende](https://github.com/rdbende) for creating the beautiful [Sun Valley theme][sv-theme]. [Rdbende](https://github.com/rdbende) for creating the beautiful Sun Valley Tkinter theme and adding it to Pypi!
[sv-theme]: https://github.com/rdbende/Sun-Valley-ttk-theme
[releases]: https://github.com/onyx-and-iris/voicemeeter-compact/releases

View File

@ -1,38 +0,0 @@
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"

View File

@ -1,84 +0,0 @@
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"

View File

@ -1,38 +0,0 @@
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"

View File

@ -1,24 +0,0 @@
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"

View File

@ -1,61 +0,0 @@
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'

View File

@ -4,12 +4,12 @@ import vmcompact
def main(): def main():
KIND_ID = 'banana' with voicemeeterlib.api(kind_id) as vmr:
app = vmcompact.connect(kind_id, vmr)
with voicemeeterlib.api(KIND_ID) as vmr:
app = vmcompact.connect(KIND_ID, vmr)
app.mainloop() app.mainloop()
if __name__ == '__main__': if __name__ == "__main__":
kind_id = "banana"
main() main()

View File

@ -1,24 +1,21 @@
# load a specific profile on start (file name without .toml ext) # load a specific profile on start (file name without .toml ext)
# [configs] # [configs]
# config="example" # config="example"
# load with themes enabled? # load with themes enabled? set the default mode
[theme] [theme]
enabled = true enabled=true
mode="light"
# load in extended mode? if so which orientation # load in extended mode? if so which orientation
[extends] [extends]
extended = true extended=true
extends_horizontal = true extends_horizontal=true
# default dimensions for channel label frames # default dimensions for channel label frames
[channel] [channel]
width = 80 width=80
height = 130 height=130
xpadding = 2
# size of a single mouse wheel scroll step # size of a single mouse wheel scroll step
[mwscroll_step] [mwscroll_step]
size = 3 size=3
# default submix bus # default submix bus
[submixes] [submixes]
default = 0 default=0
# show the navigation frame?
[navigation]
show = false

View File

@ -2,12 +2,12 @@
label = "PhysStrip0" label = "PhysStrip0"
A1 = true A1 = true
gain = -8.8 gain = -8.8
comp.knob = 3.2 comp = 3.2
[strip-1] [strip-1]
label = "PhysStrip1" label = "PhysStrip1"
B1 = true B1 = true
gate.knob = 4.1 gate = 4.1
[strip-2] [strip-2]
label = "PhysStrip2" label = "PhysStrip2"
@ -34,12 +34,12 @@ mono = true
[bus-2] [bus-2]
label = "PhysBus2" label = "PhysBus2"
eq.on = true eq = true
mode = "composite" mode = "composite"
[bus-3] [bus-3]
label = "VirtBus0" label = "VirtBus0"
eq.ab = true eq_ab = true
mode = "upmix61" mode = "upmix61"
[bus-4] [bus-4]

View File

@ -2,12 +2,12 @@
label = "PhysStrip0" label = "PhysStrip0"
A1 = true A1 = true
gain = -8.8 gain = -8.8
comp.knob = 3.2 comp = 3.2
[strip-1] [strip-1]
label = "PhysStrip1" label = "PhysStrip1"
B1 = true B1 = true
gate.knob = 4.1 gate = 4.1
[strip-2] [strip-2]
label = "PhysStrip2" label = "PhysStrip2"
@ -50,7 +50,7 @@ mono = true
[bus-2] [bus-2]
label = "PhysBus2" label = "PhysBus2"
eq.on = true eq = true
[bus-3] [bus-3]
label = "PhysBus3" label = "PhysBus3"
@ -62,7 +62,7 @@ mode = "composite"
[bus-5] [bus-5]
label = "VirtBus0" label = "VirtBus0"
eq.ab = true eq_ab = true
[bus-6] [bus-6]
label = "VirtBus1" label = "VirtBus1"

View File

@ -2,12 +2,12 @@
### set the ip then uncomment ### set the ip then uncomment
# [connection-1] # [connection-1]
# kind = 'banana' # kind = 'banana'
# ip = 'localhost' # ip = '<ip address 1>'
# streamname = 'Command1' # streamname = 'Command1'
# port = 6980 # port = 6990
# [connection-2] # [connection-2]
# kind = 'potato' # kind = 'potato'
# ip = 'gamepc.local' # ip = '<ip address 2>'
# streamname = 'Command1' # streamname = 'Command1'
# port = 6980 # port = 6990

303
poetry.lock generated
View File

@ -1,261 +1,122 @@
# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand.
[[package]] [[package]]
name = "altgraph" name = "black"
version = "0.17.4" version = "22.8.0"
description = "Python graph (network) package" description = "The uncompromising code formatter."
category = "dev"
optional = false optional = false
python-versions = "*" python-versions = ">=3.6.2"
groups = ["build"]
files = [
{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] [package.dependencies]
altgraph = ">=0.17" click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
[[package]] pathspec = ">=0.9.0"
name = "packaging" platformdirs = ">=2"
version = "24.2" tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
groups = ["build"]
files = [
{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]
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] [package.extras]
completion = ["argcomplete"] colorama = ["colorama (>=0.4.3)"]
hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]] [[package]]
name = "pyinstaller-hooks-contrib" name = "click"
version = "2024.11" version = "8.1.3"
description = "Community maintained hooks for PyInstaller" description = "Composable command line interface toolkit"
optional = false category = "dev"
python-versions = ">=3.8"
groups = ["build"]
files = [
{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 = "ruff"
version = "0.9.1"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["dev"]
files = [ [package.dependencies]
{file = "ruff-0.9.1-py3-none-linux_armv6l.whl", hash = "sha256:84330dda7abcc270e6055551aca93fdde1b0685fc4fd358f26410f9349cf1743"}, colorama = {version = "*", markers = "platform_system == \"Windows\""}
{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]] [[package]]
name = "setuptools" name = "colorama"
version = "78.1.1" version = "0.4.5"
description = "Easily download, build, install, upgrade, and uninstall Python packages" description = "Cross-platform colored terminal text."
category = "dev"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
groups = ["build"]
files = [ [[package]]
{file = "setuptools-78.1.1-py3-none-any.whl", hash = "sha256:c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561"}, name = "mypy-extensions"
{file = "setuptools-78.1.1.tar.gz", hash = "sha256:fcc17fd9cd898242f6b4adfaca46137a9edef687f43e6f78469692a5e70d851d"}, version = "0.4.3"
] description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "pathspec"
version = "0.10.1"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = ">=3.7"
[[package]]
name = "platformdirs"
version = "2.5.2"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras] [package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
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)"] test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
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]] [[package]]
name = "sv-ttk" name = "sv-ttk"
version = "2.6.0" version = "2.0"
description = "A gorgeous theme for Tkinter, based on Windows 11's UI" description = "A gorgeous theme for Tkinter that looks like Windows 11"
category = "main"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.4"
groups = ["main"]
files = [
{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]] [[package]]
name = "tomli" name = "tomli"
version = "2.2.1" version = "2.0.1"
description = "A lil' TOML parser" description = "A lil' TOML parser"
category = "main"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.7"
groups = ["main", "dev"]
markers = "python_version == \"3.10\""
files = [
{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]] [[package]]
name = "vban-cmd" name = "vban-cmd"
version = "2.10.3" version = "1.5.2"
description = "Python interface for the VBAN RT Packet Service (Sendtext)" description = "Python interface for the VBAN RT Packet Service (Sendtext)"
category = "main"
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10,<4.0"
groups = ["main", "dev"]
files = []
develop = true
[package.dependencies] [package.dependencies]
tomli = {version = ">=2.0.1,<3.0", markers = "python_version < \"3.11\""} tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
[package.source]
type = "directory"
url = "../vban-cmd-python"
[[package]] [[package]]
name = "voicemeeter-api" name = "voicemeeter-api"
version = "2.7.2" version = "0.8.1"
description = "A Python wrapper for the Voiceemeter API" description = "A Python wrapper for the Voiceemeter API"
category = "main"
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10,<4.0"
groups = ["main", "dev"]
files = []
develop = true
[package.dependencies] [package.dependencies]
tomli = {version = ">=2.0.1,<3.0", markers = "python_version < \"3.11\""} tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
[package.source]
type = "directory"
url = "../voicemeeter-api-python"
[metadata] [metadata]
lock-version = "2.1" lock-version = "1.1"
python-versions = ">=3.10,<3.14" python-versions = "^3.10"
content-hash = "f1e1782280c5e165fef043ca2695ea5f5c93fd00a66ace809266e0196fef6b71" content-hash = "0fc1f7b08a87f389504c898142c5a0f2ed20c8f56deabb3028fa815774b7fc98"
[metadata.files]
black = []
click = []
colorama = []
mypy-extensions = []
pathspec = []
platformdirs = []
sv-ttk = []
tomli = []
vban-cmd = []
voicemeeter-api = []

View File

@ -1,128 +1,27 @@
[project]
name = "voicemeeter-compact"
version = "1.10.0"
description = "A Compact Voicemeeter Remote App"
authors = [{ name = "Onyx and Iris", email = "code@onyxandiris.online" }]
license = { text = "MIT" }
readme = "README.md"
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] [tool.poetry]
packages = [{ include = "vmcompact" }] name = "voicemeeter-compact"
version = "1.6.0"
description = "A Compact Voicemeeter Remote App"
authors = ["onyx-and-iris <code@onyxandiris.online>"]
license = "MIT"
readme = "README.md"
repository = "https://github.com/onyx-and-iris/voicemeeter-compact"
packages = [
{ include = "vmcompact" },
]
include = ["vmcompact/img/cat.ico"] include = ["vmcompact/img/cat.ico"]
[tool.poetry.requires-plugins] [tool.poetry.dependencies]
poethepoet = ">=0.42.0" python = "^3.10"
sv-ttk = "^2.0"
tomli = { version = "^2.0.1", python = "<3.11" }
voicemeeter-api = "^0.8.1"
vban-cmd = "^1.5.2"
[tool.poetry.group.dev.dependencies] [tool.poetry.dev-dependencies]
ruff = "^0.9.1" black = {version = "^22.6.0", allow-prereleases = true}
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] [build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"] requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api" 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"]

View File

View File

View File

View File

View File

@ -1,322 +0,0 @@
#!/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()

View File

@ -1,285 +0,0 @@
#!/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()

View File

@ -1,192 +0,0 @@
#!/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()

View File

View File

@ -1,3 +1,3 @@
from .app import connect from .app import connect
__ALL__ = ['connect'] __ALL__ = ["connect"]

View File

@ -1,22 +1,14 @@
import logging
import tkinter as tk import tkinter as tk
from functools import cached_property
from pathlib import Path from pathlib import Path
from tkinter import messagebox, ttk from tkinter import ttk
from typing import NamedTuple from typing import NamedTuple
import voicemeeterlib
from voicemeeterlib import kinds
from .builders import MainFrameBuilder from .builders import MainFrameBuilder
from .configurations import loader from .data import _base_values, _configuration, _kinds_all
from .data import _base_values, _configuration, get_configuration from .errors import VMCompactErrors
from .errors import VMCompactError
from .menu import Menus from .menu import Menus
from .subject import Subject from .subject import Subject
logger = logging.getLogger(__name__)
class App(tk.Tk): class App(tk.Tk):
"""App mainframe""" """App mainframe"""
@ -30,46 +22,35 @@ class App(tk.Tk):
""" """
APP_cls = type( APP_cls = type(
f'Voicemeeter{kind}.Compact', f"Voicemeeter{kind}.Compact",
(cls,), (cls,),
{ {
'kind': kind, "kind": kind,
}, },
) )
return APP_cls return APP_cls
def __init__(self, vmr, theme): def __init__(self, vmr):
super().__init__() super().__init__()
self.logger = logger.getChild(self.__class__.__name__)
self._vmr = vmr self._vmr = vmr
self._vmr.event.add(['pdirty', 'ldirty']) self._vmr.event.add("ldirty")
self.subject = Subject() self._vmr.event.remove("mdirty")
self.start_updates() self._vmr.event.remove("midi")
self._vmr.init_thread() icon_path = Path(__file__).parent.resolve() / "img" / "cat.ico"
for pn in ( if icon_path.is_file():
Path(__file__).parent.resolve() / 'img' / 'cat.ico', self.iconbitmap(str(icon_path))
Path.cwd() / '_internal' / 'img' / 'cat.ico',
):
if pn.is_file():
self.iconbitmap(str(pn))
break
self.minsize(275, False) self.minsize(275, False)
self._configs = None self.subject = Subject()
self.protocol('WM_DELETE_WINDOW', self.on_close_window) self["menu"] = Menus(self, vmr)
self.menu = self['menu'] = Menus(self, vmr)
self.styletable = ttk.Style() self.styletable = ttk.Style()
if _configuration.config: if _configuration.config:
vmr.apply_config(_configuration.config) vmr.apply_config(_configuration.config)
self.build_app() self.build_app()
self.drag_id = '' self.drag_id = ""
self.bind('<Configure>', self.dragging) self.bind("<Configure>", self.dragging)
self.after(1, self.healthcheck_step)
def __str__(self):
return f'{type(self).__name__}App'
@property @property
def target(self): def target(self):
@ -85,8 +66,8 @@ class App(tk.Tk):
frame frame
for frame in self.winfo_children() for frame in self.winfo_children()
if isinstance(frame, ttk.Frame) if isinstance(frame, ttk.Frame)
and '!stripconfig' in str(frame) and "!stripconfig" in str(frame)
or '!busconfig' in str(frame) or "!busconfig" in str(frame)
) )
def build_app(self, kind=None, vban=None): def build_app(self, kind=None, vban=None):
@ -95,29 +76,29 @@ class App(tk.Tk):
if kind: if kind:
self.kind = kind self.kind = kind
# register event callbacks # register app as observer
self.target.subject.add([self.on_pdirty, self.on_ldirty]) self.target.subject.add(self)
self.bus_frame = None self.bus_frame = None
self.submix_frame = None self.submix_frame = None
self.builder = MainFrameBuilder(self) self.builder = MainFrameBuilder(self)
self.builder.setup() self.builder.setup()
self.builder.create_channelframe('strip') self.builder.create_channelframe("strip")
self.builder.create_separator() self.builder.create_separator()
self.builder.create_navframe() self.builder.create_navframe()
if _configuration.extended: if _configuration.extended:
self.nav_frame.extend.set(True) self.nav_frame.extend.set(True)
self.nav_frame.extend_frame() self.nav_frame.extend_frame()
if self.kind.name == 'potato': if self.kind.name == "potato":
self.builder.create_banner() self.builder.create_banner()
def on_pdirty(self): def on_update(self, subject):
if _base_values.run_update: """called whenever notified of update"""
self.after(1, self.subject.notify, 'pdirty')
def on_ldirty(self): if subject == "pdirty" and _base_values.run_update:
if not _base_values.dragging: self.after(1, self.subject.notify, "pdirty")
self.after(1, self.subject.notify, 'ldirty') elif subject == "ldirty" and not _base_values.dragging:
self.after(1, self.subject.notify, "ldirty")
def _destroy_top_level_frames(self): def _destroy_top_level_frames(self):
""" """
@ -127,7 +108,7 @@ class App(tk.Tk):
Destroy all top level frames. Destroy all top level frames.
""" """
self.target.subject.remove([self.on_pdirty, self.on_ldirty]) self.target.subject.remove(self)
self.subject.clear() self.subject.clear()
[ [
frame.destroy() frame.destroy()
@ -137,76 +118,25 @@ class App(tk.Tk):
def dragging(self, event, *args): def dragging(self, event, *args):
if event.widget is self: if event.widget is self:
if self.drag_id == '': if self.drag_id == "":
_base_values.dragging = True _base_values.dragging = True
else: else:
self.after_cancel(self.drag_id) self.after_cancel(self.drag_id)
self.drag_id = self.after(100, self.stop_drag) self.drag_id = self.after(100, self.stop_drag)
def stop_drag(self): def stop_drag(self):
self.drag_id = '' self.drag_id = ""
_base_values.dragging = False _base_values.dragging = False
@cached_property
def userconfigs(self):
self._configs = loader(self.kind.name, self.target)
return self._configs
def start_updates(self): _apps = {kind.name: App.make(kind) for kind in _kinds_all}
def init():
self.logger.debug('updates started')
_base_values.run_update = True
if self._vmr.gui.launched_by_api:
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?')
if resp:
self.logger.debug(
'healthcheck failed, rebuilding the app after GUI restart.'
)
self._vmr.end_thread()
self._vmr.run_voicemeeter(self._vmr.kind.name)
_base_values.run_update = False
self._vmr.init_thread()
self.after(8000, self.start_updates)
self._destroy_top_level_frames()
self.build_app(self._vmr.kind)
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')
[
self.menu.menu_vban.entryconfig(j, state='normal')
for j, _ in enumerate(self.menu.menu_vban.winfo_children())
]
else:
self.destroy()
self.after(250, self.healthcheck_step)
def on_close_window(self):
if _base_values.vban_connected:
self._vban.logout()
self.destroy()
_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""" """return App of the kind requested"""
try: try:
VMMIN_cls = _apps[kind_id] VMMIN_cls = _apps[kind_id]
except KeyError: except KeyError:
raise VMCompactError(f'Invalid kind: {kind_id}') raise VMCompactErrors(f"Invalid kind: {kind_id}")
return VMMIN_cls(vmr, theme) return VMMIN_cls(vmr)

View File

@ -1,35 +1,35 @@
import logging
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from .data import _base_values, _configuration from .data import _base_values, _configuration
logger = logging.getLogger(__name__)
class Banner(ttk.Frame): class Banner(ttk.Frame):
def __init__(self, parent): def __init__(self, parent):
super().__init__() super().__init__()
self.parent = parent self.parent = parent
self.parent.subject.add(self) self.submix = tk.StringVar()
self.logger = logger.getChild(self.__class__.__name__) self.submix.set(self.target.bus[_configuration.submixes].label)
self.submix = tk.StringVar(value=self.target.bus[_configuration.submixes].label)
self.label = ttk.Label( self.label = ttk.Label(
self, 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)) self.label.grid(column=0, row=0, sticky=(tk.N, tk.S, tk.W, tk.E))
self.upd_submix()
@property @property
def target(self): def target(self):
"""returns the current interface""" """returns the current interface"""
return self.parent.target return self.parent.target
def on_update(self, subject): def upd_submix(self):
if subject == 'submix': self.after(1, self.upd_submix_step)
if not _base_values.dragging:
self.logger.debug('checking submix for banner') def upd_submix_step(self):
self.submix.set(self.target.bus[_configuration.submixes].label) if not _base_values.dragging:
self.label['text'] = f'SUBMIX: {self.submix.get().upper()}' self.submix.set(self.target.bus[_configuration.submixes].label)
self.label["text"] = f"SUBMIX: {self.submix.get().upper()}"
self.after(100, self.upd_submix_step)

View File

@ -6,15 +6,12 @@ from tkinter import ttk
import sv_ttk import sv_ttk
from . import util
from .banner import Banner from .banner import Banner
from .channels import _make_channelframe from .channels import _make_channelframe
from .config import BusConfig, StripConfig from .config import BusConfig, StripConfig
from .data import _base_values, _configuration from .data import _base_values, _configuration
from .navigation import Navigation from .navigation import Navigation
logger = logging.getLogger(__name__)
class AbstractBuilder(abc.ABC): class AbstractBuilder(abc.ABC):
@abc.abstractmethod @abc.abstractmethod
@ -31,10 +28,11 @@ class AbstractBuilder(abc.ABC):
class MainFrameBuilder(AbstractBuilder): class MainFrameBuilder(AbstractBuilder):
"""Responsible for building the frames that sit directly on the mainframe""" """Responsible for building the frames that sit directly on the mainframe"""
logger = logging.getLogger("builders.mainframebuilder")
def __init__(self, app): def __init__(self, app):
self.kind = app.kind self.kind = app.kind
self.app = app self.app = app
self.logger = logger.getChild(self.__class__.__name__)
def setup(self): def setup(self):
self.app.title( self.app.title(
@ -42,31 +40,31 @@ class MainFrameBuilder(AbstractBuilder):
) )
self.app.resizable(False, False) self.app.resizable(False, False)
if _configuration.themes_enabled: 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) sv_ttk.set_theme(_configuration.theme_mode)
self.logger.info( 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_): def create_channelframe(self, type_):
if type_ == 'strip': if type_ == "strip":
self.app.strip_frame = _make_channelframe(self.app, type_) self.app.strip_frame = _make_channelframe(self.app, type_)
else: else:
self.app.bus_frame = _make_channelframe(self.app, type_) 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): 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.sep.grid(row=0, column=1, sticky=(tk.N, tk.S))
self.app.columnconfigure(1, minsize=15) self.app.columnconfigure(1, minsize=15)
self.logger.info('Finished building separator') self.logger.info(f"Finished building separator")
def create_navframe(self): def create_navframe(self):
self.app.nav_frame = Navigation(self.app) self.app.nav_frame = Navigation(self.app)
self.logger.info('Finished building navframe') self.logger.info(f"Finished building navframe")
def create_configframe(self, type_, index, id): def create_configframe(self, type_, index, id):
if type_ == 'strip': if type_ == "strip":
self.app.config_frame = StripConfig(self.app, index, id) self.app.config_frame = StripConfig(self.app, index, id)
if self.app.strip_frame: if self.app.strip_frame:
[ [
@ -96,20 +94,20 @@ class MainFrameBuilder(AbstractBuilder):
if self.app.strip_frame: if self.app.strip_frame:
[ [
frame.styletable.configure( frame.styletable.configure(
f'{frame.identifier}Conf{frame.index}.TButton', f"{frame.identifier}Conf{frame.index}.TButton",
background=f'{"white" if not frame.conf.get() else "yellow"}', background=f"{'white' if not frame.conf.get() else 'yellow'}",
) )
for _, frame in enumerate(self.app.strip_frame.labelframes) for _, frame in enumerate(self.app.strip_frame.labelframes)
] ]
if self.app.bus_frame: if self.app.bus_frame:
[ [
frame.styletable.configure( frame.styletable.configure(
f'{frame.identifier}Conf{frame.index}.TButton', f"{frame.identifier}Conf{frame.index}.TButton",
background=f'{"white" if not frame.conf.get() else "yellow"}', background=f"{'white' if not frame.conf.get() else 'yellow'}",
) )
for _, frame in enumerate(self.app.bus_frame.labelframes) 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) self.app.after(5, self.reset_config_frames)
def reset_config_frames(self): def reset_config_frames(self):
@ -122,7 +120,7 @@ class MainFrameBuilder(AbstractBuilder):
def create_banner(self): def create_banner(self):
self.app.banner = Banner(self.app) self.app.banner = Banner(self.app)
self.app.banner.grid(row=4, column=0, columnspan=3) self.app.banner.grid(row=4, column=0, columnspan=3)
self.logger.info('Finished building banner') self.logger.info(f"Finished building banner")
def teardown(self): def teardown(self):
pass pass
@ -141,31 +139,31 @@ class NavigationFrameBuilder(AbstractBuilder):
self.navframe.info = tk.BooleanVar() self.navframe.info = tk.BooleanVar()
self.navframe.channel_text = tk.StringVar( 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( 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() self.navframe.info_text = tk.StringVar()
def create_submix_button(self): def create_submix_button(self):
self.navframe.submix_button = ttk.Checkbutton( self.navframe.submix_button = ttk.Checkbutton(
self.navframe, self.navframe,
text='SUBMIX', text="SUBMIX",
command=self.navframe.show_submix, command=self.navframe.show_submix,
style=f'{"Toggle.TButton" if _configuration.themes_enabled else "Submix.TButton"}', style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'Submix.TButton'}",
variable=self.navframe.submix, variable=self.navframe.submix,
) )
self.navframe.submix_button.grid(column=0, row=0) self.navframe.submix_button.grid(column=0, row=0)
if self.navframe.parent.kind.name != 'potato': if self.navframe.parent.kind.name != "potato":
self.navframe.submix_button['state'] = 'disabled' self.navframe.submix_button["state"] = "disabled"
def create_channel_button(self): def create_channel_button(self):
self.navframe.channel_button = ttk.Checkbutton( self.navframe.channel_button = ttk.Checkbutton(
self.navframe, self.navframe,
textvariable=self.navframe.channel_text, textvariable=self.navframe.channel_text,
command=self.navframe.switch_channel, command=self.navframe.switch_channel,
style=f'{"Toggle.TButton" if _configuration.themes_enabled else "Channel.TButton"}', style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'Channel.TButton'}",
variable=self.navframe.channel, variable=self.navframe.channel,
) )
self.navframe.channel_button.grid(column=0, row=1, rowspan=1) self.navframe.channel_button.grid(column=0, row=1, rowspan=1)
@ -175,7 +173,7 @@ class NavigationFrameBuilder(AbstractBuilder):
self.navframe, self.navframe,
textvariable=self.navframe.extend_text, textvariable=self.navframe.extend_text,
command=self.navframe.extend_frame, command=self.navframe.extend_frame,
style=f'{"Toggle.TButton" if _configuration.themes_enabled else "Extend.TButton"}', style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'Extend.TButton'}",
variable=self.navframe.extend, variable=self.navframe.extend,
) )
self.navframe.extend_button.grid(column=0, row=2) self.navframe.extend_button.grid(column=0, row=2)
@ -184,7 +182,7 @@ class NavigationFrameBuilder(AbstractBuilder):
self.navframe.info_button = ttk.Checkbutton( self.navframe.info_button = ttk.Checkbutton(
self.navframe, self.navframe,
textvariable=self.navframe.info_text, textvariable=self.navframe.info_text,
style=f'{"Toggle.TButton" if _configuration.themes_enabled else "Rec.TButton"}', style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'Rec.TButton'}",
variable=self.navframe.info, variable=self.navframe.info,
) )
self.navframe.info_button.grid(column=0, row=3) self.navframe.info_button.grid(column=0, row=3)
@ -196,9 +194,9 @@ class NavigationFrameBuilder(AbstractBuilder):
if isinstance(child, ttk.Checkbutton) if isinstance(child, ttk.Checkbutton)
] ]
if _configuration.themes_enabled: if _configuration.themes_enabled:
self.navframe.rowconfigure(1, minsize=_configuration.channel_height) self.navframe.rowconfigure(1, minsize=_configuration.level_height)
else: else:
self.navframe.rowconfigure(1, minsize=_configuration.channel_height + 10) self.navframe.rowconfigure(1, minsize=_configuration.level_height + 10)
def teardown(self): def teardown(self):
pass pass
@ -228,9 +226,9 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
"""Adds a progress bar widget to a single label frame""" """Adds a progress bar widget to a single label frame"""
self.labelframe.pb = ttk.Progressbar( self.labelframe.pb = ttk.Progressbar(
self.labelframe, self.labelframe,
maximum=72, # Range: 0 = -60dB, 72 = +12dB (72dB total range) maximum=100,
orient='vertical', orient="vertical",
mode='determinate', mode="determinate",
variable=self.labelframe.level, variable=self.labelframe.level,
) )
self.labelframe.pb.grid(column=0, row=0) self.labelframe.pb.grid(column=0, row=0)
@ -241,22 +239,16 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
self.labelframe, self.labelframe,
from_=12.0, from_=12.0,
to=-60.0, to=-60.0,
orient='vertical', orient="vertical",
variable=self.labelframe.gain, variable=self.labelframe.gain,
command=self.labelframe.scale_callback, command=self.labelframe.scale_callback,
length=_configuration.channel_height, length=_configuration.level_height,
) )
self.scale.grid(column=1, row=0) self.scale.grid(column=1, row=0)
self.scale.bind('<Double-Button-1>', self.labelframe.reset_gain) self.scale.bind("<Double-Button-1>", self.labelframe.reset_gain)
self.scale.bind('<Button-1>', self.labelframe.scale_press) self.scale.bind("<Button-1>", self.labelframe.scale_press)
self.scale.bind('<ButtonRelease-1>', self.labelframe.scale_release) self.scale.bind("<ButtonRelease-1>", self.labelframe.scale_release)
self.scale.bind( self.scale.bind("<MouseWheel>", self.labelframe._on_mousewheel)
'<MouseWheel>',
partial(
self.labelframe.pause_updates,
self.labelframe._on_mousewheel,
),
)
def add_gain_label(self): def add_gain_label(self):
self.labelframe.gain_label = ttk.Label( self.labelframe.gain_label = ttk.Label(
@ -269,9 +261,9 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
"""Adds a mute button widget to a single label frame""" """Adds a mute button widget to a single label frame"""
self.button_mute = ttk.Checkbutton( self.button_mute = ttk.Checkbutton(
self.labelframe, self.labelframe,
text='MUTE', text="MUTE",
command=partial(self.labelframe.pause_updates, self.labelframe.toggle_mute), command=partial(self.labelframe.toggle_mute, "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, variable=self.labelframe.mute,
) )
self.button_mute.grid(column=0, row=2, columnspan=2) self.button_mute.grid(column=0, row=2, columnspan=2)
@ -279,9 +271,9 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
def add_conf_button(self): def add_conf_button(self):
self.button_conf = ttk.Checkbutton( self.button_conf = ttk.Checkbutton(
self.labelframe, self.labelframe,
text='CONFIG', text="CONFIG",
command=self.labelframe.open_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, variable=self.labelframe.conf,
) )
self.button_conf.grid(column=0, row=3, columnspan=2) self.button_conf.grid(column=0, row=3, columnspan=2)
@ -289,9 +281,9 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
def add_on_button(self): def add_on_button(self):
self.button_on = ttk.Checkbutton( self.button_on = ttk.Checkbutton(
self.labelframe, self.labelframe,
text='ON', text="ON",
command=partial(self.labelframe.pause_updates, self.labelframe.set_on), command=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, variable=self.labelframe.on,
) )
self.button_on.grid(column=0, row=2, columnspan=2) self.button_on.grid(column=0, row=2, columnspan=2)
@ -331,7 +323,7 @@ class ChannelConfigFrameBuilder(AbstractBuilder):
] ]
self.configframe.grid(sticky=(tk.W)) self.configframe.grid(sticky=(tk.W))
[ [
self.configframe.columnconfigure(i, minsize=_configuration.channel_width) self.configframe.columnconfigure(i, minsize=_configuration.level_width)
for i in range(self.configframe.phys_out + self.configframe.virt_out) for i in range(self.configframe.phys_out + self.configframe.virt_out)
] ]
@ -340,40 +332,40 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
"""Responsible for building channel configframe widgets""" """Responsible for building channel configframe widgets"""
def setup(self): def setup(self):
if self.configframe.parent.kind.name == 'basic': if self.configframe.parent.kind.name == "basic":
self.configframe.slider_params = ('audibility',) self.configframe.slider_params = ("audibility",)
self.configframe.slider_vars = (tk.DoubleVar(),) self.configframe.slider_vars = (tk.DoubleVar(),)
else: else:
self.configframe.slider_params = ('comp.knob', 'gate.knob', 'limit') self.configframe.slider_params = ("comp", "gate", "limit")
self.configframe.slider_vars = [ self.configframe.slider_vars = [
tk.DoubleVar() for _ in self.configframe.slider_params tk.DoubleVar() for _ in self.configframe.slider_params
] ]
self.configframe.phys_out_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 = [ self.configframe.phys_out_params_vars = [
tk.BooleanVar() for _ in self.configframe.phys_out_params tk.BooleanVar() for _ in self.configframe.phys_out_params
] ]
self.configframe.virt_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 = [ self.configframe.virt_out_params_vars = [
tk.BooleanVar() for _ in self.configframe.virt_out_params tk.BooleanVar() for _ in self.configframe.virt_out_params
] ]
self.configframe.bool_params = ('mono', 'solo') self.configframe.params = ("mono", "solo")
self.configframe.bool_param_vars = list( self.configframe.param_vars = list(
tk.BooleanVar() for _ in self.configframe.bool_params tk.BooleanVar() for _ in self.configframe.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: if self.configframe.index == self.configframe.phys_in:
self.configframe.params = list( self.configframe.params = list(
map(lambda x: x.replace('mono', 'mc'), self.configframe.bool_params) map(lambda x: x.replace("mono", "mc"), self.configframe.params)
) )
if self.configframe.parent.kind.name == 'banana': if self.configframe.parent.kind.name == "banana":
pass pass
# karaoke modes not in RT Packet yet. May implement in future # karaoke modes not in RT Packet yet. May implement in future
""" """
@ -389,104 +381,101 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
== self.configframe.phys_in + self.configframe.virt_in - 1 == self.configframe.phys_in + self.configframe.virt_in - 1
): ):
self.configframe.params = list( self.configframe.params = list(
map( map(lambda x: x.replace("mono", "mc"), self.configframe.params)
lambda x: x.replace('mono', 'mc'),
self.configframe.bool_params,
)
) )
def create_comp_slider(self): 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( comp_scale = ttk.Scale(
self.configframe, self.configframe,
from_=0.0, from_=0.0,
to=10.0, to=10.0,
orient='horizontal', orient="horizontal",
length=_configuration.channel_width, length=_configuration.level_width,
variable=self.configframe.slider_vars[ variable=self.configframe.slider_vars[
self.configframe.slider_params.index('comp.knob') self.configframe.slider_params.index("comp")
], ],
command=partial(self.configframe.scale_callback, 'comp.knob'), command=partial(self.configframe.scale_callback, "comp"),
) )
comp_scale.bind( comp_scale.bind(
'<Double-Button-1>', partial(self.configframe.reset_scale, 'comp.knob', 0) "<Double-Button-1>", partial(self.configframe.reset_scale, "comp", 0)
) )
comp_scale.bind('<Button-1>', self.configframe.scale_press) comp_scale.bind("<Button-1>", self.configframe.scale_press)
comp_scale.bind('<ButtonRelease-1>', self.configframe.scale_release) comp_scale.bind("<ButtonRelease-1>", self.configframe.scale_release)
comp_scale.bind('<Enter>', partial(self.configframe.scale_enter, 'comp.knob')) comp_scale.bind("<Enter>", partial(self.configframe.scale_enter, "comp"))
comp_scale.bind('<Leave>', self.configframe.scale_leave) comp_scale.bind("<Leave>", self.configframe.scale_leave)
comp_label.grid(column=0, row=0) comp_label.grid(column=0, row=0)
comp_scale.grid(column=1, row=0) comp_scale.grid(column=1, row=0)
def create_gate_slider(self): 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( gate_scale = ttk.Scale(
self.configframe, self.configframe,
from_=0.0, from_=0.0,
to=10.0, to=10.0,
orient='horizontal', orient="horizontal",
length=_configuration.channel_width, length=_configuration.level_width,
variable=self.configframe.slider_vars[ variable=self.configframe.slider_vars[
self.configframe.slider_params.index('gate.knob') self.configframe.slider_params.index("gate")
], ],
command=partial(self.configframe.scale_callback, 'gate.knob'), command=partial(self.configframe.scale_callback, "gate"),
) )
gate_scale.bind( gate_scale.bind(
'<Double-Button-1>', partial(self.configframe.reset_scale, 'gate.knob', 0) "<Double-Button-1>", partial(self.configframe.reset_scale, "gate", 0)
) )
gate_scale.bind('<Button-1>', self.configframe.scale_press) gate_scale.bind("<Button-1>", self.configframe.scale_press)
gate_scale.bind('<ButtonRelease-1>', self.configframe.scale_release) gate_scale.bind("<ButtonRelease-1>", self.configframe.scale_release)
gate_scale.bind('<Enter>', partial(self.configframe.scale_enter, 'gate.knob')) gate_scale.bind("<Enter>", partial(self.configframe.scale_enter, "gate"))
gate_scale.bind('<Leave>', self.configframe.scale_leave) gate_scale.bind("<Leave>", self.configframe.scale_leave)
gate_label.grid(column=2, row=0) gate_label.grid(column=2, row=0)
gate_scale.grid(column=3, row=0) gate_scale.grid(column=3, row=0)
def create_limit_slider(self): 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( limit_scale = ttk.Scale(
self.configframe, self.configframe,
from_=-40, from_=-40,
to=12, to=12,
orient='horizontal', orient="horizontal",
length=_configuration.channel_width, length=_configuration.level_width,
variable=self.configframe.slider_vars[ 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( 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("<Button-1>", self.configframe.scale_press)
limit_scale.bind('<ButtonRelease-1>', self.configframe.scale_release) limit_scale.bind("<ButtonRelease-1>", self.configframe.scale_release)
limit_scale.bind('<Enter>', partial(self.configframe.scale_enter, 'limit')) limit_scale.bind("<Enter>", partial(self.configframe.scale_enter, "limit"))
limit_scale.bind('<Leave>', self.configframe.scale_leave) limit_scale.bind("<Leave>", self.configframe.scale_leave)
limit_label.grid(column=4, row=0) limit_label.grid(column=4, row=0)
limit_scale.grid(column=5, row=0) limit_scale.grid(column=5, row=0)
def create_audibility_slider(self): 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( aud_scale = ttk.Scale(
self.configframe, self.configframe,
from_=0.0, from_=0.0,
to=10.0, to=10.0,
orient='horizontal', orient="horizontal",
length=_configuration.channel_width, length=_configuration.level_width,
variable=self.configframe.slider_vars[ 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( 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("<Button-1>", self.configframe.scale_press)
aud_scale.bind('<ButtonRelease-1>', self.configframe.scale_release) aud_scale.bind("<ButtonRelease-1>", self.configframe.scale_release)
aud_scale.bind('<Enter>', partial(self.configframe.scale_enter, 'audibility')) aud_scale.bind("<Enter>", partial(self.configframe.scale_enter, "audibility"))
aud_scale.bind('<Leave>', self.configframe.scale_leave) aud_scale.bind("<Leave>", self.configframe.scale_leave)
aud_label.grid(column=0, row=0) aud_label.grid(column=0, row=0)
aud_scale.grid(column=1, row=0) aud_scale.grid(column=1, row=0)
@ -496,10 +485,8 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
ttk.Checkbutton( ttk.Checkbutton(
self.configframe, self.configframe,
text=param, text=param,
command=partial( command=partial(self.configframe.toggle_a, param),
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[ variable=self.configframe.phys_out_params_vars[
self.configframe.phys_out_params.index(param) self.configframe.phys_out_params.index(param)
], ],
@ -519,10 +506,8 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
ttk.Checkbutton( ttk.Checkbutton(
self.configframe, self.configframe,
text=param, text=param,
command=partial( command=partial(self.configframe.toggle_b, param),
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[ variable=self.configframe.virt_out_params_vars[
self.configframe.virt_out_params.index(param) self.configframe.virt_out_params.index(param)
], ],
@ -542,13 +527,11 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
ttk.Checkbutton( ttk.Checkbutton(
self.configframe, self.configframe,
text=param, text=param,
command=partial( command=partial(self.configframe.toggle_p, param),
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.bool_params) for i, param in enumerate(self.configframe.params)
] ]
[ [
button.grid( button.grid(
@ -562,71 +545,42 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
class BusConfigFrameBuilder(ChannelConfigFrameBuilder): class BusConfigFrameBuilder(ChannelConfigFrameBuilder):
"""Responsible for building channel configframe widgets""" """Responsible for building channel configframe widgets"""
def __init__(self, configframe, app):
super().__init__(configframe)
self.app = app
def setup(self): def setup(self):
self.configframe.bus_mode_map = util.get_busmode_fullnames(self.app.kind) # fmt: off
self.configframe.bus_mode_map_reverse = util.get_busmode_fullnames_reversed( self.configframe.bus_mode_map = {
self.app.kind "normal": "Normal",
) "amix": "Mix Down A",
self.configframe.bus_modes = util.get_busmode_shortnames(self.app.kind) "bmix": "Mix Down B",
self.configframe.int_params = ('mono',) "repeat": "Stereo Repeat",
self.configframe.int_param_vars = [ "composite": "Composite",
tk.IntVar(value=getattr(self.configframe.target, param)) "tvmix": "Up Mix TV",
for param in self.configframe.int_params "upmix21": "Up Mix 2.1",
] "upmix41": "Up Mix 4.1",
self.configframe.mono_modes = util.get_busmono_modes() "upmix61": "Up Mix 6.1",
self.configframe.bus_mono_label_text = tk.StringVar( "centeronly": "Center Only",
value=self.configframe.mono_modes[self.configframe.target.mono] "lfeonly": "LFE Only",
) "rearonly": "Rear Only",
self.configframe.bool_params = ('eq.on', 'eq.ab') }
self.configframe.bool_param_vars = [ self.configframe.bus_modes = list(self.configframe.bus_mode_map.keys())
tk.BooleanVar() for _ in self.configframe.bool_params # fmt: on
] self.configframe.params = ("mono", "eq", "eq_ab")
self.configframe.param_vars = [tk.BooleanVar() for _ in self.configframe.params]
self.configframe.bus_mode_label_text = tk.StringVar( self.configframe.bus_mode_label_text = tk.StringVar(
value=self.configframe.bus_mode_map[self.configframe.current_bus_mode()] value=self.configframe.bus_mode_map[self.configframe.current_bus_mode()]
) )
def create_bus_mode_button(self): def create_bus_mode_button(self):
self.configframe.busmode_button = ttk.Button( self.configframe.busmode_button = ttk.Button(
self.configframe, self.configframe, textvariable=self.configframe.bus_mode_label_text
textvariable=self.configframe.bus_mode_label_text,
width=15,
) )
self.configframe.busmode_button.grid( self.configframe.busmode_button.grid(
column=0, row=0, columnspan=2, sticky=(tk.W), padx=1, pady=1 column=0, row=0, columnspan=2, sticky=(tk.W)
) )
self.configframe.busmode_button.bind( self.configframe.busmode_button.bind(
'<Button-1>', "<Button-1>", self.configframe.rotate_bus_modes_right
partial(
self.configframe.pause_updates, self.configframe.rotate_bus_modes_right
),
) )
self.configframe.busmode_button.bind( self.configframe.busmode_button.bind(
'<Button-3>', "<Button-3>", self.configframe.rotate_bus_modes_left
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): def create_param_buttons(self):
@ -634,17 +588,15 @@ class BusConfigFrameBuilder(ChannelConfigFrameBuilder):
ttk.Checkbutton( ttk.Checkbutton(
self.configframe, self.configframe,
text=param, text=param,
command=partial( command=partial(self.configframe.toggle_p, param),
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.bool_params) for i, param in enumerate(self.configframe.params)
] ]
[ [
button.grid( button.grid(
column=i + 1, column=i,
row=1, row=1,
) )
for i, button in enumerate(param_buttons) for i, button in enumerate(param_buttons)

View File

@ -1,12 +1,10 @@
import logging
import tkinter as tk import tkinter as tk
from math import log
from tkinter import ttk from tkinter import ttk
from . import builders from . import builders
from .data import _base_values, _configuration from .data import _base_values, _configuration
logger = logging.getLogger(__name__)
class ChannelLabelFrame(ttk.LabelFrame): class ChannelLabelFrame(ttk.LabelFrame):
"""Base class for a single channel""" """Base class for a single channel"""
@ -16,7 +14,6 @@ class ChannelLabelFrame(ttk.LabelFrame):
self.parent = parent self.parent = parent
self.index = index self.index = index
self.id = id self.id = id
self.logger = logger.getChild(self.__class__.__name__)
self.styletable = self.parent.parent.styletable self.styletable = self.parent.parent.styletable
self.builder = builders.ChannelLabelFrameBuilder(self, index, id) self.builder = builders.ChannelLabelFrameBuilder(self, index, id)
@ -43,32 +40,29 @@ class ChannelLabelFrame(ttk.LabelFrame):
return self.parent.target return self.parent.target
def getter(self, param): def getter(self, param):
try: if hasattr(self.target, param):
return getattr(self.target, param) return getattr(self.target, param)
except AttributeError as e:
self.logger(f'{type(e).__name__}: {e}')
def setter(self, param, value): def setter(self, param, value):
if param in dir(self.target): # avoid calling getattr (with hasattr) if hasattr(self.target, param):
setattr(self.target, param, value) setattr(self.target, param, value)
def scale_callback(self, *args): def scale_callback(self, *args):
"""callback function for scale widget""" """callback function for scale widget"""
val = round(self.gain.get(), 1) self.setter("gain", self.gain.get())
self.setter('gain', val) self.gainlabel.set(round(self.gain.get(), 1))
self.gainlabel.set(val)
def toggle_mute(self, *args): def toggle_mute(self, *args):
self.target.mute = self.mute.get() self.target.mute = self.mute.get()
if not _configuration.themes_enabled: if not _configuration.themes_enabled:
self.styletable.configure( 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"}', background=f'{"red" if self.mute.get() else "white"}',
) )
def reset_gain(self, *args): def reset_gain(self, *args):
self.setter('gain', 0) self.setter("gain", 0)
self.gain.set(0) self.gain.set(0)
self.gainlabel.set(self.gain.get()) self.gainlabel.set(self.gain.get())
@ -76,47 +70,37 @@ class ChannelLabelFrame(ttk.LabelFrame):
self.after(1, self.remove_events) self.after(1, self.remove_events)
def remove_events(self): def remove_events(self):
self.parent.target.event.remove('pdirty') self.parent.target.event.remove("pdirty")
self.parent.target.event.remove('ldirty') self.parent.target.event.remove("ldirty")
def scale_release(self, *args): def scale_release(self, *args):
_base_values.run_update = False _base_values.run_update = False
self.after(1, self.add_events) self.after(1, self.add_events)
def add_events(self): def add_events(self):
self.parent.target.event.add('pdirty') self.parent.target.event.add("pdirty")
self.parent.target.event.add('ldirty') self.parent.target.event.add("ldirty")
self.after(500, self.resume_updates) self.after(500, self.resume_updates)
def pause_updates(self, func, *args):
"""function wrapper, adds a 50ms delay on updates"""
_base_values.run_update = False
func(*args)
self.after(50, self.resume_updates)
def resume_updates(self): def resume_updates(self):
_base_values.run_update = True _base_values.run_update = True
def _on_mousewheel(self, event): def _on_mousewheel(self, event):
_base_values.run_update = False
self.gain.set( self.gain.set(
round( self.gain.get()
self.gain.get() + (
+ ( _configuration.mwscroll_step
_configuration.mwscroll_step if event.delta > 0
if event.delta > 0 else -_configuration.mwscroll_step
else -_configuration.mwscroll_step
),
1,
) )
) )
if self.gain.get() > 12: if self.gain.get() > 12:
self.gain.set(12) self.gain.set(12)
elif self.gain.get() < -60: elif self.gain.get() < -60:
self.gain.set(-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)) self.after(1, self.resume_updates)
def open_config(self): def open_config(self):
if self.conf.get(): if self.conf.get():
@ -125,47 +109,46 @@ class ChannelLabelFrame(ttk.LabelFrame):
self.parent.parent.config_frame.teardown() self.parent.parent.config_frame.teardown()
if not _configuration.themes_enabled: if not _configuration.themes_enabled:
self.styletable.configure( 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"}', background=f'{"yellow" if self.conf.get() else "white"}',
) )
def on_update(self, subject): def on_update(self, subject):
if subject == 'ldirty': if subject == "ldirty":
self.upd_levels() self.upd_levels()
elif subject == 'pdirty': elif subject == "pdirty":
self.sync_params() self.sync_params()
elif subject == 'labelframe': elif subject == "labelframe":
self.after(5, self.sync_labels) self.after(5, self.sync_labels)
def sync_params(self): def sync_params(self):
"""sync parameter states, update button colours""" """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.gainlabel.set(round(self.gain.get(), 1))
self.mute.set(self.getter('mute')) self.mute.set(self.getter("mute"))
if not _configuration.themes_enabled: if not _configuration.themes_enabled:
self.styletable.configure( 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"}', background=f'{"red" if self.mute.get() else "white"}',
) )
def sync_labels(self): def sync_labels(self):
"""sync labelframes according to label text""" """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].insert(self.index, retval)
self.parent.label_cache[self.id][self.index] = retval if len(retval) > 10:
if len(retval) > 10: retval = f"{retval[:8]}.."
retval = f'{retval[:8]}..' if not retval:
if not retval: self.parent.columnconfigure(self.index, minsize=0)
self.parent.columnconfigure(self.index, minsize=0) self.parent.parent.subject.remove(self)
self.parent.parent.subject.remove(self) self.grid_remove()
self.grid_remove() else:
else: self.parent.parent.subject.add(self)
self.parent.parent.subject.add(self) self.grid()
self.grid() self.configure(text=retval)
self.configure(text=retval)
def grid_configure(self): def grid_configure(self):
self.grid(padx=_configuration.channel_xpadding, sticky=(tk.N, tk.S)) self.grid(sticky=(tk.N, tk.S))
[ [
child.grid_configure(padx=1, pady=1, sticky=(tk.W, tk.E)) child.grid_configure(padx=1, pady=1, sticky=(tk.W, tk.E))
for child in self.winfo_children() for child in self.winfo_children()
@ -197,22 +180,14 @@ class Strip(ChannelLabelFrame):
def upd_levels(self): def upd_levels(self):
""" """
Updates level values using direct dB values. Updates level values.
""" """
if self.index < self.parent.parent.kind.num_strip: if self.index < self.parent.parent.kind.num_strip:
if self.target.levels.is_updated: if self.target.levels.is_updated:
val = max(self.target.levels.prefader) val = max(self.target.levels.prefader)
if val < -72: self.level.set(
if self.level.get() != 0: (0 if self.mute.get() else 100 + val - 18 + self.gain.get())
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): class Bus(ChannelLabelFrame):
@ -231,33 +206,21 @@ class Bus(ChannelLabelFrame):
def upd_levels(self): def upd_levels(self):
if self.index < self.parent.parent.kind.num_bus: if self.index < self.parent.parent.kind.num_bus:
if self.target.levels.is_updated: if self.target.levels.is_updated or self.level.get() != -118:
val = max(self.target.levels.all) val = max(self.target.levels.all)
if val < -72: self.level.set((0 if self.mute.get() else 100 + val - 18))
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): class ChannelFrame(ttk.Frame):
label_cache = {"strip": list(), "bus": list()}
def init(self, parent, id): def init(self, parent, id):
super().__init__(parent) super().__init__(parent)
self.parent = parent self.parent = parent
self.id = id self.id = id
self.phys_in, self.virt_in = parent.kind.ins self.phys_in, self.virt_in = parent.kind.ins
self.phys_out, self.virt_out = parent.kind.outs 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),
}
self.parent.subject.add(self) self.parent.subject.add(self)
self.update_labels()
@property @property
def target(self): def target(self):
@ -279,17 +242,17 @@ class ChannelFrame(ttk.Frame):
if isinstance(frame, ttk.LabelFrame) if isinstance(frame, ttk.LabelFrame)
) )
def update_labels(self):
for labelframe in self.labelframes:
labelframe.on_update('labelframe')
def on_update(self, subject): def on_update(self, subject):
if subject == 'pdirty': if subject == "pdirty":
self.update_labels() target = getattr(self.target, self.id)
num = getattr(self.parent.kind, f"num_{self.id}")
if self.label_cache[self.id] != [target[i].label for i in range(num)]:
for labelframe in self.labelframes:
labelframe.on_update("labelframe")
def grid_configure(self): def grid_configure(self):
[ [
self.columnconfigure(i, minsize=_configuration.channel_width) self.columnconfigure(i, minsize=_configuration.level_width)
for i, _ in enumerate(self.labelframes) for i, _ in enumerate(self.labelframes)
] ]
[self.rowconfigure(0, minsize=100) for i, _ in enumerate(self.labelframes)] [self.rowconfigure(0, minsize=100) for i, _ in enumerate(self.labelframes)]
@ -298,10 +261,10 @@ class ChannelFrame(ttk.Frame):
[self.parent.subject.remove(frame) for frame in self.labelframes] [self.parent.subject.remove(frame) for frame in self.labelframes]
self.parent.subject.remove(self) self.parent.subject.remove(self)
self.destroy() self.destroy()
setattr(self.parent, f'{self.identifier}_frame', None) setattr(self.parent, f"{self.identifier}_frame", None)
def _make_channelframe(parent, identifier): def _make_channelframe(parent, id):
""" """
Creates a Channel Frame class of type strip or bus Creates a Channel Frame class of type strip or bus
""" """
@ -309,33 +272,29 @@ def _make_channelframe(parent, identifier):
phys_in, virt_in = parent.kind.ins phys_in, virt_in = parent.kind.ins
phys_out, virt_out = parent.kind.outs phys_out, virt_out = parent.kind.outs
def init_labels(self): def init_labels(self, id):
""" """
Grids each labelframe, grid_removes any without a label Grids each labelframe, grid_removes any without a label
""" """
for i, labelframe in enumerate( for i, labelframe in enumerate(
getattr(self, 'strips' if identifier == 'strip' else 'buses') getattr(self, "strips" if id == "strip" else "buses")
): ):
labelframe.grid(row=0, column=i) labelframe.grid(row=0, column=i)
label = labelframe.target.label if not labelframe.target.label:
if not label:
self.columnconfigure(i, minsize=0) self.columnconfigure(i, minsize=0)
labelframe.grid_remove() labelframe.grid_remove()
self.label_cache[identifier][i] = label
def init_strip(self, *args, **kwargs): def init_strip(self, *args, **kwargs):
self.init(parent, identifier) self.init(parent, id)
self.strips = tuple( self.strips = tuple(Strip(self, i, id) for i in range(phys_in + virt_in))
Strip(self, i, identifier) for i in range(phys_in + virt_in)
)
self.grid(row=0, column=0, sticky=(tk.W)) self.grid(row=0, column=0, sticky=(tk.W))
self.grid_configure() self.grid_configure()
init_labels(self) init_labels(self, id)
def init_bus(self, *args, **kwargs): def init_bus(self, *args, **kwargs):
self.init(parent, identifier) self.init(parent, id)
self.buses = tuple(Bus(self, i, identifier) for i in range(phys_out + virt_out)) self.buses = tuple(Bus(self, i, id) for i in range(phys_out + virt_out))
if _configuration.extended: if _configuration.extended:
if _configuration.extends_horizontal: if _configuration.extends_horizontal:
self.grid(row=0, column=2, sticky=(tk.W)) self.grid(row=0, column=2, sticky=(tk.W))
@ -344,22 +303,22 @@ def _make_channelframe(parent, identifier):
else: else:
self.grid(row=0, column=0) self.grid(row=0, column=0)
self.grid_configure() self.grid_configure()
init_labels(self) init_labels(self, id)
if identifier == 'strip': if id == "strip":
CHANNELFRAME_cls = type( CHANNELFRAME_cls = type(
f'ChannelFrame{identifier.capitalize()}', f"ChannelFrame{id.capitalize()}",
(ChannelFrame,), (ChannelFrame,),
{ {
'__init__': init_strip, "__init__": init_strip,
}, },
) )
else: else:
CHANNELFRAME_cls = type( CHANNELFRAME_cls = type(
f'ChannelFrame{identifier.capitalize()}', f"ChannelFrame{id.capitalize()}",
(ChannelFrame,), (ChannelFrame,),
{ {
'__init__': init_bus, "__init__": init_bus,
}, },
) )
return CHANNELFRAME_cls(parent) return CHANNELFRAME_cls(parent)

View File

@ -1,11 +1,10 @@
import logging import tkinter as tk
from functools import partial
from tkinter import ttk from tkinter import ttk
from . import builders from . import builders
from .data import _base_values, _configuration from .data import _base_values, _configuration
logger = logging.getLogger(__name__)
class Config(ttk.Frame): class Config(ttk.Frame):
def __init__(self, parent, index, _id): def __init__(self, parent, index, _id):
@ -13,7 +12,6 @@ class Config(ttk.Frame):
self.parent = parent self.parent = parent
self.index = index self.index = index
self.id = _id self.id = _id
self.logger = logger.getChild(self.__class__.__name__)
self.styletable = parent.styletable self.styletable = parent.styletable
self.phys_in, self.virt_in = parent.kind.ins self.phys_in, self.virt_in = parent.kind.ins
self.phys_out, self.virt_out = parent.kind.outs self.phys_out, self.virt_out = parent.kind.outs
@ -31,51 +29,29 @@ class Config(ttk.Frame):
return self.parent.target return self.parent.target
def getter(self, param): def getter(self, param):
param = param.split('.') if hasattr(self.target, param):
try: return getattr(self.target, param)
if len(param) == 2:
target = getattr(self.target, param[0])
return getattr(target, param[1])
else:
return getattr(self.target, param[0])
except AttributeError as e:
self.logger.error(f'{type(e).__name__}: {e}')
def setter(self, param, value): def setter(self, param, value):
param = param.split('.') if hasattr(self.target, param):
try: setattr(self.target, param, value)
if len(param) == 2:
target = getattr(self.target, param[0])
setattr(target, param[1], value)
else:
setattr(self.target, param[0], value)
except AttributeError as e:
self.logger(f'{type(e).__name__}: {e}')
def scale_press(self, *args): def scale_press(self, *args):
self.after(1, self.remove_events) self.after(1, self.remove_events)
def remove_events(self): def remove_events(self):
self.parent.target.event.remove('pdirty') self.parent.target.event.remove("pdirty")
self.parent.target.event.remove('ldirty') self.parent.target.event.remove("ldirty")
def scale_release(self, *args): def scale_release(self, *args):
_base_values.run_update = False _base_values.run_update = False
self.after(1, self.add_events) self.after(1, self.add_events)
def add_events(self): def add_events(self):
self.parent.target.event.add('pdirty') self.parent.target.event.add("pdirty")
self.parent.target.event.add('ldirty') self.parent.target.event.add("ldirty")
self.after(350, self.resume_updates) self.after(350, self.resume_updates)
def pause_updates(self, func, *args):
"""function wrapper, adds a 50ms delay on updates"""
_base_values.run_update = False
func(*args)
self.after(50, self.resume_updates)
def resume_updates(self): def resume_updates(self):
_base_values.run_update = True _base_values.run_update = True
@ -84,13 +60,13 @@ class Config(ttk.Frame):
self.parent.nav_frame.info_text.set(round(val, 1)) self.parent.nav_frame.info_text.set(round(val, 1))
def scale_leave(self, *args): 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): def scale_callback(self, param, *args):
"""callback function for scale widget""" """callback function for scale widget"""
val = self.slider_vars[self.slider_params.index(param)].get() val = self.slider_vars[self.slider_params.index(param)].get()
self.setter(param, round(val, 1)) self.setter(param, val)
self.parent.nav_frame.info_text.set(round(val, 1)) self.parent.nav_frame.info_text.set(round(val, 1))
def reset_scale(self, param, val, *args): def reset_scale(self, param, val, *args):
@ -98,16 +74,16 @@ class Config(ttk.Frame):
self.slider_vars[self.slider_params.index(param)].set(val) self.slider_vars[self.slider_params.index(param)].set(val)
def toggle_p(self, param): def toggle_p(self, param):
val = self.bool_param_vars[self.bool_params.index(param)].get() val = self.param_vars[self.params.index(param)].get()
self.setter(param, val) self.setter(param, val)
if not _configuration.themes_enabled: if not _configuration.themes_enabled:
self.styletable.configure( 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): def on_update(self, subject):
"""update parameters""" """update parameters"""
if subject == 'pdirty': if subject == "pdirty":
self.sync() self.sync()
@ -122,7 +98,6 @@ class StripConfig(Config):
self.make_row_2() self.make_row_2()
self.builder.grid_configure() self.builder.grid_configure()
self.parent.target.clear_dirty()
self.sync() self.sync()
@property @property
@ -134,7 +109,7 @@ class StripConfig(Config):
def make_row_0(self): def make_row_0(self):
if self.index < self.phys_in: if self.index < self.phys_in:
if self.parent.kind.name == 'basic': if self.parent.kind.name == "basic":
self.builder.create_audibility_slider() self.builder.create_audibility_slider()
else: else:
self.builder.create_comp_slider() self.builder.create_comp_slider()
@ -153,7 +128,7 @@ class StripConfig(Config):
self.setter(param, val) self.setter(param, val)
if not _configuration.themes_enabled: if not _configuration.themes_enabled:
self.styletable.configure( 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): def toggle_b(self, param):
@ -161,7 +136,7 @@ class StripConfig(Config):
self.setter(param, val) self.setter(param, val)
if not _configuration.themes_enabled: if not _configuration.themes_enabled:
self.styletable.configure( 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): def teardown(self):
@ -177,36 +152,31 @@ class StripConfig(Config):
for i, param in enumerate(self.virt_out_params) for i, param in enumerate(self.virt_out_params)
] ]
[ [
self.bool_param_vars[i].set(self.getter(param)) self.param_vars[i].set(self.getter(param))
for i, param in enumerate(self.bool_params) for i, param in enumerate(self.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 _configuration.themes_enabled: if not _configuration.themes_enabled:
[ [
self.styletable.configure( self.styletable.configure(
f'{param}.TButton', f"{param}.TButton",
background=f'{"green" if self.phys_out_params_vars[i].get() else "white"}', background=f'{"green" if self.phys_out_params_vars[i].get() else "white"}',
) )
for i, param in enumerate(self.phys_out_params) for i, param in enumerate(self.phys_out_params)
] ]
[ [
self.styletable.configure( self.styletable.configure(
f'{param}.TButton', f"{param}.TButton",
background=f'{"green" if self.virt_out_params_vars[i].get() else "white"}', background=f'{"green" if self.virt_out_params_vars[i].get() else "white"}',
) )
for i, param in enumerate(self.virt_out_params) for i, param in enumerate(self.virt_out_params)
] ]
[ [
self.styletable.configure( self.styletable.configure(
f'{param}.TButton', f"{param}.TButton",
background=f'{"green" if self.param_vars[i].get() else "white"}', background=f'{"green" if self.param_vars[i].get() else "white"}',
) )
for i, param in enumerate(self.bool_params) for i, param in enumerate(self.params)
] ]
@ -217,13 +187,12 @@ class BusConfig(Config):
self.grid(column=0, row=1, columnspan=4, padx=(2,)) self.grid(column=0, row=1, columnspan=4, padx=(2,))
else: else:
self.grid(column=0, row=3, columnspan=4, padx=(2,)) self.grid(column=0, row=3, columnspan=4, padx=(2,))
self.builder = builders.BusConfigFrameBuilder(self, parent) self.builder = builders.BusConfigFrameBuilder(self)
self.builder.setup() self.builder.setup()
self.make_row_0() self.make_row_0()
self.make_row_1() self.make_row_1()
self.builder.grid_configure() self.builder.grid_configure()
self.parent.target.clear_dirty()
self.sync() self.sync()
@property @property
@ -237,56 +206,55 @@ class BusConfig(Config):
self.builder.create_bus_mode_button() self.builder.create_bus_mode_button()
def make_row_1(self): def make_row_1(self):
self.builder.create_bus_mono_button()
self.builder.create_param_buttons() self.builder.create_param_buttons()
def current_bus_mode(self): def current_bus_mode(self):
return self.target.mode.get() for mode in self.bus_modes:
if getattr(self.target.mode, mode):
return mode
def rotate_bus_modes_right(self, *args): def rotate_bus_modes_right(self, *args):
current_mode = self.bus_mode_map_reverse[self.bus_mode_label_text.get()] current_mode = self.current_bus_mode()
current_index = self.bus_modes.index(current_mode) next = self.bus_modes.index(current_mode) + 1
next_index = (current_index + 1) % len(self.bus_modes) if next < len(self.bus_modes):
next_mode = self.bus_modes[next_index] setattr(
self.target.mode,
setattr(self.target.mode, next_mode, True) self.bus_modes[next],
self.bus_mode_label_text.set(self.bus_mode_map[next_mode]) 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")
def rotate_bus_modes_left(self, *args): def rotate_bus_modes_left(self, *args):
current_mode = self.bus_mode_map_reverse[self.bus_mode_label_text.get()] current_mode = self.current_bus_mode()
current_index = self.bus_modes.index(current_mode) prev = self.bus_modes.index(current_mode) - 1
prev_index = (current_index - 1) % len(self.bus_modes) if prev < 0:
prev_mode = self.bus_modes[prev_index] self.target.mode.rearonly = True
self.bus_mode_label_text.set("Rear Only")
setattr(self.target.mode, prev_mode, True) else:
self.bus_mode_label_text.set(self.bus_mode_map[prev_mode]) setattr(
self.target.mode,
def rotate_mono_right(self, *args): self.bus_modes[prev],
current_val = self.mono_modes.index(self.bus_mono_label_text.get()) True,
next_val = (current_val + 1) % 3 )
self.bus_mono_label_text.set(self.mono_modes[next_val]) self.bus_mode_label_text.set(self.bus_mode_map[self.bus_modes[prev]])
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): def teardown(self):
self.builder.teardown() self.builder.teardown()
def sync(self): def sync(self):
[ [
self.bool_param_vars[i].set(self.getter(param)) self.param_vars[i].set(self.getter(param))
for i, param in enumerate(self.bool_params) for i, param in enumerate(self.params)
] ]
self.bus_mode_label_text.set(self.bus_mode_map[self.current_bus_mode()]) self.bus_mode_label_text.set(self.bus_mode_map[self.current_bus_mode()])
if not _configuration.themes_enabled: if not _configuration.themes_enabled:
[ [
self.styletable.configure( self.styletable.configure(
f'{param}.TButton', f"{param}.TButton",
background=f'{"green" if self.bool_param_vars[i].get() else "white"}', background=f'{"green" if self.param_vars[i].get() else "white"}',
) )
for i, param in enumerate(self.bool_params) for i, param in enumerate(self.params)
] ]

View File

@ -6,93 +6,57 @@ try:
except ModuleNotFoundError: except ModuleNotFoundError:
import tomli as tomllib import tomli as tomllib
logger = logging.getLogger(__name__) LOGGER = logging.getLogger("configurations")
configuration = {} configuration = {}
config_path = [Path.cwd() / "configs"]
def get_configpath(): for path in config_path:
for pn in ( if path.is_dir():
Path.home() / '.config' / 'vm-compact', filenames = list(path.glob("*.toml"))
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):
configs = {} configs = {}
for filepath in filepaths: for filename in filenames:
filename = filepath.with_suffix('').stem name = filename.with_suffix("").stem
if filename in ('app', 'vban'): try:
try: with open(filename, "rb") as f:
with open(filepath, 'rb') as f: configs[name] = tomllib.load(f)
configs[filename] = tomllib.load(f) except tomllib.TOMLDecodeError:
logger.info(f'configuration: {filename} loaded into memory') print(f"Invalid TOML config: configs/{filename.stem}")
except tomllib.TOMLDecodeError:
logger.error(f'Invalid TOML config: configs/{filename.stem}') for name, cfg in configs.items():
configuration |= configs LOGGER.info(f"Loaded configuration configs/{name}")
configuration[name] = cfg
_defaults = { _defaults = {
'configs': { "configs": {
'config': None, "config": None,
}, },
'theme': { "theme": {
'enabled': True, "enabled": True,
'mode': 'light', "mode": "light",
}, },
'extends': { "extends": {
'extended': True, "extended": True,
'extends_horizontal': True, "extends_horizontal": True,
}, },
'channel': { "channel": {
'width': 80, "width": 80,
'height': 130, "height": 130,
'xpadding': 3,
}, },
'mwscroll_step': { "mwscroll_step": {
'size': 3, "size": 3,
}, },
'submixes': { "submixes": {
'default': 0, "default": 0,
}, },
'navigation': {'show': False},
} }
if "app" in configuration:
if 'app' in configuration: configuration["app"] = _defaults | configuration["app"]
for key in _defaults:
if key in configuration['app']:
configuration['app'][key] = _defaults[key] | configuration['app'][key]
else:
configuration['app'][key] = _defaults[key]
else: else:
configuration['app'] = _defaults configuration["app"] = _defaults
def get_configuration(key): def get_configuration(key):
if key in configuration: if key in configuration:
return configuration[key] return configuration[key]
def loader(kind_id, target):
configs = {'reset': target.configs['reset']}
if configpath := get_configpath():
userconfigpath = configpath / kind_id
if userconfigpath.exists():
filepaths = list(userconfigpath.glob('*.toml'))
for filepath in filepaths:
identifier = filepath.with_suffix('').stem
try:
with open(filepath, 'rb') as f:
configs[identifier] = tomllib.load(f)
logger.info(f'loader: {identifier} loaded into memory')
except tomllib.TOMLDecodeError:
logger.error(f'Invalid TOML config: configs/{filename.stem}')
target.configs = configs
return target.configs

View File

@ -4,7 +4,7 @@ from voicemeeterlib import kinds
from .configurations import get_configuration from .configurations import get_configuration
configuration = get_configuration('app') configuration = get_configuration("app")
class SingletonMeta(type): class SingletonMeta(type):
@ -20,38 +20,33 @@ class SingletonMeta(type):
@dataclass @dataclass
class Configurations(metaclass=SingletonMeta): class Configurations(metaclass=SingletonMeta):
# is the gui extended # is the gui extended
extended: bool = configuration['extends']['extended'] extended: bool = configuration["extends"]["extended"]
# direction the gui extends # direction the gui extends
extends_horizontal: bool = configuration['extends']['extends_horizontal'] extends_horizontal: bool = configuration["extends"]["extends_horizontal"]
# are themes enabled # are themes enabled
themes_enabled: bool = configuration['theme']['enabled'] themes_enabled: bool = configuration["theme"]["enabled"]
# light or dark # light or dark
theme_mode: str = configuration['theme']['mode'] theme_mode: str = configuration["theme"]["mode"]
# size of mousewheel scroll step # size of mousewheel scroll step
mwscroll_step: int = configuration['mwscroll_step']['size'] mwscroll_step: int = configuration["mwscroll_step"]["size"]
# bus assigned as current submix # bus assigned as current submix
submixes: int = configuration['submixes']['default'] submixes: int = configuration["submixes"]["default"]
# width of a single channel labelframe # width of a single labelframe
channel_width: int = configuration['channel']['width'] level_width: int = configuration["channel"]["width"]
# height of a single channel labelframe # height of a single labelframe
channel_height: int = configuration['channel']['height'] level_height: int = configuration["channel"]["height"]
# xpadding for a single channel labelframe
channel_xpadding: int = configuration['channel']['xpadding']
# do we grid the navigation frame?
navigation_show: bool = configuration['navigation']['show']
@property @property
def config(self): def config(self):
if 'configs' in configuration: if "configs" in configuration:
return configuration['configs']['config'] return configuration["configs"]["config"]
@dataclass @dataclass
class BaseValues(metaclass=SingletonMeta): class BaseValues(metaclass=SingletonMeta):
# pause updates after releasing scale # pause updates after releasing scale
run_update: bool = False run_update: bool = True
# are we dragging main window with mouse 1 # are we dragging main window with mouse 1
dragging: bool = False dragging: bool = False
# a vban connection established # a vban connection established
@ -61,6 +56,10 @@ class BaseValues(metaclass=SingletonMeta):
_base_values = BaseValues() _base_values = BaseValues()
_configuration = Configurations() _configuration = Configurations()
_kinds = {kind.name: kind for kind in kinds.kinds_all}
_kinds_all = _kinds.values()
def kind_get(kind_id): def kind_get(kind_id):
return kinds.request_kind_map(kind_id) return _kinds[kind_id]

View File

@ -1,2 +1,4 @@
class VMCompactError(Exception): class VMCompactErrors(Exception):
"""Exception raised when general errors occur""" """Base classs for VMCompact Errors"""
pass

View File

@ -1,4 +1,5 @@
import tkinter as tk import tkinter as tk
from math import log
from tkinter import ttk from tkinter import ttk
from . import builders from . import builders
@ -19,7 +20,7 @@ class GainLayer(ttk.LabelFrame):
else: else:
self.level_offset = parent.phys_in * 2 + (index - parent.phys_in) * 8 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.setup()
self.builder.add_progressbar() self.builder.add_progressbar()
self.builder.add_scale() self.builder.add_scale()
@ -38,54 +39,43 @@ class GainLayer(ttk.LabelFrame):
@property @property
def identifier(self): def identifier(self):
return 'gainlayer' return "gainlayer"
def getter(self, param): def getter(self, param):
try: if hasattr(self.target, param):
return getattr(self.target, param) return getattr(self.target, param)
except AttributeError as e:
self.logger(f'{type(e).__name__}: {e}')
def setter(self, param, value): def setter(self, param, value):
if param in dir(self.target): # avoid calling getattr (with hasattr) if hasattr(self.target, param):
setattr(self.target, param, value) setattr(self.target, param, value)
def reset_gain(self, *args): def reset_gain(self, *args):
self.setter('gain', 0) self.setter("gain", 0)
self.gain.set(0) self.gain.set(0)
self.gainlabel.set(self.gain.get()) self.gainlabel.set(self.gain.get())
def scale_callback(self, *args): def scale_callback(self, *args):
"""callback function for scale widget""" """callback function for scale widget"""
val = round(self.gain.get(), 1) self.setter("gain", self.gain.get())
self.setter('gain', val) self.gainlabel.set(round(self.gain.get(), 1))
self.gainlabel.set(val)
def scale_press(self, *args): def scale_press(self, *args):
self.after(1, self.remove_events) self.after(1, self.remove_events)
def remove_events(self): def remove_events(self):
self.parent.target.event.remove('pdirty') self.parent.target.event.remove("pdirty")
self.parent.target.event.remove('ldirty') self.parent.target.event.remove("ldirty")
def scale_release(self, *args): def scale_release(self, *args):
_base_values.run_update = False _base_values.run_update = False
self.after(1, self.add_events) self.after(1, self.add_events)
def add_events(self): def add_events(self):
self.parent.target.event.add('pdirty') self.parent.target.event.add("pdirty")
self.parent.target.event.add('ldirty') self.parent.target.event.add("ldirty")
self.after(500, self.resume_updates) self.after(500, self.resume_updates)
def pause_updates(self, func, *args):
"""function wrapper, adds a 50ms delay on updates"""
_base_values.run_update = False
func(*args)
self.after(50, self.resume_updates)
def resume_updates(self): def resume_updates(self):
_base_values.run_update = True _base_values.run_update = True
@ -103,7 +93,7 @@ class GainLayer(ttk.LabelFrame):
self.gain.set(12) self.gain.set(12)
elif self.gain.get() < -60: elif self.gain.get() < -60:
self.gain.set(-60) self.gain.set(-60)
self.setter('gain', self.gain.get()) self.setter("gain", self.gain.get())
self.after(1, self.resume_updates) self.after(1, self.resume_updates)
def set_on(self): def set_on(self):
@ -116,20 +106,20 @@ class GainLayer(ttk.LabelFrame):
) )
if not _configuration.themes_enabled: if not _configuration.themes_enabled:
self.styletable.configure( 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"}', background=f'{"green" if self.on.get() else "white"}',
) )
def on_update(self, subject): def on_update(self, subject):
if subject == 'ldirty': if subject == "ldirty":
self.upd_levels() self.upd_levels()
elif subject == 'pdirty': elif subject == "pdirty":
self.sync_params() self.sync_params()
elif subject == 'labelframe': elif subject == "labelframe":
self.after(5, self.sync_labels) self.after(5, self.sync_labels)
def sync_params(self): 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.gainlabel.set(round(self.gain.get(), 1))
self.on.set( self.on.set(
getattr( getattr(
@ -139,7 +129,7 @@ class GainLayer(ttk.LabelFrame):
) )
if not _configuration.themes_enabled: if not _configuration.themes_enabled:
self.styletable.configure( 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"}', background=f'{"green" if self.on.get() else "white"}',
) )
@ -147,7 +137,7 @@ class GainLayer(ttk.LabelFrame):
"""sync params with voicemeeter""" """sync params with voicemeeter"""
retval = self.parent.target.strip[self.index].label retval = self.parent.target.strip[self.index].label
if len(retval) > 10: if len(retval) > 10:
retval = f'{retval[:8]}..' retval = f"{retval[:8]}.."
if not retval: if not retval:
self.parent.columnconfigure(self.index, minsize=0) self.parent.columnconfigure(self.index, minsize=0)
self.parent.parent.subject.remove(self) self.parent.parent.subject.remove(self)
@ -161,21 +151,18 @@ class GainLayer(ttk.LabelFrame):
""" """
Updates level values. Updates level values.
""" """
if self.parent.target.strip[self.index].levels.is_updated: if self.parent.target.strip[self.index].levels.is_updated:
val = max(self.parent.target.strip[self.index].levels.prefader) val = max(self.parent.target.strip[self.index].levels.prefader)
# Convert dB to progressbar: -60dB=0, 0dB=60, +12dB=72 self.level.set(
if ( (
self.parent.parent.strip_frame.strips[self.index].mute.get() 0
or not self.on.get() if self.parent.target.strip[self.index].mute or not self.on.get()
): else 100 + val - 18 + self.gain.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): def grid_configure(self):
self.grid(padx=_configuration.channel_xpadding, sticky=(tk.N, tk.S))
[ [
child.grid_configure(padx=1, pady=1, sticky=(tk.N, tk.S, tk.W, tk.E)) child.grid_configure(padx=1, pady=1, sticky=(tk.N, tk.S, tk.W, tk.E))
for child in self.winfo_children() for child in self.winfo_children()
@ -202,8 +189,8 @@ class SubMixFrame(ttk.Frame):
self.parent = parent self.parent = parent
self.phys_in, self.virt_in = parent.kind.ins self.phys_in, self.virt_in = parent.kind.ins
self.phys_out, self.virt_out = parent.kind.outs self.phys_out, self.virt_out = parent.kind.outs
self.buses = tuple(f'A{i + 1}' for i in range(self.phys_out)) + tuple( 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) f"B{i+1}" for i in range(self.virt_out)
) )
self.gainlayers = [ self.gainlayers = [
@ -222,7 +209,7 @@ class SubMixFrame(ttk.Frame):
else: else:
if parent.bus_frame and parent.bus_frame.grid_info(): if parent.bus_frame and parent.bus_frame.grid_info():
self.grid( 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() parent.bus_frame.grid_remove()
else: else:
@ -257,17 +244,17 @@ class SubMixFrame(ttk.Frame):
) )
def on_update(self, subject): def on_update(self, subject):
if subject == 'pdirty': if subject == "pdirty":
for labelframe in self.labelframes: for labelframe in self.labelframes:
labelframe.on_update('labelframe') labelframe.on_update("labelframe")
def grid_configure(self): def grid_configure(self):
[ [
self.columnconfigure(i, minsize=_configuration.channel_width) self.columnconfigure(i, minsize=_configuration.level_width)
for i, _ in enumerate(self.labelframes) for i, _ in enumerate(self.labelframes)
] ]
[ [
self.rowconfigure(0, minsize=_configuration.channel_height) self.rowconfigure(0, minsize=_configuration.level_height)
for i, _ in enumerate(self.labelframes) for i, _ in enumerate(self.labelframes)
] ]

View File

@ -1 +0,0 @@
from .main import run

View File

@ -1,11 +0,0 @@
import voicemeeterlib
import vmcompact
def run():
KIND_ID = 'banana'
with voicemeeterlib.api(KIND_ID) as vmr:
app = vmcompact.connect(KIND_ID, vmr)
app.mainloop()

View File

@ -1 +0,0 @@
from .main import run

View File

@ -1,11 +0,0 @@
import voicemeeterlib
import vmcompact
def run():
KIND_ID = 'basic'
with voicemeeterlib.api(KIND_ID) as vmr:
app = vmcompact.connect(KIND_ID, vmr)
app.mainloop()

View File

@ -1 +0,0 @@
from .main import run

View File

@ -1,11 +0,0 @@
import voicemeeterlib
import vmcompact
def run():
KIND_ID = 'potato'
with voicemeeterlib.api(KIND_ID) as vmr:
app = vmcompact.connect(KIND_ID, vmr)
app.mainloop()

View File

@ -2,37 +2,34 @@ import logging
import tkinter as tk import tkinter as tk
import webbrowser import webbrowser
from functools import partial from functools import partial
from tkinter import messagebox from tkinter import messagebox, ttk
import sv_ttk import sv_ttk
import vban_cmd import vban_cmd
from vban_cmd.error import VBANCMDConnectionError from vban_cmd.error import VBANCMDError
from .data import _base_values, _configuration, get_configuration, kind_get from .data import _base_values, _configuration, get_configuration, kind_get
logger = logging.getLogger(__name__)
class Menus(tk.Menu): class Menus(tk.Menu):
logger = logging.getLogger("menu.menus")
def __init__(self, parent, vmr): def __init__(self, parent, vmr):
super().__init__() super().__init__()
self.parent = parent self.parent = parent
self.vmr = vmr self.vmr = vmr
self.logger = logger.getChild(self.__class__.__name__) self.vban_config = get_configuration("vban")
self.vban_config = get_configuration('vban') self.app_config = get_configuration("app")
self.app_config = get_configuration('app')
self._is_topmost = tk.BooleanVar() self._is_topmost = tk.BooleanVar()
self._lock = tk.BooleanVar() self._lock = tk.BooleanVar()
self._unlock = tk.BooleanVar() self._unlock = tk.BooleanVar()
self._navigation_show = tk.BooleanVar(value=_configuration.navigation_show)
self._navigation_hide = tk.BooleanVar(value=not _configuration.navigation_show)
self._selected_bus = list(tk.BooleanVar() for _ in range(8)) self._selected_bus = list(tk.BooleanVar() for _ in range(8))
# voicemeeter menu # voicemeeter menu
self.menu_voicemeeter = tk.Menu(self, tearoff=0) 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( self.menu_voicemeeter.add_checkbutton(
label='Always On Top', label="Always On Top",
onvalue=1, onvalue=1,
offvalue=0, offvalue=0,
variable=self._is_topmost, variable=self._is_topmost,
@ -40,80 +37,78 @@ class Menus(tk.Menu):
) )
self.menu_voicemeeter.add_separator() self.menu_voicemeeter.add_separator()
self.menu_voicemeeter.add_command( self.menu_voicemeeter.add_command(
label='Show', label="Show",
underline=0, underline=0,
command=partial(self.action_invoke_voicemeeter, 'show'), command=partial(self.action_invoke_voicemeeter, "show"),
) )
self.menu_voicemeeter.add_command( self.menu_voicemeeter.add_command(
label='Hide', label="Hide",
underline=0, underline=0,
command=partial(self.action_invoke_voicemeeter, 'hide'), command=partial(self.action_invoke_voicemeeter, "hide"),
) )
self.menu_voicemeeter.add_command( self.menu_voicemeeter.add_command(
label='Restart', label="Restart",
underline=0, underline=0,
command=partial(self.action_invoke_voicemeeter, 'restart'), command=partial(self.action_invoke_voicemeeter, "restart"),
) )
self.menu_voicemeeter.add_command( self.menu_voicemeeter.add_command(
label='Shutdown', label="Shutdown",
underline=0, underline=0,
command=partial(self.action_invoke_voicemeeter, 'shutdown'), command=partial(self.action_invoke_voicemeeter, "shutdown"),
) )
self.menu_voicemeeter.add_separator() self.menu_voicemeeter.add_separator()
self.menu_lock = tk.Menu(self.menu_voicemeeter, tearoff=0) self.menu_lock = tk.Menu(self.menu_voicemeeter, tearoff=0)
self.menu_voicemeeter.add_cascade( 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( self.menu_lock.add_checkbutton(
label='Lock', label="Lock",
onvalue=1, onvalue=1,
offvalue=0, offvalue=0,
variable=self._lock, variable=self._lock,
command=partial(self.action_set_voicemeeter, 'lock'), command=partial(self.action_set_voicemeeter, "lock"),
) )
self.menu_lock.add_checkbutton( self.menu_lock.add_checkbutton(
label='Unlock', label="Unlock",
onvalue=1, onvalue=1,
offvalue=0, offvalue=0,
variable=self._unlock, variable=self._unlock,
command=partial(self.action_set_voicemeeter, 'lock', False), command=partial(self.action_set_voicemeeter, "lock", False),
) )
# configs menu # configs menu
self.menu_configs = tk.Menu(self, tearoff=0) 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_load = tk.Menu(self.menu_configs, tearoff=0)
self.menu_configs.add_cascade(menu=self.menu_configs_load, label='Load config') self.menu_configs.add_cascade(menu=self.menu_configs_load, label="Load config")
self.config_defaults = {'reset'} self.config_defaults = {"reset"}
if len(self.parent.userconfigs) > len(self.config_defaults) and all( if len(self.target.configs) > len(self.config_defaults) and all(
key in self.parent.userconfigs for key in self.config_defaults key in self.target.configs for key in self.config_defaults
): ):
[ [
self.menu_configs_load.add_command( self.menu_configs_load.add_command(
label=profile, command=partial(self.load_profile, profile) label=profile, command=partial(self.load_profile, profile)
) )
for profile in self.parent.userconfigs.keys() for profile in self.target.configs.keys()
if profile not in self.config_defaults if profile not in self.config_defaults
] ]
else: else:
self.menu_configs.entryconfig(0, state='disabled') self.menu_configs.entryconfig(0, state="disabled")
self.menu_configs.add_command( self.menu_configs.add_command(
label='Reset to defaults', command=self.load_defaults label="Reset to defaults", command=self.load_defaults
) )
# layout menu # layout menu
self.menu_layout = tk.Menu(self, tearoff=0) 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 # layout/submixes
# here we build menu regardless of kind but disable if not potato # here we build menu regardless of kind but disable if not potato
buses = tuple(f'A{i + 1}' for i in range(5)) + tuple( buses = tuple(f"A{i+1}" for i in range(5)) + tuple(f"B{i+1}" for i in range(3))
f'B{i + 1}' for i in range(3)
)
self.menu_submixes = tk.Menu(self.menu_layout, tearoff=0) 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( self.menu_submixes.add_checkbutton(
label=f'Bus {buses[i]}', label=f"Bus {buses[i]}",
underline=0, underline=0,
onvalue=1, onvalue=1,
offvalue=0, offvalue=0,
@ -123,94 +118,77 @@ class Menus(tk.Menu):
for i in range(8) for i in range(8)
] ]
self._selected_bus[_configuration.submixes].set(True) self._selected_bus[_configuration.submixes].set(True)
if self.parent.kind.name != 'potato': if self.parent.kind.name != "potato":
self.menu_layout.entryconfig(0, state='disabled') self.menu_layout.entryconfig(0, state="disabled")
# layout/extends # layout/extends
self.menu_extends = tk.Menu(self.menu_layout, tearoff=0) self.menu_extends = tk.Menu(self.menu_layout, tearoff=0)
self.menu_layout.add_cascade( 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( self.menu_extends.add_command(
label='horizontal', label="horizontal",
underline=0, underline=0,
command=partial(self.switch_orientation, extends_horizontal=True), command=partial(self.switch_orientation, extends_horizontal=True),
) )
self.menu_extends.add_command( self.menu_extends.add_command(
label='vertical', label="vertical",
underline=0, underline=0,
command=partial(self.switch_orientation, extends_horizontal=False), command=partial(self.switch_orientation, extends_horizontal=False),
) )
self.menu_extends.entryconfig( self.menu_extends.entryconfig(
0 if _configuration.extends_horizontal else 1, state='disabled' 0 if _configuration.extends_horizontal else 1, state="disabled"
) )
# layout/themes # layout/themes
self.menu_themes = tk.Menu(self.menu_layout, tearoff=0) 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( 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( 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( self.menu_themes.entryconfig(
0 if self.app_config['theme']['mode'] == 'light' else 1, 0 if self.app_config["theme"]["mode"] == "light" else 1,
state='disabled', state="disabled",
) )
if not _configuration.themes_enabled: 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_navigation.add_checkbutton(
label='show',
onvalue=1,
offvalue=0,
variable=self._navigation_show,
command=partial(self.toggle_navigation, 'show'),
)
self.menu_navigation.add_checkbutton(
label='hide',
onvalue=1,
offvalue=0,
variable=self._navigation_hide,
command=partial(self.toggle_navigation, 'hide'),
)
# vban connect menu # vban connect menu
self.menu_vban = tk.Menu(self, tearoff=0) 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: if self.vban_config:
for i, _ in enumerate(self.vban_config): for i, _ in enumerate(self.vban_config):
setattr(self, f'menu_vban_{i + 1}', tk.Menu(self.menu_vban, tearoff=0)) setattr(self, f"menu_vban_{i+1}", tk.Menu(self.menu_vban, tearoff=0))
target_menu = getattr(self, f'menu_vban_{i + 1}') target_menu = getattr(self, f"menu_vban_{i+1}")
self.menu_vban.add_cascade( self.menu_vban.add_cascade(
menu=target_menu, 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, underline=0,
) )
target_menu.add_command( target_menu.add_command(
label='Connect', command=partial(self.vban_connect, i) label="Connect", command=partial(self.vban_connect, i)
) )
target_menu.add_command( 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: else:
self.entryconfig(4, state='disabled') self.entryconfig(4, state="disabled")
# Help menu # Help menu
self.menu_help = tk.Menu(self, tearoff=0) 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( self.menu_help.add_command(
label='Voicemeeter Site', label="Voicemeeter Site",
command=self.documentation, command=self.documentation,
) )
self.menu_help.add_command( self.menu_help.add_command(
label='Source Code', label="Source Code",
command=self.github, command=self.github,
) )
self.menu_help.add_command( self.menu_help.add_command(
label='App Creator', label="App Creator",
command=self.onyxandiris, command=self.onyxandiris,
) )
@ -221,56 +199,40 @@ class Menus(tk.Menu):
def enable_vban_menus(self): 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()) for j, _ in enumerate(self.menu_vban.winfo_children())
] ]
def action_invoke_voicemeeter(self, cmd): def action_invoke_voicemeeter(self, cmd):
if fn := getattr(self.target.command, cmd): getattr(self.target.command, cmd)()
fn()
if cmd == 'shutdown':
self.parent.on_close_window()
def action_set_voicemeeter(self, cmd, val=True): def action_set_voicemeeter(self, cmd, val=True):
if cmd == 'lock': if cmd == "lock":
self._lock.set(val) self._lock.set(val)
self._unlock.set(not self._lock.get()) self._unlock.set(not self._lock.get())
setattr(self.target.command, cmd, val) setattr(self.target.command, cmd, val)
def load_custom_profile(self, 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): def load_profile(self, profile):
self.logger.info(f'loading user profile {profile}')
self.target.apply_config(profile) self.target.apply_config(profile)
if not _base_values.run_update:
self.parent.subject.notify('pdirty')
def load_defaults(self): def load_defaults(self):
msg = ( resp = messagebox.askyesno(
'Are you sure you want to Reset values to defaults?', message="Are you sure you want to Reset values to defaults?\nPhysical strips B1, Virtual strips A1\nMono, Solo, Mute, EQ all OFF"
'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))
if resp: if resp:
self.load_profile('reset') self.target.apply_config("reset")
def always_on_top(self): 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): def switch_orientation(self, extends_horizontal: bool = True, *args):
_configuration.extends_horizontal = extends_horizontal _configuration.extends_horizontal = extends_horizontal
if extends_horizontal: if extends_horizontal:
self.menu_extends.entryconfig(0, state='disabled') self.menu_extends.entryconfig(0, state="disabled")
self.menu_extends.entryconfig(1, state='normal') self.menu_extends.entryconfig(1, state="normal")
else: else:
self.menu_extends.entryconfig(1, state='disabled') self.menu_extends.entryconfig(1, state="disabled")
self.menu_extends.entryconfig(0, state='normal') self.menu_extends.entryconfig(0, state="normal")
def set_submix(self, i): def set_submix(self, i):
if _configuration.submixes != i: if _configuration.submixes != i:
@ -280,103 +242,86 @@ class Menus(tk.Menu):
self.parent.nav_frame.show_submix() self.parent.nav_frame.show_submix()
for j, var in enumerate(self._selected_bus): for j, var in enumerate(self._selected_bus):
var.set(i == j) var.set(i == j)
self.parent.subject.notify('submix')
def load_theme(self, theme): def load_theme(self, theme):
sv_ttk.set_theme(theme) sv_ttk.set_theme(theme)
_configuration.theme_mode = theme _configuration.theme_mode = theme
self.menu_themes.entryconfig( self.menu_themes.entryconfig(
0, 0,
state=f'{"disabled" if theme == "light" else "normal"}', state=f"{'disabled' if theme == 'light' else 'normal'}",
) )
self.menu_themes.entryconfig( self.menu_themes.entryconfig(
1, 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() for menu in self.winfo_children()
if isinstance(menu, tk.Menu) if isinstance(menu, tk.Menu)
] ]
self.menu_lock.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"}') 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() for menu in self.menu_vban.winfo_children()
if isinstance(menu, tk.Menu) 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() for menu in self.menu_layout.winfo_children()
if isinstance(menu, tk.Menu) if isinstance(menu, tk.Menu)
] ]
self.logger.info( 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): def menu_teardown(self, i):
# remove config load menus # remove config load menus
if len(self.parent.userconfigs) > len(self.config_defaults): [
for profile in self.parent.userconfigs: self.menu_configs_load.delete(key)
if profile not in self.config_defaults: for key in self.target.configs.keys()
try: if key not in self.config_defaults
self.menu_configs_load.delete(profile) ]
except tk._tkinter.tclError as 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()) for j, _ in enumerate(self.menu_vban.winfo_children())
if j != i if j != i
] ]
def menu_setup(self): def menu_setup(self):
if len(self.parent.userconfigs) > len(self.config_defaults): if len(self.target.configs) > len(self.config_defaults) and all(
for profile in self.parent.userconfigs: key in self.target.configs for key in self.config_defaults
if profile not in self.config_defaults: ):
self.menu_configs_load.add_command( [
label=profile, command=partial(self.load_profile, profile) self.menu_configs_load.add_command(
) label=profile, command=partial(self.load_profile, profile)
self.menu_configs.entryconfig(0, state='normal') )
for profile in self.target.configs.keys()
if profile not in self.config_defaults
]
else: 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')
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.parent.nav_frame.grid_remove()
self._navigation_hide.set(True)
self._navigation_show.set(not self._navigation_hide.get())
def vban_connect(self, i): def vban_connect(self, i):
opts = {} opts = {}
opts |= self.vban_config[f'connection-{i + 1}'] opts |= self.vban_config[f"connection-{i+1}"]
kind_id = opts.pop('kind') kind_id = opts.pop("kind")
if 'ip' in opts:
opts['host'] = opts.pop('ip')
self.vban = vban_cmd.api(kind_id, **opts) self.vban = vban_cmd.api(kind_id, **opts)
# login to vban interface # login to vban interface
try: try:
self.logger.info(f'Attempting vban connection to {opts.get("host")}') self.logger.info(f"Attempting vban connection to {opts.get('ip')}")
self.vban.login() self.vban.login()
except VBANCMDConnectionError as e: except VBANCMDError as e:
self.vban.logout() self.vban.logout()
msg = ( msg = (str(e), f"Please check your connection settings")
f'Timeout attempting to establish connection to {opts.get("host")}', messagebox.showerror("Connection Error", "\n".join(msg))
'Please check your connection settings', 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) self.after(1, self.enable_vban_menus)
return return
self.menu_teardown(i) self.menu_teardown(i)
self.vban.event.add(['pdirty', 'ldirty']) self.vban.event.add("ldirty")
# destroy the current App frames # destroy the current App frames
self.parent._destroy_top_level_frames() self.parent._destroy_top_level_frames()
_base_values.vban_connected = True _base_values.vban_connected = True
@ -385,17 +330,12 @@ class Menus(tk.Menu):
# build new app frames according to a kind # build new app frames according to a kind
kind = kind_get(kind_id) kind = kind_get(kind_id)
self.parent.build_app(kind, self.vban) self.parent.build_app(kind, self.vban)
target_menu = getattr(self, f'menu_vban_{i + 1}') target_menu = getattr(self, f"menu_vban_{i+1}")
target_menu.entryconfig(0, state='disabled') target_menu.entryconfig(0, state="disabled")
target_menu.entryconfig(1, state='normal') target_menu.entryconfig(1, state="normal")
self.menu_layout.entryconfig( 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']
self.menu_setup() self.menu_setup()
def vban_disconnect(self, i): def vban_disconnect(self, i):
@ -409,27 +349,22 @@ class Menus(tk.Menu):
self.vban.logout() self.vban.logout()
# build new app frames according to a kind # build new app frames according to a kind
kind = kind_get(self.vmr.type) kind = kind_get(self.vmr.type)
self.parent.build_app(kind) self.parent.build_app(kind, None)
target_menu = getattr(self, f'menu_vban_{i + 1}') target_menu = getattr(self, f"menu_vban_{i+1}")
target_menu.entryconfig(0, state='normal') target_menu.entryconfig(0, state="normal")
target_menu.entryconfig(1, state='disabled') target_menu.entryconfig(1, state="disabled")
self.menu_layout.entryconfig( 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']
self.menu_setup() self.menu_setup()
self.after(50, self.enable_vban_menus) self.after(15000, self.enable_vban_menus)
def documentation(self): def documentation(self):
webbrowser.open_new(r'https://voicemeeter.com/') webbrowser.open_new(r"https://voicemeeter.com/")
def github(self): 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): def onyxandiris(self):
webbrowser.open_new(r'https://onyxandiris.online') webbrowser.open_new(r"https://onyxandiris.online")

View File

@ -6,17 +6,14 @@ from . import builders
from .data import _configuration from .data import _configuration
from .gainlayer import SubMixFrame from .gainlayer import SubMixFrame
logger = logging.getLogger(__name__)
class Navigation(ttk.Frame): class Navigation(ttk.Frame):
logger = logging.getLogger("navigation.navigation")
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent) super().__init__(parent)
self.parent = parent self.parent = parent
self.logger = logger.getChild(self.__class__.__name__)
self.grid(row=0, column=3, padx=(0, 2), pady=(5, 5), sticky=(tk.W, tk.E)) self.grid(row=0, column=3, padx=(0, 2), pady=(5, 5), sticky=(tk.W, tk.E))
if not _configuration.navigation_show:
self.grid_remove()
self.styletable = self.parent.styletable self.styletable = self.parent.styletable
self.builder = builders.NavigationFrameBuilder(self) self.builder = builders.NavigationFrameBuilder(self)
@ -33,7 +30,7 @@ class Navigation(ttk.Frame):
if self.submix.get(): if self.submix.get():
self.parent.submix_frame = SubMixFrame(self.parent) self.parent.submix_frame = SubMixFrame(self.parent)
self.logger.info( self.logger.info(
f'Finished building submixframe for submix {_configuration.submixes}' f"Finished building submixframe for submix {_configuration.submixes}"
) )
else: else:
if _configuration.extends_horizontal: if _configuration.extends_horizontal:
@ -49,51 +46,51 @@ class Navigation(ttk.Frame):
else: else:
self.parent.rowconfigure(2, weight=0, minsize=0) self.parent.rowconfigure(2, weight=0, minsize=0)
self.logger.info( 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: if not _configuration.themes_enabled:
self.styletable.configure( self.styletable.configure(
'Submix.TButton', f"Submix.TButton",
background=f'{"purple" if self.submix.get() else "white"}', background=f'{"purple" if self.submix.get() else "white"}',
) )
def switch_channel(self): def switch_channel(self):
if self.channel_text.get() == 'STRIP': if self.channel_text.get() == "STRIP":
self.mainframebuilder.create_channelframe('bus') self.mainframebuilder.create_channelframe("bus")
self.parent.strip_frame.teardown() self.parent.strip_frame.teardown()
else: else:
self.mainframebuilder.create_channelframe('strip') self.mainframebuilder.create_channelframe("strip")
self.parent.bus_frame.teardown() self.parent.bus_frame.teardown()
self.extend_button['state'] = ( self.extend_button["state"] = (
'disabled' if self.channel_text.get() == 'STRIP' else 'normal' "disabled" if self.channel_text.get() == "STRIP" else "normal"
) )
[frame.teardown() for frame in self.parent.configframes] [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): def extend_frame(self):
_configuration.extended = self.extend.get() _configuration.extended = self.extend.get()
if self.extend.get(): if self.extend.get():
self.channel_button['state'] = 'disabled' self.channel_button["state"] = "disabled"
self.mainframebuilder.create_channelframe('bus') self.mainframebuilder.create_channelframe("bus")
else: else:
[ [
frame.teardown() frame.teardown()
for frame in self.parent.configframes 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.teardown()
self.parent.bus_frame = None self.parent.bus_frame = None
self.channel_button['state'] = 'normal' self.channel_button["state"] = "normal"
if self.parent.submix_frame: if self.parent.submix_frame:
self.parent.submix_frame.teardown() self.parent.submix_frame.teardown()
self.submix.set(False) self.submix.set(False)
if not _configuration.themes_enabled: if not _configuration.themes_enabled:
self.styletable.configure( self.styletable.configure(
'Submix.TButton', f"Submix.TButton",
background=f'{"purple" if self.submix.get() else "white"}', 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")

View File

@ -1,34 +0,0 @@
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']