Compare commits

...

12 Commits

Author SHA1 Message Date
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
16 changed files with 260 additions and 107 deletions

2
.gitignore vendored
View File

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

View File

@ -7,7 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
- [ ] Add support for forest theme (should be coming soon) - [ ] Add support for forest theme (if rbende adds it to pypi)
## [1.7.0] - 2023-06-26
### Changed
- There are changes to how some parameters must be set in user toml configs.
- use `comp.knob` to set a strip comp slider.
- use `gate.knob` to set a strip gate slider.
- use `eq.on` to set a bus eq.on button.
- use `eq.ab` to set a bus eq.ab button.
Check example configs.
- `configs` directory may now be located in one of the following locations:
- \<current working directory>/configs/
- \<user home directory>/.configs/vm-compact/configs/
- \<user home directory>/Documents/Voicemeeter/configs/
- dependency updates:
- sv_ttk updated to v2.4.5.
- voicemeeter-api updated to v2.0.1.
- vban-cmd updated to v2.0.0.
### Fixed
- A number of changes that reduce the amount of api calls being made.
## [1.6.0] - 2022-09-29 ## [1.6.0] - 2022-09-29

View File

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

View File

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

25
poetry.lock generated
View File

@ -38,6 +38,20 @@ category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[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]] [[package]]
name = "mypy-extensions" name = "mypy-extensions"
version = "0.4.3" version = "0.4.3"
@ -68,8 +82,8 @@ test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytes
[[package]] [[package]]
name = "sv-ttk" name = "sv-ttk"
version = "2.0" version = "2.4.5"
description = "A gorgeous theme for Tkinter that looks like Windows 11" description = "A gorgeous theme for Tkinter, based on Windows 11's UI"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.4" python-versions = ">=3.4"
@ -84,7 +98,7 @@ python-versions = ">=3.7"
[[package]] [[package]]
name = "vban-cmd" name = "vban-cmd"
version = "1.8.1" version = "2.0.0"
description = "Python interface for the VBAN RT Packet Service (Sendtext)" description = "Python interface for the VBAN RT Packet Service (Sendtext)"
category = "main" category = "main"
optional = false optional = false
@ -95,7 +109,7 @@ tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
[[package]] [[package]]
name = "voicemeeter-api" name = "voicemeeter-api"
version = "0.8.4" version = "2.0.1"
description = "A Python wrapper for the Voiceemeter API" description = "A Python wrapper for the Voiceemeter API"
category = "main" category = "main"
optional = false optional = false
@ -107,12 +121,13 @@ tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "ba702e1c74c507e75070c2c58bffe5a7787d1dfa1b4ff7b5cedce7374871f7db" content-hash = "ffb9af7ef7aa87ac08a09293de5e99487155faaa459cd49964ac95589deb69fa"
[metadata.files] [metadata.files]
black = [] black = []
click = [] click = []
colorama = [] colorama = []
isort = []
mypy-extensions = [] mypy-extensions = []
pathspec = [] pathspec = []
platformdirs = [] platformdirs = []

View File

@ -1,26 +1,25 @@
[tool.poetry] [tool.poetry]
name = "voicemeeter-compact" name = "voicemeeter-compact"
version = "1.6.5" version = "1.7.1"
description = "A Compact Voicemeeter Remote App" description = "A Compact Voicemeeter Remote App"
authors = ["onyx-and-iris <code@onyxandiris.online>"] authors = ["onyx-and-iris <code@onyxandiris.online>"]
license = "MIT" license = "MIT"
readme = "README.md" readme = "README.md"
repository = "https://github.com/onyx-and-iris/voicemeeter-compact" repository = "https://github.com/onyx-and-iris/voicemeeter-compact"
packages = [ packages = [{ include = "vmcompact" }]
{ include = "vmcompact" },
]
include = ["vmcompact/img/cat.ico"] include = ["vmcompact/img/cat.ico"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.10" python = "^3.10"
sv-ttk = "^2.0" sv-ttk = "^2.4.5"
tomli = { version = "^2.0.1", python = "<3.11" } tomli = { version = "^2.0.1", python = "<3.11" }
voicemeeter-api = "^0.8.4" voicemeeter-api = "^2.0.1"
vban-cmd = "^1.8.1" vban-cmd = "^2.0.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 }
isort = "^5.12.0"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]

View File

@ -1,11 +1,13 @@
import tkinter as tk import tkinter as tk
from functools import cached_property
from pathlib import Path from pathlib import Path
from tkinter import ttk from tkinter import ttk
from typing import NamedTuple from typing import NamedTuple
from .builders import MainFrameBuilder from .builders import MainFrameBuilder
from .configurations import loader
from .data import _base_values, _configuration, _kinds_all from .data import _base_values, _configuration, _kinds_all
from .errors import VMCompactErrors from .errors import VMCompactError
from .menu import Menus from .menu import Menus
from .subject import Subject from .subject import Subject
@ -34,13 +36,14 @@ class App(tk.Tk):
super().__init__() super().__init__()
self._vmr = vmr self._vmr = vmr
self._vmr.event.ldirty = True self._vmr.event.add(["pdirty", "ldirty"])
self._vmr.event.remove(["mdirty", "midi"]) self._vmr.init_thread()
icon_path = Path(__file__).parent.resolve() / "img" / "cat.ico" icon_path = Path(__file__).parent.resolve() / "img" / "cat.ico"
if icon_path.is_file(): if icon_path.is_file():
self.iconbitmap(str(icon_path)) self.iconbitmap(str(icon_path))
self.minsize(275, False) self.minsize(275, False)
self.subject = Subject() self.subject = Subject()
self._configs = None
self["menu"] = Menus(self, vmr) self["menu"] = Menus(self, vmr)
self.styletable = ttk.Style() self.styletable = ttk.Style()
if _configuration.config: if _configuration.config:
@ -51,6 +54,9 @@ class App(tk.Tk):
self.drag_id = "" self.drag_id = ""
self.bind("<Configure>", self.dragging) self.bind("<Configure>", self.dragging)
def __str__(self):
return f"{type(self).__name__}App"
@property @property
def target(self): def target(self):
"""returns the current interface""" """returns the current interface"""
@ -75,8 +81,8 @@ class App(tk.Tk):
if kind: if kind:
self.kind = kind self.kind = kind
# register app as observer # register event callbacks
self.target.subject.add(self) self.target.subject.add([self.on_pdirty, self.on_ldirty])
self.bus_frame = None self.bus_frame = None
self.submix_frame = None self.submix_frame = None
@ -91,12 +97,12 @@ class App(tk.Tk):
if self.kind.name == "potato": if self.kind.name == "potato":
self.builder.create_banner() self.builder.create_banner()
def on_update(self, subject): def on_pdirty(self):
"""called whenever notified of update""" if _base_values.run_update:
if subject == "pdirty" and _base_values.run_update:
self.after(1, self.subject.notify, "pdirty") self.after(1, self.subject.notify, "pdirty")
elif subject == "ldirty" and not _base_values.dragging:
def on_ldirty(self):
if not _base_values.dragging:
self.after(1, self.subject.notify, "ldirty") self.after(1, self.subject.notify, "ldirty")
def _destroy_top_level_frames(self): def _destroy_top_level_frames(self):
@ -127,6 +133,11 @@ class App(tk.Tk):
self.drag_id = "" self.drag_id = ""
_base_values.dragging = False _base_values.dragging = False
@cached_property
def userconfigs(self):
self._configs = loader(self.kind.name)
return self._configs
_apps = {kind.name: App.make(kind) for kind in _kinds_all} _apps = {kind.name: App.make(kind) for kind in _kinds_all}
@ -137,5 +148,5 @@ def connect(kind_id: str, vmr) -> App:
try: try:
VMMIN_cls = _apps[kind_id] VMMIN_cls = _apps[kind_id]
except KeyError: except KeyError:
raise VMCompactErrors(f"Invalid kind: {kind_id}") raise VMCompactError(f"Invalid kind: {kind_id}")
return VMMIN_cls(vmr) return VMMIN_cls(vmr)

View File

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

View File

@ -12,6 +12,8 @@ from .config import BusConfig, StripConfig
from .data import _base_values, _configuration from .data import _base_values, _configuration
from .navigation import Navigation from .navigation import Navigation
logger = logging.getLogger(__name__)
class AbstractBuilder(abc.ABC): class AbstractBuilder(abc.ABC):
@abc.abstractmethod @abc.abstractmethod
@ -28,11 +30,10 @@ class AbstractBuilder(abc.ABC):
class MainFrameBuilder(AbstractBuilder): class MainFrameBuilder(AbstractBuilder):
"""Responsible for building the frames that sit directly on the mainframe""" """Responsible for building the frames that sit directly on the mainframe"""
logger = logging.getLogger("builders.mainframebuilder")
def __init__(self, app): def __init__(self, app):
self.kind = app.kind self.kind = app.kind
self.app = app self.app = app
self.logger = logger.getChild(self.__class__.__name__)
def setup(self): def setup(self):
self.app.title( self.app.title(
@ -336,7 +337,7 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
self.configframe.slider_params = ("audibility",) self.configframe.slider_params = ("audibility",)
self.configframe.slider_vars = (tk.DoubleVar(),) self.configframe.slider_vars = (tk.DoubleVar(),)
else: else:
self.configframe.slider_params = ("comp", "gate", "limit") self.configframe.slider_params = ("comp.knob", "gate.knob", "limit")
self.configframe.slider_vars = [ self.configframe.slider_vars = [
tk.DoubleVar() for _ in self.configframe.slider_params tk.DoubleVar() for _ in self.configframe.slider_params
] ]
@ -393,16 +394,16 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
orient="horizontal", orient="horizontal",
length=_configuration.level_width, length=_configuration.level_width,
variable=self.configframe.slider_vars[ variable=self.configframe.slider_vars[
self.configframe.slider_params.index("comp") self.configframe.slider_params.index("comp.knob")
], ],
command=partial(self.configframe.scale_callback, "comp"), command=partial(self.configframe.scale_callback, "comp.knob"),
) )
comp_scale.bind( comp_scale.bind(
"<Double-Button-1>", partial(self.configframe.reset_scale, "comp", 0) "<Double-Button-1>", partial(self.configframe.reset_scale, "comp.knob", 0)
) )
comp_scale.bind("<Button-1>", self.configframe.scale_press) comp_scale.bind("<Button-1>", self.configframe.scale_press)
comp_scale.bind("<ButtonRelease-1>", self.configframe.scale_release) comp_scale.bind("<ButtonRelease-1>", self.configframe.scale_release)
comp_scale.bind("<Enter>", partial(self.configframe.scale_enter, "comp")) comp_scale.bind("<Enter>", partial(self.configframe.scale_enter, "comp.knob"))
comp_scale.bind("<Leave>", self.configframe.scale_leave) comp_scale.bind("<Leave>", self.configframe.scale_leave)
comp_label.grid(column=0, row=0) comp_label.grid(column=0, row=0)
@ -417,16 +418,16 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
orient="horizontal", orient="horizontal",
length=_configuration.level_width, length=_configuration.level_width,
variable=self.configframe.slider_vars[ variable=self.configframe.slider_vars[
self.configframe.slider_params.index("gate") self.configframe.slider_params.index("gate.knob")
], ],
command=partial(self.configframe.scale_callback, "gate"), command=partial(self.configframe.scale_callback, "gate.knob"),
) )
gate_scale.bind( gate_scale.bind(
"<Double-Button-1>", partial(self.configframe.reset_scale, "gate", 0) "<Double-Button-1>", partial(self.configframe.reset_scale, "gate.knob", 0)
) )
gate_scale.bind("<Button-1>", self.configframe.scale_press) gate_scale.bind("<Button-1>", self.configframe.scale_press)
gate_scale.bind("<ButtonRelease-1>", self.configframe.scale_release) gate_scale.bind("<ButtonRelease-1>", self.configframe.scale_release)
gate_scale.bind("<Enter>", partial(self.configframe.scale_enter, "gate")) gate_scale.bind("<Enter>", partial(self.configframe.scale_enter, "gate.knob"))
gate_scale.bind("<Leave>", self.configframe.scale_leave) gate_scale.bind("<Leave>", self.configframe.scale_leave)
gate_label.grid(column=2, row=0) gate_label.grid(column=2, row=0)
@ -563,7 +564,7 @@ class BusConfigFrameBuilder(ChannelConfigFrameBuilder):
} }
self.configframe.bus_modes = list(self.configframe.bus_mode_map.keys()) self.configframe.bus_modes = list(self.configframe.bus_mode_map.keys())
# fmt: on # fmt: on
self.configframe.params = ("mono", "eq", "eq_ab") self.configframe.params = ("mono", "eq.on", "eq.ab")
self.configframe.param_vars = [tk.BooleanVar() for _ in self.configframe.params] self.configframe.param_vars = [tk.BooleanVar() for _ in self.configframe.params]
self.configframe.bus_mode_label_text = tk.StringVar( self.configframe.bus_mode_label_text = tk.StringVar(
value=self.configframe.bus_mode_map[self.configframe.current_bus_mode()] value=self.configframe.bus_mode_map[self.configframe.current_bus_mode()]

View File

@ -1,10 +1,12 @@
import logging
import tkinter as tk import tkinter as tk
from math import log
from tkinter import ttk from tkinter import ttk
from . import builders from . import builders
from .data import _base_values, _configuration from .data import _base_values, _configuration
logger = logging.getLogger(__name__)
class ChannelLabelFrame(ttk.LabelFrame): class ChannelLabelFrame(ttk.LabelFrame):
"""Base class for a single channel""" """Base class for a single channel"""
@ -14,6 +16,7 @@ class ChannelLabelFrame(ttk.LabelFrame):
self.parent = parent self.parent = parent
self.index = index self.index = index
self.id = id self.id = id
self.logger = logger.getChild(self.__class__.__name__)
self.styletable = self.parent.parent.styletable self.styletable = self.parent.parent.styletable
self.builder = builders.ChannelLabelFrameBuilder(self, index, id) self.builder = builders.ChannelLabelFrameBuilder(self, index, id)
@ -40,18 +43,21 @@ class ChannelLabelFrame(ttk.LabelFrame):
return self.parent.target return self.parent.target
def getter(self, param): def getter(self, param):
if hasattr(self.target, param): try:
return getattr(self.target, param) return getattr(self.target, param)
except AttributeError as e:
self.logger(f"{type(e).__name__}: {e}")
def setter(self, param, value): def setter(self, param, value):
if hasattr(self.target, param): if param in dir(self.target): # avoid calling getattr (with hasattr)
setattr(self.target, param, value) setattr(self.target, param, value)
def scale_callback(self, *args): def scale_callback(self, *args):
"""callback function for scale widget""" """callback function for scale widget"""
self.setter("gain", self.gain.get()) val = round(self.gain.get(), 1)
self.gainlabel.set(round(self.gain.get(), 1)) self.setter("gain", val)
self.gainlabel.set(val)
def toggle_mute(self, *args): def toggle_mute(self, *args):
self.target.mute = self.mute.get() self.target.mute = self.mute.get()

View File

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

View File

@ -6,26 +6,32 @@ try:
except ModuleNotFoundError: except ModuleNotFoundError:
import tomli as tomllib import tomli as tomllib
LOGGER = logging.getLogger("configurations") logger = logging.getLogger(__name__)
configuration = {} configuration = {}
config_path = [Path.cwd() / "configs"] configpaths = [
for path in config_path: Path.cwd() / "configs",
if path.is_dir(): Path.home() / ".config" / "vm-compact" / "configs",
filenames = list(path.glob("*.toml")) Path.home() / "Documents" / "Voicemeeter" / "configs",
configs = {} ]
for filename in filenames: for configpath in configpaths:
name = filename.with_suffix("").stem if configpath.is_dir():
try: filepaths = list(configpath.glob("*.toml"))
with open(filename, "rb") as f: if any(f.stem in ("app", "vban") for f in filepaths):
configs[name] = tomllib.load(f) configs = {}
except tomllib.TOMLDecodeError: for filepath in filepaths:
print(f"Invalid TOML config: configs/{filename.stem}") 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}")
for name, cfg in configs.items(): configuration |= configs
LOGGER.info(f"Loaded configuration configs/{name}") break
configuration[name] = cfg
_defaults = { _defaults = {
"configs": { "configs": {
@ -60,3 +66,19 @@ else:
def get_configuration(key): def get_configuration(key):
if key in configuration: if key in configuration:
return configuration[key] return configuration[key]
def loader(kind_id):
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

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

View File

@ -1,5 +1,4 @@
import tkinter as tk import tkinter as tk
from math import log
from tkinter import ttk from tkinter import ttk
from . import builders from . import builders
@ -42,11 +41,13 @@ class GainLayer(ttk.LabelFrame):
return "gainlayer" return "gainlayer"
def getter(self, param): def getter(self, param):
if hasattr(self.target, param): try:
return getattr(self.target, param) return getattr(self.target, param)
except AttributeError as e:
self.logger(f"{type(e).__name__}: {e}")
def setter(self, param, value): def setter(self, param, value):
if hasattr(self.target, param): if param in dir(self.target): # avoid calling getattr (with hasattr)
setattr(self.target, param, value) setattr(self.target, param, value)
def reset_gain(self, *args): def reset_gain(self, *args):
@ -57,8 +58,9 @@ class GainLayer(ttk.LabelFrame):
def scale_callback(self, *args): def scale_callback(self, *args):
"""callback function for scale widget""" """callback function for scale widget"""
self.setter("gain", self.gain.get()) val = round(self.gain.get(), 1)
self.gainlabel.set(round(self.gain.get(), 1)) self.setter("gain", val)
self.gainlabel.set(val)
def scale_press(self, *args): def scale_press(self, *args):
self.after(1, self.remove_events) self.after(1, self.remove_events)
@ -157,7 +159,8 @@ class GainLayer(ttk.LabelFrame):
self.level.set( self.level.set(
( (
0 0
if self.parent.target.strip[self.index].mute or not self.on.get() if self.parent.parent.strip_frame.strips[self.index].mute.get()
or not self.on.get()
else 72 + val - 12 + self.gain.get() else 72 + val - 12 + self.gain.get()
) )
) )

View File

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

View File

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