Add basic user autocompletion UI
This commit is contained in:
parent
ec17d54923
commit
5ba669444d
2
TODO.md
2
TODO.md
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
- refresh server list button
|
- refresh server list button
|
||||||
- server list sorting method / explain what the % number is (stability)
|
- server list sorting method / explain what the % number is (stability)
|
||||||
- spam alt+shift+a when starting app on server browser → segfault
|
- spam alt+shift+a/going to room when starting app on server browser → segfault
|
||||||
- cursor shape in HBox/HTabbedBox pages over fields
|
- cursor shape in HBox/HTabbedBox pages over fields
|
||||||
- login with account already added → infinite spinner in room list
|
- login with account already added → infinite spinner in room list
|
||||||
- verify onKeyboardAccept/Cancel things respect button enabled state
|
- verify onKeyboardAccept/Cancel things respect button enabled state
|
||||||
|
|
|
@ -22,7 +22,7 @@ from .errors import MatrixError
|
||||||
from .matrix_client import MatrixClient
|
from .matrix_client import MatrixClient
|
||||||
from .media_cache import MediaCache
|
from .media_cache import MediaCache
|
||||||
from .models import SyncId
|
from .models import SyncId
|
||||||
from .models.filters import FieldSubstringFilter
|
from .models.filters import FieldStringFilter
|
||||||
from .models.items import Account, Event, Homeserver, PingStatus
|
from .models.items import Account, Event, Homeserver, PingStatus
|
||||||
from .models.model import Model
|
from .models.model import Model
|
||||||
from .models.model_store import ModelStore
|
from .models.model_store import ModelStore
|
||||||
|
@ -458,8 +458,8 @@ class Backend:
|
||||||
return (settings, ui_state, history, theme)
|
return (settings, ui_state, history, theme)
|
||||||
|
|
||||||
|
|
||||||
async def set_substring_filter(self, model_id: SyncId, value: str) -> None:
|
async def set_string_filter(self, model_id: SyncId, value: str) -> None:
|
||||||
"""Set a FieldSubstringFilter model's filter property.
|
"""Set a FieldStringFilter (or derived class) model's filter property.
|
||||||
|
|
||||||
This should only be called from QML.
|
This should only be called from QML.
|
||||||
"""
|
"""
|
||||||
|
@ -469,8 +469,8 @@ class Backend:
|
||||||
|
|
||||||
model = Model.proxies[model_id]
|
model = Model.proxies[model_id]
|
||||||
|
|
||||||
if not isinstance(model, FieldSubstringFilter):
|
if not isinstance(model, FieldStringFilter):
|
||||||
raise TypeError("model_id must point to a FieldSubstringFilter")
|
raise TypeError("model_id must point to a FieldStringFilter")
|
||||||
|
|
||||||
model.filter = value
|
model.filter = value
|
||||||
|
|
||||||
|
|
|
@ -112,13 +112,12 @@ class ModelFilter(ModelProxy):
|
||||||
callback()
|
callback()
|
||||||
|
|
||||||
|
|
||||||
class FieldSubstringFilter(ModelFilter):
|
class FieldStringFilter(ModelFilter):
|
||||||
"""Filter source models based on if their fields matches a string.
|
"""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
|
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
|
items with a certain field (typically `display_name`) that starts with the
|
||||||
words of the text (can be partial, e.g. "red" matches "red" or "tired")
|
entered text will be shown.
|
||||||
will be shown.
|
|
||||||
|
|
||||||
Matching is done using "smart case": insensitive if the filter text is
|
Matching is done using "smart case": insensitive if the filter text is
|
||||||
all lowercase, sensitive otherwise.
|
all lowercase, sensitive otherwise.
|
||||||
|
@ -128,6 +127,8 @@ class FieldSubstringFilter(ModelFilter):
|
||||||
self.fields: Collection[str] = fields
|
self.fields: Collection[str] = fields
|
||||||
self._filter: str = ""
|
self._filter: str = ""
|
||||||
|
|
||||||
|
self.no_filter_accept_all_items: bool = True
|
||||||
|
|
||||||
super().__init__(sync_id)
|
super().__init__(sync_id)
|
||||||
|
|
||||||
|
|
||||||
|
@ -138,24 +139,48 @@ class FieldSubstringFilter(ModelFilter):
|
||||||
|
|
||||||
@filter.setter
|
@filter.setter
|
||||||
def filter(self, value: str) -> None:
|
def filter(self, value: str) -> None:
|
||||||
|
if value != self._filter:
|
||||||
self._filter = value
|
self._filter = value
|
||||||
self.refilter()
|
self.refilter()
|
||||||
|
|
||||||
|
|
||||||
def accept_item(self, item: "ModelItem") -> bool:
|
def accept_item(self, item: "ModelItem") -> bool:
|
||||||
if not self.filter:
|
if not self.filter:
|
||||||
|
return self.no_filter_accept_all_items
|
||||||
|
|
||||||
|
fields = {f: getattr(item, f) for f in self.fields}
|
||||||
|
filtr = self.filter
|
||||||
|
lowercase = filtr.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()}
|
||||||
|
|
||||||
|
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 True
|
||||||
|
|
||||||
text = " ".join((getattr(item, f) for f in self.fields))
|
return False
|
||||||
filt = self.filter
|
|
||||||
filt_lower = filt.lower()
|
|
||||||
|
|
||||||
if filt_lower == filt:
|
|
||||||
# Consider case only if filter isn't all lowercase (smart case)
|
|
||||||
filt = filt_lower
|
|
||||||
text = text.lower()
|
|
||||||
|
|
||||||
for word in filt.split():
|
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:
|
if word and word not in text:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,8 @@ from typing import Dict
|
||||||
from . import SyncId
|
from . import SyncId
|
||||||
from .model import Model
|
from .model import Model
|
||||||
from .special_models import (
|
from .special_models import (
|
||||||
AllRooms, FilteredMembers, FilteredHomeservers, MatchingAccounts,
|
AllRooms, AutoCompletedMembers, FilteredHomeservers, FilteredMembers,
|
||||||
|
MatchingAccounts,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,6 +43,8 @@ class ModelStore(UserDict):
|
||||||
model = FilteredHomeservers()
|
model = FilteredHomeservers()
|
||||||
elif is_tuple and len(key) == 3 and key[2] == "filtered_members":
|
elif is_tuple and len(key) == 3 and key[2] == "filtered_members":
|
||||||
model = FilteredMembers(user_id=key[0], room_id=key[1])
|
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:
|
else:
|
||||||
model = Model(sync_id=key) # type: ignore
|
model = Model(sync_id=key) # type: ignore
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
from dataclasses import asdict
|
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 .items import Account, AccountOrRoom, Room
|
||||||
from .model import Model
|
from .model import Model
|
||||||
from .model_item import ModelItem
|
from .model_item import ModelItem
|
||||||
|
@ -117,6 +117,27 @@ class FilteredMembers(FieldSubstringFilter):
|
||||||
return source.sync_id == (self.user_id, self.room_id, "members")
|
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):
|
class FilteredHomeservers(FieldSubstringFilter):
|
||||||
"""Filtered list of public Matrix homeservers."""
|
"""Filtered list of public Matrix homeservers."""
|
||||||
|
|
||||||
|
|
|
@ -175,7 +175,7 @@ HListView {
|
||||||
}
|
}
|
||||||
|
|
||||||
onFilterChanged: {
|
onFilterChanged: {
|
||||||
py.callCoro("set_substring_filter", ["all_rooms", filter], () => {
|
py.callCoro("set_string_filter", ["all_rooms", filter], () => {
|
||||||
if (filter) {
|
if (filter) {
|
||||||
currentIndex = 1 // highlight the first matching room
|
currentIndex = 1 // highlight the first matching room
|
||||||
return
|
return
|
||||||
|
|
|
@ -134,7 +134,7 @@ HBox {
|
||||||
|
|
||||||
onTextEdited: {
|
onTextEdited: {
|
||||||
py.callCoro(
|
py.callCoro(
|
||||||
"set_substring_filter", ["filtered_homeservers", text],
|
"set_string_filter", ["filtered_homeservers", text],
|
||||||
)
|
)
|
||||||
serverList.currentIndex = -1
|
serverList.currentIndex = -1
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,14 +11,22 @@ Rectangle {
|
||||||
function takeFocus() { messageArea.forceActiveFocus() }
|
function takeFocus() { messageArea.forceActiveFocus() }
|
||||||
|
|
||||||
|
|
||||||
implicitHeight:
|
implicitHeight: Math.max(theme.baseElementsHeight, column.implicitHeight)
|
||||||
Math.max(theme.baseElementsHeight, messageArea.implicitHeight)
|
|
||||||
|
|
||||||
color: theme.chat.composer.background
|
color: theme.chat.composer.background
|
||||||
|
|
||||||
HRowLayout {
|
HColumnLayout {
|
||||||
|
id: column
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
|
UserAutoCompletion {
|
||||||
|
id: userCompletion
|
||||||
|
textArea: messageArea
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.maximumHeight: chat.height / 3
|
||||||
|
}
|
||||||
|
|
||||||
|
HRowLayout {
|
||||||
HUserAvatar {
|
HUserAvatar {
|
||||||
id: avatar
|
id: avatar
|
||||||
radius: 0
|
radius: 0
|
||||||
|
@ -39,7 +47,19 @@ Rectangle {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
MessageArea { id: messageArea }
|
MessageArea {
|
||||||
|
id: messageArea
|
||||||
|
autoCompletionOpen: userCompletion.open
|
||||||
|
|
||||||
|
onAutoCompletePrevious: userCompletion.previous()
|
||||||
|
onAutoCompleteNext: userCompletion.next()
|
||||||
|
onCancelAutoCompletion: userCompletion.cancel()
|
||||||
|
onExtraCharacterCloseAutoCompletion:
|
||||||
|
! userCompletion.autoOpen ||
|
||||||
|
userCompletion.autoOpenCompleted ?
|
||||||
|
userCompletion.open = false :
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UploadButton {
|
UploadButton {
|
||||||
|
@ -47,3 +67,4 @@ Rectangle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,9 +6,10 @@ import "../../.."
|
||||||
import "../../../Base"
|
import "../../../Base"
|
||||||
|
|
||||||
HTextArea {
|
HTextArea {
|
||||||
id: textArea
|
id: area
|
||||||
|
|
||||||
property HListView eventList
|
property HListView eventList
|
||||||
|
property bool autoCompletionOpen
|
||||||
|
|
||||||
property string indent: " "
|
property string indent: " "
|
||||||
|
|
||||||
|
@ -20,12 +21,12 @@ HTextArea {
|
||||||
ModelStore.get("accounts").find(writingUserId)
|
ModelStore.get("accounts").find(writingUserId)
|
||||||
|
|
||||||
readonly property int cursorY:
|
readonly property int cursorY:
|
||||||
textArea.text.substring(0, cursorPosition).split("\n").length - 1
|
area.text.substring(0, cursorPosition).split("\n").length - 1
|
||||||
|
|
||||||
readonly property int cursorX:
|
readonly property int cursorX:
|
||||||
cursorPosition - lines.slice(0, cursorY).join("").length - cursorY
|
cursorPosition - lines.slice(0, cursorY).join("").length - cursorY
|
||||||
|
|
||||||
readonly property var lines: textArea.text.split("\n")
|
readonly property var lines: area.text.split("\n")
|
||||||
readonly property string lineText: lines[cursorY] || ""
|
readonly property string lineText: lines[cursorY] || ""
|
||||||
|
|
||||||
readonly property string lineTextUntilCursor:
|
readonly property string lineTextUntilCursor:
|
||||||
|
@ -50,8 +51,13 @@ HTextArea {
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signal autoCompletePrevious()
|
||||||
|
signal autoCompleteNext()
|
||||||
|
signal extraCharacterCloseAutoCompletion()
|
||||||
|
signal cancelAutoCompletion()
|
||||||
|
|
||||||
function setTyping(typing) {
|
function setTyping(typing) {
|
||||||
if (! textArea.enabled) return
|
if (! area.enabled) return
|
||||||
|
|
||||||
py.callClientCoro(
|
py.callClientCoro(
|
||||||
writingUserId, "room_typing", [chat.roomId, typing, 5000],
|
writingUserId, "room_typing", [chat.roomId, typing, 5000],
|
||||||
|
@ -76,7 +82,7 @@ HTextArea {
|
||||||
}
|
}
|
||||||
|
|
||||||
const add = indent.repeat(indents)
|
const add = indent.repeat(indents)
|
||||||
textArea.insertAtCursor("\n" + add)
|
area.insertAtCursor("\n" + add)
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendText() {
|
function sendText() {
|
||||||
|
@ -85,7 +91,7 @@ HTextArea {
|
||||||
const args = [chat.roomId, toSend, chat.replyToEventId]
|
const args = [chat.roomId, toSend, chat.replyToEventId]
|
||||||
py.callClientCoro(writingUserId, "send_text", args)
|
py.callClientCoro(writingUserId, "send_text", args)
|
||||||
|
|
||||||
textArea.clear()
|
area.clear()
|
||||||
clearReplyTo()
|
clearReplyTo()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,9 +180,11 @@ HTextArea {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
Keys.onEscapePressed: clearReplyTo()
|
Keys.onEscapePressed:
|
||||||
|
autoCompletionOpen ? cancelAutoCompletion() : clearReplyTo()
|
||||||
|
|
||||||
Keys.onReturnPressed: ev => {
|
Keys.onReturnPressed: ev => {
|
||||||
|
extraCharacterCloseAutoCompletion()
|
||||||
ev.accepted = true
|
ev.accepted = true
|
||||||
|
|
||||||
ev.modifiers & Qt.ShiftModifier ||
|
ev.modifiers & Qt.ShiftModifier ||
|
||||||
|
@ -189,18 +197,43 @@ HTextArea {
|
||||||
Keys.onEnterPressed: ev => Keys.returnPressed(ev)
|
Keys.onEnterPressed: ev => Keys.returnPressed(ev)
|
||||||
|
|
||||||
Keys.onMenuPressed: ev => {
|
Keys.onMenuPressed: ev => {
|
||||||
|
extraCharacterCloseAutoCompletion()
|
||||||
|
|
||||||
if (eventList && eventList.currentItem)
|
if (eventList && eventList.currentItem)
|
||||||
eventList.currentItem.openContextMenu()
|
eventList.currentItem.openContextMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onBacktabPressed: ev => {
|
||||||
|
ev.accepted = true
|
||||||
|
autoCompletePrevious()
|
||||||
|
}
|
||||||
|
|
||||||
Keys.onTabPressed: ev => {
|
Keys.onTabPressed: ev => {
|
||||||
ev.accepted = true
|
ev.accepted = true
|
||||||
textArea.insertAtCursor(indent)
|
|
||||||
|
if (text.slice(-1).trim()) { // previous char isn't a space/tab
|
||||||
|
autoCompleteNext()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
area.insertAtCursor(indent)
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onUpPressed: ev => {
|
||||||
|
ev.accepted = autoCompletionOpen
|
||||||
|
if (autoCompletionOpen) autoCompletePrevious()
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onDownPressed: ev => {
|
||||||
|
ev.accepted = autoCompletionOpen
|
||||||
|
if (autoCompletionOpen) autoCompleteNext()
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onPressed: ev => {
|
Keys.onPressed: ev => {
|
||||||
|
if (ev.text) extraCharacterCloseAutoCompletion()
|
||||||
|
|
||||||
if (ev.matches(StandardKey.Copy) &&
|
if (ev.matches(StandardKey.Copy) &&
|
||||||
! textArea.selectedText &&
|
! area.selectedText &&
|
||||||
eventList &&
|
eventList &&
|
||||||
(eventList.selectedCount || eventList.currentIndex !== -1))
|
(eventList.selectedCount || eventList.currentIndex !== -1))
|
||||||
{
|
{
|
||||||
|
@ -212,10 +245,10 @@ HTextArea {
|
||||||
// FIXME: buggy
|
// FIXME: buggy
|
||||||
// if (ev.modifiers === Qt.NoModifier &&
|
// if (ev.modifiers === Qt.NoModifier &&
|
||||||
// ev.key === Qt.Key_Backspace &&
|
// ev.key === Qt.Key_Backspace &&
|
||||||
// ! textArea.selectedText)
|
// ! area.selectedText)
|
||||||
// {
|
// {
|
||||||
// ev.accepted = true
|
// ev.accepted = true
|
||||||
// textArea.remove(
|
// area.remove(
|
||||||
// cursorPosition - deleteCharsOnBackspace,
|
// cursorPosition - deleteCharsOnBackspace,
|
||||||
// cursorPosition
|
// cursorPosition
|
||||||
// )
|
// )
|
||||||
|
|
105
src/gui/Pages/Chat/Composer/UserAutoCompletion.qml
Normal file
105
src/gui/Pages/Chat/Composer/UserAutoCompletion.qml
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick 2.12
|
||||||
|
import QtQuick.Layouts 1.12
|
||||||
|
import "../../.."
|
||||||
|
import "../../../Base"
|
||||||
|
import "../../../Base/HTile"
|
||||||
|
|
||||||
|
HListView {
|
||||||
|
id: listView
|
||||||
|
|
||||||
|
property HTextArea textArea
|
||||||
|
property bool open: false
|
||||||
|
|
||||||
|
property string originalText: ""
|
||||||
|
property bool autoOpenCompleted: false
|
||||||
|
|
||||||
|
readonly property bool autoOpen:
|
||||||
|
autoOpenCompleted || textArea.text.match(/.*(^|\W)@[^\s]+$/)
|
||||||
|
|
||||||
|
readonly property string wordToComplete:
|
||||||
|
open ?
|
||||||
|
(originalText || textArea.text).split(/\s/).slice(-1)[0].replace(
|
||||||
|
autoOpen ? /^@/ : "", "",
|
||||||
|
) :
|
||||||
|
""
|
||||||
|
|
||||||
|
function replaceLastWord(withText) {
|
||||||
|
const lastWordStart = /(?:^|\s)[^\s]+$/.exec(textArea.text).index
|
||||||
|
const isTextStart =
|
||||||
|
lastWordStart === 0 && ! textArea.text[0].match(/\s/)
|
||||||
|
|
||||||
|
textArea.remove(lastWordStart + (isTextStart ? 0 : 1), textArea.length)
|
||||||
|
textArea.insertAtCursor(withText)
|
||||||
|
}
|
||||||
|
|
||||||
|
function previous() {
|
||||||
|
if (open) {
|
||||||
|
decrementCurrentIndex()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
open = true
|
||||||
|
const args = [model.modelId, wordToComplete]
|
||||||
|
py.callCoro("set_string_filter", args, decrementCurrentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
function next() {
|
||||||
|
if (open) {
|
||||||
|
incrementCurrentIndex()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
open = true
|
||||||
|
const args = [model.modelId, wordToComplete]
|
||||||
|
py.callCoro("set_string_filter", args, incrementCurrentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
if (originalText)
|
||||||
|
replaceLastWord(originalText.split(/\s/).splice(-1)[0])
|
||||||
|
|
||||||
|
open = false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
visible: opacity > 0
|
||||||
|
opacity: open && count ? 1 : 0
|
||||||
|
implicitHeight: open && count ? Math.min(window.height, contentHeight) : 0
|
||||||
|
model: ModelStore.get(chat.userId, chat.roomId, "autocompleted_members")
|
||||||
|
|
||||||
|
delegate: HTile {
|
||||||
|
width: listView.width
|
||||||
|
contentItem: HLabel { text: model.display_name + " (" + model.id + ")"}
|
||||||
|
onClicked: {
|
||||||
|
currentIndex = model.index
|
||||||
|
listView.open = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCountChanged: if (! count && open) open = false
|
||||||
|
onAutoOpenChanged: open = autoOpen
|
||||||
|
onOpenChanged: if (! open) {
|
||||||
|
originalText = ""
|
||||||
|
currentIndex = -1
|
||||||
|
autoOpenCompleted = false
|
||||||
|
py.callCoro("set_string_filter", [model.modelId, ""])
|
||||||
|
}
|
||||||
|
|
||||||
|
onWordToCompleteChanged: {
|
||||||
|
if (! open) return
|
||||||
|
py.callCoro("set_string_filter", [model.modelId, wordToComplete])
|
||||||
|
}
|
||||||
|
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
if (currentIndex === -1) return
|
||||||
|
if (! originalText) originalText = textArea.text
|
||||||
|
if (autoOpen) autoOpenCompleted = true
|
||||||
|
|
||||||
|
replaceLastWord(model.get(currentIndex).display_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity { HNumberAnimation {} }
|
||||||
|
Behavior on implicitHeight { HNumberAnimation {} }
|
||||||
|
}
|
|
@ -94,7 +94,7 @@ HColumnLayout {
|
||||||
|
|
||||||
onTextChanged: {
|
onTextChanged: {
|
||||||
stackView.pop(stackView.initialItem)
|
stackView.pop(stackView.initialItem)
|
||||||
py.callCoro("set_substring_filter", [modelSyncId, text])
|
py.callCoro("set_string_filter", [modelSyncId, text])
|
||||||
}
|
}
|
||||||
|
|
||||||
onActiveFocusChanged: {
|
onActiveFocusChanged: {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user