mirror of
https://github.com/onyx-and-iris/voicemeeter-compact.git
synced 2026-03-09 11:49:15 +00:00
bus mode button now reads current mode from tkVar (more reliable). bus mono button fixed (possible to set stereo reverse)
434 lines
16 KiB
Python
434 lines
16 KiB
Python
import logging
|
|
import tkinter as tk
|
|
import webbrowser
|
|
from functools import partial
|
|
from tkinter import messagebox
|
|
|
|
import sv_ttk
|
|
import vban_cmd
|
|
from vban_cmd.error import VBANCMDConnectionError
|
|
|
|
from .data import _base_values, _configuration, get_configuration, kind_get
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class Menus(tk.Menu):
|
|
def __init__(self, parent, vmr):
|
|
super().__init__()
|
|
self.parent = parent
|
|
self.vmr = vmr
|
|
self.logger = logger.getChild(self.__class__.__name__)
|
|
self.vban_config = get_configuration('vban')
|
|
self.app_config = get_configuration('app')
|
|
self._is_topmost = tk.BooleanVar()
|
|
self._lock = tk.BooleanVar()
|
|
self._unlock = tk.BooleanVar()
|
|
self._navigation_show = tk.BooleanVar(value=_configuration.navigation_show)
|
|
self._navigation_hide = tk.BooleanVar(value=not _configuration.navigation_show)
|
|
self._selected_bus = list(tk.BooleanVar() for _ in range(8))
|
|
|
|
# voicemeeter menu
|
|
self.menu_voicemeeter = tk.Menu(self, tearoff=0)
|
|
self.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_checkbutton(
|
|
label='Lock',
|
|
onvalue=1,
|
|
offvalue=0,
|
|
variable=self._lock,
|
|
command=partial(self.action_set_voicemeeter, 'lock'),
|
|
)
|
|
self.menu_lock.add_checkbutton(
|
|
label='Unlock',
|
|
onvalue=1,
|
|
offvalue=0,
|
|
variable=self._unlock,
|
|
command=partial(self.action_set_voicemeeter, 'lock', False),
|
|
)
|
|
|
|
# configs menu
|
|
self.menu_configs = tk.Menu(self, tearoff=0)
|
|
self.add_cascade(menu=self.menu_configs, label='Configs')
|
|
self.menu_configs_load = tk.Menu(self.menu_configs, tearoff=0)
|
|
self.menu_configs.add_cascade(menu=self.menu_configs_load, label='Load config')
|
|
self.config_defaults = {'reset'}
|
|
if len(self.parent.userconfigs) > len(self.config_defaults) and all(
|
|
key in self.parent.userconfigs for key in self.config_defaults
|
|
):
|
|
[
|
|
self.menu_configs_load.add_command(
|
|
label=profile, command=partial(self.load_profile, profile)
|
|
)
|
|
for profile in self.parent.userconfigs.keys()
|
|
if profile not in self.config_defaults
|
|
]
|
|
else:
|
|
self.menu_configs.entryconfig(0, state='disabled')
|
|
self.menu_configs.add_command(
|
|
label='Reset to defaults', command=self.load_defaults
|
|
)
|
|
|
|
# layout menu
|
|
self.menu_layout = tk.Menu(self, tearoff=0)
|
|
self.add_cascade(menu=self.menu_layout, label='Layout')
|
|
# 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)
|
|
)
|
|
self.menu_submixes = tk.Menu(self.menu_layout, tearoff=0)
|
|
self.menu_layout.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[_configuration.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.menu_layout.entryconfig(2, state='disabled')
|
|
# layout/navigation
|
|
self.menu_navigation = tk.Menu(self.menu_layout, tearoff=0)
|
|
self.menu_layout.add_cascade(menu=self.menu_navigation, label='Navigation')
|
|
self.menu_navigation.add_checkbutton(
|
|
label='show',
|
|
onvalue=1,
|
|
offvalue=0,
|
|
variable=self._navigation_show,
|
|
command=partial(self.toggle_navigation, 'show'),
|
|
)
|
|
self.menu_navigation.add_checkbutton(
|
|
label='hide',
|
|
onvalue=1,
|
|
offvalue=0,
|
|
variable=self._navigation_hide,
|
|
command=partial(self.toggle_navigation, 'hide'),
|
|
)
|
|
|
|
# vban connect menu
|
|
self.menu_vban = tk.Menu(self, tearoff=0)
|
|
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(4, state='disabled')
|
|
|
|
# Help menu
|
|
self.menu_help = tk.Menu(self, tearoff=0)
|
|
self.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
|
|
|
|
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):
|
|
if fn := getattr(self.target.command, cmd):
|
|
fn()
|
|
if cmd == 'shutdown':
|
|
self.parent.on_close_window()
|
|
|
|
def action_set_voicemeeter(self, cmd, val=True):
|
|
if cmd == 'lock':
|
|
self._lock.set(val)
|
|
self._unlock.set(not self._lock.get())
|
|
setattr(self.target.command, cmd, val)
|
|
|
|
def load_custom_profile(self, profile):
|
|
self.logger.info(f'loading user profile {profile}')
|
|
self.target.apply(profile)
|
|
if not _base_values.run_update:
|
|
self.parent.subject.notify('pdirty')
|
|
|
|
def load_profile(self, profile):
|
|
self.logger.info(f'loading user profile {profile}')
|
|
self.target.apply_config(profile)
|
|
if not _base_values.run_update:
|
|
self.parent.subject.notify('pdirty')
|
|
|
|
def load_defaults(self):
|
|
msg = (
|
|
'Are you sure you want to Reset values to defaults?',
|
|
'Physical strips B1, Virtual strips A1',
|
|
'Mono, Solo, Mute, EQ all OFF',
|
|
'Gain sliders for Strip/Bus at 0.0',
|
|
)
|
|
resp = messagebox.askyesno(message='\n'.join(msg))
|
|
if resp:
|
|
self.load_profile('reset')
|
|
|
|
def always_on_top(self):
|
|
self.parent.attributes('-topmost', self._is_topmost.get())
|
|
|
|
def switch_orientation(self, extends_horizontal: bool = True, *args):
|
|
_configuration.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 _configuration.submixes != i:
|
|
_configuration.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(i == j)
|
|
self.parent.subject.notify('submix')
|
|
|
|
def load_theme(self, theme):
|
|
sv_ttk.set_theme(theme)
|
|
_configuration.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.winfo_children()
|
|
if isinstance(menu, tk.Menu)
|
|
]
|
|
self.menu_lock.config(bg=f'{"black" if theme == "dark" else "white"}')
|
|
self.menu_configs_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)
|
|
]
|
|
[
|
|
menu.config(bg=f'{"black" if theme == "dark" else "white"}')
|
|
for menu in self.menu_layout.winfo_children()
|
|
if isinstance(menu, tk.Menu)
|
|
]
|
|
self.logger.info(
|
|
f'Finished loading theme Sunvalley {sv_ttk.get_theme().capitalize()} theme'
|
|
)
|
|
|
|
def menu_teardown(self, i):
|
|
# remove config load menus
|
|
if len(self.parent.userconfigs) > len(self.config_defaults):
|
|
for profile in self.parent.userconfigs:
|
|
if profile not in self.config_defaults:
|
|
try:
|
|
self.menu_configs_load.delete(profile)
|
|
except tk._tkinter.tclError as e:
|
|
self.logger.warning(f'{type(e).__name__}: {e}')
|
|
|
|
[
|
|
self.menu_vban.entryconfig(j, state='disabled')
|
|
for j, _ in enumerate(self.menu_vban.winfo_children())
|
|
if j != i
|
|
]
|
|
|
|
def menu_setup(self):
|
|
if len(self.parent.userconfigs) > len(self.config_defaults):
|
|
for profile in self.parent.userconfigs:
|
|
if profile not in self.config_defaults:
|
|
self.menu_configs_load.add_command(
|
|
label=profile, command=partial(self.load_profile, profile)
|
|
)
|
|
self.menu_configs.entryconfig(0, state='normal')
|
|
else:
|
|
self.menu_configs.entryconfig(0, state='disabled')
|
|
|
|
def toggle_navigation(self, cmd=None):
|
|
if cmd == 'show':
|
|
self.logger.debug('show navframe')
|
|
self.parent.nav_frame.grid()
|
|
self._navigation_show.set(True)
|
|
self._navigation_hide.set(not self._navigation_show.get())
|
|
else:
|
|
self.logger.debug('hide navframe')
|
|
self.parent.nav_frame.grid_remove()
|
|
self._navigation_hide.set(True)
|
|
self._navigation_show.set(not self._navigation_hide.get())
|
|
|
|
def vban_connect(self, i):
|
|
opts = {}
|
|
opts |= self.vban_config[f'connection-{i + 1}']
|
|
kind_id = opts.pop('kind')
|
|
self.vban = vban_cmd.api(kind_id, **opts)
|
|
# login to vban interface
|
|
try:
|
|
self.logger.info(f'Attempting vban connection to {opts.get("ip")}')
|
|
self.vban.login()
|
|
except VBANCMDConnectionError as e:
|
|
self.vban.logout()
|
|
msg = (
|
|
f'Timeout attempting to establish connection to {opts.get("ip")}',
|
|
'Please check your connection settings',
|
|
)
|
|
messagebox.showerror('Connection Error', '\n'.join(msg))
|
|
msg = (str(e), 'resuming local connection')
|
|
self.logger.error(', '.join(msg))
|
|
self.after(1, self.enable_vban_menus)
|
|
return
|
|
self.menu_teardown(i)
|
|
self.vban.event.add(['pdirty', 'ldirty'])
|
|
# destroy the current App frames
|
|
self.parent._destroy_top_level_frames()
|
|
_base_values.vban_connected = True
|
|
self.vmr.end_thread()
|
|
|
|
# build new app frames according to a kind
|
|
kind = kind_get(kind_id)
|
|
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(
|
|
0, state=f'{"normal" if kind.name == "potato" else "disabled"}'
|
|
)
|
|
# ensure the configs are reloaded into memory
|
|
if 'config' in self.parent.target.__dict__:
|
|
del self.parent.target.__dict__['config']
|
|
if 'userconfigs' in self.parent.__dict__:
|
|
del self.parent.__dict__['userconfigs']
|
|
self.menu_setup()
|
|
|
|
def vban_disconnect(self, i):
|
|
self.menu_teardown(i)
|
|
|
|
# destroy the current App frames
|
|
self.parent._destroy_top_level_frames()
|
|
_base_values.vban_connected = False
|
|
# logout of vban interface
|
|
self.vmr.init_thread()
|
|
self.vban.logout()
|
|
# build new app frames according to a kind
|
|
kind = kind_get(self.vmr.type)
|
|
self.parent.build_app(kind)
|
|
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(
|
|
0, state=f'{"normal" if kind.name == "potato" else "disabled"}'
|
|
)
|
|
# ensure the configs are reloaded into memory
|
|
if 'config' in self.parent.target.__dict__:
|
|
del self.parent.target.__dict__['config']
|
|
if 'userconfigs' in self.parent.__dict__:
|
|
del self.parent.__dict__['userconfigs']
|
|
self.menu_setup()
|
|
|
|
self.after(50, self.enable_vban_menus)
|
|
|
|
def documentation(self):
|
|
webbrowser.open_new(r'https://voicemeeter.com/')
|
|
|
|
def github(self):
|
|
webbrowser.open_new(r'https://github.com/onyx-and-iris/voicemeeter-compact')
|
|
|
|
def onyxandiris(self):
|
|
webbrowser.open_new(r'https://onyxandiris.online')
|