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

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
self.kind = kind
self.strip_levels = self.target.strip_levels
self.bus_levels = self.target.bus_levels
if kind:
self.kind = kind
# 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,
)
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 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"}',
)
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)
):
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.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,
def update(self):
"""update levels"""
vals = (
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())
)
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.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,
vals = (
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)))
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",
)
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
]
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)
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)
return getattr(self.target, param)
def setter(self, param, value):
if param in dir(self.target):
setattr(self.target, param, value)
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:
self.sync()
self.after(_base_vals.pdelay, self.watch_pdirty_step)
def update(self):
self.sync()
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)
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)
# on button
if _configuration.themes_enabled:
self.rowconfigure(1, minsize=70)
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)
):
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.level.set(
(
0
if self._parent._parent.channel_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,
def update(self):
"""update levels"""
vals = (
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.strip_frame.strips[self.index].mute.get()
or not self.on.get()
else 100 + (max(vals) - 18) + self.gain.get()
)
)
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()
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()
self.parent.submix_frame = SubMixFrame(self.parent)
else:
if _base_vals.extends_horizontal:
self._parent.submix_frame.destroy()
if self._parent.bus_frame:
self._parent.bus_frame.grid()
if _configuration.extends_horizontal:
self.parent.submix_frame.teardown()
if self.parent.bus_frame:
self.parent.bus_frame.grid()
else:
self._parent.columnconfigure(1, weight=0)
self.parent.columnconfigure(1, weight=0)
else:
self._parent.submix_frame.destroy()
if self._parent.bus_frame:
self._parent.bus_frame.grid()
self.parent.submix_frame.teardown()
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()