Implement cursor movement

This commit is contained in:
Joscha 2019-06-07 16:18:32 +00:00
parent 05809b0723
commit bb6d7830ea
3 changed files with 248 additions and 12 deletions

View file

@ -1,7 +1,7 @@
# TODO move meta spaces rendering to message # TODO move meta spaces rendering to message
from abc import ABC, abstractmethod 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 .attributed_lines import AttributedLines
from .element import Element, Id, Message, RenderedElement, RenderedMessage from .element import Element, Id, Message, RenderedElement, RenderedMessage
@ -420,7 +420,7 @@ class CursorTreeRenderer(Generic[E]):
return mid, index return mid, index
def _find_cursor(self) -> Optional[int]: def _find_cursor_on_screen(self) -> Optional[int]:
for index, line in enumerate(self.lines): for index, line in enumerate(self.lines):
attrs, _ = line attrs, _ = line
@ -429,8 +429,14 @@ class CursorTreeRenderer(Generic[E]):
return None return None
def _cursor_visible(self) -> bool: def _focus_on_visible_cursor(self) -> bool:
return True in self.lines.all_values("cursor") 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: def scroll(self, scroll_delta: int) -> None:
self._absolute_anchor_offset += scroll_delta self._absolute_anchor_offset += scroll_delta
@ -440,18 +446,140 @@ class CursorTreeRenderer(Generic[E]):
self._absolute_anchor_offset += delta + scroll_delta self._absolute_anchor_offset += delta + scroll_delta
self._render() self._render()
cursor_index = self._find_cursor() if not self._focus_on_visible_cursor():
if cursor_index is None:
closest, offset = self._closest_to_middle() closest, offset = self._closest_to_middle()
self._anchor_id = closest self._anchor_id = closest
self._absolute_anchor_offset = offset self._absolute_anchor_offset = offset
else:
self._anchor_id = None
self._absolute_anchor_offset = cursor_index
# Moving the cursor # 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): class BasicCursorRenderer(CursorRenderer):
META_FORMAT = "%H:%M " META_FORMAT = "%H:%M "

View file

@ -24,8 +24,6 @@ class CursorTreeWidget(urwid.WidgetWrap):
self._lines = AttributedLinesWidget() self._lines = AttributedLinesWidget()
super().__init__(self._lines) super().__init__(self._lines)
self._tree._cursor_id = "->3->2->3"
def render(self, size: Tuple[int, int], focus: bool) -> None: def render(self, size: Tuple[int, int], focus: bool) -> None:
width, height = size width, height = size
@ -38,7 +36,15 @@ class CursorTreeWidget(urwid.WidgetWrap):
return True return True
def keypress(self, size: Tuple[int, int], key: str) -> Optional[str]: 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._tree.scroll(1)
self._invalidate() self._invalidate()
elif key == "shift down": elif key == "shift down":
@ -51,6 +57,12 @@ class CursorTreeWidget(urwid.WidgetWrap):
elif key in {"home", "shift home"}: elif key in {"home", "shift home"}:
self._lines.horizontal_offset = 0 self._lines.horizontal_offset = 0
self._invalidate() 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: else:
t = datetime.datetime(2019,5,7,13,25,6) t = datetime.datetime(2019,5,7,13,25,6)
self._tree._supply.add(Message( self._tree._supply.add(Message(

View file

@ -120,6 +120,102 @@ class ElementSupply(ABC, Generic[E]):
except ValueError: except ValueError:
return None 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]): class InMemorySupply(ElementSupply[E]):
""" """
This supply stores messages in memory. It orders the messages by their ids. This supply stores messages in memory. It orders the messages by their ids.