diff --git a/CHANGELOG.md b/CHANGELOG.md index 884e9a4..f56228a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Dumdum client improvements: - Dynamically wrap message content for larger window sizes + - Automatically scroll message feed ## [0.2.1] - 2024-03-21 diff --git a/src/dumdum/client/chat_frame.py b/src/dumdum/client/chat_frame.py index fc61a9c..96c2d2a 100644 --- a/src/dumdum/client/chat_frame.py +++ b/src/dumdum/client/chat_frame.py @@ -144,7 +144,7 @@ def __init__(self, parent: ChatFrame): self.messages: list[MessageView] = [] - self._scroll_frame = ScrollableFrame(self) + self._scroll_frame = ScrollableFrame(self, autoscroll=True) self._scroll_frame.grid(row=0, column=0, sticky="nesw") self._last_configured = None diff --git a/src/dumdum/client/scrollable_frame.py b/src/dumdum/client/scrollable_frame.py index c626ead..c566e29 100644 --- a/src/dumdum/client/scrollable_frame.py +++ b/src/dumdum/client/scrollable_frame.py @@ -4,9 +4,13 @@ class ScrollableFrame(Frame): - def __init__(self, *args, **kwargs): + _last_scrollregion: tuple[int, int, int, int] | None + + def __init__(self, *args, autoscroll: bool = False, **kwargs): super().__init__(*args, **kwargs) + self.autoscroll = autoscroll + self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure(0, weight=1) @@ -34,6 +38,7 @@ def __init__(self, *args, **kwargs): self._canvas.bind("", lambda event: self._update()) self.inner.bind("", self._on_inner_configure) + self._last_scrollregion = None self._scrolled_widgets = WeakSet() self._style = Style(self) self._update_rate = 125 @@ -51,6 +56,8 @@ def _update_loop(self): self.after(self._update_rate, self._update_loop) def _update(self): + scroll_edges = self._get_scroll_edges() + # self._canvas.bbox("all") doesn't update until window resize # so we're relying on the inner frame's requested height instead. new_width = max(self._canvas.winfo_width(), self.inner.winfo_reqwidth()) @@ -62,6 +69,14 @@ def _update(self): self._update_scrollbar_visibility(self._xscrollbar) self._update_scrollbar_visibility(self._yscrollbar) self._propagate_scroll_binds(self.inner) + self._update_scroll_edges(bbox, *scroll_edges) + + def _get_scroll_edges(self) -> tuple[bool, bool]: + xview = self._canvas.xview() + yview = self._canvas.yview() + scrolled_to_right = xview[1] == 1 and xview[0] != 0 + scrolled_to_bottom = yview[1] == 1 and yview[0] != 0 + return scrolled_to_right, scrolled_to_bottom def _propagate_scroll_binds(self, parent: Widget): if parent not in self._scrolled_widgets: @@ -72,6 +87,23 @@ def _propagate_scroll_binds(self, parent: Widget): for child in parent.winfo_children(): self._propagate_scroll_binds(child) + def _update_scroll_edges( + self, + bbox: tuple[int, int, int, int], + scrolled_to_right: bool, + scrolled_to_bottom: bool, + ) -> None: + self._last_scrollregion, last_bbox = bbox, self._last_scrollregion + if not self.autoscroll: + return + elif bbox == last_bbox: + return + + if scrolled_to_right: + self._canvas.xview_moveto(1) + if scrolled_to_bottom: + self._canvas.yview_moveto(1) + def _on_mouse_xscroll(self, event: Event): delta = int(-event.delta / 100) self._canvas.xview_scroll(delta, "units")