Implement basic AttributedLinesWidget
This commit is contained in:
parent
66a67f3f28
commit
32bc9af2d8
3 changed files with 217 additions and 14 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue