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(15000, 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')