initial commit

initial commit
This commit is contained in:
onyx-and-iris 2022-04-11 18:35:28 +01:00
parent 594988aa67
commit 3eec86e5ed
180 changed files with 3140 additions and 0 deletions

1
.gitignore vendored
View File

@ -109,6 +109,7 @@ venv/
ENV/ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
venv_vmcompact/
# Spyder project settings # Spyder project settings
.spyderproject .spyderproject

12
__main__.py Normal file
View File

@ -0,0 +1,12 @@
import voicemeeter
import vmcompact
if __name__ == "__main__":
kind_id = "banana"
voicemeeter.launch(kind_id, hide=False)
with voicemeeter.remote(kind_id) as vmr:
app = vmcompact.connect(kind_id, vmr)
app.mainloop()

15
configs/app.toml Normal file
View File

@ -0,0 +1,15 @@
# load with themes enabled? set the default mode
[theme]
enabled=true
mode="light"
# load in extended mode? if so which orientation
[extends]
extended=true
extends_horizontal=false
# default dimensions for channel label frames
[channel]
width=80
height=130
# default submix bus
[submixes]
default=6

12
configs/vban.toml Normal file
View File

@ -0,0 +1,12 @@
# example connections
[connection-1]
kind = 'banana'
ip = '<ip address 1>'
streamname = 'Command1'
port = 6990
[connection-2]
kind = 'potato'
ip = '<ip address 2>'
streamname = 'Command1'
port = 6990

View File

@ -0,0 +1,30 @@
extends = 'blank'
[strip-0]
label = "PhysStrip0"
[strip-1]
label = "PhysStrip1"
[strip-2]
label = "PhysStrip2"
[strip-3]
label = "VirtStrip0"
[strip-4]
label = "VirtStrip1"
[bus-0]
label = "PhysBus0"
[bus-1]
label = "PhysBus1"
[bus-2]
label = "PhysBus2"
[bus-3]
label = "VirtBus0"
[bus-4]
label = "VirtBus1"

View File

@ -0,0 +1,15 @@
extends = 'blank'
[strip-0]
label = "PhysStrip0"
[strip-1]
label = "PhysStrip1"
[strip-2]
label = "VirtStrip0"
[bus-0]
label = "PhysBus0"
[bus-1]
label = "PhysBus1"

View File

@ -0,0 +1,48 @@
extends = 'blank'
[strip-0]
label = "PhysStrip0"
[strip-1]
label = "PhysStrip1"
[strip-2]
label = "PhysStrip2"
[strip-3]
label = "PhysStrip3"
[strip-4]
label = "PhysStrip4"
[strip-5]
label = "VirtStrip0"
[strip-6]
label = "VirtStrip1"
[strip-7]
label = "VirtStrip2"
[bus-0]
label = "PhysBus0"
[bus-1]
label = "PhysBus1"
[bus-2]
label = "PhysBus2"
[bus-3]
label = "PhysBus3"
[bus-4]
label = "PhysBus4"
[bus-5]
label = "VirtBus0"
[bus-6]
label = "VirtBus1"
[bus-7]
label = "VirtBus2"

25
setup.py Normal file
View File

@ -0,0 +1,25 @@
import setuptools
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
setuptools.setup(
name='vmcompact',
version='0.0.1',
author='Onyx and Iris',
author_email='code@onyxandiris.online',
description='Compact Tkinter Voicemeeter Remote App',
long_description=long_description,
long_description_content_type="text/markdown",
url='https://github.com/onyx-and-iris/voicemeeter-compact',
project_urls = {
"Bug Tracker": "https://github.com/onyx-and-iris/voicemeeter-compact/issues"
},
license='MIT',
packages=['vmcompact'],
install_requires=[
'toml',
'voicemeeter@git+https://github.com/onyx-and-iris/voicemeeter-api-python#egg=voicemeeter',
'vbancmd@git+https://github.com/onyx-and-iris/vban-cmd-python#egg=vbancmd',
],
)

3
vmcompact/__init__.py Normal file
View File

@ -0,0 +1,3 @@
from .app import connect
__ALL__ = ["connect"]

177
vmcompact/app.py Normal file
View File

@ -0,0 +1,177 @@
import tkinter as tk
from tkinter import ttk
from typing import NamedTuple
from functools import partial
from pathlib import Path
from .errors import VMCompactErrors
from .data import _base_vals, _kinds_all
from .channels import ChannelFrame
from .navigation import Navigation
from .menu import Menus
from .banner import Banner
from .configurations import configuration
class App(tk.Tk):
"""Topmost Level of App"""
@classmethod
def make(cls, kind: NamedTuple):
"""
Factory function for App
Returns an App class of a kind
"""
APP_cls = type(
f"Voicemeeter{kind.name}.Compact",
(cls,),
{
"kind": kind,
},
)
return APP_cls
def __init__(self, vmr):
super().__init__()
defaults = {
"theme": {
"mode": "light",
},
"extends": {
"extended": False,
"extends_horizontal": True,
},
}
self.configuration = defaults | self.configuration
_base_vals.themes_enabled = self.configuration["theme"]["enabled"]
_base_vals.extends_horizontal = self.configuration["extends"][
"extends_horizontal"
]
_base_vals.submixes = self.configuration["submixes"]["default"]
# create menus
self.menus = Menus(self, vmr)
self.styletable = ttk.Style()
self._vmr = vmr
# start pdirty watcher
self.upd_pdirty()
self.resizable(False, False)
if _base_vals.themes_enabled:
self.apply_theme()
self._make_app(self.kind)
self.drag_id = ""
self.bind("<Configure>", self.dragging)
self.iconbitmap(Path.cwd() / "vmcompact" / "img" / "cat.ico")
@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 configuration(self):
return configuration["app"]
@configuration.setter
def configuration(self, val):
self.configuration["app"] = val
@property
def configframes(self):
"""returns a tuple of current config frame addresses"""
return tuple(
frame
for frame in self.winfo_children()
if isinstance(frame, ttk.Frame)
and "!stripconfig" in str(frame)
or "!busconfig" in str(frame)
)
def apply_theme(self):
_base_vals.using_theme = True
self.tk.call("source", "./vmcompact/sun-valley-theme/sun-valley.tcl")
self.tk.call("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]'
)
self._vban = vban
self.kind = kind
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.S, tk.N))
self.columnconfigure(1, minsize=15)
# navigation frame
self.nav_frame = Navigation(self)
self.nav_frame.grid(row=0, column=3)
if self.configuration["extends"]["extended"]:
self.nav_frame.extend.set(True)
self.nav_frame.extend_frame()
self.banner = Banner(self)
self.banner.grid(row=4, column=0, columnspan=3)
def _destroy_top_level_frames(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(1, self.upd_pdirty_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
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
self.drag_id = ""
_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)
except KeyError:
raise VMCompactErrors(f"Invalid kind: {kind_id}")

37
vmcompact/banner.py Normal file
View File

@ -0,0 +1,37 @@
import tkinter as tk
from tkinter import ttk
from .data import _base_vals
class Banner(ttk.Frame):
def __init__(self, parent):
super().__init__()
self._parent = parent
self.web = "onyxandiris.online"
self.submix = tk.StringVar()
if self._parent.kind.name == "Potato":
self.submix.set(self.target.bus[_base_vals.submixes].label)
if self._parent.kind.name == "Potato":
self.label = ttk.Label(
self,
text=f"SUBMIX: {self.submix.get().upper()}",
)
self.label.grid(column=0, row=0, sticky=(tk.N, tk.S, tk.W, tk.E))
self.upd_submix()
@property
def target(self):
"""use the correct interface"""
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)
self.label["text"] = f"SUBMIX: {self.submix.get().upper()}"
self.after(100, self.upd_submix_step)

376
vmcompact/channels.py Normal file
View File

@ -0,0 +1,376 @@
import tkinter as tk
from tkinter import ttk
from functools import partial
from .data import _base_vals
from .config import StripConfig, BusConfig
class Channel(ttk.LabelFrame):
"""Base class for a single channel"""
def __init__(self, parent, index, id):
super().__init__(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.sync()
self._make_widgets()
self.col_row_configure()
self.watch_pdirty()
self.watch_levels()
@property
def identifier(self):
return self.id
@property
def target(self):
"""use the correct interface"""
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 toggle_mute(self, *args):
self.target.mute = self.mute.get()
if not _base_vals.using_theme:
self.s.configure(
f"{self.identifier}Mute{self.index}.TButton",
background=f'{"red" if self.mute.get() else "white"}',
)
def reset_gain(self, *args):
self.setter("gain", 0)
self.gain.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_leave(self, *args):
self._parent._parent.nav_frame.info_text.set("")
def scale_press(self, *args):
_base_vals.in_scale_button_1 = True
def scale_release(self, *args):
_base_vals.in_scale_button_1 = False
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 _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)
# 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(1, self.watch_pdirty_step)
def sync(self):
"""sync params with voicemeeter"""
retval = self.getter("label")
if len(retval) > 10:
retval = f"{retval[:8]}.."
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(
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):
self.grid(sticky=(tk.N, tk.S))
[
child.grid_configure(padx=1, pady=1, sticky=(tk.W, tk.E))
for child in self.winfo_children()
if isinstance(child, ttk.Checkbutton)
]
[
child.grid_configure(padx=1, pady=1, sticky=(tk.N, tk.S))
for child in self.winfo_children()
if isinstance(child, ttk.Progressbar) or isinstance(child, ttk.Scale)
]
class Strip(Channel):
"""Concrete class representing a single"""
def __init__(self, parent, index, id):
super().__init__(parent, index, id)
@property
def target(self):
"""use the correct 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:
vals = self.target.levels.prefader
val = vals[0] if vals[0] > vals[1] else vals[0]
self.level.set(val)
self.level.set(
(0 if self.mute.get() else 100 + (val - 18) + self.gain.get())
)
self.after(
25 if not _base_vals.in_scale_button_1 else 100, self.watch_levels_step
)
class Bus(Channel):
"""Concrete bus class representing a single bus"""
def __init__(self, parent, index, id):
super().__init__(parent, index, id)
@property
def target(self):
"""use the correct 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=3)
else:
self.config_frame.grid(column=0, row=3, columnspan=3)
self._parent._parent.channel_frame.reset_config_buttons(self)
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:
vals = self.target.levels.all
val = vals[0] if vals[0] > vals[1] else vals[0]
self.level.set(val)
self.level.set(
(0 if self.mute.get() else 100 + (val - 18) + self.gain.get())
)
self.after(
25 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):
super().__init__(parent)
self._parent = parent
self._is_strip = is_strip
self.phys_in, self.virt_in = parent.kind.ins
self.phys_out, self.virt_out = parent.kind.outs
defaults = {
"width": 80,
"height": 150,
}
self.configuration = defaults | self.configuration
self.width = self.configuration["width"]
self.height = self.configuration["height"]
self.watch_pdirty()
# create labelframes
if is_strip:
self.strips = [
Strip(self, i, self.identifier)
for i in range(self.phys_in + self.virt_in)
]
else:
self.buses = [
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()
@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
@property
def identifier(self):
return "strip" if self._is_strip else "bus"
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
]
def col_row_configure(self):
[
self.columnconfigure(i, minsize=self.width)
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 watch_pdirty_step(self):
if self._parent.pdirty:
self.watch_labels()
self.after(1, self.watch_pdirty_step)
def watch_labels(self):
for i, labelframe in enumerate(self.labelframes):
if not labelframe.getter("label"):
self.columnconfigure(i, minsize=0)
labelframe.grid_remove()
else:
self.columnconfigure(i, minsize=self.width)
labelframe.grid()

458
vmcompact/config.py Normal file
View File

@ -0,0 +1,458 @@
import tkinter as tk
from tkinter import ttk
from functools import partial
from .data import _base_vals
class Config(ttk.Frame):
def __init__(self, parent, index, _id):
super().__init__(parent)
self._parent = parent
self.index = index
self.id = _id
self.s = parent.styletable
self.phys_in, self.virt_in = parent.kind.ins
self.phys_out, self.virt_out = parent.kind.outs
self.watch_pdirty()
@property
def identifier(self):
return self.id
@property
def target(self):
"""returns the current interface"""
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
def scale_leave(self, *args):
_base_vals.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))
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 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(1, self.watch_pdirty_step)
class StripConfig(Config):
def __init__(self, parent, index, _id):
super().__init__(parent, index, _id)
# 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"""
_target = super(StripConfig, self).target
return getattr(_target, self.identifier)[self.index]
def make_row0(self):
# Create sliders
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)
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)
# 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)
# 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 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(
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(
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 sync(self):
[
self.phys_out_params_vars[self.phys_out_params.index(param)].set(
self.getter(param)
)
for param in self.phys_out_params
]
[
self.virt_out_params_vars[self.virt_out_params.index(param)].set(
self.getter(param)
)
for param in self.virt_out_params
]
[
self.param_vars[self.params.index(param)].set(self.getter(param))
for param in self.params
]
if not _base_vals.using_theme:
[
self.s.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(
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(
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)]
self.make_row0()
self.make_row1()
# sync all parameters
self.sync()
self.col_row_configure()
@property
def target(self):
"""returns the current interface"""
_target = super(BusConfig, self).target
return getattr(_target, self.identifier)[self.index]
def make_row0(self):
self._cur_bus_mode = tk.StringVar()
self._cur_bus_mode.set(f"Bus Mode: {self.bus_modes[self.get_current_index()]}")
self.busmode_button = ttk.Button(self, textvariable=self._cur_bus_mode)
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 get_current_index(self):
for mode in self.bus_modes:
if getattr(self.target.mode, mode.lower()):
return self.bus_modes.index(mode)
def rotate_bus_modes_right(self, *args):
current_index = self.get_current_index()
if current_index + 1 < len(self.bus_modes):
next = self.bus_modes[current_index + 1]
else:
next = self.bus_modes[0]
setattr(self.target.mode, next.lower(), True)
self._cur_bus_mode.set(f"Bus Mode: {next}")
def rotate_bus_modes_left(self, *args):
current_index = self.get_current_index()
if current_index == 0:
next = self.bus_modes[-1]
else:
next = self.bus_modes[current_index - 1]
setattr(self.target.mode, next.lower(), True)
self._cur_bus_mode.set(f"Bus Mode: {next}")
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)],
)
for param in self.params
]
[
button.grid(
column=param_buttons.index(button),
row=1,
)
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 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 sync(self):
[
self.param_vars[self.params.index(param)].set(self.getter(param))
for param in self.params
]
if not _base_vals.using_theme:
[
self.s.configure(
f"{param}.TButton",
background=f'{"green" if self.param_vars[self.params.index(param)].get() else "white"}',
)
for param in self.params
]

View File

@ -0,0 +1,20 @@
import toml
from pathlib import Path
configuration = {}
config_path = [Path.cwd() / "configs"]
for path in config_path:
if path.is_dir():
filenames = list(path.glob("*.toml"))
configs = {}
for filename in filenames:
name = filename.with_suffix("").stem
try:
configs[name] = toml.load(filename)
except toml.TomlDecodeError:
print(f"Invalid TOML profile: configs/{filename.stem}")
for name, cfg in configs.items():
print(f"Loaded profile configs/{name}")
configuration[name] = cfg

35
vmcompact/data.py Normal file
View File

@ -0,0 +1,35 @@
from dataclasses import dataclass
from voicemeeter import kinds
@dataclass
class BaseValues:
level_height: int = 100
level_width: int = 80
# 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
_base_vals = BaseValues()
_kinds = {kind.id: kind for kind in kinds.all}
_kinds_all = _kinds.values()
def kind_get(kind_id):
return _kinds[kind_id]

4
vmcompact/errors.py Normal file
View File

@ -0,0 +1,4 @@
class VMCompactErrors(Exception):
"""Base classs for VMCompact Errors"""
pass

244
vmcompact/gainlayer.py Normal file
View File

@ -0,0 +1,244 @@
import tkinter as tk
from tkinter import ttk, messagebox as msg
from functools import partial
from .data import _base_vals
class GainLayer(ttk.LabelFrame):
"""Concrete class representing a single gainlayer"""
def __init__(self, parent, index, j):
super().__init__(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.sync()
self._make_widgets()
self.col_row_configure()
self.watch_pdirty()
self.watch_levels()
@property
def target(self):
"""returns the current interface"""
_target = self._parent.target
return _target.strip[self.index].gainlayer[self.j]
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 reset_gain(self, *args):
self.setter("gain", 0)
self.gain.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_leave(self, *args):
self._parent._parent.nav_frame.info_text.set("")
def scale_press(self, *args):
_base_vals.in_scale_button_1 = True
def scale_release(self, *args):
_base_vals.in_scale_button_1 = False
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 set_on(self):
"""enables a gainlayer. sets its button colour"""
setattr(
self._parent.target.strip[self.index],
self._parent.buses[self.j],
self.on.get(),
)
if not _base_vals.using_theme:
self.s.configure(
f"On.TButton",
background=f'{"green" if self.on.get() else "white"}',
)
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)
# 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):
[
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)
]
[
child.grid_configure(padx=1, pady=1, sticky=(tk.N, tk.S))
for child in self.winfo_children()
if isinstance(child, ttk.Progressbar) or isinstance(child, ttk.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(1, self.watch_pdirty_step)
def sync(self):
"""sync params with voicemeeter"""
retval = self.getter("label")
if len(retval) > 10:
retval = f"{retval[:8]}.."
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],
)
)
def watch_levels(self):
self.after(1, self.watch_levels_step)
def watch_levels_step(self):
if not _base_vals.dragging:
vals = self._parent.target.strip[self.index].levels.prefader
val = vals[0] if vals[0] > vals[1] else vals[0]
self.level.set(val)
self.level.set(
(
0
if self._parent._parent.channel_frame.strips[self.index].mute.get()
else 100 + (val - 18) + self.gain.get()
)
)
self.after(
25 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.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.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"):
self.columnconfigure(i, minsize=0)
gainlayer.grid_remove()
self.watch_pdirty()
@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
def col_row_configure(self):
[
self.columnconfigure(i, minsize=self.width)
for i, _ in enumerate(self.gainlayers)
]
[self.rowconfigure(0, minsize=130) for i, _ in enumerate(self.gainlayers)]
def watch_pdirty(self):
self.after(1, self.watch_pdirty_step)
def watch_pdirty_step(self):
if self._parent.pdirty:
self.watch_labels()
self.after(1, 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()

BIN
vmcompact/img/cat.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

316
vmcompact/menu.py Normal file
View File

@ -0,0 +1,316 @@
import tkinter as tk
from tkinter import ttk, messagebox
from functools import partial
import webbrowser
import vbancmd
from .configurations import configuration
from .data import _base_vals, kind_get
from .gainlayer import SubMixFrame
class Menus(tk.Menu):
def __init__(self, parent, vmr):
super().__init__()
self._parent = parent
self._vmr = vmr
self.vban_conns = [None for i, _ in enumerate(self.configuration_vban)]
self.menubar = tk.Menu(self, tearoff=0)
parent["menu"] = self.menubar
self._is_topmost = tk.BooleanVar()
self._selected_bus = list(tk.BooleanVar() for _ in range(8))
# voicemeeter menu
self.menu_voicemeeter = tk.Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(menu=self.menu_voicemeeter, label="Voicemeeter")
self.menu_voicemeeter.add_checkbutton(
label="Always On Top",
onvalue=1,
offvalue=0,
variable=self._is_topmost,
command=self.always_on_top,
)
self.menu_voicemeeter.add_separator()
self.menu_voicemeeter.add_command(
label="Show",
underline=0,
command=partial(self.action_invoke_voicemeeter, "show"),
)
self.menu_voicemeeter.add_command(
label="Hide",
underline=0,
command=partial(self.action_invoke_voicemeeter, "hide"),
)
self.menu_voicemeeter.add_command(
label="Restart",
underline=0,
command=partial(self.action_invoke_voicemeeter, "restart"),
)
self.menu_voicemeeter.add_command(
label="Shutdown",
underline=0,
command=partial(self.action_invoke_voicemeeter, "shutdown"),
)
self.menu_voicemeeter.add_separator()
self.menu_lock = tk.Menu(self.menu_voicemeeter, tearoff=0)
self.menu_voicemeeter.add_cascade(
menu=self.menu_lock, label="GUI Lock", underline=0
)
self.menu_lock.add_command(
label="Lock", command=partial(self.action_set_voicemeeter, "lock")
)
self.menu_lock.add_command(
label="Unlock", command=partial(self.action_set_voicemeeter, "lock", False)
)
# profiles menu
menu_profiles = tk.Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(menu=menu_profiles, label="Profiles")
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
):
[
self.menu_profiles_load.add_command(
label=profile, command=partial(self.load_profile, profile)
)
for profile in vmr.profiles.keys()
if profile not in defaults
]
else:
menu_profiles.entryconfig(0, state="disabled")
menu_profiles.add_separator()
menu_profiles.add_command(label="Reset to defaults", command=self.load_defaults)
# vban connect menu
self.menu_vban = tk.Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(menu=self.menu_vban, label="VBAN Connect")
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")
# extends menu
self.menu_extends = tk.Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(menu=self.menu_extends, label="Extends")
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"
)
# submixes menu
# 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))
self.menu_submixes = tk.Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(menu=self.menu_submixes, label="Submixes")
[
self.menu_submixes.add_checkbutton(
label=f"Bus {buses[i]}",
underline=0,
onvalue=1,
offvalue=0,
variable=self._selected_bus[i],
command=partial(self.set_submix, i),
)
for i in range(8)
]
self._selected_bus[_base_vals.submixes].set(True)
if self._parent.kind.name != "Potato":
self.menubar.entryconfig(4, state="disabled")
# themes menu
self.menu_themes = tk.Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(menu=self.menu_themes, label="Themes")
tcl_themes = [
"light",
"dark",
]
[
self.menu_themes.add_command(
label=theme.capitalize(),
command=partial(self.load_theme, theme),
)
for theme in tcl_themes
]
self.menu_themes.entryconfig(
0 if self.configuration_app["theme"]["mode"] == "light" else 1,
state="disabled",
)
if not _base_vals.themes_enabled:
self.menubar.entryconfig(6, state="disabled")
# Help menu
self.menu_help = tk.Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(menu=self.menu_help, label="Help")
self.menu_help.add_command(
label="Voicemeeter Site",
command=self.documentation,
)
self.menu_help.add_command(
label="Source Code",
command=self.github,
)
self.menu_help.add_command(
label="App Creator",
command=self.onyxandiris,
)
@property
def target(self):
"""use the correct interface"""
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):
return configuration["vban"]
def action_invoke_voicemeeter(self, cmd):
getattr(self.target.command, cmd)()
def action_set_voicemeeter(self, cmd, val=True):
setattr(self.target.command, cmd, val)
def load_profile(self, profile):
self.target.apply_profile(profile)
def load_defaults(self):
resp = messagebox.askyesno(
message="Are you sure you want to Reset values to defaults?\nPhysical strips B1, Virtual strips A1\nMono, Solo, Mute, EQ all OFF"
)
if resp:
self.target.apply_profile("base")
def always_on_top(self):
self._parent.attributes("-topmost", self._is_topmost.get())
self._parent.update()
def switch_orientation(self, extends_horizontal: bool = True, *args):
_base_vals.extends_horizontal = extends_horizontal
if extends_horizontal:
self.menu_extends.entryconfig(0, state="disabled")
self.menu_extends.entryconfig(1, state="normal")
else:
self.menu_extends.entryconfig(1, state="disabled")
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()
for j, var in enumerate(self._selected_bus):
var.set(True if i == j else False)
def load_theme(self, theme):
self.tk.call("set_theme", theme)
self.configuration_app["theme"]["mode"] = theme
self.menu_themes.entryconfig(
0,
state=f"{'disabled' if theme == 'light' else 'normal'}",
)
self.menu_themes.entryconfig(
1,
state=f"{'disabled' if theme == 'dark' else 'normal'}",
)
[
menu.config(bg=f"{'black' if theme == 'dark' else 'white'}")
for menu in self.menubar.winfo_children()
if isinstance(menu, tk.Menu)
]
self.menu_lock.config(bg=f"{'black' if theme == 'dark' else 'white'}")
self.menu_profiles_load.config(bg=f"{'black' if theme == 'dark' else 'white'}")
[
menu.config(bg=f"{'black' if theme == 'dark' else 'white'}")
for menu in self.menu_vban.winfo_children()
if isinstance(menu, tk.Menu)
]
def vban_connect(self, i):
[
self.menu_vban.entryconfig(j, state="disabled")
for j, _ in enumerate(self.menu_vban.winfo_children())
if j != i
]
opts = {}
opts |= self.configuration_vban[f"connection-{i+1}"]
kind_id = opts.pop("kind")
self.vban_conns[i] = vbancmd.connect(kind_id, **opts)
# login to vban interface
self.vban_conns[i].login()
# destroy the current App frames
self._parent._destroy_top_level_frames()
_base_vals.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])
target_menu = getattr(self, f"menu_vban_{i+1}")
target_menu.entryconfig(0, state="disabled")
target_menu.entryconfig(1, state="normal")
self.menubar.entryconfig(
4, 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
# logout of vban interface
i_to_close = self.vban_conns[i]
self.vban_conns[i] = None
i_to_close.logout()
# build new app frames according to a kind
kind = kind_get(self._vmr.type)
# destroy the current App frames
self._parent._destroy_top_level_frames()
self._parent._make_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.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/")
def github(self):
webbrowser.open_new(r"https://github.com/onyx-and-iris")
def onyxandiris(self):
webbrowser.open_new(r"https://onyxandiris.online")

156
vmcompact/navigation.py Normal file
View File

@ -0,0 +1,156 @@
import tkinter as tk
from tkinter import ttk
from .channels import ChannelFrame
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.submix = tk.BooleanVar()
self.channel = tk.BooleanVar()
self.extend = tk.BooleanVar()
self.info = tk.BooleanVar()
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"
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()
else:
if _base_vals.extends_horizontal:
self._parent.submix_frame.destroy()
if self._parent.bus_frame:
self._parent.bus_frame.grid()
else:
self._parent.columnconfigure(1, weight=0)
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)
if not _base_vals.using_theme:
self.s.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()
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.extend_button["state"] = (
"disabled" if self.channel_text.get() == "STRIP" else "normal"
)
[frame.destroy() for frame in self._parent.configframes]
self.channel_text.set("BUS" if self.channel_text.get() == "STRIP" else "STRIP")
def extend_frame(self):
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))
else:
[
frame.destroy()
for frame in self._parent.configframes
if "!busconfig" in str(frame)
]
self._parent.bus_frame.destroy()
self._parent.bus_frame = None
self.channel_button["state"] = "normal"
if self._parent.submix_frame:
self._parent.submix_frame.destroy()
self.submix.set(False)
if not _base_vals.using_theme:
self.s.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))

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 rdbende
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,88 @@
# Copyright © 2021 rdbende <rdbende@gmail.com>
source [file join [file dirname [info script]] theme light.tcl]
source [file join [file dirname [info script]] theme dark.tcl]
option add *tearOff 0
proc set_theme {mode} {
if {$mode == "dark"} {
ttk::style theme use "sun-valley-dark"
array set colors {
-fg "#ffffff"
-bg "#1c1c1c"
-disabledfg "#595959"
-selectfg "#ffffff"
-selectbg "#2f60d8"
}
ttk::style configure . \
-background $colors(-bg) \
-foreground $colors(-fg) \
-troughcolor $colors(-bg) \
-focuscolor $colors(-selectbg) \
-selectbackground $colors(-selectbg) \
-selectforeground $colors(-selectfg) \
-insertwidth 1 \
-insertcolor $colors(-fg) \
-fieldbackground $colors(-selectbg) \
-font {"Segoe UI" 10} \
-borderwidth 0 \
-relief flat
tk_setPalette \
background [ttk::style lookup . -background] \
foreground [ttk::style lookup . -foreground] \
highlightColor [ttk::style lookup . -focuscolor] \
selectBackground [ttk::style lookup . -selectbackground] \
selectForeground [ttk::style lookup . -selectforeground] \
activeBackground [ttk::style lookup . -selectbackground] \
activeForeground [ttk::style lookup . -selectforeground]
ttk::style map . -foreground [list disabled $colors(-disabledfg)]
option add *font [ttk::style lookup . -font]
option add *Menu.selectcolor $colors(-fg)
option add *Menu.background #2f2f2f
} elseif {$mode == "light"} {
ttk::style theme use "sun-valley-light"
array set colors {
-fg "#202020"
-bg "#fafafa"
-disabledfg "#a0a0a0"
-selectfg "#ffffff"
-selectbg "#2f60d8"
}
ttk::style configure . \
-background $colors(-bg) \
-foreground $colors(-fg) \
-troughcolor $colors(-bg) \
-focuscolor $colors(-selectbg) \
-selectbackground $colors(-selectbg) \
-selectforeground $colors(-selectfg) \
-insertwidth 1 \
-insertcolor $colors(-fg) \
-fieldbackground $colors(-selectbg) \
-font {"Segoe UI" 10} \
-borderwidth 0 \
-relief flat
tk_setPalette background [ttk::style lookup . -background] \
foreground [ttk::style lookup . -foreground] \
highlightColor [ttk::style lookup . -focuscolor] \
selectBackground [ttk::style lookup . -selectbackground] \
selectForeground [ttk::style lookup . -selectforeground] \
activeBackground [ttk::style lookup . -selectbackground] \
activeForeground [ttk::style lookup . -selectforeground]
ttk::style map . -foreground [list disabled $colors(-disabledfg)]
option add *font [ttk::style lookup . -font]
option add *Menu.selectcolor $colors(-fg)
option add *Menu.background #e7e7e7
}
}

View File

@ -0,0 +1,521 @@
# Copyright © 2021 rdbende <rdbende@gmail.com>
# A stunning dark theme for ttk based on Microsoft's Sun Valley visual style
package require Tk 8.6
namespace eval ttk::theme::sun-valley-dark {
variable version 1.0
package provide ttk::theme::sun-valley-dark $version
ttk::style theme create sun-valley-dark -parent clam -settings {
proc load_images {imgdir} {
variable images
foreach file [glob -directory $imgdir *.png] {
set images([file tail [file rootname $file]]) \
[image create photo -file $file -format png]
}
}
load_images [file join [file dirname [info script]] dark]
array set colors {
-fg "#ffffff"
-bg "#1c1c1c"
-disabledfg "#595959"
-selectfg "#ffffff"
-selectbg "#2f60d8"
}
ttk::style layout TButton {
Button.button -children {
Button.padding -children {
Button.label -side left -expand 1
}
}
}
ttk::style layout Toolbutton {
Toolbutton.button -children {
Toolbutton.padding -children {
Toolbutton.label -side left -expand 1
}
}
}
ttk::style layout TMenubutton {
Menubutton.button -children {
Menubutton.padding -children {
Menubutton.label -side left -expand 1
Menubutton.indicator -side right -sticky nsew
}
}
}
ttk::style layout TOptionMenu {
OptionMenu.button -children {
OptionMenu.padding -children {
OptionMenu.label -side left -expand 1
OptionMenu.indicator -side right -sticky nsew
}
}
}
ttk::style layout Accent.TButton {
AccentButton.button -children {
AccentButton.padding -children {
AccentButton.label -side left -expand 1
}
}
}
ttk::style layout Titlebar.TButton {
TitlebarButton.button -children {
TitlebarButton.padding -children {
TitlebarButton.label -side left -expand 1
}
}
}
ttk::style layout Close.Titlebar.TButton {
CloseButton.button -children {
CloseButton.padding -children {
CloseButton.label -side left -expand 1
}
}
}
ttk::style layout TCheckbutton {
Checkbutton.button -children {
Checkbutton.padding -children {
Checkbutton.indicator -side left
Checkbutton.label -side right -expand 1
}
}
}
ttk::style layout Switch.TCheckbutton {
Switch.button -children {
Switch.padding -children {
Switch.indicator -side left
Switch.label -side right -expand 1
}
}
}
ttk::style layout Toggle.TButton {
ToggleButton.button -children {
ToggleButton.padding -children {
ToggleButton.label -side left -expand 1
}
}
}
ttk::style layout TRadiobutton {
Radiobutton.button -children {
Radiobutton.padding -children {
Radiobutton.indicator -side left
Radiobutton.label -side right -expand 1
}
}
}
ttk::style layout Vertical.TScrollbar {
Vertical.Scrollbar.trough -sticky ns -children {
Vertical.Scrollbar.uparrow -side top
Vertical.Scrollbar.downarrow -side bottom
Vertical.Scrollbar.thumb -expand 1
}
}
ttk::style layout Horizontal.TScrollbar {
Horizontal.Scrollbar.trough -sticky ew -children {
Horizontal.Scrollbar.leftarrow -side left
Horizontal.Scrollbar.rightarrow -side right
Horizontal.Scrollbar.thumb -expand 1
}
}
ttk::style layout TSeparator {
TSeparator.separator -sticky nsew
}
ttk::style layout TCombobox {
Combobox.field -sticky nsew -children {
Combobox.padding -expand 1 -sticky nsew -children {
Combobox.textarea -sticky nsew
}
}
null -side right -sticky ns -children {
Combobox.arrow -sticky nsew
}
}
ttk::style layout TSpinbox {
Spinbox.field -sticky nsew -children {
Spinbox.padding -expand 1 -sticky nsew -children {
Spinbox.textarea -sticky nsew
}
}
null -side right -sticky nsew -children {
Spinbox.uparrow -side left -sticky nsew
Spinbox.downarrow -side right -sticky nsew
}
}
ttk::style layout Card.TFrame {
Card.field {
Card.padding -expand 1
}
}
ttk::style layout TLabelframe {
Labelframe.border {
Labelframe.padding -expand 1 -children {
Labelframe.label -side left
}
}
}
ttk::style layout TNotebook {
Notebook.border -children {
TNotebook.Tab -expand 1
Notebook.client -sticky nsew
}
}
ttk::style layout Treeview.Item {
Treeitem.padding -sticky nsew -children {
Treeitem.image -side left -sticky {}
Treeitem.indicator -side left -sticky {}
Treeitem.text -side left -sticky {}
}
}
# Button
ttk::style configure TButton -padding {8 4} -anchor center -foreground $colors(-fg)
ttk::style map TButton -foreground \
[list disabled #7a7a7a \
pressed #d0d0d0]
ttk::style element create Button.button image \
[list $images(button-rest) \
{selected disabled} $images(button-disabled) \
disabled $images(button-disabled) \
selected $images(button-rest) \
pressed $images(button-pressed) \
active $images(button-hover) \
] -border 4 -sticky nsew
# Toolbutton
ttk::style configure Toolbutton -padding {8 4} -anchor center
ttk::style element create Toolbutton.button image \
[list $images(empty) \
{selected disabled} $images(button-disabled) \
selected $images(button-rest) \
pressed $images(button-pressed) \
active $images(button-hover) \
] -border 4 -sticky nsew
# Menubutton
ttk::style configure TMenubutton -padding {8 4 0 4}
ttk::style element create Menubutton.button \
image [list $images(button-rest) \
disabled $images(button-disabled) \
pressed $images(button-pressed) \
active $images(button-hover) \
] -border 4 -sticky nsew
ttk::style element create Menubutton.indicator image $images(arrow-down) -width 28 -sticky {}
# OptionMenu
ttk::style configure TOptionMenu -padding {8 4 0 4}
ttk::style element create OptionMenu.button \
image [list $images(button-rest) \
disabled $images(button-disabled) \
pressed $images(button-pressed) \
active $images(button-hover) \
] -border 4 -sticky nsew
ttk::style element create OptionMenu.indicator image $images(arrow-down) -width 28 -sticky {}
# Accent.TButton
ttk::style configure Accent.TButton -padding {8 4} -anchor center -foreground #000000
ttk::style map Accent.TButton -foreground \
[list pressed #25536a \
disabled #a5a5a5]
ttk::style element create AccentButton.button image \
[list $images(button-accent-rest) \
{selected disabled} $images(button-accent-disabled) \
disabled $images(button-accent-disabled) \
selected $images(button-accent-rest) \
pressed $images(button-accent-pressed) \
active $images(button-accent-hover) \
] -border 4 -sticky nsew
# Titlebar.TButton
ttk::style configure Titlebar.TButton -padding {8 4} -anchor center -foreground #ffffff
ttk::style map Titlebar.TButton -foreground \
[list disabled #6f6f6f \
pressed #d1d1d1 \
active #ffffff]
ttk::style element create TitlebarButton.button image \
[list $images(empty) \
disabled $images(empty) \
pressed $images(button-titlebar-pressed) \
active $images(button-titlebar-hover) \
] -border 4 -sticky nsew
# Close.Titlebar.TButton
ttk::style configure Close.Titlebar.TButton -padding {8 4} -anchor center -foreground #ffffff
ttk::style map Close.Titlebar.TButton -foreground \
[list disabled #6f6f6f \
pressed #e8bfbb \
active #ffffff]
ttk::style element create CloseButton.button image \
[list $images(empty) \
disabled $images(empty) \
pressed $images(button-close-pressed) \
active $images(button-close-hover) \
] -border 4 -sticky nsew
# Checkbutton
ttk::style configure TCheckbutton -padding 4
ttk::style element create Checkbutton.indicator image \
[list $images(check-unsel-rest) \
{alternate disabled} $images(check-tri-disabled) \
{selected disabled} $images(check-disabled) \
disabled $images(check-unsel-disabled) \
{pressed alternate} $images(check-tri-hover) \
{active alternate} $images(check-tri-hover) \
alternate $images(check-tri-rest) \
{pressed selected} $images(check-hover) \
{active selected} $images(check-hover) \
selected $images(check-rest) \
{pressed !selected} $images(check-unsel-pressed) \
active $images(check-unsel-hover) \
] -width 26 -sticky w
# Switch.TCheckbutton
ttk::style element create Switch.indicator image \
[list $images(switch-off-rest) \
{selected disabled} $images(switch-on-disabled) \
disabled $images(switch-off-disabled) \
{pressed selected} $images(switch-on-pressed) \
{active selected} $images(switch-on-hover) \
selected $images(switch-on-rest) \
{pressed !selected} $images(switch-off-pressed) \
active $images(switch-off-hover) \
] -width 46 -sticky w
# Toggle.TButton
ttk::style configure Toggle.TButton -padding {8 4 8 4} -anchor center -foreground $colors(-fg)
ttk::style map Toggle.TButton -foreground \
[list {selected disabled} #a5a5a5 \
{selected pressed} #d0d0d0 \
selected #000000 \
pressed #25536a \
disabled #7a7a7a
]
ttk::style element create ToggleButton.button image \
[list $images(button-rest) \
{selected disabled} $images(button-accent-disabled) \
disabled $images(button-disabled) \
{pressed selected} $images(button-rest) \
{active selected} $images(button-accent-hover) \
selected $images(button-accent-rest) \
{pressed !selected} $images(button-accent-rest) \
active $images(button-hover) \
] -border 4 -sticky nsew
# Radiobutton
ttk::style configure TRadiobutton -padding 4
ttk::style element create Radiobutton.indicator image \
[list $images(radio-unsel-rest) \
{selected disabled} $images(radio-disabled) \
disabled $images(radio-unsel-disabled) \
{pressed selected} $images(radio-pressed) \
{active selected} $images(radio-hover) \
selected $images(radio-rest) \
{pressed !selected} $images(radio-unsel-pressed) \
active $images(radio-unsel-hover) \
] -width 26 -sticky w
# Scrollbar
ttk::style element create Horizontal.Scrollbar.trough image $images(scroll-hor-trough) -sticky ew -border 6
ttk::style element create Horizontal.Scrollbar.thumb image $images(scroll-hor-thumb) -sticky ew -border 3
ttk::style element create Horizontal.Scrollbar.rightarrow image $images(scroll-right) -sticky {} -width 12
ttk::style element create Horizontal.Scrollbar.leftarrow image $images(scroll-left) -sticky {} -width 12
ttk::style element create Vertical.Scrollbar.trough image $images(scroll-vert-trough) -sticky ns -border 6
ttk::style element create Vertical.Scrollbar.thumb image $images(scroll-vert-thumb) -sticky ns -border 3
ttk::style element create Vertical.Scrollbar.uparrow image $images(scroll-up) -sticky {} -height 12
ttk::style element create Vertical.Scrollbar.downarrow image $images(scroll-down) -sticky {} -height 12
# Scale
ttk::style element create Horizontal.Scale.trough image $images(scale-trough-hor) \
-border 5 -padding 0
ttk::style element create Vertical.Scale.trough image $images(scale-trough-vert) \
-border 5 -padding 0
ttk::style element create Scale.slider \
image [list $images(scale-thumb-rest) \
disabled $images(scale-thumb-disabled) \
pressed $images(scale-thumb-pressed) \
active $images(scale-thumb-hover) \
] -sticky {}
# Progressbar
ttk::style element create Horizontal.Progressbar.trough image $images(progress-trough-hor) \
-border 1 -sticky ew
ttk::style element create Horizontal.Progressbar.pbar image $images(progress-pbar-hor) \
-border 2 -sticky ew
ttk::style element create Vertical.Progressbar.trough image $images(progress-trough-vert) \
-border 1 -sticky ns
ttk::style element create Vertical.Progressbar.pbar image $images(progress-pbar-vert) \
-border 2 -sticky ns
# Entry
ttk::style configure TEntry -foreground $colors(-fg)
ttk::style map TEntry -foreground \
[list disabled #757575 \
pressed #cfcfcf
]
ttk::style element create Entry.field \
image [list $images(entry-rest) \
{focus hover !invalid} $images(entry-focus) \
invalid $images(entry-invalid) \
disabled $images(entry-disabled) \
{focus !invalid} $images(entry-focus) \
hover $images(entry-hover) \
] -border 5 -padding 8 -sticky nsew
# Combobox
ttk::style configure TCombobox -foreground $colors(-fg)
ttk::style map TCombobox -foreground \
[list disabled #757575 \
pressed #cfcfcf
]
ttk::style configure ComboboxPopdownFrame -borderwidth 1 -relief solid
ttk::style map TCombobox -selectbackground [list \
{readonly hover} $colors(-selectbg) \
{readonly focus} $colors(-selectbg) \
] -selectforeground [list \
{readonly hover} $colors(-selectfg) \
{readonly focus} $colors(-selectfg) \
]
ttk::style element create Combobox.field \
image [list $images(entry-rest) \
{readonly disabled} $images(button-disabled) \
{readonly pressed} $images(button-pressed) \
{readonly hover} $images(button-hover) \
readonly $images(button-rest) \
invalid $images(entry-invalid) \
disabled $images(entry-disabled) \
focus $images(entry-focus) \
hover $images(entry-hover) \
] -border 5 -padding {8 8 28 8}
ttk::style element create Combobox.arrow image $images(arrow-down) -width 35 -sticky {}
# Spinbox
ttk::style configure TSpinbox -foreground $colors(-fg)
ttk::style map TSpinbox -foreground \
[list disabled #757575 \
pressed #cfcfcf
]
ttk::style element create Spinbox.field \
image [list $images(entry-rest) \
invalid $images(entry-invalid) \
disabled $images(entry-disabled) \
focus $images(entry-focus) \
hover $images(entry-hover) \
] -border 5 -padding {8 8 54 8} -sticky nsew
ttk::style element create Spinbox.uparrow image $images(arrow-up) -width 35 -sticky {}
ttk::style element create Spinbox.downarrow image $images(arrow-down) -width 35 -sticky {}
# Sizegrip
ttk::style element create Sizegrip.sizegrip image $images(sizegrip) \
-sticky nsew
# Separator
ttk::style element create TSeparator.separator image $images(separator)
# Card
ttk::style element create Card.field image $images(card) \
-border 10 -padding 4 -sticky nsew
# Labelframe
ttk::style element create Labelframe.border image $images(card) \
-border 5 -padding 4 -sticky nsew
# Notebook
ttk::style configure TNotebook -padding 1
ttk::style element create Notebook.border \
image $images(notebook-border) -border 5 -padding 5
ttk::style element create Notebook.client image $images(notebook)
ttk::style element create Notebook.tab \
image [list $images(tab-rest) \
selected $images(tab-selected) \
active $images(tab-hover) \
] -border 13 -padding {16 14 16 6} -height 32
# Treeview
ttk::style element create Treeview.field image $images(card) \
-border 5
ttk::style element create Treeheading.cell \
image [list $images(treeheading-rest) \
pressed $images(treeheading-pressed) \
active $images(treeheading-hover)
] -border 5 -padding 15 -sticky nsew
ttk::style element create Treeitem.indicator \
image [list $images(arrow-right) \
user2 $images(empty) \
user1 $images(arrow-down) \
] -width 26 -sticky {}
ttk::style configure Treeview -background $colors(-bg) -rowheight [expr {[font metrics font -linespace] + 2}]
ttk::style map Treeview \
-background [list selected #292929] \
-foreground [list selected $colors(-selectfg)]
# Panedwindow
# Insane hack to remove clam's ugly sash
ttk::style configure Sash -gripcount 0
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 771 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Some files were not shown because too many files have changed in this diff Show More