Philipp Hörist pushed to branch refactor_view at gajim / gajim
Commits: e63fe3cc by wurstsalat at 2022-09-16T22:52:00+02:00 imprv: SearchView: Add hint for search filters
Fixes #11136
- - - - - 78319461 by wurstsalat at 2022-09-18T11:41:42+02:00 cfix: ChatBanner: Escape contact name for title and tooltip
Fixes #11154
- - - - - efb26916 by wurstsalat at 2022-09-18T13:56:12+02:00 imprv: Preview: Improve mime type detection
Fixes #11150
- - - - - 615d6b5e by Philipp Hörist at 2022-09-18T18:00:13+02:00 refactor: Contacts: Add chatstate_string property
- - - - - ad4cbd0e by Philipp Hörist at 2022-09-18T18:01:46+02:00 refactor: ChatBanner: Split update_content() into dedicated methods
- - - - - dc02bdc8 by wurstsalat at 2022-09-18T19:16:05+02:00 fix: Corrections: Don’t correct message if text has not changed
Fixes #11146
- - - - - 9fd1b326 by Philipp Hörist at 2022-09-18T20:09:23+02:00 refactor: ChatBanner: Move more code into dedicated methods
- - - - - 27a31d44 by Philipp Hörist at 2022-09-18T21:10:04+02:00 refactor: Merge ScrolledView into ConversationView
- - - - - 4b5ecc89 by Philipp Hörist at 2022-09-18T21:10:04+02:00 cfix: Don’t use disconnect_by_func()
- - - - - 86e8f394 by Philipp Hörist at 2022-09-18T22:17:52+02:00 refactor: ConversationView: Use action for Scroll Up/Down
- - - - -
12 changed files:
- gajim/common/helpers.py - gajim/common/modules/contacts.py - gajim/common/preview.py - gajim/data/gui/search_view.ui - gajim/data/other/shortcuts.json - gajim/gtk/chat_banner.py - gajim/gtk/chat_stack.py - gajim/gtk/const.py - gajim/gtk/control.py - − gajim/gtk/conversation/scrolled.py - gajim/gtk/conversation/view.py - gajim/gtk/message_actions_box.py
Changes:
===================================== gajim/common/helpers.py ===================================== @@ -64,6 +64,7 @@
from nbxmpp.namespaces import Namespace from nbxmpp.const import Role +from nbxmpp.const import Chatstate from nbxmpp.const import ConnectionProtocol from nbxmpp.const import ConnectionType from nbxmpp.const import Affiliation @@ -304,17 +305,23 @@ def from_one_line(msg: str) -> str: return msg
-def get_uf_chatstate(chatstate: str) -> str: - ''' - Remove chatstate jargon and returns user friendly messages - ''' - if chatstate in ('inactive', 'gone'): - return _('is doing something else') - if chatstate == 'composing': +def chatstate_to_string(chatstate: Optional[Chatstate]) -> str: + if chatstate is None: + return '' + + if chatstate == Chatstate.ACTIVE: + return '' + + if chatstate == Chatstate.COMPOSING: return _('is composing a message…') - if chatstate == 'paused': + + if chatstate in (Chatstate.INACTIVE, Chatstate.GONE): + return _('is doing something else') + + if chatstate == Chatstate.PAUSED: return _('paused composing a message') - return '' + + raise ValueError('unknown value: %s' % chatstate)
def exec_command(command: str,
===================================== gajim/common/modules/contacts.py ===================================== @@ -45,6 +45,7 @@ from gajim.common.structs import UNKNOWN_MUC_PRESENCE from gajim.common.structs import MUCPresenceData from gajim.common.helpers import Observable +from gajim.common.helpers import chatstate_to_string from gajim.common.modules.base import BaseModule from gajim.common.modules.util import LogAdapter from gajim.common.helpers import get_groupchat_name @@ -322,6 +323,14 @@ def is_jingle_available(self) -> bool: def get_address(self, _prefer_real: bool = True) -> JID: return self._jid
+ @property + def chatstate(self) -> Optional[Chatstate]: + return None + + @property + def chatstate_string(self) -> str: + return chatstate_to_string(self.chatstate) + def __repr__(self) -> str: return f'{self.jid} ({self._account})'
===================================== gajim/common/preview.py ===================================== @@ -237,8 +237,13 @@ def _accept_uri(urlparts: ParseResult, if urlparts.scheme in ('https', 'http'): if app.settings.get('preview_allow_all_images'): mime_type = guess_mime_type(uri) - if (mime_type not in MIME_TYPES or - mime_type == 'application/octet-stream'): + if mime_type not in MIME_TYPES: + return False + + if mime_type == 'application/octet-stream' and uri != oob_url: + # guess_mime_type yields 'application/octet-stream' for + # paths without suffix. Check oob_url to make sure we + # display a preview for files sent via http upload. return False return True
@@ -345,7 +350,7 @@ def _on_orig_load_finished(self, log.error('%s: %s', preview.orig_path.name, error) return
- preview.mime_type = guess_mime_type(preview.orig_path) + preview.mime_type = guess_mime_type(preview.orig_path, data) preview.file_size = os.path.getsize(preview.orig_path) if preview.is_previewable: if preview.create_thumbnail(data): @@ -368,7 +373,7 @@ def _on_thumb_load_finished(data: Optional[bytes], return
preview.thumbnail = data - preview.mime_type = guess_mime_type(preview.orig_path) + preview.mime_type = guess_mime_type(preview.orig_path, data) preview.file_size = os.path.getsize(preview.orig_path)
try:
===================================== gajim/data/gui/search_view.ui ===================================== @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.38.2 --> +<!-- Generated with glade 3.40.0 --> <interface> <requires lib="gtk+" version="3.24"/> <object class="GtkPopover" id="calendar_popover"> @@ -215,6 +215,10 @@ <property name="primary-icon-name">edit-find-symbolic</property> <property name="primary-icon-activatable">False</property> <property name="primary-icon-sensitive">False</property> + <property name="primary-icon-tooltip-text" translatable="yes">Use filters to narrow down your search: +from:username +before:yyyy-mm-dd +after:yyyy-mm-dd</property> <signal name="activate" handler="_on_search" swapped="no"/> </object> <packing>
===================================== gajim/data/other/shortcuts.json ===================================== @@ -37,5 +37,7 @@ "win.switch-workspace-7": ["<Primary>7", "<Primary>KP_7"], "win.switch-workspace-8": ["<Primary>8", "<Primary>KP_8"], "win.switch-workspace-9": ["<Primary>9", "<Primary>KP_9"], - "win.toggle-chat-list": ["<Primary>R"] + "win.toggle-chat-list": ["<Primary>R"], + "win.scroll-view-up": ["<Shift>Page_Up"], + "win.scroll-view-down": ["<Shift>Page_Down"] }
===================================== gajim/gtk/chat_banner.py ===================================== @@ -17,6 +17,7 @@ from typing import Any from typing import Optional
+from gi.repository import GLib from gi.repository import Gtk
import cairo @@ -31,7 +32,6 @@ from gajim.common.events import MessageReceived from gajim.common.events import MucDiscoUpdate from gajim.common.ged import EventHelper -from gajim.common.helpers import get_uf_chatstate from gajim.common.i18n import _ from gajim.common.modules.contacts import BareContact from gajim.common.modules.contacts import GroupchatContact @@ -63,37 +63,54 @@ def __init__(self) -> None:
hide_roster = app.settings.get('hide_groupchat_occupants_list') self._set_toggle_roster_button_icon(hide_roster) + app.settings.connect_signal( 'hide_groupchat_occupants_list', self._set_toggle_roster_button_icon)
- self.show_all() + app.settings.connect_signal( + 'hide_chat_banner', + self._update_hide_banner)
- def clear(self) -> None: - if self._contact is not None: - self._contact.disconnect_all_from_obj(self) - self._contact = None + app.settings.connect_signal( + 'hide_groupchat_banner', + self._update_hide_banner)
- if self._client is not None: - self._client.disconnect_all_from_obj(self) - self._client = None + self.show_all()
+ def clear(self) -> None: + self._disconnect_signals() self.unregister_events()
def switch_contact(self, contact: types.ChatContactT) -> None: - self._update_account_badge(contact.account) + self._disconnect_signals() + self._client = app.get_client(contact.account) + self._contact = contact + self._connect_signals()
- if self._client is not None: - self._client.disconnect_all_from_obj(self) + if not self.has_events_registered(): + self.register_events([ + ('message-received', ged.GUI2, self._on_message_received), + ('muc-disco-update', ged.GUI2, self._on_muc_disco_update), + ('bookmarks-received', ged.GUI2, self._on_bookmarks_received), + ('account-enabled', ged.GUI2, self._on_account_changed), + ('account-disabled', ged.GUI2, self._on_account_changed) + ])
- self._client = app.get_client(contact.account) - self._client.connect_signal('state-changed', - self._on_client_state_changed) + self._voice_requests_button.switch_contact(self._contact)
- if self._contact is not None: - self._contact.disconnect_all_from_obj(self) + self._update_phone_image() + self._update_roster_button() + self._update_avatar() + self._update_visitor_button() + self._update_name_label() + self._update_account_badge() + self._update_hide_banner() + + def _connect_signals(self) -> None: + assert self._contact is not None + assert self._client is not None
- self._contact = contact self._contact.multi_connect({ 'chatstate-update': self._on_chatstate_update, 'nickname-update': self._on_nickname_update, @@ -108,93 +125,81 @@ def switch_contact(self, contact: types.ChatContactT) -> None: 'state-changed': self._on_muc_state_changed, 'room-voice-request': self._on_room_voice_request }) - self._ui.toggle_roster_button.show() - hide_banner = app.settings.get('hide_groupchat_banner') - else: - self._ui.toggle_roster_button.hide() - hide_banner = app.settings.get('hide_chat_banner')
- if isinstance(self._contact, GroupchatParticipant): + elif isinstance(self._contact, GroupchatParticipant): self._contact.multi_connect({ 'user-joined': self._on_user_state_changed, 'user-left': self._on_user_state_changed, 'user-avatar-update': self._on_user_avatar_update, })
- if not self.has_events_registered(): - self.register_events([ - ('message-received', ged.GUI2, self._on_message_received), - ('muc-disco-update', ged.GUI2, self._on_muc_disco_update), - ('bookmarks-received', ged.GUI2, self._on_bookmarks_received), - ('account-enabled', ged.GUI2, self._on_account_changed), - ('account-disabled', ged.GUI2, self._on_account_changed) - ]) - - self._ui.phone_image.set_visible( - self._contact in self._last_message_from_phone) - - self._voice_requests_button.switch_contact(self._contact) + self._client.connect_signal('state-changed', + self._on_client_state_changed)
- if hide_banner: - self.set_no_show_all(True) - self.hide() - else: - self.set_no_show_all(False) - self.show_all() + def _disconnect_signals(self) -> None: + if self._contact is not None: + self._contact.disconnect_all_from_obj(self) + self._contact = None
- self._update_avatar() - self._update_content() + if self._client is not None: + self._client.disconnect_all_from_obj(self) + self._client = None
def _on_client_state_changed(self, _client: types.Client, _signal_name: str, state: SimpleClientState): + self._update_avatar() - self._update_content()
def _on_presence_update(self, _contact: types.BareContact, _signal_name: str ) -> None: + self._update_avatar()
def _on_chatstate_update(self, _contact: types.BareContact, _signal_name: str ) -> None: - self._update_content() + + self._update_name_label()
def _on_nickname_update(self, _contact: types.BareContact, _signal_name: str ) -> None: - self._update_content() + + self._update_name_label()
def _on_avatar_update(self, _contact: types.BareContact, _signal_name: str ) -> None: + self._update_avatar()
def _on_caps_update(self, _contact: types.BareContact, _signal_name: str ) -> None: + self._update_avatar()
def _on_muc_state_changed(self, contact: GroupchatContact, _signal_name: str ) -> None: - if contact.is_joined: - self._update_content() + + self._update_visitor_button()
def _on_room_voice_request(self, *args: Any) -> None: self._voice_requests_button.set_no_show_all(False) self._voice_requests_button.show_all()
def _on_user_role_changed(self, *args: Any) -> None: - self._update_content() + self._update_visitor_button()
def _on_user_state_changed(self, *args: Any) -> None: self._update_avatar() @@ -206,18 +211,17 @@ def _on_bookmarks_received(self, _event: BookmarksReceived) -> None: if not isinstance(self._contact, GroupchatContact): return
- self._update_content() + self._update_name_label()
def _on_muc_disco_update(self, event: MucDiscoUpdate) -> None: assert self._contact is not None if event.jid != self._contact.jid: return
- self._update_content() + self._update_name_label()
def _on_account_changed(self, event: AccountEnabled) -> None: - assert self._contact is not None - self._update_account_badge(self._contact.account) + self._update_account_badge()
def _on_message_received(self, event: MessageReceived) -> None: if (not isinstance(self._contact, BareContact) or @@ -233,7 +237,31 @@ def _on_message_received(self, event: MessageReceived) -> None: else: self._last_message_from_phone.discard(self._contact)
- self._ui.phone_image.set_visible(resource_contact.is_phone) + self._update_phone_image() + + def _update_phone_image(self) -> None: + self._ui.phone_image.set_visible( + self._contact in self._last_message_from_phone) + + def _update_roster_button(self) -> None: + self._ui.toggle_roster_button.set_visible( + isinstance(self._contact, GroupchatContact)) + + def _update_hide_banner(self, *args: Any) -> None: + if self._contact is None: + return + + if isinstance(self._contact, GroupchatContact): + hide_banner = app.settings.get('hide_groupchat_banner') + else: + hide_banner = app.settings.get('hide_chat_banner') + + if hide_banner: + self.set_no_show_all(True) + self.hide() + else: + self.set_no_show_all(False) + self.show_all()
def _update_avatar(self) -> None: scale = app.window.get_scale_factor() @@ -242,54 +270,50 @@ def _update_avatar(self) -> None: assert isinstance(surface, cairo.ImageSurface) self._ui.avatar_image.set_from_surface(surface)
- def _update_content(self) -> None: - assert self._contact is not None - assert self._client is not None + def _update_visitor_button(self) -> None: + if not isinstance(self._contact, GroupchatContact): + self._ui.visitor_box.set_visible(False) + return
- name = self._contact.name + if self._contact.is_not_joined: + self._ui.visitor_box.set_visible(False) + return
- if self._contact.jid.bare_match(self._client.get_own_jid()): - name = _('Note to myself') + self_contact = self._contact.get_self() + assert self_contact is not None + self._ui.visitor_box.set_visible(self_contact.role.is_visitor)
- if self._contact.is_pm_contact: - gc_contact = self._client.get_module('Contacts').get_contact( - self._contact.jid.bare) - name = f'{name} ({gc_contact.name})' + def _update_name_label(self) -> None: + assert self._contact is not None + + name = self._get_name_from_contact(self._contact)
- label_text = f'<span>{name}</span>' - label_tooltip = name - show_chatstate = app.settings.get('show_chatstate_in_banner') - if (show_chatstate and isinstance( - self._contact, (BareContact, GroupchatParticipant))): - chatstate = self._contact.chatstate - if chatstate is not None: - chatstate = get_uf_chatstate(chatstate.value) - else: - chatstate = '' + chatstate = '' + if app.settings.get('show_chatstate_in_banner'): + chatstate = self._contact.chatstate_string
- label_text = f'<span>{name}</span>' \ - f'<span size="60%" weight="light">' \ - f' {chatstate}</span>' - label_tooltip = f'{name} {chatstate}' + label_text = f'<span>{GLib.markup_escape_text(name)}</span>' + if chatstate: + label_markup = '<span size="60%" weight="light"> %s</span>' + label_text += label_markup % GLib.markup_escape_text(chatstate)
self._ui.name_label.set_markup(label_text) - self._ui.name_label.set_tooltip_text(label_tooltip)
- if isinstance(self._contact, GroupchatContact): - self_contact = self._contact.get_self() - if self_contact: - self._ui.visitor_box.set_visible(self_contact.role.is_visitor) - return + tooltip_text = name + if chatstate: + tooltip_text = f'{name} {chatstate}'
- self._ui.visitor_box.set_visible(False) + self._ui.name_label.set_tooltip_text(tooltip_text) + + def _update_account_badge(self) -> None: + assert self._contact is not None
- def _update_account_badge(self, account: str) -> None: if self._account_badge is not None: self._account_badge.destroy()
enabled_accounts = app.get_enabled_accounts_with_labels() if len(enabled_accounts) > 1: - self._account_badge = AccountBadge(account) + self._account_badge = AccountBadge(self._contact.account) self._ui.additional_items_box.pack_end( self._account_badge, False, True, 0)
@@ -308,3 +332,17 @@ def _set_toggle_roster_button_icon(self, icon = 'go-next-symbolic' if not hide_roster else 'go-previous-symbolic' self._ui.toggle_roster_image.set_from_icon_name( icon, Gtk.IconSize.BUTTON) + + @staticmethod + def _get_name_from_contact(contact: types.ChatContactT) -> str: + name = contact.name + + if isinstance(contact, BareContact): + if contact.is_self: + return _('Note to myself') + return name + + if isinstance(contact, GroupchatParticipant): + return f'{name} ({contact.room.name})' + + return name
===================================== gajim/gtk/chat_stack.py ===================================== @@ -704,9 +704,23 @@ def _on_send_message(self) -> None:
correct_id = None if self._message_action_box.is_correcting: + self._message_action_box.is_correcting = False + context = self._message_action_box.msg_textview.get_style_context() + context.remove_class('gajim-msg-correcting') + correct_id = self._message_action_box.last_message_id.get( (contact.account, contact.jid)) - self._message_action_box.is_correcting = False + message_row = app.storage.archive.get_last_correctable_message( + contact.account, contact.jid, correct_id) + if message_row is None: + self._message_action_box.msg_textview.clear() + log.info('Trying to correct message older than threshold') + return + + if message_row.message == message: + self._message_action_box.msg_textview.clear() + log.info('Trying to correct message with original text') + return
chatstate = client.get_module('Chatstate').get_active_chatstate( contact)
===================================== gajim/gtk/const.py ===================================== @@ -214,6 +214,8 @@ def __str__(self): ('muc-change-role', 'as', False), ('muc-change-affiliation', 'as', False), ('muc-request-voice', None, False), + ('scroll-view-up', None, True), + ('scroll-view-down', None, True), ]
===================================== gajim/gtk/control.py ===================================== @@ -48,8 +48,8 @@ from gajim.common.modules.contacts import GroupchatContact from gajim.common.modules.httpupload import HTTPFileTransfer from gajim.common.storage.archive import ConversationRow +from gajim.gtk.conversation.view import ConversationView
-from gajim.gui.conversation.scrolled import ScrolledView from gajim.gui.conversation.jump_to_end_button import JumpToEndButton from gajim.gui.builder import get_builder from gajim.gui.groupchat_roster import GroupchatRoster @@ -69,15 +69,13 @@ def __init__(self) -> None:
self._ui = get_builder('chat_control.ui')
- self._scrolled_view = ScrolledView() + self._scrolled_view = ConversationView() self._scrolled_view.connect('autoscroll-changed', self._on_autoscroll_changed) self._scrolled_view.connect('request-history', self._fetch_n_lines_history, 20) self._ui.conv_view_overlay.add(self._scrolled_view)
- self.conversation_view = self._scrolled_view.get_view() - self._groupchat_state = GroupchatState() self._ui.conv_view_overlay.add_overlay(self._groupchat_state)
@@ -136,7 +134,6 @@ def clear(self) -> None:
self._contact = None self._client = None - self.reset_view() self._scrolled_view.clear() self._groupchat_state.clear() self._roster.clear() @@ -199,7 +196,7 @@ def switch_contact(self, contact: Union[BareContact, muc_data = self._client.get_module('MUC').get_muc_data( str(contact.jid)) if muc_data is not None and muc_data.subject is not None: - self.conversation_view.add_muc_subject( + self._scrolled_view.add_muc_subject( muc_data.subject, muc_data.last_subject_timestamp)
def _register_events(self) -> None: @@ -245,9 +242,9 @@ def _on_presence_received(self, event: events.PresenceReceived) -> None: contact = self.client.get_module('Contacts').get_contact(event.fjid) if isinstance(contact, BareContact): return - self.conversation_view.add_user_status(self.contact.name, - contact.show.value, - contact.status) + self._scrolled_view.add_user_status(self.contact.name, + contact.show.value, + contact.status)
def _on_message_sent(self, event: events.MessageSent) -> None: if not self._is_event_processable(event): @@ -267,7 +264,7 @@ def _on_message_sent(self, event: events.MessageSent) -> None: displaymarking = None
if event.correct_id: - self.conversation_view.correct_message( + self._scrolled_view.correct_message( event.correct_id, event.message, self.get_our_nick()) return
@@ -363,7 +360,7 @@ def _on_message_updated(self, event: events.MessageUpdated) -> None: if not self._is_event_processable(event): return
- self.conversation_view.correct_message( + self._scrolled_view.correct_message( event.correct_id, event.msgtxt, event.nickname)
def _on_message_moderated(self, event: events.MessageModerated) -> None: @@ -374,32 +371,32 @@ def _on_message_moderated(self, event: events.MessageModerated) -> None: self.contact.account, event.moderation.moderator_jid, event.moderation.reason) - self.conversation_view.show_message_retraction( + self._scrolled_view.show_message_retraction( event.moderation.stanza_id, text)
def _on_receipt_received(self, event: events.ReceiptReceived) -> None: if not self._is_event_processable(event): return
- self.conversation_view.show_receipt(event.receipt_id) + self._scrolled_view.show_receipt(event.receipt_id)
def _on_displayed_received(self, event: events.DisplayedReceived) -> None: if not self._is_event_processable(event): return
- self.conversation_view.set_read_marker(event.marker_id) + self._scrolled_view.set_read_marker(event.marker_id)
def _on_message_error(self, event: events.MessageError) -> None: if not self._is_event_processable(event): return
- self.conversation_view.show_error(event.message_id, event.error) + self._scrolled_view.show_error(event.message_id, event.error)
def _on_call_stopped(self, event: events.CallStopped) -> None: if not self._is_event_processable(event): return
- self.conversation_view.update_call_rows() + self._scrolled_view.update_call_rows()
def _on_jingle_request_received(self, event: events.JingleRequestReceived @@ -439,7 +436,7 @@ def mark_as_read(self, send_marker: bool = True) -> None: self._jump_to_end_button.reset_unread_count()
def _on_autoscroll_changed(self, - _widget: ScrolledView, + _widget: ConversationView, autoscroll: bool ) -> None:
@@ -480,17 +477,17 @@ def _allow_add_message(self) -> bool: return self._scrolled_view.get_lower_complete()
def add_command_output(self, text: str, is_error: bool) -> None: - self.conversation_view.add_command_output(text, is_error) + self._scrolled_view.add_command_output(text, is_error)
def add_info_message(self, text: str, timestamp: Optional[float] = None ) -> None:
- self.conversation_view.add_info_message(text, timestamp) + self._scrolled_view.add_info_message(text, timestamp)
def add_file_transfer(self, transfer: HTTPFileTransfer) -> None: - self.conversation_view.add_file_transfer(transfer) + self._scrolled_view.add_file_transfer(transfer)
def add_jingle_file_transfer(self, event: Union[ @@ -499,11 +496,11 @@ def add_jingle_file_transfer(self, None] ) -> None: if self._allow_add_message(): - self.conversation_view.add_jingle_file_transfer(event) + self._scrolled_view.add_jingle_file_transfer(event)
def add_call_message(self, event: events.JingleRequestReceived) -> None: if self._allow_add_message(): - self.conversation_view.add_call_message(event=event) + self._scrolled_view.add_call_message(event=event)
def _add_message(self, text: str, @@ -521,7 +518,7 @@ def _add_message(self, additional_data = AdditionalDataDict()
if self._allow_add_message(): - self.conversation_view.add_message( + self._scrolled_view.add_message( text, kind, name, @@ -547,7 +544,7 @@ def get_autoscroll(self) -> bool: return self._scrolled_view.get_autoscroll()
def scroll_to_message(self, log_line_id: int, timestamp: float) -> None: - row = self.conversation_view.get_row_by_log_line_id(log_line_id) + row = self._scrolled_view.get_row_by_log_line_id(log_line_id) if row is None: # Clear view and reload conversation around timestamp self._scrolled_view.block_signals(True) @@ -558,7 +555,7 @@ def scroll_to_message(self, log_line_id: int, timestamp: float) -> None: self.add_messages(at_after)
GLib.idle_add( - self.conversation_view.scroll_to_message_and_highlight, + self._scrolled_view.scroll_to_message_and_highlight, log_line_id) GLib.idle_add(self._scrolled_view.block_signals, False)
@@ -571,9 +568,9 @@ def _fetch_n_lines_history(self, self._scrolled_view.block_signals(True)
if before: - row = self.conversation_view.get_first_message_row() + row = self._scrolled_view.get_first_message_row() else: - row = self.conversation_view.get_last_message_row() + row = self._scrolled_view.get_last_message_row()
if row is None: timestamp = time.time() @@ -598,7 +595,7 @@ def _fetch_n_lines_history(self, self._scrolled_view.set_history_complete(before, True)
# if self._scrolled_view.get_autoscroll(): - # if self.conversation_view.reduce_message_count(before): + # if self._scrolled_view.reduce_message_count(before): # self._scrolled_view.set_history_complete(before, False)
assert self._contact is not None @@ -645,13 +642,13 @@ def add_messages(self, messages: list[ConversationRow]): if msg.kind in (KindConstant.FILE_TRANSFER_INCOMING, KindConstant.FILE_TRANSFER_OUTGOING): if msg.additional_data.get_value('gajim', 'type') == 'jingle': - self.conversation_view.add_jingle_file_transfer( + self._scrolled_view.add_jingle_file_transfer( db_message=msg) continue
if msg.kind in (KindConstant.CALL_INCOMING, KindConstant.CALL_OUTGOING): - self.conversation_view.add_call_message(db_message=msg) + self._scrolled_view.add_call_message(db_message=msg) continue
if not msg.message: @@ -683,7 +680,7 @@ def add_messages(self, messages: list[ConversationRow]): message_text = get_retraction_text( self.contact.account, retracted_by, reason)
- self.conversation_view.add_message( + self._scrolled_view.add_message( message_text, kind, contact_name, @@ -1041,7 +1038,7 @@ def _process_muc_user_joined(self, event: events.MUCUserJoined) -> None:
if not event.is_self: if self.contact.is_joined: - self.conversation_view.add_muc_user_joined(event) + self._scrolled_view.add_muc_user_joined(event) return
status_codes = event.status_codes or [] @@ -1083,7 +1080,7 @@ def _process_muc_user_left(self, event: events.MUCUserLeft) -> None:
if StatusCode.REMOVED_ERROR in status_codes: # Handle 333 before 307, some MUCs add both - self.conversation_view.add_muc_user_left(event, error=True) + self._scrolled_view.add_muc_user_left(event, error=True) return
reason = event.reason @@ -1118,7 +1115,7 @@ def _process_muc_user_left(self, event: events.MUCUserLeft) -> None: message = message.format(nick=nick, by=actor, reason=reason)
else: - self.conversation_view.add_muc_user_left(event) + self._scrolled_view.add_muc_user_left(event) return
self.add_info_message(message, event.timestamp) @@ -1158,4 +1155,4 @@ def _on_room_subject(self,
if (app.settings.get('show_subject_on_join') or not contact.is_joining): - self.conversation_view.add_muc_subject(subject) + self._scrolled_view.add_muc_subject(subject)
===================================== gajim/gtk/conversation/scrolled.py deleted ===================================== @@ -1,203 +0,0 @@ -# This file is part of Gajim. -# -# Gajim is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published -# by the Free Software Foundation; version 3 only. -# -# Gajim is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Gajim. If not, see http://www.gnu.org/licenses/. - -from __future__ import annotations - -from typing import Any -from typing import Optional - -from gi.repository import Gtk -from gi.repository import GObject - -from gajim.common.types import ChatContactT - -from .view import ConversationView - - -class ScrolledView(Gtk.ScrolledWindow): - - __gsignals__ = { - 'request-history': ( - GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION, - None, - (bool, ) - ), - 'autoscroll-changed': ( - GObject.SignalFlags.RUN_LAST, - None, - (bool,) - ) - } - - def __init__(self) -> None: - Gtk.ScrolledWindow.__init__(self) - - self.set_overlay_scrolling(False) - self.get_style_context().add_class('scrolled-no-border') - self.get_style_context().add_class('no-scroll-indicator') - self.get_style_context().add_class('scrollbar-style') - self.set_shadow_type(Gtk.ShadowType.IN) - self.set_vexpand(True) - - self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - - # This is a workaround: as soon as a line break occurs in Gtk.TextView - # with word-char wrapping enabled, a hyphen character is automatically - # inserted before the line break. This triggers the hscrollbar to show, - # see: https://gitlab.gnome.org/GNOME/gtk/-/issues/2384 - # Using set_hscroll_policy(Gtk.Scrollable.Policy.NEVER) would cause bad - # performance during resize, and prevent the window from being shrunk - # horizontally under certain conditions (applies to GroupchatControl) - self.get_hscrollbar().hide() - - self._current_upper: float = 0 - self._autoscroll: bool = True - self._request_history_at_upper: Optional[float] = None - self._upper_complete: bool = False - self._lower_complete: bool = False - self._requesting: Optional[str] = None - self._block_signals = False - - vadjustment = self.get_vadjustment() - vadjustment.connect('notify::upper', self._on_adj_upper_changed) - vadjustment.connect('notify::value', self._on_adj_value_changed) - - self._view = ConversationView() - self.add(self._view) - self.set_focus_vadjustment(Gtk.Adjustment()) - - def clear(self) -> None: - self._block_signals = True - self._reset() - self._view.clear() - - def switch_contact(self, contact: ChatContactT) -> None: - self._block_signals = True - self._reset() - self._view.switch_contact(contact) - self._block_signals = False - self._emit('request-history', True) - - def get_autoscroll(self) -> bool: - return self._autoscroll - - def get_view(self) -> ConversationView: - return self._view - - def block_signals(self, value: bool) -> None: - self._block_signals = value - - def _emit(self, signal_name: str, *args: Any) -> None: - if not self._block_signals: - self.emit(signal_name, *args) - - def _reset(self) -> None: - self._current_upper = 0 - self._request_history_at_upper = None - self._upper_complete = False - self._lower_complete = False - self._requesting = None - self.set_history_complete(True, False) - - def reset(self) -> None: - self._reset() - self._view.reset() - - def set_history_complete(self, before: bool, complete: bool) -> None: - if before: - self._upper_complete = complete - self._view.set_history_complete(complete) - else: - self._lower_complete = complete - - def get_lower_complete(self) -> bool: - return self._lower_complete - - def _on_adj_upper_changed(self, - adj: Gtk.Adjustment, - _pspec: GObject.ParamSpec) -> None: - - upper = adj.get_upper() - diff = upper - self._current_upper - - 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.set_kinetic_scrolling(True) - if self._requesting == 'before': - adj.set_value(adj.get_value() + diff) - - if upper == adj.get_page_size(): - # There is no scrollbar - if not self._block_signals: - self._emit('request-history', True) - self._lower_complete = True - self._autoscroll = True - self._emit('autoscroll-changed', self._autoscroll) - - self._requesting = None - - def _on_adj_value_changed(self, - adj: Gtk.Adjustment, - _pspec: GObject.ParamSpec) -> None: - - if self._requesting is not None: - return - - bottom = adj.get_upper() - adj.get_page_size() - if (bottom - adj.get_value()) < 1: - self._autoscroll = True - self._emit('autoscroll-changed', self._autoscroll) - else: - self._autoscroll = False - self._emit('autoscroll-changed', self._autoscroll) - - if self._upper_complete: - self._request_history_at_upper = None - if self._lower_complete: - return - - if self._request_history_at_upper == adj.get_upper(): - # Abort here if we already did a history request and the upper - # did not change. This can happen if we scroll very fast and the - # value changes while the request has not been fulfilled. - return - - self._request_history_at_upper = None - - distance = adj.get_page_size() * 2 - if adj.get_value() < distance: - # Load messages when we are near the top - if self._upper_complete: - return - self._request_history_at_upper = adj.get_upper() - # Workaround: https://gitlab.gnome.org/GNOME/gtk/merge_requests/395 - self.set_kinetic_scrolling(False) - if not self._block_signals: - self._emit('request-history', True) - self._requesting = 'before' - elif (adj.get_upper() - (adj.get_value() + adj.get_page_size()) < - distance): - # ..or near the bottom - if self._lower_complete: - return - # Workaround: https://gitlab.gnome.org/GNOME/gtk/merge_requests/395 - self.set_kinetic_scrolling(False) - if not self._block_signals: - self._emit('request-history', False) - self._requesting = 'after'
===================================== gajim/gtk/conversation/view.py ===================================== @@ -15,6 +15,7 @@ from __future__ import annotations
from typing import Any +from typing import Literal from typing import Optional from typing import Union from typing import Generator @@ -28,6 +29,8 @@
from gi.repository import GLib from gi.repository import Gtk +from gi.repository import GObject +from gi.repository import Gio
from nbxmpp.errors import StanzaError from nbxmpp.modules.security_labels import Displaymarking @@ -64,13 +67,47 @@ log = logging.getLogger('gajim.gui.conversation_view')
-class ConversationView(Gtk.ListBox): - def __init__(self) -> None: - Gtk.ListBox.__init__(self) - self.set_selection_mode(Gtk.SelectionMode.NONE) - self.set_sort_func(self._sort_func) +class ConversationView(Gtk.ScrolledWindow):
- self._contact = None + __gsignals__ = { + 'request-history': ( + GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION, + None, + (bool, ) + ), + 'autoscroll-changed': ( + GObject.SignalFlags.RUN_LAST, + None, + (bool,) + ) + } + + def __init__(self) -> None: + Gtk.ScrolledWindow.__init__(self) + + self.set_overlay_scrolling(False) + self.get_style_context().add_class('scrolled-no-border') + self.get_style_context().add_class('no-scroll-indicator') + self.get_style_context().add_class('scrollbar-style') + self.set_shadow_type(Gtk.ShadowType.IN) + self.set_vexpand(True) + + self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + + # This is a workaround: as soon as a line break occurs in Gtk.TextView + # with word-char wrapping enabled, a hyphen character is automatically + # inserted before the line break. This triggers the hscrollbar to show, + # see: https://gitlab.gnome.org/GNOME/gtk/-/issues/2384 + # Using set_hscroll_policy(Gtk.Scrollable.Policy.NEVER) would cause bad + # performance during resize, and prevent the window from being shrunk + # horizontally under certain conditions (applies to GroupchatControl) + self.get_hscrollbar().hide() + + self._list_box = Gtk.ListBox() + self._list_box.set_selection_mode(Gtk.SelectionMode.NONE) + self._list_box.set_sort_func(self._sort_func) + + self._contact: Optional[ChatContactT] = None self._client = None
# Keeps track of the number of rows shown in ConversationView @@ -86,24 +123,70 @@ def __init__(self) -> None: self._read_marker_row = None self._scroll_hint_row = None
- @property - def contact(self) -> types.ChatContactT: - assert self._contact is not None - return self._contact + self._current_upper: float = 0 + self._autoscroll: bool = True + self._request_history_at_upper: Optional[float] = None + self._upper_complete: bool = False + self._lower_complete: bool = True + self._requesting: Optional[str] = None + self._block_signals = False
- def clear(self) -> None: - self._contact = None - self._client = None + self._signal_handlers_enabled = False + self._signal_handler_ids = (0, 0) + + self.add(self._list_box) + self.set_focus_vadjustment(Gtk.Adjustment()) + + app.window.get_action('scroll-view-up').connect( + 'activate', self._on_scroll_view) + app.window.get_action('scroll-view-down').connect( + 'activate', self._on_scroll_view) + + def _on_scroll_view(self, + action: Gio.SimpleAction, + _param: Literal[None]) -> None: + + action_name = action.get_name() + if action_name == 'scroll-view-down': + self.emit('scroll-child', Gtk.ScrollType.PAGE_DOWN, False)
+ elif action_name == 'scroll-view-up': + self.emit('scroll-child', Gtk.ScrollType.PAGE_UP, False) + + def _enable_signal_handlers(self, enable: bool) -> None: + if self._signal_handlers_enabled == enable: + return + + vadjustment = self.get_vadjustment() + + if enable: + upper_id = vadjustment.connect('notify::upper', + self._on_adj_upper_changed) + value_id = vadjustment.connect('notify::value', + self._on_adj_value_changed) + self._signal_handler_ids = (upper_id, value_id) + else: + upper_id, value_id = self._signal_handler_ids + vadjustment.disconnect(upper_id) + vadjustment.disconnect(value_id) + + self._signal_handlers_enabled = enable + + def clear(self) -> None: app.settings.disconnect_signals(self) + self._enable_signal_handlers(False) + self._reset()
- self.reset() + self._contact = None + self._client = None
def switch_contact(self, contact: ChatContactT) -> None: self._contact = contact self._client = app.get_client(contact.account)
- self.reset() + self._enable_signal_handlers(False) + self._block_signals = True + self._reset()
app.settings.disconnect_signals(self)
@@ -117,11 +200,30 @@ def switch_contact(self, contact: ChatContactT) -> None: account=contact.account, jid=contact.jid)
- def get_row_at_index(self, index: int) -> BaseRow: - return cast(BaseRow, Gtk.ListBox.get_row_at_index(self, index)) + self._block_signals = False + self._enable_signal_handlers(True) + self._emit('request-history', True)
- def reset(self) -> None: - for row in self.get_children(): + def get_autoscroll(self) -> bool: + return self._autoscroll + + def block_signals(self, value: bool) -> None: + self._block_signals = value + + def _emit(self, signal_name: str, *args: Any) -> None: + if not self._block_signals: + log.debug('emit %s, %s', signal_name, args) + self.emit(signal_name, *args) + + def _reset(self) -> None: + self._current_upper = 0 + self._request_history_at_upper = None + self._upper_complete = False + self._lower_complete = True + self._requesting = None + self.set_history_complete(True, False) + + for row in self._list_box.get_children(): row.destroy()
self._row_count = 0 @@ -134,30 +236,125 @@ def reset(self) -> None: # These need to be present if ConversationView is reset # without switch_contact being invoked self._read_marker_row = ReadMarkerRow(self._contact) - self.add(self._read_marker_row) + self._list_box.add(self._read_marker_row)
self._scroll_hint_row = ScrollHintRow(self._contact.account) - self.add(self._scroll_hint_row) + self._list_box.add(self._scroll_hint_row) + + def reset(self) -> None: + assert self._contact is not None + self.switch_contact(self._contact) + + def set_history_complete(self, before: bool, complete: bool) -> None: + if before: + self._upper_complete = complete + if self._scroll_hint_row is None: + return + self._scroll_hint_row.set_history_complete(complete) + else: + self._lower_complete = complete + + def get_lower_complete(self) -> bool: + return self._lower_complete + + def _on_adj_upper_changed(self, + adj: Gtk.Adjustment, + _pspec: GObject.ParamSpec) -> None: + + upper = adj.get_upper() + diff = upper - self._current_upper + + 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.set_kinetic_scrolling(True) + if self._requesting == 'before': + adj.set_value(adj.get_value() + diff) + + if upper == adj.get_page_size(): + # There is no scrollbar + if not self._block_signals: + self._emit('request-history', True) + self._lower_complete = True + self._autoscroll = True + self._emit('autoscroll-changed', self._autoscroll) + + self._requesting = None + + def _on_adj_value_changed(self, + adj: Gtk.Adjustment, + _pspec: GObject.ParamSpec) -> None: + + if self._requesting is not None: + return + + bottom = adj.get_upper() - adj.get_page_size() + + self._autoscroll = bottom - adj.get_value() < 1 + self._emit('autoscroll-changed', self._autoscroll) + + if self._upper_complete: + self._request_history_at_upper = None + if self._lower_complete: + return + + if self._request_history_at_upper == adj.get_upper(): + # Abort here if we already did a history request and the upper + # did not change. This can happen if we scroll very fast and the + # value changes while the request has not been fulfilled. + return + + self._request_history_at_upper = None + + distance = adj.get_page_size() * 2 + if adj.get_value() < distance: + # Load messages when we are near the top + if self._upper_complete: + return + self._request_history_at_upper = adj.get_upper() + # Workaround: https://gitlab.gnome.org/GNOME/gtk/merge_requests/395 + self.set_kinetic_scrolling(False) + if not self._block_signals: + self._emit('request-history', True) + self._requesting = 'before' + + elif (adj.get_upper() - (adj.get_value() + adj.get_page_size()) < + distance): + # ..or near the bottom + if self._lower_complete: + return + # Workaround: https://gitlab.gnome.org/GNOME/gtk/merge_requests/395 + self.set_kinetic_scrolling(False) + if not self._block_signals: + self._emit('request-history', False) + self._requesting = 'after' + + @property + def contact(self) -> types.ChatContactT: + assert self._contact is not None + return self._contact + + def _get_row_at_index(self, index: int) -> BaseRow: + return cast(BaseRow, self._list_box.get_row_at_index(index))
def get_first_message_row(self) -> Optional[MessageRow]: - for row in self.get_children(): + for row in self._list_box.get_children(): if isinstance(row, MessageRow): return row return None
def get_last_message_row(self) -> Optional[MessageRow]: - children = self.get_children() + children = self._list_box.get_children() children.reverse() for row in children: if isinstance(row, MessageRow): return row return None
- def set_history_complete(self, complete: bool) -> None: - if self._scroll_hint_row is None: - return - self._scroll_hint_row.set_history_complete(complete) - @staticmethod def _sort_func(row1: BaseRow, row2: BaseRow) -> int: if row1.timestamp == row2.timestamp: @@ -293,7 +490,7 @@ def add_message(self, self._insert_message(message_row)
def _insert_message(self, message: BaseRow) -> None: - self.add(message) + self._list_box.add(message) self._add_date_row(message.timestamp) self._check_for_merge(message) assert self._read_marker_row is not None @@ -311,9 +508,9 @@ def _add_date_row(self, timestamp: datetime) -> None:
date_row = DateRow(self.contact.account, start_of_day) self._active_date_rows.add(start_of_day) - self.add(date_row) + self._list_box.add(date_row)
- row = self.get_row_at_index(date_row.get_index() + 1) + row = self._get_row_at_index(date_row.get_index() + 1) if row is None: return
@@ -340,7 +537,7 @@ def _find_ancestor(self, message: MessageRow) -> Optional[MessageRow]: index = message.get_index() while index != 0: index -= 1 - row = self.get_row_at_index(index) + row = self._get_row_at_index(index) if row is None: return None
@@ -361,7 +558,7 @@ def _update_descendants(self, message: MessageRow) -> None: index = message.get_index() while True: index += 1 - row = self.get_row_at_index(index) + row = self._get_row_at_index(index) if row is None: return
@@ -382,7 +579,7 @@ def _update_descendants(self, message: MessageRow) -> None:
def reduce_message_count(self, before: bool) -> bool: success = False - row_count = len(self.get_children()) + row_count = len(self._list_box.get_children()) while row_count > self._max_row_count: if before: if self._reduce_messages_before(): @@ -399,8 +596,8 @@ def _reduce_messages_before(self) -> bool: success = False
# We want to keep relevant DateRows when removing rows - row1 = self.get_row_at_index(2) - row2 = self.get_row_at_index(3) + row1 = self._get_row_at_index(2) + row2 = self._get_row_at_index(3)
if row1.type == 'date' and row2.type == 'date': # First two rows are date rows, @@ -422,12 +619,12 @@ def _reduce_messages_before(self) -> bool: return success
def _reduce_messages_after(self) -> None: - row = self.get_row_at_index(len(self.get_children()) - 1) + row = self._get_row_at_index(len(self._list_box.get_children()) - 1) row.destroy()
def scroll_to_message_and_highlight(self, log_line_id: int) -> None: highlight_row = None - for row in cast(list[BaseRow], self.get_children()): + for row in cast(list[BaseRow], self._list_box.get_children()): row.get_style_context().remove_class( 'conversation-search-highlight') if row.log_line_id == log_line_id: @@ -443,7 +640,7 @@ def _get_row_by_message_id(self, id_: str) -> Optional[MessageRow]: return self._message_id_row_map.get(id_)
def get_row_by_log_line_id(self, log_line_id: int) -> Optional[MessageRow]: - for row in cast(list[BaseRow], self.get_children()): + for row in cast(list[BaseRow], self._list_box.get_children()): if not isinstance(row, MessageRow): continue if row.log_line_id == log_line_id: @@ -451,7 +648,7 @@ def get_row_by_log_line_id(self, log_line_id: int) -> Optional[MessageRow]: return None
def get_row_by_stanza_id(self, stanza_id: str) -> Optional[MessageRow]: - for row in cast(list[BaseRow], self.get_children()): + for row in cast(list[BaseRow], self._list_box.get_children()): if not isinstance(row, MessageRow): continue if row.stanza_id == stanza_id: @@ -459,7 +656,7 @@ def get_row_by_stanza_id(self, stanza_id: str) -> Optional[MessageRow]: return None
def iter_rows(self) -> Generator[BaseRow, None, None]: - for row in cast(list[BaseRow], self.get_children()): + for row in cast(list[BaseRow], self._list_box.get_children()): yield row
def remove_rows_by_type(self, row_type: str) -> None: @@ -468,7 +665,7 @@ def remove_rows_by_type(self, row_type: str) -> None: row.destroy()
def update_call_rows(self) -> None: - for row in cast(list[BaseRow], self.get_children()): + for row in cast(list[BaseRow], self._list_box.get_children()): if isinstance(row, CallRow): row.update()
@@ -489,7 +686,7 @@ def set_read_marker(self, id_: str) -> None: self._read_marker_row.set_timestamp(timestamp)
def update_avatars(self) -> None: - for row in cast(list[BaseRow], self.get_children()): + for row in cast(list[BaseRow], self._list_box.get_children()): if isinstance(row, MessageRow): row.update_avatar()
@@ -538,5 +735,5 @@ def _on_contact_setting_changed(self, self.remove_rows_by_type('muc-user-status')
def remove(self, widget: Gtk.Widget) -> None: - super().remove(widget) + self._list_box.remove(widget) widget.destroy()
===================================== gajim/gtk/message_actions_box.py ===================================== @@ -515,12 +515,6 @@ def _on_msg_textview_key_press_event(self, Direction.PREV, unread_first=True) return True
- if event.keyval in (Gdk.KEY_Page_Down, Gdk.KEY_Page_Up): - control = app.window.get_control() - if control.has_active_chat(): - control.conversation_view.event(event) - return True - if event_state & Gdk.ModifierType.CONTROL_MASK: if event.keyval == Gdk.KEY_Tab: app.window.select_next_chat(Direction.NEXT, unread_first=True) @@ -659,6 +653,3 @@ def _on_message_sent(self, event: events.MessageSent) -> None: self.last_message_id[(account, jid)] = None else: self.last_message_id[(account, jid)] = event.message_id - - self.msg_textview.get_style_context().remove_class( - 'gajim-msg-correcting')
View it on GitLab: https://dev.gajim.org/gajim/gajim/-/compare/207f8155f5445bc24dcae4afca08dc47...