import logging import tkinter as tk from functools import cached_property from pathlib import Path from tkinter import messagebox, ttk from typing import NamedTuple import voicemeeterlib from voicemeeterlib import kinds from .builders import MainFrameBuilder from .configurations import loader from .data import _base_values, _configuration, get_configuration from .errors import VMCompactError from .menu import Menus from .subject import Subject logger = logging.getLogger(__name__) class App(tk.Tk): """App mainframe""" @classmethod def make(cls, kind: NamedTuple): """ Factory function for App. Returns an App class of a kind. """ APP_cls = type( f'Voicemeeter{kind}.Compact', (cls,), { 'kind': kind, }, ) return APP_cls def __init__(self, vmr, theme): super().__init__() self.logger = logger.getChild(self.__class__.__name__) self._vmr = vmr self._vmr.event.add(['pdirty', 'ldirty']) self.subject = Subject() self.start_updates() self._vmr.init_thread() for pn in ( Path(__file__).parent.resolve() / 'img' / 'cat.ico', Path.cwd() / '_internal' / 'img' / 'cat.ico', ): if pn.is_file(): self.iconbitmap(str(pn)) break self.minsize(275, False) self._configs = None self.protocol('WM_DELETE_WINDOW', self.on_close_window) self.menu = self['menu'] = Menus(self, vmr) self.styletable = ttk.Style() if _configuration.config: vmr.apply_config(_configuration.config) self.build_app() self.drag_id = '' self.bind('', self.dragging) self.after(1, self.healthcheck_step) def __str__(self): return f'{type(self).__name__}App' @property def target(self): """returns the current interface""" return self._vban if _base_values.vban_connected else self._vmr @property def configframes(self): """returns the current configframes""" return ( frame for frame in self.winfo_children() if isinstance(frame, ttk.Frame) and '!stripconfig' in str(frame) or '!busconfig' in str(frame) ) def build_app(self, kind=None, vban=None): """builds the app frames according to a kind""" self._vban = vban if kind: self.kind = kind # register event callbacks self.target.subject.add([self.on_pdirty, self.on_ldirty]) self.bus_frame = None self.submix_frame = None self.builder = MainFrameBuilder(self) self.builder.setup() self.builder.create_channelframe('strip') self.builder.create_separator() self.builder.create_navframe() if _configuration.extended: self.nav_frame.extend.set(True) self.nav_frame.extend_frame() if self.kind.name == 'potato': self.builder.create_banner() def on_pdirty(self): if _base_values.run_update: self.after(1, self.subject.notify, 'pdirty') def on_ldirty(self): if not _base_values.dragging: self.after(1, self.subject.notify, 'ldirty') def _destroy_top_level_frames(self): """ Clear observables. Deregister app as observer. Destroy all top level frames. """ self.target.subject.remove([self.on_pdirty, self.on_ldirty]) self.subject.clear() [ frame.destroy() for frame in self.winfo_children() if isinstance(frame, ttk.Frame) ] def dragging(self, event, *args): if event.widget is self: if self.drag_id == '': _base_values.dragging = True else: self.after_cancel(self.drag_id) self.drag_id = self.after(100, self.stop_drag) def stop_drag(self): self.drag_id = '' _base_values.dragging = False @cached_property def userconfigs(self): self._configs = loader(self.kind.name, self.target) return self._configs def start_updates(self): def init(): self.logger.debug('updates started') _base_values.run_update = True if self._vmr.gui.launched_by_api: self.subject.notify('pdirty') self.after(12000, init) else: init() def healthcheck_step(self): if not _base_values.vban_connected: try: self._vmr.version except voicemeeterlib.error.CAPIError: resp = messagebox.askyesno(message='Restart Voicemeeter GUI?') if resp: self.logger.debug( 'healthcheck failed, rebuilding the app after GUI restart.' ) self._vmr.end_thread() self._vmr.run_voicemeeter(self._vmr.kind.name) _base_values.run_update = False self._vmr.init_thread() self.after(8000, self.start_updates) self._destroy_top_level_frames() self.build_app(self._vmr.kind) vban_config = get_configuration('vban') for i, _ in enumerate(vban_config): target = getattr(self.menu, f'menu_vban_{i + 1}') target.entryconfig(0, state='normal') target.entryconfig(1, state='disabled') [ self.menu.menu_vban.entryconfig(j, state='normal') for j, _ in enumerate(self.menu.menu_vban.winfo_children()) ] else: self.destroy() self.after(250, self.healthcheck_step) def on_close_window(self): if _base_values.vban_connected: self._vban.logout() self.destroy() _apps = {kind.name: App.make(kind) for kind in kinds.all} def connect(kind_id: str, vmr, theme=None) -> App: """return App of the kind requested""" try: VMMIN_cls = _apps[kind_id] except KeyError: raise VMCompactError(f'Invalid kind: {kind_id}') return VMMIN_cls(vmr, theme)