35 Commits

Author SHA1 Message Date
c684ed9981 should a config be loaded during engine startup
bypass _base_values.run_update

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

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

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

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

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

patch bump
2023-06-26 16:08:58 +01:00
d896193ade md fix 2023-06-26 14:18:23 +01:00
8c0c31dc0d 1.7.0 section added 2023-06-26 14:17:24 +01:00
c8b3e9fc33 dependencies updated.
minor version bump
2023-06-26 13:58:22 +01:00
d4358bf7d3 update comp, gate, eq settings in example.toml 2023-06-26 13:57:39 +01:00
194b95d67b use subject class to notify of busmix update
update messageboxes
2023-06-26 13:57:08 +01:00
0f734e87b7 upd docstring 2023-06-26 13:56:06 +01:00
944ef9ca1c updates to events, callbacks 2023-06-26 13:55:50 +01:00
fc20bb0c1e configpaths added to configurations 2023-06-26 13:53:38 +01:00
5ccc2a6dab error class renamed
doc string expanded
2023-06-26 13:52:42 +01:00
cfc1279f6c module level loggers added 2023-06-26 13:52:24 +01:00
d4df11f62d optimizations to reduce the number of api calls
comp, gate, eq parameter calls updated.
2023-06-26 13:51:21 +01:00
24 changed files with 1058 additions and 258 deletions

9
.gitignore vendored
View File

@@ -131,3 +131,12 @@ dmypy.json
# Pyre type checker # Pyre type checker
.pyre/ .pyre/
# build
sv_ttk/
theme/
sv_*.py
fst_*.py
.vscode/

View File

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

View File

@@ -1,5 +1,6 @@
[![PyPI version](https://badge.fury.io/py/voicemeeter-compact.svg)](https://badge.fury.io/py/voicemeeter-compact) [![PyPI version](https://badge.fury.io/py/voicemeeter-compact.svg)](https://badge.fury.io/py/voicemeeter-compact)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/onyx-and-iris/voicemeeter-compact/blob/main/LICENSE) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/onyx-and-iris/voicemeeter-compact/blob/main/LICENSE)
[![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
![OS: Windows](https://img.shields.io/badge/os-windows-red) ![OS: Windows](https://img.shields.io/badge/os-windows-red)
@@ -34,16 +35,16 @@ import vmcompact
def main(): def main():
# pass the kind_id and the vm object to the app # choose the kind of Voicemeeter (Local connection)
with voicemeeterlib.api(kind_id) as vm: KIND_ID = "banana"
app = vmcompact.connect(kind_id, vm)
# pass the KIND_ID and the vm object to the app
with voicemeeterlib.api(KIND_ID) as vm:
app = vmcompact.connect(KIND_ID, vm)
app.mainloop() app.mainloop()
if __name__ == "__main__": if __name__ == "__main__":
# choose the kind of Voicemeeter (Local connection)
kind_id = "banana"
main() main()
``` ```
@@ -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 :). If the GUI looks like the above when you first load it, then no channels are labelled. From the menu, `Configs->Load config` you may load an example config. Save your current Voicemeeter settings first :).
### kind_id ### KIND_ID
Set the kind of Voicemeeter, kind_id may be: Set the kind of Voicemeeter, KIND_ID may be:
- `basic` - `basic`
- `banana` - `banana`

View File

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

40
build.ps1 Normal file
View 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 }

View File

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

View File

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

View File

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

238
poetry.lock generated
View File

@@ -1,10 +1,36 @@
# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
[[package]]
name = "altgraph"
version = "0.17.3"
description = "Python graph (network) package"
optional = false
python-versions = "*"
files = [
{file = "altgraph-0.17.3-py2.py3-none-any.whl", hash = "sha256:c8ac1ca6772207179ed8003ce7687757c04b0b71536f81e2ac5755c6226458fe"},
{file = "altgraph-0.17.3.tar.gz", hash = "sha256:ad33358114df7c9416cdb8fa1eaa5852166c505118717021c6a8c7c7abbd03dd"},
]
[[package]] [[package]]
name = "black" name = "black"
version = "22.10.0" version = "22.12.0"
description = "The uncompromising code formatter." description = "The uncompromising code formatter."
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [
{file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"},
{file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"},
{file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"},
{file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"},
{file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"},
{file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"},
{file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"},
{file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"},
{file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"},
{file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"},
{file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"},
{file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"},
]
[package.dependencies] [package.dependencies]
click = ">=8.0.0" click = ">=8.0.0"
@@ -21,102 +47,230 @@ uvloop = ["uvloop (>=0.15.2)"]
[[package]] [[package]]
name = "click" name = "click"
version = "8.1.3" version = "8.1.4"
description = "Composable command line interface toolkit" description = "Composable command line interface toolkit"
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [
{file = "click-8.1.4-py3-none-any.whl", hash = "sha256:2739815aaa5d2c986a88f1e9230c55e17f0caad3d958a5e13ad0797c166db9e3"},
{file = "click-8.1.4.tar.gz", hash = "sha256:b97d0c74955da062a7d4ef92fadb583806a585b2ea81958a81bd72726cbb8e37"},
]
[package.dependencies] [package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""} colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.5" version = "0.4.6"
description = "Cross-platform colored terminal text." description = "Cross-platform colored terminal text."
category = "dev"
optional = false 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.12.0"
description = "A Python utility / library to sort Python imports."
optional = false
python-versions = ">=3.8.0"
files = [
{file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"},
{file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"},
]
[package.extras]
colors = ["colorama (>=0.4.3)"]
pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
plugins = ["setuptools"]
requirements-deprecated-finder = ["pip-api", "pipreqs"]
[[package]]
name = "macholib"
version = "1.16.2"
description = "Mach-O header analysis and editing"
optional = false
python-versions = "*"
files = [
{file = "macholib-1.16.2-py2.py3-none-any.whl", hash = "sha256:44c40f2cd7d6726af8fa6fe22549178d3a4dfecc35a9cd15ea916d9c83a688e0"},
{file = "macholib-1.16.2.tar.gz", hash = "sha256:557bbfa1bb255c20e9abafe7ed6cd8046b48d9525db2f9b77d3122a63a2a8bf8"},
]
[package.dependencies]
altgraph = ">=0.17"
[[package]] [[package]]
name = "mypy-extensions" name = "mypy-extensions"
version = "0.4.3" version = "1.0.0"
description = "Experimental type system extensions for programs checked with the mypy typechecker." description = "Type system extensions for programs checked with the mypy type checker."
category = "dev"
optional = false 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]] [[package]]
name = "pathspec" name = "pathspec"
version = "0.10.1" version = "0.11.1"
description = "Utility library for gitignore style pattern matching of file paths." description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [
{file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"},
{file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"},
]
[[package]]
name = "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]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "2.5.2" version = "3.8.1"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [
{file = "platformdirs-3.8.1-py3-none-any.whl", hash = "sha256:cec7b889196b9144d088e4c57d9ceef7374f6c39694ad1577a0aab50d27ea28c"},
{file = "platformdirs-3.8.1.tar.gz", hash = "sha256:f87ca4fcff7d2b0f81c6a748a77973d7af0f4d526f98f308477c3c436c74d528"},
]
[package.extras] [package.extras]
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"]
[[package]]
name = "pyinstaller"
version = "5.13.1"
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
optional = false
python-versions = "<3.13,>=3.7"
files = [
{file = "pyinstaller-5.13.1-py3-none-macosx_10_13_universal2.whl", hash = "sha256:3c9cfe6d5d2f392d5d47389f6d377a8f225db460cdd01048b5a3de1d99c24ebe"},
{file = "pyinstaller-5.13.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:29341d2e86d5ce7df993e797ee96ef679041fc85376d31c35c7b714085a21299"},
{file = "pyinstaller-5.13.1-py3-none-manylinux2014_i686.whl", hash = "sha256:ad6e31a8f35a463c6140e4cf979859197edc9831a1039253408b0fe5eec274dc"},
{file = "pyinstaller-5.13.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:5d801db3ceee58d01337473ea897e96e4bb21421a169dd7cf8716754617ff7fc"},
{file = "pyinstaller-5.13.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:2519db3edec87d8c33924c2c4b7e176d8c1bbd9ba892d77efb67281925e621d6"},
{file = "pyinstaller-5.13.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e033218c8922f0342b6095fb444ecb3bc6747dfa58cac5eac2b985350f4b681e"},
{file = "pyinstaller-5.13.1-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:086e68aa1e72f6aa13b9d170a395755e2b194b8ab410caeed02d16b432410c8c"},
{file = "pyinstaller-5.13.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:aa609aca62edd8cdcf7740677a21525e6c23b5e9a8f821ec8a80c68947771b5d"},
{file = "pyinstaller-5.13.1-py3-none-win32.whl", hash = "sha256:b8d4000af72bf72f8185d420cd0a0aee0961f03a5c3511dc3ff08cdaef0583de"},
{file = "pyinstaller-5.13.1-py3-none-win_amd64.whl", hash = "sha256:b70ebc10811b30bbea4cf5b81fd1477db992c2614cf215edc987cda9c5468911"},
{file = "pyinstaller-5.13.1-py3-none-win_arm64.whl", hash = "sha256:78d1601a11475b95dceff6eaf0c9cd74d93e3f47b5ce4ad63cd76e7a369d3d04"},
{file = "pyinstaller-5.13.1.tar.gz", hash = "sha256:a2e7a1d76a7ac26f1db849d691a374f2048b0e204233028d25d79a90ecd1fec8"},
]
[package.dependencies]
altgraph = "*"
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""}
pyinstaller-hooks-contrib = ">=2021.4"
pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""}
setuptools = ">=42.0.0"
[package.extras]
encryption = ["tinyaes (>=1.0.0)"]
hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"]
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2023.7"
description = "Community maintained hooks for PyInstaller"
optional = false
python-versions = ">=3.7"
files = [
{file = "pyinstaller-hooks-contrib-2023.7.tar.gz", hash = "sha256:0c436a4c3506020e34116a8a7ddfd854c1ad6ddca9a8cd84500bd6e69c9e68f9"},
{file = "pyinstaller_hooks_contrib-2023.7-py2.py3-none-any.whl", hash = "sha256:3c10df14c0f71ab388dfbf1625375b087e7330d9444cbfd2b310ba027fa0cff0"},
]
[[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 = "68.1.2"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "setuptools-68.1.2-py3-none-any.whl", hash = "sha256:3d8083eed2d13afc9426f227b24fd1659489ec107c0e86cec2ffdde5c92e790b"},
{file = "setuptools-68.1.2.tar.gz", hash = "sha256:3d4dfa6d95f1b101d695a6160a7626e15583af71a5f52176efa5d39a054d475d"},
]
[package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5,<=7.1.2)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]] [[package]]
name = "sv-ttk" name = "sv-ttk"
version = "2.0" version = "2.5.5"
description = "A gorgeous theme for Tkinter that looks like Windows 11" description = "A gorgeous theme for Tkinter, based on Windows 11's UI"
category = "main"
optional = false optional = false
python-versions = ">=3.4" python-versions = ">=3.7"
files = [
{file = "sv_ttk-2.5.5-py3-none-any.whl", hash = "sha256:49d1cd03c032728c183d1fe2318f88cdb658ef3e87157e1ca3fcf6661054965b"},
{file = "sv_ttk-2.5.5.tar.gz", hash = "sha256:9bbfe2aba6cc6f9fdf70d79331046543c9666fcccc78bad5ff648a9987e3cedb"},
]
[[package]] [[package]]
name = "tomli" name = "tomli"
version = "2.0.1" version = "2.0.1"
description = "A lil' TOML parser" description = "A lil' TOML parser"
category = "main"
optional = false optional = false
python-versions = ">=3.7" 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]] [[package]]
name = "vban-cmd" name = "vban-cmd"
version = "1.8.1" version = "2.4.4"
description = "Python interface for the VBAN RT Packet Service (Sendtext)" description = "Python interface for the VBAN RT Packet Service (Sendtext)"
category = "main"
optional = false optional = false
python-versions = ">=3.10,<4.0" python-versions = ">=3.10,<4.0"
files = [
{file = "vban_cmd-2.4.4-py3-none-any.whl", hash = "sha256:f439219a2dc6a45123bb70fc94d2aabfc643f26dffc9206da2228e2520f83e01"},
{file = "vban_cmd-2.4.4.tar.gz", hash = "sha256:31dbf6abb681d57772c9082722b024d0798eff99b4c622bfbb539179852a935d"},
]
[package.dependencies] [package.dependencies]
tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""} tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
[[package]] [[package]]
name = "voicemeeter-api" name = "voicemeeter-api"
version = "0.8.4" version = "2.4.4"
description = "A Python wrapper for the Voiceemeter API" description = "A Python wrapper for the Voiceemeter API"
category = "main"
optional = false optional = false
python-versions = ">=3.10,<4.0" python-versions = ">=3.10,<4.0"
files = [
{file = "voicemeeter_api-2.4.4-py3-none-any.whl", hash = "sha256:2e2f0b475de7cfc0d1c397838498162f0b405ab82a6f6ca0095434b43385b43e"},
{file = "voicemeeter_api-2.4.4.tar.gz", hash = "sha256:a4f8ecaa7f5d6b9e9a8545dcf047754711af9a120628b3a98821466c7d65c1e7"},
]
[package.dependencies] [package.dependencies]
tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""} tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
[metadata] [metadata]
lock-version = "1.1" lock-version = "2.0"
python-versions = "^3.10" python-versions = ">=3.10,<3.13"
content-hash = "ba702e1c74c507e75070c2c58bffe5a7787d1dfa1b4ff7b5cedce7374871f7db" content-hash = "2eaf6fcdc32697296201706d983c0d336af4e6644bae2a0d38d11fff8fa21985"
[metadata.files]
black = []
click = []
colorama = []
mypy-extensions = []
pathspec = []
platformdirs = []
sv-ttk = []
tomli = []
vban-cmd = []
voicemeeter-api = []

View File

@@ -1,27 +1,34 @@
[tool.poetry] [tool.poetry]
name = "voicemeeter-compact" name = "voicemeeter-compact"
version = "1.6.5" version = "1.9.3"
description = "A Compact Voicemeeter Remote App" description = "A Compact Voicemeeter Remote App"
authors = ["onyx-and-iris <code@onyxandiris.online>"] authors = ["onyx-and-iris <code@onyxandiris.online>"]
license = "MIT" license = "MIT"
readme = "README.md" readme = "README.md"
repository = "https://github.com/onyx-and-iris/voicemeeter-compact" repository = "https://github.com/onyx-and-iris/voicemeeter-compact"
packages = [ packages = [{ include = "vmcompact" }]
{ include = "vmcompact" },
]
include = ["vmcompact/img/cat.ico"] include = ["vmcompact/img/cat.ico"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.10" python = ">=3.10,<3.13"
sv-ttk = "^2.0" sv-ttk = "^2.5.5"
tomli = { version = "^2.0.1", python = "<3.11" } tomli = { version = "^2.0.1", python = "<3.11" }
voicemeeter-api = "^0.8.4" voicemeeter-api = "^2.4.4"
vban-cmd = "^1.8.1" vban-cmd = "^2.4.4"
[tool.poetry.dev-dependencies] [tool.poetry.group.dev.dependencies]
black = {version = "^22.6.0", allow-prereleases = true} black = { version = "^22.6.0", allow-prereleases = true }
isort = "^5.12.0"
[tool.poetry.group.build.dependencies]
pyinstaller = "^5.13.1"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api" 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
View 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
View File

@@ -0,0 +1,250 @@
import argparse
import logging
from pathlib import Path
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger("vm-compact-rewriter")
PACKAGE_DIR = Path(__file__).parent.parent / "vmcompact"
SRC_DIR = Path(__file__).parent / "src"
def write_outs(output, outs: tuple):
for out in outs:
output.write(out)
def rewrite_app():
app_logger = logger.getChild("app")
app_logger.info("rewriting app.py")
infile = Path(SRC_DIR) / "app.bk"
outfile = Path(PACKAGE_DIR) / "app.py"
with open(infile, "r") as input:
with open(outfile, "w") as output:
for line in input:
match line:
# App init()
case " def __init__(self, vmr):\n":
output.write(" def __init__(self, vmr, theme):\n")
case " self._vmr = vmr\n":
write_outs(
output,
(
" self._vmr = vmr\n",
" self._theme = theme\n",
' tcldir = Path.cwd() / "theme"\n',
" if not tcldir.is_dir():\n",
' tcldir = Path.cwd() / "builds" / "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
View File

View File

@@ -1,14 +1,21 @@
import logging
import tkinter as tk import tkinter as tk
from functools import cached_property
from pathlib import Path from pathlib import Path
from tkinter import ttk from tkinter import messagebox, ttk
from typing import NamedTuple from typing import NamedTuple
import voicemeeterlib
from .builders import MainFrameBuilder from .builders import MainFrameBuilder
from .data import _base_values, _configuration, _kinds_all from .configurations import loader
from .errors import VMCompactErrors from .data import _base_values, _configuration, _kinds_all, get_configuration
from .errors import VMCompactError
from .menu import Menus from .menu import Menus
from .subject import Subject from .subject import Subject
logger = logging.getLogger(__name__)
class App(tk.Tk): class App(tk.Tk):
"""App mainframe""" """App mainframe"""
@@ -32,16 +39,19 @@ class App(tk.Tk):
def __init__(self, vmr): def __init__(self, vmr):
super().__init__() super().__init__()
self.logger = logger.getChild(self.__class__.__name__)
self._vmr = vmr self._vmr = vmr
self._vmr.event.ldirty = True self._vmr.event.add(["pdirty", "ldirty"])
self._vmr.event.remove(["mdirty", "midi"]) self.subject = Subject()
self.start_updates()
self._vmr.init_thread()
icon_path = Path(__file__).parent.resolve() / "img" / "cat.ico" icon_path = Path(__file__).parent.resolve() / "img" / "cat.ico"
if icon_path.is_file(): if icon_path.is_file():
self.iconbitmap(str(icon_path)) self.iconbitmap(str(icon_path))
self.minsize(275, False) self.minsize(275, False)
self.subject = Subject() self._configs = None
self["menu"] = Menus(self, vmr) self.protocol("WM_DELETE_WINDOW", self.on_close_window)
self.menu = self["menu"] = Menus(self, vmr)
self.styletable = ttk.Style() self.styletable = ttk.Style()
if _configuration.config: if _configuration.config:
vmr.apply_config(_configuration.config) vmr.apply_config(_configuration.config)
@@ -51,6 +61,11 @@ class App(tk.Tk):
self.drag_id = "" self.drag_id = ""
self.bind("<Configure>", self.dragging) self.bind("<Configure>", self.dragging)
self.after(1, self.healthcheck_step)
def __str__(self):
return f"{type(self).__name__}App"
@property @property
def target(self): def target(self):
"""returns the current interface""" """returns the current interface"""
@@ -75,8 +90,8 @@ class App(tk.Tk):
if kind: if kind:
self.kind = kind self.kind = kind
# register app as observer # register event callbacks
self.target.subject.add(self) self.target.subject.add([self.on_pdirty, self.on_ldirty])
self.bus_frame = None self.bus_frame = None
self.submix_frame = None self.submix_frame = None
@@ -91,12 +106,12 @@ class App(tk.Tk):
if self.kind.name == "potato": if self.kind.name == "potato":
self.builder.create_banner() self.builder.create_banner()
def on_update(self, subject): def on_pdirty(self):
"""called whenever notified of update""" if _base_values.run_update:
if subject == "pdirty" and _base_values.run_update:
self.after(1, self.subject.notify, "pdirty") 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") self.after(1, self.subject.notify, "ldirty")
def _destroy_top_level_frames(self): def _destroy_top_level_frames(self):
@@ -107,7 +122,7 @@ class App(tk.Tk):
Destroy all top level frames. Destroy all top level frames.
""" """
self.target.subject.remove(self) self.target.subject.remove([self.on_pdirty, self.on_ldirty])
self.subject.clear() self.subject.clear()
[ [
frame.destroy() frame.destroy()
@@ -127,6 +142,57 @@ class App(tk.Tk):
self.drag_id = "" self.drag_id = ""
_base_values.dragging = False _base_values.dragging = False
@cached_property
def userconfigs(self):
self._configs = loader(self.kind.name, self.target)
return self._configs
def start_updates(self):
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} _apps = {kind.name: App.make(kind) for kind in _kinds_all}
@@ -137,5 +203,5 @@ def connect(kind_id: str, vmr) -> App:
try: try:
VMMIN_cls = _apps[kind_id] VMMIN_cls = _apps[kind_id]
except KeyError: except KeyError:
raise VMCompactErrors(f"Invalid kind: {kind_id}") raise VMCompactError(f"Invalid kind: {kind_id}")
return VMMIN_cls(vmr) return VMMIN_cls(vmr)

View File

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

View File

@@ -12,6 +12,8 @@ from .config import BusConfig, StripConfig
from .data import _base_values, _configuration from .data import _base_values, _configuration
from .navigation import Navigation from .navigation import Navigation
logger = logging.getLogger(__name__)
class AbstractBuilder(abc.ABC): class AbstractBuilder(abc.ABC):
@abc.abstractmethod @abc.abstractmethod
@@ -28,11 +30,10 @@ class AbstractBuilder(abc.ABC):
class MainFrameBuilder(AbstractBuilder): class MainFrameBuilder(AbstractBuilder):
"""Responsible for building the frames that sit directly on the mainframe""" """Responsible for building the frames that sit directly on the mainframe"""
logger = logging.getLogger("builders.mainframebuilder")
def __init__(self, app): def __init__(self, app):
self.kind = app.kind self.kind = app.kind
self.app = app self.app = app
self.logger = logger.getChild(self.__class__.__name__)
def setup(self): def setup(self):
self.app.title( self.app.title(
@@ -194,9 +195,9 @@ class NavigationFrameBuilder(AbstractBuilder):
if isinstance(child, ttk.Checkbutton) if isinstance(child, ttk.Checkbutton)
] ]
if _configuration.themes_enabled: if _configuration.themes_enabled:
self.navframe.rowconfigure(1, minsize=_configuration.level_height) self.navframe.rowconfigure(1, minsize=_configuration.channel_height)
else: else:
self.navframe.rowconfigure(1, minsize=_configuration.level_height + 10) self.navframe.rowconfigure(1, minsize=_configuration.channel_height + 10)
def teardown(self): def teardown(self):
pass pass
@@ -242,13 +243,19 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
orient="vertical", orient="vertical",
variable=self.labelframe.gain, variable=self.labelframe.gain,
command=self.labelframe.scale_callback, command=self.labelframe.scale_callback,
length=_configuration.level_height, length=_configuration.channel_height,
) )
self.scale.grid(column=1, row=0) self.scale.grid(column=1, row=0)
self.scale.bind("<Double-Button-1>", self.labelframe.reset_gain) self.scale.bind("<Double-Button-1>", self.labelframe.reset_gain)
self.scale.bind("<Button-1>", self.labelframe.scale_press) self.scale.bind("<Button-1>", self.labelframe.scale_press)
self.scale.bind("<ButtonRelease-1>", self.labelframe.scale_release) self.scale.bind("<ButtonRelease-1>", self.labelframe.scale_release)
self.scale.bind("<MouseWheel>", self.labelframe._on_mousewheel) self.scale.bind(
"<MouseWheel>",
partial(
self.labelframe.pause_updates,
self.labelframe._on_mousewheel,
),
)
def add_gain_label(self): def add_gain_label(self):
self.labelframe.gain_label = ttk.Label( self.labelframe.gain_label = ttk.Label(
@@ -262,7 +269,7 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
self.button_mute = ttk.Checkbutton( self.button_mute = ttk.Checkbutton(
self.labelframe, self.labelframe,
text="MUTE", 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'}", style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{self.identifier}Mute{self.index}.TButton'}",
variable=self.labelframe.mute, variable=self.labelframe.mute,
) )
@@ -282,7 +289,7 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
self.button_on = ttk.Checkbutton( self.button_on = ttk.Checkbutton(
self.labelframe, self.labelframe,
text="ON", 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'}", style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{self.identifier}On{self.index}.TButton'}",
variable=self.labelframe.on, variable=self.labelframe.on,
) )
@@ -323,7 +330,7 @@ class ChannelConfigFrameBuilder(AbstractBuilder):
] ]
self.configframe.grid(sticky=(tk.W)) 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) for i in range(self.configframe.phys_out + self.configframe.virt_out)
] ]
@@ -336,7 +343,7 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
self.configframe.slider_params = ("audibility",) self.configframe.slider_params = ("audibility",)
self.configframe.slider_vars = (tk.DoubleVar(),) self.configframe.slider_vars = (tk.DoubleVar(),)
else: else:
self.configframe.slider_params = ("comp", "gate", "limit") self.configframe.slider_params = ("comp.knob", "gate.knob", "limit")
self.configframe.slider_vars = [ self.configframe.slider_vars = [
tk.DoubleVar() for _ in self.configframe.slider_params tk.DoubleVar() for _ in self.configframe.slider_params
] ]
@@ -391,18 +398,18 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
from_=0.0, from_=0.0,
to=10.0, to=10.0,
orient="horizontal", orient="horizontal",
length=_configuration.level_width, length=_configuration.channel_width,
variable=self.configframe.slider_vars[ 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( 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("<Button-1>", self.configframe.scale_press)
comp_scale.bind("<ButtonRelease-1>", self.configframe.scale_release) comp_scale.bind("<ButtonRelease-1>", self.configframe.scale_release)
comp_scale.bind("<Enter>", partial(self.configframe.scale_enter, "comp")) comp_scale.bind("<Enter>", partial(self.configframe.scale_enter, "comp.knob"))
comp_scale.bind("<Leave>", self.configframe.scale_leave) comp_scale.bind("<Leave>", self.configframe.scale_leave)
comp_label.grid(column=0, row=0) comp_label.grid(column=0, row=0)
@@ -415,18 +422,18 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
from_=0.0, from_=0.0,
to=10.0, to=10.0,
orient="horizontal", orient="horizontal",
length=_configuration.level_width, length=_configuration.channel_width,
variable=self.configframe.slider_vars[ 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( 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("<Button-1>", self.configframe.scale_press)
gate_scale.bind("<ButtonRelease-1>", self.configframe.scale_release) gate_scale.bind("<ButtonRelease-1>", self.configframe.scale_release)
gate_scale.bind("<Enter>", partial(self.configframe.scale_enter, "gate")) gate_scale.bind("<Enter>", partial(self.configframe.scale_enter, "gate.knob"))
gate_scale.bind("<Leave>", self.configframe.scale_leave) gate_scale.bind("<Leave>", self.configframe.scale_leave)
gate_label.grid(column=2, row=0) gate_label.grid(column=2, row=0)
@@ -439,7 +446,7 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
from_=-40, from_=-40,
to=12, to=12,
orient="horizontal", orient="horizontal",
length=_configuration.level_width, length=_configuration.channel_width,
variable=self.configframe.slider_vars[ variable=self.configframe.slider_vars[
self.configframe.slider_params.index("limit") self.configframe.slider_params.index("limit")
], ],
@@ -463,7 +470,7 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
from_=0.0, from_=0.0,
to=10.0, to=10.0,
orient="horizontal", orient="horizontal",
length=_configuration.level_width, length=_configuration.channel_width,
variable=self.configframe.slider_vars[ variable=self.configframe.slider_vars[
self.configframe.slider_params.index("audibility") self.configframe.slider_params.index("audibility")
], ],
@@ -485,7 +492,9 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
ttk.Checkbutton( ttk.Checkbutton(
self.configframe, self.configframe,
text=param, 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'}", style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{param}.TButton'}",
variable=self.configframe.phys_out_params_vars[ variable=self.configframe.phys_out_params_vars[
self.configframe.phys_out_params.index(param) self.configframe.phys_out_params.index(param)
@@ -506,7 +515,9 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
ttk.Checkbutton( ttk.Checkbutton(
self.configframe, self.configframe,
text=param, 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'}", style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{param}.TButton'}",
variable=self.configframe.virt_out_params_vars[ variable=self.configframe.virt_out_params_vars[
self.configframe.virt_out_params.index(param) self.configframe.virt_out_params.index(param)
@@ -527,7 +538,9 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
ttk.Checkbutton( ttk.Checkbutton(
self.configframe, self.configframe,
text=param, 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'}", style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{param}.TButton'}",
variable=self.configframe.param_vars[i], variable=self.configframe.param_vars[i],
) )
@@ -563,7 +576,7 @@ class BusConfigFrameBuilder(ChannelConfigFrameBuilder):
} }
self.configframe.bus_modes = list(self.configframe.bus_mode_map.keys()) self.configframe.bus_modes = list(self.configframe.bus_mode_map.keys())
# fmt: on # 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.param_vars = [tk.BooleanVar() for _ in self.configframe.params]
self.configframe.bus_mode_label_text = tk.StringVar( self.configframe.bus_mode_label_text = tk.StringVar(
value=self.configframe.bus_mode_map[self.configframe.current_bus_mode()] value=self.configframe.bus_mode_map[self.configframe.current_bus_mode()]
@@ -577,10 +590,16 @@ class BusConfigFrameBuilder(ChannelConfigFrameBuilder):
column=0, row=0, columnspan=2, sticky=(tk.W) column=0, row=0, columnspan=2, sticky=(tk.W)
) )
self.configframe.busmode_button.bind( 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( 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): def create_param_buttons(self):
@@ -588,7 +607,9 @@ class BusConfigFrameBuilder(ChannelConfigFrameBuilder):
ttk.Checkbutton( ttk.Checkbutton(
self.configframe, self.configframe,
text=param, 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'}", style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{param}.TButton'}",
variable=self.configframe.param_vars[i], variable=self.configframe.param_vars[i],
) )

View File

@@ -1,10 +1,12 @@
import logging
import tkinter as tk import tkinter as tk
from math import log
from tkinter import ttk from tkinter import ttk
from . import builders from . import builders
from .data import _base_values, _configuration from .data import _base_values, _configuration
logger = logging.getLogger(__name__)
class ChannelLabelFrame(ttk.LabelFrame): class ChannelLabelFrame(ttk.LabelFrame):
"""Base class for a single channel""" """Base class for a single channel"""
@@ -14,6 +16,7 @@ class ChannelLabelFrame(ttk.LabelFrame):
self.parent = parent self.parent = parent
self.index = index self.index = index
self.id = id self.id = id
self.logger = logger.getChild(self.__class__.__name__)
self.styletable = self.parent.parent.styletable self.styletable = self.parent.parent.styletable
self.builder = builders.ChannelLabelFrameBuilder(self, index, id) self.builder = builders.ChannelLabelFrameBuilder(self, index, id)
@@ -40,18 +43,21 @@ class ChannelLabelFrame(ttk.LabelFrame):
return self.parent.target return self.parent.target
def getter(self, param): def getter(self, param):
if hasattr(self.target, param): try:
return getattr(self.target, param) return getattr(self.target, param)
except AttributeError as e:
self.logger(f"{type(e).__name__}: {e}")
def setter(self, param, value): def setter(self, param, value):
if hasattr(self.target, param): if param in dir(self.target): # avoid calling getattr (with hasattr)
setattr(self.target, param, value) setattr(self.target, param, value)
def scale_callback(self, *args): def scale_callback(self, *args):
"""callback function for scale widget""" """callback function for scale widget"""
self.setter("gain", self.gain.get()) val = round(self.gain.get(), 1)
self.gainlabel.set(round(self.gain.get(), 1)) self.setter("gain", val)
self.gainlabel.set(val)
def toggle_mute(self, *args): def toggle_mute(self, *args):
self.target.mute = self.mute.get() self.target.mute = self.mute.get()
@@ -82,17 +88,27 @@ class ChannelLabelFrame(ttk.LabelFrame):
self.parent.target.event.add("ldirty") self.parent.target.event.add("ldirty")
self.after(500, self.resume_updates) self.after(500, self.resume_updates)
def pause_updates(self, func, *args):
"""function wrapper, adds a 50ms delay on updates"""
_base_values.run_update = False
func(*args)
self.after(50, self.resume_updates)
def resume_updates(self): def resume_updates(self):
_base_values.run_update = True _base_values.run_update = True
def _on_mousewheel(self, event): def _on_mousewheel(self, event):
_base_values.run_update = False
self.gain.set( self.gain.set(
self.gain.get() round(
+ ( self.gain.get()
_configuration.mwscroll_step + (
if event.delta > 0 _configuration.mwscroll_step
else -_configuration.mwscroll_step if event.delta > 0
else -_configuration.mwscroll_step
),
1,
) )
) )
if self.gain.get() > 12: if self.gain.get() > 12:
@@ -100,7 +116,7 @@ class ChannelLabelFrame(ttk.LabelFrame):
elif self.gain.get() < -60: elif self.gain.get() < -60:
self.gain.set(-60) self.gain.set(-60)
self.setter("gain", self.gain.get()) self.setter("gain", self.gain.get())
self.after(1, self.resume_updates) self.gainlabel.set(round(self.gain.get(), 1))
def open_config(self): def open_config(self):
if self.conf.get(): if self.conf.get():
@@ -135,20 +151,21 @@ class ChannelLabelFrame(ttk.LabelFrame):
def sync_labels(self): def sync_labels(self):
"""sync labelframes according to label text""" """sync labelframes according to label text"""
retval = self.getter("label") retval = self.getter("label")
self.parent.label_cache[self.id].insert(self.index, retval) if self.parent.label_cache[self.id][self.index] != retval:
if len(retval) > 10: self.parent.label_cache[self.id][self.index] = retval
retval = f"{retval[:8]}.." if len(retval) > 10:
if not retval: retval = f"{retval[:8]}.."
self.parent.columnconfigure(self.index, minsize=0) if not retval:
self.parent.parent.subject.remove(self) self.parent.columnconfigure(self.index, minsize=0)
self.grid_remove() self.parent.parent.subject.remove(self)
else: self.grid_remove()
self.parent.parent.subject.add(self) else:
self.grid() self.parent.parent.subject.add(self)
self.configure(text=retval) self.grid()
self.configure(text=retval)
def grid_configure(self): 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)) child.grid_configure(padx=1, pady=1, sticky=(tk.W, tk.E))
for child in self.winfo_children() for child in self.winfo_children()
@@ -212,15 +229,18 @@ class Bus(ChannelLabelFrame):
class ChannelFrame(ttk.Frame): class ChannelFrame(ttk.Frame):
label_cache = {"strip": list(), "bus": list()}
def init(self, parent, id): def init(self, parent, id):
super().__init__(parent) super().__init__(parent)
self.parent = parent self.parent = parent
self.id = id self.id = id
self.phys_in, self.virt_in = parent.kind.ins self.phys_in, self.virt_in = parent.kind.ins
self.phys_out, self.virt_out = parent.kind.outs self.phys_out, self.virt_out = parent.kind.outs
self.label_cache = {
"strip": [""] * (self.phys_in + self.virt_in),
"bus": [""] * (self.phys_out + self.virt_out),
}
self.parent.subject.add(self) self.parent.subject.add(self)
self.update_labels()
@property @property
def target(self): def target(self):
@@ -242,17 +262,17 @@ class ChannelFrame(ttk.Frame):
if isinstance(frame, ttk.LabelFrame) if isinstance(frame, ttk.LabelFrame)
) )
def update_labels(self):
for labelframe in self.labelframes:
labelframe.on_update("labelframe")
def on_update(self, subject): def on_update(self, subject):
if subject == "pdirty": if subject == "pdirty":
target = getattr(self.target, self.id) self.update_labels()
num = getattr(self.parent.kind, f"num_{self.id}")
if self.label_cache[self.id] != [target[i].label for i in range(num)]:
for labelframe in self.labelframes:
labelframe.on_update("labelframe")
def grid_configure(self): def grid_configure(self):
[ [
self.columnconfigure(i, minsize=_configuration.level_width) self.columnconfigure(i, minsize=_configuration.channel_width)
for i, _ in enumerate(self.labelframes) for i, _ in enumerate(self.labelframes)
] ]
[self.rowconfigure(0, minsize=100) for i, _ in enumerate(self.labelframes)] [self.rowconfigure(0, minsize=100) for i, _ in enumerate(self.labelframes)]
@@ -264,7 +284,7 @@ class ChannelFrame(ttk.Frame):
setattr(self.parent, f"{self.identifier}_frame", None) setattr(self.parent, f"{self.identifier}_frame", None)
def _make_channelframe(parent, id): def _make_channelframe(parent, identifier):
""" """
Creates a Channel Frame class of type strip or bus 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_in, virt_in = parent.kind.ins
phys_out, virt_out = parent.kind.outs 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 Grids each labelframe, grid_removes any without a label
""" """
for i, labelframe in enumerate( 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) labelframe.grid(row=0, column=i)
if not labelframe.target.label: label = labelframe.target.label
if not label:
self.columnconfigure(i, minsize=0) self.columnconfigure(i, minsize=0)
labelframe.grid_remove() labelframe.grid_remove()
self.label_cache[identifier][i] = label
def init_strip(self, *args, **kwargs): def init_strip(self, *args, **kwargs):
self.init(parent, id) self.init(parent, identifier)
self.strips = tuple(Strip(self, i, id) for i in range(phys_in + virt_in)) 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(row=0, column=0, sticky=(tk.W))
self.grid_configure() self.grid_configure()
init_labels(self, id) init_labels(self)
def init_bus(self, *args, **kwargs): def init_bus(self, *args, **kwargs):
self.init(parent, id) self.init(parent, identifier)
self.buses = tuple(Bus(self, i, id) for i in range(phys_out + virt_out)) self.buses = tuple(Bus(self, i, identifier) for i in range(phys_out + virt_out))
if _configuration.extended: if _configuration.extended:
if _configuration.extends_horizontal: if _configuration.extends_horizontal:
self.grid(row=0, column=2, sticky=(tk.W)) self.grid(row=0, column=2, sticky=(tk.W))
@@ -303,11 +327,11 @@ def _make_channelframe(parent, id):
else: else:
self.grid(row=0, column=0) self.grid(row=0, column=0)
self.grid_configure() self.grid_configure()
init_labels(self, id) init_labels(self)
if id == "strip": if identifier == "strip":
CHANNELFRAME_cls = type( CHANNELFRAME_cls = type(
f"ChannelFrame{id.capitalize()}", f"ChannelFrame{identifier.capitalize()}",
(ChannelFrame,), (ChannelFrame,),
{ {
"__init__": init_strip, "__init__": init_strip,
@@ -315,7 +339,7 @@ def _make_channelframe(parent, id):
) )
else: else:
CHANNELFRAME_cls = type( CHANNELFRAME_cls = type(
f"ChannelFrame{id.capitalize()}", f"ChannelFrame{identifier.capitalize()}",
(ChannelFrame,), (ChannelFrame,),
{ {
"__init__": init_bus, "__init__": init_bus,

View File

@@ -1,10 +1,11 @@
import tkinter as tk import logging
from functools import partial
from tkinter import ttk from tkinter import ttk
from . import builders from . import builders
from .data import _base_values, _configuration from .data import _base_values, _configuration
logger = logging.getLogger(__name__)
class Config(ttk.Frame): class Config(ttk.Frame):
def __init__(self, parent, index, _id): def __init__(self, parent, index, _id):
@@ -12,6 +13,7 @@ class Config(ttk.Frame):
self.parent = parent self.parent = parent
self.index = index self.index = index
self.id = _id self.id = _id
self.logger = logger.getChild(self.__class__.__name__)
self.styletable = parent.styletable self.styletable = parent.styletable
self.phys_in, self.virt_in = parent.kind.ins self.phys_in, self.virt_in = parent.kind.ins
self.phys_out, self.virt_out = parent.kind.outs self.phys_out, self.virt_out = parent.kind.outs
@@ -29,12 +31,26 @@ class Config(ttk.Frame):
return self.parent.target return self.parent.target
def getter(self, param): def getter(self, param):
if hasattr(self.target, param): param = param.split(".")
return getattr(self.target, param) 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): def setter(self, param, value):
if hasattr(self.target, param): param = param.split(".")
setattr(self.target, param, value) 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): def scale_press(self, *args):
self.after(1, self.remove_events) self.after(1, self.remove_events)
@@ -52,6 +68,14 @@ class Config(ttk.Frame):
self.parent.target.event.add("ldirty") self.parent.target.event.add("ldirty")
self.after(350, self.resume_updates) self.after(350, self.resume_updates)
def pause_updates(self, func, *args):
"""function wrapper, adds a 50ms delay on updates"""
_base_values.run_update = False
func(*args)
self.after(50, self.resume_updates)
def resume_updates(self): def resume_updates(self):
_base_values.run_update = True _base_values.run_update = True
@@ -66,7 +90,7 @@ class Config(ttk.Frame):
"""callback function for scale widget""" """callback function for scale widget"""
val = self.slider_vars[self.slider_params.index(param)].get() val = self.slider_vars[self.slider_params.index(param)].get()
self.setter(param, val) self.setter(param, round(val, 1))
self.parent.nav_frame.info_text.set(round(val, 1)) self.parent.nav_frame.info_text.set(round(val, 1))
def reset_scale(self, param, val, *args): def reset_scale(self, param, val, *args):
@@ -98,6 +122,7 @@ class StripConfig(Config):
self.make_row_2() self.make_row_2()
self.builder.grid_configure() self.builder.grid_configure()
self.parent.target.clear_dirty()
self.sync() self.sync()
@property @property
@@ -155,6 +180,12 @@ class StripConfig(Config):
self.param_vars[i].set(self.getter(param)) self.param_vars[i].set(self.getter(param))
for i, param in enumerate(self.params) 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: if not _configuration.themes_enabled:
[ [
@@ -193,6 +224,7 @@ class BusConfig(Config):
self.make_row_1() self.make_row_1()
self.builder.grid_configure() self.builder.grid_configure()
self.parent.target.clear_dirty()
self.sync() self.sync()
@property @property

View File

@@ -6,26 +6,36 @@ try:
except ModuleNotFoundError: except ModuleNotFoundError:
import tomli as tomllib import tomli as tomllib
LOGGER = logging.getLogger("configurations") logger = logging.getLogger(__name__)
configuration = {} 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(): def get_configpath():
LOGGER.info(f"Loaded configuration configs/{name}") configpaths = [
configuration[name] = cfg 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 = { _defaults = {
"configs": { "configs": {
@@ -42,6 +52,7 @@ _defaults = {
"channel": { "channel": {
"width": 80, "width": 80,
"height": 130, "height": 130,
"xpadding": 3,
}, },
"mwscroll_step": { "mwscroll_step": {
"size": 3, "size": 3,
@@ -49,10 +60,16 @@ _defaults = {
"submixes": { "submixes": {
"default": 0, "default": 0,
}, },
"navigation": {"show": True},
} }
if "app" in configuration: if "app" in configuration:
configuration["app"] = _defaults | configuration["app"] for key in _defaults:
if key in configuration["app"]:
configuration["app"][key] = _defaults[key] | configuration["app"][key]
else:
configuration["app"][key] = _defaults[key]
else: else:
configuration["app"] = _defaults configuration["app"] = _defaults
@@ -60,3 +77,22 @@ else:
def get_configuration(key): def get_configuration(key):
if key in configuration: if key in configuration:
return configuration[key] return configuration[key]
def loader(kind_id, target):
configs = {"reset": target.configs["reset"]}
if configpath := get_configpath():
userconfigpath = configpath / kind_id
if userconfigpath.exists():
filepaths = list(userconfigpath.glob("*.toml"))
for filepath in filepaths:
identifier = filepath.with_suffix("").stem
try:
with open(filepath, "rb") as f:
configs[identifier] = tomllib.load(f)
logger.info(f"loader: {identifier} loaded into memory")
except tomllib.TOMLDecodeError:
logger.error(f"Invalid TOML config: configs/{filename.stem}")
target.configs = configs
return target.configs

View File

@@ -32,10 +32,15 @@ class Configurations(metaclass=SingletonMeta):
# bus assigned as current submix # bus assigned as current submix
submixes: int = configuration["submixes"]["default"] submixes: int = configuration["submixes"]["default"]
# width of a single labelframe # width of a single channel labelframe
level_width: int = configuration["channel"]["width"] channel_width: int = configuration["channel"]["width"]
# height of a single labelframe # height of a single channel labelframe
level_height: int = configuration["channel"]["height"] 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 @property
def config(self): def config(self):
@@ -46,7 +51,7 @@ class Configurations(metaclass=SingletonMeta):
@dataclass @dataclass
class BaseValues(metaclass=SingletonMeta): class BaseValues(metaclass=SingletonMeta):
# pause updates after releasing scale # pause updates after releasing scale
run_update: bool = True run_update: bool = False
# are we dragging main window with mouse 1 # are we dragging main window with mouse 1
dragging: bool = False dragging: bool = False
# a vban connection established # a vban connection established

View File

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

View File

@@ -1,5 +1,4 @@
import tkinter as tk import tkinter as tk
from math import log
from tkinter import ttk from tkinter import ttk
from . import builders from . import builders
@@ -42,11 +41,13 @@ class GainLayer(ttk.LabelFrame):
return "gainlayer" return "gainlayer"
def getter(self, param): def getter(self, param):
if hasattr(self.target, param): try:
return getattr(self.target, param) return getattr(self.target, param)
except AttributeError as e:
self.logger(f"{type(e).__name__}: {e}")
def setter(self, param, value): def setter(self, param, value):
if hasattr(self.target, param): if param in dir(self.target): # avoid calling getattr (with hasattr)
setattr(self.target, param, value) setattr(self.target, param, value)
def reset_gain(self, *args): def reset_gain(self, *args):
@@ -57,8 +58,9 @@ class GainLayer(ttk.LabelFrame):
def scale_callback(self, *args): def scale_callback(self, *args):
"""callback function for scale widget""" """callback function for scale widget"""
self.setter("gain", self.gain.get()) val = round(self.gain.get(), 1)
self.gainlabel.set(round(self.gain.get(), 1)) self.setter("gain", val)
self.gainlabel.set(val)
def scale_press(self, *args): def scale_press(self, *args):
self.after(1, self.remove_events) self.after(1, self.remove_events)
@@ -76,6 +78,14 @@ class GainLayer(ttk.LabelFrame):
self.parent.target.event.add("ldirty") self.parent.target.event.add("ldirty")
self.after(500, self.resume_updates) self.after(500, self.resume_updates)
def pause_updates(self, func, *args):
"""function wrapper, adds a 50ms delay on updates"""
_base_values.run_update = False
func(*args)
self.after(50, self.resume_updates)
def resume_updates(self): def resume_updates(self):
_base_values.run_update = True _base_values.run_update = True
@@ -157,12 +167,14 @@ class GainLayer(ttk.LabelFrame):
self.level.set( self.level.set(
( (
0 0
if self.parent.target.strip[self.index].mute or not self.on.get() if self.parent.parent.strip_frame.strips[self.index].mute.get()
or not self.on.get()
else 72 + val - 12 + self.gain.get() else 72 + val - 12 + self.gain.get()
) )
) )
def grid_configure(self): def grid_configure(self):
self.grid(padx=_configuration.channel_xpadding, sticky=(tk.N, tk.S))
[ [
child.grid_configure(padx=1, pady=1, sticky=(tk.N, tk.S, tk.W, tk.E)) child.grid_configure(padx=1, pady=1, sticky=(tk.N, tk.S, tk.W, tk.E))
for child in self.winfo_children() for child in self.winfo_children()
@@ -250,11 +262,11 @@ class SubMixFrame(ttk.Frame):
def grid_configure(self): def grid_configure(self):
[ [
self.columnconfigure(i, minsize=_configuration.level_width) self.columnconfigure(i, minsize=_configuration.channel_width)
for i, _ in enumerate(self.labelframes) 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) for i, _ in enumerate(self.labelframes)
] ]

View File

@@ -2,27 +2,31 @@ import logging
import tkinter as tk import tkinter as tk
import webbrowser import webbrowser
from functools import partial from functools import partial
from tkinter import messagebox, ttk from tkinter import messagebox
import vban_cmd
from vban_cmd.error import VBANCMDConnectionError
import sv_ttk import sv_ttk
import vban_cmd
from vban_cmd.error import VBANCMDError
from .data import _base_values, _configuration, get_configuration, kind_get from .data import _base_values, _configuration, get_configuration, kind_get
logger = logging.getLogger(__name__)
class Menus(tk.Menu): class Menus(tk.Menu):
logger = logging.getLogger("menu.menus")
def __init__(self, parent, vmr): def __init__(self, parent, vmr):
super().__init__() super().__init__()
self.parent = parent self.parent = parent
self.vmr = vmr self.vmr = vmr
self.logger = logger.getChild(self.__class__.__name__)
self.vban_config = get_configuration("vban") self.vban_config = get_configuration("vban")
self.app_config = get_configuration("app") self.app_config = get_configuration("app")
self._is_topmost = tk.BooleanVar() self._is_topmost = tk.BooleanVar()
self._lock = tk.BooleanVar() self._lock = tk.BooleanVar()
self._unlock = tk.BooleanVar() self._unlock = tk.BooleanVar()
self._navigation_show = tk.BooleanVar(value=_configuration.navigation_show)
self._navigation_hide = tk.BooleanVar(value=not _configuration.navigation_show)
self._selected_bus = list(tk.BooleanVar() for _ in range(8)) self._selected_bus = list(tk.BooleanVar() for _ in range(8))
# voicemeeter menu # voicemeeter menu
@@ -82,14 +86,14 @@ class Menus(tk.Menu):
self.menu_configs_load = tk.Menu(self.menu_configs, tearoff=0) self.menu_configs_load = tk.Menu(self.menu_configs, tearoff=0)
self.menu_configs.add_cascade(menu=self.menu_configs_load, label="Load config") self.menu_configs.add_cascade(menu=self.menu_configs_load, label="Load config")
self.config_defaults = {"reset"} self.config_defaults = {"reset"}
if len(self.target.configs) > len(self.config_defaults) and all( if len(self.parent.userconfigs) > len(self.config_defaults) and all(
key in self.target.configs for key in self.config_defaults key in self.parent.userconfigs for key in self.config_defaults
): ):
[ [
self.menu_configs_load.add_command( self.menu_configs_load.add_command(
label=profile, command=partial(self.load_profile, profile) label=profile, command=partial(self.load_profile, profile)
) )
for profile in self.target.configs.keys() for profile in self.parent.userconfigs.keys()
if profile not in self.config_defaults if profile not in self.config_defaults
] ]
else: else:
@@ -153,6 +157,23 @@ class Menus(tk.Menu):
) )
if not _configuration.themes_enabled: if not _configuration.themes_enabled:
self.menu_layout.entryconfig(2, state="disabled") self.menu_layout.entryconfig(2, state="disabled")
# layout/navigation
self.menu_navigation = tk.Menu(self.menu_layout, tearoff=0)
self.menu_layout.add_cascade(menu=self.menu_navigation, label="Navigation")
self.menu_navigation.add_checkbutton(
label="show",
onvalue=1,
offvalue=0,
variable=self._navigation_show,
command=partial(self.toggle_navigation, "show"),
)
self.menu_navigation.add_checkbutton(
label="hide",
onvalue=1,
offvalue=0,
variable=self._navigation_hide,
command=partial(self.toggle_navigation, "hide"),
)
# vban connect menu # vban connect menu
self.menu_vban = tk.Menu(self, tearoff=0) self.menu_vban = tk.Menu(self, tearoff=0)
@@ -204,7 +225,10 @@ class Menus(tk.Menu):
] ]
def action_invoke_voicemeeter(self, cmd): 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): def action_set_voicemeeter(self, cmd, val=True):
if cmd == "lock": if cmd == "lock":
@@ -212,15 +236,28 @@ class Menus(tk.Menu):
self._unlock.set(not self._lock.get()) self._unlock.set(not self._lock.get())
setattr(self.target.command, cmd, val) setattr(self.target.command, cmd, val)
def load_custom_profile(self, profile):
self.logger.info(f"loading user profile {profile}")
self.target.apply(profile)
if not _base_values.run_update:
self.parent.subject.notify("pdirty")
def load_profile(self, profile): def load_profile(self, profile):
self.logger.info(f"loading user profile {profile}")
self.target.apply_config(profile) self.target.apply_config(profile)
if not _base_values.run_update:
self.parent.subject.notify("pdirty")
def load_defaults(self): def load_defaults(self):
resp = messagebox.askyesno( msg = (
message="Are you sure you want to Reset values to defaults?\nPhysical strips B1, Virtual strips A1\nMono, Solo, Mute, EQ all OFF" "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: if resp:
self.target.apply_config("reset") self.load_profile("reset")
def always_on_top(self): def always_on_top(self):
self.parent.attributes("-topmost", self._is_topmost.get()) self.parent.attributes("-topmost", self._is_topmost.get())
@@ -242,6 +279,7 @@ class Menus(tk.Menu):
self.parent.nav_frame.show_submix() self.parent.nav_frame.show_submix()
for j, var in enumerate(self._selected_bus): for j, var in enumerate(self._selected_bus):
var.set(i == j) var.set(i == j)
self.parent.subject.notify("submix")
def load_theme(self, theme): def load_theme(self, theme):
sv_ttk.set_theme(theme) sv_ttk.set_theme(theme)
@@ -277,11 +315,13 @@ class Menus(tk.Menu):
def menu_teardown(self, i): def menu_teardown(self, i):
# remove config load menus # remove config load menus
[ if len(self.parent.userconfigs) > len(self.config_defaults):
self.menu_configs_load.delete(key) for profile in self.parent.userconfigs:
for key in self.target.configs.keys() if profile not in self.config_defaults:
if key not in self.config_defaults try:
] self.menu_configs_load.delete(profile)
except tk._tkinter.tclError as e:
self.logger.warning(f"{type(e).__name__}: {e}")
[ [
self.menu_vban.entryconfig(j, state="disabled") self.menu_vban.entryconfig(j, state="disabled")
@@ -290,19 +330,28 @@ class Menus(tk.Menu):
] ]
def menu_setup(self): def menu_setup(self):
if len(self.target.configs) > len(self.config_defaults) and all( if len(self.parent.userconfigs) > len(self.config_defaults):
key in self.target.configs for key in self.config_defaults for profile in self.parent.userconfigs:
): if profile not in self.config_defaults:
[ self.menu_configs_load.add_command(
self.menu_configs_load.add_command( label=profile, command=partial(self.load_profile, profile)
label=profile, command=partial(self.load_profile, profile) )
) self.menu_configs.entryconfig(0, state="normal")
for profile in self.target.configs.keys()
if profile not in self.config_defaults
]
else: else:
self.menu_configs.entryconfig(0, state="disabled") self.menu_configs.entryconfig(0, state="disabled")
def toggle_navigation(self, cmd=None):
if cmd == "show":
self.logger.debug("show navframe")
self.parent.nav_frame.grid()
self._navigation_show.set(True)
self._navigation_hide.set(not self._navigation_show.get())
else:
self.logger.debug("hide navframe")
self.parent.nav_frame.grid_remove()
self._navigation_hide.set(True)
self._navigation_show.set(not self._navigation_hide.get())
def vban_connect(self, i): def vban_connect(self, i):
opts = {} opts = {}
opts |= self.vban_config[f"connection-{i+1}"] opts |= self.vban_config[f"connection-{i+1}"]
@@ -312,16 +361,19 @@ class Menus(tk.Menu):
try: try:
self.logger.info(f"Attempting vban connection to {opts.get('ip')}") self.logger.info(f"Attempting vban connection to {opts.get('ip')}")
self.vban.login() self.vban.login()
except VBANCMDError as e: except VBANCMDConnectionError as e:
self.vban.logout() self.vban.logout()
msg = (str(e), f"Please check your connection settings") msg = (
f"Timeout attempting to establish connection to {opts.get('ip')}",
f"Please check your connection settings",
)
messagebox.showerror("Connection Error", "\n".join(msg)) messagebox.showerror("Connection Error", "\n".join(msg))
msg = (str(e), f"resuming local connection") msg = (str(e), f"resuming local connection")
self.logger.error(", ".join(msg)) self.logger.error(", ".join(msg))
self.after(1, self.enable_vban_menus) self.after(1, self.enable_vban_menus)
return return
self.menu_teardown(i) self.menu_teardown(i)
self.vban.event.ldirty = True self.vban.event.add(["pdirty", "ldirty"])
# destroy the current App frames # destroy the current App frames
self.parent._destroy_top_level_frames() self.parent._destroy_top_level_frames()
_base_values.vban_connected = True _base_values.vban_connected = True
@@ -336,6 +388,11 @@ class Menus(tk.Menu):
self.menu_layout.entryconfig( self.menu_layout.entryconfig(
0, state=f"{'normal' if kind.name == 'potato' else 'disabled'}" 0, state=f"{'normal' if kind.name == 'potato' else 'disabled'}"
) )
# ensure the configs are reloaded into memory
if "config" in self.parent.target.__dict__:
del self.parent.target.__dict__["config"]
if "userconfigs" in self.parent.__dict__:
del self.parent.__dict__["userconfigs"]
self.menu_setup() self.menu_setup()
def vban_disconnect(self, i): def vban_disconnect(self, i):
@@ -349,13 +406,18 @@ class Menus(tk.Menu):
self.vban.logout() self.vban.logout()
# build new app frames according to a kind # build new app frames according to a kind
kind = kind_get(self.vmr.type) kind = kind_get(self.vmr.type)
self.parent.build_app(kind, None) self.parent.build_app(kind)
target_menu = getattr(self, f"menu_vban_{i+1}") target_menu = getattr(self, f"menu_vban_{i+1}")
target_menu.entryconfig(0, state="normal") target_menu.entryconfig(0, state="normal")
target_menu.entryconfig(1, state="disabled") target_menu.entryconfig(1, state="disabled")
self.menu_layout.entryconfig( self.menu_layout.entryconfig(
0, state=f"{'normal' if kind.name == 'potato' else 'disabled'}" 0, state=f"{'normal' if kind.name == 'potato' else 'disabled'}"
) )
# ensure the configs are reloaded into memory
if "config" in self.parent.target.__dict__:
del self.parent.target.__dict__["config"]
if "userconfigs" in self.parent.__dict__:
del self.parent.__dict__["userconfigs"]
self.menu_setup() self.menu_setup()
self.after(15000, self.enable_vban_menus) self.after(15000, self.enable_vban_menus)

View File

@@ -6,14 +6,17 @@ from . import builders
from .data import _configuration from .data import _configuration
from .gainlayer import SubMixFrame from .gainlayer import SubMixFrame
logger = logging.getLogger(__name__)
class Navigation(ttk.Frame): class Navigation(ttk.Frame):
logger = logging.getLogger("navigation.navigation")
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent) super().__init__(parent)
self.parent = parent self.parent = parent
self.logger = logger.getChild(self.__class__.__name__)
self.grid(row=0, column=3, padx=(0, 2), pady=(5, 5), sticky=(tk.W, tk.E)) self.grid(row=0, column=3, padx=(0, 2), pady=(5, 5), sticky=(tk.W, tk.E))
if not _configuration.navigation_show:
self.grid_remove()
self.styletable = self.parent.styletable self.styletable = self.parent.styletable
self.builder = builders.NavigationFrameBuilder(self) self.builder = builders.NavigationFrameBuilder(self)