From a9ddf275258285341ff6245b782336c0058f23a8 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 7 Jun 2019 17:27:29 +0000 Subject: [PATCH] Remove old files --- cheuph/message.py | 69 ---- cheuph/message_supply.py | 140 -------- cheuph/message_tree_widget.py | 585 ------------------------------- cheuph/rendered_message_cache.py | 10 - 4 files changed, 804 deletions(-) delete mode 100644 cheuph/message.py delete mode 100644 cheuph/message_supply.py delete mode 100644 cheuph/message_tree_widget.py delete mode 100644 cheuph/rendered_message_cache.py diff --git a/cheuph/message.py b/cheuph/message.py deleted file mode 100644 index 593a149..0000000 --- a/cheuph/message.py +++ /dev/null @@ -1,69 +0,0 @@ -import datetime -from dataclasses import dataclass -from typing import Hashable, List, Optional - -from .attributed_lines import AttributedLines -from .markup import AT, AttributedText - -__all__ = ["Id", "Message", "RenderedMessage"] - - -Id = Hashable - - -@dataclass -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. - """ - - id: Id - parent_id: Optional[Id] - time: datetime.datetime - nick: str - content: str - - def render(self, width: int) -> "RenderedMessage": - lines = self.content.split("\n") - - meta = AT(self.time.strftime("%H:%M ")) - - nick = AT(f"[{self.nick}] ") - nick_spaces = AT(" " * len(nick)) - - result = [] - result.append(nick + AT(lines[0])) - result.extend(nick_spaces + AT(line) for line in lines[1:]) - - return RenderedMessage(self.id, meta, result) - -@dataclass -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. - """ - - message_id: Id - meta: AttributedText - lines: List[AttributedText] diff --git a/cheuph/message_supply.py b/cheuph/message_supply.py deleted file mode 100644 index 823a8c0..0000000 --- a/cheuph/message_supply.py +++ /dev/null @@ -1,140 +0,0 @@ -import abc -from typing import Dict, List, Optional - -from .exceptions import MessageSupplyException -from .message import Id, Message - -__all__ = ["MessageSupply", "InMemoryMessageSupply"] - - -class MessageSupply(abc.ABC): - """ - 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. - """ - - @abc.abstractmethod - def get(self, message_id: Id) -> Message: - pass - - @abc.abstractmethod - def children_ids(self, message_id: Id) -> List[Id]: - pass - - @abc.abstractmethod - def sibling_ids(self, message_id: Id) -> List[Id]: - pass - - @abc.abstractmethod - def parent_id(self, message_id: Id) -> Optional[Id]: - pass - - def oldest_ancestor_id(self, message_id: Id) -> Id: - ancestor_id = message_id - - while True: - parent_id = self.parent_id(ancestor_id) - if parent_id is None: break - ancestor_id = parent_id - - return ancestor_id - - def previous_id(self, message_id: Id) -> Optional[Id]: - sibling_ids = self.sibling_ids(message_id) - - try: - i = sibling_ids.index(message_id) - if i <= 0: - return None - else: - return sibling_ids[i - 1] - except ValueError: - return None - - def next_id(self, message_id: Id) -> Optional[Id]: - sibling_ids = self.sibling_ids(message_id) - - try: - i = sibling_ids.index(message_id) - if i >= len(sibling_ids) - 1: - return None - else: - return sibling_ids[i + 1] - except ValueError: - return None - - @abc.abstractmethod - def lowest_root_id(self) -> Optional[Id]: - pass - -class InMemoryMessageSupply(MessageSupply): - """ - This message supply stores messages in memory. It orders the messages by - their ids. - """ - - def __init__(self) -> None: - self._messages: Dict[Id, Message] = {} - self._children: Dict[Id, List[Message]] = {} - - def add(self, message: Message) -> None: - if message.id in self._messages: - self.remove(message.id) - - self._messages[message.id] = message - - if message.parent_id is not None: - children = self._children.get(message.parent_id, []) - children.append(message) - children.sort(key=lambda m: m.id) - self._children[message.parent_id] = children - - def remove(self, message_id: Id) -> None: - message = self._messages.get(message_id) - if message is None: return - - self._messages.pop(message) - - if message.parent_id is not None: - children = self._children.get(message.id) - if children is not None: # just to satisfy mypy - children.remove(message) - - if not children: - self._children.pop(message.id) - - def get(self, message_id: Id) -> Message: - message = self._messages.get(message_id) - - if message is None: - raise MessageSupplyException( - f"message with id {message_id!r} does not exist") - - return message - - def child_ids(self, message_id: Id) -> List[Id]: - return [m.id for m in self._children.get(message_id, [])] - - def parent_id(self, message_id: Id) -> Optional[Id]: - message = self.get(message_id) - return message.parent_id - - def sibling_ids(self, message_id: Id) -> List[Id]: - parent_id = self.parent_id(message_id) - - if parent_id is None: - roots = [m for m in self._messages.values() if m.parent_id is None] - sibling_ids = list(sorted(root.id for root in roots)) - else: - sibling_ids = self.children_ids(parent_id) - - return sibling_ids - - def lowest_root_id(self) -> Optional[Id]: - roots = list(sorted(self._messages.keys())) - return roots[-1] if roots else None diff --git a/cheuph/message_tree_widget.py b/cheuph/message_tree_widget.py deleted file mode 100644 index aa549bf..0000000 --- a/cheuph/message_tree_widget.py +++ /dev/null @@ -1,585 +0,0 @@ -from typing import Optional, Set, Tuple - -import urwid -import yaboli - -from .attributed_lines import AttributedLines -from .attributed_lines_widget import AttributedLinesWidget -from .markup import AT, AttributedText -from .message import Id, RenderedMessage -from .message_supply import MessageSupply -from .rendered_message_cache import RenderedMessageCache - -__all__ = ["MessageTreeWidget"] - - -""" -(lines, delta, hit_top, hit_bottom) - -- lines - the rendered AttributedLines -- delta - how the absolute_anchor_offset needed to be changed to comply with - the scrolling rules -- hit_top - whether the renderer arrived at the topmost message of the supply -- hit_bottom - whether the renderer arrived at the bottommost message of the - supply -""" -RenderResult = Tuple[AttributedLines, int, bool, bool] - -class MessageTreeWidget(urwid.WidgetWrap): - """ - 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. - """ - - ROOM_IS_EMPTY_MESSAGE = "" - - def __init__(self, - # TODO config - room: yaboli.Room, - supply: MessageSupply, - ) -> None: - - # yaboli.Room, used only for the current nick - self.room = room - # A supply of Message-s - self.supply = supply - # A cache of RenderedMessage-s - self.rendered = RenderedMessageCache() - # The lines that were last rendered - self.lines = AttributedLines() - # Widget tha displays self.lines - self.lines_widget = AttributedLinesWidget() - # A placeholder if there are no messages to display - self.placeholder = urwid.Filler(urwid.Text(self.ROOM_IS_EMPTY_MESSAGE, - align=urwid.CENTER)) - - # The id of the message that the cursor is displayed under. - self.cursor_id: Optional[Id] = None - # If the anchor is None, but the cursor isn't, the cursor is used as - # the anchor. - self.anchor_id: Optional[Id] = None - # The anchor's line's offset on the screen, measured in percent of the - # total height. For more information, see the comment above - # _get_absolute_offset() and _get_relative_offset(). - self.anchor_offset = 0.5 - - # The last known width (use this to invalidate the cache when needed) - self.width = 80 - # Columns per indentation level - self.indent_width = 2 - # Columns at beginning of line that are reserved for date etc. - self.meta_width = 6 # "HH:MM " - # Columns at the end to mark overlapping lines - self.overlap_width = 1 - - # Which sub-threads are folded - #self.folds: Set[Id] = set() # TODO - - super().__init__(self.placeholder) - - @property - def usable_width(self) -> int: - """ - The width that's available for everything, while staying inside the - bounds of the overlap indicators. - """ - - return self.width - self.overlap_width - - @property - def content_width(self) -> int: - """ - The width that's left over for messages and their indentation - information, after meta_width etc. are removed. - """ - - return self.usable_width - self.meta_width - - # Offsets - - """ - On offsets: - - An offset of 0.0 describes the middle of the first line on screen, whereas - an offset of 1.0 describes the middle of the last line on screen. - - An example: - - line 0 - 0.0 - line 1 - 0.25 - line 2 - 0.5 - line 3 - 0.75 - line 4 - 1.0 - - Let l be a line's index (starts with 0), o the offset and n the number of - lines visible on the screen. - - OFFSET -> LINE NUMBER - - l = round(o * (n - 1)) - - LINE NUMBER -> OFFSET - - o = l / (n - 1) - - Be careful if only one line is visible on the screen! Setting o to 0.5 is - recommended in that case. - """ - - @staticmethod - def _get_absolute_offset(offset: float, height: int) -> int: - return round(offset * (height - 1)) - - @staticmethod - def _get_relative_offset(line: int, height: int) -> float: - if height <= 1: - return 0.5 - - return line / (height - 1) - - @property - def absolute_anchor_offset(self) -> int: - return self._get_absolute_offset(self.anchor_offset, self.height) - - @absolute_anchor_offset.setter - def absolute_anchor_offset(self, offset: int) -> None: - self.anchor_offset = self._get_relative_offset(offset, self.height) - - # Message cache operations and maintenance - - def invalidate_message(self, message_id: Id) -> None: - """ - Invalidate the RenderedMessage cached under message_id. - """ - - self.cache.invalidate(message_id) - - def invalidate_all_messages(self) -> None: - """ - Invalidate all cached RenderedMessage-s. - """ - - self.cache.invalidate_all() - - # Rendering a single message - - def _render_message(self, message_id: Id, width: int) -> RenderedMessage: - """ - Somehow obtain a RenderedMessage for the specified message_id. - - If the cache does not contain this message yet, or if it contains an - invalid message (wrong width), the message is rendered and then added - to the cache. Otherwise, the cached message is returned. - """ - - cached = self.cache.get(message_id) - - if cached.width != width: - self.invalidate_message(message_id) - cached = None - - if cached is not None: - return cached - - message = self.supply.get(message_id) - # TODO give current Room to message so it knows the current nick(s) - # TODO give current meta format to message - rendered = message.render(width) - self.cache.set(message_id, rendered) - return rendered - - def _render_message_lines(self, - message_id: Id, - indent: AttributedText = AT(), - ) -> AttributedLines: - """ - Render the message with the specified id into AttributedLines. - - The lines have the format: - - - - Each line has the following line-wide attributes: - - - mid - the id of the message - - offset - the offset to the message's topmost line - """ - - width = self.content_width - len(indent) - rendered = self._render_message(message_id, width) - - meta = rendered.meta - meta_spaces = AT(" " * len(rendered.meta)) - - lines = AttributedLines() - - mid = rendered.message_id - offset = 0 - - lines.append_below({"mid": mid, "offset": offset}, - meta + indent + rendered.lines[0]) - - for line in rendered.lines[1:]: - offset += 1 - lines.append_below({"mid": mid, "offset": offset}, - meta_spaces + indent + line) - - return lines - - def _render_cursor(self, indent: AttributedText = AT()) -> AttributedLines: - # Quick and dirty cursor rendering - nick = self.room.session.nick - text = indent + AT(f"[{nick}]") - - lines = AttributedLines() - lines.append_below({"cursor": True}, text) - - return lines - - # Rendering the tree - - def _render_subtree(self, - lines: AttributedLines, - root_id: Id, - indent: AttributedText = AT(), - ) -> None: - """ - Render a (sub-)tree to the AttributedLines specified. - - This function also sets the vertical offset to be 0 on the anchor's - first line, or the cursor's first (and only) line if the cursor is the - anchor. - - - lines - the AttributedLines object to render to - - root_id - the id of the message to start rendering at - - indent - the indent string to prepend - """ - - if self.anchor_id == root_id: - lines.lower_offset = -1 - - # Render main message (root) - rendered = self._render_message_lines(root_id, indent) - lines.extend_below(rendered) - - # Determine new indent - extra_indent = AT("┃ " if self.cursor_id == root_id else "│ ") - new_indent = indent + extra_indent - - # Render children - for child_id in self.supply.children_ids(root_id): - self._render_subtree(lines, child_id, new_indent) - - # Render cursor if necessary - if self.cursor_id == root_id: - # The cursor also acts as anchor if anchor is not specified - if self.anchor_id is None: - lines.lower_offset = -1 - - cursor_indent = indent + AT("┗━") - lines.extend_below(self._render_cursor(indent)) - - def _render_tree(self, root_id: Id) -> AttributedLines: - """ - A wrapper around _render_subtree(), for ease of use. - - Doesn't adjust the offset; the AttributedLines returned does NOT take - into account the attribute_offset. - """ - - lines = AttributedLines() - self.render_subtree(lines, root_id) - return lines - - def _render_tree_containing(self, message_id: Id) -> AttributedLines: - """ - Similar to _render_tree(), but finds the root of the specified message - first. - """ - - root_id = self.supply.oldest_ancestor_id(message_id) - # Puts the message with the specific id into the cache - return self._render_tree(root_id) - - def _expand_upwards_until(self, - lines: AttributedLines, - ancestor_id: Id, - target_upper_offset: int, - ) -> Tuple[Id, bool]: - """ - Render trees (including the cursor) and prepend them to the - AttributedLines until its upper_offset matches or exceeds the - target_upper_offset. - - Returns whether it has hit the top of the supply. - - Assumes that the ancestor_id's tree is already rendered. Moves upwards - through the siblings of the ancestor_id. - """ - - # This loop doesn't use a condition but rather break-s, because I think - # it looks cleaner that way. I don't like mixing conditions and breaks - # too much, if the conditions are of the same importance. - last_rendered_id = ancestor_id - - while True: - # Doing this check first because of a possible edge case: Using the - # other order, if the first message fills the screen, the function - # would return False, even though we've hit the top. - next_id = self.supply.previous_id(last_rendered_id) - if next_id is None: - return last_rendered_id, True - - if lines.upper_offset <= target_upper_offset: - return last_rendered_id, False - - lines.extend_above(self._render_tree(next_id)) - last_rendered_id = next_id - - def _expand_downwards_until(self, - lines: AttributedLines, - ancestor_id: Id, - target_lower_offset: int, - ) -> Tuple[Id, bool]: - """ - Render trees (including the cursor, even if it's at the bottom) and - append them to the AttributedLines until its lower_offset matches or - exceeds the target_lower_offset. - - Returns whether it has hit the bottom of the supply. - - Assumes that the ancestor_id's tree is already rendered. Moves - downwards through the siblings of the ancestor_id. - """ - - # Almost the same as _expand_upwards_until(), but with small changes. - # Maybe these could one day be combined into one function. - # - # This loop doesn't use a condition but rather break-s, because I think - # it looks cleaner that way. I don't like mixing conditions and breaks - # too much, if the conditions are of the same importance. - - last_rendered_id = ancestor_id - - while True: - # Doing this check first because of a possible edge case: Using the - # other order, if the last message fills the screen, the function - # would return False, even though we've hit the bottom. - next_id = self.supply.next_id(last_rendered_id) - if next_id is None: - break - - if lines.lower_offset >= target_lower_offset: - return last_rendered_id, False - - lines.extend_below(self._draw_tree(next_id)) - last_rendered_id = next_id - - lines.extend_below(self._render_cursor()) - return last_rendered_id, True - - # Rendering the screen - - """ - On scrolling: - - These are some restrictions on how the screen can scroll and thus how the - anchor_offset is interpreted. They are listed from most to least important. - - 1. There must always be something (a message, the cursor or similar) on the - bottommost line. - - 2. There must always be something on the topmost line. - - Good: - - ------------------------ - - - first message - | bla - | | blabla - | last message - ------------------------ - - and - - ------------------------ - first message - | bla - | blabla - | | more bla - | | even more bla - | not the last message - ------------------------ - - Bad: - - ------------------------ - first message - | bla - | | blabla - | last message - - - ------------------------ - - and - - ------------------------ - - - first message - | bla - | | blabla - | not the last message - ------------------------ - """ - - def _render_screen_from_cursor(self) -> RenderResult: - """ - Uses the following strategy: - 1. Render the cursor - 2. Render the lowest tree, if there is one - 3. Extend upwards until the top of the screen, if necessary - """ - - # Step 1 - lines = self._render_cursor() - # No need to use the anchor offset since we know we're always at the - # bottom of the screen - lines.lower_offset = self.height - 1 - delta = self.height - 1 - self.absolute_anchor_offset - - # Step 2 - hit_top: bool - lowest_root_id = self.supply.lowest_root_id() - if lowest_root_id is None: - hit_top = True - else: - lines.extend_above(self._render_tree(lowest_root_id)) - - # Step 3 - _, hit_top = self._expand_upwards_until(lines, lowest_root_id, 0) - - return lines, delta, hit_top, True # we're always at the bottom - - def _render_screen_from_anchor(self, anchor_id: Id) -> RenderResult: - """ - Uses the following strategy: - 1. Render the anchor's tree - 2. Extend upwards until the top of the screen - 3. Adjust the offset to match rule 2 - 4. Extend downwards until the bottom of the screen - 5. Adjust the offset to match rule 1 - 6. Extend upwards again until the top of the screen - """ - - delta = 0 - - # Step 1 - ancestor_id = self.supply.oldest_ancestor_id(anchor_id) - lines = self._render_tree(ancestor_id) - lines.upper_offset += self.absolute_anchor_offset - - # Step 2 - upper_id, hit_top = self._expand_upwards_until(lines, ancestor_id, 0) - - # Step 3 - if lines.upper_offset > 0: - delta -= lines.upper_offset - lines.upper_offset = 0 - - # Step 4 - _, hit_bottom = self._expand_downwards_until(lines, ancestor_id, - self.height - 1) - - # Step 5 - if lines.lower_offset < self.height - 1: - delta += self.height - 1 - lines.lower_offset - - # Step 6 - if not hit_top: - _, hit_top = self._expand_upwards_until(lines, upper_id, 0) - - return lines, delta, hit_top, hit_bottom - - def _render_screen(self) -> RenderResult: - """ - Render an AttributedLines that fills the screen (as far as possible), - taking into account the anchor offset. - """ - - if self.cursor_id is None and self.anchor_id is None: - return self._render_screen_from_cursor() - - working_id: Id - if self.anchor_id is None: - # self.cursor_id can't be None, otherwise the first if - # condition would have been met and this part wouldn't have - # been executed in the first place. - working_id = self.cursor_id # type: ignore - else: - working_id = self.anchor_id - - return self._render_screen_from_anchor(working_id) - - # Updating the internal widget - - def _update_with_lines(self, lines: AttributedLines) -> None: - """ - Update evrything that needs to be updated when a new set of lines comes - in. - """ - - self.lines = lines - self.lines_widget.set_lines(self.lines) - - self._w = self.lines_widget - self._invalidate() # Just to make sure this really gets rendered - - def redraw(self, fix_anchor_offset: bool = False) -> Tuple[bool, bool]: - """ - Render new lines and draw them (to the internal widget and thus to the - screen on the next screen update). - - Returns a tuple (hit_top, hit_bottom): - - hit_top - whether the renderer arrived at the topmost message of the - supply - - hit_bottom - whether the renderer arrived at the bottommost message - of the supply - """ - - lines, delta, hit_top, hit_bottom = self._render_screen() - self._update_with_lines(lines) - - if fix_anchor_offset and delta != 0: - self.absolute_anchor_offset += delta - - return hit_top, hit_bottom - - # Scrolling - - def scroll_by(self, delta: int) -> None: - self.absolute_anchor_offset += delta - - # Cursor movement - - # TODO diff --git a/cheuph/rendered_message_cache.py b/cheuph/rendered_message_cache.py deleted file mode 100644 index a083696..0000000 --- a/cheuph/rendered_message_cache.py +++ /dev/null @@ -1,10 +0,0 @@ -__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