Implement basic AttributedLinesWidget

This commit is contained in:
Joscha 2019-05-28 11:17:58 +00:00
parent 66a67f3f28
commit 32bc9af2d8
3 changed files with 217 additions and 14 deletions

View file

@ -1,5 +1,12 @@
__all__ = ["AttributedLines"] import collections
from typing import Deque, Iterator, List, Optional, Tuple
from .markup import AT, AttributedText, Attributes
__all__ = ["Line", "AttributedLines"]
Line = Tuple[Attributes, AttributedText]
class AttributedLines: class AttributedLines:
""" """
@ -7,11 +14,153 @@ class AttributedLines:
vertical offset. vertical offset.
When rendering a tree of messages, the RenderedMessage-s are drawn line by When rendering a tree of messages, the RenderedMessage-s are drawn line by
line to an AttributedLines. AttributedLines. The AttributedLines is then line to an AttributedLines. The AttributedLines is then displayed in an
displayed in an AttributedLinesWidget. AttributedLinesWidget.
Multiple AttributedLines can be concatenated, keeping either the first or Multiple AttributedLines can be concatenated, keeping either the first or
the second AttributedLines's offset. the second AttributedLines's offset.
""" """
pass def __init__(self, lines: Optional[List[Line]] = None) -> None:
self.upper_offset = 0
self._lines: Deque[Line] = collections.deque(lines or [])
def __iter__(self) -> Iterator[Line]:
return self._lines.__iter__()
def __len__(self) -> int:
return len(self._lines)
@property
def lower_offset(self) -> int:
# When there's one element in the list, the lower and upper offsets are
# the same. From that follows that in an empty list, the lower offset
# must be smaller than the upper offset.
return self.upper_offset + (len(self) - 1)
@lower_offset.setter
def lower_offset(self, lower_offset: int) -> None:
self.upper_offset = lower_offset - (len(self) - 1)
def append_above(self, line: Line) -> None:
self._lines.appendleft(line)
self.upper_offset -= 1
def append_below(self, line: Line) -> None:
self._lines.append(line)
# lower offset does not need to be modified since it's calculated based
# on the upper offset
def expand_above(self, lines: "AttributedLines") -> None:
"""
Prepend an AttributedLines, ignoring its offsets and using the current
AttributedLines's offsets instead.
"""
self._lines.extendleft(lines._lines)
self.upper_offset -= len(lines)
def expand_below(self, lines: "AttributedLines") -> None:
"""
Append an AttributedLines, ignoring its offsets and using the current
AttributedLines's offsets instead.
"""
self._lines.extend(lines._lines)
# lower offset does not need to be modified since it's calculated based
# on the upper offset
def between(self, start_offset: int, end_offset: int) -> "AttributedLines":
lines = []
for i, line in enumerate(self):
line_offset = self.upper_offset + i
if start_offset <= line_offset <= end_offset:
lines.append(line)
attr_lines = AttributedLines(lines)
attr_lines.upper_offset = max(start_offset, self.upper_offset)
return attr_lines
def to_size(self, start_offset: int, end_offset: int) -> "AttributedLines":
between = self.between(start_offset, end_offset)
while between.upper_offset > start_offset:
between.append_above(({}, AT()))
while between.lower_offset < end_offset:
between.append_below(({}, AT()))
return between
@staticmethod
def render_line(
line: Line,
width: int,
horizontal_offset: int,
offset_char: str = " ",
overlap_char: str = "",
) -> AttributedText:
attributes, text = line
# column to the right is reserved for the overlap char
text_width = width - 1
start_offset = horizontal_offset
end_offset = start_offset + text_width
result: AttributedText = AT()
if start_offset < 0:
pad_length = min(text_width, -start_offset)
result += AT(offset_char * pad_length)
if end_offset < 0:
pass # the text should not be displayed at all
elif end_offset < len(text):
if start_offset > 0:
result += text[start_offset:end_offset]
else:
result += text[:end_offset]
else:
if start_offset > 0:
result += text[start_offset:]
else:
result += text
if end_offset > len(text):
pad_length = min(text_width, end_offset - len(text))
result += AT(offset_char * pad_length)
if end_offset < len(text):
result += AT(overlap_char)
else:
result += AT(offset_char)
for k, v in attributes.items():
result = result.set(k, v)
return result
def render_lines(self,
width: int,
height: int,
horizontal_offset: int,
) -> List[AttributedText]:
lines = []
for line in self.to_size(0, height - 1):
lines.append(self.render_line(line, width, horizontal_offset))
return lines
def render(self,
width: int,
height: int,
horizontal_offset: int,
) -> AttributedText:
lines = self.render_lines(width, height,
horizontal_offset=horizontal_offset)
return AT("\n").join(lines)

View file

@ -1,7 +1,15 @@
from typing import Any, Optional, Tuple
import urwid
from .attributed_lines import AttributedLines
from .attributed_text_widget import AttributedTextWidget
from .markup import AT, AttributedText, Attributes
__all__ = ["AttributedLinesWidget"] __all__ = ["AttributedLinesWidget"]
class AttributedLinesWidget: class AttributedLinesWidget(urwid.WidgetWrap):
""" """
This widget draws lines of AttributedText with a horizontal and a vertical 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) offset. It can retrieve the attributes of any character by its (x, y)
@ -9,6 +17,60 @@ class AttributedLinesWidget:
When clicked, it sends an event containing the attributes of the character When clicked, it sends an event containing the attributes of the character
that was just clicked. that was just clicked.
Uses the following config values:
- "filler_symbol"
- "overflow_symbol"
""" """
pass def __init__(self,
lines: Optional[AttributedLines] = None,
) -> None:
self._horizontal_offset = 0
self._text = AttributedTextWidget(AT())
self._filler = urwid.Filler(self._text, valign=urwid.TOP)
super().__init__(self._filler)
self.set_lines(lines or AttributedLines())
@property
def horizontal_offset(self) -> int:
return self._horizontal_offset
@horizontal_offset.setter
def horizontal_offset(self, offset: int) -> None:
if offset != self._horizontal_offset:
self._horizontal_offset = offset
self._invalidate()
@property
def upper_offset(self) -> int:
return self._lines.upper_offset
@upper_offset.setter
def upper_offset(self, offset: int) -> None:
self._lines.upper_offset = offset
self._invalidate()
@property
def lower_offset(self) -> int:
return self._lines.lower_offset
@lower_offset.setter
def lower_offset(self, offset: int) -> None:
self._lines.lower_offset = offset
self._invalidate()
def set_lines(self, lines: AttributedLines) -> None:
self._lines = lines
self._invalidate()
def render(self, size: Tuple[int, int], focus: bool) -> None:
width, height = size
text = self._lines.render(width, height, self.horizontal_offset)
self._text.set_attributed_text(text)
return super().render(size, focus)

View file

@ -52,14 +52,6 @@ class AttributedTextWidget(urwid.Text):
self._attributed_text = text self._attributed_text = text
super().set_text(self._convert_to_markup(text)) super().set_text(self._convert_to_markup(text))
def set_text(self, *args: Any, **kwargs: Any) -> None:
"""
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: def get_attributed_text(self) -> AttributedText:
""" """
Returns the currently used AttributedText. Returns the currently used AttributedText.