18 Commits

Author SHA1 Message Date
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
15 changed files with 414 additions and 214 deletions

View File

@@ -9,6 +9,32 @@ 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)
## [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

View File

@@ -34,16 +34,16 @@ import vmcompact
def main():
# pass the kind_id and the vm object to the app
with voicemeeterlib.api(kind_id) as vm:
app = vmcompact.connect(kind_id, vm)
# choose the kind of Voicemeeter (Local connection)
KIND_ID = "banana"
# pass the KIND_ID and the vm object to the app
with voicemeeterlib.api(KIND_ID) as vm:
app = vmcompact.connect(KIND_ID, vm)
app.mainloop()
if __name__ == "__main__":
# choose the kind of Voicemeeter (Local connection)
kind_id = "banana"
main()
```
@@ -53,9 +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 :).
### kind_id
### KIND_ID
Set the kind of Voicemeeter, kind_id may be:
Set the kind of Voicemeeter, KIND_ID may be:
- `basic`
- `banana`

View File

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

View File

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

118
poetry.lock generated
View File

@@ -1,10 +1,25 @@
# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
[[package]]
name = "black"
version = "22.10.0"
version = "22.12.0"
description = "The uncompromising code formatter."
category = "dev"
optional = false
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]
click = ">=8.0.0"
@@ -21,117 +36,134 @@ uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "click"
version = "8.1.3"
version = "8.1.4"
description = "Composable command line interface toolkit"
category = "dev"
optional = false
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]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.5"
version = "0.4.6"
description = "Cross-platform colored terminal text."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "isort"
version = "5.12.0"
description = "A Python utility / library to sort Python imports."
category = "dev"
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)"]
requirements-deprecated-finder = ["pip-api", "pipreqs"]
pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
plugins = ["setuptools"]
requirements-deprecated-finder = ["pip-api", "pipreqs"]
[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = "*"
python-versions = ">=3.5"
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]]
name = "pathspec"
version = "0.10.1"
version = "0.11.1"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
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 = "platformdirs"
version = "2.5.2"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
version = "3.8.1"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
optional = false
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]
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
docs = ["furo (>=2023.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 (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"]
[[package]]
name = "sv-ttk"
version = "2.4.5"
version = "2.5.5"
description = "A gorgeous theme for Tkinter, based on Windows 11's UI"
category = "main"
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]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
[[package]]
name = "vban-cmd"
version = "2.0.0"
version = "2.4.4"
description = "Python interface for the VBAN RT Packet Service (Sendtext)"
category = "main"
optional = false
python-versions = ">=3.10,<4.0"
files = [
{file = "vban_cmd-2.4.4-py3-none-any.whl", hash = "sha256:f439219a2dc6a45123bb70fc94d2aabfc643f26dffc9206da2228e2520f83e01"},
{file = "vban_cmd-2.4.4.tar.gz", hash = "sha256:31dbf6abb681d57772c9082722b024d0798eff99b4c622bfbb539179852a935d"},
]
[package.dependencies]
tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
[[package]]
name = "voicemeeter-api"
version = "2.0.1"
version = "2.4.4"
description = "A Python wrapper for the Voiceemeter API"
category = "main"
optional = false
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]
tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
[metadata]
lock-version = "1.1"
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "ffb9af7ef7aa87ac08a09293de5e99487155faaa459cd49964ac95589deb69fa"
[metadata.files]
black = []
click = []
colorama = []
isort = []
mypy-extensions = []
pathspec = []
platformdirs = []
sv-ttk = []
tomli = []
vban-cmd = []
voicemeeter-api = []
content-hash = "cde74de8c9de0895555068efea354edf6b51f2332c253f02f6c8f26fe6b4e795"

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "voicemeeter-compact"
version = "1.7.1"
version = "1.9.2"
description = "A Compact Voicemeeter Remote App"
authors = ["onyx-and-iris <code@onyxandiris.online>"]
license = "MIT"
@@ -12,10 +12,10 @@ include = ["vmcompact/img/cat.ico"]
[tool.poetry.dependencies]
python = "^3.10"
sv-ttk = "^2.4.5"
sv-ttk = "^2.5.5"
tomli = { version = "^2.0.1", python = "<3.11" }
voicemeeter-api = "^2.0.1"
vban-cmd = "^2.0.0"
voicemeeter-api = "^2.4.4"
vban-cmd = "^2.4.4"
[tool.poetry.dev-dependencies]
black = { version = "^22.6.0", allow-prereleases = true }

View File

@@ -1,16 +1,21 @@
import logging
import tkinter as tk
from functools import cached_property
from pathlib import Path
from tkinter import ttk
from tkinter import messagebox, ttk
from typing import NamedTuple
import voicemeeterlib
from .builders import MainFrameBuilder
from .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 .menu import Menus
from .subject import Subject
logger = logging.getLogger(__name__)
class App(tk.Tk):
"""App mainframe"""
@@ -34,9 +39,10 @@ class App(tk.Tk):
def __init__(self, vmr):
super().__init__()
self.logger = logger.getChild(self.__class__.__name__)
self._vmr = vmr
self._vmr.event.add(["pdirty", "ldirty"])
self.after(12000 if self._vmr.gui.launched_by_api else 1, self.start_updates)
self._vmr.init_thread()
icon_path = Path(__file__).parent.resolve() / "img" / "cat.ico"
if icon_path.is_file():
@@ -44,7 +50,8 @@ class App(tk.Tk):
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()
if _configuration.config:
vmr.apply_config(_configuration.config)
@@ -54,6 +61,8 @@ class App(tk.Tk):
self.drag_id = ""
self.bind("<Configure>", self.dragging)
self.after(1, self.healthcheck_step)
def __str__(self):
return f"{type(self).__name__}App"
@@ -113,7 +122,7 @@ class App(tk.Tk):
Destroy all top level frames.
"""
self.target.subject.remove(self)
self.target.subject.remove([self.on_pdirty, self.on_ldirty])
self.subject.clear()
[
frame.destroy()
@@ -135,9 +144,50 @@ class App(tk.Tk):
@cached_property
def userconfigs(self):
self._configs = loader(self.kind.name)
self._configs = loader(self.kind.name, self.target)
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.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}

View File

@@ -195,9 +195,9 @@ class NavigationFrameBuilder(AbstractBuilder):
if isinstance(child, ttk.Checkbutton)
]
if _configuration.themes_enabled:
self.navframe.rowconfigure(1, minsize=_configuration.level_height)
self.navframe.rowconfigure(1, minsize=_configuration.channel_height)
else:
self.navframe.rowconfigure(1, minsize=_configuration.level_height + 10)
self.navframe.rowconfigure(1, minsize=_configuration.channel_height + 10)
def teardown(self):
pass
@@ -243,13 +243,19 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
orient="vertical",
variable=self.labelframe.gain,
command=self.labelframe.scale_callback,
length=_configuration.level_height,
length=_configuration.channel_height,
)
self.scale.grid(column=1, row=0)
self.scale.bind("<Double-Button-1>", self.labelframe.reset_gain)
self.scale.bind("<Button-1>", self.labelframe.scale_press)
self.scale.bind("<ButtonRelease-1>", self.labelframe.scale_release)
self.scale.bind("<MouseWheel>", self.labelframe._on_mousewheel)
self.scale.bind(
"<MouseWheel>",
partial(
self.labelframe.pause_updates,
self.labelframe._on_mousewheel,
),
)
def add_gain_label(self):
self.labelframe.gain_label = ttk.Label(
@@ -263,7 +269,7 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
self.button_mute = ttk.Checkbutton(
self.labelframe,
text="MUTE",
command=partial(self.labelframe.toggle_mute, "mute"),
command=partial(self.labelframe.pause_updates, self.labelframe.toggle_mute),
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{self.identifier}Mute{self.index}.TButton'}",
variable=self.labelframe.mute,
)
@@ -283,7 +289,7 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
self.button_on = ttk.Checkbutton(
self.labelframe,
text="ON",
command=self.labelframe.set_on,
command=partial(self.labelframe.pause_updates, self.labelframe.set_on),
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{self.identifier}On{self.index}.TButton'}",
variable=self.labelframe.on,
)
@@ -324,7 +330,7 @@ class ChannelConfigFrameBuilder(AbstractBuilder):
]
self.configframe.grid(sticky=(tk.W))
[
self.configframe.columnconfigure(i, minsize=_configuration.level_width)
self.configframe.columnconfigure(i, minsize=_configuration.channel_width)
for i in range(self.configframe.phys_out + self.configframe.virt_out)
]
@@ -392,7 +398,7 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
from_=0.0,
to=10.0,
orient="horizontal",
length=_configuration.level_width,
length=_configuration.channel_width,
variable=self.configframe.slider_vars[
self.configframe.slider_params.index("comp.knob")
],
@@ -416,7 +422,7 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
from_=0.0,
to=10.0,
orient="horizontal",
length=_configuration.level_width,
length=_configuration.channel_width,
variable=self.configframe.slider_vars[
self.configframe.slider_params.index("gate.knob")
],
@@ -440,7 +446,7 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
from_=-40,
to=12,
orient="horizontal",
length=_configuration.level_width,
length=_configuration.channel_width,
variable=self.configframe.slider_vars[
self.configframe.slider_params.index("limit")
],
@@ -464,7 +470,7 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
from_=0.0,
to=10.0,
orient="horizontal",
length=_configuration.level_width,
length=_configuration.channel_width,
variable=self.configframe.slider_vars[
self.configframe.slider_params.index("audibility")
],
@@ -486,7 +492,9 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
ttk.Checkbutton(
self.configframe,
text=param,
command=partial(self.configframe.toggle_a, param),
command=partial(
self.configframe.pause_updates, self.configframe.toggle_a, param
),
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{param}.TButton'}",
variable=self.configframe.phys_out_params_vars[
self.configframe.phys_out_params.index(param)
@@ -507,7 +515,9 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
ttk.Checkbutton(
self.configframe,
text=param,
command=partial(self.configframe.toggle_b, param),
command=partial(
self.configframe.pause_updates, self.configframe.toggle_b, param
),
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{param}.TButton'}",
variable=self.configframe.virt_out_params_vars[
self.configframe.virt_out_params.index(param)
@@ -528,7 +538,9 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
ttk.Checkbutton(
self.configframe,
text=param,
command=partial(self.configframe.toggle_p, param),
command=partial(
self.configframe.pause_updates, self.configframe.toggle_p, param
),
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{param}.TButton'}",
variable=self.configframe.param_vars[i],
)
@@ -578,10 +590,16 @@ class BusConfigFrameBuilder(ChannelConfigFrameBuilder):
column=0, row=0, columnspan=2, sticky=(tk.W)
)
self.configframe.busmode_button.bind(
"<Button-1>", self.configframe.rotate_bus_modes_right
"<Button-1>",
partial(
self.configframe.pause_updates, self.configframe.rotate_bus_modes_right
),
)
self.configframe.busmode_button.bind(
"<Button-3>", self.configframe.rotate_bus_modes_left
"<Button-3>",
partial(
self.configframe.pause_updates, self.configframe.rotate_bus_modes_left
),
)
def create_param_buttons(self):
@@ -589,7 +607,9 @@ class BusConfigFrameBuilder(ChannelConfigFrameBuilder):
ttk.Checkbutton(
self.configframe,
text=param,
command=partial(self.configframe.toggle_p, param),
command=partial(
self.configframe.pause_updates, self.configframe.toggle_p, param
),
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{param}.TButton'}",
variable=self.configframe.param_vars[i],
)

View File

@@ -88,17 +88,27 @@ class ChannelLabelFrame(ttk.LabelFrame):
self.parent.target.event.add("ldirty")
self.after(500, self.resume_updates)
def pause_updates(self, func, *args):
"""function wrapper, adds a 50ms delay on updates"""
_base_values.run_update = False
func(*args)
self.after(50, self.resume_updates)
def resume_updates(self):
_base_values.run_update = True
def _on_mousewheel(self, event):
_base_values.run_update = False
self.gain.set(
self.gain.get()
+ (
_configuration.mwscroll_step
if event.delta > 0
else -_configuration.mwscroll_step
round(
self.gain.get()
+ (
_configuration.mwscroll_step
if event.delta > 0
else -_configuration.mwscroll_step
),
1,
)
)
if self.gain.get() > 12:
@@ -106,7 +116,7 @@ class ChannelLabelFrame(ttk.LabelFrame):
elif self.gain.get() < -60:
self.gain.set(-60)
self.setter("gain", self.gain.get())
self.after(1, self.resume_updates)
self.gainlabel.set(round(self.gain.get(), 1))
def open_config(self):
if self.conf.get():
@@ -141,20 +151,21 @@ class ChannelLabelFrame(ttk.LabelFrame):
def sync_labels(self):
"""sync labelframes according to label text"""
retval = self.getter("label")
self.parent.label_cache[self.id].insert(self.index, retval)
if len(retval) > 10:
retval = f"{retval[:8]}.."
if not retval:
self.parent.columnconfigure(self.index, minsize=0)
self.parent.parent.subject.remove(self)
self.grid_remove()
else:
self.parent.parent.subject.add(self)
self.grid()
self.configure(text=retval)
if self.parent.label_cache[self.id][self.index] != retval:
self.parent.label_cache[self.id][self.index] = retval
if len(retval) > 10:
retval = f"{retval[:8]}.."
if not retval:
self.parent.columnconfigure(self.index, minsize=0)
self.parent.parent.subject.remove(self)
self.grid_remove()
else:
self.parent.parent.subject.add(self)
self.grid()
self.configure(text=retval)
def grid_configure(self):
self.grid(sticky=(tk.N, tk.S))
self.grid(padx=_configuration.channel_xpadding, sticky=(tk.N, tk.S))
[
child.grid_configure(padx=1, pady=1, sticky=(tk.W, tk.E))
for child in self.winfo_children()
@@ -218,15 +229,18 @@ class Bus(ChannelLabelFrame):
class ChannelFrame(ttk.Frame):
label_cache = {"strip": list(), "bus": list()}
def init(self, parent, id):
super().__init__(parent)
self.parent = parent
self.id = id
self.phys_in, self.virt_in = parent.kind.ins
self.phys_out, self.virt_out = parent.kind.outs
self.label_cache = {
"strip": [""] * (self.phys_in + self.virt_in),
"bus": [""] * (self.phys_out + self.virt_out),
}
self.parent.subject.add(self)
self.update_labels()
@property
def target(self):
@@ -248,17 +262,17 @@ class ChannelFrame(ttk.Frame):
if isinstance(frame, ttk.LabelFrame)
)
def update_labels(self):
for labelframe in self.labelframes:
labelframe.on_update("labelframe")
def on_update(self, subject):
if subject == "pdirty":
target = getattr(self.target, self.id)
num = getattr(self.parent.kind, f"num_{self.id}")
if self.label_cache[self.id] != [target[i].label for i in range(num)]:
for labelframe in self.labelframes:
labelframe.on_update("labelframe")
self.update_labels()
def grid_configure(self):
[
self.columnconfigure(i, minsize=_configuration.level_width)
self.columnconfigure(i, minsize=_configuration.channel_width)
for i, _ in enumerate(self.labelframes)
]
[self.rowconfigure(0, minsize=100) for i, _ in enumerate(self.labelframes)]
@@ -270,7 +284,7 @@ class ChannelFrame(ttk.Frame):
setattr(self.parent, f"{self.identifier}_frame", None)
def _make_channelframe(parent, id):
def _make_channelframe(parent, identifier):
"""
Creates a Channel Frame class of type strip or bus
"""
@@ -278,29 +292,33 @@ def _make_channelframe(parent, id):
phys_in, virt_in = parent.kind.ins
phys_out, virt_out = parent.kind.outs
def init_labels(self, id):
def init_labels(self):
"""
Grids each labelframe, grid_removes any without a label
"""
for i, labelframe in enumerate(
getattr(self, "strips" if id == "strip" else "buses")
getattr(self, "strips" if identifier == "strip" else "buses")
):
labelframe.grid(row=0, column=i)
if not labelframe.target.label:
label = labelframe.target.label
if not label:
self.columnconfigure(i, minsize=0)
labelframe.grid_remove()
self.label_cache[identifier][i] = label
def init_strip(self, *args, **kwargs):
self.init(parent, id)
self.strips = tuple(Strip(self, i, id) for i in range(phys_in + virt_in))
self.init(parent, identifier)
self.strips = tuple(
Strip(self, i, identifier) for i in range(phys_in + virt_in)
)
self.grid(row=0, column=0, sticky=(tk.W))
self.grid_configure()
init_labels(self, id)
init_labels(self)
def init_bus(self, *args, **kwargs):
self.init(parent, id)
self.buses = tuple(Bus(self, i, id) for i in range(phys_out + virt_out))
self.init(parent, identifier)
self.buses = tuple(Bus(self, i, identifier) for i in range(phys_out + virt_out))
if _configuration.extended:
if _configuration.extends_horizontal:
self.grid(row=0, column=2, sticky=(tk.W))
@@ -309,11 +327,11 @@ def _make_channelframe(parent, id):
else:
self.grid(row=0, column=0)
self.grid_configure()
init_labels(self, id)
init_labels(self)
if id == "strip":
if identifier == "strip":
CHANNELFRAME_cls = type(
f"ChannelFrame{id.capitalize()}",
f"ChannelFrame{identifier.capitalize()}",
(ChannelFrame,),
{
"__init__": init_strip,
@@ -321,7 +339,7 @@ def _make_channelframe(parent, id):
)
else:
CHANNELFRAME_cls = type(
f"ChannelFrame{id.capitalize()}",
f"ChannelFrame{identifier.capitalize()}",
(ChannelFrame,),
{
"__init__": init_bus,

View File

@@ -68,6 +68,14 @@ class Config(ttk.Frame):
self.parent.target.event.add("ldirty")
self.after(350, self.resume_updates)
def pause_updates(self, func, *args):
"""function wrapper, adds a 50ms delay on updates"""
_base_values.run_update = False
func(*args)
self.after(50, self.resume_updates)
def resume_updates(self):
_base_values.run_update = True

View File

@@ -10,28 +10,32 @@ logger = logging.getLogger(__name__)
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
break
def get_configpath():
configpaths = [
Path.cwd() / "configs",
Path.home() / ".config" / "vm-compact" / "configs",
Path.home() / "Documents" / "Voicemeeter" / "configs",
]
for configpath in configpaths:
if configpath.exists():
return configpath
if configpath := get_configpath():
filepaths = list(configpath.glob("*.toml"))
if any(f.stem in ("app", "vban") for f in filepaths):
configs = {}
for filepath in filepaths:
filename = filepath.with_suffix("").stem
if filename in ("app", "vban"):
try:
with open(filepath, "rb") as f:
configs[filename] = tomllib.load(f)
logger.info(f"configuration: {filename} loaded into memory")
except tomllib.TOMLDecodeError:
logger.error(f"Invalid TOML config: configs/{filename.stem}")
configuration |= configs
_defaults = {
"configs": {
@@ -48,6 +52,7 @@ _defaults = {
"channel": {
"width": 80,
"height": 130,
"xpadding": 3,
},
"mwscroll_step": {
"size": 3,
@@ -55,10 +60,16 @@ _defaults = {
"submixes": {
"default": 0,
},
"navigation": {"show": True},
}
if "app" in configuration:
configuration["app"] = _defaults | configuration["app"]
for key in _defaults:
if key in configuration["app"]:
configuration["app"][key] = _defaults[key] | configuration["app"][key]
else:
configuration["app"][key] = _defaults[key]
else:
configuration["app"] = _defaults
@@ -68,17 +79,20 @@ def get_configuration(key):
return configuration[key]
def loader(kind_id):
configs = {}
userconfigpath = Path.home() / ".config" / "vm-compact" / "configs" / 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}")
return configs
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
submixes: int = configuration["submixes"]["default"]
# width of a single labelframe
level_width: int = configuration["channel"]["width"]
# height of a single labelframe
level_height: int = configuration["channel"]["height"]
# width of a single channel labelframe
channel_width: int = configuration["channel"]["width"]
# height of a single channel labelframe
channel_height: int = configuration["channel"]["height"]
# xpadding for a single channel labelframe
channel_xpadding: int = configuration["channel"]["xpadding"]
# do we grid the navigation frame?
navigation_show: bool = configuration["navigation"]["show"]
@property
def config(self):
@@ -46,7 +51,7 @@ class Configurations(metaclass=SingletonMeta):
@dataclass
class BaseValues(metaclass=SingletonMeta):
# pause updates after releasing scale
run_update: bool = True
run_update: bool = False
# are we dragging main window with mouse 1
dragging: bool = False
# a vban connection established

View File

@@ -78,6 +78,14 @@ class GainLayer(ttk.LabelFrame):
self.parent.target.event.add("ldirty")
self.after(500, self.resume_updates)
def pause_updates(self, func, *args):
"""function wrapper, adds a 50ms delay on updates"""
_base_values.run_update = False
func(*args)
self.after(50, self.resume_updates)
def resume_updates(self):
_base_values.run_update = True
@@ -166,6 +174,7 @@ class GainLayer(ttk.LabelFrame):
)
def grid_configure(self):
self.grid(padx=_configuration.channel_xpadding, sticky=(tk.N, tk.S))
[
child.grid_configure(padx=1, pady=1, sticky=(tk.N, tk.S, tk.W, tk.E))
for child in self.winfo_children()
@@ -253,11 +262,11 @@ class SubMixFrame(ttk.Frame):
def grid_configure(self):
[
self.columnconfigure(i, minsize=_configuration.level_width)
self.columnconfigure(i, minsize=_configuration.channel_width)
for i, _ in enumerate(self.labelframes)
]
[
self.rowconfigure(0, minsize=_configuration.level_height)
self.rowconfigure(0, minsize=_configuration.channel_height)
for i, _ in enumerate(self.labelframes)
]

View File

@@ -24,6 +24,8 @@ class Menus(tk.Menu):
self._is_topmost = tk.BooleanVar()
self._lock = tk.BooleanVar()
self._unlock = tk.BooleanVar()
self._navigation_show = tk.BooleanVar(value=_configuration.navigation_show)
self._navigation_hide = tk.BooleanVar(value=not _configuration.navigation_show)
self._selected_bus = list(tk.BooleanVar() for _ in range(8))
# voicemeeter menu
@@ -83,24 +85,16 @@ class Menus(tk.Menu):
self.menu_configs_load = tk.Menu(self.menu_configs, tearoff=0)
self.menu_configs.add_cascade(menu=self.menu_configs_load, label="Load config")
self.config_defaults = {"reset"}
if len(self.target.configs) > len(self.config_defaults) and all(
key in self.target.configs for key in self.config_defaults
if len(self.parent.userconfigs) > len(self.config_defaults) and all(
key in self.parent.userconfigs for key in self.config_defaults
):
[
self.menu_configs_load.add_command(
label=profile, command=partial(self.load_profile, profile)
)
for profile in self.target.configs.keys()
for profile in self.parent.userconfigs.keys()
if profile not in self.config_defaults
]
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:
self.menu_configs.entryconfig(0, state="disabled")
self.menu_configs.add_command(
@@ -162,6 +156,23 @@ class Menus(tk.Menu):
)
if not _configuration.themes_enabled:
self.menu_layout.entryconfig(2, state="disabled")
# layout/navigation
self.menu_navigation = tk.Menu(self.menu_layout, tearoff=0)
self.menu_layout.add_cascade(menu=self.menu_navigation, label="Navigation")
self.menu_navigation.add_checkbutton(
label="show",
onvalue=1,
offvalue=0,
variable=self._navigation_show,
command=partial(self.toggle_navigation, "show"),
)
self.menu_navigation.add_checkbutton(
label="hide",
onvalue=1,
offvalue=0,
variable=self._navigation_hide,
command=partial(self.toggle_navigation, "hide"),
)
# vban connect menu
self.menu_vban = tk.Menu(self, tearoff=0)
@@ -213,7 +224,10 @@ class Menus(tk.Menu):
]
def action_invoke_voicemeeter(self, cmd):
getattr(self.target.command, cmd)()
if fn := getattr(self.target.command, cmd):
fn()
if cmd == "shutdown":
self.parent.on_close_window()
def action_set_voicemeeter(self, cmd, val=True):
if cmd == "lock":
@@ -296,16 +310,13 @@ class Menus(tk.Menu):
def menu_teardown(self, i):
# remove config load menus
[
self.menu_configs_load.delete(key)
for key in self.target.configs.keys()
if key not in self.config_defaults
]
[
self.menu_configs_load.delete(key)
for key in self.parent.userconfigs.keys()
if key not in self.config_defaults
]
if len(self.parent.userconfigs) > len(self.config_defaults):
for profile in self.parent.userconfigs:
if profile not in self.config_defaults:
try:
self.menu_configs_load.delete(profile)
except tk._tkinter.tclError as e:
self.logger.warning(f"{type(e).__name__}: {e}")
[
self.menu_vban.entryconfig(j, state="disabled")
@@ -314,27 +325,28 @@ class Menus(tk.Menu):
]
def menu_setup(self):
if len(self.target.configs) > len(self.config_defaults) and all(
key in self.target.configs for key in self.config_defaults
):
[
self.menu_configs_load.add_command(
label=profile, command=partial(self.load_profile, profile)
)
for profile in self.target.configs.keys()
if profile not in self.config_defaults
]
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
]
if len(self.parent.userconfigs) > len(self.config_defaults):
for profile in self.parent.userconfigs:
if profile not in self.config_defaults:
self.menu_configs_load.add_command(
label=profile, command=partial(self.load_profile, profile)
)
self.menu_configs.entryconfig(0, state="normal")
else:
self.menu_configs.entryconfig(0, state="disabled")
def toggle_navigation(self, cmd=None):
if cmd == "show":
self.logger.debug("show navframe")
self.parent.nav_frame.grid()
self._navigation_show.set(True)
self._navigation_hide.set(not self._navigation_show.get())
else:
self.logger.debug("hide navframe")
self.parent.nav_frame.grid_remove()
self._navigation_hide.set(True)
self._navigation_show.set(not self._navigation_hide.get())
def vban_connect(self, i):
opts = {}
opts |= self.vban_config[f"connection-{i+1}"]
@@ -389,7 +401,7 @@ class Menus(tk.Menu):
self.vban.logout()
# build new app frames according to a kind
kind = kind_get(self.vmr.type)
self.parent.build_app(kind, None)
self.parent.build_app(kind)
target_menu = getattr(self, f"menu_vban_{i+1}")
target_menu.entryconfig(0, state="normal")
target_menu.entryconfig(1, state="disabled")

View File

@@ -15,6 +15,8 @@ class Navigation(ttk.Frame):
self.parent = parent
self.logger = logger.getChild(self.__class__.__name__)
self.grid(row=0, column=3, padx=(0, 2), pady=(5, 5), sticky=(tk.W, tk.E))
if not _configuration.navigation_show:
self.grid_remove()
self.styletable = self.parent.styletable
self.builder = builders.NavigationFrameBuilder(self)