Source code for Karana.KUtils.MultibodyTUI.mode

# Copyright (c) 2024-2026 Karana Dynamics Pty Ltd. All rights reserved.
#
# NOTICE TO USER:
#
# This source code and/or documentation (the "Licensed Materials") is
# the confidential and proprietary information of Karana Dynamics Inc.
# Use of these Licensed Materials is governed by the terms and conditions
# of a separate software license agreement between Karana Dynamics and the
# Licensee ("License Agreement"). Unless expressly permitted under that
# agreement, any reproduction, modification, distribution, or disclosure
# of the Licensed Materials, in whole or in part, to any third party
# without the prior written consent of Karana Dynamics is strictly prohibited.
#
# THE LICENSED MATERIALS ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
# KARANA DYNAMICS DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
# BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, NON-INFRINGEMENT, AND
# FITNESS FOR A PARTICULAR PURPOSE.
#
# IN NO EVENT SHALL KARANA DYNAMICS BE LIABLE FOR ANY DAMAGES WHATSOEVER,
# INCLUDING BUT NOT LIMITED TO LOSS OF PROFITS, DATA, OR USE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGES, WHETHER IN CONTRACT, TORT,
# OR OTHERWISE ARISING OUT OF OR IN CONNECTION WITH THE LICENSED MATERIALS.
#
# U.S. Government End Users: The Licensed Materials are a "commercial item"
# as defined at 48 C.F.R. 2.101, and are provided to the U.S. Government
# only as a commercial end item under the terms of this license.
#
# Any use of the Licensed Materials in individual or commercial software must
# include, in the user documentation and internal source code comments,
# this Notice, Disclaimer, and U.S. Government Use Provision.

"""Contains ModeCycler, which can be used to cycle through TUI modes."""

from typing import get_args, Literal, Generic, TypeVar, Self

T = TypeVar("T")


[docs] class ModeCycler(Generic[T]): """Cycles through a finite list of literals representing modes. It's somewhat awkward in Python to access the generic type at runtime. This class provides an abstraction that handles the gory details so that it's straightforward to bind a typing.Literal to the generic and use the literal options both in type hints and at runtime. Should be specialized with a typing.Literal - for example: FooBarOptions = typing.Literal["foo", "bar"] FooBarCycler = ModeCycler[FooBarOptions] Then FooBarCycler is a class that can be instantiated to cycle between modes "foo" and "bar". """ def __init__(self): """Create an instance of ModeCycler.""" self._index = 0 # Cannot access modes while in __init__ so we defer # to the first time it's actually needed self._cached_modes = None
[docs] def copy(self) -> Self: """Get a copy of self.""" copy = self.__orig_class__() copy._index = self._index copy._cached_modes = self._cached_modes return copy
[docs] def __len__(self) -> int: """Return the number of modes.""" return len(self._modes_nocopy)
@property def mode(self) -> T: """The current mode.""" return self._modes_nocopy[self._index] @mode.setter def mode(self, new_mode: T, /): for i, mode in enumerate(self._modes_nocopy): if new_mode == mode: self._index = i return raise ValueError(f"{new_mode} is not a valid mode") @property def modes(self) -> list[T]: """The list of available modes.""" # Return a copy for encapsulation return self._modes_nocopy[:] @property def _modes_nocopy(self) -> list[T]: # An accessor for the modes list that avoids creating a copy if not self._cached_modes: self._cached_modes = self._computeModeList() return self._cached_modes @property def index(self) -> int: """The current index into the mode list.""" return self._index @index.setter def index(self, index: int, /): """Set the mode by index ensuring it's in bounds.""" assert index >= 0 and index < len(self) self._index = index
[docs] def next(self): """Change to the next mode in the list.""" self._index = (self._index + 1) % len(self)
[docs] def prev(self): """Change to the previous mode in the list.""" self._index = (self._index - 1) % len(self)
[docs] def reset(self): """Reset to the default mode.""" self._index = 0
def _computeModeList(self) -> list[T]: """Get Literal options from the generic type.""" # Note: cannot be called in __init__ orig_class = getattr(self, "__orig_class__", None) if not orig_class: raise RuntimeError("__orig_class__ not defined") type_params = get_args(orig_class) if len(type_params) != 1: raise TypeError(f"concrete type should be a single typing.Literal, not {type_params}") if not hasattr(type_params[0], "__origin__"): raise TypeError(f"concrete type should be a single typing.Literal, not {type_params}") if type_params[0].__origin__ != Literal: raise TypeError(f"concrete type should be a single typing.Literal, not {type_params}") modes = get_args(type_params[0]) return list(modes)