gui rewritten with builder classes and observables

gui rewritten with builder classes and observables
This commit is contained in:
onyx-and-iris 2022-05-10 20:34:29 +01:00
parent 69eed318c9
commit 92aead8754
12 changed files with 1251 additions and 1366 deletions

172
README.md
View File

@ -1,171 +1 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/onyx-and-iris/voicemeeter-compact/blob/main/LICENSE)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
![OS: Windows](https://img.shields.io/badge/os-windows-red)
![Image of app/potato size comparison](./doc_imgs/potatocomparisonsmaller.png)
# Voicemeeter Compact
A compact Voicemeeter remote app, works locally and over LAN.
For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md)
## Prerequisites
- [Voicemeeter](https://voicemeeter.com/) (Basic v1.0.8.2), (Banana v2.0.6.2) or (Potato v3.0.2.2)
- [Git for Windows](https://gitforwindows.org/)
- Python 3.9+
## Installation
For a step-by-step guide [click here](INSTALLATION.md)
```
git clone https://github.com/onyx-and-iris/voicemeeter-compact
cd voicemeeter-compact
pip install .
```
## Usage
Example `__main__.py` file:
```python
import voicemeeter
import vmcompact
def main():
# pass the kind_id and the vmr object to the app
with voicemeeter.remote(kind_id) as vmr:
app = vmcompact.connect(kind_id, vmr)
app.mainloop()
if __name__ == "__main__":
# choose the kind of Voicemeeter (Local connection)
kind_id = "banana"
voicemeeter.launch(kind_id, hide=False)
main()
```
It's important to know that only labelled strips and buses will appear in the Channel frames. Removing a Channels label will cause the GUI to grow/shrink in real time.
![Image of unlabelled app](./doc_imgs/nolabels.png)
If the GUI looks like the above when you first load it, then no channels are labelled. From the menu, `Profiles->Load Profile` you may load an example config. Save your current Voicemeeter settings first :).
### kind_id
A _kind_id_ specifies a major Voicemeeter version. This may be one of:
- `basic`
- `banana`
- `potato`
## TOML Files
This is how your files should be organised. Wherever your `__main__.py` file is located (after install this can be any location), `config` and `profiles` directories
should be in the same location.
Regarding profiles, a directory for each kind should hold the profile files and in each there can be any number of config files. Example, a config for streaming, a config for general listening/movie watching or any other type of config.
.
├── `__main__.py`
├── configs
        ├── app.toml
        ├── vban.toml
├── profiles
        ├── basic
                ├── example.toml
                ├── other_config.toml
                ├── streaming_config.toml
        ├── banana
                ├── example.toml
                ├── other.toml
                ├── ...
        ├── potato
                ├── example.toml
                ├── ...
## Configs
### app.toml
Configure certain startup states for the app.
- `profiles`
Configure a profile to load on app startup. Don't include the .toml extension in the profile name.
- `theme`
By default the app loads up the [Sun Valley light or dark theme](https://github.com/rdbende/Sun-Valley-ttk-theme) by @rdbende. You have the option to load up the app without any theme loaded. Simply set `enabled` to false and `mode` will take no effect.
- `extends`
Extending the app will show both strips and buses. In reduced mode only one or the other. This app will extend both horizontally and vertically, simply set `extends_horizontal` true or false accordingly.
- `channel`
For each channel labelframe the width and height may be adjusted which effects the spacing between widgets and the length of the scales and progressbars respectively.
- `mwscroll_step`
Sets the amount (in db) the gain slider moves with a single mousewheel step. Default 3.
- `submixes`
Select the default submix bus when Submix frame is shown. For example, a dedicated bus for OBS.
### vban.toml
Configure as many vban connections as you wish. This allows the app to work over a LAN connection as well as with a local Voicemeeter installation.
For vban connections to work correctly VBAN TEXT incoming stream MUST be configured correctly on the remote machine. Both pcs ought to be connected to a local private network and should be able to ping one another.
A valid `vban.toml` might look like this:
```toml
[connection-1]
kind = 'banana'
ip = '192.168.1.2'
streamname = 'streampc'
port = 6990
[connection-2]
kind = 'potato'
ip = '192.168.1.3'
streamname = 'worklaptop'
port = 6990
```
## Profiles
Three example profiles are included with the package, one for each kind of Voicemeeter. Use these to configure parameter startup states. Any parameter supported by the underlying interfaces may be used. For a detailed description of parameter coverage see:
[Voicemeeter Remote API Python](https://github.com/onyx-and-iris/voicemeeter-api-python)
[VBAN CMD API Python](https://github.com/onyx-and-iris/vban-cmd-python)
Profiles may be loaded at any time via the menu.
## Special Thanks
[Vincent Burel](https://github.com/vburel2018) for creating Voicemeeter, its SDK, the C Remote API, the RT Packet service and Streamer View app!
[Christian Volkmann](https://github.com/chvolkmann) for the detailed work that went into creating the underlying Remote API Python Interface.
Unfortunately, the Remote API Python Interface has `NOT` been open source licensed. I have [raised an issue](https://github.com/chvolkmann/voicemeeter-remote-python/issues/13) and asked directly and politely but so far no response. If a license is added in future I will update this section. Without an open source license there is no guarantee that in future this package may not be pulled down, without any notice.
[Rdbende](https://github.com/rdbende) for creating the beautiful Sun Valley Tkinter theme and adding it to Pypi!
# Not Ready Yet

View File

@ -2,27 +2,27 @@ import tkinter as tk
from tkinter import ttk
from typing import NamedTuple
from pathlib import Path
import sv_ttk
from .errors import VMCompactErrors
from .data import _base_vals, _kinds_all
from .channels import ChannelFrame
from .navigation import Navigation
from .data import _kinds_all, _configuration, _base_values
from .subject import Subject
from .builders import MainFrameBuilder
from .menu import Menus
from .banner import Banner
from .configurations import configuration
class App(tk.Tk):
"""Topmost Level of App"""
"""App mainframe"""
_instances = {}
@classmethod
def make(cls, kind: NamedTuple):
"""
Factory function for App
Factory function for App.
Returns an App class of a kind
Returns an App class of a kind.
"""
APP_cls = type(
f"Voicemeeter{kind.name}.Compact",
(cls,),
@ -34,106 +34,34 @@ class App(tk.Tk):
def __init__(self, vmr):
super().__init__()
defaults = {
"profiles": {
"profile": None,
},
"theme": {
"enabled": True,
"mode": "light",
},
"extends": {
"extended": True,
"extends_horizontal": True,
},
"channel": {
"width": 80,
"height": 130,
},
"mwscroll_step": {
"size": 3,
},
"submixes": {
"default": 0,
},
}
if configuration:
self.configuration = defaults | self.configuration
else:
configuration["app"] = defaults
_base_vals.themes_enabled = self.configuration["theme"]["enabled"]
_base_vals.extends_horizontal = self.configuration["extends"][
"extends_horizontal"
]
_base_vals.submixes = self.configuration["submixes"]["default"]
_base_vals.mwscroll_step = self.configuration["mwscroll_step"]["size"]
self.bus_modes_cache = {
"vmr": list(tk.StringVar(value="normal") for _ in range(8)),
"vban": list(tk.StringVar(value="normal") for _ in range(8)),
}
if (
"profiles" in self.configuration
and self.configuration["profiles"]["profile"]
):
vmr.apply_profile(self.configuration["profiles"]["profile"])
# create menus
self._vmr = vmr
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_pdirty = Subject()
self.subject_ldirty = Subject()
self["menu"] = Menus(self, vmr)
self.styletable = ttk.Style()
self._vmr = vmr
if _configuration.profile:
vmr.apply_profile(_configuration.profile)
# start watchers, initialize level arrays
self.upd_pdirty()
self.strip_levels = self.target.strip_levels
self.bus_levels = self.target.bus_levels
self.watch_levels()
self.resizable(False, False)
if _base_vals.themes_enabled:
self.apply_theme()
self._make_app(self.kind)
self.build_app()
self.drag_id = ""
self.bind("<Configure>", self.dragging)
self.iconbitmap(Path(__file__).parent.resolve() / "img" / "cat.ico")
self.minsize(275, False)
@property
def target(self):
"""returns the current interface"""
return self._vban if _base_vals.vban_connected else self._vmr
@property
def pdirty(self):
return self._pdirty
@pdirty.setter
def pdirty(self, val):
self._pdirty = val
@property
def ldirty(self):
return self._ldirty
@ldirty.setter
def ldirty(self, val):
self._ldirty = val
@property
def configuration(self):
return configuration["app"]
@configuration.setter
def configuration(self, val):
self.configuration["app"] = val
return self._vban if _base_values.vban_connected else self._vmr
@property
def configframes(self):
"""returns a tuple of current config frame addresses"""
return tuple(
"""returns the current configframes"""
return (
frame
for frame in self.winfo_children()
if isinstance(frame, ttk.Frame)
@ -141,86 +69,73 @@ class App(tk.Tk):
or "!busconfig" in str(frame)
)
def apply_theme(self):
_base_vals.using_theme = True
sv_ttk.set_theme(self.configuration["theme"]["mode"])
def _make_app(self, kind, vban=None):
self.title(
f'Voicemeeter{kind.name}.Compact [{"Local" if not vban else "Network"} Connection]'
)
def build_app(self, kind=None, vban=None):
"""builds the app frames according to a kind"""
self._vban = vban
if kind:
self.kind = kind
self.strip_levels = self.target.strip_levels
self.bus_levels = self.target.bus_levels
# register as observer
self.target.subject.add(self)
self._make_top_level_frames()
def _make_top_level_frames(self):
# initialize bus frame variable
self.bus_frame = None
# channel_frame, left aligned
self.channel_frame = ChannelFrame.make_strips(self)
self.channel_frame.grid(row=0, column=0, sticky=(tk.W))
# separator
self.sep = ttk.Separator(self, orient="vertical")
self.sep.grid(row=0, column=1, sticky=(tk.N, tk.S))
self.columnconfigure(1, minsize=15)
# navigation frame
self.nav_frame = Navigation(self)
self.nav_frame.grid(row=0, column=3, sticky=(tk.E))
if self.configuration["extends"]["extended"]:
self.submix_frame = None
self.builder = MainFrameBuilder(self)
self.builder.setup()
self.builder.create_channelframe("strip")
self.builder.create_separator()
self.builder.create_navframe()
if _configuration.extended:
self.nav_frame.extend.set(True)
self.nav_frame.extend_frame()
if self.kind.name == "Potato":
self.banner = Banner(self)
self.banner.grid(row=4, column=0, columnspan=3)
self.builder.create_banner()
def update(self, subject):
"""
called whenever notified of update
after 1 to prevent vmr,vban interface waiting.
"""
if subject == "pdirty" and not _base_values.in_scale_button_1:
self.after(1, self.notify_pdirty)
elif subject == "ldirty" and not _base_values.dragging:
self.after(1, self.notify_ldirty)
def notify_pdirty(self):
self.subject_pdirty.notify()
def notify_ldirty(self):
self.subject_ldirty.notify()
def _destroy_top_level_frames(self):
"""
Clear observables.
Unregister app as observer.
Destroy all top level frames.
"""
self.subject_pdirty.clear()
self.subject_ldirty.clear()
self.target.subject.remove(self)
[
frame.destroy()
for frame in self.winfo_children()
if isinstance(frame, ttk.Frame)
]
def upd_pdirty(self):
self.after(1, self.upd_pdirty_step)
def upd_pdirty_step(self):
self.pdirty = self.target.pdirty
self.after(_base_vals.pdelay, self.upd_pdirty_step)
def watch_levels(self):
self.after(1, self.watch_levels_step)
def watch_levels_step(self):
"""Continuously fetch level arrays, only update if ldirty"""
_strip_levels = self.target.strip_levels
_bus_levels = self.target.bus_levels
self.comp_strip = [not a == b for a, b in zip(self.strip_levels, _strip_levels)]
self.comp_bus = [not a == b for a, b in zip(self.bus_levels, _bus_levels)]
self.ldirty = any(any(l) for l in (self.comp_strip, self.comp_bus))
if self.ldirty:
self.strip_levels = _strip_levels
self.bus_levels = _bus_levels
self.after(_base_vals.ldelay, self.watch_levels_step)
def dragging(self, event, *args):
if event.widget is self:
if self.drag_id == "":
_base_vals.in_scale_button_1 = True
_base_vals.dragging = True
_base_values.in_scale_button_1 = True
_base_values.dragging = True
else:
self.after_cancel(self.drag_id)
self.drag_id = self.after(100, self.stop_drag)
def stop_drag(self):
_base_vals.dragging = False
_base_vals.in_scale_button_1 = False
_base_values.dragging = False
_base_values.in_scale_button_1 = False
self.drag_id = ""
@ -229,6 +144,7 @@ _apps = {kind.id: App.make(kind) for kind in _kinds_all}
def connect(kind_id: str, vmr) -> App:
"""return App of the kind requested"""
try:
VMMIN_cls = _apps[kind_id]
return VMMIN_cls(vmr)

View File

@ -1,15 +1,15 @@
import tkinter as tk
from tkinter import ttk
from .data import _base_vals
from .data import _base_values
class Banner(ttk.Frame):
def __init__(self, parent):
super().__init__()
self._parent = parent
self.parent = parent
self.submix = tk.StringVar()
self.submix.set(self.target.bus[_base_vals.submixes].label)
self.submix.set(self.target.bus[_base_values.submixes].label)
self.label = ttk.Label(
self,
@ -22,13 +22,13 @@ class Banner(ttk.Frame):
@property
def target(self):
"""use the correct interface"""
return self._parent.target
return self.parent.target
def upd_submix(self):
self.after(1, self.upd_submix_step)
def upd_submix_step(self):
if not _base_vals.dragging:
self.submix.set(self.target.bus[_base_vals.submixes].label)
if not _base_values.dragging:
self.submix.set(self.target.bus[_base_values.submixes].label)
self.label["text"] = f"SUBMIX: {self.submix.get().upper()}"
self.after(100, self.upd_submix_step)

555
vmcompact/builders.py Normal file
View File

@ -0,0 +1,555 @@
import tkinter as tk
from tkinter import ttk
from functools import partial
import abc
import sv_ttk
from .data import _base_values, _configuration
from .channels import _make_channelframe
from .navigation import Navigation
from .config import StripConfig, BusConfig
from .banner import Banner
class AbstractBuilder(abc.ABC):
@abc.abstractmethod
def setup(self):
"""register as observable"""
pass
@abc.abstractmethod
def teardown(self):
"""unregister as observable"""
pass
class MainFrameBuilder(AbstractBuilder):
"""Responsible for building the frames that sit directly on the mainframe"""
def __init__(self, app):
self.kind = app.kind
self.app = app
def setup(self):
self.app.title(
f'Voicemeeter{self.kind.name}.Compact [{"Local" if not _base_values.vban_connected else "Network"} Connection]'
)
self.app.resizable(False, False)
if _configuration.themes_enabled:
sv_ttk.set_theme(_configuration.theme_mode)
def create_channelframe(self, type_):
if type_ == "strip":
self.app.strip_frame = _make_channelframe(self.app, type_)
else:
self.app.bus_frame = _make_channelframe(self.app, type_)
def create_separator(self):
self.app.sep = ttk.Separator(self.app, orient="vertical")
self.app.sep.grid(row=0, column=1, sticky=(tk.N, tk.S))
self.app.columnconfigure(1, minsize=15)
def create_navframe(self):
self.app.nav_frame = Navigation(self.app)
def create_configframe(self, type_, index, id):
if type_ == "strip":
self.app.config_frame = StripConfig(self.app, index, id)
[
frame.conf.set(False)
for i, frame in enumerate(self.app.strip_frame.labelframes)
if i != index
]
if self.app.bus_frame:
[
frame.conf.set(False)
for _, frame in enumerate(self.app.bus_frame.labelframes)
]
else:
self.app.config_frame = BusConfig(self.app, index, id)
[
frame.conf.set(False)
for i, frame in enumerate(self.app.bus_frame.labelframes)
if i != index
]
if self.app.strip_frame:
[
frame.conf.set(False)
for _, frame in enumerate(self.app.strip_frame.labelframes)
]
if not _configuration.themes_enabled:
if self.app.strip_frame:
[
frame.styletable.configure(
f"{frame.identifier}Conf{frame.index}.TButton",
background=f"{'white' if not frame.conf.get() else 'yellow'}",
)
for _, frame in enumerate(self.app.strip_frame.labelframes)
]
if self.app.bus_frame:
[
frame.styletable.configure(
f"{frame.identifier}Conf{frame.index}.TButton",
background=f"{'white' if not frame.conf.get() else 'yellow'}",
)
for _, frame in enumerate(self.app.bus_frame.labelframes)
]
self.app.after(5, self.reset_config_frames)
def reset_config_frames(self):
[
frame.teardown()
for frame in self.app.configframes
if frame != self.app.config_frame
]
def create_banner(self):
self.app.banner = Banner(self.app)
self.app.banner.grid(row=4, column=0, columnspan=3)
def teardown(self):
pass
class NavigationFrameBuilder(AbstractBuilder):
"""Responsible for building navigation frame widgets"""
def __init__(self, navframe):
self.navframe = navframe
def setup(self):
self.navframe.submix = tk.BooleanVar()
self.navframe.channel = tk.BooleanVar()
self.navframe.extend = tk.BooleanVar(value=_configuration.extended)
self.navframe.info = tk.BooleanVar()
self.navframe.channel_text = tk.StringVar(
value=f"{self.navframe.parent.strip_frame.identifier.upper()}"
)
self.navframe.extend_text = tk.StringVar(
value=f"{'REDUCE' if self.navframe.extend.get() else 'EXTEND'}"
)
self.navframe.info_text = tk.StringVar()
def create_submix_button(self):
self.navframe.submix_button = ttk.Checkbutton(
self.navframe,
text="SUBMIX",
command=self.navframe.show_submix,
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'Submix.TButton'}",
variable=self.navframe.submix,
)
self.navframe.submix_button.grid(column=0, row=0)
if self.navframe.parent.kind.name != "Potato":
self.navframe.submix_button["state"] = "disabled"
def create_channel_button(self):
self.navframe.channel_button = ttk.Checkbutton(
self.navframe,
textvariable=self.navframe.channel_text,
command=self.navframe.switch_channel,
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'Channel.TButton'}",
variable=self.navframe.channel,
)
self.navframe.channel_button.grid(column=0, row=1, rowspan=1)
def create_extend_button(self):
self.navframe.extend_button = ttk.Checkbutton(
self.navframe,
textvariable=self.navframe.extend_text,
command=self.navframe.extend_frame,
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'Extend.TButton'}",
variable=self.navframe.extend,
)
self.navframe.extend_button.grid(column=0, row=2)
def create_info_button(self):
self.navframe.info_button = ttk.Checkbutton(
self.navframe,
textvariable=self.navframe.info_text,
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'Rec.TButton'}",
variable=self.navframe.info,
)
self.navframe.info_button.grid(column=0, row=3)
def grid_configure(self):
[
child.grid_configure(padx=1, pady=1, sticky=(tk.N, tk.S, tk.W, tk.E))
for child in self.navframe.winfo_children()
if isinstance(child, ttk.Checkbutton)
]
if _configuration.themes_enabled:
self.navframe.rowconfigure(1, minsize=_configuration.level_height - 25)
else:
self.navframe.rowconfigure(1, minsize=_configuration.level_height - 5)
def teardown(self):
pass
class ChannelLabelFrameBuilder(AbstractBuilder):
"""Responsible for building channel labelframe widgets"""
def __init__(self, labelframe, index, id):
self.labelframe = labelframe
self.index = index
self.identifier = id
self.using_theme = False
def setup(self):
"""Create class variables for widgets"""
self.labelframe.gain = tk.DoubleVar()
self.labelframe.level = tk.DoubleVar()
self.labelframe.mute = tk.BooleanVar()
self.labelframe.conf = tk.BooleanVar()
"""for gainlayers"""
self.labelframe.on = tk.BooleanVar()
def add_progressbar(self):
"""Adds a progress bar widget to a single label frame"""
self.labelframe.pb = ttk.Progressbar(
self.labelframe,
maximum=100,
orient="vertical",
mode="determinate",
variable=self.labelframe.level,
)
self.labelframe.pb.grid(column=0, row=0)
def add_scale(self):
"""Adds a scale widget to a single label frame"""
self.scale = ttk.Scale(
self.labelframe,
from_=12.0,
to=-60.0,
orient="vertical",
variable=self.labelframe.gain,
command=self.labelframe.scale_callback,
length=100,
)
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("<Enter>", self.labelframe.scale_enter)
self.scale.bind("<ButtonRelease-1>", self.labelframe.scale_release)
self.scale.bind("<Leave>", self.labelframe.scale_leave)
self.scale.bind("<MouseWheel>", self.labelframe._on_mousewheel)
def add_mute_button(self):
"""Adds a mute button widget to a single label frame"""
self.button_mute = ttk.Checkbutton(
self.labelframe,
text="MUTE",
command=partial(self.labelframe.toggle_mute, "mute"),
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{self.identifier}Mute{self.index}.TButton'}",
variable=self.labelframe.mute,
)
self.button_mute.grid(column=0, row=1, columnspan=2)
def add_conf_button(self):
self.button_conf = ttk.Checkbutton(
self.labelframe,
text="CONFIG",
command=self.labelframe.open_config,
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{self.identifier}Conf{self.index}.TButton'}",
variable=self.labelframe.conf,
)
self.button_conf.grid(column=0, row=2, columnspan=2)
def add_on_button(self):
self.button_on = ttk.Checkbutton(
self.labelframe,
text="ON",
command=self.labelframe.set_on,
style=f"{'Toggle.TButton' if _configuration.themes_enabled else 'On.TButton'}",
variable=self.labelframe.on,
)
self.button_on.grid(column=0, row=1, columnspan=2)
def teardown(self):
self.labelframe.grid_remove()
class ChannelConfigFrameBuilder(AbstractBuilder):
"""Responsible for building channel configframe widgets"""
def __init__(self, configframe):
self.configframe = configframe
(
self.configframe.phys_in,
self.configframe.virt_in,
) = self.configframe.parent.kind.ins
(
self.configframe.phys_out,
self.configframe.virt_out,
) = self.configframe.parent.kind.outs
def setup(self):
"register configframe as observable"
pass
def teardown(self):
"""Unregister as observable, then destroy frame"""
self.configframe.parent.subject_pdirty.remove(self.configframe)
self.configframe.destroy()
def grid_configure(self):
[
child.grid_configure(padx=1, pady=1, sticky=(tk.W, tk.E))
for child in self.configframe.winfo_children()
if isinstance(child, ttk.Checkbutton)
]
self.configframe.grid(sticky=(tk.W))
[
self.configframe.columnconfigure(i, minsize=_configuration.level_width)
for i in range(self.configframe.phys_out + self.configframe.virt_out)
]
class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
"""Responsible for building channel configframe widgets"""
def setup(self):
if self.configframe.parent.kind.ins == "Basic":
self.configframe.slider_params = ("audibility",)
self.configframe.slider_vars = (tk.DoubleVar(),)
else:
self.configframe.slider_params = ("comp", "gate", "limit")
self.configframe.slider_vars = [
tk.DoubleVar() for _ in self.configframe.slider_params
]
self.configframe.phys_out_params = [
f"A{i+1}" for i in range(self.configframe.phys_out)
]
self.configframe.phys_out_params_vars = [
tk.BooleanVar() for _ in self.configframe.phys_out_params
]
self.configframe.virt_out_params = [
f"B{i+1}" for i in range(self.configframe.virt_out)
]
self.configframe.virt_out_params_vars = [
tk.BooleanVar() for _ in self.configframe.virt_out_params
]
self.configframe.params = ("mono", "solo")
self.configframe.param_vars = list(
tk.BooleanVar() for _ in self.configframe.params
)
def create_comp_slider(self):
comp_label = ttk.Label(self.configframe, text="Comp")
comp_scale = ttk.Scale(
self.configframe,
from_=0.0,
to=10.0,
orient="horizontal",
length=_configuration.level_width,
variable=self.configframe.slider_vars[
self.configframe.slider_params.index("comp")
],
command=partial(self.configframe.scale_callback, "comp"),
)
comp_scale.bind(
"<Double-Button-1>", partial(self.configframe.reset_scale, "comp", 0)
)
comp_scale.bind("<Button-1>", self.configframe.scale_enter)
comp_scale.bind("<ButtonRelease-1>", self.configframe.scale_leave)
comp_label.grid(column=0, row=0)
comp_scale.grid(column=1, row=0)
def create_gate_slider(self):
gate_label = ttk.Label(self.configframe, text="Gate")
gate_scale = ttk.Scale(
self.configframe,
from_=0.0,
to=10.0,
orient="horizontal",
length=_configuration.level_width,
variable=self.configframe.slider_vars[
self.configframe.slider_params.index("gate")
],
command=partial(self.configframe.scale_callback, "gate"),
)
gate_scale.bind(
"<Double-Button-1>", partial(self.configframe.reset_scale, "gate", 0)
)
gate_scale.bind("<Button-1>", self.configframe.scale_enter)
gate_scale.bind("<ButtonRelease-1>", self.configframe.scale_leave)
gate_label.grid(column=2, row=0)
gate_scale.grid(column=3, row=0)
def create_limit_slider(self):
limit_label = ttk.Label(self.configframe, text="Limit")
limit_scale = ttk.Scale(
self.configframe,
from_=-40,
to=12,
orient="horizontal",
length=_configuration.level_width,
variable=self.configframe.slider_vars[
self.configframe.slider_params.index("limit")
],
command=partial(self.configframe.scale_callback, "limit"),
)
limit_scale.bind(
"<Double-Button-1>", partial(self.configframe.reset_scale, "limit", 12)
)
limit_scale.bind("<Button-1>", self.configframe.scale_enter)
limit_scale.bind("<ButtonRelease-1>", self.configframe.scale_leave)
limit_label.grid(column=4, row=0)
limit_scale.grid(column=5, row=0)
def create_audibility_slider(self):
aud_label = ttk.Label(self.configframe, text="Audibility")
aud_scale = ttk.Scale(
self,
from_=0.0,
to=10.0,
orient="horizontal",
length=_base_values.level_width,
variable=self.configframe.slider_vars[
self.configframe.slider_params.index("audibility")
],
command=partial(self.configframe.scale_callback, "audibility"),
)
aud_scale.bind(
"<Double-Button-1>", partial(self.configframe.reset_scale, "audibility", 0)
)
aud_scale.bind("<Button-1>", self.configframe.scale_enter)
aud_scale.bind("<ButtonRelease-1>", self.configframe.scale_leave)
aud_label.grid(column=0, row=0)
aud_scale.grid(column=1, row=0)
def create_a_buttons(self):
self.configframe.a_buttons = [
ttk.Checkbutton(
self.configframe,
text=param,
command=partial(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)
],
)
for param in self.configframe.phys_out_params
]
[
button.grid(
column=i,
row=1,
)
for i, button in enumerate(self.configframe.a_buttons)
]
def create_b_buttons(self):
self.configframe.b_buttons = [
ttk.Checkbutton(
self.configframe,
text=param,
command=partial(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)
],
)
for param in self.configframe.virt_out_params
]
[
button.grid(
column=len(self.configframe.a_buttons) + i,
row=1,
)
for i, button in enumerate(self.configframe.b_buttons)
]
def create_param_buttons(self):
param_buttons = [
ttk.Checkbutton(
self.configframe,
text=param,
command=partial(self.configframe.toggle_p, param),
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{param}.TButton'}",
variable=self.configframe.param_vars[
self.configframe.params.index(param)
],
)
for param in self.configframe.params
]
[
button.grid(
column=i,
row=2,
)
for i, button in enumerate(param_buttons)
]
class BusConfigFrameBuilder(ChannelConfigFrameBuilder):
"""Responsible for building channel configframe widgets"""
def setup(self):
# fmt: off
self.configframe.bus_mode_map = {
"normal": "Normal",
"amix": "Mix Down A",
"bmix": "Mix Down B",
"repeat": "Stereo Repeat",
"composite": "Composite",
"tvmix": "Up Mix TV",
"upmix21": "Up Mix 2.1",
"upmix41": "Up Mix 4.1",
"upmix61": "Up Mix 6.1",
"centeronly": "Center Only",
"lfeonly": "LFE Only",
"rearonly": "Rear Only",
}
self.configframe.bus_modes = list(self.configframe.bus_mode_map.keys())
# fmt: on
self.configframe.params = ("mono", "eq", "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()]
)
def create_bus_mode_button(self):
self.configframe.busmode_button = ttk.Button(
self.configframe, textvariable=self.configframe.bus_mode_label_text
)
self.configframe.busmode_button.grid(
column=0, row=0, columnspan=2, sticky=(tk.W)
)
self.configframe.busmode_button.bind(
"<Button-1>", self.configframe.rotate_bus_modes_right
)
self.configframe.busmode_button.bind(
"<Button-3>", self.configframe.rotate_bus_modes_left
)
def create_param_buttons(self):
param_buttons = [
ttk.Checkbutton(
self.configframe,
text=param,
command=partial(self.configframe.toggle_p, param),
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{param}.TButton'}",
variable=self.configframe.param_vars[
self.configframe.params.index(param)
],
)
for param in self.configframe.params
]
[
button.grid(
column=i,
row=1,
)
for i, button in enumerate(param_buttons)
]

View File

@ -1,34 +1,31 @@
import tkinter as tk
from tkinter import ttk
from functools import partial
from math import log
from .data import _base_vals
from .config import StripConfig, BusConfig
from . import builders
from .data import _base_values, _configuration
class Channel(ttk.LabelFrame):
class ChannelLabelFrame(ttk.LabelFrame):
"""Base class for a single channel"""
def __init__(self, parent, index, id):
super().__init__(parent)
self._parent = parent
self.parent = parent
self.index = index
self.id = id
self.s = self._parent._parent.styletable
self.config_frame = None
self.gain = tk.DoubleVar()
self.level = tk.DoubleVar()
self.mute = tk.BooleanVar()
self.conf = tk.BooleanVar()
self.styletable = self.parent.parent.styletable
self.builder = builders.ChannelLabelFrameBuilder(self, index, id)
self.builder.setup()
self.builder.add_progressbar()
self.builder.add_scale()
self.builder.add_mute_button()
self.builder.add_conf_button()
self.sync()
self._make_widgets()
self.grid_configure()
self.col_row_configure()
self.watch_pdirty()
self.watch_levels()
self.configbuilder = builders.MainFrameBuilder(self.parent.parent)
@property
def identifier(self):
@ -36,8 +33,9 @@ class Channel(ttk.LabelFrame):
@property
def target(self):
"""use the correct interface"""
return self._parent.target
"""returns the current interface"""
return self.parent.target
def getter(self, param):
if param in dir(self.target):
@ -47,10 +45,16 @@ class Channel(ttk.LabelFrame):
if param in dir(self.target):
setattr(self.target, param, value)
def scale_callback(self, *args):
"""callback function for scale widget"""
self.setter("gain", self.gain.get())
self.parent.parent.nav_frame.info_text.set(round(self.gain.get(), 1))
def toggle_mute(self, *args):
self.target.mute = self.mute.get()
if not _base_vals.using_theme:
self.s.configure(
if not _configuration.themes_enabled:
self.styletable.configure(
f"{self.identifier}Mute{self.index}.TButton",
background=f'{"red" if self.mute.get() else "white"}',
)
@ -58,27 +62,27 @@ class Channel(ttk.LabelFrame):
def reset_gain(self, *args):
self.setter("gain", 0)
self.gain.set(0)
self._parent._parent.nav_frame.info_text.set(0)
self.parent.parent.nav_frame.info_text.set(0)
def scale_enter(self, *args):
self._parent._parent.nav_frame.info_text.set(round(self.gain.get(), 1))
self.parent.parent.nav_frame.info_text.set(round(self.gain.get(), 1))
def scale_leave(self, *args):
self._parent._parent.nav_frame.info_text.set("")
self.parent.parent.nav_frame.info_text.set("")
def scale_press(self, *args):
_base_vals.in_scale_button_1 = True
_base_values.in_scale_button_1 = True
def scale_release(self, *args):
_base_vals.in_scale_button_1 = False
_base_values.in_scale_button_1 = False
def _on_mousewheel(self, event):
self.gain.set(
self.gain.get()
+ (
_base_vals.mwscroll_step
_configuration.mwscroll_step
if event.delta > 0
else -_base_vals.mwscroll_step
else -_configuration.mwscroll_step
)
)
if self.gain.get() > 12:
@ -86,94 +90,46 @@ class Channel(ttk.LabelFrame):
elif self.gain.get() < -60:
self.gain.set(-60)
self.setter("gain", self.gain.get())
self._parent._parent.nav_frame.info_text.set(round(self.gain.get(), 1))
self.parent.parent.nav_frame.info_text.set(round(self.gain.get(), 1))
def scale_callback(self, *args):
"""callback function for scale widget"""
self.setter("gain", self.gain.get())
self._parent._parent.nav_frame.info_text.set(round(self.gain.get(), 1))
def convert_level(self, val):
if _base_vals.vban_connected:
return round(-val * 0.01, 1)
return round(20 * log(val, 10), 1) if val > 0 else -200.0
def _make_widgets(self):
"""Creates a progressbar, scale, mute button and config button for a single channel"""
# Progress bar
self.pb = ttk.Progressbar(
self,
maximum=100,
orient="vertical",
mode="determinate",
variable=self.level,
def open_config(self):
if self.conf.get():
self.configbuilder.create_configframe(self.identifier, self.index, self.id)
else:
self.parent.parent.config_frame.teardown()
if not _configuration.themes_enabled:
self.styletable.configure(
f"{self.identifier}Conf{self.index}.TButton",
background=f'{"yellow" if self.conf.get() else "white"}',
)
self.pb.grid(column=0, row=0)
# Scale
self.scale = ttk.Scale(
self,
from_=12.0,
to=-60.0,
orient="vertical",
variable=self.gain,
command=self.scale_callback,
length=self._parent.height,
)
self.scale.grid(column=1, row=0)
self.scale.bind("<Double-Button-1>", self.reset_gain)
self.scale.bind("<Button-1>", self.scale_press)
self.scale.bind("<Enter>", self.scale_enter)
self.scale.bind("<ButtonRelease-1>", self.scale_release)
self.scale.bind("<Leave>", self.scale_leave)
self.scale.bind("<MouseWheel>", self._on_mousewheel)
# Mute button
self.button_mute = ttk.Checkbutton(
self,
text="MUTE",
command=partial(self.toggle_mute, "mute"),
style=f"{'Toggle.TButton' if _base_vals.using_theme else f'{self.identifier}Mute{self.index}.TButton'}",
variable=self.mute,
)
self.button_mute.grid(column=0, row=1, columnspan=2)
self.button_conf = ttk.Checkbutton(
self,
text="CONFIG",
command=self.open_config,
style=f"{'Toggle.TButton' if _base_vals.using_theme else f'{self.identifier}Conf{self.index}.TButton'}",
variable=self.conf,
)
self.button_conf.grid(column=0, row=2, columnspan=2)
def watch_pdirty(self):
self.after(1, self.watch_pdirty_step)
def watch_pdirty_step(self):
"""keeps params synced but ensures sliders are responsive"""
if self._parent._parent.pdirty and not _base_vals.in_scale_button_1:
self.sync()
self.after(_base_vals.pdelay, self.watch_pdirty_step)
def sync(self):
"""sync params with voicemeeter"""
"""sync parameters"""
retval = self.getter("label")
if len(retval) > 10:
retval = f"{retval[:8]}.."
if not retval:
self.parent.columnconfigure(self.index, minsize=0)
self.parent.parent.subject_ldirty.remove(self)
self.grid_remove()
else:
self.parent.parent.subject_ldirty.add(self)
self.grid()
self.configure(text=retval)
self.gain.set(self.getter("gain"))
self.mute.set(self.getter("mute"))
if not _base_vals.using_theme:
self.s.configure(
if not _configuration.themes_enabled:
self.styletable.configure(
f"{self.identifier}Mute{self.index}.TButton",
background=f'{"red" if self.mute.get() else "white"}',
)
self.s.configure(
f"{self.identifier}Conf{self.index}.TButton", background="white"
)
def col_row_configure(self):
def convert_level(self, val):
if _base_values.vban_connected:
return round(-val * 0.01, 1)
return round(20 * log(val, 10), 1) if val > 0 else -200.0
def grid_configure(self):
self.grid(sticky=(tk.N, tk.S))
[
child.grid_configure(padx=1, pady=1, sticky=(tk.W, tk.E))
@ -187,8 +143,8 @@ class Channel(ttk.LabelFrame):
]
class Strip(Channel):
"""Concrete class representing a single"""
class Strip(ChannelLabelFrame):
"""Concrete class representing a single strip"""
def __init__(self, parent, index, id):
super().__init__(parent, index, id)
@ -199,251 +155,153 @@ class Strip(Channel):
@property
def target(self):
"""use the correct interface"""
"""returns the strip class for this labelframe in the current interface"""
_target = super(Strip, self).target
return getattr(_target, self.identifier)[self.index]
def open_config(self):
if self.conf.get():
self.config_frame = StripConfig(
self._parent._parent,
self.index,
self.identifier,
)
self.config_frame.grid(column=0, row=1, columnspan=4)
self._parent._parent.channel_frame.reset_config_buttons(self)
if self._parent._parent.bus_frame is not None:
self._parent._parent.bus_frame.reset_config_buttons(self)
else:
self.config_frame.destroy()
if not _base_vals.using_theme:
self.s.configure(
f"{self.identifier}Conf{self.index}.TButton",
background=f'{"yellow" if self.conf.get() else "white"}',
)
def watch_levels(self):
self.after(1, self.watch_levels_step)
def watch_levels_step(self):
if not _base_vals.dragging:
if (
self._parent._parent.ldirty
and any(
self._parent._parent.comp_strip[
self.level_offset : self.level_offset + 1
]
)
and _base_vals.strip_level_array_size
== len(self._parent._parent.comp_strip)
):
def update(self):
"""update levels"""
vals = (
self.convert_level(
self._parent._parent.strip_levels[self.level_offset]
),
self.convert_level(
self._parent._parent.strip_levels[self.level_offset + 1]
),
self.convert_level(self.parent.target.strip_levels[self.level_offset]),
self.convert_level(self.parent.target.strip_levels[self.level_offset + 1]),
)
self.level.set(
(0 if self.mute.get() else 100 + (max(vals) - 18) + self.gain.get())
)
self.after(
_base_vals.ldelay if not _base_vals.in_scale_button_1 else 100,
self.watch_levels_step,
)
class Bus(Channel):
class Bus(ChannelLabelFrame):
"""Concrete bus class representing a single bus"""
def __init__(self, parent, index, id):
super().__init__(parent, index, id)
self.level_offset = self.index * 8
self.level_offset = index * 8
@property
def target(self):
"""use the correct interface"""
"""returns the bus class for this labelframe in the current interface"""
_target = super(Bus, self).target
return getattr(_target, self.identifier)[self.index]
def open_config(self):
if self.conf.get():
self.config_frame = BusConfig(
self._parent._parent,
self.index,
self.identifier,
)
if _base_vals.extends_horizontal:
self.config_frame.grid(column=0, row=1, columnspan=4)
else:
self.config_frame.grid(column=0, row=3, columnspan=4)
self._parent._parent.channel_frame.reset_config_buttons(self)
self._parent._parent.bus_frame.update_bus_modes()
self._parent._parent.bus_frame.reset_config_buttons(self)
else:
self._parent._parent.bus_modes_cache[
"vban" if _base_vals.vban_connected else "vmr"
][self.index].set(self.config_frame.bus_mode)
self.config_frame.destroy()
def update(self):
"""update levels"""
if not _base_vals.using_theme:
self.s.configure(
f"{self.identifier}Conf{self.index}.TButton",
background=f'{"yellow" if self.conf.get() else "white"}',
)
def watch_levels(self):
self.after(1, self.watch_levels_step)
def watch_levels_step(self):
if not _base_vals.dragging:
if (
self._parent._parent.ldirty
and any(
self._parent._parent.comp_bus[
self.level_offset : self.level_offset + 1
]
)
and _base_vals.bus_level_array_size
== len(self._parent._parent.comp_bus)
):
vals = (
self.convert_level(
self._parent._parent.bus_levels[self.level_offset]
),
self.convert_level(
self._parent._parent.bus_levels[self.level_offset + 1]
),
self.convert_level(self.parent.target.bus_levels[self.level_offset]),
self.convert_level(self.parent.target.bus_levels[self.level_offset + 1]),
)
self.level.set((0 if self.mute.get() else 100 + (max(vals) - 18)))
self.after(
_base_vals.ldelay if not _base_vals.in_scale_button_1 else 100,
self.watch_levels_step,
)
class ChannelFrame(ttk.Frame):
@classmethod
def make_strips(cls, parent):
return cls(parent, is_strip=True)
@classmethod
def make_buses(cls, parent):
return cls(parent, is_strip=False)
def __init__(self, parent, is_strip: bool = True):
def init(self, parent, id):
super().__init__(parent)
self._parent = parent
self._is_strip = is_strip
self.parent = parent
self.id = id
self.phys_in, self.virt_in = parent.kind.ins
self.phys_out, self.virt_out = parent.kind.outs
_base_vals.strip_level_array_size = 2 * self.phys_in + 8 * self.virt_in
_base_vals.bus_level_array_size = 8 * (self.phys_out + self.virt_out)
defaults = {
"width": 80,
"height": 150,
}
self.configuration = defaults | self.configuration
self.width = self.configuration["width"]
self.height = self.configuration["height"]
# create labelframes
if is_strip:
self.strips = tuple(
Strip(self, i, self.identifier)
for i in range(self.phys_in + self.virt_in)
)
else:
self.buses = tuple(
Bus(self, i, self.identifier)
for i in range(self.phys_out + self.virt_out)
)
# position label frames. destroy any without label text
self.labelframes = self.strips if is_strip else self.buses
self.col_row_configure()
for i, labelframe in enumerate(self.labelframes):
labelframe.grid(row=0, column=i)
if not labelframe.cget("text"):
self.columnconfigure(i, minsize=0)
labelframe.grid_remove()
self.watch_pdirty()
# registers channelframe as pdirty observer
self.parent.subject_pdirty.add(self)
@property
def target(self):
"""returns the current interface"""
return self._parent.target
@property
def configuration(self):
return self._parent.configuration["channel"]
@configuration.setter
def configuration(self, val):
self._parent.configuration["channel"] = val
return self.parent.target
@property
def identifier(self):
return "strip" if self._is_strip else "bus"
return self.id
def update_bus_modes(self):
[
self._parent.bus_modes_cache[
"vban" if _base_vals.vban_connected else "vmr"
][i].set(labelframe.config_frame.bus_mode)
for i, labelframe in enumerate(self.labelframes)
if labelframe is not None and labelframe.config_frame
]
@property
def labelframes(self):
"""returns a tuple of current channel labelframe addresses"""
def reset_config_buttons(self, current):
if not _base_vals.using_theme:
[
labelframe.s.configure(
f"{labelframe.identifier}Conf{labelframe.index}.TButton",
background="white",
return tuple(
frame
for frame in self.winfo_children()
if isinstance(frame, ttk.LabelFrame)
)
for labelframe in self.labelframes
if labelframe is not None
]
[
labelframe.conf.set(False)
for labelframe in self.labelframes
if labelframe is not None and labelframe != current
]
[
labelframe.config_frame.destroy()
for labelframe in self.labelframes
if labelframe is not None
and labelframe.config_frame
and labelframe != current
]
def col_row_configure(self):
def grid_configure(self):
[
self.columnconfigure(i, minsize=self.width)
self.columnconfigure(i, minsize=_configuration.level_width)
for i, _ in enumerate(self.labelframes)
]
[
self.rowconfigure(0, minsize=_configuration.level_height)
for i, _ in enumerate(self.labelframes)
]
[self.rowconfigure(0, minsize=130) for i, _ in enumerate(self.labelframes)]
def watch_pdirty(self):
self.after(1, self.watch_pdirty_step)
def update(self):
for labelframe in self.labelframes:
labelframe.sync()
def watch_pdirty_step(self):
if self._parent.pdirty:
self.watch_labels()
self.after(_base_vals.pdelay, self.watch_pdirty_step)
def teardown(self):
# deregisters channelframe as pdirty observer
def watch_labels(self):
for i, labelframe in enumerate(self.labelframes):
if not labelframe.getter("label"):
self.parent.subject_pdirty.remove(self)
self.destroy()
def _make_channelframe(parent, id):
"""
Creates a Channel Frame class of type strip or bus
"""
phys_in, virt_in = parent.kind.ins
phys_out, virt_out = parent.kind.outs
def init_labels(self, id):
"""
Grids each labelframe, grid_removes any without a label
"""
for i, labelframe in enumerate(
getattr(self, "strips" if id == "strip" else "buses")
):
labelframe.grid(row=0, column=i)
if not labelframe.target.label:
self.columnconfigure(i, minsize=0)
labelframe.grid_remove()
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.grid(row=0, column=0, sticky=(tk.W))
self.grid_configure()
init_labels(self, id)
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))
if _configuration.extended:
if _configuration.extends_horizontal:
self.grid(row=0, column=2)
else:
self.columnconfigure(i, minsize=self.width)
labelframe.grid()
self.grid(row=2, column=0, sticky=(tk.W))
else:
self.grid(row=0, column=0)
self.grid_configure()
init_labels(self, id)
if id == "strip":
CHANNELFRAME_cls = type(
f"ChannelFrame{id.capitalize}",
(ChannelFrame,),
{
"__init__": init_strip,
},
)
else:
CHANNELFRAME_cls = type(
f"ChannelFrame{id.capitalize}",
(ChannelFrame,),
{
"__init__": init_bus,
},
)
return CHANNELFRAME_cls(parent)

View File

@ -2,21 +2,21 @@ import tkinter as tk
from tkinter import ttk
from functools import partial
from .data import _base_vals
from . import builders
from .data import _configuration, _base_values
class Config(ttk.Frame):
def __init__(self, parent, index, _id):
super().__init__(parent)
self._parent = parent
self.parent = parent
self.index = index
self.id = _id
self.s = parent.styletable
self.styletable = parent.styletable
self.phys_in, self.virt_in = parent.kind.ins
self.phys_out, self.virt_out = parent.kind.outs
self.watch_pdirty()
self.parent.subject_pdirty.add(self)
@property
def identifier(self):
@ -25,277 +25,99 @@ class Config(ttk.Frame):
@property
def target(self):
"""returns the current interface"""
return self._parent.target
return self.parent.target
def getter(self, param):
if param in dir(self.target):
return getattr(self.target, param)
def setter(self, param, value):
if param in dir(self.target):
setattr(self.target, param, value)
def scale_enter(self, *args):
_base_vals.in_scale_button_1 = True
_base_values.in_scale_button_1 = True
def scale_leave(self, *args):
_base_vals.in_scale_button_1 = False
self._parent.nav_frame.info_text.set("")
_base_values.in_scale_button_1 = False
self.parent.nav_frame.info_text.set("")
def scale_callback(self, param, *args):
"""callback function for scale widget"""
val = self.slider_vars[self.slider_params.index(param)].get()
self.setter(param, val)
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):
self.setter(param, val)
self.slider_vars[self.slider_params.index(param)].set(val)
def col_row_configure(self):
[
child.grid_configure(padx=1, pady=1, sticky=(tk.W, tk.E))
for child in self.winfo_children()
if isinstance(child, ttk.Checkbutton)
]
self.grid(sticky=(tk.W))
def toggle_p(self, param):
val = self.param_vars[self.params.index(param)].get()
self.setter(param, val)
if not _configuration.themes_enabled:
self.styletable.configure(
f"{param}.TButton", background=f'{"green" if val else "white"}'
)
def watch_pdirty(self):
self.after(1, self.watch_pdirty_step)
def watch_pdirty_step(self):
"""keeps params synced but ensures sliders are responsive"""
if self._parent.pdirty and not _base_vals.in_scale_button_1:
def update(self):
self.sync()
self.after(_base_vals.pdelay, self.watch_pdirty_step)
class StripConfig(Config):
def __init__(self, parent, index, _id):
super().__init__(parent, index, _id)
self.grid(column=0, row=1, columnspan=4)
self.builder = builders.StripConfigFrameBuilder(self)
self.builder.setup()
self.make_row_0()
self.make_row_1()
self.make_row_2()
self.builder.grid_configure()
# create parameter variables
if self._parent.kind.name == "Basic":
self.slider_params = ("audibility",)
self.slider_vars = (tk.DoubleVar(),)
else:
self.slider_params = ("comp", "gate", "limit")
self.slider_vars = [
tk.DoubleVar() for i, _ in enumerate(self.slider_params)
]
self.phys_out_params = [f"A{i+1}" for i in range(self.phys_out)]
self.phys_out_params_vars = [
tk.BooleanVar() for i, _ in enumerate(self.phys_out_params)
]
self.virt_out_params = [f"B{i+1}" for i in range(self.virt_out)]
self.virt_out_params_vars = [
tk.BooleanVar() for i, _ in enumerate(self.virt_out_params)
]
self.params = ("mono", "solo")
self.param_vars = list(tk.BooleanVar() for i, _ in enumerate(self.params))
self.make_row0()
self.make_row1()
self.make_row2()
# sync all parameters
self.sync()
self.sync_sliders()
self.col_row_configure()
@property
def target(self):
"""use the correct interface"""
"""returns the strip class for this configframe in the current interface"""
_target = super(StripConfig, self).target
return getattr(_target, self.identifier)[self.index]
def make_row0(self):
# Create sliders
def make_row_0(self):
if self.index < self.phys_in:
if self._parent.kind.name == "Basic":
# audibility
aud_label = ttk.Label(self, text="Audibility")
aud_scale = ttk.Scale(
self,
from_=0.0,
to=10.0,
orient="horizontal",
length=_base_vals.level_width,
variable=self.slider_vars[self.slider_params.index("audibility")],
command=partial(self.scale_callback, "audibility"),
)
aud_scale.bind(
"<Double-Button-1>", partial(self.reset_scale, "audibility", 0)
)
aud_scale.bind("<Button-1>", self.scale_enter)
aud_scale.bind("<ButtonRelease-1>", self.scale_leave)
aud_label.grid(column=0, row=0)
aud_scale.grid(column=1, row=0)
if self.parent.kind.name == "Basic":
self.builder.create_audibility_slider()
else:
# comp
comp_label = ttk.Label(self, text="Comp")
comp_scale = ttk.Scale(
self,
from_=0.0,
to=10.0,
orient="horizontal",
length=_base_vals.level_width,
variable=self.slider_vars[self.slider_params.index("comp")],
command=partial(self.scale_callback, "comp"),
)
comp_scale.bind(
"<Double-Button-1>", partial(self.reset_scale, "comp", 0)
)
comp_scale.bind("<Button-1>", self.scale_enter)
comp_scale.bind("<ButtonRelease-1>", self.scale_leave)
self.builder.create_comp_slider()
self.builder.create_gate_slider()
self.builder.create_limit_slider()
# gate
gate_label = ttk.Label(self, text="Gate")
gate_scale = ttk.Scale(
self,
from_=0.0,
to=10.0,
orient="horizontal",
length=_base_vals.level_width,
variable=self.slider_vars[self.slider_params.index("gate")],
command=partial(self.scale_callback, "gate"),
)
gate_scale.bind(
"<Double-Button-1>", partial(self.reset_scale, "gate", 0)
)
gate_scale.bind("<Button-1>", self.scale_enter)
gate_scale.bind("<ButtonRelease-1>", self.scale_leave)
def make_row_1(self):
self.builder.create_a_buttons()
self.builder.create_b_buttons()
# limit
limit_label = ttk.Label(self, text="Limit")
limit_scale = ttk.Scale(
self,
from_=-40,
to=12,
orient="horizontal",
length=_base_vals.level_width,
variable=self.slider_vars[self.slider_params.index("limit")],
command=partial(self.scale_callback, "limit"),
)
limit_scale.bind(
"<Double-Button-1>", partial(self.reset_scale, "limit", 12)
)
limit_scale.bind("<Button-1>", self.scale_enter)
limit_scale.bind("<ButtonRelease-1>", self.scale_leave)
# Position sliders
comp_label.grid(column=0, row=0)
comp_scale.grid(column=1, row=0)
gate_label.grid(column=2, row=0)
gate_scale.grid(column=3, row=0)
limit_label.grid(column=4, row=0)
limit_scale.grid(column=5, row=0)
def make_row1(self):
# create buttons
self.a_buttons = [
ttk.Checkbutton(
self,
text=param,
command=partial(self.toggle_a, param),
style=f"{'Toggle.TButton' if _base_vals.using_theme else f'{param}.TButton'}",
variable=self.phys_out_params_vars[self.phys_out_params.index(param)],
)
for param in self.phys_out_params
]
self.b_buttons = [
ttk.Checkbutton(
self,
text=param,
command=partial(self.toggle_b, param),
style=f"{'Toggle.TButton' if _base_vals.using_theme else f'{param}.TButton'}",
variable=self.virt_out_params_vars[self.virt_out_params.index(param)],
)
for param in self.virt_out_params
]
# set button positions
[
button.grid(
column=self.a_buttons.index(button),
row=1,
)
for button in self.a_buttons
]
[
button.grid(
column=len(self.a_buttons) + self.b_buttons.index(button),
row=1,
)
for button in self.b_buttons
]
def make_row_2(self):
self.builder.create_param_buttons()
def toggle_a(self, param):
val = self.phys_out_params_vars[self.phys_out_params.index(param)].get()
self.setter(param, val)
if not _base_vals.using_theme:
self.s.configure(
if not _configuration.themes_enabled:
self.styletable.configure(
f"{param}.TButton", background=f'{"green" if val else "white"}'
)
def toggle_b(self, param):
val = self.virt_out_params_vars[self.virt_out_params.index(param)].get()
self.setter(param, val)
if not _base_vals.using_theme:
self.s.configure(
if not _configuration.themes_enabled:
self.styletable.configure(
f"{param}.TButton", background=f'{"green" if val else "white"}'
)
def make_row2(self):
if self._parent.kind.name in ("Banana", "Potato"):
if self.index == self.phys_in:
self.params = list(map(lambda x: x.replace("mono", "mc"), self.params))
if self._parent.kind.name == "Banana":
pass
# karaoke modes not in RT Packet yet. May implement in future
"""
if self.index == self.phys_in + 1:
self.params = list(
map(lambda x: x.replace("mono", "k"), self.params)
)
self.param_vars[self.params.index("k")] = tk.IntVar
"""
else:
if self.index == self.phys_in + self.virt_in - 1:
self.params = list(
map(lambda x: x.replace("mono", "mc"), self.params)
)
param_buttons = [
ttk.Checkbutton(
self,
text=param,
command=partial(self.toggle_p, param),
style=f"{'Toggle.TButton' if _base_vals.using_theme else f'{param}.TButton'}",
variable=self.param_vars[self.params.index(param)],
)
for param in self.params
]
[
button.grid(
column=param_buttons.index(button),
row=2,
)
for button in param_buttons
]
def toggle_p(self, param):
val = self.param_vars[self.params.index(param)].get()
self.setter(param, val)
if not _base_vals.using_theme:
self.s.configure(
f"{param}.TButton", background=f'{"green" if val else "white"}'
)
def teardown(self):
self.builder.teardown()
def sync(self):
[
@ -314,154 +136,110 @@ class StripConfig(Config):
self.param_vars[self.params.index(param)].set(self.getter(param))
for param in self.params
]
if not _base_vals.using_theme:
if not _configuration.themes_enabled:
[
self.s.configure(
self.styletable.configure(
f"{param}.TButton",
background=f'{"green" if self.phys_out_params_vars[self.phys_out_params.index(param)].get() else "white"}',
)
for param in self.phys_out_params
]
[
self.s.configure(
self.styletable.configure(
f"{param}.TButton",
background=f'{"green" if self.virt_out_params_vars[self.virt_out_params.index(param)].get() else "white"}',
)
for param in self.virt_out_params
]
[
self.s.configure(
self.styletable.configure(
f"{param}.TButton",
background=f'{"green" if self.param_vars[self.params.index(param)].get() else "white"}',
)
for param in self.params
]
def sync_sliders(self):
[
self.slider_vars[self.slider_params.index(param)].set(self.getter(param))
for param in self.slider_params
]
def col_row_configure(self):
super(StripConfig, self).col_row_configure()
[
self.columnconfigure(i, minsize=80)
for i in range(self.phys_out + self.virt_out)
]
class BusConfig(Config):
def __init__(self, parent, index, _id):
super().__init__(parent, index, _id)
# fmt: off
# create parameter variables
self.bus_modes = (
"normal", "Amix", "Bmix", "Repeat", "Composite", "TVMix", "UpMix21",
"UpMix41", "UpMix61", "CenterOnly", "LFEOnly", "RearOnly",
)
# fmt: on
self.params = ("mono", "eq", "eq_ab")
self.param_vars = [tk.BooleanVar() for i, _ in enumerate(self.params)]
if _configuration.extends_horizontal:
self.grid(column=0, row=1, columnspan=4)
else:
self.grid(column=0, row=3, columnspan=4)
self.builder = builders.BusConfigFrameBuilder(self)
self.builder.setup()
self.make_row_0()
self.make_row_1()
self.builder.grid_configure()
# sync all parameters
self.sync()
self.make_row0()
self.make_row1()
self.col_row_configure()
@property
def target(self):
"""returns the current interface"""
"""returns the bus class for this configframe in the current interface"""
_target = super(BusConfig, self).target
return getattr(_target, self.identifier)[self.index]
@property
def bus_mode(self):
return self._parent.bus_modes_cache[
"vban" if _base_vals.vban_connected else "vmr"
][self.index].get()
def make_row_0(self):
self.builder.create_bus_mode_button()
@bus_mode.setter
def bus_mode(self, val):
self._parent.bus_modes_cache["vban" if _base_vals.vban_connected else "vmr"][
self.index
].set(val)
def make_row_1(self):
self.builder.create_param_buttons()
def make_row0(self):
self.bus_mode_label_text = tk.StringVar(value=f"Bus Mode: {self.bus_mode}")
self.busmode_button = ttk.Button(self, textvariable=self.bus_mode_label_text)
self.busmode_button.grid(column=0, row=0, columnspan=2, sticky=(tk.W))
self.busmode_button.bind("<Button-1>", self.rotate_bus_modes_right)
self.busmode_button.bind("<Button-3>", self.rotate_bus_modes_left)
def current_bus_mode(self):
for mode in self.bus_modes:
if getattr(self.target.mode, mode):
return mode
def rotate_bus_modes_right(self, *args):
current_index = self.bus_modes.index(self.bus_mode)
if current_index + 1 < len(self.bus_modes):
self.bus_mode = self.bus_modes[current_index + 1]
current_mode = self.current_bus_mode()
next = self.bus_modes.index(current_mode) + 1
if next < len(self.bus_modes):
setattr(
self.target.mode,
self.bus_modes[next],
True,
)
self.bus_mode_label_text.set(self.bus_mode_map[self.bus_modes[next]])
else:
self.bus_mode = self.bus_modes[0]
setattr(self.target.mode, self.bus_mode.lower(), True)
self.bus_mode_label_text.set(f"Bus Mode: {self.bus_mode}")
self.target.mode.normal = True
self.bus_mode_label_text.set("Normal")
def rotate_bus_modes_left(self, *args):
current_index = self.bus_modes.index(self.bus_mode)
if current_index == 0:
self.bus_mode = self.bus_modes[-1]
current_mode = self.current_bus_mode()
prev = self.bus_modes.index(current_mode) - 1
if prev < 0:
self.target.mode.rearonly = True
self.bus_mode_label_text.set("Rear Only")
else:
self.bus_mode = self.bus_modes[current_index - 1]
setattr(self.target.mode, self.bus_mode.lower(), True)
self.bus_mode_label_text.set(f"Bus Mode: {self.bus_mode}")
def make_row1(self):
param_buttons = [
ttk.Checkbutton(
self,
text=param,
command=partial(self.toggle_p, param),
style=f"{'Toggle.TButton' if _base_vals.using_theme else f'{param}.TButton'}",
variable=self.param_vars[self.params.index(param)],
setattr(
self.target.mode,
self.bus_modes[prev],
True,
)
for param in self.params
]
[
button.grid(
column=param_buttons.index(button),
row=1,
)
for button in param_buttons
]
self.bus_mode_label_text.set(self.bus_mode_map[self.bus_modes[prev]])
def toggle_p(self, param):
val = self.param_vars[self.params.index(param)].get()
self.setter(param, val)
if not _base_vals.using_theme:
self.s.configure(
f"{param}.TButton", background=f'{"green" if val else "white"}'
)
def col_row_configure(self):
super(BusConfig, self).col_row_configure()
[
self.columnconfigure(i, minsize=80)
for i in range(self.phys_out + self.virt_out)
]
def teardown(self):
self.builder.teardown()
def sync(self):
for i, mode in enumerate(self.bus_modes):
if getattr(self.target.mode, mode.lower()):
self.bus_mode = self.bus_modes[i]
[
self.param_vars[self.params.index(param)].set(self.getter(param))
for param in self.params
]
if not _base_vals.using_theme:
self.bus_mode_label_text.set(self.bus_mode_map[self.current_bus_mode()])
if not _configuration.themes_enabled:
[
self.s.configure(
self.styletable.configure(
f"{param}.TButton",
background=f'{"green" if self.param_vars[self.params.index(param)].get() else "white"}',
)
for param in self.params
]
class Iterator:
pass

View File

@ -16,5 +16,39 @@ for path in config_path:
print(f"Invalid TOML profile: configs/{filename.stem}")
for name, cfg in configs.items():
print(f"Loaded profile configs/{name}")
print(f"Loaded configuration configs/{name}")
configuration[name] = cfg
_defaults = {
"profiles": {
"profile": None,
},
"theme": {
"enabled": True,
"mode": "light",
},
"extends": {
"extended": True,
"extends_horizontal": True,
},
"channel": {
"width": 80,
"height": 130,
},
"mwscroll_step": {
"size": 3,
},
"submixes": {
"default": 0,
},
}
if "app" in configuration:
configuration["app"] = _defaults | configuration["app"]
else:
configuration["app"] = _defaults
def get_configuration(key):
if key in configuration:
return configuration[key]

View File

@ -1,24 +1,53 @@
from dataclasses import dataclass
from voicemeeter import kinds
from .configurations import get_configuration
configuration = get_configuration("app")
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
@dataclass
class BaseValues:
class Configurations(metaclass=SingletonMeta):
# width of a single labelframe
level_width: int = 75
# height of a single labelframe
level_height: int = 100
level_width: int = 80
# is the gui extended
extended: bool = configuration["extends"]["extended"]
# direction the gui extends
extends_horizontal: bool = configuration["extends"]["extends_horizontal"]
# are themes enabled
themes_enabled: bool = configuration["theme"]["enabled"]
# light or dark
theme_mode: str = configuration["theme"]["mode"]
# size of mousewheel scroll step
mwscroll_step: int = configuration["mwscroll_step"]["size"]
@property
def profile(self):
if "profiles" in configuration:
return configuration["profiles"]["profile"]
@dataclass
class BaseValues(metaclass=SingletonMeta):
# are we dragging a scale with mouse 1
in_scale_button_1: bool = False
# are we dragging main window with mouse 1
dragging: bool = False
# direction the gui extends
extends_horizontal: bool = True
# a vban connection established
vban_connected: bool = False
# are themes enabled
themes_enabled: bool = True
# are we using a theme
using_theme: bool = False
# bus assigned as current submix
submixes: int = 0
# pdirty delay
@ -29,12 +58,10 @@ class BaseValues:
strip_level_array_size: int = None
# size of bus level array for a kind
bus_level_array_size: int = None
# size of mousewheel scroll step
mwscroll_step: int = 3
_base_vals = BaseValues()
_base_values = BaseValues()
_configuration = Configurations()
_kinds = {kind.id: kind for kind in kinds.all}

View File

@ -2,7 +2,8 @@ import tkinter as tk
from tkinter import ttk
from math import log
from .data import _base_vals
from .data import _base_values, _configuration
from . import builders
class GainLayer(ttk.LabelFrame):
@ -10,29 +11,28 @@ class GainLayer(ttk.LabelFrame):
def __init__(self, parent, index, j):
super().__init__(parent)
self._parent = parent
self.parent = parent
self.index = index
self.j = j
self.gain = tk.DoubleVar()
self.level = tk.DoubleVar()
self.on = tk.BooleanVar()
self.s = self._parent._parent.styletable
self.styletable = self.parent.parent.styletable
if index <= parent.phys_in:
self.level_offset = index * 2
else:
self.level_offset = parent.phys_in * 2 + (index - parent.phys_in) * 8
self.builder = builders.ChannelLabelFrameBuilder(self, index, id="gainlayer")
self.builder.setup()
self.builder.add_progressbar()
self.builder.add_scale()
self.builder.add_on_button()
self.sync()
self._make_widgets()
self.col_row_configure()
self.watch_pdirty()
self.watch_levels()
self.grid_configure()
@property
def target(self):
"""returns the current interface"""
_target = self._parent.target
"""returns the strip[i].gainlayer class in the current interface"""
_target = self.parent.target
return _target.strip[self.index].gainlayer[self.j]
def getter(self, param):
@ -46,27 +46,33 @@ class GainLayer(ttk.LabelFrame):
def reset_gain(self, *args):
self.setter("gain", 0)
self.gain.set(0)
self._parent._parent.nav_frame.info_text.set(0)
self.parent.parent.nav_frame.info_text.set(0)
def scale_enter(self, *args):
self._parent._parent.nav_frame.info_text.set(round(self.gain.get(), 1))
def scale_callback(self, *args):
"""callback function for scale widget"""
def scale_leave(self, *args):
self._parent._parent.nav_frame.info_text.set("")
self.setter("gain", self.gain.get())
self.parent.parent.nav_frame.info_text.set(round(self.gain.get(), 1))
def scale_press(self, *args):
_base_vals.in_scale_button_1 = True
_base_values.in_scale_button_1 = True
def scale_release(self, *args):
_base_vals.in_scale_button_1 = False
_base_values.in_scale_button_1 = False
def scale_enter(self, *args):
self.parent.parent.nav_frame.info_text.set(round(self.gain.get(), 1))
def scale_leave(self, *args):
self.parent.parent.nav_frame.info_text.set("")
def _on_mousewheel(self, event):
self.gain.set(
self.gain.get()
+ (
_base_vals.mwscroll_step
_base_values.mwscroll_step
if event.delta > 0
else -_base_vals.mwscroll_step
else -_base_values.mwscroll_step
)
)
if self.gain.get() > 12:
@ -74,72 +80,23 @@ class GainLayer(ttk.LabelFrame):
elif self.gain.get() < -60:
self.gain.set(-60)
self.setter("gain", self.gain.get())
self._parent._parent.nav_frame.info_text.set(round(self.gain.get(), 1))
def scale_callback(self, *args):
"""callback function for scale widget"""
self.setter("gain", self.gain.get())
self._parent._parent.nav_frame.info_text.set(round(self.gain.get(), 1))
self.parent.parent.nav_frame.info_text.set(round(self.gain.get(), 1))
def set_on(self):
"""enables a gainlayer. sets its button colour"""
setattr(
self._parent.target.strip[self.index],
self._parent.buses[self.j],
self.parent.target.strip[self.index],
self.parent.buses[self.j],
self.on.get(),
)
if not _base_vals.using_theme:
self.s.configure(
if not _configuration.themes_enabled:
self.styletable.configure(
f"On.TButton",
background=f'{"green" if self.on.get() else "white"}',
)
def convert_level(self, val):
if _base_vals.vban_connected:
return round(-val * 0.01, 1)
return round(20 * log(val, 10), 1) if val > 0 else -200.0
def _make_widgets(self):
"""Creates a progressbar, scale, on button and config button for a single channel"""
# Progress bar
self.pb = ttk.Progressbar(
self,
maximum=100,
orient="vertical",
mode="determinate",
variable=self.level,
)
self.pb.grid(column=0, row=0)
# Scale
self.scale = ttk.Scale(
self,
from_=12.0,
to=-60.0,
orient="vertical",
variable=self.gain,
command=self.scale_callback,
length=self._parent.height,
)
self.scale.grid(column=1, row=0)
self.scale.bind("<Double-Button-1>", self.reset_gain)
self.scale.bind("<Button-1>", self.scale_press)
self.scale.bind("<Enter>", self.scale_enter)
self.scale.bind("<ButtonRelease-1>", self.scale_release)
self.scale.bind("<Leave>", self.scale_leave)
self.scale.bind("<MouseWheel>", self._on_mousewheel)
# On button
self.button_on = ttk.Checkbutton(
self,
text="ON",
command=self.set_on,
style=f"{'Toggle.TButton' if _base_vals.using_theme else 'On.TButton'}",
variable=self.on,
)
self.button_on.grid(column=0, row=1, columnspan=2)
def col_row_configure(self):
def grid_configure(self):
[
child.grid_configure(padx=1, pady=1, sticky=(tk.N, tk.S, tk.W, tk.E))
for child in self.winfo_children()
@ -150,141 +107,119 @@ class GainLayer(ttk.LabelFrame):
for child in self.winfo_children()
if isinstance(child, ttk.Progressbar) or isinstance(child, ttk.Scale)
]
# pb and scale
self.columnconfigure(0, minsize=36)
self.columnconfigure(1, minsize=36)
# on button
if _configuration.themes_enabled:
self.rowconfigure(1, minsize=70)
def watch_pdirty(self):
self.after(1, self.watch_pdirty_step)
def watch_pdirty_step(self):
"""keeps params synced but ensures sliders are responsive"""
if self._parent._parent.pdirty and not _base_vals.in_scale_button_1:
self.sync()
self.after(_base_vals.pdelay, self.watch_pdirty_step)
else:
self.rowconfigure(1, minsize=55)
def sync(self):
"""sync params with voicemeeter"""
retval = self._parent.target.strip[self.index].label
retval = self.parent.target.strip[self.index].label
if len(retval) > 10:
retval = f"{retval[:8]}.."
if not retval:
self.parent.columnconfigure(self.index, minsize=0)
self.parent.parent.subject_ldirty.remove(self)
self.grid_remove()
else:
self.parent.parent.subject_ldirty.add(self)
self.grid()
self.configure(text=retval)
self.gain.set(self.getter("gain"))
self.on.set(
getattr(
self._parent.target.strip[self.index],
self._parent.buses[self.j],
self.parent.target.strip[self.index],
self.parent.buses[self.j],
)
)
def watch_levels(self):
self.after(1, self.watch_levels_step)
def convert_level(self, val):
if _base_values.vban_connected:
return round(-val * 0.01, 1)
return round(20 * log(val, 10), 1) if val > 0 else -200.0
def watch_levels_step(self):
if not _base_vals.dragging:
if (
self._parent._parent.ldirty
and any(
self._parent._parent.comp_strip[
self.level_offset : self.level_offset + 1
]
)
and _base_vals.strip_level_array_size
== len(self._parent._parent.comp_strip)
):
def update(self):
"""update levels"""
vals = (
self.convert_level(
self._parent._parent.strip_levels[self.level_offset]
),
self.convert_level(
self._parent._parent.strip_levels[self.level_offset + 1]
),
self.convert_level(self.parent.target.strip_levels[self.level_offset]),
self.convert_level(self.parent.target.strip_levels[self.level_offset + 1]),
)
self.level.set(
(
0
if self._parent._parent.channel_frame.strips[
self.index
].mute.get()
if self.parent.parent.strip_frame.strips[self.index].mute.get()
or not self.on.get()
else 100 + (max(vals) - 18) + self.gain.get()
)
)
self.after(
_base_vals.ldelay if not _base_vals.in_scale_button_1 else 100,
self.watch_levels_step,
)
class SubMixFrame(ttk.Frame):
def __init__(self, parent):
super().__init__(parent)
self._parent = parent
self.parent = parent
self.phys_in, self.virt_in = parent.kind.ins
self.phys_out, self.virt_out = parent.kind.outs
self.buses = tuple(f"A{i+1}" for i in range(self.phys_out)) + tuple(
f"B{i+1}" for i in range(self.virt_out)
)
defaults = {
"width": 80,
"height": 150,
}
self.configuration = defaults | self.configuration
self.width = self.configuration["width"]
self.height = self.configuration["height"]
self.gainlayers = [
GainLayer(self, index, _base_vals.submixes) for index in range(8)
GainLayer(self, index, _base_values.submixes) for index in range(8)
]
[
gainlayer.grid(row=0, column=self.gainlayers.index(gainlayer))
for gainlayer in self.gainlayers
]
self.col_row_configure()
# destroy any without label text
for i, gainlayer in enumerate(self.gainlayers):
gainlayer.grid(row=0, column=i)
if not gainlayer.cget("text"):
for i, labelframe in enumerate(self.labelframes):
labelframe.grid(row=0, column=i)
if not self.target.strip[i].label:
self.columnconfigure(i, minsize=0)
gainlayer.grid_remove()
labelframe.grid_remove()
self.watch_pdirty()
if _configuration.extends_horizontal:
self.grid(row=0, column=2)
if parent.bus_frame:
parent.bus_frame.grid_remove()
else:
self._parent.submix_frame.grid(row=2, column=0)
if parent.bus_frame:
parent.bus_frame.grid_remove()
# registers submixframe as pdirty observer
self.parent.subject_pdirty.add(self)
@property
def target(self):
"""returns the current interface"""
return self._parent.target
return self.parent.target
@property
def configuration(self):
return self._parent.configuration["channel"]
def labelframes(self):
"""returns a tuple of current gainlayer labelframe addresses"""
@configuration.setter
def configuration(self, val):
self._parent.configuration["channel"] = val
return tuple(
frame
for frame in self.winfo_children()
if isinstance(frame, ttk.LabelFrame)
)
def col_row_configure(self):
def grid_configure(self):
[
self.columnconfigure(i, minsize=self.width)
for i, _ in enumerate(self.gainlayers)
self.columnconfigure(i, minsize=_configuration.level_width)
for i, _ in enumerate(self.labelframes)
]
[
self.rowconfigure(0, minsize=_configuration.level_height)
for i, _ in enumerate(self.labelframes)
]
[self.rowconfigure(0, minsize=130) for i, _ in enumerate(self.gainlayers)]
def watch_pdirty(self):
self.after(1, self.watch_pdirty_step)
def update(self):
for labelframe in self.labelframes:
labelframe.sync()
def watch_pdirty_step(self):
if self._parent.pdirty:
self.watch_labels()
self.after(_base_vals.pdelay, self.watch_pdirty_step)
def watch_labels(self):
for i, gainlayer in enumerate(self.gainlayers):
if not self.target.strip[gainlayer.index].label:
self.columnconfigure(i, minsize=0)
gainlayer.grid_remove()
else:
self.columnconfigure(i, minsize=80)
gainlayer.grid()
def teardown(self):
# deregisters submixframe as pdirty observer
self.parent.subject_pdirty.remove(self)
self.destroy()

View File

@ -3,20 +3,23 @@ from tkinter import ttk, messagebox
from functools import partial
import webbrowser
import sv_ttk
import vbancmd
from .configurations import configuration
from .data import _base_vals, kind_get
from .data import (
get_configuration,
_base_values,
_configuration,
kind_get,
)
class Menus(tk.Menu):
def __init__(self, parent, vmr):
super().__init__()
self._parent = parent
self._vmr = vmr
if self.configuration_vban is not None:
self.vban_conns = [None for i, _ in enumerate(self.configuration_vban)]
self.parent = parent
self.vmr = vmr
self.vban_config = get_configuration("vban")
self.app_config = get_configuration("app")
self._is_topmost = tk.BooleanVar()
self._selected_bus = list(tk.BooleanVar() for _ in range(8))
@ -69,76 +72,23 @@ class Menus(tk.Menu):
self.menu_profiles_load = tk.Menu(menu_profiles, tearoff=0)
menu_profiles.add_cascade(menu=self.menu_profiles_load, label="Load profile")
defaults = {"base", "blank"}
if len(vmr.profiles) > len(defaults) and all(
key in vmr.profiles for key in defaults
if len(self.target.profiles) > len(defaults) and all(
key in self.target.profiles for key in defaults
):
[
self.menu_profiles_load.add_command(
label=profile, command=partial(self.load_profile, profile)
)
for profile in vmr.profiles.keys()
for profile in self.target.profiles.keys()
if profile not in defaults
]
else:
menu_profiles.entryconfig(0, state="disabled")
menu_profiles.add_command(label="Reset to defaults", command=self.load_defaults)
# vban connect menu
self.menu_vban = tk.Menu(self, tearoff=0)
self.add_cascade(menu=self.menu_vban, label="VBAN")
if self.configuration_vban:
for i, _ in enumerate(self.configuration_vban):
setattr(self, f"menu_vban_{i+1}", tk.Menu(self.menu_vban, tearoff=0))
target_menu = getattr(self, f"menu_vban_{i+1}")
self.menu_vban.add_cascade(
menu=target_menu, label=f"VBAN Connect #{i+1}", underline=0
)
target_menu.add_command(
label="Connect", command=partial(self.vban_connect, i)
)
target_menu.add_command(
label="Disconnect", command=partial(self.vban_disconnect, i)
)
target_menu.entryconfig(1, state="disabled")
else:
self.entryconfig(3, state="disabled")
# layout menu
self.menu_layout = tk.Menu(self, tearoff=0)
self.add_cascade(menu=self.menu_layout, label="Layout")
# layout/extends
self.menu_extends = tk.Menu(self.menu_layout, tearoff=0)
self.menu_layout.add_cascade(
menu=self.menu_extends, label="Extends", underline=0
)
self.menu_extends.add_command(
label="horizontal",
underline=0,
command=partial(self.switch_orientation, extends_horizontal=True),
)
self.menu_extends.add_command(
label="vertical",
underline=0,
command=partial(self.switch_orientation, extends_horizontal=False),
)
self.menu_extends.entryconfig(
0 if _base_vals.extends_horizontal else 1, state="disabled"
)
# layout/themes
self.menu_themes = tk.Menu(self.menu_layout, tearoff=0)
self.menu_layout.add_cascade(menu=self.menu_themes, label="Themes")
self.menu_themes.add_command(
label="light", command=partial(self.load_theme, "light")
)
self.menu_themes.add_command(
label="dark", command=partial(self.load_theme, "dark")
)
self.menu_themes.entryconfig(
0 if self.configuration_app["theme"]["mode"] == "light" else 1,
state="disabled",
)
if not _base_vals.themes_enabled:
self.entryconfig(6, state="disabled")
# layout/submixes
# here we build menu regardless of kind but disable if not Potato
buses = tuple(f"A{i+1}" for i in range(5)) + tuple(f"B{i+1}" for i in range(3))
@ -155,9 +105,64 @@ class Menus(tk.Menu):
)
for i in range(8)
]
self._selected_bus[_base_vals.submixes].set(True)
if self._parent.kind.name != "Potato":
self.menu_layout.entryconfig(2, state="disabled")
self._selected_bus[_base_values.submixes].set(True)
if self.parent.kind.name != "Potato":
self.menu_layout.entryconfig(0, state="disabled")
# layout/extends
self.menu_extends = tk.Menu(self.menu_layout, tearoff=0)
self.menu_layout.add_cascade(
menu=self.menu_extends, label="Extends", underline=0
)
self.menu_extends.add_command(
label="horizontal",
underline=0,
command=partial(self.switch_orientation, extends_horizontal=True),
)
self.menu_extends.add_command(
label="vertical",
underline=0,
command=partial(self.switch_orientation, extends_horizontal=False),
)
self.menu_extends.entryconfig(
0 if _configuration.extends_horizontal else 1, state="disabled"
)
# layout/themes
self.menu_themes = tk.Menu(self.menu_layout, tearoff=0)
self.menu_layout.add_cascade(menu=self.menu_themes, label="Themes")
self.menu_themes.add_command(
label="light", command=partial(self.load_theme, "light")
)
self.menu_themes.add_command(
label="dark", command=partial(self.load_theme, "dark")
)
self.menu_themes.entryconfig(
0 if self.app_config["theme"]["mode"] == "light" else 1,
state="disabled",
)
if not _configuration.themes_enabled:
self.entryconfig(6, state="disabled")
# vban connect menu
self.menu_vban = tk.Menu(self, tearoff=0)
self.add_cascade(menu=self.menu_vban, label="VBAN")
if self.vban_config:
for i, _ in enumerate(self.vban_config):
setattr(self, f"menu_vban_{i+1}", tk.Menu(self.menu_vban, tearoff=0))
target_menu = getattr(self, f"menu_vban_{i+1}")
self.menu_vban.add_cascade(
menu=target_menu,
label=f"{self.vban_config[f'connection-{i+1}']['streamname']}",
underline=0,
)
target_menu.add_command(
label="Connect", command=partial(self.vban_connect, i)
)
target_menu.add_command(
label="Disconnect", command=partial(self.vban_disconnect, i)
)
target_menu.entryconfig(1, state="disabled")
else:
self.entryconfig(3, state="disabled")
# Help menu
self.menu_help = tk.Menu(self, tearoff=0)
@ -178,20 +183,13 @@ class Menus(tk.Menu):
@property
def target(self):
"""use the correct interface"""
return self._parent.target
return self.parent.target
@property
def configuration_app(self):
return configuration["app"]
@configuration_app.setter
def configuration_app(self, val):
self.configuration_app = val
@property
def configuration_vban(self):
if "vban" in configuration:
return configuration["vban"]
def enable_vban_menus(self):
[
self.menu_vban.entryconfig(j, state="normal")
for j, _ in enumerate(self.menu_vban.winfo_children())
]
def action_invoke_voicemeeter(self, cmd):
getattr(self.target.command, cmd)()
@ -210,11 +208,10 @@ class Menus(tk.Menu):
self.target.apply_profile("base")
def always_on_top(self):
self._parent.attributes("-topmost", self._is_topmost.get())
self._parent.update()
self.parent.attributes("-topmost", self._is_topmost.get())
def switch_orientation(self, extends_horizontal: bool = True, *args):
_base_vals.extends_horizontal = extends_horizontal
_configuration.extends_horizontal = extends_horizontal
if extends_horizontal:
self.menu_extends.entryconfig(0, state="disabled")
self.menu_extends.entryconfig(1, state="normal")
@ -223,17 +220,17 @@ class Menus(tk.Menu):
self.menu_extends.entryconfig(0, state="normal")
def set_submix(self, i):
if _base_vals.submixes != i:
_base_vals.submixes = i
if self._parent.submix_frame is not None:
self._parent.submix_frame.destroy()
self._parent.nav_frame.show_submix()
if _base_values.submixes != i:
_base_values.submixes = i
if self.parent.submix_frame is not None:
self.parent.submix_frame.teardown()
self.parent.nav_frame.show_submix()
for j, var in enumerate(self._selected_bus):
var.set(True if i == j else False)
def load_theme(self, theme):
sv_ttk.set_theme(theme)
self.configuration_app["theme"]["mode"] = theme
self.app_config["theme"]["mode"] = theme
self.menu_themes.entryconfig(
0,
state=f"{'disabled' if theme == 'light' else 'normal'}",
@ -268,50 +265,42 @@ class Menus(tk.Menu):
]
opts = {}
opts |= self.configuration_vban[f"connection-{i+1}"]
opts |= self.vban_config[f"connection-{i+1}"]
kind_id = opts.pop("kind")
self.vban_conns[i] = vbancmd.connect(kind_id, **opts)
self.vban = vbancmd.connect(kind_id, **opts)
# login to vban interface
self.vban_conns[i].login()
self.vban.login()
# destroy the current App frames
self._parent._destroy_top_level_frames()
_base_vals.vban_connected = True
self.parent._destroy_top_level_frames()
_base_values.vban_connected = True
# build new app frames according to a kind
kind = kind_get(kind_id)
self._parent._make_app(kind, self.vban_conns[i])
self.parent.build_app(kind, self.vban)
target_menu = getattr(self, f"menu_vban_{i+1}")
target_menu.entryconfig(0, state="disabled")
target_menu.entryconfig(1, state="normal")
self.menu_layout.entryconfig(
2, state=f"{'normal' if kind.name == 'Potato' else 'disabled'}"
0, state=f"{'normal' if kind.name == 'Potato' else 'disabled'}"
)
def vban_disconnect(self, i):
# destroy the current App frames
self._parent._destroy_top_level_frames()
_base_vals.vban_connected = False
self.parent._destroy_top_level_frames()
_base_values.vban_connected = False
# logout of vban interface
i_to_close = self.vban_conns[i]
self.vban_conns[i] = None
i_to_close.logout()
self.vban.logout()
# build new app frames according to a kind
kind = kind_get(self._vmr.type)
self._parent._make_app(kind, None)
kind = kind_get(self.vmr.type)
self.parent.build_app(kind, None)
target_menu = getattr(self, f"menu_vban_{i+1}")
target_menu.entryconfig(0, state="normal")
target_menu.entryconfig(1, state="disabled")
self.menu_layout.entryconfig(
2, state=f"{'normal' if kind.name == 'Potato' else 'disabled'}"
0, state=f"{'normal' if kind.name == 'Potato' else 'disabled'}"
)
self.after(15000, self.enable_vban_menus)
def enable_vban_menus(self):
[
self.menu_vban.entryconfig(j, state="normal")
for j, _ in enumerate(self.menu_vban.winfo_children())
]
def documentation(self):
webbrowser.open_new(r"https://voicemeeter.com/")

View File

@ -1,156 +1,87 @@
import tkinter as tk
from tkinter import ttk
from .channels import ChannelFrame
from . import builders
from .data import _configuration
from .gainlayer import SubMixFrame
from .data import _base_vals
class Navigation(ttk.Frame):
def __init__(self, parent):
super().__init__(parent)
self._parent = parent
self.s = parent.styletable
self.parent = parent
self.grid(row=0, column=3, sticky=(tk.W, tk.E))
self.styletable = self.parent.styletable
self.submix = tk.BooleanVar()
self.channel = tk.BooleanVar()
self.extend = tk.BooleanVar()
self.info = tk.BooleanVar()
self.builder = builders.NavigationFrameBuilder(self)
self.builder.setup()
self.builder.create_submix_button()
self.builder.create_channel_button()
self.builder.create_extend_button()
self.builder.create_info_button()
self.builder.grid_configure()
self.channel_text = tk.StringVar()
self.channel_text.set(parent.channel_frame.identifier.upper())
self.extend_text = tk.StringVar()
self.extend_text.set("EXTEND")
self.info_text = tk.StringVar()
self._parent.submix_frame = None
self._make_widgets()
self.col_row_configure()
def _make_widgets(self):
"""Creates the navigation buttons"""
self.submix_button = ttk.Checkbutton(
self,
text="SUBMIX",
command=self.show_submix,
style=f"{'Toggle.TButton' if _base_vals.using_theme else f'Submix.TButton'}",
variable=self.submix,
)
self.channel_button = ttk.Checkbutton(
self,
textvariable=self.channel_text,
command=self.switch_channel,
style=f"{'Toggle.TButton' if _base_vals.using_theme else f'Channel.TButton'}",
variable=self.channel,
)
self.extend_button = ttk.Checkbutton(
self,
textvariable=self.extend_text,
command=self.extend_frame,
style=f"{'Toggle.TButton' if _base_vals.using_theme else f'Extend.TButton'}",
variable=self.extend,
)
self.info_button = ttk.Checkbutton(
self,
textvariable=self.info_text,
style=f"{'Toggle.TButton' if _base_vals.using_theme else f'Rec.TButton'}",
variable=self.info,
)
self.info_button["state"] = "active"
""" Position navigation buttons """
self.submix_button.grid(column=0, row=0)
self.channel_button.grid(column=0, row=1, rowspan=1)
self.extend_button.grid(column=0, row=2)
self.info_button.grid(column=0, row=3)
if self._parent.kind.name != "Potato":
self.submix_button["state"] = "disabled"
self.mainframebuilder = builders.MainFrameBuilder(self.parent)
def show_submix(self):
if self.submix.get():
if _base_vals.extends_horizontal:
self._parent.submix_frame = SubMixFrame(self._parent)
self._parent.submix_frame.grid(row=0, column=2)
if self._parent.bus_frame:
self._parent.bus_frame.grid_remove()
self.parent.submix_frame = SubMixFrame(self.parent)
else:
self._parent.submix_frame = SubMixFrame(self._parent)
self._parent.submix_frame.grid(row=2, column=0, sticky=(tk.W))
if self._parent.bus_frame:
self._parent.bus_frame.grid_remove()
if _configuration.extends_horizontal:
self.parent.submix_frame.teardown()
if self.parent.bus_frame:
self.parent.bus_frame.grid()
else:
if _base_vals.extends_horizontal:
self._parent.submix_frame.destroy()
if self._parent.bus_frame:
self._parent.bus_frame.grid()
self.parent.columnconfigure(1, weight=0)
else:
self._parent.columnconfigure(1, weight=0)
self.parent.submix_frame.teardown()
if self.parent.bus_frame:
self.parent.bus_frame.grid()
else:
self._parent.submix_frame.destroy()
if self._parent.bus_frame:
self._parent.bus_frame.grid()
else:
self._parent.rowconfigure(2, weight=0, minsize=0)
self.parent.rowconfigure(2, weight=0, minsize=0)
if not _base_vals.using_theme:
self.s.configure(
if not _configuration.themes_enabled:
self.styletable.configure(
f"Submix.TButton",
background=f'{"purple" if self.submix.get() else "white"}',
)
def switch_channel(self):
if self.channel_text.get() == "STRIP":
self._parent.bus_frame = ChannelFrame.make_buses(self._parent)
self._parent.bus_frame.grid(row=0, column=0)
self._parent.channel_frame.destroy()
self.mainframebuilder.create_channelframe("bus")
self.parent.strip_frame.teardown()
else:
self._parent.channel_frame = ChannelFrame.make_strips(self._parent)
self._parent.channel_frame.grid(row=0, column=0)
self._parent.bus_frame.destroy()
self.mainframebuilder.create_channelframe("strip")
self.parent.bus_frame.teardown()
self.extend_button["state"] = (
"disabled" if self.channel_text.get() == "STRIP" else "normal"
)
[frame.destroy() for frame in self._parent.configframes]
[frame.teardown() for frame in self.parent.configframes]
self.channel_text.set("BUS" if self.channel_text.get() == "STRIP" else "STRIP")
def extend_frame(self):
_configuration.extended = self.extend.get()
if self.extend.get():
self.channel_button["state"] = "disabled"
self._parent.bus_frame = ChannelFrame.make_buses(self._parent)
if _base_vals.extends_horizontal:
self._parent.bus_frame.grid(row=0, column=2)
else:
self._parent.bus_frame.grid(row=2, column=0, sticky=(tk.W))
self.mainframebuilder.create_channelframe("bus")
else:
[
frame.destroy()
for frame in self._parent.configframes
frame.teardown()
for frame in self.parent.configframes
if "!busconfig" in str(frame)
]
self._parent.bus_frame.destroy()
self._parent.bus_frame = None
self.parent.bus_frame.teardown()
self.parent.bus_frame = None
self.channel_button["state"] = "normal"
if self._parent.submix_frame:
self._parent.submix_frame.destroy()
if self.parent.submix_frame:
self.parent.submix_frame.teardown()
self.submix.set(False)
if not _base_vals.using_theme:
self.s.configure(
if not _configuration.themes_enabled:
self.styletable.configure(
f"Submix.TButton",
background=f'{"purple" if self.submix.get() else "white"}',
)
self.extend_text.set("REDUCE" if self.extend.get() else "EXTEND")
def col_row_configure(self):
[
child.grid_configure(padx=1, pady=1, sticky=(tk.N, tk.S, tk.W, tk.E))
for child in self.winfo_children()
if isinstance(child, ttk.Checkbutton)
]
self.rowconfigure(1, minsize=self._parent.channel_frame.height - 18)
self.grid(sticky=(tk.N))

32
vmcompact/subject.py Normal file
View File

@ -0,0 +1,32 @@
class Subject:
def __init__(self):
"""list of current observers"""
self._observables = []
def notify(self, modifier=None):
"""Alert the observers"""
for observer in self._observables:
observer.update()
def add(self, observer):
"""adds an observer to observables"""
if observer not in self._observables:
self._observables.append(observer)
def remove(self, observer):
"""removes an observer from observables"""
try:
self._observables.remove(observer)
except ValueError:
pass
def get(self) -> list:
"""returns the current observables"""
return self._observables
def clear(self):
self._observables.clear()