Skip to content
Snippets Groups Projects
Commit 07c793dc authored by Philipp Hörist's avatar Philipp Hörist Committed by mesonium
Browse files

refactor: Add GroupchatNick completion

parent 92e82516
No related branches found
No related tags found
No related merge requests found
......@@ -4,6 +4,7 @@
from __future__ import annotations
from typing import Any
from typing import Generic
from typing import Type
from typing import TypeVar
......@@ -47,5 +48,5 @@ def get_model(self) -> tuple[Gio.ListModel, Type[Gtk.Widget]]:
def check(self, candidate: str, start_iter: Gtk.TextIter) -> bool:
raise NotImplementedError
def populate(self, candidate: str) -> bool:
def populate(self, candidate: str, contact: Any) -> bool:
raise NotImplementedError
......@@ -4,6 +4,7 @@
from __future__ import annotations
from typing import Any
from typing import Final
from typing import Type
......@@ -102,7 +103,7 @@ def check(self, candidate: str, start_iter: Gtk.TextIter) -> bool:
return False
return start_iter.get_offset() == 0
def populate(self, candidate: str) -> bool:
def populate(self, candidate: str, contact: Any) -> bool:
candidate = candidate.lstrip(self.trigger_char)
self._string_filter.set_search(candidate)
return self._model.get_n_items() > 0
......@@ -4,6 +4,7 @@
from __future__ import annotations
from typing import Any
from typing import Final
from typing import Type
......@@ -304,7 +305,7 @@ def _load_emoji_data() -> Gio.ListStore:
def check(self, candidate: str, start_iter: Gtk.TextIter) -> bool:
return candidate.startswith(self.trigger_char)
def populate(self, candidate: str) -> bool:
def populate(self, candidate: str, contact: Any) -> bool:
candidate = candidate.lstrip(self.trigger_char)
if not candidate or not len(candidate) > 1:
# Don't activate until a sufficient # of chars have been typed,
......@@ -321,4 +322,4 @@ def populate(self, candidate: str) -> bool:
self._load_complete = True
self._string_filter.set_search(candidate)
return True
return self._model.get_n_items() > 0
# This file is part of Gajim.
#
# SPDX-License-Identifier: GPL-3.0-only
from __future__ import annotations
from typing import Any
from typing import Final
from typing import Type
import logging
from gi.repository import Gio
from gi.repository import GObject
from gi.repository import Gtk
from gajim.common import app
from gajim.common import ged
from gajim.common.events import MessageReceived
from gajim.common.helpers import jid_is_blocked
from gajim.common.modules.contacts import GroupchatContact
from gajim.gtk.completion.base import BaseCompletionListItem
from gajim.gtk.completion.base import BaseCompletionProvider
from gajim.gtk.completion.base import BaseCompletionViewItem
log = logging.getLogger('gajim.gtk.completion.nickname')
MAX_COMPLETION_ENTRIES = 10
class NicknameCompletionListItem(BaseCompletionListItem, GObject.Object):
__gtype_name__ = "NicknameCompletionListItem"
nickname = GObject.Property(type=str)
def get_text(self) -> str:
return f'{self.props.nickname}{app.settings.get("gc_refer_to_nick_char")}'
class NicknameCompletionViewItem(
BaseCompletionViewItem[NicknameCompletionListItem], Gtk.Box
):
__gtype_name__ = "NicknameCompletionViewItem"
def __init__(self) -> None:
BaseCompletionViewItem.__init__(self)
Gtk.Box.__init__(self)
self._label = Gtk.Label()
self.append(self._label)
def bind(self, obj: NicknameCompletionListItem) -> None:
bind_spec = [
('nickname', self._label, 'label'),
]
for source_prop, widget, target_prop in bind_spec:
bind = obj.bind_property(
source_prop, widget, target_prop, GObject.BindingFlags.SYNC_CREATE
)
self._bindings.append(bind)
def unbind(self) -> None:
for bind in self._bindings:
bind.unbind()
self._bindings.clear()
def do_unroot(self) -> None:
Gtk.Box.do_unroot(self)
app.check_finalize(self)
class NicknameCompletionProvider(BaseCompletionProvider):
trigger_char: Final = '@'
def __init__(self) -> None:
self._list_store = Gio.ListStore(item_type=NicknameCompletionListItem)
self._contact: GroupchatContact | None = None
app.ged.register_event_handler(
'message-received', ged.GUI2, self._on_message_received
)
expression = Gtk.PropertyExpression.new(
NicknameCompletionListItem, None, 'nickname'
)
self._string_filter = Gtk.StringFilter(expression=expression)
filter_model = Gtk.FilterListModel(
model=self._list_store, filter=self._string_filter
)
self._model = Gtk.SliceListModel(
model=filter_model, size=MAX_COMPLETION_ENTRIES
)
def get_model(self) -> tuple[Gio.ListModel, Type[Gtk.Widget]]:
return self._model, NicknameCompletionViewItem
def check(self, candidate: str, start_iter: Gtk.TextIter) -> bool:
return candidate.startswith(self.trigger_char)
def populate(self, candidate: str, contact: Any) -> bool:
if not isinstance(contact, GroupchatContact):
return False
if self._contact is not contact:
# New contact, regenerate suggestions
self._list_store.remove_all()
for nickname in self._generate_suggestions(contact):
self._list_store.append(NicknameCompletionListItem(nickname=nickname))
self._contact = contact
candidate = candidate.lstrip(self.trigger_char)
self._string_filter.set_search(candidate)
return self._model.get_n_items() > 0
def _generate_suggestions(self, contact: GroupchatContact) -> list[str]:
def _nick_matching(nick: str) -> bool:
if nick == contact.nickname:
return False
participant = contact.get_resource(nick)
return not jid_is_blocked(contact.account, str(participant.jid))
# Get recent nicknames from DB. This enables us to suggest
# nicknames even if no message arrived since Gajim was started.
recent_nicknames = app.storage.archive.get_recent_muc_nicks(
contact.account, contact.jid
)
matches: list[str] = []
for nick in recent_nicknames:
if _nick_matching(nick):
matches.append(nick)
# Add all other MUC participants
other_nicks: list[str] = []
for participant in contact.get_participants():
if _nick_matching(participant.name):
if participant.name not in matches:
other_nicks.append(participant.name)
other_nicks.sort(key=str.lower)
return matches + other_nicks
def _on_message_received(self, event: MessageReceived) -> None:
if self._contact is None:
return
if event.jid != self._contact.jid:
return
# This will trigger generating new suggestions
self._contact = None
......@@ -52,8 +52,6 @@ def __init__(self, message_input: GtkSource.View) -> None:
# self._message_input.connect('key-press-event', self._on_key_press)
# self._message_input.connect('focus-out-event', self._on_focus_out)
# self._nick_completion = GroupChatNickCompletion()
self._view = CompletionListView(
model=Gtk.SingleSelection(), single_click_activate=True
)
......
# This file is part of Gajim.
#
# SPDX-License-Identifier: GPL-3.0-only
from __future__ import annotations
import logging
from gi.repository import Gdk
from gi.repository import GtkSource
from gajim.common import app
from gajim.common import ged
from gajim.common.events import MessageReceived
from gajim.common.ged import EventHelper
from gajim.common.helpers import jid_is_blocked
from gajim.common.modules.contacts import GroupchatContact
log = logging.getLogger('gajim.gtk.groupchat_nick_completion')
class GroupChatNickCompletion(EventHelper):
def __init__(self) -> None:
EventHelper.__init__(self)
self._contact: GroupchatContact | None = None
self._suggestions: list[str] = []
self._last_key_tab = False
self.register_event(
'message-received', ged.GUI2, self._on_message_received)
def switch_contact(self, contact: GroupchatContact) -> None:
self._suggestions.clear()
self._last_key_tab = False
self._contact = contact
def process_key_press(self,
source_view: GtkSource.View,
event: Any
) -> bool:
if (event.get_state() & Gdk.ModifierType.SHIFT_MASK or
event.get_state() & Gdk.ModifierType.CONTROL_MASK or
event.keyval not in (Gdk.KEY_ISO_Left_Tab, Gdk.KEY_Tab)):
self._last_key_tab = False
return False
message_buffer = source_view.get_buffer()
start_iter, end_iter = message_buffer.get_bounds()
cursor_position = message_buffer.get_insert()
end_iter = message_buffer.get_iter_at_mark(cursor_position)
text = message_buffer.get_text(start_iter, end_iter, False)
if text.split():
# Store last word for autocompletion
prefix = text.split()[-1]
else:
prefix = ''
# Configurable string to be displayed after the nick:
# e.g. "user," or "user:"
ref_ext = app.settings.get('gc_refer_to_nick_char')
has_ref_ext = False
# Default suffix to 1: space printed after completion
suffix_len = 1
if ref_ext and text.endswith(ref_ext + ' '):
has_ref_ext = True
suffix_len = len(ref_ext + ' ')
if not self._last_key_tab or not self._suggestions:
self._suggestions = self._generate_suggestions(prefix)
if not self._suggestions:
self._last_key_tab = True
return False
if (self._last_key_tab and
text[:-suffix_len].endswith(self._suggestions[0])):
# Cycle suggestions list
self._suggestions.append(self._suggestions[0])
prefix = self._suggestions.pop(0)
if len(text.split()) < 2 or has_ref_ext:
suffix = ref_ext + ' '
else:
suffix = ' '
start_iter = end_iter.copy()
if (self._last_key_tab and has_ref_ext or (text and text[-1] == ' ')):
# Mind the added space from last completion;
# ref_ext may also consist of more than one char
start_iter.backward_chars(len(prefix) + len(suffix))
else:
start_iter.backward_chars(len(prefix))
assert self._contact is not None
client = app.get_client(self._contact.account)
client.get_module('Chatstate').block_chatstates(self._contact, True)
message_buffer.delete(start_iter, end_iter)
completion = self._suggestions[0]
message_buffer.insert_at_cursor(completion + suffix)
client.get_module('Chatstate').block_chatstates(self._contact, False)
self._last_key_tab = True
return True
def _generate_suggestions(self, prefix: str) -> list[str]:
def _nick_matching(nick: str) -> bool:
assert self._contact is not None
if nick == self._contact.nickname:
return False
participant = self._contact.get_resource(nick)
if jid_is_blocked(self._contact.account, str(participant.jid)):
return False
if prefix == '':
return True
return nick.lower().startswith(prefix.lower())
assert self._contact is not None
# Get recent nicknames from DB. This enables us to suggest
# nicknames even if no message arrived since Gajim was started.
recent_nicknames = app.storage.archive.get_recent_muc_nicks(
self._contact.account, self._contact.jid)
matches: list[str] = []
for nick in recent_nicknames:
if _nick_matching(nick):
matches.append(nick)
# Add all other MUC participants
other_nicks: list[str] = []
for contact in self._contact.get_participants():
if _nick_matching(contact.name):
if contact.name not in matches:
other_nicks.append(contact.name)
other_nicks.sort(key=str.lower)
return matches + other_nicks
def _on_message_received(self, event: MessageReceived) -> None:
if self._contact is None:
return
if event.jid != self._contact.jid:
return
if not self._last_key_tab:
# Clear suggestions if not actively using them
# (new messages may have new nicks)
self._suggestions.clear()
......@@ -32,6 +32,7 @@
from gajim.gtk.completion.commands import CommandsCompletionProvider
from gajim.gtk.completion.emoji import EmojiCompletionProvider
from gajim.gtk.completion.nickname import NicknameCompletionProvider
from gajim.gtk.completion.popover import CompletionPopover
from gajim.gtk.const import MAX_MESSAGE_LENGTH
from gajim.gtk.menus import get_message_input_extra_context_menu
......@@ -75,6 +76,7 @@ def __init__(self) -> None:
self._completion_providers = [
EmojiCompletionProvider(),
CommandsCompletionProvider(),
NicknameCompletionProvider(),
]
self.add_css_class('gajim-conversation-text')
......@@ -172,7 +174,7 @@ def _populate_completion(self, buf: Gtk.TextBuffer) -> bool:
if not provider.check(candidate, start):
continue
if provider.populate(candidate):
if provider.populate(candidate, self._contact):
self._completion_popover.set_provider(provider)
return True
return False
......
......@@ -7,7 +7,7 @@
from gajim.common import app
from gajim.gtk.groupchat_nick_completion import GroupChatNickCompletion
# Broken
class Test(unittest.TestCase):
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment