Add basic user autocompletion UI
This commit is contained in:
@@ -22,7 +22,7 @@ from .errors import MatrixError
|
||||
from .matrix_client import MatrixClient
|
||||
from .media_cache import MediaCache
|
||||
from .models import SyncId
|
||||
from .models.filters import FieldSubstringFilter
|
||||
from .models.filters import FieldStringFilter
|
||||
from .models.items import Account, Event, Homeserver, PingStatus
|
||||
from .models.model import Model
|
||||
from .models.model_store import ModelStore
|
||||
@@ -458,8 +458,8 @@ class Backend:
|
||||
return (settings, ui_state, history, theme)
|
||||
|
||||
|
||||
async def set_substring_filter(self, model_id: SyncId, value: str) -> None:
|
||||
"""Set a FieldSubstringFilter model's filter property.
|
||||
async def set_string_filter(self, model_id: SyncId, value: str) -> None:
|
||||
"""Set a FieldStringFilter (or derived class) model's filter property.
|
||||
|
||||
This should only be called from QML.
|
||||
"""
|
||||
@@ -469,8 +469,8 @@ class Backend:
|
||||
|
||||
model = Model.proxies[model_id]
|
||||
|
||||
if not isinstance(model, FieldSubstringFilter):
|
||||
raise TypeError("model_id must point to a FieldSubstringFilter")
|
||||
if not isinstance(model, FieldStringFilter):
|
||||
raise TypeError("model_id must point to a FieldStringFilter")
|
||||
|
||||
model.filter = value
|
||||
|
||||
|
@@ -112,13 +112,12 @@ class ModelFilter(ModelProxy):
|
||||
callback()
|
||||
|
||||
|
||||
class FieldSubstringFilter(ModelFilter):
|
||||
class FieldStringFilter(ModelFilter):
|
||||
"""Filter source models based on if their fields matches a string.
|
||||
|
||||
This is used for filter fields in QML: the user enters some text and only
|
||||
items with a certain field (typically `display_name`) that contain the
|
||||
words of the text (can be partial, e.g. "red" matches "red" or "tired")
|
||||
will be shown.
|
||||
items with a certain field (typically `display_name`) that starts with the
|
||||
entered text will be shown.
|
||||
|
||||
Matching is done using "smart case": insensitive if the filter text is
|
||||
all lowercase, sensitive otherwise.
|
||||
@@ -128,6 +127,8 @@ class FieldSubstringFilter(ModelFilter):
|
||||
self.fields: Collection[str] = fields
|
||||
self._filter: str = ""
|
||||
|
||||
self.no_filter_accept_all_items: bool = True
|
||||
|
||||
super().__init__(sync_id)
|
||||
|
||||
|
||||
@@ -138,24 +139,48 @@ class FieldSubstringFilter(ModelFilter):
|
||||
|
||||
@filter.setter
|
||||
def filter(self, value: str) -> None:
|
||||
self._filter = value
|
||||
self.refilter()
|
||||
if value != self._filter:
|
||||
self._filter = value
|
||||
self.refilter()
|
||||
|
||||
|
||||
def accept_item(self, item: "ModelItem") -> bool:
|
||||
if not self.filter:
|
||||
return True
|
||||
return self.no_filter_accept_all_items
|
||||
|
||||
text = " ".join((getattr(item, f) for f in self.fields))
|
||||
filt = self.filter
|
||||
filt_lower = filt.lower()
|
||||
fields = {f: getattr(item, f) for f in self.fields}
|
||||
filtr = self.filter
|
||||
lowercase = filtr.lower()
|
||||
|
||||
if filt_lower == filt:
|
||||
# Consider case only if filter isn't all lowercase (smart case)
|
||||
filt = filt_lower
|
||||
text = text.lower()
|
||||
if lowercase == filtr:
|
||||
# Consider case only if filter isn't all lowercase
|
||||
filtr = lowercase
|
||||
fields = {name: value.lower() for name, value in fields.items()}
|
||||
|
||||
for word in filt.split():
|
||||
return self.match(fields, filtr)
|
||||
|
||||
|
||||
def match(self, fields: Dict[str, str], filtr: str) -> bool:
|
||||
for value in fields.values():
|
||||
if value.startswith(filtr):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class FieldSubstringFilter(FieldStringFilter):
|
||||
"""Fuzzy-like alternative to `FieldStringFilter`.
|
||||
|
||||
All words in the filter string must fully or partially match words in the
|
||||
item field values, e.g. "red l" can match "red light",
|
||||
"tired legs", "light red" (order of the filter words doesn't matter),
|
||||
but not just "red" or "light" by themselves.
|
||||
"""
|
||||
|
||||
def match(self, fields: Dict[str, str], filtr: str) -> bool:
|
||||
text = " ".join(fields.values())
|
||||
|
||||
for word in filtr.split():
|
||||
if word and word not in text:
|
||||
return False
|
||||
|
||||
|
@@ -7,7 +7,8 @@ from typing import Dict
|
||||
from . import SyncId
|
||||
from .model import Model
|
||||
from .special_models import (
|
||||
AllRooms, FilteredMembers, FilteredHomeservers, MatchingAccounts,
|
||||
AllRooms, AutoCompletedMembers, FilteredHomeservers, FilteredMembers,
|
||||
MatchingAccounts,
|
||||
)
|
||||
|
||||
|
||||
@@ -42,6 +43,8 @@ class ModelStore(UserDict):
|
||||
model = FilteredHomeservers()
|
||||
elif is_tuple and len(key) == 3 and key[2] == "filtered_members":
|
||||
model = FilteredMembers(user_id=key[0], room_id=key[1])
|
||||
elif is_tuple and len(key) == 3 and key[2] == "autocompleted_members":
|
||||
model = AutoCompletedMembers(user_id=key[0], room_id=key[1])
|
||||
else:
|
||||
model = Model(sync_id=key) # type: ignore
|
||||
|
||||
|
@@ -1,9 +1,9 @@
|
||||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
from dataclasses import asdict
|
||||
from typing import Set
|
||||
from typing import Dict, Set
|
||||
|
||||
from .filters import FieldSubstringFilter, ModelFilter
|
||||
from .filters import FieldStringFilter, FieldSubstringFilter, ModelFilter
|
||||
from .items import Account, AccountOrRoom, Room
|
||||
from .model import Model
|
||||
from .model_item import ModelItem
|
||||
@@ -117,6 +117,27 @@ class FilteredMembers(FieldSubstringFilter):
|
||||
return source.sync_id == (self.user_id, self.room_id, "members")
|
||||
|
||||
|
||||
class AutoCompletedMembers(FieldStringFilter):
|
||||
"""Filtered list of mentionable members for tab-completion."""
|
||||
|
||||
def __init__(self, user_id: str, room_id: str) -> None:
|
||||
self.user_id = user_id
|
||||
self.room_id = room_id
|
||||
sync_id = (user_id, room_id, "autocompleted_members")
|
||||
|
||||
super().__init__(sync_id=sync_id, fields=("display_name", "id"))
|
||||
self.no_filter_accept_all_items = False
|
||||
|
||||
|
||||
def accept_source(self, source: Model) -> bool:
|
||||
return source.sync_id == (self.user_id, self.room_id, "members")
|
||||
|
||||
|
||||
def match(self, fields: Dict[str, str], filtr: str) -> bool:
|
||||
fields["id"] = fields["id"][1:] # remove leading @
|
||||
return super().match(fields, filtr)
|
||||
|
||||
|
||||
class FilteredHomeservers(FieldSubstringFilter):
|
||||
"""Filtered list of public Matrix homeservers."""
|
||||
|
||||
|
Reference in New Issue
Block a user