# Released under the MIT License. See LICENSE for details.
#
"""Dev-Console functionality."""
from __future__ import annotations
import os
import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING
import _babase
if TYPE_CHECKING:
from typing import Callable, Any, Literal
[docs]
class DevConsoleTab:
"""Base class for a :class:`~babase.DevConsoleSubsystem` tab."""
[docs]
def refresh(self) -> None:
"""Called when the tab should refresh itself.
Overridden by subclasses to implement tab behavior.
"""
[docs]
def request_refresh(self) -> None:
"""The tab can call this to request that it be refreshed."""
_babase.dev_console_request_refresh()
[docs]
def text(
self,
text: str,
pos: tuple[float, float],
*,
h_anchor: Literal['left', 'center', 'right'] = 'center',
h_align: Literal['left', 'center', 'right'] = 'center',
v_align: Literal['top', 'center', 'bottom', 'none'] = 'center',
scale: float = 1.0,
) -> None:
"""Add a button to the tab being refreshed."""
assert _babase.app.devconsole.is_refreshing
_babase.dev_console_add_text(
text, pos[0], pos[1], h_anchor, h_align, v_align, scale
)
[docs]
def python_terminal(self) -> None:
"""Add a Python Terminal to the tab being refreshed."""
assert _babase.app.devconsole.is_refreshing
_babase.dev_console_add_python_terminal()
@property
def width(self) -> float:
"""The current tab width. Only valid during refreshes."""
assert _babase.app.devconsole.is_refreshing
return _babase.dev_console_tab_width()
@property
def height(self) -> float:
"""The current tab height. Only valid during refreshes."""
assert _babase.app.devconsole.is_refreshing
return _babase.dev_console_tab_height()
@property
def base_scale(self) -> float:
"""A scale value based on the app's current :class:`~babase.UIScale`.
Dev-console tabs can manually incorporate this into their UI
sizes and positions if they desire. By default, dev-console tabs
are uniform across all ui-scales.
"""
assert _babase.app.devconsole.is_refreshing
return _babase.dev_console_base_scale()
[docs]
@dataclass
class DevConsoleTabEntry:
"""Represents a distinct tab in the :class:`~babase.DevConsoleSubsystem`."""
name: str
factory: Callable[[], DevConsoleTab]
[docs]
class DevConsoleSubsystem:
"""Subsystem for wrangling the dev-console.
Access the single shared instance of this class via the
:attr:`~babase.App.devconsole` attr on the :class:`~babase.App`
class. The dev-console is a simple always-available UI intended for
use by developers; not end users. Traditionally it is available by
typing a backtick (`) key on a keyboard, but can also be accessed
via an on-screen button (see settings/advanced/dev-tools to enable
said button).
"""
def __init__(self) -> None:
# pylint: disable=cyclic-import
from babase._devconsoletabs import (
DevConsoleTabPython,
DevConsoleTabAppModes,
DevConsoleTabUI,
DevConsoleTabLogging,
DevConsoleTabTest,
)
#: All tabs in the dev-console. Add your own stuff here via
#: plugins or whatnot to customize the console.
self.tabs: list[DevConsoleTabEntry] = [
DevConsoleTabEntry('Python', DevConsoleTabPython),
DevConsoleTabEntry('AppModes', DevConsoleTabAppModes),
DevConsoleTabEntry('UI', DevConsoleTabUI),
DevConsoleTabEntry('Logging', DevConsoleTabLogging),
]
if os.environ.get('BA_DEV_CONSOLE_TEST_TAB', '0') == '1':
self.tabs.append(DevConsoleTabEntry('Test', DevConsoleTabTest))
self.is_refreshing = False
self._tab_instances: dict[str, DevConsoleTab] = {}
def do_refresh_tab(self, tabname: str) -> None:
"""Called by the C++ layer when a tab should be filled out.
:meta private:
"""
assert _babase.in_logic_thread()
# Make noise if we have repeating tab names, as that breaks our
# logic.
if __debug__:
alltabnames = set[str](tabentry.name for tabentry in self.tabs)
if len(alltabnames) != len(self.tabs):
logging.error(
'Duplicate dev-console tab names found;'
' tabs may behave unpredictably.'
)
tab: DevConsoleTab | None = self._tab_instances.get(tabname)
# If we haven't instantiated this tab yet, do so.
if tab is None:
for tabentry in self.tabs:
if tabentry.name == tabname:
tab = self._tab_instances[tabname] = tabentry.factory()
break
if tab is None:
logging.error(
'DevConsole got refresh request for tab'
" '%s' which does not exist.",
tabname,
)
return
self.is_refreshing = True
try:
tab.refresh()
finally:
self.is_refreshing = False
# Docs-generation hack; import some stuff that we likely only forward-declared
# in our actual source code so that docs tools can find it.
from typing import (Coroutine, Any, Literal, Callable,
Generator, Awaitable, Sequence, Self)
import asyncio
from concurrent.futures import Future
from pathlib import Path
from enum import Enum