Implement cursor movement
This commit is contained in:
parent
05809b0723
commit
bb6d7830ea
3 changed files with 248 additions and 12 deletions
|
|
@ -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 "
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue