22 Commits

Author SHA1 Message Date
e4068277f7 ensure we don't attempt to delete a menu key twice
patch bump
2023-07-07 03:37:06 +01:00
674999a461 remove callbacks instead
patch bump
2023-06-30 04:27:10 +01:00
e5975f0772 fixes bug where old configs may not have new keys
patch bump
2023-06-29 19:13:06 +01:00
59d2a95ec4 1.8.0 section added to changelog
minor bump
2023-06-29 18:24:54 +01:00
4bae1e1d15 set greace period if gui was launched by the api 2023-06-29 18:14:38 +01:00
1e3751b19f channel_xpadding and navigation_show added to data.
app.toml example now includes channel padding and nav show

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

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

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

2
.gitignore vendored
View File

@@ -131,3 +131,5 @@ dmypy.json
# Pyre type checker
.pyre/
.vscode/

View File

@@ -7,7 +7,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- [ ] Add support for forest theme (should be coming soon)
- [ ] Add support for forest theme (if rbende adds it to pypi)
## [1.8.0] - 2023-06-29
### Added
- Ability to toggle the navigation frame. This may also be set in app.toml, check example config.
### Changed
- xpadding added to channel labelframes. This may also be configured through app.toml.
- During startup of the app there is now a twelve second grace period before parameter updates begin if the GUI was not previously launched. This is aimed at removing the stutter (due to VM engine startup) on initial launch. Be mindful of this if changing settings on the base Voicemeeter app. After the grace period all updates continue as normal.
- dependency updates:
- sv_ttk updated to v2.5.1.
- voicemeeter-api updated to v2.0.2.
## [1.7.0] - 2023-06-26
### Changed
- There are changes to how some parameters must be set in user toml configs.
- use `comp.knob` to set a strip comp slider.
- use `gate.knob` to set a strip gate slider.
- use `eq.on` to set a bus eq.on button.
- use `eq.ab` to set a bus eq.ab button.
Check example configs.
- `configs` directory may now be located in one of the following locations:
- \<current working directory>/configs/
- \<user home directory>/.configs/vm-compact/configs/
- \<user home directory>/Documents/Voicemeeter/configs/
- dependency updates:
- sv_ttk updated to v2.4.5.
- voicemeeter-api updated to v2.0.1.
- vban-cmd updated to v2.0.0.
### Fixed
- A number of changes that reduce the amount of api calls being made.
## [1.6.0] - 2022-09-29

View File

@@ -13,9 +13,13 @@ extends_horizontal=true
[channel]
width = 80
height = 130
xpadding = 2
# size of a single mouse wheel scroll step
[mwscroll_step]
size = 3
# default submix bus
[submixes]
default = 0
# show the navigation frame?
[navigation]
show = true

View File

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

View File

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

51
poetry.lock generated
View File

@@ -1,10 +1,10 @@
[[package]]
name = "black"
version = "22.8.0"
version = "22.12.0"
description = "The uncompromising code formatter."
category = "dev"
optional = false
python-versions = ">=3.6.2"
python-versions = ">=3.7"
[package.dependencies]
click = ">=8.0.0"
@@ -32,23 +32,37 @@ 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"
[[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"
[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"]
[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
category = "dev"
optional = false
python-versions = "*"
python-versions = ">=3.5"
[[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
@@ -56,23 +70,23 @@ python-versions = ">=3.7"
[[package]]
name = "platformdirs"
version = "2.5.2"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
version = "3.8.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.7"
[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-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx (>=7.0.1)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest (>=7.3.1)"]
[[package]]
name = "sv-ttk"
version = "2.0"
description = "A gorgeous theme for Tkinter that looks like Windows 11"
version = "2.5.1"
description = "A gorgeous theme for Tkinter, based on Windows 11's UI"
category = "main"
optional = false
python-versions = ">=3.4"
python-versions = ">=3.7"
[[package]]
name = "tomli"
@@ -84,7 +98,7 @@ python-versions = ">=3.7"
[[package]]
name = "vban-cmd"
version = "1.5.4"
version = "2.0.0"
description = "Python interface for the VBAN RT Packet Service (Sendtext)"
category = "main"
optional = false
@@ -95,7 +109,7 @@ tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
[[package]]
name = "voicemeeter-api"
version = "0.8.2"
version = "2.0.2"
description = "A Python wrapper for the Voiceemeter API"
category = "main"
optional = false
@@ -107,12 +121,13 @@ tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
[metadata]
lock-version = "1.1"
python-versions = "^3.10"
content-hash = "5c59ce43f535924db8cfddd40b9b97ff53861fa43f340396d06a69a0ca80451f"
content-hash = "3a59de3a76e4c0ca11c0166750fa1af7d7c887750f855b48c45359068ef04798"
[metadata.files]
black = []
click = []
colorama = []
isort = []
mypy-extensions = []
pathspec = []
platformdirs = []

View File

@@ -1,26 +1,25 @@
[tool.poetry]
name = "voicemeeter-compact"
version = "1.6.4"
version = "1.8.3"
description = "A Compact Voicemeeter Remote App"
authors = ["onyx-and-iris <code@onyxandiris.online>"]
license = "MIT"
readme = "README.md"
repository = "https://github.com/onyx-and-iris/voicemeeter-compact"
packages = [
{ include = "vmcompact" },
]
packages = [{ include = "vmcompact" }]
include = ["vmcompact/img/cat.ico"]
[tool.poetry.dependencies]
python = "^3.10"
sv-ttk = "^2.0"
sv-ttk = "^2.5.1"
tomli = { version = "^2.0.1", python = "<3.11" }
voicemeeter-api = "^0.8.2"
vban-cmd = "^1.5.4"
voicemeeter-api = "^2.0.2"
vban-cmd = "^2.0.0"
[tool.poetry.dev-dependencies]
black = { version = "^22.6.0", allow-prereleases = true }
isort = "^5.12.0"
[build-system]
requires = ["poetry-core>=1.0.0"]

View File

@@ -1,14 +1,19 @@
import logging
import tkinter as tk
from functools import cached_property
from pathlib import Path
from tkinter import ttk
from typing import NamedTuple
from .builders import MainFrameBuilder
from .configurations import loader
from .data import _base_values, _configuration, _kinds_all
from .errors import VMCompactErrors
from .errors import VMCompactError
from .menu import Menus
from .subject import Subject
logger = logging.getLogger(__name__)
class App(tk.Tk):
"""App mainframe"""
@@ -32,16 +37,17 @@ class App(tk.Tk):
def __init__(self, vmr):
super().__init__()
self.logger = logger.getChild(self.__class__.__name__)
self._vmr = vmr
self._vmr.event.add("ldirty")
self._vmr.event.remove("mdirty")
self._vmr.event.remove("midi")
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():
self.iconbitmap(str(icon_path))
self.minsize(275, False)
self.subject = Subject()
self._configs = None
self["menu"] = Menus(self, vmr)
self.styletable = ttk.Style()
if _configuration.config:
@@ -52,6 +58,15 @@ class App(tk.Tk):
self.drag_id = ""
self.bind("<Configure>", self.dragging)
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 __str__(self):
return f"{type(self).__name__}App"
@property
def target(self):
"""returns the current interface"""
@@ -76,8 +91,8 @@ class App(tk.Tk):
if kind:
self.kind = kind
# register app as observer
self.target.subject.add(self)
# register event callbacks
self.target.subject.add([self.on_pdirty, self.on_ldirty])
self.bus_frame = None
self.submix_frame = None
@@ -92,12 +107,12 @@ class App(tk.Tk):
if self.kind.name == "potato":
self.builder.create_banner()
def on_update(self, subject):
"""called whenever notified of update"""
if subject == "pdirty" and _base_values.run_update:
def on_pdirty(self):
if _base_values.run_update:
self.after(1, self.subject.notify, "pdirty")
elif subject == "ldirty" and not _base_values.dragging:
def on_ldirty(self):
if not _base_values.dragging:
self.after(1, self.subject.notify, "ldirty")
def _destroy_top_level_frames(self):
@@ -108,7 +123,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()
@@ -128,6 +143,11 @@ class App(tk.Tk):
self.drag_id = ""
_base_values.dragging = False
@cached_property
def userconfigs(self):
self._configs = loader(self.kind.name)
return self._configs
_apps = {kind.name: App.make(kind) for kind in _kinds_all}
@@ -138,5 +158,5 @@ def connect(kind_id: str, vmr) -> App:
try:
VMMIN_cls = _apps[kind_id]
except KeyError:
raise VMCompactErrors(f"Invalid kind: {kind_id}")
raise VMCompactError(f"Invalid kind: {kind_id}")
return VMMIN_cls(vmr)

View File

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

View File

@@ -12,6 +12,8 @@ from .config import BusConfig, StripConfig
from .data import _base_values, _configuration
from .navigation import Navigation
logger = logging.getLogger(__name__)
class AbstractBuilder(abc.ABC):
@abc.abstractmethod
@@ -28,11 +30,10 @@ class AbstractBuilder(abc.ABC):
class MainFrameBuilder(AbstractBuilder):
"""Responsible for building the frames that sit directly on the mainframe"""
logger = logging.getLogger("builders.mainframebuilder")
def __init__(self, app):
self.kind = app.kind
self.app = app
self.logger = logger.getChild(self.__class__.__name__)
def setup(self):
self.app.title(
@@ -194,9 +195,9 @@ class NavigationFrameBuilder(AbstractBuilder):
if isinstance(child, ttk.Checkbutton)
]
if _configuration.themes_enabled:
self.navframe.rowconfigure(1, minsize=_configuration.level_height)
self.navframe.rowconfigure(1, minsize=_configuration.channel_height)
else:
self.navframe.rowconfigure(1, minsize=_configuration.level_height + 10)
self.navframe.rowconfigure(1, minsize=_configuration.channel_height + 10)
def teardown(self):
pass
@@ -242,7 +243,7 @@ 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)
@@ -323,7 +324,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)
]
@@ -336,7 +337,7 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
self.configframe.slider_params = ("audibility",)
self.configframe.slider_vars = (tk.DoubleVar(),)
else:
self.configframe.slider_params = ("comp", "gate", "limit")
self.configframe.slider_params = ("comp.knob", "gate.knob", "limit")
self.configframe.slider_vars = [
tk.DoubleVar() for _ in self.configframe.slider_params
]
@@ -391,18 +392,18 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
from_=0.0,
to=10.0,
orient="horizontal",
length=_configuration.level_width,
length=_configuration.channel_width,
variable=self.configframe.slider_vars[
self.configframe.slider_params.index("comp")
self.configframe.slider_params.index("comp.knob")
],
command=partial(self.configframe.scale_callback, "comp"),
command=partial(self.configframe.scale_callback, "comp.knob"),
)
comp_scale.bind(
"<Double-Button-1>", partial(self.configframe.reset_scale, "comp", 0)
"<Double-Button-1>", partial(self.configframe.reset_scale, "comp.knob", 0)
)
comp_scale.bind("<Button-1>", self.configframe.scale_press)
comp_scale.bind("<ButtonRelease-1>", self.configframe.scale_release)
comp_scale.bind("<Enter>", partial(self.configframe.scale_enter, "comp"))
comp_scale.bind("<Enter>", partial(self.configframe.scale_enter, "comp.knob"))
comp_scale.bind("<Leave>", self.configframe.scale_leave)
comp_label.grid(column=0, row=0)
@@ -415,18 +416,18 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
from_=0.0,
to=10.0,
orient="horizontal",
length=_configuration.level_width,
length=_configuration.channel_width,
variable=self.configframe.slider_vars[
self.configframe.slider_params.index("gate")
self.configframe.slider_params.index("gate.knob")
],
command=partial(self.configframe.scale_callback, "gate"),
command=partial(self.configframe.scale_callback, "gate.knob"),
)
gate_scale.bind(
"<Double-Button-1>", partial(self.configframe.reset_scale, "gate", 0)
"<Double-Button-1>", partial(self.configframe.reset_scale, "gate.knob", 0)
)
gate_scale.bind("<Button-1>", self.configframe.scale_press)
gate_scale.bind("<ButtonRelease-1>", self.configframe.scale_release)
gate_scale.bind("<Enter>", partial(self.configframe.scale_enter, "gate"))
gate_scale.bind("<Enter>", partial(self.configframe.scale_enter, "gate.knob"))
gate_scale.bind("<Leave>", self.configframe.scale_leave)
gate_label.grid(column=2, row=0)
@@ -439,7 +440,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")
],
@@ -463,7 +464,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")
],
@@ -563,7 +564,7 @@ class BusConfigFrameBuilder(ChannelConfigFrameBuilder):
}
self.configframe.bus_modes = list(self.configframe.bus_mode_map.keys())
# fmt: on
self.configframe.params = ("mono", "eq", "eq_ab")
self.configframe.params = ("mono", "eq.on", "eq.ab")
self.configframe.param_vars = [tk.BooleanVar() for _ in self.configframe.params]
self.configframe.bus_mode_label_text = tk.StringVar(
value=self.configframe.bus_mode_map[self.configframe.current_bus_mode()]

View File

@@ -1,10 +1,12 @@
import logging
import tkinter as tk
from math import log
from tkinter import ttk
from . import builders
from .data import _base_values, _configuration
logger = logging.getLogger(__name__)
class ChannelLabelFrame(ttk.LabelFrame):
"""Base class for a single channel"""
@@ -14,6 +16,7 @@ class ChannelLabelFrame(ttk.LabelFrame):
self.parent = parent
self.index = index
self.id = id
self.logger = logger.getChild(self.__class__.__name__)
self.styletable = self.parent.parent.styletable
self.builder = builders.ChannelLabelFrameBuilder(self, index, id)
@@ -40,18 +43,21 @@ class ChannelLabelFrame(ttk.LabelFrame):
return self.parent.target
def getter(self, param):
if hasattr(self.target, param):
try:
return getattr(self.target, param)
except AttributeError as e:
self.logger(f"{type(e).__name__}: {e}")
def setter(self, param, value):
if hasattr(self.target, param):
if param in dir(self.target): # avoid calling getattr (with hasattr)
setattr(self.target, param, value)
def scale_callback(self, *args):
"""callback function for scale widget"""
self.setter("gain", self.gain.get())
self.gainlabel.set(round(self.gain.get(), 1))
val = round(self.gain.get(), 1)
self.setter("gain", val)
self.gainlabel.set(val)
def toggle_mute(self, *args):
self.target.mute = self.mute.get()
@@ -148,7 +154,7 @@ class ChannelLabelFrame(ttk.LabelFrame):
self.configure(text=retval)
def grid_configure(self):
self.grid(sticky=(tk.N, tk.S))
self.grid(padx=_configuration.channel_xpadding, sticky=(tk.N, tk.S))
[
child.grid_configure(padx=1, pady=1, sticky=(tk.W, tk.E))
for child in self.winfo_children()
@@ -252,7 +258,7 @@ class ChannelFrame(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=100) for i, _ in enumerate(self.labelframes)]

View File

@@ -1,10 +1,11 @@
import tkinter as tk
from functools import partial
import logging
from tkinter import ttk
from . import builders
from .data import _base_values, _configuration
logger = logging.getLogger(__name__)
class Config(ttk.Frame):
def __init__(self, parent, index, _id):
@@ -12,6 +13,7 @@ class Config(ttk.Frame):
self.parent = parent
self.index = index
self.id = _id
self.logger = logger.getChild(self.__class__.__name__)
self.styletable = parent.styletable
self.phys_in, self.virt_in = parent.kind.ins
self.phys_out, self.virt_out = parent.kind.outs
@@ -29,12 +31,26 @@ class Config(ttk.Frame):
return self.parent.target
def getter(self, param):
if hasattr(self.target, param):
return getattr(self.target, param)
param = param.split(".")
try:
if len(param) == 2:
target = getattr(self.target, param[0])
return getattr(target, param[1])
else:
return getattr(self.target, param[0])
except AttributeError as e:
self.logger.error(f"{type(e).__name__}: {e}")
def setter(self, param, value):
if hasattr(self.target, param):
setattr(self.target, param, value)
param = param.split(".")
try:
if len(param) == 2:
target = getattr(self.target, param[0])
setattr(target, param[1], value)
else:
setattr(self.target, param[0], value)
except AttributeError as e:
self.logger(f"{type(e).__name__}: {e}")
def scale_press(self, *args):
self.after(1, self.remove_events)
@@ -66,7 +82,7 @@ class Config(ttk.Frame):
"""callback function for scale widget"""
val = self.slider_vars[self.slider_params.index(param)].get()
self.setter(param, val)
self.setter(param, round(val, 1))
self.parent.nav_frame.info_text.set(round(val, 1))
def reset_scale(self, param, val, *args):
@@ -98,6 +114,7 @@ class StripConfig(Config):
self.make_row_2()
self.builder.grid_configure()
self.parent.target.clear_dirty()
self.sync()
@property
@@ -155,6 +172,12 @@ class StripConfig(Config):
self.param_vars[i].set(self.getter(param))
for i, param in enumerate(self.params)
]
if not _base_values.vban_connected: # slider vars not defined in RT Packet
[
self.slider_vars[i].set(self.getter(param))
for i, param in enumerate(self.slider_params)
if self.index < self.phys_in
]
if not _configuration.themes_enabled:
[
@@ -193,6 +216,7 @@ class BusConfig(Config):
self.make_row_1()
self.builder.grid_configure()
self.parent.target.clear_dirty()
self.sync()
@property

View File

@@ -6,26 +6,32 @@ try:
except ModuleNotFoundError:
import tomli as tomllib
LOGGER = logging.getLogger("configurations")
logger = logging.getLogger(__name__)
configuration = {}
config_path = [Path.cwd() / "configs"]
for path in config_path:
if path.is_dir():
filenames = list(path.glob("*.toml"))
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 filename in filenames:
name = filename.with_suffix("").stem
for filepath in filepaths:
filename = filepath.with_suffix("").stem
if filename in ("app", "vban"):
try:
with open(filename, "rb") as f:
configs[name] = tomllib.load(f)
with open(filepath, "rb") as f:
configs[filename] = tomllib.load(f)
logger.info(f"configuration: {filename} loaded into memory")
except tomllib.TOMLDecodeError:
print(f"Invalid TOML config: configs/{filename.stem}")
logger.error(f"Invalid TOML config: configs/{filename.stem}")
for name, cfg in configs.items():
LOGGER.info(f"Loaded configuration configs/{name}")
configuration[name] = cfg
configuration |= configs
break
_defaults = {
"configs": {
@@ -42,6 +48,7 @@ _defaults = {
"channel": {
"width": 80,
"height": 130,
"xpadding": 3,
},
"mwscroll_step": {
"size": 3,
@@ -49,10 +56,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
@@ -60,3 +73,19 @@ else:
def get_configuration(key):
if key in configuration:
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

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

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

View File

@@ -1,5 +1,4 @@
import tkinter as tk
from math import log
from tkinter import ttk
from . import builders
@@ -42,11 +41,13 @@ class GainLayer(ttk.LabelFrame):
return "gainlayer"
def getter(self, param):
if hasattr(self.target, param):
try:
return getattr(self.target, param)
except AttributeError as e:
self.logger(f"{type(e).__name__}: {e}")
def setter(self, param, value):
if hasattr(self.target, param):
if param in dir(self.target): # avoid calling getattr (with hasattr)
setattr(self.target, param, value)
def reset_gain(self, *args):
@@ -57,8 +58,9 @@ class GainLayer(ttk.LabelFrame):
def scale_callback(self, *args):
"""callback function for scale widget"""
self.setter("gain", self.gain.get())
self.gainlabel.set(round(self.gain.get(), 1))
val = round(self.gain.get(), 1)
self.setter("gain", val)
self.gainlabel.set(val)
def scale_press(self, *args):
self.after(1, self.remove_events)
@@ -157,12 +159,14 @@ class GainLayer(ttk.LabelFrame):
self.level.set(
(
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()
)
)
def grid_configure(self):
self.grid(padx=_configuration.channel_xpadding, sticky=(tk.N, tk.S))
[
child.grid_configure(padx=1, pady=1, sticky=(tk.N, tk.S, tk.W, tk.E))
for child in self.winfo_children()
@@ -250,11 +254,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

@@ -2,27 +2,30 @@ import logging
import tkinter as tk
import webbrowser
from functools import partial
from tkinter import messagebox, ttk
from tkinter import messagebox
import sv_ttk
import vban_cmd
from vban_cmd.error import VBANCMDError
from vban_cmd.error import VBANCMDConnectionError
from .data import _base_values, _configuration, get_configuration, kind_get
logger = logging.getLogger(__name__)
class Menus(tk.Menu):
logger = logging.getLogger("menu.menus")
def __init__(self, parent, vmr):
super().__init__()
self.parent = parent
self.vmr = vmr
self.logger = logger.getChild(self.__class__.__name__)
self.vban_config = get_configuration("vban")
self.app_config = get_configuration("app")
self._is_topmost = tk.BooleanVar()
self._lock = tk.BooleanVar()
self._unlock = tk.BooleanVar()
self._navigation_show = tk.BooleanVar(value=_configuration.navigation_show)
self._navigation_hide = tk.BooleanVar(value=not _configuration.navigation_show)
self._selected_bus = list(tk.BooleanVar() for _ in range(8))
# voicemeeter menu
@@ -92,6 +95,14 @@ class Menus(tk.Menu):
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:
self.menu_configs.entryconfig(0, state="disabled")
self.menu_configs.add_command(
@@ -153,6 +164,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)
@@ -212,15 +240,24 @@ class Menus(tk.Menu):
self._unlock.set(not self._lock.get())
setattr(self.target.command, cmd, val)
def load_custom_profile(self, profile):
self.logger.info(f"loading user profile {profile}")
self.target.apply(profile)
def load_profile(self, profile):
self.logger.info(f"loading user profile {profile}")
self.target.apply_config(profile)
def load_defaults(self):
resp = messagebox.askyesno(
message="Are you sure you want to Reset values to defaults?\nPhysical strips B1, Virtual strips A1\nMono, Solo, Mute, EQ all OFF"
msg = (
"Are you sure you want to Reset values to defaults?",
"Physical strips B1, Virtual strips A1",
"Mono, Solo, Mute, EQ all OFF",
"Gain sliders for Strip/Bus at 0.0",
)
resp = messagebox.askyesno(message="\n".join(msg))
if resp:
self.target.apply_config("reset")
self.load_profile("reset")
def always_on_top(self):
self.parent.attributes("-topmost", self._is_topmost.get())
@@ -242,6 +279,7 @@ class Menus(tk.Menu):
self.parent.nav_frame.show_submix()
for j, var in enumerate(self._selected_bus):
var.set(i == j)
self.parent.subject.notify("submix")
def load_theme(self, theme):
sv_ttk.set_theme(theme)
@@ -277,11 +315,21 @@ class Menus(tk.Menu):
def menu_teardown(self, i):
# remove config load menus
[
removed = []
for key in self.target.configs.keys():
if key not in self.config_defaults:
try:
self.menu_configs_load.delete(key)
for key in self.target.configs.keys()
if key not in self.config_defaults
]
removed.append(key)
except tk._tkinter.tclError as e:
self.logger.warning(f"{type(e).__name__}: {e}")
for key in self.parent.userconfigs.keys():
if key not in self.config_defaults and key not in removed:
try:
self.menu_configs_load.delete(key)
except tk._tkinter.tclError as e:
self.logger.warning(f"{type(e).__name__}: {e}")
[
self.menu_vban.entryconfig(j, state="disabled")
@@ -300,9 +348,29 @@ class Menus(tk.Menu):
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:
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}"]
@@ -312,16 +380,19 @@ class Menus(tk.Menu):
try:
self.logger.info(f"Attempting vban connection to {opts.get('ip')}")
self.vban.login()
except VBANCMDError as e:
except VBANCMDConnectionError as e:
self.vban.logout()
msg = (str(e), f"Please check your connection settings")
msg = (
f"Timeout attempting to establish connection to {opts.get('ip')}",
f"Please check your connection settings",
)
messagebox.showerror("Connection Error", "\n".join(msg))
msg = (str(e), f"resuming local connection")
self.logger.error(", ".join(msg))
self.after(1, self.enable_vban_menus)
return
self.menu_teardown(i)
self.vban.event.add("ldirty")
self.vban.event.add(["pdirty", "ldirty"])
# destroy the current App frames
self.parent._destroy_top_level_frames()
_base_values.vban_connected = True
@@ -336,6 +407,11 @@ class Menus(tk.Menu):
self.menu_layout.entryconfig(
0, state=f"{'normal' if kind.name == 'potato' else 'disabled'}"
)
# ensure the configs are reloaded into memory
if "config" in self.parent.target.__dict__:
del self.parent.target.__dict__["config"]
if "userconfigs" in self.parent.__dict__:
del self.parent.__dict__["userconfigs"]
self.menu_setup()
def vban_disconnect(self, i):
@@ -356,6 +432,11 @@ class Menus(tk.Menu):
self.menu_layout.entryconfig(
0, state=f"{'normal' if kind.name == 'potato' else 'disabled'}"
)
# ensure the configs are reloaded into memory
if "config" in self.parent.target.__dict__:
del self.parent.target.__dict__["config"]
if "userconfigs" in self.parent.__dict__:
del self.parent.__dict__["userconfigs"]
self.menu_setup()
self.after(15000, self.enable_vban_menus)

View File

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