Add basic user autocompletion UI

This commit is contained in:
miruka
2020-08-20 12:21:47 -04:00
parent ec17d54923
commit 5ba669444d
11 changed files with 271 additions and 63 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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."""