Compare commits

...

73 Commits
v1.6.0 ... main

Author SHA1 Message Date
293bccc5ba call task from poe
remove build.ps1 and scripts.py
2025-02-13 16:23:35 +00:00
1d8ffdc756 mark rewrite,restore tasks as internal 2025-02-12 18:26:43 +00:00
e4d87334cb reformat 2025-02-07 22:58:30 +00:00
ad3020809e merge commands 2025-02-07 16:37:00 +00:00
76c6630892 fix deferred task 2025-02-07 15:09:36 +00:00
cc46fc31f8 upd build script, format files/dirs the same as Taskfile 2025-02-07 15:09:21 +00:00
8657e8846a add Taskfile 2025-02-07 14:53:21 +00:00
43aad156a0 upd flags passed to rewriter 2025-02-07 14:53:07 +00:00
5101ff01f2 change --cleanup flag for --restore
run file through ruff formatter
2025-02-07 14:52:40 +00:00
c437ae5843 rename entry points 2025-01-29 15:55:24 +00:00
ae59ba30f9 add 1.9.8 section to CHANGELOG 2025-01-22 16:46:25 +00:00
a3fa227ac1 patch bump 2025-01-22 16:38:52 +00:00
b1b6c66828 reduce the time vban menus are re-enabled after a disconnect 2025-01-22 16:38:44 +00:00
cb00de36f0 add _internal/configs to config paths.
vm-compact dirs now override _internal/config

upd README TOML Files section
2025-01-22 16:30:06 +00:00
ae200068d0 upd vban-cmd dep 2025-01-17 03:06:55 +00:00
b720494c68 add gui scripts 2025-01-16 20:38:29 +00:00
6e6308a17f patch bump 2025-01-15 21:03:40 +00:00
848248d02b run example snippet through formatter 2025-01-15 20:58:47 +00:00
e4fc68c1ad re-run through ruff formatter 2025-01-15 20:56:37 +00:00
752d1d7dd9 add ruff badge 2025-01-15 20:56:25 +00:00
70c225bda3 upd spec path 2025-01-15 20:56:11 +00:00
f459cdee44 remake pyproject with poetry 2 2025-01-15 20:10:30 +00:00
198c08003e use {Bus}.mode.get() to fetch current bus mode
vban-cmd dep bump

patch bump
2024-07-05 18:06:22 +01:00
6fa9bf7131 add 1.9.5 section to CHANGELOG 2024-07-03 08:06:06 +01:00
c3f20ea43f patch bump 2024-07-03 07:57:21 +01:00
2e868705d5 bump dependencies 2024-07-02 22:24:51 +01:00
df6f215514
Merge pull request #18 from onyx-and-iris/dependabot/pip/black-24.3.0
Bump black from 22.12.0 to 24.3.0
2024-03-28 16:10:04 +00:00
dependabot[bot]
0a5987631f
Bump black from 22.12.0 to 24.3.0
Bumps [black](https://github.com/psf/black) from 22.12.0 to 24.3.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/22.12.0...24.3.0)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-28 16:09:32 +00:00
3d3bc71d15 upd rewriter to reflect changes to pyinstaller 2024-01-30 15:07:20 +00:00
7b148b2614 upd build dep pyinstaller 2024-01-30 14:11:13 +00:00
ef558fdde6 dependency updates 2024-01-30 11:09:15 +00:00
c684ed9981 should a config be loaded during engine startup
bypass _base_values.run_update

patch bump
2023-09-07 08:39:20 +01:00
1498daf36f refactor 2023-08-28 05:17:54 +01:00
e964c94d07 update dependency groups 2023-08-27 18:42:26 +01:00
9f20225a59 build scripts added 2023-08-27 02:41:07 +01:00
f63c36c94a add poetry badge to readme 2023-08-19 19:58:27 +01:00
c2db0f2757 dependencies updated.
patch bump
2023-08-06 23:16:20 +01:00
bfb0482c32 on_close_window() callback added.
cleanly shuts down vban connection on windows close
if vban connected.
2023-08-06 23:15:54 +01:00
6222ab1e62 id renamed to identifier in _make_channelframe()
label_cache arrays now initialised with empty strings

update_levels() now called when initialising ChannelFrame
2023-08-06 23:15:08 +01:00
0ad40ab708 use vm.version for healthcheck
update deps

patch bump
2023-07-13 01:32:01 +01:00
b809bcb28f 1.9.0 section added to changelog
minor version bump
2023-07-11 08:36:19 +01:00
a0b9a92a2a added healthcheck_step
checks connection to Voicemeeter.
2023-07-11 08:35:23 +01:00
9faf8ae10c easier to read 2023-07-11 01:27:05 +01:00
82cf0e914b wrap button callbacks with {cls}.pause_updates() 2023-07-11 01:26:46 +01:00
3e68488231 bump deps 2023-07-08 18:19:06 +01:00
6d46d9a9a5 userconfigs now returns target.configs
patch bump
2023-07-08 00:22:07 +01:00
e4068277f7 ensure we don't attempt to delete a menu key twice
patch bump
2023-07-07 03:37:06 +01:00
674999a461 remove callbacks instead
patch bump
2023-06-30 04:27:10 +01:00
e5975f0772 fixes bug where old configs may not have new keys
patch bump
2023-06-29 19:13:06 +01:00
59d2a95ec4 1.8.0 section added to changelog
minor bump
2023-06-29 18:24:54 +01:00
4bae1e1d15 set greace period if gui was launched by the api 2023-06-29 18:14:38 +01:00
1e3751b19f channel_xpadding and navigation_show added to data.
app.toml example now includes channel padding and nav show

[channel] xpadding and [navigation] show added to app.toml

delay parameter updates if gui needed launching
2023-06-29 17:15:03 +01:00
2ec1c74b7d navigation_menu added, toggles the nav frame.
may be configured through app.toml
2023-06-29 15:51:17 +01:00
0a19e28370 xpadding on channels
may be configured through app.toml
2023-06-29 15:50:39 +01:00
5ab1fd7102 add loader to handle userprofiles in vm-compact
ensure we reload user profiles into memory
when rebuilding the config menu

patch bump
2023-06-26 16:08:58 +01:00
d896193ade md fix 2023-06-26 14:18:23 +01:00
8c0c31dc0d 1.7.0 section added 2023-06-26 14:17:24 +01:00
c8b3e9fc33 dependencies updated.
minor version bump
2023-06-26 13:58:22 +01:00
d4358bf7d3 update comp, gate, eq settings in example.toml 2023-06-26 13:57:39 +01:00
194b95d67b use subject class to notify of busmix update
update messageboxes
2023-06-26 13:57:08 +01:00
0f734e87b7 upd docstring 2023-06-26 13:56:06 +01:00
944ef9ca1c updates to events, callbacks 2023-06-26 13:55:50 +01:00
fc20bb0c1e configpaths added to configurations 2023-06-26 13:53:38 +01:00
5ccc2a6dab error class renamed
doc string expanded
2023-06-26 13:52:42 +01:00
cfc1279f6c module level loggers added 2023-06-26 13:52:24 +01:00
d4df11f62d optimizations to reduce the number of api calls
comp, gate, eq parameter calls updated.
2023-06-26 13:51:21 +01:00
onyx-and-iris
3b6e1f61a4 patch ver bump 2022-10-19 14:54:25 +01:00
onyx-and-iris
3f306ddf62 now using event property setters. 2022-10-19 14:54:00 +01:00
onyx-and-iris
732368a65b fix math error in level updates 2022-10-05 23:39:51 +01:00
onyx-and-iris
d7df79b798 upd vban-cmd ver
patch bump
2022-10-05 23:12:25 +01:00
onyx-and-iris
0906f3343b upd dependencies
patch bump
2022-10-05 23:03:46 +01:00
onyx-and-iris
294dfe7d03 changes to progressbar level udpates 2022-10-05 23:03:27 +01:00
onyx-and-iris
4db7be172b bump dep versions
minor version bump
2022-10-04 16:06:47 +01:00
31 changed files with 1556 additions and 605 deletions

6
.gitignore vendored
View File

@ -131,3 +131,9 @@ dmypy.json
# Pyre type checker
.pyre/
# build
theme/
spec/
.vscode/

View File

@ -7,7 +7,72 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- [ ] Add support for forest theme (should be coming soon)
- [ ]
## [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

View File

@ -1,6 +1,7 @@
[![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)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/)
[![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)
![Image of app/potato size comparison](./doc_imgs/potatocomparisonsmaller.png)
@ -30,20 +31,21 @@ Example `__main__.py` file:
```python
import voicemeeterlib
import vmcompact
def main():
# pass the kind_id and the vm object to the app
with voicemeeterlib.api(kind_id) as vm:
app = vmcompact.connect(kind_id, vm)
# choose the kind of Voicemeeter (Local connection)
KIND_ID = 'banana'
# 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()
if __name__ == "__main__":
# choose the kind of Voicemeeter (Local connection)
kind_id = "banana"
if __name__ == '__main__':
main()
```
@ -53,9 +55,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 :).
### kind_id
### KIND_ID
Set the kind of Voicemeeter, kind_id may be:
Set the kind of Voicemeeter, KIND_ID may be:
- `basic`
- `banana`
@ -63,15 +65,18 @@ Set the kind of Voicemeeter, kind_id may be:
## TOML Files
This is how your files should be organised. Wherever your `__main__.py` file is located (after install this can be any location), `configs` should be in the same location.
Directly inside of configs directory you may place an app.toml, vban.toml and a directory for each kind.
Inside each kind directory you may place as many custom toml configurations as you wish.
If you've downloaded the binary from [Releases][releases] you can find configs included in the `_internal/configs` directory.
You may override these configs by placing a directory `vm-compact` in one of the following locations:
- `user home directory / .config`
- `user home directory / Documents / Voicemeeter`
The contents should match the following directory structure:
.
├── `__main__.py`
├── configs
├── vm-compact
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;├── app.toml
@ -109,7 +114,7 @@ Configure certain startup states for the app.
Configure a user config to load on app startup. Don't include the .toml extension in the config name.
- `theme`
By default the app loads up the [Sun Valley light or dark theme](https://github.com/rdbende/Sun-Valley-ttk-theme) by @rdbende. You have the option to load up the app without any theme loaded. Simply set `enabled` to false and `mode` will take no effect.
By default the app loads up the [Sun Valley light or dark theme][releases] by @rdbende. You have the option to load up the app without any theme loaded. Simply set `enabled` to false and `mode` will take no effect.
- `extends`
Extending the app will show both strips and buses. In reduced mode only one or the other. This app will extend both horizontally and vertically, simply set `extends_horizontal` true or false accordingly.
@ -149,7 +154,7 @@ port = 6990
Three example user configs are included with the package, one for each kind of Voicemeeter. Use these to configure parameter startup states. Any parameter supported by the underlying interfaces may be used. Check the 'multiple-parameters' section for more info:
[Python Interface for Voicemeeter API](https://github.com/onyx-and-iris/voicemeeter-api-python#multiple-parameters)
[Python Interface for the Voicemeeter API](https://github.com/onyx-and-iris/voicemeeter-api-python#multiple-parameters)
[Python Interface for VBAN CMD](https://github.com/onyx-and-iris/vban-cmd-python#multiple-parameters)
@ -157,6 +162,10 @@ User configs may be loaded at any time via the menu.
## Special Thanks
[Vincent Burel](https://github.com/vburel2018) for creating Voicemeeter, its SDK, the C Remote API, the RT Packet service and Streamer View app!
[Vincent Burel](https://github.com/vburel2018) for creating Voicemeeter and its SDK.
[Rdbende](https://github.com/rdbende) for creating the beautiful Sun Valley Tkinter theme and adding it to Pypi!
[Rdbende](https://github.com/rdbende) for creating the beautiful [Sun Valley theme][sv-theme].
[sv-theme]: https://github.com/rdbende/Sun-Valley-ttk-theme
[releases]: https://github.com/onyx-and-iris/voicemeeter-compact/releases

81
Taskfile.yml Normal file
View File

@ -0,0 +1,81 @@
version: '3'
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"
build:
desc: Build all artifacts
cmds:
- task: build-sunvalley
- echo "Sunvalley build complete"
- task: build-forest
- echo "Forest build complete"
build-sunvalley:
desc: Build Sunvalley artifacts
cmds:
- for:
matrix:
KIND: [basic, banana, potato]
cmd: poetry run pyinstaller --noconfirm --distpath dist/sunvalley-{{.ITEM.KIND}} spec/sunvalley-{{.ITEM.KIND}}.spec
build-forest:
desc: Build Forest artifacts
deps: [rewrite]
cmds:
- defer: { task: restore }
- for:
matrix:
KIND: [basic, banana, potato]
THEME: [light, dark]
cmd: poetry run pyinstaller --noconfirm --distpath dist/forest-{{.ITEM.KIND}}-{{.ITEM.THEME}} spec/forest-{{.ITEM.KIND}}-{{.ITEM.THEME}}.spec
rewrite:
internal: true
desc: Run the source code rewriter
cmds:
- poetry run python tools/rewriter.py --rewrite
restore:
internal: true
desc: Restore the backup files
cmds:
- poetry run python tools/rewriter.py --restore
compress:
deps: [compress-sunvalley, compress-forest]
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"'
compress-forest:
cmds:
- for:
matrix:
KIND: [basic, banana, potato]
THEME: [light, dark]
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/forest-{{.ITEM.KIND}}-{{.ITEM.THEME}} -DestinationPath dist/forest-{{.ITEM.KIND}}-{{.ITEM.THEME}}.zip -Force"'
clean:
desc: Clean up build and dist directories
cmds:
- |
{{.SHELL}} -Command "
Remove-Item -Path build/forest-*,build/sunvalley-*,dist/forest-*,dist/sunvalley-* -Recurse -Force"

View File

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

View File

@ -3,19 +3,23 @@
# config="example"
# load with themes enabled? set the default mode
[theme]
enabled=true
mode="light"
enabled = true
mode = "light"
# load in extended mode? if so which orientation
[extends]
extended=true
extends_horizontal=true
extended = true
extends_horizontal = true
# default dimensions for channel label frames
[channel]
width=80
height=130
width = 80
height = 130
xpadding = 2
# size of a single mouse wheel scroll step
[mwscroll_step]
size=3
size = 3
# default submix bus
[submixes]
default=0
default = 0
# show the navigation frame?
[navigation]
show = true

View File

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

View File

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

View File

@ -4,10 +4,10 @@
# kind = 'banana'
# ip = '<ip address 1>'
# streamname = 'Command1'
# port = 6990
# port = 6980
# [connection-2]
# kind = 'potato'
# ip = '<ip address 2>'
# streamname = 'Command1'
# port = 6990
# port = 6980

305
poetry.lock generated
View File

@ -1,122 +1,257 @@
[[package]]
name = "black"
version = "22.8.0"
description = "The uncompromising code formatter."
category = "dev"
optional = false
python-versions = ">=3.6.2"
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
[[package]]
name = "click"
version = "8.1.3"
description = "Composable command line interface toolkit"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.5"
description = "Cross-platform colored terminal text."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
name = "altgraph"
version = "0.17.4"
description = "Python graph (network) package"
optional = false
python-versions = "*"
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 = "pathspec"
version = "0.10.1"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
name = "macholib"
version = "1.16.3"
description = "Mach-O header analysis and editing"
optional = false
python-versions = ">=3.7"
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]
altgraph = ">=0.17"
[[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"
name = "packaging"
version = "24.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
groups = ["build"]
files = [
{file = "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]
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
completion = ["argcomplete"]
hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"]
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2024.11"
description = "Community maintained hooks for PyInstaller"
optional = false
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
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "ruff-0.9.1-py3-none-linux_armv6l.whl", hash = "sha256:84330dda7abcc270e6055551aca93fdde1b0685fc4fd358f26410f9349cf1743"},
{file = "ruff-0.9.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3cae39ba5d137054b0e5b472aee3b78a7c884e61591b100aeb544bcd1fc38d4f"},
{file = "ruff-0.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:50c647ff96f4ba288db0ad87048257753733763b409b2faf2ea78b45c8bb7fcb"},
{file = "ruff-0.9.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0c8b149e9c7353cace7d698e1656ffcf1e36e50f8ea3b5d5f7f87ff9986a7ca"},
{file = "ruff-0.9.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:beb3298604540c884d8b282fe7625651378e1986c25df51dec5b2f60cafc31ce"},
{file = "ruff-0.9.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39d0174ccc45c439093971cc06ed3ac4dc545f5e8bdacf9f067adf879544d969"},
{file = "ruff-0.9.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:69572926c0f0c9912288915214ca9b2809525ea263603370b9e00bed2ba56dbd"},
{file = "ruff-0.9.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:937267afce0c9170d6d29f01fcd1f4378172dec6760a9f4dface48cdabf9610a"},
{file = "ruff-0.9.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:186c2313de946f2c22bdf5954b8dd083e124bcfb685732cfb0beae0c47233d9b"},
{file = "ruff-0.9.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f94942a3bb767675d9a051867c036655fe9f6c8a491539156a6f7e6b5f31831"},
{file = "ruff-0.9.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:728d791b769cc28c05f12c280f99e8896932e9833fef1dd8756a6af2261fd1ab"},
{file = "ruff-0.9.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2f312c86fb40c5c02b44a29a750ee3b21002bd813b5233facdaf63a51d9a85e1"},
{file = "ruff-0.9.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ae017c3a29bee341ba584f3823f805abbe5fe9cd97f87ed07ecbf533c4c88366"},
{file = "ruff-0.9.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5dc40a378a0e21b4cfe2b8a0f1812a6572fc7b230ef12cd9fac9161aa91d807f"},
{file = "ruff-0.9.1-py3-none-win32.whl", hash = "sha256:46ebf5cc106cf7e7378ca3c28ce4293b61b449cd121b98699be727d40b79ba72"},
{file = "ruff-0.9.1-py3-none-win_amd64.whl", hash = "sha256:342a824b46ddbcdddd3abfbb332fa7fcaac5488bf18073e841236aadf4ad5c19"},
{file = "ruff-0.9.1-py3-none-win_arm64.whl", hash = "sha256:1cd76c7f9c679e6e8f2af8f778367dca82b95009bc7b1a85a47f1521ae524fa7"},
{file = "ruff-0.9.1.tar.gz", hash = "sha256:fd2b25ecaf907d6458fa842675382c8597b3c746a2dde6717fe3415425df0c17"},
]
[[package]]
name = "setuptools"
version = "75.8.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.9"
groups = ["build"]
files = [
{file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"},
{file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"},
]
[package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"]
core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
enabler = ["pytest-enabler (>=2.2)"]
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "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", "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)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"]
[[package]]
name = "sv-ttk"
version = "2.0"
description = "A gorgeous theme for Tkinter that looks like Windows 11"
category = "main"
version = "2.6.0"
description = "A gorgeous theme for Tkinter, based on Windows 11's UI"
optional = false
python-versions = ">=3.4"
python-versions = ">=3.8"
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]]
name = "tomli"
version = "2.0.1"
version = "2.2.1"
description = "A lil' TOML parser"
category = "main"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version < \"3.11\""
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]]
name = "vban-cmd"
version = "1.5.2"
version = "2.5.0"
description = "Python interface for the VBAN RT Packet Service (Sendtext)"
category = "main"
optional = false
python-versions = ">=3.10,<4.0"
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "vban_cmd-2.5.0-py3-none-any.whl", hash = "sha256:22a19037066487d464a61941a3b85a0331b498a9efb1bcacdc932e9d06c5bf87"},
{file = "vban_cmd-2.5.0.tar.gz", hash = "sha256:691a852e5052e50103839b06a0a9d0746b90df3346545c2cf4f10b099d9666e4"},
]
[package.dependencies]
tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
tomli = {version = ">=2.0.1,<3.0", markers = "python_version < \"3.11\""}
[[package]]
name = "voicemeeter-api"
version = "0.8.1"
version = "2.6.1"
description = "A Python wrapper for the Voiceemeter API"
category = "main"
optional = false
python-versions = ">=3.10,<4.0"
python-versions = "<4.0,>=3.10"
groups = ["main"]
files = [
{file = "voicemeeter_api-2.6.1-py3-none-any.whl", hash = "sha256:8ae3bce0f9ad6bbad78f2f69f522b6fb2e229d314918a075ad83d4009aff7020"},
{file = "voicemeeter_api-2.6.1.tar.gz", hash = "sha256:34d8672603ec66197f2d61fd8f038f46d8451759c81fbe222b00e7d3ccccd1f5"},
]
[package.dependencies]
tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
tomli = {version = ">=2.0.1,<3.0", markers = "python_version < \"3.11\""}
[metadata]
lock-version = "1.1"
python-versions = "^3.10"
content-hash = "0fc1f7b08a87f389504c898142c5a0f2ed20c8f56deabb3028fa815774b7fc98"
[metadata.files]
black = []
click = []
colorama = []
mypy-extensions = []
pathspec = []
platformdirs = []
sv-ttk = []
tomli = []
vban-cmd = []
voicemeeter-api = []
lock-version = "2.1"
python-versions = ">=3.10,<3.14"
content-hash = "19c384acd36868a5bfdc3f3173f444858136603694c3f1134c0d30cd17157651"

View File

@ -1,27 +1,131 @@
[tool.poetry]
[project]
name = "voicemeeter-compact"
version = "1.6.0"
version = "1.9.8"
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" },
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.6.1,<3.0.0)",
"vban-cmd (>=2.5.0,<3.0.0)",
"sv-ttk (>=2.6.0,<3.0.0)",
"tomli (>=2.0.1,<3.0) ; python_version < '3.11'",
]
[project.scripts]
gui-basic-compact = "vmcompact.gui.basic:run"
gui-banana-compact = "vmcompact.gui.banana:run"
gui-potato-compact = "vmcompact.gui.potato:run"
[tool.poetry]
packages = [{ include = "vmcompact" }]
include = ["vmcompact/img/cat.ico"]
[tool.poetry.dependencies]
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.requires-plugins]
poethepoet = "^0.32.1"
[tool.poetry.dev-dependencies]
black = {version = "^22.6.0", allow-prereleases = true}
[tool.poetry.group.dev.dependencies]
ruff = "^0.9.1"
[tool.poetry.group.build.dependencies]
pyinstaller = "^6.11.1"
[build-system]
requires = ["poetry-core>=1.0.0"]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poe.tasks]
build-sunvalley = "task build-sunvalley"
build-forest = "task build-forest"
release = [
{ ref = "build-sunvalley" },
{ ref = "build-forest" },
{ cmd = "task compress-sunvalley" },
{ cmd = "task compress-forest" },
]
[tool.ruff]
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".mypy_cache",
".nox",
".pants.d",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
]
# Same as Black.
line-length = 88
indent-width = 4
# Assume Python 3.10
target-version = "py310"
[tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
select = ["E4", "E7", "E9", "F"]
ignore = []
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.format]
# Unlike Black, use single quotes for strings.
quote-style = "single"
# Like Black, indent with spaces, rather than tabs.
indent-style = "space"
# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"
# Enable auto-formatting of code examples in docstrings. Markdown,
# reStructuredText code/literal blocks and doctests are all supported.
#
# This is currently disabled by default, but it is planned for this
# to be opt-out in the future.
docstring-code-format = false
# Set the line length limit used when formatting code snippets in
# docstrings.
#
# This only has an effect when the `docstring-code-format` setting is
# enabled.
docstring-code-line-length = "dynamic"
[tool.ruff.lint.mccabe]
max-complexity = 10
[tool.ruff.lint.per-file-ignores]
"__init__.py" = [
"E402",
"F401",
]

250
tools/rewriter.py Normal file
View File

@ -0,0 +1,250 @@
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():
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:
# App init()
case ' def __init__(self, vmr):\n':
output.write(' def __init__(self, vmr, theme):\n')
case ' self._vmr = vmr\n':
write_outs(
output,
(
' self._vmr = vmr\n',
' self._theme = theme\n',
' tcldir = Path.cwd() / "theme"\n',
' if not tcldir.is_dir():\n',
' tcldir = Path.cwd() / "_internal" / "theme"\n',
' self.tk.call("source", tcldir.resolve() / f"forest-{self._theme}.tcl")\n',
),
)
# def connect()
case 'def connect(kind_id: str, vmr) -> App:\n':
output.write(
'def connect(kind_id: str, vmr, theme="light") -> App:\n'
)
case ' return VMMIN_cls(vmr)\n':
output.write(' return VMMIN_cls(vmr, theme)\n')
case _:
output.write(line)
def rewrite_builders():
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':
write_outs(
output,
(
' self.app.resizable(False, False)\n'
' if _configuration.themes_enabled:\n',
' ttk.Style().theme_use(f"forest-{self.app._theme}")\n',
' self.logger.info(f"Forest 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:
output.write(line.replace('Toggle.TButton', 'ToggleButton'))
else:
output.write(line)
def rewrite_menu():
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 prepare_for_build():
################# MOVE FILES FROM PACKAGE DIR INTO SRC DIR #########################
for file in (
PACKAGE_DIR / 'app.py',
PACKAGE_DIR / 'builders.py',
PACKAGE_DIR / 'menu.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 #########################
steps = (
rewrite_app,
rewrite_builders,
rewrite_menu,
)
[step() for step in steps]
def cleanup():
########################## RESTORE *.BK FILES #####################################
for file in (
PACKAGE_DIR / 'app.py',
PACKAGE_DIR / 'builders.py',
PACKAGE_DIR / 'menu.py',
):
file.unlink()
for file in (
SRC_DIR / 'app.bk',
SRC_DIR / 'builders.bk',
SRC_DIR / 'menu.bk',
):
file.rename(PACKAGE_DIR / f'{file.stem}.py')
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--rewrite', action='store_true')
parser.add_argument('--restore', action='store_true')
args = parser.parse_args()
if args.rewrite:
logger.info('preparing files for build')
prepare_for_build()
elif args.restore:
logger.info('cleaning up files')
cleanup()

0
tools/src/.gitkeep Normal file
View File

View File

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

View File

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

View File

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

View File

@ -12,6 +12,8 @@ from .config import BusConfig, StripConfig
from .data import _base_values, _configuration
from .navigation import Navigation
logger = logging.getLogger(__name__)
class AbstractBuilder(abc.ABC):
@abc.abstractmethod
@ -28,11 +30,10 @@ class AbstractBuilder(abc.ABC):
class MainFrameBuilder(AbstractBuilder):
"""Responsible for building the frames that sit directly on the mainframe"""
logger = logging.getLogger("builders.mainframebuilder")
def __init__(self, app):
self.kind = app.kind
self.app = app
self.logger = logger.getChild(self.__class__.__name__)
def setup(self):
self.app.title(
@ -40,31 +41,31 @@ class MainFrameBuilder(AbstractBuilder):
)
self.app.resizable(False, False)
if _configuration.themes_enabled:
if sv_ttk.get_theme() not in ("light", "dark"):
if sv_ttk.get_theme() not in ('light', 'dark'):
sv_ttk.set_theme(_configuration.theme_mode)
self.logger.info(
f"Sunvalley {sv_ttk.get_theme().capitalize()} Theme applied"
f'Sunvalley {sv_ttk.get_theme().capitalize()} Theme applied'
)
def create_channelframe(self, type_):
if type_ == "strip":
if type_ == 'strip':
self.app.strip_frame = _make_channelframe(self.app, type_)
else:
self.app.bus_frame = _make_channelframe(self.app, type_)
self.logger.info(f"Finished building channelframe type {type_}")
self.logger.info(f'Finished building channelframe type {type_}')
def create_separator(self):
self.app.sep = ttk.Separator(self.app, orient="vertical")
self.app.sep = ttk.Separator(self.app, orient='vertical')
self.app.sep.grid(row=0, column=1, sticky=(tk.N, tk.S))
self.app.columnconfigure(1, minsize=15)
self.logger.info(f"Finished building separator")
self.logger.info('Finished building separator')
def create_navframe(self):
self.app.nav_frame = Navigation(self.app)
self.logger.info(f"Finished building navframe")
self.logger.info('Finished building navframe')
def create_configframe(self, type_, index, id):
if type_ == "strip":
if type_ == 'strip':
self.app.config_frame = StripConfig(self.app, index, id)
if self.app.strip_frame:
[
@ -94,20 +95,20 @@ class MainFrameBuilder(AbstractBuilder):
if self.app.strip_frame:
[
frame.styletable.configure(
f"{frame.identifier}Conf{frame.index}.TButton",
background=f"{'white' if not frame.conf.get() else 'yellow'}",
f'{frame.identifier}Conf{frame.index}.TButton',
background=f'{"white" if not frame.conf.get() else "yellow"}',
)
for _, frame in enumerate(self.app.strip_frame.labelframes)
]
if self.app.bus_frame:
[
frame.styletable.configure(
f"{frame.identifier}Conf{frame.index}.TButton",
background=f"{'white' if not frame.conf.get() else 'yellow'}",
f'{frame.identifier}Conf{frame.index}.TButton',
background=f'{"white" if not frame.conf.get() else "yellow"}',
)
for _, frame in enumerate(self.app.bus_frame.labelframes)
]
self.logger.info(f"Finished building configframe for {type_}[{index}]")
self.logger.info(f'Finished building configframe for {type_}[{index}]')
self.app.after(5, self.reset_config_frames)
def reset_config_frames(self):
@ -120,7 +121,7 @@ class MainFrameBuilder(AbstractBuilder):
def create_banner(self):
self.app.banner = Banner(self.app)
self.app.banner.grid(row=4, column=0, columnspan=3)
self.logger.info(f"Finished building banner")
self.logger.info('Finished building banner')
def teardown(self):
pass
@ -139,31 +140,31 @@ class NavigationFrameBuilder(AbstractBuilder):
self.navframe.info = tk.BooleanVar()
self.navframe.channel_text = tk.StringVar(
value=f"{self.navframe.parent.strip_frame.identifier.upper()}"
value=f'{self.navframe.parent.strip_frame.identifier.upper()}'
)
self.navframe.extend_text = tk.StringVar(
value=f"{'REDUCE' if self.navframe.extend.get() else 'EXTEND'}"
value=f'{"REDUCE" if self.navframe.extend.get() else "EXTEND"}'
)
self.navframe.info_text = tk.StringVar()
def create_submix_button(self):
self.navframe.submix_button = ttk.Checkbutton(
self.navframe,
text="SUBMIX",
text='SUBMIX',
command=self.navframe.show_submix,
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'Submix.TButton'}",
style=f'{"Toggle.TButton" if _configuration.themes_enabled else "Submix.TButton"}',
variable=self.navframe.submix,
)
self.navframe.submix_button.grid(column=0, row=0)
if self.navframe.parent.kind.name != "potato":
self.navframe.submix_button["state"] = "disabled"
if self.navframe.parent.kind.name != 'potato':
self.navframe.submix_button['state'] = 'disabled'
def create_channel_button(self):
self.navframe.channel_button = ttk.Checkbutton(
self.navframe,
textvariable=self.navframe.channel_text,
command=self.navframe.switch_channel,
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'Channel.TButton'}",
style=f'{"Toggle.TButton" if _configuration.themes_enabled else "Channel.TButton"}',
variable=self.navframe.channel,
)
self.navframe.channel_button.grid(column=0, row=1, rowspan=1)
@ -173,7 +174,7 @@ class NavigationFrameBuilder(AbstractBuilder):
self.navframe,
textvariable=self.navframe.extend_text,
command=self.navframe.extend_frame,
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'Extend.TButton'}",
style=f'{"Toggle.TButton" if _configuration.themes_enabled else "Extend.TButton"}',
variable=self.navframe.extend,
)
self.navframe.extend_button.grid(column=0, row=2)
@ -182,7 +183,7 @@ class NavigationFrameBuilder(AbstractBuilder):
self.navframe.info_button = ttk.Checkbutton(
self.navframe,
textvariable=self.navframe.info_text,
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'Rec.TButton'}",
style=f'{"Toggle.TButton" if _configuration.themes_enabled else "Rec.TButton"}',
variable=self.navframe.info,
)
self.navframe.info_button.grid(column=0, row=3)
@ -194,9 +195,9 @@ class NavigationFrameBuilder(AbstractBuilder):
if isinstance(child, ttk.Checkbutton)
]
if _configuration.themes_enabled:
self.navframe.rowconfigure(1, minsize=_configuration.level_height)
self.navframe.rowconfigure(1, minsize=_configuration.channel_height)
else:
self.navframe.rowconfigure(1, minsize=_configuration.level_height + 10)
self.navframe.rowconfigure(1, minsize=_configuration.channel_height + 10)
def teardown(self):
pass
@ -226,9 +227,9 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
"""Adds a progress bar widget to a single label frame"""
self.labelframe.pb = ttk.Progressbar(
self.labelframe,
maximum=100,
orient="vertical",
mode="determinate",
maximum=72,
orient='vertical',
mode='determinate',
variable=self.labelframe.level,
)
self.labelframe.pb.grid(column=0, row=0)
@ -239,16 +240,22 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
self.labelframe,
from_=12.0,
to=-60.0,
orient="vertical",
orient='vertical',
variable=self.labelframe.gain,
command=self.labelframe.scale_callback,
length=_configuration.level_height,
length=_configuration.channel_height,
)
self.scale.grid(column=1, row=0)
self.scale.bind("<Double-Button-1>", self.labelframe.reset_gain)
self.scale.bind("<Button-1>", self.labelframe.scale_press)
self.scale.bind("<ButtonRelease-1>", self.labelframe.scale_release)
self.scale.bind("<MouseWheel>", self.labelframe._on_mousewheel)
self.scale.bind('<Double-Button-1>', self.labelframe.reset_gain)
self.scale.bind('<Button-1>', self.labelframe.scale_press)
self.scale.bind('<ButtonRelease-1>', self.labelframe.scale_release)
self.scale.bind(
'<MouseWheel>',
partial(
self.labelframe.pause_updates,
self.labelframe._on_mousewheel,
),
)
def add_gain_label(self):
self.labelframe.gain_label = ttk.Label(
@ -261,9 +268,9 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
"""Adds a mute button widget to a single label frame"""
self.button_mute = ttk.Checkbutton(
self.labelframe,
text="MUTE",
command=partial(self.labelframe.toggle_mute, "mute"),
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{self.identifier}Mute{self.index}.TButton'}",
text='MUTE',
command=partial(self.labelframe.pause_updates, self.labelframe.toggle_mute),
style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{self.identifier}Mute{self.index}.TButton"}',
variable=self.labelframe.mute,
)
self.button_mute.grid(column=0, row=2, columnspan=2)
@ -271,9 +278,9 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
def add_conf_button(self):
self.button_conf = ttk.Checkbutton(
self.labelframe,
text="CONFIG",
text='CONFIG',
command=self.labelframe.open_config,
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{self.identifier}Conf{self.index}.TButton'}",
style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{self.identifier}Conf{self.index}.TButton"}',
variable=self.labelframe.conf,
)
self.button_conf.grid(column=0, row=3, columnspan=2)
@ -281,9 +288,9 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
def add_on_button(self):
self.button_on = ttk.Checkbutton(
self.labelframe,
text="ON",
command=self.labelframe.set_on,
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{self.identifier}On{self.index}.TButton'}",
text='ON',
command=partial(self.labelframe.pause_updates, self.labelframe.set_on),
style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{self.identifier}On{self.index}.TButton"}',
variable=self.labelframe.on,
)
self.button_on.grid(column=0, row=2, columnspan=2)
@ -323,7 +330,7 @@ class ChannelConfigFrameBuilder(AbstractBuilder):
]
self.configframe.grid(sticky=(tk.W))
[
self.configframe.columnconfigure(i, minsize=_configuration.level_width)
self.configframe.columnconfigure(i, minsize=_configuration.channel_width)
for i in range(self.configframe.phys_out + self.configframe.virt_out)
]
@ -332,40 +339,40 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
"""Responsible for building channel configframe widgets"""
def setup(self):
if self.configframe.parent.kind.name == "basic":
self.configframe.slider_params = ("audibility",)
if self.configframe.parent.kind.name == 'basic':
self.configframe.slider_params = ('audibility',)
self.configframe.slider_vars = (tk.DoubleVar(),)
else:
self.configframe.slider_params = ("comp", "gate", "limit")
self.configframe.slider_params = ('comp.knob', 'gate.knob', 'limit')
self.configframe.slider_vars = [
tk.DoubleVar() for _ in self.configframe.slider_params
]
self.configframe.phys_out_params = [
f"A{i+1}" for i in range(self.configframe.phys_out)
f'A{i + 1}' for i in range(self.configframe.phys_out)
]
self.configframe.phys_out_params_vars = [
tk.BooleanVar() for _ in self.configframe.phys_out_params
]
self.configframe.virt_out_params = [
f"B{i+1}" for i in range(self.configframe.virt_out)
f'B{i + 1}' for i in range(self.configframe.virt_out)
]
self.configframe.virt_out_params_vars = [
tk.BooleanVar() for _ in self.configframe.virt_out_params
]
self.configframe.params = ("mono", "solo")
self.configframe.params = ('mono', 'solo')
self.configframe.param_vars = list(
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:
self.configframe.params = list(
map(lambda x: x.replace("mono", "mc"), self.configframe.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
# karaoke modes not in RT Packet yet. May implement in future
"""
@ -381,101 +388,101 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
== self.configframe.phys_in + self.configframe.virt_in - 1
):
self.configframe.params = list(
map(lambda x: x.replace("mono", "mc"), self.configframe.params)
map(lambda x: x.replace('mono', 'mc'), self.configframe.params)
)
def create_comp_slider(self):
comp_label = ttk.Label(self.configframe, text="Comp")
comp_label = ttk.Label(self.configframe, text='Comp')
comp_scale = ttk.Scale(
self.configframe,
from_=0.0,
to=10.0,
orient="horizontal",
length=_configuration.level_width,
orient='horizontal',
length=_configuration.channel_width,
variable=self.configframe.slider_vars[
self.configframe.slider_params.index("comp")
self.configframe.slider_params.index('comp.knob')
],
command=partial(self.configframe.scale_callback, "comp"),
command=partial(self.configframe.scale_callback, 'comp.knob'),
)
comp_scale.bind(
"<Double-Button-1>", partial(self.configframe.reset_scale, "comp", 0)
'<Double-Button-1>', partial(self.configframe.reset_scale, 'comp.knob', 0)
)
comp_scale.bind("<Button-1>", self.configframe.scale_press)
comp_scale.bind("<ButtonRelease-1>", self.configframe.scale_release)
comp_scale.bind("<Enter>", partial(self.configframe.scale_enter, "comp"))
comp_scale.bind("<Leave>", self.configframe.scale_leave)
comp_scale.bind('<Button-1>', self.configframe.scale_press)
comp_scale.bind('<ButtonRelease-1>', self.configframe.scale_release)
comp_scale.bind('<Enter>', partial(self.configframe.scale_enter, 'comp.knob'))
comp_scale.bind('<Leave>', self.configframe.scale_leave)
comp_label.grid(column=0, row=0)
comp_scale.grid(column=1, row=0)
def create_gate_slider(self):
gate_label = ttk.Label(self.configframe, text="Gate")
gate_label = ttk.Label(self.configframe, text='Gate')
gate_scale = ttk.Scale(
self.configframe,
from_=0.0,
to=10.0,
orient="horizontal",
length=_configuration.level_width,
orient='horizontal',
length=_configuration.channel_width,
variable=self.configframe.slider_vars[
self.configframe.slider_params.index("gate")
self.configframe.slider_params.index('gate.knob')
],
command=partial(self.configframe.scale_callback, "gate"),
command=partial(self.configframe.scale_callback, 'gate.knob'),
)
gate_scale.bind(
"<Double-Button-1>", partial(self.configframe.reset_scale, "gate", 0)
'<Double-Button-1>', partial(self.configframe.reset_scale, 'gate.knob', 0)
)
gate_scale.bind("<Button-1>", self.configframe.scale_press)
gate_scale.bind("<ButtonRelease-1>", self.configframe.scale_release)
gate_scale.bind("<Enter>", partial(self.configframe.scale_enter, "gate"))
gate_scale.bind("<Leave>", self.configframe.scale_leave)
gate_scale.bind('<Button-1>', self.configframe.scale_press)
gate_scale.bind('<ButtonRelease-1>', self.configframe.scale_release)
gate_scale.bind('<Enter>', partial(self.configframe.scale_enter, 'gate.knob'))
gate_scale.bind('<Leave>', self.configframe.scale_leave)
gate_label.grid(column=2, row=0)
gate_scale.grid(column=3, row=0)
def create_limit_slider(self):
limit_label = ttk.Label(self.configframe, text="Limit")
limit_label = ttk.Label(self.configframe, text='Limit')
limit_scale = ttk.Scale(
self.configframe,
from_=-40,
to=12,
orient="horizontal",
length=_configuration.level_width,
orient='horizontal',
length=_configuration.channel_width,
variable=self.configframe.slider_vars[
self.configframe.slider_params.index("limit")
self.configframe.slider_params.index('limit')
],
command=partial(self.configframe.scale_callback, "limit"),
command=partial(self.configframe.scale_callback, 'limit'),
)
limit_scale.bind(
"<Double-Button-1>", partial(self.configframe.reset_scale, "limit", 12)
'<Double-Button-1>', partial(self.configframe.reset_scale, 'limit', 12)
)
limit_scale.bind("<Button-1>", self.configframe.scale_press)
limit_scale.bind("<ButtonRelease-1>", self.configframe.scale_release)
limit_scale.bind("<Enter>", partial(self.configframe.scale_enter, "limit"))
limit_scale.bind("<Leave>", self.configframe.scale_leave)
limit_scale.bind('<Button-1>', self.configframe.scale_press)
limit_scale.bind('<ButtonRelease-1>', self.configframe.scale_release)
limit_scale.bind('<Enter>', partial(self.configframe.scale_enter, 'limit'))
limit_scale.bind('<Leave>', self.configframe.scale_leave)
limit_label.grid(column=4, row=0)
limit_scale.grid(column=5, row=0)
def create_audibility_slider(self):
aud_label = ttk.Label(self.configframe, text="Audibility")
aud_label = ttk.Label(self.configframe, text='Audibility')
aud_scale = ttk.Scale(
self.configframe,
from_=0.0,
to=10.0,
orient="horizontal",
length=_configuration.level_width,
orient='horizontal',
length=_configuration.channel_width,
variable=self.configframe.slider_vars[
self.configframe.slider_params.index("audibility")
self.configframe.slider_params.index('audibility')
],
command=partial(self.configframe.scale_callback, "audibility"),
command=partial(self.configframe.scale_callback, 'audibility'),
)
aud_scale.bind(
"<Double-Button-1>", partial(self.configframe.reset_scale, "audibility", 0)
'<Double-Button-1>', partial(self.configframe.reset_scale, 'audibility', 0)
)
aud_scale.bind("<Button-1>", self.configframe.scale_press)
aud_scale.bind("<ButtonRelease-1>", self.configframe.scale_release)
aud_scale.bind("<Enter>", partial(self.configframe.scale_enter, "audibility"))
aud_scale.bind("<Leave>", self.configframe.scale_leave)
aud_scale.bind('<Button-1>', self.configframe.scale_press)
aud_scale.bind('<ButtonRelease-1>', self.configframe.scale_release)
aud_scale.bind('<Enter>', partial(self.configframe.scale_enter, 'audibility'))
aud_scale.bind('<Leave>', self.configframe.scale_leave)
aud_label.grid(column=0, row=0)
aud_scale.grid(column=1, row=0)
@ -485,8 +492,10 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
ttk.Checkbutton(
self.configframe,
text=param,
command=partial(self.configframe.toggle_a, param),
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{param}.TButton'}",
command=partial(
self.configframe.pause_updates, self.configframe.toggle_a, param
),
style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{param}.TButton"}',
variable=self.configframe.phys_out_params_vars[
self.configframe.phys_out_params.index(param)
],
@ -506,8 +515,10 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
ttk.Checkbutton(
self.configframe,
text=param,
command=partial(self.configframe.toggle_b, param),
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{param}.TButton'}",
command=partial(
self.configframe.pause_updates, self.configframe.toggle_b, param
),
style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{param}.TButton"}',
variable=self.configframe.virt_out_params_vars[
self.configframe.virt_out_params.index(param)
],
@ -527,8 +538,10 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
ttk.Checkbutton(
self.configframe,
text=param,
command=partial(self.configframe.toggle_p, param),
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{param}.TButton'}",
command=partial(
self.configframe.pause_updates, self.configframe.toggle_p, param
),
style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{param}.TButton"}',
variable=self.configframe.param_vars[i],
)
for i, param in enumerate(self.configframe.params)
@ -563,7 +576,7 @@ class BusConfigFrameBuilder(ChannelConfigFrameBuilder):
}
self.configframe.bus_modes = list(self.configframe.bus_mode_map.keys())
# fmt: on
self.configframe.params = ("mono", "eq", "eq_ab")
self.configframe.params = ('mono', 'eq.on', 'eq.ab')
self.configframe.param_vars = [tk.BooleanVar() for _ in self.configframe.params]
self.configframe.bus_mode_label_text = tk.StringVar(
value=self.configframe.bus_mode_map[self.configframe.current_bus_mode()]
@ -577,10 +590,16 @@ class BusConfigFrameBuilder(ChannelConfigFrameBuilder):
column=0, row=0, columnspan=2, sticky=(tk.W)
)
self.configframe.busmode_button.bind(
"<Button-1>", self.configframe.rotate_bus_modes_right
'<Button-1>',
partial(
self.configframe.pause_updates, self.configframe.rotate_bus_modes_right
),
)
self.configframe.busmode_button.bind(
"<Button-3>", self.configframe.rotate_bus_modes_left
'<Button-3>',
partial(
self.configframe.pause_updates, self.configframe.rotate_bus_modes_left
),
)
def create_param_buttons(self):
@ -588,8 +607,10 @@ class BusConfigFrameBuilder(ChannelConfigFrameBuilder):
ttk.Checkbutton(
self.configframe,
text=param,
command=partial(self.configframe.toggle_p, param),
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{param}.TButton'}",
command=partial(
self.configframe.pause_updates, self.configframe.toggle_p, param
),
style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{param}.TButton"}',
variable=self.configframe.param_vars[i],
)
for i, param in enumerate(self.configframe.params)

View File

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

View File

@ -1,10 +1,11 @@
import tkinter as tk
from functools import partial
import logging
from tkinter import ttk
from . import builders
from .data import _base_values, _configuration
logger = logging.getLogger(__name__)
class Config(ttk.Frame):
def __init__(self, parent, index, _id):
@ -12,6 +13,7 @@ class Config(ttk.Frame):
self.parent = parent
self.index = index
self.id = _id
self.logger = logger.getChild(self.__class__.__name__)
self.styletable = parent.styletable
self.phys_in, self.virt_in = parent.kind.ins
self.phys_out, self.virt_out = parent.kind.outs
@ -29,29 +31,51 @@ class Config(ttk.Frame):
return self.parent.target
def getter(self, param):
if hasattr(self.target, param):
return getattr(self.target, param)
param = param.split('.')
try:
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):
if hasattr(self.target, param):
setattr(self.target, param, value)
param = param.split('.')
try:
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):
self.after(1, self.remove_events)
def remove_events(self):
self.parent.target.event.remove("pdirty")
self.parent.target.event.remove("ldirty")
self.parent.target.event.remove('pdirty')
self.parent.target.event.remove('ldirty')
def scale_release(self, *args):
_base_values.run_update = False
self.after(1, self.add_events)
def add_events(self):
self.parent.target.event.add("pdirty")
self.parent.target.event.add("ldirty")
self.parent.target.event.add('pdirty')
self.parent.target.event.add('ldirty')
self.after(350, self.resume_updates)
def pause_updates(self, func, *args):
"""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):
_base_values.run_update = True
@ -60,13 +84,13 @@ class Config(ttk.Frame):
self.parent.nav_frame.info_text.set(round(val, 1))
def scale_leave(self, *args):
self.parent.nav_frame.info_text.set("")
self.parent.nav_frame.info_text.set('')
def scale_callback(self, param, *args):
"""callback function for scale widget"""
val = self.slider_vars[self.slider_params.index(param)].get()
self.setter(param, val)
self.setter(param, round(val, 1))
self.parent.nav_frame.info_text.set(round(val, 1))
def reset_scale(self, param, val, *args):
@ -78,12 +102,12 @@ class Config(ttk.Frame):
self.setter(param, val)
if not _configuration.themes_enabled:
self.styletable.configure(
f"{param}.TButton", background=f'{"green" if val else "white"}'
f'{param}.TButton', background=f'{"green" if val else "white"}'
)
def on_update(self, subject):
"""update parameters"""
if subject == "pdirty":
if subject == 'pdirty':
self.sync()
@ -98,6 +122,7 @@ class StripConfig(Config):
self.make_row_2()
self.builder.grid_configure()
self.parent.target.clear_dirty()
self.sync()
@property
@ -109,7 +134,7 @@ class StripConfig(Config):
def make_row_0(self):
if self.index < self.phys_in:
if self.parent.kind.name == "basic":
if self.parent.kind.name == 'basic':
self.builder.create_audibility_slider()
else:
self.builder.create_comp_slider()
@ -128,7 +153,7 @@ class StripConfig(Config):
self.setter(param, val)
if not _configuration.themes_enabled:
self.styletable.configure(
f"{param}.TButton", background=f'{"green" if val else "white"}'
f'{param}.TButton', background=f'{"green" if val else "white"}'
)
def toggle_b(self, param):
@ -136,7 +161,7 @@ class StripConfig(Config):
self.setter(param, val)
if not _configuration.themes_enabled:
self.styletable.configure(
f"{param}.TButton", background=f'{"green" if val else "white"}'
f'{param}.TButton', background=f'{"green" if val else "white"}'
)
def teardown(self):
@ -155,25 +180,31 @@ class StripConfig(Config):
self.param_vars[i].set(self.getter(param))
for i, param in enumerate(self.params)
]
if not _base_values.vban_connected: # slider vars not defined in RT Packet
[
self.slider_vars[i].set(self.getter(param))
for i, param in enumerate(self.slider_params)
if self.index < self.phys_in
]
if not _configuration.themes_enabled:
[
self.styletable.configure(
f"{param}.TButton",
f'{param}.TButton',
background=f'{"green" if self.phys_out_params_vars[i].get() else "white"}',
)
for i, param in enumerate(self.phys_out_params)
]
[
self.styletable.configure(
f"{param}.TButton",
f'{param}.TButton',
background=f'{"green" if self.virt_out_params_vars[i].get() else "white"}',
)
for i, param in enumerate(self.virt_out_params)
]
[
self.styletable.configure(
f"{param}.TButton",
f'{param}.TButton',
background=f'{"green" if self.param_vars[i].get() else "white"}',
)
for i, param in enumerate(self.params)
@ -193,6 +224,7 @@ class BusConfig(Config):
self.make_row_1()
self.builder.grid_configure()
self.parent.target.clear_dirty()
self.sync()
@property
@ -209,9 +241,7 @@ class BusConfig(Config):
self.builder.create_param_buttons()
def current_bus_mode(self):
for mode in self.bus_modes:
if getattr(self.target.mode, mode):
return mode
return self.target.mode.get()
def rotate_bus_modes_right(self, *args):
current_mode = self.current_bus_mode()
@ -225,14 +255,14 @@ class BusConfig(Config):
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")
self.bus_mode_label_text.set('Normal')
def rotate_bus_modes_left(self, *args):
current_mode = self.current_bus_mode()
prev = self.bus_modes.index(current_mode) - 1
if prev < 0:
self.target.mode.rearonly = True
self.bus_mode_label_text.set("Rear Only")
self.bus_mode_label_text.set('Rear Only')
else:
setattr(
self.target.mode,
@ -253,7 +283,7 @@ class BusConfig(Config):
if not _configuration.themes_enabled:
[
self.styletable.configure(
f"{param}.TButton",
f'{param}.TButton',
background=f'{"green" if self.param_vars[i].get() else "white"}',
)
for i, param in enumerate(self.params)

View File

@ -6,57 +6,93 @@ try:
except ModuleNotFoundError:
import tomli as tomllib
LOGGER = logging.getLogger("configurations")
logger = logging.getLogger(__name__)
configuration = {}
config_path = [Path.cwd() / "configs"]
for path in config_path:
if path.is_dir():
filenames = list(path.glob("*.toml"))
configs = {}
for filename in filenames:
name = filename.with_suffix("").stem
try:
with open(filename, "rb") as f:
configs[name] = tomllib.load(f)
except tomllib.TOMLDecodeError:
print(f"Invalid TOML config: configs/{filename.stem}")
for name, cfg in configs.items():
LOGGER.info(f"Loaded configuration configs/{name}")
configuration[name] = cfg
def get_configpath():
for pn in (
Path.home() / '.config' / 'vm-compact',
Path.home() / 'Documents' / 'Voicemeeter' / 'vm-compact',
Path.cwd() / '_internal' / 'configs',
Path.cwd() / 'configs',
):
if pn.exists():
return pn
if configpath := get_configpath():
filepaths = list(configpath.glob('*.toml'))
if any(f.stem in ('app', 'vban') for f in filepaths):
configs = {}
for filepath in filepaths:
filename = filepath.with_suffix('').stem
if filename in ('app', 'vban'):
try:
with open(filepath, 'rb') as f:
configs[filename] = tomllib.load(f)
logger.info(f'configuration: {filename} loaded into memory')
except tomllib.TOMLDecodeError:
logger.error(f'Invalid TOML config: configs/{filename.stem}')
configuration |= configs
_defaults = {
"configs": {
"config": None,
'configs': {
'config': None,
},
"theme": {
"enabled": True,
"mode": "light",
'theme': {
'enabled': True,
'mode': 'light',
},
"extends": {
"extended": True,
"extends_horizontal": True,
'extends': {
'extended': True,
'extends_horizontal': True,
},
"channel": {
"width": 80,
"height": 130,
'channel': {
'width': 80,
'height': 130,
'xpadding': 3,
},
"mwscroll_step": {
"size": 3,
'mwscroll_step': {
'size': 3,
},
"submixes": {
"default": 0,
'submixes': {
'default': 0,
},
'navigation': {'show': True},
}
if "app" in configuration:
configuration["app"] = _defaults | configuration["app"]
if 'app' in configuration:
for key in _defaults:
if key in configuration['app']:
configuration['app'][key] = _defaults[key] | configuration['app'][key]
else:
configuration['app'][key] = _defaults[key]
else:
configuration["app"] = _defaults
configuration['app'] = _defaults
def get_configuration(key):
if key in configuration:
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
configuration = get_configuration("app")
configuration = get_configuration('app')
class SingletonMeta(type):
@ -20,33 +20,38 @@ class SingletonMeta(type):
@dataclass
class Configurations(metaclass=SingletonMeta):
# is the gui extended
extended: bool = configuration["extends"]["extended"]
extended: bool = configuration['extends']['extended']
# direction the gui extends
extends_horizontal: bool = configuration["extends"]["extends_horizontal"]
extends_horizontal: bool = configuration['extends']['extends_horizontal']
# are themes enabled
themes_enabled: bool = configuration["theme"]["enabled"]
themes_enabled: bool = configuration['theme']['enabled']
# light or dark
theme_mode: str = configuration["theme"]["mode"]
theme_mode: str = configuration['theme']['mode']
# size of mousewheel scroll step
mwscroll_step: int = configuration["mwscroll_step"]["size"]
mwscroll_step: int = configuration['mwscroll_step']['size']
# bus assigned as current submix
submixes: int = configuration["submixes"]["default"]
submixes: int = configuration['submixes']['default']
# width of a single labelframe
level_width: int = configuration["channel"]["width"]
# height of a single labelframe
level_height: int = configuration["channel"]["height"]
# width of a single channel labelframe
channel_width: int = configuration['channel']['width']
# height of a single channel labelframe
channel_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
def config(self):
if "configs" in configuration:
return configuration["configs"]["config"]
if 'configs' in configuration:
return configuration['configs']['config']
@dataclass
class BaseValues(metaclass=SingletonMeta):
# pause updates after releasing scale
run_update: bool = True
run_update: bool = False
# are we dragging main window with mouse 1
dragging: bool = False
# a vban connection established

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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