"""Kivy application entry point for the touchscreen mixer UI. Provides a ScreenManager with four screens: - Mixer surface (faders, meters, mute/solo) - Routing matrix - Plugin chain - Settings (brightness, timeout, DPI) Designed for Raspberry Pi 5"/7" touch displays. Runs fullscreen at native resolution with DPI-aware layout. Usage: python3 main_touch.py # connect to localhost:8000 python3 main_touch.py --host 192.168.1.10 # remote mixer KIVY_DPI=220 python3 main_touch.py # override DPI Environment: MIXER_HOST — mixer server hostname (default 127.0.0.1) MIXER_PORT — REST API port (default 8000) MIXER_OSC_PORT — OSC server port (default 9001) """ from __future__ import annotations import os import sys import logging from kivy.app import App from kivy.clock import Clock from kivy.config import Config from kivy.core.window import Window from kivy.properties import ObjectProperty from kivy.uix.boxlayout import BoxLayout from kivy.uix.button import Button from kivy.uix.label import Label from kivy.uix.screenmanager import ScreenManager, Screen, FadeTransition, SlideTransition from kivy.metrics import dp from .ipc import MixerClient from .theme import Colors, Fonts, Sizes from .screens.mixer import MixerScreen from .screens.routing import RoutingScreen from .screens.plugins import PluginScreen from .screens.settings import SettingsScreen from .screens.sessions import SessionsScreen logger = logging.getLogger(__name__) # ── Touch-optimized Kivy config ────────────────────────────────────────────── def _configure_kivy(): """Apply touch-optimized Kivy settings before window creation.""" Config.set("input", "mouse", "mouse,disable_multitouch") Config.set("kivy", "window_icon", "") Config.set("kivy", "exit_on_escape", 0) # No accidental exit Config.set("graphics", "fullscreen", "auto") Config.set("graphics", "show_cursor", 0) # Hide cursor on touchscreen Config.set("graphics", "resizable", 0) # Fixed size for embedded # Allow environment overrides if os.environ.get("KIVY_SHOW_CURSOR"): Config.set("graphics", "show_cursor", 1) # ── Tab bar ────────────────────────────────────────────────────────────────── class TabBar(BoxLayout): """Bottom tab bar for screen switching. Four tabs: Mixer, Routing, Plugins, Settings. Touch-optimized: large hit targets, clear active indicator. """ screen_manager = ObjectProperty(None) def __init__(self, **kwargs): super().__init__(**kwargs) self.orientation = "horizontal" self.size_hint = (1, None) self.height = Sizes.TAB_BAR_HEIGHT self.spacing = 0 self._tabs: dict[str, Button] = {} self._active_tab = "mixer" tabs = [ ("mixer", "MIX"), ("routing", "RTG"), ("plugins", "PLG"), ("sessions", "SES"), ("settings", "SET"), ] for name, label in tabs: btn = Button( text=label, background_normal="", background_color=Colors.BG_HEADER, color=Colors.ACCENT if name == "mixer" else Colors.TEXT_MUTED, font_size=Fonts.SMALL, bold=True, ) btn.bind(on_press=self._make_switch(name)) self._tabs[name] = btn self.add_widget(btn) def _make_switch(self, screen_name: str): def cb(instance): if self.screen_manager: self.screen_manager.current = screen_name self._set_active(screen_name) return cb def _set_active(self, name: str): self._active_tab = name for tab_name, btn in self._tabs.items(): if tab_name == name: btn.color = Colors.ACCENT btn.font_size = Fonts.BODY else: btn.color = Colors.TEXT_MUTED btn.font_size = Fonts.SMALL def switch_to(self, name: str): """External switch (e.g., from gesture).""" self._set_active(name) if self.screen_manager: self.screen_manager.current = name # ── Root widget ────────────────────────────────────────────────────────────── class MixerRoot(BoxLayout): """Root layout: header + screen area + tab bar.""" def __init__(self, **kwargs): super().__init__(**kwargs) self.orientation = "vertical" # Status bar (top) self._status_bar = BoxLayout( orientation="horizontal", size_hint=(1, None), height=Sizes.HEADER_HEIGHT, padding=[Sizes.PAD_MD, Sizes.PAD_XS], ) self._status_bar.bg_color = Colors.BG_HEADER self._status_label = Label( text="● Connected", color=Colors.ACTIVE, font_size=Fonts.SMALL, size_hint=(None, 1), width=dp(120), halign="left", ) self._status_label.bind(size=self._status_label.setter("text_size")) self._time_label = Label( text="", color=Colors.TEXT_MUTED, font_size=Fonts.SMALL, size_hint=(1, 1), halign="right", ) self._status_bar.add_widget(self._status_label) self._status_bar.add_widget(self._time_label) # Screen manager (center) self.screen_manager = ScreenManager(transition=FadeTransition(duration=0.2)) # Screens self.mixer_screen = MixerScreen(name="mixer") self.routing_screen = RoutingScreen(name="routing") self.plugin_screen = PluginScreen(name="plugins") self.sessions_screen = SessionsScreen(name="sessions") self.settings_screen = SettingsScreen(name="settings") self.screen_manager.add_widget(self.mixer_screen) self.screen_manager.add_widget(self.routing_screen) self.screen_manager.add_widget(self.plugin_screen) self.screen_manager.add_widget(self.sessions_screen) self.screen_manager.add_widget(self.settings_screen) # Tab bar (bottom) self.tab_bar = TabBar(screen_manager=self.screen_manager) self.add_widget(self._status_bar) self.add_widget(self.screen_manager) self.add_widget(self.tab_bar) def set_client(self, client: MixerClient): """Inject the mixer client into all screens.""" self.mixer_screen.client = client self.routing_screen.client = client self.plugin_screen.client = client self.sessions_screen.client = client self.settings_screen.client = client def update_status(self, connected: bool, info: str = ""): """Update the status bar.""" if connected: self._status_label.text = "● Connected" + (f" {info}" if info else "") self._status_label.color = Colors.ACTIVE else: self._status_label.text = "○ Disconnected" self._status_label.color = Colors.MUTE_ON # ── Kivy App ───────────────────────────────────────────────────────────────── class MixerApp(App): """Touchscreen mixer application. Args to build(): host: Mixer server hostname. rest_port: REST API port. osc_port: OSC server port. api_key: API key for authentication. """ client: MixerClient = None # type: ignore def __init__(self, host="127.0.0.1", rest_port=8000, osc_port=9001, api_key="", **kwargs): super().__init__(**kwargs) self._host = host self._rest_port = rest_port self._osc_port = osc_port self._api_key = api_key def build(self): """Build the UI and return the root widget.""" _configure_kivy() # Set window properties Window.clearcolor = Colors.BG_DARK Window.bind(on_key_down=self._on_key_down) self.client = MixerClient( host=self._host, rest_port=self._rest_port, osc_port=self._osc_port, api_key=self._api_key, ) self.root_widget = MixerRoot() self.root_widget.set_client(self.client) # Periodic health check Clock.schedule_interval(self._health_check, 5.0) # Update clock Clock.schedule_interval(self._update_clock, 1.0) return self.root_widget def _health_check(self, dt): """Check mixer server connectivity.""" self.client.fetch_state( on_success=lambda state: self.root_widget.update_status(True), on_error=lambda e: self.root_widget.update_status(False, str(e)[:20]), ) def _update_clock(self, dt): """Update the time display.""" import time t = time.localtime() self.root_widget._time_label.text = time.strftime("%H:%M:%S", t) def _on_key_down(self, window, key, scancode, codepoint, modifier): """Handle keyboard shortcuts.""" # ESC to toggle between screens if key == 27: # ESC sm = self.root_widget.screen_manager screens = sm.screen_names idx = screens.index(sm.current) next_idx = (idx + 1) % len(screens) sm.current = screens[next_idx] self.root_widget.tab_bar.switch_to(screens[next_idx]) return True return False def on_stop(self): """Cleanup on app exit.""" if self.client: self.client.close()