Source code for babase._stringedit

# Released under the MIT License. See LICENSE for details.
#
"""Functionality for editing text strings.

This abstracts native edit dialogs as well as ones implemented via our
own ui toolkits.
"""

from __future__ import annotations

import time
import logging
import weakref
from typing import TYPE_CHECKING, final

from efro.util import empty_weakref

import _babase

if TYPE_CHECKING:
    pass


[docs] class StringEditSubsystem: """Full string-edit state for the app. Access the single shared instance of this class via the :attr:`~babase.App.stringedit` attr on the :class:`~babase.App` class. """ def __init__(self) -> None: self.active_adapter = empty_weakref(StringEditAdapter)
[docs] class StringEditAdapter: """Represents a string editing operation on some object. Editable objects such as text widgets or in-app-consoles can subclass this to make their contents editable on all platforms. There can only be one string-edit at a time for the app. New string-edits will attempt to register themselves as the globally active one in their constructor, but this may not succeed. If :meth:`can_be_replaced()` returns ``True`` for an adapter immediately after creating it, that means it was not able to set itself as the global one. """ def __init__( self, description: str, initial_text: str, max_length: int | None, screen_space_center: tuple[float, float] | None, ) -> None: if not _babase.in_logic_thread(): raise RuntimeError('This must be called from the logic thread.') self.create_time = time.monotonic() # Note: these attr names are hard-coded in C++ code so don't # change them willy-nilly. self.description = description self.initial_text = initial_text self.max_length = max_length self.screen_space_center = screen_space_center # Attempt to register ourself as the active edit. subsys = _babase.app.stringedit current_edit = subsys.active_adapter() if current_edit is None or current_edit.can_be_replaced(): subsys.active_adapter = weakref.ref(self)
[docs] @final def can_be_replaced(self) -> bool: """Return whether this adapter can be replaced by a new one. This is mainly a safeguard to allow adapters whose drivers have gone away without calling :meth:`apply` or :meth:`cancel` to time out and be replaced with new ones. """ if not _babase.in_logic_thread(): raise RuntimeError('This must be called from the logic thread.') # Allow ourself to be replaced after a bit. if time.monotonic() - self.create_time > 5.0: if _babase.do_once(): logging.warning( 'StringEditAdapter can_be_replaced() check for %s' ' yielding True due to timeout; ideally this should' ' not be possible as the StringEditAdapter driver' ' should be blocking anything else from kicking off' ' new edits.', self, ) return True # We also are always considered replaceable if we're not the # active global adapter. current_edit = _babase.app.stringedit.active_adapter() if current_edit is not self: return True return False
[docs] @final def apply(self, new_text: str) -> None: """Should be called by the owner when editing is complete. Note that in some cases this call may be a no-op (such as if this adapter is no longer the globally active one). """ if not _babase.in_logic_thread(): raise RuntimeError('This must be called from the logic thread.') # Make sure whoever is feeding this adapter is honoring max-length. if self.max_length is not None and len(new_text) > self.max_length: logging.warning( 'apply() on %s was passed a string of length %d,' ' but adapter max_length is %d; this should not happen' ' (will truncate).', self, len(new_text), self.max_length, stack_info=True, ) new_text = new_text[: self.max_length] self._do_apply(new_text)
[docs] @final def cancel(self) -> None: """Should be called by the owner when editing is cancelled.""" if not _babase.in_logic_thread(): raise RuntimeError('This must be called from the logic thread.') self._do_cancel()
def _do_apply(self, new_text: str) -> None: """Should be overridden by subclasses to handle apply. Will always be called in the logic thread. """ raise NotImplementedError('Subclasses must override this.') def _do_cancel(self) -> None: """Should be overridden by subclasses to handle cancel. Will always be called in the logic thread. """ raise NotImplementedError('Subclasses must override this.')
# 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