mirror of
https://github.com/onyx-and-iris/voicemeeter-compact.git
synced 2026-04-08 17:03:32 +00:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3f20ea43f | |||
| 2e868705d5 | |||
| df6f215514 | |||
|
|
0a5987631f | ||
| 3d3bc71d15 | |||
| 7b148b2614 | |||
| ef558fdde6 | |||
| c684ed9981 | |||
| 1498daf36f | |||
| e964c94d07 | |||
| 9f20225a59 | |||
| f63c36c94a | |||
| c2db0f2757 | |||
| bfb0482c32 | |||
| 6222ab1e62 | |||
| 0ad40ab708 | |||
| b809bcb28f | |||
| a0b9a92a2a | |||
| 9faf8ae10c | |||
| 82cf0e914b | |||
| 3e68488231 | |||
| 6d46d9a9a5 | |||
| e4068277f7 | |||
| 674999a461 | |||
| e5975f0772 | |||
| 59d2a95ec4 | |||
| 4bae1e1d15 | |||
| 1e3751b19f | |||
| 2ec1c74b7d | |||
| 0a19e28370 | |||
| 5ab1fd7102 | |||
| d896193ade | |||
| 8c0c31dc0d | |||
| c8b3e9fc33 | |||
| d4358bf7d3 | |||
| 194b95d67b | |||
| 0f734e87b7 | |||
| 944ef9ca1c | |||
| fc20bb0c1e | |||
| 5ccc2a6dab | |||
| cfc1279f6c | |||
| d4df11f62d | |||
|
|
3b6e1f61a4 | ||
|
|
3f306ddf62 | ||
|
|
732368a65b | ||
|
|
d7df79b798 | ||
|
|
0906f3343b | ||
|
|
294dfe7d03 | ||
|
|
4db7be172b | ||
|
|
35775f5024 | ||
|
|
d4b2b90fc0 | ||
|
|
6de79977cc |
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,5 +1,5 @@
|
||||
# quick test
|
||||
z_*.py
|
||||
quick.py
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
@@ -131,3 +131,12 @@ dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# build
|
||||
sv_ttk/
|
||||
theme/
|
||||
|
||||
sv_*.py
|
||||
fst_*.py
|
||||
|
||||
.vscode/
|
||||
62
CHANGELOG.md
62
CHANGELOG.md
@@ -7,7 +7,65 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- [ ] Add support for forest theme (should be coming soon)
|
||||
- [ ] Add support for forest theme (if rbende adds it to pypi)
|
||||
|
||||
## [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
|
||||
|
||||
### Added
|
||||
|
||||
- Logging module used in place of print statements across the interface.
|
||||
|
||||
## [1.5.1] - 2022-09-16
|
||||
|
||||
@@ -20,8 +78,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- sv_ttk updated to v2.0.
|
||||
- event toggles used to pause updates when dragging sliders.
|
||||
|
||||
### Removed
|
||||
|
||||
## [1.4.2] - 2022-09-03
|
||||
|
||||
### Added
|
||||
|
||||
28
README.md
28
README.md
@@ -1,5 +1,6 @@
|
||||
[](https://badge.fury.io/py/voicemeeter-compact)
|
||||
[](https://github.com/onyx-and-iris/voicemeeter-compact/blob/main/LICENSE)
|
||||
[](https://python-poetry.org/)
|
||||
[](https://github.com/psf/black)
|
||||

|
||||
|
||||
@@ -34,16 +35,16 @@ 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"
|
||||
|
||||
main()
|
||||
```
|
||||
|
||||
@@ -53,9 +54,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`
|
||||
@@ -109,7 +110,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][def] 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 +150,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 +158,9 @@ 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
|
||||
@@ -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"
|
||||
|
||||
main()
|
||||
|
||||
40
build.ps1
Normal file
40
build.ps1
Normal file
@@ -0,0 +1,40 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$prefix,
|
||||
[string]$theme
|
||||
)
|
||||
|
||||
function Format-Path {
|
||||
param($Kind)
|
||||
return @(
|
||||
$prefix,
|
||||
(& { if ($theme) { $theme } else { "" } }),
|
||||
"${Kind}"
|
||||
).Where({ $_ -ne "" }) -Join "_"
|
||||
}
|
||||
|
||||
function Compress-Builds {
|
||||
$target = Join-Path -Path $PSScriptRoot -ChildPath "dist"
|
||||
@("basic", "banana", "potato") | ForEach-Object {
|
||||
$compress_path = Format-Path -Kind $_
|
||||
Compress-Archive -Path $(Join-Path -Path $target -ChildPath $compress_path) -DestinationPath $(Join-Path -Path $target -ChildPath "${compress_path}.zip") -Force
|
||||
}
|
||||
}
|
||||
|
||||
function Get-Builds {
|
||||
@("basic", "banana", "potato") | ForEach-Object {
|
||||
$spec_path = Format-Path -Kind $_
|
||||
|
||||
"building $spec_path" | Write-Host
|
||||
|
||||
poetry run pyinstaller "$spec_path.spec" --noconfirm
|
||||
}
|
||||
}
|
||||
|
||||
function main {
|
||||
Get-Builds
|
||||
|
||||
Compress-Builds
|
||||
}
|
||||
|
||||
if ($MyInvocation.InvocationName -ne '.') { main }
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
301
poetry.lock
generated
301
poetry.lock
generated
@@ -1,137 +1,312 @@
|
||||
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "altgraph"
|
||||
version = "0.17.4"
|
||||
description = "Python graph (network) package"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
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 = "black"
|
||||
version = "22.8.0"
|
||||
version = "24.4.2"
|
||||
description = "The uncompromising code formatter."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6.2"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"},
|
||||
{file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"},
|
||||
{file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"},
|
||||
{file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"},
|
||||
{file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"},
|
||||
{file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"},
|
||||
{file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"},
|
||||
{file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"},
|
||||
{file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"},
|
||||
{file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"},
|
||||
{file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"},
|
||||
{file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"},
|
||||
{file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"},
|
||||
{file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"},
|
||||
{file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"},
|
||||
{file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"},
|
||||
{file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"},
|
||||
{file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"},
|
||||
{file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"},
|
||||
{file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"},
|
||||
{file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"},
|
||||
{file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8.0.0"
|
||||
mypy-extensions = ">=0.4.3"
|
||||
packaging = ">=22.0"
|
||||
pathspec = ">=0.9.0"
|
||||
platformdirs = ">=2"
|
||||
tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
|
||||
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
||||
typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
colorama = ["colorama (>=0.4.3)"]
|
||||
d = ["aiohttp (>=3.7.4)"]
|
||||
d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
|
||||
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||
uvloop = ["uvloop (>=0.15.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.3"
|
||||
version = "8.1.7"
|
||||
description = "Composable command line interface toolkit"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
||||
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.5"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "isort"
|
||||
version = "5.13.2"
|
||||
description = "A Python utility / library to sort Python imports."
|
||||
optional = false
|
||||
python-versions = ">=3.8.0"
|
||||
files = [
|
||||
{file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
|
||||
{file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
colors = ["colorama (>=0.4.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "macholib"
|
||||
version = "1.16.3"
|
||||
description = "Mach-O header analysis and editing"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
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 = "mypy-extensions"
|
||||
version = "0.4.3"
|
||||
description = "Experimental type system extensions for programs checked with the mypy typechecker."
|
||||
category = "dev"
|
||||
version = "1.0.0"
|
||||
description = "Type system extensions for programs checked with the mypy type checker."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
||||
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.1"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
|
||||
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.10.1"
|
||||
version = "0.12.1"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
|
||||
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pefile"
|
||||
version = "2023.2.7"
|
||||
description = "Python PE parsing module"
|
||||
optional = false
|
||||
python-versions = ">=3.6.0"
|
||||
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 = "platformdirs"
|
||||
version = "2.5.2"
|
||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
category = "dev"
|
||||
version = "4.2.2"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
|
||||
{file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
|
||||
]
|
||||
|
||||
[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)"]
|
||||
docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
|
||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
|
||||
type = ["mypy (>=1.8)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller"
|
||||
version = "6.8.0"
|
||||
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
|
||||
optional = false
|
||||
python-versions = "<3.13,>=3.8"
|
||||
files = [
|
||||
{file = "pyinstaller-6.8.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:5ff6bc2784c1026f8e2f04aa3760cbed41408e108a9d4cf1dd52ee8351a3f6e1"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:39ac424d2ee2457d2ab11a5091436e75a0cccae207d460d180aa1fcbbafdd528"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_i686.whl", hash = "sha256:355832a3acc7de90a255ecacd4b9f9e166a547a79c8905d49f14e3a75c1acdb9"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:6303c7a009f47e6a96ef65aed49f41e36ece8d079b9193ca92fe807403e5fe80"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2b71509468c811968c0b5decb5bbe85b6292ea52d7b1f26313d2aabb673fa9a5"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ff31c5b99e05a4384bbe2071df67ec8b2b347640a375eae9b40218be2f1754c6"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:000c36b13fe4cd8d0d8c2bc855b1ddcf39867b5adf389e6b5ca45b25fa3e619d"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:fe0af018d7d5077180e3144ada89a4da5df8d07716eb7e9482834a56dc57a4e8"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-win32.whl", hash = "sha256:d257f6645c7334cbd66f38a4fac62c3ad614cc46302b2b5d9f8cc48c563bce0e"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-win_amd64.whl", hash = "sha256:81cccfa9b16699b457f4788c5cc119b50f3cd4d0db924955f15c33f2ad27a50d"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-win_arm64.whl", hash = "sha256:1c3060a263758cf7f0144ab4c016097b20451b2469d468763414665db1bb743d"},
|
||||
{file = "pyinstaller-6.8.0.tar.gz", hash = "sha256:3f4b6520f4423fe19bcc2fd63ab7238851ae2bdcbc98f25bc5d2f97cc62012e9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
altgraph = "*"
|
||||
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
|
||||
packaging = ">=22.0"
|
||||
pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""}
|
||||
pyinstaller-hooks-contrib = ">=2024.6"
|
||||
pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""}
|
||||
setuptools = ">=42.0.0"
|
||||
|
||||
[package.extras]
|
||||
completion = ["argcomplete"]
|
||||
hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller-hooks-contrib"
|
||||
version = "2024.7"
|
||||
description = "Community maintained hooks for PyInstaller"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pyinstaller_hooks_contrib-2024.7-py2.py3-none-any.whl", hash = "sha256:8bf0775771fbaf96bcd2f4dfd6f7ae6c1dd1b1efe254c7e50477b3c08e7841d8"},
|
||||
{file = "pyinstaller_hooks_contrib-2024.7.tar.gz", hash = "sha256:fd5f37dcf99bece184e40642af88be16a9b89613ecb958a8bd1136634fc9fac5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
packaging = ">=22.0"
|
||||
setuptools = ">=42.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "pywin32-ctypes"
|
||||
version = "0.2.2"
|
||||
description = "A (partial) reimplementation of pywin32 using ctypes/cffi"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"},
|
||||
{file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "70.2.0"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "setuptools-70.2.0-py3-none-any.whl", hash = "sha256:b8b8060bb426838fbe942479c90296ce976249451118ef566a5a0b7d8b78fb05"},
|
||||
{file = "setuptools-70.2.0.tar.gz", hash = "sha256:bd63e505105011b25c3c11f753f7e3b8465ea739efddaccef8f0efac2137bac1"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
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"]
|
||||
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
|
||||
|
||||
[[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"
|
||||
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"
|
||||
description = "A lil' TOML parser"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
||||
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vban-cmd"
|
||||
version = "1.4.2"
|
||||
version = "2.4.11"
|
||||
description = "Python interface for the VBAN RT Packet Service (Sendtext)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.10,<4.0"
|
||||
files = [
|
||||
{file = "vban_cmd-2.4.11-py3-none-any.whl", hash = "sha256:a74b7631222f340488f5d45bc9aa9d4e7a0f919687c9715619e8809c684875b7"},
|
||||
{file = "vban_cmd-2.4.11.tar.gz", hash = "sha256:250ca8043f075eee11d2a811142d0205900d14d7e2f0cd98eb597901ead738f5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
|
||||
|
||||
[[package]]
|
||||
name = "voicemeeter-api"
|
||||
version = "0.7.0"
|
||||
version = "2.6.0"
|
||||
description = "A Python wrapper for the Voiceemeter API"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.10,<4.0"
|
||||
python-versions = "<4.0,>=3.10"
|
||||
files = [
|
||||
{file = "voicemeeter_api-2.6.0-py3-none-any.whl", hash = "sha256:c2ef8eb063ce3aeac4827ad7883150c407a0effb0fde3778782cd3024a295255"},
|
||||
{file = "voicemeeter_api-2.6.0.tar.gz", hash = "sha256:db93f27b58ce927c7d56084b224e1d0cdd6406ab7f26e4f99a9a873495a3d2e7"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "738f40473a635574dc00427d128af1c3bf45f4290aca9b4d8ae78b6992a486c7"
|
||||
|
||||
[metadata.files]
|
||||
black = []
|
||||
click = [
|
||||
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
|
||||
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
|
||||
]
|
||||
colorama = [
|
||||
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
|
||||
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
|
||||
]
|
||||
mypy-extensions = [
|
||||
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
|
||||
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
|
||||
]
|
||||
pathspec = []
|
||||
platformdirs = [
|
||||
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
|
||||
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
|
||||
]
|
||||
sv-ttk = []
|
||||
tomli = [
|
||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
vban-cmd = []
|
||||
voicemeeter-api = []
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.10,<3.13"
|
||||
content-hash = "343eb64c79bba2d92eeb9af614c782dd175bd8a7b1a236a330b1dd9ae4d18a57"
|
||||
|
||||
@@ -1,27 +1,35 @@
|
||||
[tool.poetry]
|
||||
name = "voicemeeter-compact"
|
||||
version = "1.5.1"
|
||||
version = "1.9.5"
|
||||
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" },
|
||||
]
|
||||
packages = [{ include = "vmcompact" }]
|
||||
include = ["vmcompact/img/cat.ico"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
sv-ttk = "^2.0"
|
||||
python = ">=3.10,<3.13"
|
||||
sv-ttk = "^2.6.0"
|
||||
tomli = { version = "^2.0.1", python = "<3.11" }
|
||||
voicemeeter-api = "^0.7.0"
|
||||
vban-cmd = "^1.4.2"
|
||||
voicemeeter-api = "^2.6.0"
|
||||
vban-cmd = "^2.4.11"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = {version = "^22.6.0", allow-prereleases = true}
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = { version = ">=22.6,<25.0", allow-prereleases = true }
|
||||
isort = "^5.12.0"
|
||||
|
||||
|
||||
[tool.poetry.group.build.dependencies]
|
||||
pyinstaller = "^6.3.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
build_sunvalley = "scripts:build_sunvalley"
|
||||
build_forest = "scripts:build_forest"
|
||||
build_all = "scripts:build_all"
|
||||
|
||||
24
scripts.py
Normal file
24
scripts.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def build_sunvalley():
|
||||
buildscript = Path.cwd() / "build.ps1"
|
||||
subprocess.run(["powershell", str(buildscript), "sv"])
|
||||
|
||||
|
||||
def build_forest():
|
||||
rewriter = Path.cwd() / "tools" / "rewriter.py"
|
||||
subprocess.run([sys.executable, str(rewriter), "-r"])
|
||||
|
||||
buildscript = Path.cwd() / "build.ps1"
|
||||
for theme in ("light", "dark"):
|
||||
subprocess.run(["powershell", str(buildscript), "fst", theme])
|
||||
|
||||
subprocess.run([sys.executable, str(rewriter), "-c"])
|
||||
|
||||
|
||||
def build_all():
|
||||
steps = (build_sunvalley, build_forest)
|
||||
[step() for step in steps]
|
||||
250
tools/rewriter.py
Normal file
250
tools/rewriter.py
Normal 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("-r", "--rewrite", action="store_true")
|
||||
parser.add_argument("-c", "--cleanup", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.rewrite:
|
||||
logger.info("preparing files for build")
|
||||
prepare_for_build()
|
||||
elif args.cleanup:
|
||||
logger.info("cleaning up files")
|
||||
cleanup()
|
||||
0
tools/src/.gitkeep
Normal file
0
tools/src/.gitkeep
Normal 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"""
|
||||
@@ -32,15 +39,19 @@ class App(tk.Tk):
|
||||
|
||||
def __init__(self, vmr):
|
||||
super().__init__()
|
||||
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
self._vmr = vmr
|
||||
self._vmr.event.add("ldirty")
|
||||
self._vmr.event.add(["pdirty", "ldirty"])
|
||||
self.subject = Subject()
|
||||
self.start_updates()
|
||||
self._vmr.init_thread()
|
||||
icon_path = Path(__file__).parent.resolve() / "img" / "cat.ico"
|
||||
if icon_path.is_file():
|
||||
self.iconbitmap(str(icon_path))
|
||||
self.minsize(275, False)
|
||||
self.subject = Subject()
|
||||
self["menu"] = Menus(self, vmr)
|
||||
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)
|
||||
@@ -50,6 +61,11 @@ class App(tk.Tk):
|
||||
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):
|
||||
"""returns the current interface"""
|
||||
@@ -74,8 +90,8 @@ 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
|
||||
@@ -90,12 +106,12 @@ class App(tk.Tk):
|
||||
if self.kind.name == "potato":
|
||||
self.builder.create_banner()
|
||||
|
||||
def on_update(self, subject):
|
||||
"""called whenever notified of update"""
|
||||
|
||||
if subject == "pdirty" and _base_values.run_update:
|
||||
def on_pdirty(self):
|
||||
if _base_values.run_update:
|
||||
self.after(1, self.subject.notify, "pdirty")
|
||||
elif subject == "ldirty" and not _base_values.dragging:
|
||||
|
||||
def on_ldirty(self):
|
||||
if not _base_values.dragging:
|
||||
self.after(1, self.subject.notify, "ldirty")
|
||||
|
||||
def _destroy_top_level_frames(self):
|
||||
@@ -106,7 +122,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()
|
||||
@@ -126,6 +142,57 @@ class App(tk.Tk):
|
||||
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}
|
||||
|
||||
@@ -136,5 +203,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)
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
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,
|
||||
@@ -17,19 +21,15 @@ class Banner(ttk.Frame):
|
||||
)
|
||||
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):
|
||||
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()}"
|
||||
self.after(100, self.upd_submix_step)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import abc
|
||||
import logging
|
||||
import tkinter as tk
|
||||
from functools import partial
|
||||
from tkinter import ttk
|
||||
@@ -11,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
|
||||
@@ -30,6 +33,7 @@ class MainFrameBuilder(AbstractBuilder):
|
||||
def __init__(self, app):
|
||||
self.kind = app.kind
|
||||
self.app = app
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
|
||||
def setup(self):
|
||||
self.app.title(
|
||||
@@ -39,24 +43,26 @@ class MainFrameBuilder(AbstractBuilder):
|
||||
if _configuration.themes_enabled:
|
||||
if sv_ttk.get_theme() not in ("light", "dark"):
|
||||
sv_ttk.set_theme(_configuration.theme_mode)
|
||||
print(f"Sunvalley {sv_ttk.get_theme().capitalize()} Theme applied")
|
||||
|
||||
self.app.target.event.remove("mdirty")
|
||||
self.app.target.event.remove("midi")
|
||||
self.logger.info(
|
||||
f"Sunvalley {sv_ttk.get_theme().capitalize()} Theme applied"
|
||||
)
|
||||
|
||||
def create_channelframe(self, type_):
|
||||
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_}")
|
||||
|
||||
def create_separator(self):
|
||||
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")
|
||||
|
||||
def create_navframe(self):
|
||||
self.app.nav_frame = Navigation(self.app)
|
||||
self.logger.info(f"Finished building navframe")
|
||||
|
||||
def create_configframe(self, type_, index, id):
|
||||
if type_ == "strip":
|
||||
@@ -102,6 +108,7 @@ class MainFrameBuilder(AbstractBuilder):
|
||||
)
|
||||
for _, frame in enumerate(self.app.bus_frame.labelframes)
|
||||
]
|
||||
self.logger.info(f"Finished building configframe for {type_}[{index}]")
|
||||
self.app.after(5, self.reset_config_frames)
|
||||
|
||||
def reset_config_frames(self):
|
||||
@@ -114,6 +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")
|
||||
|
||||
def teardown(self):
|
||||
pass
|
||||
@@ -187,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
|
||||
@@ -219,7 +227,7 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
|
||||
"""Adds a progress bar widget to a single label frame"""
|
||||
self.labelframe.pb = ttk.Progressbar(
|
||||
self.labelframe,
|
||||
maximum=100,
|
||||
maximum=72,
|
||||
orient="vertical",
|
||||
mode="determinate",
|
||||
variable=self.labelframe.level,
|
||||
@@ -235,13 +243,19 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
|
||||
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(
|
||||
"<MouseWheel>",
|
||||
partial(
|
||||
self.labelframe.pause_updates,
|
||||
self.labelframe._on_mousewheel,
|
||||
),
|
||||
)
|
||||
|
||||
def add_gain_label(self):
|
||||
self.labelframe.gain_label = ttk.Label(
|
||||
@@ -255,7 +269,7 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
|
||||
self.button_mute = ttk.Checkbutton(
|
||||
self.labelframe,
|
||||
text="MUTE",
|
||||
command=partial(self.labelframe.toggle_mute, "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,
|
||||
)
|
||||
@@ -275,7 +289,7 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
|
||||
self.button_on = ttk.Checkbutton(
|
||||
self.labelframe,
|
||||
text="ON",
|
||||
command=self.labelframe.set_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,
|
||||
)
|
||||
@@ -316,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)
|
||||
]
|
||||
|
||||
@@ -329,7 +343,7 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
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
|
||||
]
|
||||
@@ -384,18 +398,18 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
from_=0.0,
|
||||
to=10.0,
|
||||
orient="horizontal",
|
||||
length=_configuration.level_width,
|
||||
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("<Enter>", partial(self.configframe.scale_enter, "comp.knob"))
|
||||
comp_scale.bind("<Leave>", self.configframe.scale_leave)
|
||||
|
||||
comp_label.grid(column=0, row=0)
|
||||
@@ -408,18 +422,18 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
from_=0.0,
|
||||
to=10.0,
|
||||
orient="horizontal",
|
||||
length=_configuration.level_width,
|
||||
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("<Enter>", partial(self.configframe.scale_enter, "gate.knob"))
|
||||
gate_scale.bind("<Leave>", self.configframe.scale_leave)
|
||||
|
||||
gate_label.grid(column=2, row=0)
|
||||
@@ -432,7 +446,7 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
from_=-40,
|
||||
to=12,
|
||||
orient="horizontal",
|
||||
length=_configuration.level_width,
|
||||
length=_configuration.channel_width,
|
||||
variable=self.configframe.slider_vars[
|
||||
self.configframe.slider_params.index("limit")
|
||||
],
|
||||
@@ -456,7 +470,7 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
from_=0.0,
|
||||
to=10.0,
|
||||
orient="horizontal",
|
||||
length=_configuration.level_width,
|
||||
length=_configuration.channel_width,
|
||||
variable=self.configframe.slider_vars[
|
||||
self.configframe.slider_params.index("audibility")
|
||||
],
|
||||
@@ -478,7 +492,9 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
ttk.Checkbutton(
|
||||
self.configframe,
|
||||
text=param,
|
||||
command=partial(self.configframe.toggle_a, param),
|
||||
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)
|
||||
@@ -499,7 +515,9 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
ttk.Checkbutton(
|
||||
self.configframe,
|
||||
text=param,
|
||||
command=partial(self.configframe.toggle_b, param),
|
||||
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)
|
||||
@@ -520,7 +538,9 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
ttk.Checkbutton(
|
||||
self.configframe,
|
||||
text=param,
|
||||
command=partial(self.configframe.toggle_p, param),
|
||||
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],
|
||||
)
|
||||
@@ -556,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()]
|
||||
@@ -570,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):
|
||||
@@ -581,7 +607,9 @@ class BusConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
ttk.Checkbutton(
|
||||
self.configframe,
|
||||
text=param,
|
||||
command=partial(self.configframe.toggle_p, param),
|
||||
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],
|
||||
)
|
||||
|
||||
@@ -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,18 +43,21 @@ 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()
|
||||
@@ -82,17 +88,27 @@ class ChannelLabelFrame(ttk.LabelFrame):
|
||||
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(
|
||||
round(
|
||||
self.gain.get()
|
||||
+ (
|
||||
_configuration.mwscroll_step
|
||||
if event.delta > 0
|
||||
else -_configuration.mwscroll_step
|
||||
),
|
||||
1,
|
||||
)
|
||||
)
|
||||
if self.gain.get() > 12:
|
||||
@@ -100,7 +116,7 @@ class ChannelLabelFrame(ttk.LabelFrame):
|
||||
elif self.gain.get() < -60:
|
||||
self.gain.set(-60)
|
||||
self.setter("gain", self.gain.get())
|
||||
self.after(1, self.resume_updates)
|
||||
self.gainlabel.set(round(self.gain.get(), 1))
|
||||
|
||||
def open_config(self):
|
||||
if self.conf.get():
|
||||
@@ -135,7 +151,8 @@ class ChannelLabelFrame(ttk.LabelFrame):
|
||||
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 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:
|
||||
@@ -148,7 +165,7 @@ class ChannelLabelFrame(ttk.LabelFrame):
|
||||
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 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)]:
|
||||
def update_labels(self):
|
||||
for labelframe in self.labelframes:
|
||||
labelframe.on_update("labelframe")
|
||||
|
||||
def on_update(self, subject):
|
||||
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)]
|
||||
@@ -264,7 +284,7 @@ class ChannelFrame(ttk.Frame):
|
||||
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,11 +327,11 @@ 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,
|
||||
@@ -315,7 +339,7 @@ def _make_channelframe(parent, id):
|
||||
)
|
||||
else:
|
||||
CHANNELFRAME_cls = type(
|
||||
f"ChannelFrame{id.capitalize()}",
|
||||
f"ChannelFrame{identifier.capitalize()}",
|
||||
(ChannelFrame,),
|
||||
{
|
||||
"__init__": init_bus,
|
||||
|
||||
@@ -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,12 +31,26 @@ 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)
|
||||
@@ -52,6 +68,14 @@ class Config(ttk.Frame):
|
||||
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
|
||||
|
||||
@@ -66,7 +90,7 @@ class Config(ttk.Frame):
|
||||
"""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):
|
||||
@@ -98,6 +122,7 @@ class StripConfig(Config):
|
||||
self.make_row_2()
|
||||
self.builder.grid_configure()
|
||||
|
||||
self.parent.target.clear_dirty()
|
||||
self.sync()
|
||||
|
||||
@property
|
||||
@@ -155,6 +180,12 @@ 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:
|
||||
[
|
||||
@@ -193,6 +224,7 @@ class BusConfig(Config):
|
||||
self.make_row_1()
|
||||
self.builder.grid_configure()
|
||||
|
||||
self.parent.target.clear_dirty()
|
||||
self.sync()
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
@@ -5,24 +6,36 @@ try:
|
||||
except ModuleNotFoundError:
|
||||
import tomli as tomllib
|
||||
|
||||
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():
|
||||
print(f"Loaded configuration configs/{name}")
|
||||
configuration[name] = cfg
|
||||
def get_configpath():
|
||||
configpaths = [
|
||||
Path.cwd() / "configs",
|
||||
Path.home() / ".config" / "vm-compact" / "configs",
|
||||
Path.home() / "Documents" / "Voicemeeter" / "configs",
|
||||
]
|
||||
for configpath in configpaths:
|
||||
if configpath.exists():
|
||||
return configpath
|
||||
|
||||
|
||||
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": {
|
||||
@@ -39,6 +52,7 @@ _defaults = {
|
||||
"channel": {
|
||||
"width": 80,
|
||||
"height": 130,
|
||||
"xpadding": 3,
|
||||
},
|
||||
"mwscroll_step": {
|
||||
"size": 3,
|
||||
@@ -46,10 +60,16 @@ _defaults = {
|
||||
"submixes": {
|
||||
"default": 0,
|
||||
},
|
||||
"navigation": {"show": True},
|
||||
}
|
||||
|
||||
|
||||
if "app" in configuration:
|
||||
configuration["app"] = _defaults | configuration["app"]
|
||||
for key in _defaults:
|
||||
if key in configuration["app"]:
|
||||
configuration["app"][key] = _defaults[key] | configuration["app"][key]
|
||||
else:
|
||||
configuration["app"][key] = _defaults[key]
|
||||
else:
|
||||
configuration["app"] = _defaults
|
||||
|
||||
@@ -57,3 +77,22 @@ else:
|
||||
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
|
||||
|
||||
@@ -32,10 +32,15 @@ class Configurations(metaclass=SingletonMeta):
|
||||
# bus assigned as current submix
|
||||
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):
|
||||
@@ -46,7 +51,7 @@ class Configurations(metaclass=SingletonMeta):
|
||||
@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
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
class VMCompactErrors(Exception):
|
||||
"""Base classs for VMCompact Errors"""
|
||||
|
||||
pass
|
||||
class VMCompactError(Exception):
|
||||
"""Exception raised when general errors occur"""
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import tkinter as tk
|
||||
from math import log
|
||||
from tkinter import ttk
|
||||
|
||||
from . import builders
|
||||
@@ -42,11 +41,13 @@ class GainLayer(ttk.LabelFrame):
|
||||
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):
|
||||
@@ -57,8 +58,9 @@ class GainLayer(ttk.LabelFrame):
|
||||
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)
|
||||
@@ -76,6 +78,14 @@ class GainLayer(ttk.LabelFrame):
|
||||
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
|
||||
|
||||
@@ -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()
|
||||
@@ -250,11 +262,11 @@ class SubMixFrame(ttk.Frame):
|
||||
|
||||
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)
|
||||
]
|
||||
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
import logging
|
||||
import tkinter as tk
|
||||
import webbrowser
|
||||
from functools import partial
|
||||
from tkinter import messagebox, ttk
|
||||
from tkinter import messagebox
|
||||
|
||||
import vban_cmd
|
||||
from vban_cmd.error import VBANCMDConnectionError
|
||||
|
||||
import sv_ttk
|
||||
import vban_cmd
|
||||
|
||||
from .data import _base_values, _configuration, get_configuration, kind_get
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Menus(tk.Menu):
|
||||
def __init__(self, parent, vmr):
|
||||
super().__init__()
|
||||
self.parent = parent
|
||||
self.vmr = vmr
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
self.vban_config = get_configuration("vban")
|
||||
self.app_config = get_configuration("app")
|
||||
self._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
|
||||
@@ -78,14 +86,14 @@ class Menus(tk.Menu):
|
||||
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
|
||||
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:
|
||||
@@ -149,6 +157,23 @@ class Menus(tk.Menu):
|
||||
)
|
||||
if not _configuration.themes_enabled:
|
||||
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)
|
||||
@@ -200,7 +225,10 @@ class Menus(tk.Menu):
|
||||
]
|
||||
|
||||
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":
|
||||
@@ -208,15 +236,28 @@ class Menus(tk.Menu):
|
||||
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())
|
||||
@@ -238,6 +279,7 @@ 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)
|
||||
@@ -267,14 +309,19 @@ class Menus(tk.Menu):
|
||||
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"
|
||||
)
|
||||
|
||||
def vban_connect(self, i):
|
||||
def menu_teardown(self, i):
|
||||
# remove config load menus
|
||||
[
|
||||
self.menu_configs_load.delete(key)
|
||||
for key in self.vmr.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")
|
||||
@@ -282,13 +329,51 @@ class Menus(tk.Menu):
|
||||
if j != i
|
||||
]
|
||||
|
||||
def menu_setup(self):
|
||||
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")
|
||||
|
||||
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")
|
||||
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.vban.login()
|
||||
self.vban.event.add("ldirty")
|
||||
except VBANCMDConnectionError as e:
|
||||
self.vban.logout()
|
||||
msg = (
|
||||
f"Timeout attempting to establish connection to {opts.get('ip')}",
|
||||
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))
|
||||
self.after(1, self.enable_vban_menus)
|
||||
return
|
||||
self.menu_teardown(i)
|
||||
self.vban.event.add(["pdirty", "ldirty"])
|
||||
# destroy the current App frames
|
||||
self.parent._destroy_top_level_frames()
|
||||
_base_values.vban_connected = True
|
||||
@@ -303,27 +388,15 @@ class Menus(tk.Menu):
|
||||
self.menu_layout.entryconfig(
|
||||
0, state=f"{'normal' if kind.name == 'potato' else 'disabled'}"
|
||||
)
|
||||
# rebuild config load menus
|
||||
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
|
||||
]
|
||||
else:
|
||||
self.menu_configs.entryconfig(0, state="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):
|
||||
# remove config load menus
|
||||
[
|
||||
self.menu_configs_load.delete(key)
|
||||
for key in self.vban.configs.keys()
|
||||
if key not in self.config_defaults
|
||||
]
|
||||
self.menu_teardown(i)
|
||||
|
||||
# destroy the current App frames
|
||||
self.parent._destroy_top_level_frames()
|
||||
@@ -333,26 +406,19 @@ 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)
|
||||
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'}"
|
||||
)
|
||||
# rebuild config load menus
|
||||
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
|
||||
]
|
||||
else:
|
||||
self.menu_configs.entryconfig(0, state="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)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
@@ -5,12 +6,17 @@ from . import builders
|
||||
from .data import _configuration
|
||||
from .gainlayer import SubMixFrame
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Navigation(ttk.Frame):
|
||||
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)
|
||||
@@ -26,6 +32,9 @@ class Navigation(ttk.Frame):
|
||||
def show_submix(self):
|
||||
if self.submix.get():
|
||||
self.parent.submix_frame = SubMixFrame(self.parent)
|
||||
self.logger.info(
|
||||
f"Finished building submixframe for submix {_configuration.submixes}"
|
||||
)
|
||||
else:
|
||||
if _configuration.extends_horizontal:
|
||||
self.parent.submix_frame.teardown()
|
||||
@@ -39,6 +48,9 @@ class Navigation(ttk.Frame):
|
||||
self.parent.bus_frame.grid()
|
||||
else:
|
||||
self.parent.rowconfigure(2, weight=0, minsize=0)
|
||||
self.logger.info(
|
||||
f"Finished tearing down submixframe for submix {_configuration.submixes}"
|
||||
)
|
||||
|
||||
if not _configuration.themes_enabled:
|
||||
self.styletable.configure(
|
||||
|
||||
Reference in New Issue
Block a user