diff --git a/cheuph/cursor_rendering.py b/cheuph/cursor_rendering.py index dbc14bf..f5fd811 100644 --- a/cheuph/cursor_rendering.py +++ b/cheuph/cursor_rendering.py @@ -1,7 +1,7 @@ # TODO move meta spaces rendering to message from abc import ABC, abstractmethod -from typing import Generic, Optional, Tuple, TypeVar +from typing import Generic, List, Optional, Tuple, TypeVar from .attributed_lines import AttributedLines from .element import Element, Id, Message, RenderedElement, RenderedMessage @@ -420,7 +420,7 @@ class CursorTreeRenderer(Generic[E]): return mid, index - def _find_cursor(self) -> Optional[int]: + def _find_cursor_on_screen(self) -> Optional[int]: for index, line in enumerate(self.lines): attrs, _ = line @@ -429,8 +429,14 @@ class CursorTreeRenderer(Generic[E]): return None - def _cursor_visible(self) -> bool: - return True in self.lines.all_values("cursor") + def _focus_on_visible_cursor(self) -> bool: + index = self._find_cursor_on_screen() + if index is not None: + self._anchor_id = None + self._absolute_anchor_offset = index + return True + + return False def scroll(self, scroll_delta: int) -> None: self._absolute_anchor_offset += scroll_delta @@ -440,18 +446,140 @@ class CursorTreeRenderer(Generic[E]): self._absolute_anchor_offset += delta + scroll_delta self._render() - cursor_index = self._find_cursor() - if cursor_index is None: + if not self._focus_on_visible_cursor(): closest, offset = self._closest_to_middle() self._anchor_id = closest self._absolute_anchor_offset = offset - else: - self._anchor_id = None - self._absolute_anchor_offset = cursor_index # Moving the cursor + def _element_id_above_cursor(self, + cursor_id: Optional[Id], + ) -> Optional[Id]: + + if cursor_id is None: + cursor_id = self._supply.lowest_root_id() + if cursor_id is None: + return None # empty supply + + elem_id: Id = cursor_id + while True: + child_ids = self._supply.child_ids(elem_id) + + if child_ids: + elem_id = child_ids[-1] + else: + return elem_id + + def _element_id_below_cursor(self, + cursor_id: Optional[Id], + ) -> Optional[Id]: + + above_id = self._element_id_above_cursor(cursor_id) + if above_id is None: + return None + else: + return self._supply.below_id(above_id) + + def _focus_on_offscreen_cursor(self) -> None: + self._anchor_id = None + + # There is always at least one element above the cursor if the supply + # isn't empty + closest_id = self._element_id_above_cursor(self._cursor_id) + if not closest_id: + # The supply is empty + self._anchor_offset = 0.5 + + # This can't be the cursor id since the cursor is offscreen + middle_id, _ = self._closest_to_middle() + + cursor_ancestor_path = self._supply.ancestor_path(closest_id) + middle_ancestor_path = self._supply.ancestor_path(middle_id) + + if cursor_ancestor_path < middle_ancestor_path: + # Cursor is above the screen somewhere + self._anchor_offset = 0 + else: + # Cursor is below the screen somewhere + self._anchor_offset = 1 + + def _focus_on_cursor(self) -> None: + if not self._focus_on_visible_cursor(): + self._focus_on_offscreen_cursor() + + def _cursor_visible(self) -> bool: + return True in self.lines.all_values("cursor") + + def _height_of(self, between_ids: List[Id]) -> int: + height = 0 + + for mid in between_ids: + message = self._cache.get(mid) + if message is None: + self._render_tree_containing(mid) + message = self._cache.get(mid) + + if message is None: + raise Exception() # TODO use better exception + + height += len(message.lines) + + return height + + def move_cursor_up(self) -> None: + new_cursor_id = self._supply.position_above_id(self._cursor_id) + + if new_cursor_id is None: + # Already at the top + self._focus_on_cursor() + return + + above_old = self._element_id_above_cursor(self._cursor_id) + below_new = self._element_id_below_cursor(new_cursor_id) + + if above_old is None: + raise Exception() # TODO use better exception + + # Moving horizontally at the bottom of the supply + if below_new is None: + height = 0 + else: + between_ids = self._supply.between_ids(below_new, above_old) + height = self._height_of(between_ids) + + self._cursor_id = new_cursor_id + self._absolute_anchor_offset -= height + self._render() + self._focus_on_cursor() + + def move_cursor_down(self) -> None: + if self._cursor_id is None: + # Already at the bottom + self._focus_on_cursor() + return + + new_cursor_id = self._supply.position_below_id(self._cursor_id) + + below_old = self._element_id_below_cursor(self._cursor_id) + above_new = self._element_id_above_cursor(new_cursor_id) + + if above_new is None: + raise Exception() # TODO use better exception + + # Moving horizontally at the bottom of the supply + if below_old is None: + height = 0 + else: + between_ids = self._supply.between_ids(below_old, above_new) + height = self._height_of(between_ids) + + self._cursor_id = new_cursor_id + self._absolute_anchor_offset += height + self._render() + self._focus_on_cursor() + class BasicCursorRenderer(CursorRenderer): META_FORMAT = "%H:%M " diff --git a/cheuph/cursor_tree_widget.py b/cheuph/cursor_tree_widget.py index 1caa3c8..98544a2 100644 --- a/cheuph/cursor_tree_widget.py +++ b/cheuph/cursor_tree_widget.py @@ -24,8 +24,6 @@ class CursorTreeWidget(urwid.WidgetWrap): self._lines = AttributedLinesWidget() super().__init__(self._lines) - self._tree._cursor_id = "->3->2->3" - def render(self, size: Tuple[int, int], focus: bool) -> None: width, height = size @@ -38,7 +36,15 @@ class CursorTreeWidget(urwid.WidgetWrap): return True def keypress(self, size: Tuple[int, int], key: str) -> Optional[str]: - if key == "shift up": + width, height = size + + if key == "up": + self._tree.move_cursor_up() + self._invalidate() + elif key == "down": + self._tree.move_cursor_down() + self._invalidate() + elif key == "shift up": self._tree.scroll(1) self._invalidate() elif key == "shift down": @@ -51,6 +57,12 @@ class CursorTreeWidget(urwid.WidgetWrap): elif key in {"home", "shift home"}: self._lines.horizontal_offset = 0 self._invalidate() + elif key == "shift page up": + self._tree.scroll(height - 1) + self._invalidate() + elif key == "shift page down": + self._tree.scroll(-(height - 1)) + self._invalidate() else: t = datetime.datetime(2019,5,7,13,25,6) self._tree._supply.add(Message( diff --git a/cheuph/element_supply.py b/cheuph/element_supply.py index da20fa6..2818720 100644 --- a/cheuph/element_supply.py +++ b/cheuph/element_supply.py @@ -120,6 +120,102 @@ class ElementSupply(ABC, Generic[E]): except ValueError: return None + def above_id(self, elem_id: Id) -> Optional[Id]: + above_id = self.previous_id(elem_id) + if above_id is None: + return self.parent_id(elem_id) + + while True: + child_ids = self.child_ids(above_id) + if child_ids: + above_id = child_ids[-1] + else: + return above_id + + def below_id(self, elem_id: Id) -> Optional[Id]: + child_ids = self.child_ids(elem_id) + if child_ids: + return child_ids[0] + + ancestor_id = elem_id + while True: + next_id = self.next_id(ancestor_id) + if next_id is not None: + return next_id + + parent_id = self.parent_id(ancestor_id) + if parent_id is None: + return None + + ancestor_id = parent_id + + def position_above_id(self, elem_id: Optional[Id]) -> Optional[Id]: + if elem_id is None: + return self.lowest_root_id() + + child_ids = self.child_ids(elem_id) + if child_ids: + return child_ids[-1] + + ancestor_id = elem_id + while True: + prev_id = self.previous_id(ancestor_id) + if prev_id is not None: + return prev_id + + parent_id = self.parent_id(ancestor_id) + if parent_id is None: + return None + + ancestor_id = parent_id + + def position_below_id(self, elem_id: Id) -> Optional[Id]: + below_id = self.next_id(elem_id) + if below_id is None: + return self.parent_id(elem_id) + + while True: + child_ids = self.child_ids(below_id) + if child_ids: + below_id = child_ids[0] + else: + return below_id + + def between_ids(self, + start_id: Id, + stop_id: Optional[Id], + ) -> List[Id]: + + start_path = self.ancestor_path(start_id) + stop_path = self.ancestor_path(stop_id) + + if start_path > stop_path: + return [] + elif start_id == stop_id: + return [start_id] + + between_ids = [start_id] + current_id = start_id + while current_id != stop_id: + below_id = self.below_id(current_id) + + if below_id is None: + break + + current_id = below_id + between_ids.append(current_id) + + return between_ids + + def ancestor_path(self, elem_id: Optional[Id]) -> List[Id]: + path = [] + + while elem_id is not None: + path.append(elem_id) + elem_id = self.parent_id(elem_id) + + return list(reversed(path)) + class InMemorySupply(ElementSupply[E]): """ This supply stores messages in memory. It orders the messages by their ids.