Lay out new client structure
This commit is contained in:
parent
6c4bfe2752
commit
7da4bf36d5
21 changed files with 174 additions and 1137 deletions
|
|
@ -1,20 +1,29 @@
|
|||
from typing import List
|
||||
|
||||
from .attributed_lines import *
|
||||
from .attributed_lines_widget import *
|
||||
from .attributed_text_widget import *
|
||||
from .config import *
|
||||
from .element import *
|
||||
from .element_supply import *
|
||||
from .exceptions import *
|
||||
from .markup import *
|
||||
from .tree_display import *
|
||||
from .tree_list import *
|
||||
from .widgets import *
|
||||
from .message import *
|
||||
from .message_cache import *
|
||||
from .message_editor_widget import *
|
||||
from .message_supply import *
|
||||
from .message_tree_widget import *
|
||||
from .user_list_widget import *
|
||||
|
||||
__all__: List[str] = []
|
||||
|
||||
__all__ += attributed_lines.__all__
|
||||
__all__ += attributed_lines_widget.__all__
|
||||
__all__ += attributed_text_widget.__all__
|
||||
__all__ += config.__all__
|
||||
__all__ += element.__all__
|
||||
__all__ += element_supply.__all__
|
||||
__all__ += exceptions.__all__
|
||||
__all__ += markup.__all__
|
||||
__all__ += tree_display.__all__
|
||||
__all__ += tree_list.__all__
|
||||
__all__ += widgets.__all__
|
||||
__all__ += message.__all__
|
||||
__all__ += message_cache.__all__
|
||||
__all__ += message_editor_widget.__all__
|
||||
__all__ += message_supply.__all__
|
||||
__all__ += message_tree_widget.__all__
|
||||
__all__ += user_list_widget.__all__
|
||||
|
|
|
|||
16
cheuph/attributed_lines.py
Normal file
16
cheuph/attributed_lines.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
__all__ = ["AttributedLines"]
|
||||
|
||||
class AttributedLines:
|
||||
"""
|
||||
AttributedLines is a list of lines of AttributedText that maintains a
|
||||
vertical offset.
|
||||
|
||||
When rendering a tree of messages, the RenderedMessage-s are drawn line by
|
||||
line to an AttributedLines. AttributedLines. The AttributedLines is then
|
||||
displayed in an AttributedLinesWidget.
|
||||
|
||||
Multiple AttributedLines can be concatenated, keeping either the first or
|
||||
the second AttributedLines's offset.
|
||||
"""
|
||||
|
||||
pass
|
||||
15
cheuph/attributed_lines_widget.py
Normal file
15
cheuph/attributed_lines_widget.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
__all__ = ["AttributedLinesWidget", "ALWidget"]
|
||||
|
||||
class AttributedLinesWidget:
|
||||
"""
|
||||
This widget draws lines of AttributedText with a horizontal and a vertical
|
||||
offset. It can retrieve the attributes of any character by its (x, y)
|
||||
coordinates. Line-wide attributes may be specified.
|
||||
|
||||
When clicked, it sends an event containing the attributes of the character
|
||||
that was just clicked.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
ALWidget = AttributedLinesWidget
|
||||
|
|
@ -33,7 +33,7 @@ class AttributedTextWidget(urwid.Text):
|
|||
def _convert_to_markup(text: AttributedText
|
||||
) -> List[Union[str, Tuple[str, str]]]:
|
||||
|
||||
# Wonder why it can't figure out the type signature of markup on its
|
||||
# Wonder why mypy can't figure out the type signature of markup on its
|
||||
# own... :P
|
||||
markup: List[Union[str, Tuple[str, str]]]
|
||||
markup = [
|
||||
|
|
@ -51,6 +51,14 @@ class AttributedTextWidget(urwid.Text):
|
|||
self._attributed_text = text
|
||||
super().set_text(self._convert_to_markup(text))
|
||||
|
||||
def set_text(self, *args, **kwargs):
|
||||
"""
|
||||
This function should not be used directly. Instead, use
|
||||
set_attributed_text().
|
||||
"""
|
||||
|
||||
raise NotImplementedError("use set_attributed_text() instead")
|
||||
|
||||
def get_attributed_text(self) -> AttributedText:
|
||||
"""
|
||||
Returns the currently used AttributedText.
|
||||
|
|
@ -1,3 +1,11 @@
|
|||
# TODO define a config structure including config element descriptions and
|
||||
# default values
|
||||
#
|
||||
# TODO improve interface for accessing config values
|
||||
#
|
||||
# TODO load from and save to yaml file (only the values which differ from the
|
||||
# defaults or which were explicitly set)
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
__all__ = ["Fields", "Config", "ConfigView"]
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
import abc
|
||||
from typing import Hashable, List, Optional
|
||||
|
||||
from .exceptions import ElementException, TreeException
|
||||
from .markup import AttributedText
|
||||
|
||||
__all__ = ["Id", "Element", "RenderedElement"]
|
||||
|
||||
Id = Hashable
|
||||
|
||||
class Element(abc.ABC):
|
||||
def __init__(self,
|
||||
id: Id,
|
||||
parent_id: Optional[Id],
|
||||
) -> None:
|
||||
self._id = id
|
||||
self._parent_id = parent_id
|
||||
|
||||
|
||||
@property
|
||||
def id(self) -> Id:
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def parent_id(self) -> Optional[Id]:
|
||||
return self._parent_id
|
||||
|
||||
@abc.abstractmethod
|
||||
def render(self,
|
||||
width: int,
|
||||
depth: int,
|
||||
highlighted: bool = False,
|
||||
folded: bool = False,
|
||||
) -> "RenderedElement":
|
||||
pass
|
||||
|
||||
class RenderedElement:
|
||||
def __init__(self,
|
||||
element: Element,
|
||||
rendered: List[AttributedText],
|
||||
) -> None:
|
||||
self._element = element
|
||||
self._lines = rendered
|
||||
|
||||
@property
|
||||
def element(self) -> Element:
|
||||
return self._element
|
||||
|
||||
@property
|
||||
def lines(self) -> List[AttributedText]:
|
||||
return self._lines
|
||||
|
||||
@property
|
||||
def height(self) -> int:
|
||||
return len(self._lines)
|
||||
|
|
@ -1,207 +0,0 @@
|
|||
import abc
|
||||
from typing import Dict, List, Optional, Set
|
||||
|
||||
from .element import Element, Id
|
||||
from .exceptions import TreeException
|
||||
|
||||
__all__ = ["ElementSupply", "MemoryElementSupply"]
|
||||
|
||||
class ElementSupply(abc.ABC):
|
||||
"""
|
||||
An ElementSupply is an interface to query some resource containing
|
||||
Elements. The elements could for example be kept in memory, in a database
|
||||
or somewhere else.
|
||||
|
||||
The element ids must be unique, and the elements and their parents must
|
||||
form one or more trees (i. e. must not contain any cycles).
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get(self, element_id: Id) -> Element:
|
||||
"""
|
||||
Get a single element by its id.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def get_parent_id(self, element_id: Id) -> Optional[Id]:
|
||||
"""
|
||||
Get the id of the parent's element.
|
||||
|
||||
This function is redundant, since you can just use element.parent_id.
|
||||
"""
|
||||
|
||||
return self.get(element_id).parent_id
|
||||
|
||||
def get_parent(self, element_id: Id) -> Optional[Element]:
|
||||
"""
|
||||
Like get_parent_id, but returns the Element instead.
|
||||
"""
|
||||
|
||||
parent_id = self.get_parent_id(element_id)
|
||||
|
||||
if parent_id is not None:
|
||||
return self.get(parent_id)
|
||||
else:
|
||||
return None
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_children_ids(self, element_id: Optional[Id]) -> List[Id]:
|
||||
"""
|
||||
Get a list of the ids of all the element's children.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def get_children(self, element_id: Optional[Id]) -> List[Element]:
|
||||
"""
|
||||
Get a list of all children of an element.
|
||||
|
||||
If the id passed is None, return a list of all top-level elements
|
||||
instead.
|
||||
"""
|
||||
|
||||
children_ids = self.get_children_ids(element_id)
|
||||
|
||||
children: List[Element] = []
|
||||
for child_id in children_ids:
|
||||
children.append(self.get(child_id))
|
||||
|
||||
return children
|
||||
|
||||
def get_previous_id(self, element_id: Id) -> Optional[Id]:
|
||||
"""
|
||||
Get the id of an element's previous sibling (i. e. the sibling just
|
||||
above it).
|
||||
|
||||
Returns None if there is no previous sibling.
|
||||
|
||||
Depending on the amount of elements in your ElementSupply, the default
|
||||
implementation might get very slow and/or use a lot of memory.
|
||||
"""
|
||||
|
||||
siblings = self.get_children_ids(self.get_parent_id(element_id))
|
||||
index = siblings.index(element_id)
|
||||
|
||||
if index <= 0:
|
||||
return None
|
||||
else:
|
||||
return siblings[index - 1]
|
||||
|
||||
def get_previous(self, element_id: Id) -> Optional[Element]:
|
||||
"""
|
||||
Like get_previous_id(), but returns the Element instead.
|
||||
"""
|
||||
|
||||
previous_id = self.get_previous_id(element_id)
|
||||
|
||||
if previous_id is not None:
|
||||
return self.get(previous_id)
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_next_id(self, element_id: Id) -> Optional[Id]:
|
||||
"""
|
||||
Get the id of an element's next sibling (i. e. the sibling just below
|
||||
it).
|
||||
|
||||
Returns None if there is no next sibling.
|
||||
|
||||
Depending on the amount of elements in your ElementSupply, the default
|
||||
implementation might get very slow and/or use a lot of memory.
|
||||
"""
|
||||
|
||||
siblings = self.get_children_ids(self.get_parent_id(element_id))
|
||||
index = siblings.index(element_id)
|
||||
|
||||
if index >= len(siblings) - 1:
|
||||
return None
|
||||
else:
|
||||
return siblings[index + 1]
|
||||
|
||||
def get_next(self, element_id: Id) -> Optional[Element]:
|
||||
"""
|
||||
Like get_next_id(), but returns the Element instead.
|
||||
"""
|
||||
|
||||
next_id = self.get_next_id(element_id)
|
||||
|
||||
if next_id is not None:
|
||||
return self.get(next_id)
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_furthest_ancestor_id(self,
|
||||
element_id: Id,
|
||||
root_id: Optional[Id] = None,
|
||||
) -> Id:
|
||||
current_id = element_id
|
||||
|
||||
while True:
|
||||
parent_id = self.get_parent_id(current_id)
|
||||
|
||||
if parent_id == root_id:
|
||||
return current_id
|
||||
elif parent_id is None:
|
||||
raise TreeException(
|
||||
"Reached implicit root before hitting specified root")
|
||||
|
||||
current_id = parent_id
|
||||
|
||||
def get_furthest_ancestor(self,
|
||||
element_id: Id,
|
||||
root_id: Optional[Id] = None,
|
||||
) -> Element:
|
||||
return self.get(self.get_furthest_ancestor_id(element_id,
|
||||
root_id=root_id))
|
||||
|
||||
class MemoryElementSupply(ElementSupply):
|
||||
"""
|
||||
An in-memory implementation of an ElementSupply that works with any type of
|
||||
Element.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._elements: Dict[Id, Element] = {}
|
||||
self._children: Dict[Optional[Id], Set[Id]] = {None: set()}
|
||||
|
||||
def add(self, element: Element) -> None:
|
||||
"""
|
||||
Add a new element or overwrite an existing element with the same id.
|
||||
"""
|
||||
|
||||
if element.id in self._elements:
|
||||
self.remove(element.id)
|
||||
|
||||
self._elements[element.id] = element
|
||||
self._children[element.id] = set()
|
||||
self._children[element.parent_id].add(element.id)
|
||||
|
||||
def remove(self, element_id: Id) -> None:
|
||||
"""
|
||||
Remove an element. This function does nothing if the element doesn't
|
||||
exist in this ElementSupply.
|
||||
"""
|
||||
|
||||
if element_id in self._elements:
|
||||
element = self.get(element_id)
|
||||
|
||||
self._elements.pop(element_id)
|
||||
self._children.pop(element_id)
|
||||
self._children[element.parent_id].remove(element.id)
|
||||
|
||||
def get(self, element_id: Id) -> Element:
|
||||
result = self._elements.get(element_id)
|
||||
|
||||
if result is None:
|
||||
raise TreeException(f"Element with id {element_id!r} could not be found")
|
||||
|
||||
return result
|
||||
|
||||
def get_children_ids(self, element_id: Optional[Id]) -> List[Id]:
|
||||
result = self._children.get(element_id)
|
||||
|
||||
if result is None:
|
||||
raise TreeException(f"Element with id {element_id!r} could not be found")
|
||||
|
||||
return list(sorted(result))
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
from typing import List
|
||||
|
||||
from .single_room_application import *
|
||||
from .util import *
|
||||
|
||||
__all__: List[str] = []
|
||||
__all__ += single_room_application.__all__
|
||||
__all__ += util.__all__
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
import asyncio
|
||||
from typing import Any, List, Optional
|
||||
|
||||
import urwid
|
||||
import yaboli
|
||||
|
||||
from ..config import Config
|
||||
from ..markup import AT
|
||||
from ..widgets import ATWidget
|
||||
|
||||
|
||||
class CenteredTextWidget(urwid.WidgetWrap):
|
||||
def __init__(self, lines: List[AT]):
|
||||
max_width = max(map(len, lines))
|
||||
text = AT("\n").join(lines)
|
||||
filler = urwid.Filler(ATWidget(text, align="center"))
|
||||
super().__init__(filler)
|
||||
|
||||
class RoomWidget(urwid.WidgetWrap):
|
||||
"""
|
||||
The RoomWidget connects to and displays a single yaboli room.
|
||||
|
||||
Its life cycle looks like this:
|
||||
1. Create widget
|
||||
2. Call connect() (while the event loop is running)
|
||||
3. Keep widget around and occasionally display it
|
||||
4. Call disconnect() (while the event loop is runnning)
|
||||
5. When the room should be destroyed/forgotten about, it sends a "close"
|
||||
event
|
||||
"""
|
||||
|
||||
def __init__(self, config: Config, roomname: str) -> None:
|
||||
self.c = config
|
||||
self._room = yaboli.Room(roomname)
|
||||
|
||||
super().__init__(self._connecting_widget())
|
||||
self._room_view = self._connected_widget()
|
||||
|
||||
def _connecting_widget(self) -> Any:
|
||||
lines = [AT("Connecting to ")
|
||||
+ AT("&" + self.room.name, style=self.c.v.element.room)
|
||||
+ AT("...")]
|
||||
return CenteredTextWidget(lines)
|
||||
|
||||
def _connected_widget(self) -> Any:
|
||||
lines = [AT("Connected to ")
|
||||
+ AT("&" + self.room.name, style=self.c.v.element.room)
|
||||
+ AT(".")]
|
||||
return CenteredTextWidget(lines)
|
||||
|
||||
def _connection_failed_widget(self) -> Any:
|
||||
lines = [AT("Could not connect to ")
|
||||
+ AT("&" + self.room.name, style=self.c.v.element.room)
|
||||
+ AT(".")]
|
||||
return CenteredTextWidget(lines)
|
||||
|
||||
@property
|
||||
def room(self) -> yaboli.Room:
|
||||
return self._room
|
||||
|
||||
# Start up the connection and room
|
||||
|
||||
async def _connect(self) -> None:
|
||||
success = await self._room.connect()
|
||||
if success:
|
||||
self._w = self._room_view
|
||||
else:
|
||||
self._w = self._connection_failed_widget()
|
||||
urwid.emit_signal(self, "close")
|
||||
|
||||
def connect(self) -> None:
|
||||
asyncio.create_task(self._connect())
|
||||
|
||||
# Handle input
|
||||
|
||||
#def selectable(self) -> bool:
|
||||
# return True
|
||||
|
||||
#def keypress(self, size: Any, key: str) -> Optional[str]:
|
||||
# pass
|
||||
|
||||
urwid.register_signal(RoomWidget, ["close"])
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
from typing import Any, Optional
|
||||
|
||||
import urwid
|
||||
|
||||
from ..config import Config
|
||||
from .room_widget import RoomWidget
|
||||
|
||||
__all__ = ["SingleRoomApplication"]
|
||||
|
||||
class ChooseRoomWidget(urwid.WidgetWrap):
|
||||
def __init__(self, config: Config) -> None:
|
||||
self.c = config
|
||||
|
||||
self.error = None
|
||||
self.text = urwid.Text("Choose a room:", align=urwid.CENTER)
|
||||
self.edit = urwid.Edit("&", align=urwid.CENTER)
|
||||
self.pile = urwid.Pile([
|
||||
self.text,
|
||||
urwid.AttrMap(self.edit, self.c.v.element.room),
|
||||
])
|
||||
self.filler = urwid.Filler(self.pile)
|
||||
super().__init__(self.filler)
|
||||
|
||||
def render(self, size: Any, focus: Any) -> Any:
|
||||
if self.error:
|
||||
width, _ = size
|
||||
rows = self.error.rows((width,), focus)
|
||||
self.filler.bottom = rows
|
||||
|
||||
return super().render(size, focus)
|
||||
|
||||
def set_error(self, text: Any) -> None:
|
||||
self.error = urwid.Text(text, align=urwid.CENTER)
|
||||
self.pile = urwid.Pile([
|
||||
self.error,
|
||||
self.text,
|
||||
urwid.AttrMap(self.edit, self.c.v.element.room),
|
||||
])
|
||||
self.filler = urwid.Filler(self.pile)
|
||||
self._w = self.filler
|
||||
|
||||
def unset_error(self) -> None:
|
||||
self.error = None
|
||||
self.pile = urwid.Pile([
|
||||
self.text,
|
||||
urwid.AttrMap(self.edit, self.c.v.element.room),
|
||||
])
|
||||
self.filler = urwid.Filler(self.pile)
|
||||
self._w = self.filler
|
||||
|
||||
def could_not_connect(self, roomname: str) -> None:
|
||||
text = [
|
||||
"Could not connect to ",
|
||||
(self.c.v.element.room, "&" + roomname),
|
||||
".\n",
|
||||
]
|
||||
self.set_error(text)
|
||||
|
||||
def invalid_room_name(self, reason: str) -> None:
|
||||
text = [f"Invalid room name: {reason}\n"]
|
||||
self.set_error(text)
|
||||
|
||||
class SingleRoomApplication(urwid.WidgetWrap):
|
||||
# The characters in the ALPHABET make up the characters that are allowed in
|
||||
# room names.
|
||||
ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
|
||||
# These are other characters or character combinations necessary for the
|
||||
# editor to function well.
|
||||
ALLOWED_EDITOR_KEYS = {
|
||||
"backspace", "delete",
|
||||
"left", "right",
|
||||
"home", "end",
|
||||
}
|
||||
|
||||
def __init__(self, config: Config) -> None:
|
||||
self.c = config
|
||||
|
||||
self.choose_room = ChooseRoomWidget(self.c)
|
||||
super().__init__(self.choose_room)
|
||||
|
||||
def selectable(self) -> bool:
|
||||
return True
|
||||
|
||||
def switch_to_choose(self) -> None:
|
||||
self.choose_room.could_not_connect(self.choose_room.edit.edit_text)
|
||||
self._w = self.choose_room
|
||||
|
||||
def keypress(self, size: Any, key: str) -> Optional[str]:
|
||||
if self._w == self.choose_room:
|
||||
if key == "esc":
|
||||
raise urwid.ExitMainLoop()
|
||||
|
||||
self.choose_room.unset_error()
|
||||
|
||||
if key == "enter":
|
||||
roomname = self.choose_room.edit.edit_text
|
||||
|
||||
if roomname:
|
||||
room = RoomWidget(self.c, roomname)
|
||||
urwid.connect_signal(room, "close", self.switch_to_choose)
|
||||
room.connect()
|
||||
self._w = room
|
||||
else:
|
||||
self.choose_room.invalid_room_name("too short")
|
||||
|
||||
elif not super().selectable():
|
||||
return key
|
||||
# Make sure we only enter valid room names
|
||||
elif key.lower() in self.ALPHABET:
|
||||
return super().keypress(size, key.lower())
|
||||
elif key in self.ALLOWED_EDITOR_KEYS:
|
||||
return super().keypress(size, key)
|
||||
|
||||
return None
|
||||
|
||||
elif super().selectable():
|
||||
return super().keypress(size, key)
|
||||
|
||||
return key
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
from typing import List, Tuple, Union
|
||||
|
||||
from ..config import Config
|
||||
|
||||
__all__ = ["UtilException","Palette", "palette_from_config", "DEFAULT_CONFIG"]
|
||||
|
||||
class UtilException(Exception):
|
||||
pass
|
||||
|
||||
Palette = List[Union[Tuple[str, str], Tuple[str, str, str],
|
||||
Tuple[str, str, str, str]]]
|
||||
|
||||
def palette_from_config(conf: Config) -> Palette:
|
||||
palette: Palette = []
|
||||
|
||||
styles = conf.tree["style"]
|
||||
for style, info in styles.items():
|
||||
# First, do the alias stuff
|
||||
alias = info.get("alias")
|
||||
if isinstance(alias, str):
|
||||
if alias in styles:
|
||||
palette.append((style, alias))
|
||||
continue
|
||||
else:
|
||||
raise UtilException((f"style.{style}.alias must be the name of"
|
||||
" another style"))
|
||||
elif alias is not None:
|
||||
raise UtilException(f"style.{style}.alias must be a string")
|
||||
|
||||
# Foreground/background
|
||||
fg = info.get("fg")
|
||||
bg = info.get("bg")
|
||||
|
||||
if not isinstance(fg, str) and fg is not None:
|
||||
raise TypeError(f"style.{style}.fg must be a string")
|
||||
|
||||
if not isinstance(bg, str) and bg is not None:
|
||||
raise TypeError(f"style.{style}.bg must be a string")
|
||||
|
||||
fg = fg or ""
|
||||
bg = bg or ""
|
||||
|
||||
palette.append((style, fg, bg))
|
||||
|
||||
return palette
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"element": {
|
||||
"room": "room",
|
||||
},
|
||||
"style": {
|
||||
"room": {
|
||||
"fg": "light blue, bold",
|
||||
},
|
||||
},
|
||||
}
|
||||
36
cheuph/message.py
Normal file
36
cheuph/message.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
__all__ = ["Message", "RenderedMessage"]
|
||||
|
||||
class Message:
|
||||
"""
|
||||
A Message represents a single euphoria message. It contains the information
|
||||
and functionality necessary to render itself to lines of AttributedText.
|
||||
|
||||
It does not contain information that usually changes, like a list of its
|
||||
child messages, or if it is currently folded.
|
||||
|
||||
A message's content is assumed to never change. Truncated messages are
|
||||
never untruncated and displayed in full. Thus, the Message can ignore
|
||||
truncation status.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class RenderedMessage:
|
||||
"""
|
||||
A RenderedMessage is the result of rendering a Message. It contains lines
|
||||
of AttributedText, the target width to which the Message was rendered, its
|
||||
final dimensions and possibly some other useful information.
|
||||
|
||||
It only contains the rendered sender nick and message body, NOT the
|
||||
message's indentation.
|
||||
|
||||
A RenderedMessage is immutable. It can be used in a cache to prevent
|
||||
re-rendering each message every time it is needed (preventing word wrapping
|
||||
and other fancy calculations to be repeated on every re-render).
|
||||
|
||||
It is also useful for scrolling and cursor movement, since the height of
|
||||
the message displayed on screen is not inherent to the Message object, but
|
||||
rather the result of rendering a Message.
|
||||
"""
|
||||
|
||||
pass
|
||||
8
cheuph/message_cache.py
Normal file
8
cheuph/message_cache.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
__all__ = ["RenderedMessageCache"]
|
||||
class RenderedMessageCache:
|
||||
"""
|
||||
This is a cache for RenderedMessage-s. Message-s should not need to be
|
||||
redrawn every frame (and every in-between calculation).
|
||||
"""
|
||||
|
||||
pass
|
||||
12
cheuph/message_editor_widget.py
Normal file
12
cheuph/message_editor_widget.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
__all__ = ["MessageEditorWidget"]
|
||||
|
||||
class MessageEditorWidget:
|
||||
"""
|
||||
This widget allows the user to compose a new message. It is based on
|
||||
urwid's Edit widget.
|
||||
|
||||
One day, it will (hopefully) support syntax highlighting and tab
|
||||
completion.
|
||||
"""
|
||||
|
||||
pass
|
||||
14
cheuph/message_supply.py
Normal file
14
cheuph/message_supply.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
__all__ = ["MessageSupply"]
|
||||
|
||||
class MessageSupply:
|
||||
"""
|
||||
A MessageSupply holds all of a room's known messages. It can be queried in
|
||||
different ways. Messages can also be added to or removed from the MessageSupply
|
||||
as they are received by the client.
|
||||
|
||||
The MessageSupply may use a database or keep messages in memory. A
|
||||
MessageSupply may also wrap around another MessageSupply, to provide caching or
|
||||
similar.
|
||||
"""
|
||||
|
||||
pass
|
||||
26
cheuph/message_tree_widget.py
Normal file
26
cheuph/message_tree_widget.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
__all__ = ["MessageTreeWidget"]
|
||||
|
||||
class MessageTreeWidget:
|
||||
"""
|
||||
This widget displays an ElementSupply, including user interface like a
|
||||
cursor or folding markers. It usually is part of a RoomWidget. It also
|
||||
keeps a RenderedMessageCache (and maybe even other caches).
|
||||
|
||||
It receives key presses and mouse clicks from its parent widget. It
|
||||
receives redraw requests and cache invalidation notices from the
|
||||
RoomWidget.
|
||||
|
||||
It doesn't directly receive new messages. Rather, the RoomWidget adds them
|
||||
to the ElementSupply and then submits a cache invalidation notice and a
|
||||
redraw request.
|
||||
|
||||
It emits a "room top hit" event (unnamed as of yet). When the RoomWidget
|
||||
receives this event, it should retrieve more messages from the server.
|
||||
|
||||
It emits a "edit" event (unnamed as of yet) when the user attempts to edit
|
||||
a message. When the RoomWidget receives this event, it should open a text
|
||||
editor to compose a new message, and send that message once the user
|
||||
finishes editing it.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
|
@ -1,386 +0,0 @@
|
|||
import collections
|
||||
from typing import Any, List, Optional, Set
|
||||
|
||||
from .element import Element, Id, RenderedElement
|
||||
from .element_supply import ElementSupply
|
||||
from .exceptions import TreeException
|
||||
from .markup import AttributedText
|
||||
from .tree_list import TreeList
|
||||
|
||||
__all__ = ["TreeDisplay"]
|
||||
|
||||
class TreeDisplay:
|
||||
"""
|
||||
This class renders elements from an ElementSupply to a list of lines, which
|
||||
can then be drawn onto a curses window. It maintains two "pointers" to
|
||||
specific messages: An anchor, which is used for scrolling, and a cursor,
|
||||
which highlights elements and can be used for interaction.
|
||||
|
||||
ANCHOR
|
||||
|
||||
The anchor is a message at a fixed vertical screen position. This position
|
||||
is called the anchor offset and it specifies the position of the first line
|
||||
of the anchor message relative to the top of the screen.
|
||||
|
||||
A position of 0 would mean that the anchor is displayed at the top of the
|
||||
screen, where a position of (height - 1) would mean that only the first
|
||||
line of the anchor is displayed at the bottom of the screen (the rest is
|
||||
offscreen).
|
||||
|
||||
If no anchor is set, any attempt to render will result in a blank screen
|
||||
(as if the room was empty) since the anchor is the point from which the
|
||||
TreeDisplay starts to render the tree(s).
|
||||
|
||||
CURSOR
|
||||
|
||||
The cursor is a highlighted message that is meant to be used for user
|
||||
interaction.
|
||||
|
||||
The element that the cursor points to is passed highlighed=True in its
|
||||
render() method, whereas all other elements are passed highlighted=False.
|
||||
The cursor can also point to None (i. e. no message), in which case no
|
||||
element is highlighted.
|
||||
|
||||
At the moment, the TreeDisplay contains no cursor movement code, so the
|
||||
user of the TreeDisplay has to implement their own by setting the cursor to
|
||||
the respective element id themselves. You might also want to set the anchor
|
||||
to the cursor and maybe center it on screen when the cursor is moved.
|
||||
|
||||
RENDERING
|
||||
|
||||
Rendering consists of these steps:
|
||||
|
||||
0. Initialize the TreeDisplay with the correct screen width and height and
|
||||
keep the width and height up to date when the window resolution changes.
|
||||
1. Update the internal TreeList through one of various functions
|
||||
2. Cut out the display lines (the text that is later visible on screen)
|
||||
3. Draw the display lines to the curses window
|
||||
|
||||
Step 2 uses the contents of the TreeList from step 1, but DOESN'T happen
|
||||
automatically when the TreeList changes. It is also guaranteed to not
|
||||
modify the display lines if it fails in some way.
|
||||
|
||||
The display lines are supposed to contain the same text that is currently
|
||||
visible on the screen, and can be used to look up mouse clicks (see the
|
||||
"id" attribute in the "RENDERING - technical details" section below).
|
||||
They're a simple format for translating between elements and onscreen text.
|
||||
They are always as wide as the current width. Any missing characters are
|
||||
filled with spaces.
|
||||
|
||||
RENDERING - technical details
|
||||
|
||||
The process of rendering results in a TreeList and a list of lines (called
|
||||
display lines) representing the text that should be displayed on the
|
||||
screen.
|
||||
|
||||
The TreeList's (vertical) offset of messages corresponds to the line on the
|
||||
screen where the message will be drawn. This means that the anchor
|
||||
message's offset in the TreeList will be the anchor offset referred to in
|
||||
the ANCHOR section above.
|
||||
|
||||
Like the Elements and RenderedElements, the TreeDisplay and TreeList also
|
||||
use AttributedStrings for rendered content, especially in the display
|
||||
lines. In addition to the attributes added by the Element during rendering,
|
||||
the TreeDisplay also adds the following attributes to the display lines:
|
||||
|
||||
1. "id" - the id of the element visible on this line
|
||||
2. "parent" - the parent id (or None) of the element visible on this line
|
||||
3. "cursor" - True, only added if the cursor points to the element visible
|
||||
on this line
|
||||
|
||||
When an Element is rendered (producing a RenderedElement), its render()
|
||||
function is called with:
|
||||
1. the element's depth/level (top-level elements have a depth of 0)
|
||||
1. the current screen width
|
||||
2. whether the element is highlighted (by the cursor)
|
||||
3. whether the element is folded
|
||||
|
||||
The RenderedElement contains one or more lines (zero lines may break the
|
||||
TreeDisplay, not sure yet) of AttributedText, which may contain formatting
|
||||
information (such as text color or style). This means that an Element can
|
||||
decide its own height.
|
||||
|
||||
These lines should generally stay within the width passed to render(), but
|
||||
may exceed it in certain exceptional situations (e. g. too close to the
|
||||
right side of the screen). Because of this, the TreeDisplay supports
|
||||
horizontal scrolling.
|
||||
|
||||
SCROLLING
|
||||
|
||||
The most basic form of scrolling is to just increase or decrease the anchor
|
||||
offset. Depending on the application, this approach is enough. In some
|
||||
cases, more complex behaviour is required.
|
||||
|
||||
For example, if you're displaying a section from a live tree structure like
|
||||
a chat room (see https://euphoria.io), there is no static top-most or
|
||||
bottom-most message to anchor to (unless you want to download the whole
|
||||
room's log). Also, new messages may appear at any time in (almost) any
|
||||
place.
|
||||
|
||||
With the above, basic scrolling model, offscreen conversations would often
|
||||
slide around the visible messages, leading to frustration of the user. In
|
||||
cases like this, an alternative scrolling model can be employed:
|
||||
|
||||
First, change the anchor offset as required and render the messages. Then,
|
||||
select the middle-most message as the new anchor and adapt the anchor
|
||||
offset such that the message stays in the same position.
|
||||
|
||||
FOLDING
|
||||
|
||||
Finally, the TreeDisplay supports folding and unfolding elements, making
|
||||
their subtrees invisible.
|
||||
|
||||
Folding happens on elements. When an element is folded, it is still
|
||||
displayed but its (direct and indirect) child messages are no longer
|
||||
displayed.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
supply: ElementSupply,
|
||||
width: int,
|
||||
height: int,
|
||||
) -> None:
|
||||
"""
|
||||
supply - the ElementSupply that this TreeDisplay should use
|
||||
width - the width of the target window (in characters)
|
||||
height - the width of the target window (in lines)
|
||||
|
||||
To use the TreeDisplay, you might also want to:
|
||||
- set the anchor
|
||||
- make sure the anchor is visible
|
||||
"""
|
||||
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
#self._root_id: Optional[Id] = None # TODO add root stuff
|
||||
|
||||
self.anchor_id: Optional[Id] = None
|
||||
self.anchor_offset: int = 0
|
||||
|
||||
self.cursor_id: Optional[Id] = None
|
||||
|
||||
self.horizontal_offset: int = 0
|
||||
|
||||
# Object references
|
||||
self._supply = supply
|
||||
self._folded: Set[Id] = set()
|
||||
|
||||
self._rendered: Optional[TreeList] = None
|
||||
self._display_lines: Optional[List[AttributedText]] = None
|
||||
|
||||
# RENDERING
|
||||
|
||||
@property
|
||||
def display_lines(self) -> List[AttributedText]:
|
||||
if self._display_lines is None:
|
||||
raise TreeException((
|
||||
"No display lines available (have you called"
|
||||
" render_display_lines() yet?)"
|
||||
))
|
||||
|
||||
return self._display_lines
|
||||
|
||||
def rerender(self) -> None:
|
||||
"""
|
||||
This function updates the internal TreeList (step 1).
|
||||
|
||||
It should be called when the ElementSupply changes or when the anchor,
|
||||
anchor offset, cursor, width or height are manually changed.
|
||||
"""
|
||||
|
||||
if self.anchor_id is None:
|
||||
# As described in the class docstring, we have no starting point
|
||||
# for rendering, so we don't even attempt it.
|
||||
self._rendered = None
|
||||
return
|
||||
|
||||
ancestor_id = self._supply.get_furthest_ancestor_id(self.anchor_id)
|
||||
ancestor_tree = self._render_tree(ancestor_id)
|
||||
|
||||
self._rendered = TreeList(ancestor_tree, self.anchor_id)
|
||||
self._rendered.offset_by(self.anchor_offset)
|
||||
|
||||
self._fill_screen_upwards()
|
||||
self._fill_screen_downwards()
|
||||
|
||||
def _render_tree(self,
|
||||
tree_id: Id,
|
||||
depth: int = 0
|
||||
) -> List[RenderedElement]:
|
||||
|
||||
elements: List[RenderedElement] = []
|
||||
|
||||
highlighted = tree_id == self.cursor_id
|
||||
folded = tree_id in self._folded
|
||||
|
||||
tree = self._supply.get(tree_id)
|
||||
rendered = tree.render(width=self.width, depth=depth,
|
||||
highlighted=highlighted, folded=folded)
|
||||
|
||||
elements.append(rendered)
|
||||
|
||||
if not folded:
|
||||
for child_id in self._supply.get_children_ids(tree_id):
|
||||
subelements = self._render_tree(child_id, depth=depth+1)
|
||||
elements.extend(subelements)
|
||||
|
||||
return elements
|
||||
|
||||
def _fill_screen_upwards(self) -> None:
|
||||
if self._rendered is None:
|
||||
raise TreeException((
|
||||
"Can't fill screen upwards without a TreeList. This exception"
|
||||
" should never occur."
|
||||
))
|
||||
|
||||
while True:
|
||||
if self._rendered.upper_offset <= 0:
|
||||
break
|
||||
|
||||
above_tree_id = self._supply.get_previous_id(
|
||||
self._rendered.upper_tree_id)
|
||||
|
||||
if above_tree_id is None:
|
||||
break # We've hit the top of the supply
|
||||
|
||||
self._rendered.add_above(self._render_tree(above_tree_id))
|
||||
|
||||
def _fill_screen_downwards(self) -> None:
|
||||
"""
|
||||
Eerily similar to _fill_screen_upwards()...
|
||||
"""
|
||||
|
||||
if self._rendered is None:
|
||||
raise TreeException((
|
||||
"Can't fill screen downwards without a TreeList. This exception"
|
||||
" should never occur."
|
||||
))
|
||||
|
||||
while True:
|
||||
if self._rendered.lower_offset >= self.height - 1:
|
||||
break
|
||||
|
||||
below_tree_id = self._supply.get_next_id(
|
||||
self._rendered.lower_tree_id)
|
||||
|
||||
if below_tree_id is None:
|
||||
break # We've hit the bottom of the supply
|
||||
|
||||
self._rendered.add_below(self._render_tree(below_tree_id))
|
||||
|
||||
def render_display_lines(self) -> None:
|
||||
"""
|
||||
This function updates the display lines (step 2).
|
||||
|
||||
It should be called just before drawing the display lines to the curses
|
||||
window.
|
||||
"""
|
||||
filler_line = AttributedText(" " * self.width)
|
||||
|
||||
if self._rendered is None:
|
||||
self._display_lines = [filler_line] * self.height
|
||||
return
|
||||
|
||||
lines = []
|
||||
|
||||
if self._rendered.upper_offset > 0:
|
||||
# Fill the screen with empty lines until we hit the actual messages
|
||||
lines.extend([filler_line] * self._rendered.upper_offset)
|
||||
|
||||
rendered_lines = self._rendered.to_lines(start=0, stop=self.height-1)
|
||||
lines.extend(line for line, rendered in rendered_lines)
|
||||
|
||||
if self._rendered.lower_offset < self.height - 1:
|
||||
# Fill the rest of the screen with empty lines
|
||||
lines_left = self.height - 1 - self._rendered.lower_offset
|
||||
lines.extend([filler_line] * lines_left)
|
||||
|
||||
self._display_lines = lines
|
||||
|
||||
# SCROLLING
|
||||
|
||||
def center_anchor(self) -> None:
|
||||
"""
|
||||
Center the anchor vertically on the screen.
|
||||
|
||||
This does not render anything.
|
||||
"""
|
||||
|
||||
pass # TODO
|
||||
|
||||
def ensure_anchor_is_visible(self) -> None:
|
||||
"""
|
||||
Scroll up or down far enough that the anchor is completely visible.
|
||||
|
||||
If the anchor is higher than the screen, scroll such that the first
|
||||
line of the anchor is at the top of the screen.
|
||||
|
||||
This does not render anything.
|
||||
"""
|
||||
|
||||
pass # TODO
|
||||
|
||||
def anchor_center_element(self) -> None:
|
||||
"""
|
||||
Select the element closest to the center of the screen (vertically) as
|
||||
anchor. Set the anchor offset such that no scrolling happens.
|
||||
|
||||
This function updates the internal TreeList (step 1).
|
||||
"""
|
||||
|
||||
pass # TODO
|
||||
|
||||
# FOLDING
|
||||
|
||||
def is_folded(self, element_id: Id) -> bool:
|
||||
"""
|
||||
Check whether an element is folded.
|
||||
|
||||
This does not render anything.
|
||||
"""
|
||||
|
||||
return element_id in self._folded
|
||||
|
||||
def fold(self, element_id: Id) -> None:
|
||||
"""
|
||||
Fold an element.
|
||||
|
||||
This does not render anything.
|
||||
"""
|
||||
|
||||
self._folded.add(element_id)
|
||||
|
||||
def unfold(self, element_id: Id) -> None:
|
||||
"""
|
||||
Unfold an element.
|
||||
|
||||
This does not render anything.
|
||||
"""
|
||||
|
||||
if element_id in self._folded:
|
||||
self._folded.remove(element_id)
|
||||
|
||||
def toggle_fold(self, element_id: Id) -> bool:
|
||||
"""
|
||||
Toggle whether an element is folded.
|
||||
|
||||
Returns whether the element is folded now.
|
||||
|
||||
This does not render anything.
|
||||
"""
|
||||
|
||||
if self.is_folded(element_id):
|
||||
self.unfold(element_id)
|
||||
return False
|
||||
else:
|
||||
self.fold(element_id)
|
||||
return True
|
||||
|
||||
# Terminology:
|
||||
#
|
||||
# root
|
||||
# ancestor
|
||||
# parent
|
||||
# sibling
|
||||
# child
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
import collections
|
||||
from typing import Deque, List, Optional, Tuple
|
||||
|
||||
from .element import Id, RenderedElement
|
||||
from .markup import AttributedText
|
||||
|
||||
__all__ = ["TreeList"]
|
||||
|
||||
class TreeList:
|
||||
"""
|
||||
This class is the stage between tree-like Element structures and lines of
|
||||
text like the TreeDisplay's DisplayLines.
|
||||
|
||||
It keeps track of the results of rendering Element trees, and also the top
|
||||
and bottom tree's ids, so the TreeList can be expanded easily by appending
|
||||
trees to the top and bottom.
|
||||
|
||||
Despite its name, the "trees" it stores are just flat lists, and they're
|
||||
stored in a flat deque one message at a time. Its name comes from how i is
|
||||
used with rendered Element trees.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
tree: List[RenderedElement],
|
||||
anchor_id: Id,
|
||||
) -> None:
|
||||
self._deque: Deque[RenderedElement] = collections.deque()
|
||||
|
||||
# The offsets can be thought of as the index of a line relative to the
|
||||
# anchor's first line.
|
||||
#
|
||||
# The upper offset is the index of the uppermost message's first line.
|
||||
# The lower offset is the index of the lowermost message's LAST line.
|
||||
self._upper_offset: int
|
||||
self._lower_offset: int
|
||||
|
||||
# The upper and lower tree ids are the ids of the uppermost or
|
||||
# lowermost tree added to the TreeList. They can be used to request the
|
||||
# previous or next tree from an ElementSupply.
|
||||
self._upper_tree_id: Id
|
||||
self._lower_tree_id: Id
|
||||
|
||||
self._add_first_tree(tree, anchor_id)
|
||||
|
||||
@property
|
||||
def upper_offset(self) -> int:
|
||||
return self._upper_offset
|
||||
|
||||
@property
|
||||
def lower_offset(self) -> int:
|
||||
return self._lower_offset
|
||||
|
||||
@property
|
||||
def upper_tree_id(self) -> Id:
|
||||
return self._upper_tree_id
|
||||
|
||||
@property
|
||||
def lower_tree_id(self) -> Id:
|
||||
return self._lower_tree_id
|
||||
|
||||
def offset_by(self, delta: int) -> None:
|
||||
"""
|
||||
Change all the TreeList's offsets by a delta (which is added to each
|
||||
offset).
|
||||
"""
|
||||
|
||||
self._upper_offset += delta
|
||||
self._lower_offset += delta
|
||||
|
||||
def _add_first_tree(self,
|
||||
tree: List[RenderedElement],
|
||||
anchor_id: Id
|
||||
) -> None:
|
||||
if len(tree) == 0:
|
||||
raise ValueError("The tree must contain at least one element")
|
||||
|
||||
tree_id = tree[0].element.id
|
||||
self._upper_tree_id = tree_id
|
||||
self._lower_tree_id = tree_id
|
||||
|
||||
offset = 0
|
||||
found_anchor = False
|
||||
|
||||
for rendered in tree:
|
||||
if rendered.element.id == anchor_id:
|
||||
found_anchor = True
|
||||
self._upper_offset = -offset
|
||||
|
||||
offset += rendered.height
|
||||
|
||||
if not found_anchor:
|
||||
raise ValueError("The initial tree must contain the anchor")
|
||||
|
||||
# Subtracting 1 because the lower offset is the index of the lowermost
|
||||
# message's last line, not the first line of a hypothetical message
|
||||
# below that.
|
||||
self._lower_offset = offset - 1
|
||||
|
||||
self._deque.extend(tree)
|
||||
|
||||
def add_above(self, tree: List[RenderedElement]) -> None:
|
||||
"""
|
||||
Add a rendered tree above all current trees.
|
||||
"""
|
||||
|
||||
if len(tree) == 0:
|
||||
raise ValueError("The tree must contain at least one element")
|
||||
|
||||
self._upper_tree_id = tree[0].element.id
|
||||
|
||||
for rendered in reversed(tree):
|
||||
self._deque.appendleft(rendered)
|
||||
self._upper_offset -= rendered.height
|
||||
|
||||
# Alternative to the above for loop
|
||||
#delta = sum(map(lambda r: r.height, tree))
|
||||
#self._upper_offset -= delta
|
||||
#self._deque.extendLeft(reversed(tree))
|
||||
|
||||
def add_below(self, tree: List[RenderedElement]) -> None:
|
||||
"""
|
||||
Add a rendered tree below all current trees.
|
||||
"""
|
||||
|
||||
if len(tree) == 0:
|
||||
raise ValueError("The tree must contain at least one element")
|
||||
|
||||
self._lower_tree_id = tree[0].element.id
|
||||
|
||||
for rendered in tree:
|
||||
self._deque.append(rendered)
|
||||
self._lower_offset += rendered.height
|
||||
|
||||
# Alternative to the above for loop
|
||||
#delta = sum(map(lambda r: r.height, tree))
|
||||
#self._lower_offset += delta
|
||||
#self._deque.extend(tree)
|
||||
|
||||
def to_lines(self,
|
||||
start: Optional[int] = None,
|
||||
stop: Optional[int] = None,
|
||||
) -> List[Tuple[AttributedText, RenderedElement]]:
|
||||
|
||||
offset = self.upper_offset
|
||||
lines: List[Tuple[AttributedText, RenderedElement]] = []
|
||||
|
||||
# I'm creating this generator instead of using two nested for loops
|
||||
# below, because I want to be able to break out of the for loop without
|
||||
# the code getting too ugly, and because it's fun :)
|
||||
all_lines = ((line, rendered)
|
||||
for rendered in self._deque
|
||||
for line in rendered.lines)
|
||||
|
||||
for line, rendered in all_lines:
|
||||
after_start = start is not None and offset >= start
|
||||
before_stop = stop is not None and offset <= stop
|
||||
|
||||
if after_start and before_stop:
|
||||
lines.append((line, rendered))
|
||||
|
||||
if not before_stop:
|
||||
break
|
||||
|
||||
offset += 1
|
||||
|
||||
return lines
|
||||
11
cheuph/user_list_widget.py
Normal file
11
cheuph/user_list_widget.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
__all__ = ["UserListWidget"]
|
||||
|
||||
class UserListWidget:
|
||||
"""
|
||||
This widget displays the users currently connected to a Room.
|
||||
|
||||
It must be notified of changes in the user list by the RoomWidget it is a
|
||||
part of.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
from typing import List
|
||||
|
||||
from .attributed_text_widget import *
|
||||
from .tree_display_widget import *
|
||||
|
||||
__all__: List[str] = []
|
||||
__all__ += attributed_text_widget.__all__
|
||||
__all__ += tree_display_widget.__all__
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
from typing import Any, FrozenSet
|
||||
|
||||
import urwid
|
||||
|
||||
from ..element_supply import ElementSupply
|
||||
from ..markup import AT
|
||||
from ..tree_display import TreeDisplay
|
||||
from .attributed_text_widget import AttributedTextWidget
|
||||
|
||||
__all__ = ["TreeDisplayWidget"]
|
||||
|
||||
class TreeDisplayWidget(urwid.WidgetWrap):
|
||||
def __init__(self, supply: ElementSupply) -> None:
|
||||
self._display = TreeDisplay(supply, 80, 50)
|
||||
|
||||
self._sizing = frozenset({"box"})
|
||||
self._selectable = False
|
||||
|
||||
# I could set wrap="clip", but the TreeDisplay should already cut its
|
||||
# display_lines to the correct width, based on its size. Leaving the
|
||||
# wrap on might help with users spotting things going wrong.
|
||||
self._text_widget = AttributedTextWidget(AT())
|
||||
|
||||
super().__init__(urwid.Filler(self._text_widget))
|
||||
|
||||
@property
|
||||
def display(self) -> TreeDisplay:
|
||||
return self._display
|
||||
|
||||
def render(self, size: Any, focus: Any) -> Any:
|
||||
self._display.width, self._display.height = size
|
||||
self._display.rerender()
|
||||
self._display.render_display_lines()
|
||||
|
||||
text = AT("\n").join(self._display.display_lines)
|
||||
self._text_widget.set_attributed_text(text)
|
||||
|
||||
return self._w.render(size, focus)
|
||||
Loading…
Add table
Add a link
Reference in a new issue