voicemeeter-compact/vmcompact/app.py

212 lines
6.2 KiB
Python
Raw Normal View History

import logging
2022-04-11 18:35:28 +01:00
import tkinter as tk
from functools import cached_property
from pathlib import Path
from tkinter import messagebox, ttk
2022-04-11 18:35:28 +01:00
from typing import NamedTuple
import voicemeeterlib
from .builders import MainFrameBuilder
from .configurations import loader
from .data import _base_values, _configuration, _kinds_all, get_configuration
2023-06-26 13:55:50 +01:00
from .errors import VMCompactError
2022-04-11 18:35:28 +01:00
from .menu import Menus
from .subject import Subject
2022-04-11 18:35:28 +01:00
logger = logging.getLogger(__name__)
2022-04-11 18:35:28 +01:00
class App(tk.Tk):
"""App mainframe"""
2022-04-11 18:35:28 +01:00
@classmethod
def make(cls, kind: NamedTuple):
"""
Factory function for App.
2022-04-11 18:35:28 +01:00
Returns an App class of a kind.
2022-04-11 18:35:28 +01:00
"""
2022-04-11 18:35:28 +01:00
APP_cls = type(
2025-01-15 20:56:37 +00:00
f'Voicemeeter{kind}.Compact',
2022-04-11 18:35:28 +01:00
(cls,),
{
2025-01-15 20:56:37 +00:00
'kind': kind,
2022-04-11 18:35:28 +01:00
},
)
return APP_cls
def __init__(self, vmr):
super().__init__()
self.logger = logger.getChild(self.__class__.__name__)
self._vmr = vmr
2025-01-15 20:56:37 +00:00
self._vmr.event.add(['pdirty', 'ldirty'])
self.subject = Subject()
self.start_updates()
2023-06-26 13:55:50 +01:00
self._vmr.init_thread()
2025-01-15 20:56:37 +00:00
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
2025-01-15 20:56:37 +00:00
self.protocol('WM_DELETE_WINDOW', self.on_close_window)
self.menu = self['menu'] = Menus(self, vmr)
2022-04-11 18:35:28 +01:00
self.styletable = ttk.Style()
if _configuration.config:
vmr.apply_config(_configuration.config)
2022-04-11 18:35:28 +01:00
self.build_app()
2022-04-11 18:35:28 +01:00
2025-01-15 20:56:37 +00:00
self.drag_id = ''
self.bind('<Configure>', self.dragging)
2022-04-11 18:35:28 +01:00
self.after(1, self.healthcheck_step)
2023-06-26 13:55:50 +01:00
def __str__(self):
2025-01-15 20:56:37 +00:00
return f'{type(self).__name__}App'
2023-06-26 13:55:50 +01:00
2022-04-11 18:35:28 +01:00
@property
def target(self):
"""returns the current interface"""
return self._vban if _base_values.vban_connected else self._vmr
2022-04-11 18:35:28 +01:00
@property
def configframes(self):
"""returns the current configframes"""
return (
2022-04-11 18:35:28 +01:00
frame
for frame in self.winfo_children()
if isinstance(frame, ttk.Frame)
2025-01-15 20:56:37 +00:00
and '!stripconfig' in str(frame)
or '!busconfig' in str(frame)
2022-04-11 18:35:28 +01:00
)
def build_app(self, kind=None, vban=None):
"""builds the app frames according to a kind"""
2022-04-11 18:35:28 +01:00
self._vban = vban
if kind:
self.kind = kind
2023-06-26 13:56:06 +01:00
# register event callbacks
2023-06-26 13:55:50 +01:00
self.target.subject.add([self.on_pdirty, self.on_ldirty])
2022-04-11 18:35:28 +01:00
self.bus_frame = None
self.submix_frame = None
self.builder = MainFrameBuilder(self)
self.builder.setup()
2025-01-15 20:56:37 +00:00
self.builder.create_channelframe('strip')
self.builder.create_separator()
self.builder.create_navframe()
if _configuration.extended:
2022-04-11 18:35:28 +01:00
self.nav_frame.extend.set(True)
self.nav_frame.extend_frame()
2025-01-15 20:56:37 +00:00
if self.kind.name == 'potato':
self.builder.create_banner()
2023-06-26 13:55:50 +01:00
def on_pdirty(self):
if _base_values.run_update:
2025-01-15 20:56:37 +00:00
self.after(1, self.subject.notify, 'pdirty')
2023-06-26 13:55:50 +01:00
def on_ldirty(self):
if not _base_values.dragging:
2025-01-15 20:56:37 +00:00
self.after(1, self.subject.notify, 'ldirty')
2022-04-11 18:35:28 +01:00
def _destroy_top_level_frames(self):
"""
Clear observables.
Deregister app as observer.
Destroy all top level frames.
"""
2023-06-30 04:27:10 +01:00
self.target.subject.remove([self.on_pdirty, self.on_ldirty])
self.subject.clear()
2022-04-11 18:35:28 +01:00
[
frame.destroy()
for frame in self.winfo_children()
if isinstance(frame, ttk.Frame)
]
def dragging(self, event, *args):
if event.widget is self:
2025-01-15 20:56:37 +00:00
if self.drag_id == '':
_base_values.dragging = True
2022-04-11 18:35:28 +01:00
else:
self.after_cancel(self.drag_id)
self.drag_id = self.after(100, self.stop_drag)
def stop_drag(self):
2025-01-15 20:56:37 +00:00
self.drag_id = ''
_base_values.dragging = False
2022-04-11 18:35:28 +01:00
@cached_property
def userconfigs(self):
self._configs = loader(self.kind.name, self.target)
return self._configs
def start_updates(self):
def init():
2025-01-15 20:56:37 +00:00
self.logger.debug('updates started')
_base_values.run_update = True
if self._vmr.gui.launched_by_api:
2025-01-15 20:56:37 +00:00
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:
2025-01-15 20:56:37 +00:00
resp = messagebox.askyesno(message='Restart Voicemeeter GUI?')
if resp:
self.logger.debug(
2025-01-15 20:56:37 +00:00
'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)
2025-01-15 20:56:37 +00:00
vban_config = get_configuration('vban')
for i, _ in enumerate(vban_config):
2025-01-15 20:56:37 +00:00
target = getattr(self.menu, f'menu_vban_{i + 1}')
target.entryconfig(0, state='normal')
target.entryconfig(1, state='disabled')
[
2025-01-15 20:56:37 +00:00
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()
2022-04-11 18:35:28 +01:00
_apps = {kind.name: App.make(kind) for kind in _kinds_all}
2022-04-11 18:35:28 +01:00
def connect(kind_id: str, vmr) -> App:
"""return App of the kind requested"""
2022-04-11 18:35:28 +01:00
try:
VMMIN_cls = _apps[kind_id]
except KeyError:
2025-01-15 20:56:37 +00:00
raise VMCompactError(f'Invalid kind: {kind_id}')
return VMMIN_cls(vmr)