Source code for Karana.KUtils.MultibodyTUI.view

from typing import TypeVar, Generic, Protocol, Self
from collections.abc import Iterable


[docs] class HasId(Protocol): """Structural type to require an id getter method"""
[docs] def id(self) -> int: ...
# Generic type satisfying the HasId protocol Value = TypeVar("Value", bound=HasId)
[docs] class View(Generic[Value]): """Container for exploring subsets of items of a given type At any time, the view has a current list of items (the 'selection'). And, if the selection is nonempty, a single specific item from the selection (the 'current item'). Key features: - Change to a different selection, maintaining the current item if it is a member of the new selection. - Mark the current item as a 'favorite' and list all favorites. - Take any number of steps forward or backward in the selection. """ def __init__(self, name: str, init_values: Iterable[Value]): """View constructor Parameters ---------- name: str Name of the view init_values: Iterable[Value] The initial selection """ self._name = name self._values = list(init_values) self._index = 0 self._favorites = {}
[docs] def copy(self) -> Self: """Create a copy of this view Returns ------- typing.Self A copy of this view """ copy = View(self._name, self._values) copy._index = self._index copy._favorites = self._favorites return copy
@property def current(self) -> Value | None: """The current item or None if the selection is empty""" if not self._values: return None return self._values[self._index] @property def selection(self) -> list[Value]: """The selection""" return self._values[:] @property def index(self) -> int: """The index of the current item in the selection list""" return self._index @property def name(self) -> str: """The name of the view""" return self._name
[docs] def select(self, values: Iterable[Value], current: Value | None = None): """Change to a new selection Parameters ---------- values: Iterable[Value] The new selection current: Value | None = None The prefered current item in the new selection, using the current item from the prior selection if None. """ values = list(values) if not values: return if current is None: current = self.current if current is None: index = 0 else: for index, candidate in enumerate(values): if candidate.id() == current.id(): break else: index = 0 self._values = values self._index = index
[docs] def is_favorite(self, value: Value) -> bool: """Whether the given item is a favorite Parameters ---------- value: Value The item to check Returns ------- bool Whether the given item is a favorite """ return value.id() in self._favorites
@property def on_favorite(self) -> bool: """Whether the current item is a favorite""" value = self.current if value is None: return False return self.is_favorite(value) @property def favorites(self) -> list[Value]: """The list of all favorite items""" return list(self._favorites.values())
[docs] def toggle_favorite(self, value: Value | None = None): """Toggle the favorite status of an item Parameters ---------- value: Value | None = None The item to toggle, or the current item if None """ if value is None: value = self.current if value is None: return # Copy the favorites if we're going to modify it. Without # doing this copy, toggling a favorite in a copy of this # View would also affect the favorites of this View. # Instead, View.copy could copy the favorites dictionary, # but that would result in more overall favorites copies # getting created, so we do the copy here instead. favorites = self._favorites.copy() if value.id() in favorites: del favorites[value.id()] else: favorites[value.id()] = value self._favorites = favorites
[docs] def next(self, step: int = 1): """Increment the current item index When the step size is greater than one, this has 'sticky' behavior around the endpoints of the selection, stopping early at both the end and beginning of the selection. This is more complex than the more obvious solution of using modulo, but will hopefully result in more intuitive behavior to the user. Parameters ---------- step: int = 1 The amount to increment the current item index """ nval = len(self._values) if nval < 2: return i = self._index + step if i >= nval: # Make the point where we wrap around "sticky" if self._index < nval - 1: i = nval - 1 else: i = 0 self._index = i
[docs] def prev(self, step: int = 1): """Decrement the current item index When the step size is greater than one, this has 'sticky' behavior around the endpoints of the selection, stopping early at both the beginning and end of the selection. This is more complex than the more obvious solution of using modulo, but will hopefully result in more intuitive behavior to the user. Parameters ---------- step: int = 1 The amount to decrement the current item index """ nval = len(self._values) if nval < 2: return i = self._index - step if i < 0: # Make the point where we wrap around "sticky" if self._index > 0: i = 0 else: i = nval - 1 self._index = i
[docs] def __bool__(self) -> bool: """Whether the view's selection is nonempty""" return bool(self._values)
[docs] def __eq__(self, other: "View") -> bool: """Whether the views have identical state""" if self._index != other._index: return False if self._values != other._values: return False if self._favorites != other._favorites: return False return True
[docs] def __len__(self) -> int: """Number of items in the view's selection""" return len(self._values)