mirror of
https://github.com/onyx-and-iris/voicemeeter-compact.git
synced 2026-04-08 17:03:32 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b809bcb28f | |||
| a0b9a92a2a | |||
| 9faf8ae10c | |||
| 82cf0e914b | |||
| 3e68488231 | |||
| 6d46d9a9a5 | |||
| e4068277f7 | |||
| 674999a461 |
11
CHANGELOG.md
11
CHANGELOG.md
@@ -9,6 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
- [ ] Add support for forest theme (if rbende adds it to pypi)
|
- [ ] 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
|
## [1.8.0] - 2023-06-29
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -34,16 +34,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 +53,9 @@ It's important to know that only labelled strips and buses will appear in the Ch
|
|||||||
|
|
||||||
If the GUI looks like the above when you first load it, then no channels are labelled. From the menu, `Configs->Load config` you may load an example config. Save your current Voicemeeter settings first :).
|
If the GUI looks like the above when you first load it, then no channels are labelled. From the menu, `Configs->Load config` you may load an example config. Save your current Voicemeeter settings first :).
|
||||||
|
|
||||||
### kind_id
|
### KIND_ID
|
||||||
|
|
||||||
Set the kind of Voicemeeter, kind_id may be:
|
Set the kind of Voicemeeter, KIND_ID may be:
|
||||||
|
|
||||||
- `basic`
|
- `basic`
|
||||||
- `banana`
|
- `banana`
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
92
poetry.lock
generated
92
poetry.lock
generated
@@ -1,10 +1,25 @@
|
|||||||
|
# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "black"
|
name = "black"
|
||||||
version = "22.12.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"
|
||||||
@@ -23,9 +38,12 @@ uvloop = ["uvloop (>=0.15.2)"]
|
|||||||
name = "click"
|
name = "click"
|
||||||
version = "8.1.3"
|
version = "8.1.3"
|
||||||
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.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
|
||||||
|
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
|
||||||
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||||
@@ -34,104 +52,118 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
|||||||
name = "colorama"
|
name = "colorama"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
description = "Cross-platform colored terminal text."
|
description = "Cross-platform colored terminal text."
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
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]]
|
[[package]]
|
||||||
name = "isort"
|
name = "isort"
|
||||||
version = "5.12.0"
|
version = "5.12.0"
|
||||||
description = "A Python utility / library to sort Python imports."
|
description = "A Python utility / library to sort Python imports."
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8.0"
|
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]
|
[package.extras]
|
||||||
colors = ["colorama (>=0.4.3)"]
|
colors = ["colorama (>=0.4.3)"]
|
||||||
requirements-deprecated-finder = ["pip-api", "pipreqs"]
|
|
||||||
pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
|
pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
|
||||||
plugins = ["setuptools"]
|
plugins = ["setuptools"]
|
||||||
|
requirements-deprecated-finder = ["pip-api", "pipreqs"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mypy-extensions"
|
name = "mypy-extensions"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
description = "Type system extensions for programs checked with the mypy type checker."
|
description = "Type system extensions for programs checked with the mypy type checker."
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5"
|
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.11.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]]
|
[[package]]
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
version = "3.8.0"
|
version = "3.8.0"
|
||||||
description = "A small Python package 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.0-py3-none-any.whl", hash = "sha256:ca9ed98ce73076ba72e092b23d3c93ea6c4e186b3f1c3dad6edd98ff6ffcca2e"},
|
||||||
|
{file = "platformdirs-3.8.0.tar.gz", hash = "sha256:b0cabcb11063d21a0b261d557acb0a9d2126350e63b70cdf7db6347baea456dc"},
|
||||||
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx (>=7.0.1)"]
|
docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
|
||||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest (>=7.3.1)"]
|
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sv-ttk"
|
name = "sv-ttk"
|
||||||
version = "2.5.1"
|
version = "2.5.1"
|
||||||
description = "A gorgeous theme for Tkinter, based on Windows 11's UI"
|
description = "A gorgeous theme for Tkinter, based on Windows 11's UI"
|
||||||
category = "main"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "sv_ttk-2.5.1-py3-none-any.whl", hash = "sha256:c7388741e14316b4e9c3b9fd135e1e2e5b501f1b30c89b7af1e26286b38f2ccc"},
|
||||||
|
{file = "sv_ttk-2.5.1.tar.gz", hash = "sha256:e32d60587db7debe4d7d7438f66257ffcd9db5b8efbcb52151697fa8a662a4f5"},
|
||||||
|
]
|
||||||
|
|
||||||
[[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 = "2.0.0"
|
version = "2.2.0"
|
||||||
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.2.0-py3-none-any.whl", hash = "sha256:ad0bd848b5412004c4c14976e8a9a18d0fc727599e343db0fb67f9427c8541fa"},
|
||||||
|
{file = "vban_cmd-2.2.0.tar.gz", hash = "sha256:7bad6001504fd052df3192c7d817e604703353f57efa7648c1f1d6e425665006"},
|
||||||
|
]
|
||||||
|
|
||||||
[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 = "2.0.2"
|
version = "2.1.2"
|
||||||
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.1.2-py3-none-any.whl", hash = "sha256:43a6e36282a89f2701b58f627bdd21c5e17f9f9044bf3337059d89e91f25fc09"},
|
||||||
|
{file = "voicemeeter_api-2.1.2.tar.gz", hash = "sha256:c24c643e868535786860420fe5775a10e2ace6f79ec4905a172a1c64712f3d6b"},
|
||||||
|
]
|
||||||
|
|
||||||
[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"
|
||||||
content-hash = "3a59de3a76e4c0ca11c0166750fa1af7d7c887750f855b48c45359068ef04798"
|
content-hash = "3a59de3a76e4c0ca11c0166750fa1af7d7c887750f855b48c45359068ef04798"
|
||||||
|
|
||||||
[metadata.files]
|
|
||||||
black = []
|
|
||||||
click = []
|
|
||||||
colorama = []
|
|
||||||
isort = []
|
|
||||||
mypy-extensions = []
|
|
||||||
pathspec = []
|
|
||||||
platformdirs = []
|
|
||||||
sv-ttk = []
|
|
||||||
tomli = []
|
|
||||||
vban-cmd = []
|
|
||||||
voicemeeter-api = []
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "voicemeeter-compact"
|
name = "voicemeeter-compact"
|
||||||
version = "1.8.1"
|
version = "1.9.0"
|
||||||
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"
|
||||||
@@ -14,8 +14,8 @@ include = ["vmcompact/img/cat.ico"]
|
|||||||
python = "^3.10"
|
python = "^3.10"
|
||||||
sv-ttk = "^2.5.1"
|
sv-ttk = "^2.5.1"
|
||||||
tomli = { version = "^2.0.1", python = "<3.11" }
|
tomli = { version = "^2.0.1", python = "<3.11" }
|
||||||
voicemeeter-api = "^2.0.2"
|
voicemeeter-api = "^2.1.2"
|
||||||
vban-cmd = "^2.0.0"
|
vban-cmd = "^2.2.0"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
black = { version = "^22.6.0", allow-prereleases = true }
|
black = { version = "^22.6.0", allow-prereleases = true }
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import logging
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from functools import cached_property
|
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 .configurations import loader
|
from .configurations import loader
|
||||||
from .data import _base_values, _configuration, _kinds_all
|
from .data import _base_values, _configuration, _kinds_all, get_configuration
|
||||||
from .errors import VMCompactError
|
from .errors import VMCompactError
|
||||||
from .menu import Menus
|
from .menu import Menus
|
||||||
from .subject import Subject
|
from .subject import Subject
|
||||||
@@ -48,7 +50,7 @@ class App(tk.Tk):
|
|||||||
self.minsize(275, False)
|
self.minsize(275, False)
|
||||||
self.subject = Subject()
|
self.subject = Subject()
|
||||||
self._configs = None
|
self._configs = None
|
||||||
self["menu"] = Menus(self, vmr)
|
self.menu = self["menu"] = Menus(self, vmr)
|
||||||
self.styletable = ttk.Style()
|
self.styletable = ttk.Style()
|
||||||
if _configuration.config:
|
if _configuration.config:
|
||||||
vmr.apply_config(_configuration.config)
|
vmr.apply_config(_configuration.config)
|
||||||
@@ -58,11 +60,7 @@ class App(tk.Tk):
|
|||||||
self.drag_id = ""
|
self.drag_id = ""
|
||||||
self.bind("<Configure>", self.dragging)
|
self.bind("<Configure>", self.dragging)
|
||||||
|
|
||||||
def start_updates(self):
|
self.after(1, self.healthcheck_step)
|
||||||
self.logger.debug("updates started")
|
|
||||||
_base_values.run_update = True
|
|
||||||
if self._vmr.gui.launched_by_api:
|
|
||||||
self.on_pdirty()
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{type(self).__name__}App"
|
return f"{type(self).__name__}App"
|
||||||
@@ -123,7 +121,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()
|
||||||
@@ -145,9 +143,45 @@ class App(tk.Tk):
|
|||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def userconfigs(self):
|
def userconfigs(self):
|
||||||
self._configs = loader(self.kind.name)
|
self._configs = loader(self.kind.name, self.target)
|
||||||
return self._configs
|
return self._configs
|
||||||
|
|
||||||
|
def start_updates(self):
|
||||||
|
self.logger.debug("updates started")
|
||||||
|
_base_values.run_update = True
|
||||||
|
if self._vmr.gui.launched_by_api:
|
||||||
|
self.on_pdirty()
|
||||||
|
|
||||||
|
def healthcheck_step(self):
|
||||||
|
if not _base_values.vban_connected:
|
||||||
|
try:
|
||||||
|
self._vmr.pdirty
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
_apps = {kind.name: App.make(kind) for kind in _kinds_all}
|
_apps = {kind.name: App.make(kind) for kind in _kinds_all}
|
||||||
|
|
||||||
|
|||||||
@@ -249,7 +249,13 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
|
|||||||
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(
|
||||||
@@ -263,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,
|
||||||
)
|
)
|
||||||
@@ -283,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,
|
||||||
)
|
)
|
||||||
@@ -486,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)
|
||||||
@@ -507,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)
|
||||||
@@ -528,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],
|
||||||
)
|
)
|
||||||
@@ -578,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):
|
||||||
@@ -589,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],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -88,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:
|
||||||
@@ -106,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():
|
||||||
|
|||||||
@@ -68,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
|
||||||
|
|
||||||
|
|||||||
@@ -10,28 +10,32 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
configuration = {}
|
configuration = {}
|
||||||
|
|
||||||
configpaths = [
|
|
||||||
Path.cwd() / "configs",
|
|
||||||
Path.home() / ".config" / "vm-compact" / "configs",
|
|
||||||
Path.home() / "Documents" / "Voicemeeter" / "configs",
|
|
||||||
]
|
|
||||||
for configpath in configpaths:
|
|
||||||
if configpath.is_dir():
|
|
||||||
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
|
def get_configpath():
|
||||||
break
|
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 = {
|
_defaults = {
|
||||||
"configs": {
|
"configs": {
|
||||||
@@ -75,17 +79,20 @@ def get_configuration(key):
|
|||||||
return configuration[key]
|
return configuration[key]
|
||||||
|
|
||||||
|
|
||||||
def loader(kind_id):
|
def loader(kind_id, target):
|
||||||
configs = {}
|
configs = {"reset": target.configs["reset"]}
|
||||||
userconfigpath = Path.home() / ".config" / "vm-compact" / "configs" / kind_id
|
if configpath := get_configpath():
|
||||||
if userconfigpath.exists():
|
userconfigpath = configpath / kind_id
|
||||||
filepaths = list(userconfigpath.glob("*.toml"))
|
if userconfigpath.exists():
|
||||||
for filepath in filepaths:
|
filepaths = list(userconfigpath.glob("*.toml"))
|
||||||
identifier = filepath.with_suffix("").stem
|
for filepath in filepaths:
|
||||||
try:
|
identifier = filepath.with_suffix("").stem
|
||||||
with open(filepath, "rb") as f:
|
try:
|
||||||
configs[identifier] = tomllib.load(f)
|
with open(filepath, "rb") as f:
|
||||||
logger.info(f"loader: {identifier} loaded into memory")
|
configs[identifier] = tomllib.load(f)
|
||||||
except tomllib.TOMLDecodeError:
|
logger.info(f"loader: {identifier} loaded into memory")
|
||||||
logger.error(f"Invalid TOML config: configs/{filename.stem}")
|
except tomllib.TOMLDecodeError:
|
||||||
return configs
|
logger.error(f"Invalid TOML config: configs/{filename.stem}")
|
||||||
|
|
||||||
|
target.configs = configs
|
||||||
|
return target.configs
|
||||||
|
|||||||
@@ -78,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
|
||||||
|
|
||||||
|
|||||||
@@ -85,24 +85,16 @@ 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
|
||||||
]
|
]
|
||||||
elif self.parent.userconfigs:
|
|
||||||
[
|
|
||||||
self.menu_configs_load.add_command(
|
|
||||||
label=name, command=partial(self.load_custom_profile, data)
|
|
||||||
)
|
|
||||||
for name, data in self.parent.userconfigs.items()
|
|
||||||
if name not in self.config_defaults
|
|
||||||
]
|
|
||||||
else:
|
else:
|
||||||
self.menu_configs.entryconfig(0, state="disabled")
|
self.menu_configs.entryconfig(0, state="disabled")
|
||||||
self.menu_configs.add_command(
|
self.menu_configs.add_command(
|
||||||
@@ -232,7 +224,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.destroy()
|
||||||
|
|
||||||
def action_set_voicemeeter(self, cmd, val=True):
|
def action_set_voicemeeter(self, cmd, val=True):
|
||||||
if cmd == "lock":
|
if cmd == "lock":
|
||||||
@@ -315,16 +310,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.menu_configs_load.delete(key)
|
self.logger.warning(f"{type(e).__name__}: {e}")
|
||||||
for key in self.parent.userconfigs.keys()
|
|
||||||
if key not in self.config_defaults
|
|
||||||
]
|
|
||||||
|
|
||||||
[
|
[
|
||||||
self.menu_vban.entryconfig(j, state="disabled")
|
self.menu_vban.entryconfig(j, state="disabled")
|
||||||
@@ -333,24 +325,13 @@ 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
|
|
||||||
]
|
|
||||||
elif self.parent.userconfigs:
|
|
||||||
[
|
|
||||||
self.menu_configs_load.add_command(
|
|
||||||
label=name, command=partial(self.load_custom_profile, data)
|
|
||||||
)
|
|
||||||
for name, data in self.parent.userconfigs.items()
|
|
||||||
if name not in self.config_defaults
|
|
||||||
]
|
|
||||||
else:
|
else:
|
||||||
self.menu_configs.entryconfig(0, state="disabled")
|
self.menu_configs.entryconfig(0, state="disabled")
|
||||||
|
|
||||||
@@ -420,7 +401,7 @@ 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")
|
||||||
|
|||||||
Reference in New Issue
Block a user