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:
|
||||
"""
|
||||
|
|
@ -7,11 +14,153 @@ class AttributedLines:
|
|||
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.
|
||||
line to an 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
|
||||
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"]
|
||||
|
||||
|
||||
class AttributedLinesWidget:
|
||||
class AttributedLinesWidget(urwid.WidgetWrap):
|
||||
"""
|
||||
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)
|
||||
|
|
@ -9,6 +17,60 @@ class AttributedLinesWidget:
|
|||
|
||||
When clicked, it sends an event containing the attributes of the character
|
||||
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
|
||||
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:
|
||||
"""
|
||||
Returns the currently used AttributedText.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue