From e75092f816d330e184674f1cfcb77c9aab15fa2d Mon Sep 17 00:00:00 2001 From: lovetox <philipp@hoerist.com> Date: Sat, 3 Apr 2021 22:48:50 +0200 Subject: [PATCH] Refactor scrolling --- gajim/chat_control_base.py | 197 +++++++------------------ gajim/common/storage/archive.py | 17 +-- gajim/data/gui/chat_control.ui | 4 - gajim/data/gui/groupchat_control.ui | 4 - gajim/gtk/conversation/rows/info.py | 7 +- gajim/gtk/conversation/rows/message.py | 7 +- gajim/gtk/conversation/view.py | 77 +++++----- 7 files changed, 113 insertions(+), 200 deletions(-) diff --git a/gajim/chat_control_base.py b/gajim/chat_control_base.py index 00ebd77368..24072cf7ee 100644 --- a/gajim/chat_control_base.py +++ b/gajim/chat_control_base.py @@ -25,7 +25,6 @@ import os import sys -import datetime import time import uuid import tempfile @@ -189,18 +188,16 @@ def __init__(self, parent_win, widget_name, jid, acct, self.xml.conversation_scrolledwindow.add(self.conversation_view) self._scrolled_old_upper = None self._scrolled_old_value = None - self._scrolled_previous_value = 0 - self._fetching_history = False - self._initial_fetch_done = False + self._fetch_start_upper = None + self._current_upper = 0 + self._autoscroll = True + self._no_more_messages = False vadjustment = self.xml.conversation_scrolledwindow.get_vadjustment() - vadjustment.connect( - 'changed', self._on_conversation_vadjustment_changed) - vadjustment.connect('value-changed', self._on_scroll_value_changed) - vscrollbar = self.xml.conversation_scrolledwindow.get_vscrollbar() - vscrollbar.connect('button-release-event', - self._on_scrollbar_button_release) + + vadjustment.connect('notify::upper', self._on_adj_upper_changed) + vadjustment.connect('notify::value', self._on_adj_value_changed) self.msg_textview = MessageInputTextView() self.msg_textview.connect('paste-clipboard', @@ -1413,32 +1410,30 @@ def _on_load_history(self, _button, lines): self.fetch_n_lines_history(lines) def fetch_n_lines_history(self, n_lines): - if self._fetching_history: - return - - timestamp_end = self.conversation_view.first_message_timestamp - if not timestamp_end: - timestamp_end = datetime.datetime.now() - self._fetching_history = True - adjustment = self.xml.conversation_scrolledwindow.get_vadjustment() - - # Store these values so we can restore scroll position later - self._scrolled_old_upper = adjustment.get_upper() - self._scrolled_old_value = adjustment.get_value() + row = self.conversation_view.get_first_message_row() + if row is None: + timestamp = time.time() + else: + timestamp = row.db_timestamp if self.is_groupchat: messages = app.storage.archive.get_conversation_muc_before( self.account, self.contact.jid, - timestamp_end, + timestamp, n_lines) else: messages = app.storage.archive.get_conversation_before( self.account, self.contact.jid, - timestamp_end, + timestamp, n_lines) + if not messages: + self._no_more_messages = True + print('SET NO MORE') + return + for msg in messages: if not msg: continue @@ -1468,79 +1463,6 @@ def fetch_n_lines_history(self, n_lines): error=msg.error, history=True) - def _on_edge_reached(self, _scrolledwindow, pos): - if pos != Gtk.PositionType.BOTTOM: - return - # Remove all events and set autoscroll True - app.log('autoscroll').info('Autoscroll enabled') - self.conversation_view.autoscroll = True - if self.resource: - jid = self.contact.get_full_jid() - else: - jid = self.contact.jid - types_list = [] - if self._type.is_groupchat: - types_list = ['printed_gc_msg', 'gc_msg', 'printed_marked_gc_msg'] - else: - types_list = [f'printed_{self._type}', str(self._type)] - - if not app.events.get_events(self.account, jid, types_list): - return - if not self.parent_win: - return - if (self.parent_win.get_active_control() == self and - self.parent_win.window.is_active()): - # we are at the end - if not app.events.remove_events( - self.account, jid, types=types_list): - # There were events to remove - self.redraw_after_event_removed(jid) - # XEP-0333 Send <displayed> tag - self._client.get_module('ChatMarkers').send_displayed_marker( - self.contact, - self.last_msg_id, - self._type) - self.last_msg_id = None - - def _on_edge_overshot(self, _scrolledwindow, pos): - # Fetch messages if we scroll against the top - if pos != Gtk.PositionType.TOP: - return - self.fetch_n_lines_history(30) - - def _on_scroll_value_changed(self, adjustment): - if self.conversation_view.clearing or self._fetching_history: - return - - value = adjustment.get_value() - previous_value = self._scrolled_previous_value - self._scrolled_previous_value = value - - if value >= previous_value: - return - - # Fetch messages before we reach the top - if value <= int(0.1 * adjustment.get_upper()): - self.fetch_n_lines_history(10) - - def _on_scroll_realize(self, _widget): - # Initial message fetching when ChatControl is first opened - if not self._initial_fetch_done: - self._initial_fetch_done = True - restore_lines = app.settings.get('restore_lines') - if restore_lines <= 0: - return - self.fetch_n_lines_history(restore_lines) - - def _on_scrollbar_button_release(self, scrollbar, event): - if event.get_button()[1] != 1: - # We want only to catch the left mouse button - return - - if not at_the_end(scrollbar.get_parent()): - app.log('autoscroll').info('Autoscroll disabled') - self.conversation_view.autoscroll = False - def has_focus(self): if self.parent_win: if self.parent_win.window.get_property('has-toplevel-focus'): @@ -1548,57 +1470,46 @@ def has_focus(self): return True return False - def _on_scroll(self, widget, event): - if not self.conversation_view.autoscroll: - # autoscroll is already disabled - return + def _on_adj_upper_changed(self, adj, *args): + upper = adj.get_upper() + diff = upper - self._current_upper - if widget is None: - # call from _on_conversation_view_key_press() - # SHIFT + Gdk.KEY_Page_Up - if event != Gdk.KEY_Page_Up: - return + if diff != 0: + self._current_upper = upper + if self._autoscroll: + adj.set_value(adj.get_upper() - adj.get_page_size()) + else: + # Workaround: https://gitlab.gnome.org/GNOME/gtk/merge_requests/395 + self.xml.conversation_scrolledwindow.set_kinetic_scrolling(True) + adj.set_value(adj.get_value() + diff) + + if upper == adj.get_page_size(): + # There is no scrollbar, load history until there is + self.fetch_n_lines_history(30) + + def _on_adj_value_changed(self, adj, *args): + bottom = adj.get_upper() - adj.get_page_size() + if (bottom - adj.get_value()) < 1: + self._autoscroll = True else: - # On scrolling UP disable autoscroll - # get_scroll_direction() sets has_direction only TRUE - # if smooth scrolling is deactivated. If we have smooth - # smooth scrolling we have to use get_scroll_deltas() - has_direction, direction = event.get_scroll_direction() - if not has_direction: - direction = None - smooth, delta_x, delta_y = event.get_scroll_deltas() - if smooth: - if delta_y < 0: - direction = Gdk.ScrollDirection.UP - elif delta_y > 0: - direction = Gdk.ScrollDirection.DOWN - elif delta_x < 0: - direction = Gdk.ScrollDirection.LEFT - elif delta_x > 0: - direction = Gdk.ScrollDirection.RIGHT - else: - app.log('autoscroll').warning( - 'Scroll directions can’t be determined') + self._autoscroll = False - if direction != Gdk.ScrollDirection.UP: - return - # Check if we have a Scrollbar - adjustment = self.xml.conversation_scrolledwindow.get_vadjustment() - if adjustment.get_upper() != adjustment.get_page_size(): - app.log('autoscroll').info('Autoscroll disabled') - self.conversation_view.autoscroll = False - - def _on_conversation_vadjustment_changed(self, adjustment): - self.scroll_to_end() - if self._fetching_history: - # Make sure the scroll position is kept - new_upper = adjustment.get_upper() - diff = new_upper - self._scrolled_old_upper - new_value = diff + self._scrolled_old_value - adjustment.set_value(new_value) - self._fetching_history = False + if self._no_more_messages: + self._fetch_start_upper = None return + if self._fetch_start_upper == adj.get_upper(): + return + + self._fetch_start_upper = None + + # Load messages when we are near the top + if adj.get_value() < adj.get_page_size() * 2: + self._fetch_start_upper = adj.get_upper() + # Workaround: https://gitlab.gnome.org/GNOME/gtk/merge_requests/395 + self.xml.conversation_scrolledwindow.set_kinetic_scrolling(False) + self.fetch_n_lines_history(30) + def scroll_messages(self, direction, msg_buf, msg_type): if msg_type == 'sent': history = self.sent_history diff --git a/gajim/common/storage/archive.py b/gajim/common/storage/archive.py index a1b618e4e0..f9a0bae50b 100644 --- a/gajim/common/storage/archive.py +++ b/gajim/common/storage/archive.py @@ -321,15 +321,15 @@ def convert_show_values_to_db_api_values(show): return None @timeit - def get_conversation_before(self, account, jid, end_timestamp, n_lines): + def get_conversation_before(self, account, jid, timestamp, n_lines): """ - Load n_lines lines of conversation with jid before end_timestamp + Load n_lines lines of conversation with jid before timestamp :param account: The account :param jid: The jid for which we request the conversation - :param end_timestamp: end timestamp / datetime.datetime instance + :param timestamp: timestamp returns a list of namedtuples """ @@ -351,19 +351,18 @@ def get_conversation_before(self, account, jid, end_timestamp, n_lines): return self._con.execute( sql, - tuple(jids) + (end_timestamp.timestamp(), n_lines)).fetchall() + tuple(jids) + (timestamp, n_lines)).fetchall() @timeit - def get_conversation_muc_before(self, account, jid, end_timestamp, - n_lines): + def get_conversation_muc_before(self, account, jid, timestamp, n_lines): """ - Load n_lines lines of conversation with jid before end_timestamp + Load n_lines lines of conversation with jid before timestamp :param account: The account :param jid: The jid for which we request the conversation - :param end_timestamp: end timestamp / datetime.datetime instance + :param timestamp: timestamp returns a list of namedtuples """ @@ -386,7 +385,7 @@ def get_conversation_muc_before(self, account, jid, end_timestamp, return self._con.execute( sql, - tuple(jids) + (end_timestamp.timestamp(), n_lines)).fetchall() + tuple(jids) + (timestamp, n_lines)).fetchall() @timeit def get_last_conversation_line(self, account, jid): diff --git a/gajim/data/gui/chat_control.ui b/gajim/data/gui/chat_control.ui index a402f01708..d64734e464 100644 --- a/gajim/data/gui/chat_control.ui +++ b/gajim/data/gui/chat_control.ui @@ -623,10 +623,6 @@ <property name="can_focus">True</property> <property name="shadow_type">in</property> <property name="overlay_scrolling">False</property> - <signal name="edge-overshot" handler="_on_edge_overshot" swapped="no"/> - <signal name="edge-reached" handler="_on_edge_reached" swapped="no"/> - <signal name="realize" handler="_on_scroll_realize" swapped="no"/> - <signal name="scroll-event" handler="_on_scroll" swapped="no"/> <child> <placeholder/> </child> diff --git a/gajim/data/gui/groupchat_control.ui b/gajim/data/gui/groupchat_control.ui index 5e11b173c0..f55130f6d7 100644 --- a/gajim/data/gui/groupchat_control.ui +++ b/gajim/data/gui/groupchat_control.ui @@ -348,10 +348,6 @@ <property name="can_focus">True</property> <property name="shadow_type">in</property> <property name="overlay_scrolling">False</property> - <signal name="edge-overshot" handler="_on_edge_overshot" swapped="no"/> - <signal name="edge-reached" handler="_on_edge_reached" swapped="no"/> - <signal name="realize" handler="_on_scroll_realize" swapped="no"/> - <signal name="scroll-event" handler="_on_scroll" swapped="no"/> <child> <placeholder/> </child> diff --git a/gajim/gtk/conversation/rows/info.py b/gajim/gtk/conversation/rows/info.py index 9835f5e671..2a0184cf61 100644 --- a/gajim/gtk/conversation/rows/info.py +++ b/gajim/gtk/conversation/rows/info.py @@ -12,6 +12,8 @@ # You should have received a copy of the GNU General Public License # along with Gajim. If not, see <http://www.gnu.org/licenses/>. +from datetime import datetime + from gi.repository import GLib from gi.repository import Gtk @@ -34,7 +36,8 @@ def __init__(self, BaseRow.__init__(self, account, widget='textview', history_mode=history_mode) self.type = 'info' - self.timestamp = timestamp + self.timestamp = datetime.fromtimestamp(timestamp) + self.db_timestamp = timestamp self.kind = kind if subject: @@ -50,7 +53,7 @@ def __init__(self, avatar_placeholder = Gtk.Box() avatar_placeholder.set_size_request(AvatarSize.ROSTER, -1) self.grid.attach(avatar_placeholder, 0, 0, 1, 2) - timestamp_widget = self.create_timestamp_widget(timestamp) + timestamp_widget = self.create_timestamp_widget(self.timestamp) timestamp_widget.set_valign(Gtk.Align.START) self.grid.attach(timestamp_widget, 2, 0, 1, 1) diff --git a/gajim/gtk/conversation/rows/message.py b/gajim/gtk/conversation/rows/message.py index 84363293aa..667efee978 100644 --- a/gajim/gtk/conversation/rows/message.py +++ b/gajim/gtk/conversation/rows/message.py @@ -12,6 +12,8 @@ # You should have received a copy of the GNU General Public License # along with Gajim. If not, see <http://www.gnu.org/licenses/>. +from datetime import datetime + from gi.repository import Gdk from gi.repository import GLib from gi.repository import Gtk @@ -57,7 +59,8 @@ def __init__(self, BaseRow.__init__(self, account, widget='textview', history_mode=history_mode) self.type = 'chat' - self.timestamp = timestamp + self.timestamp = datetime.fromtimestamp(timestamp) + self.db_timestamp = timestamp self.message_id = message_id self.log_line_id = log_line_id self.kind = kind @@ -84,7 +87,7 @@ def __init__(self, self._meta_box = Gtk.Box(spacing=6) self._meta_box.pack_start( self.create_name_widget(name, kind, is_groupchat), False, True, 0) - timestamp_label = self.create_timestamp_widget(timestamp) + timestamp_label = self.create_timestamp_widget(self.timestamp) timestamp_label.set_margin_start(6) self._meta_box.pack_end(timestamp_label, False, True, 0) # TODO: implement app.settings.get('print_time') 'always', 'sometimes'? diff --git a/gajim/gtk/conversation/view.py b/gajim/gtk/conversation/view.py index a20a3caf27..6382c688ff 100644 --- a/gajim/gtk/conversation/view.py +++ b/gajim/gtk/conversation/view.py @@ -105,6 +105,12 @@ def clear(self): GLib.idle_add(self._reset_conversation_view) + def get_first_message_row(self): + for row in self.get_children(): + if isinstance(row, MessageRow): + return row + return None + def _reset_conversation_view(self): self._first_date = None self._last_date = None @@ -149,7 +155,6 @@ def add_message(self, if not timestamp: timestamp = time.time() - time_ = datetime.fromtimestamp(timestamp) if other_text_tags is None: other_text_tags = [] @@ -158,7 +163,7 @@ def add_message(self, (subject and self._contact.is_groupchat)): message = InfoMessageRow( self._account, - time_, + timestamp, text, other_text_tags, kind, @@ -181,7 +186,7 @@ def add_message(self, message = MessageRow( self._account, message_id, - time_, + timestamp, kind, name, text, @@ -196,11 +201,11 @@ def add_message(self, history_mode=self._history_mode, log_line_id=log_line_id) - self._insert_message(message, time_, kind, history) + self._insert_message(message, kind, history) # Check for maximum message count - if self.autoscroll and self._row_count > self._max_row_count: - self._reduce_message_count() + # if self.autoscroll and self._row_count > self._max_row_count: + # self._reduce_message_count() def _get_avatar(self, kind, name): scale = self.get_scale_factor() @@ -216,20 +221,20 @@ def _get_avatar(self, kind, name): return contact.get_avatar(AvatarSize.ROSTER, scale, add_show=False) - def _insert_message(self, message, time_, kind, history): - current_date = time_.strftime('%a, %d %b %Y') + def _insert_message(self, message, kind, history): + current_date = message.timestamp.strftime('%a, %d %b %Y') - if self._is_out_of_order(time_, history): - insertion_point = bisect_left(self._timestamps_inserted, time_) + if self._is_out_of_order(message.timestamp, history): + insertion_point = bisect_left(self._timestamps_inserted, message.timestamp) date_check_point = min(len( self._timestamps_inserted) - 1, insertion_point - 1) date_at_dcp = self._timestamps_inserted[date_check_point].strftime( '%a, %d %b %Y') if date_at_dcp != current_date: - associated_timestamp = time_ - timedelta( - hours=time_.hour, - minutes=time_.minute, - seconds=time_.second) + associated_timestamp = message.timestamp - timedelta( + hours=message.timestamp.hour, + minutes=message.timestamp.minute, + seconds=message.timestamp.second) date_row = DateRow( self._account, current_date, associated_timestamp) self.insert(date_row, insertion_point) @@ -238,18 +243,18 @@ def _insert_message(self, message, time_, kind, history): self._row_count += 1 insertion_point += 1 if (kind in ('incoming', 'incoming_queue') and - time_ > self._last_incoming_timestamp): - self._last_incoming_timestamp = time_ + message.timestamp > self._last_incoming_timestamp): + self._last_incoming_timestamp = message.timestamp self.insert(message, insertion_point) - self._timestamps_inserted.insert(insertion_point, time_) - current_timestamp = time_ + self._timestamps_inserted.insert(insertion_point, message.timestamp) + current_timestamp = message.timestamp self._row_count += 1 elif history: if current_date != self._first_date: - associated_timestamp = time_ - timedelta( - hours=time_.hour, - minutes=time_.minute, - seconds=time_.second) + associated_timestamp = message.timestamp - timedelta( + hours=message.timestamp.hour, + minutes=message.timestamp.minute, + seconds=message.timestamp.second) date_row = DateRow( self._account, current_date, associated_timestamp) self.insert(date_row, 1) @@ -257,22 +262,22 @@ def _insert_message(self, message, time_, kind, history): self._row_count += 1 self._first_date = current_date if kind in ('incoming', 'incoming_queue', 'outgoing'): - self.first_message_timestamp = time_ + self.first_message_timestamp = message.timestamp if (kind in ('incoming', 'incoming_queue') and - time_ > self._last_incoming_timestamp): - self._last_incoming_timestamp = time_ + message.timestamp > self._last_incoming_timestamp): + self._last_incoming_timestamp = message.timestamp self.insert(message, 2) - self._timestamps_inserted.insert(2, time_) + self._timestamps_inserted.insert(2, message.timestamp) if self._last_date is None: self._last_date = current_date - current_timestamp = time_ + current_timestamp = message.timestamp self._row_count += 1 else: if current_date != self._last_date: - associated_timestamp = time_ - timedelta( - hours=time_.hour, - minutes=time_.minute, - seconds=time_.second) + associated_timestamp = message.timestamp - timedelta( + hours=message.timestamp.hour, + minutes=message.timestamp.minute, + seconds=message.timestamp.second) date_row = DateRow( self._account, current_date, associated_timestamp) self.add(date_row) @@ -282,14 +287,14 @@ def _insert_message(self, message, time_, kind, history): self._first_date = current_date if (kind in ('incoming', 'incoming_queue', 'outgoing') and not self.first_message_timestamp): - self.first_message_timestamp = time_ + self.first_message_timestamp = message.timestamp if (kind in ('incoming', 'incoming_queue') and - time_ > self._last_incoming_timestamp): - self._last_incoming_timestamp = time_ + message.timestamp > self._last_incoming_timestamp): + self._last_incoming_timestamp = message.timestamp self._last_date = current_date self.add(message) - self._timestamps_inserted.append(time_) - current_timestamp = time_ + self._timestamps_inserted.append(message.timestamp) + current_timestamp = message.timestamp self._row_count += 1 if message.type == 'chat': -- GitLab