Add basic user autocompletion UI
This commit is contained in:
@@ -134,7 +134,7 @@ HBox {
|
||||
|
||||
onTextEdited: {
|
||||
py.callCoro(
|
||||
"set_substring_filter", ["filtered_homeservers", text],
|
||||
"set_string_filter", ["filtered_homeservers", text],
|
||||
)
|
||||
serverList.currentIndex = -1
|
||||
}
|
||||
|
@@ -11,39 +11,60 @@ Rectangle {
|
||||
function takeFocus() { messageArea.forceActiveFocus() }
|
||||
|
||||
|
||||
implicitHeight:
|
||||
Math.max(theme.baseElementsHeight, messageArea.implicitHeight)
|
||||
|
||||
implicitHeight: Math.max(theme.baseElementsHeight, column.implicitHeight)
|
||||
color: theme.chat.composer.background
|
||||
|
||||
HRowLayout {
|
||||
HColumnLayout {
|
||||
id: column
|
||||
anchors.fill: parent
|
||||
|
||||
HUserAvatar {
|
||||
id: avatar
|
||||
radius: 0
|
||||
userId: messageArea.writingUserId
|
||||
UserAutoCompletion {
|
||||
id: userCompletion
|
||||
textArea: messageArea
|
||||
|
||||
mxc:
|
||||
messageArea.writingUserInfo ?
|
||||
messageArea.writingUserInfo.avatar_url :
|
||||
""
|
||||
|
||||
displayName:
|
||||
messageArea.writingUserInfo ?
|
||||
messageArea.writingUserInfo.display_name :
|
||||
""
|
||||
}
|
||||
|
||||
HScrollView {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
MessageArea { id: messageArea }
|
||||
Layout.maximumHeight: chat.height / 3
|
||||
}
|
||||
|
||||
UploadButton {
|
||||
Layout.fillHeight: true
|
||||
HRowLayout {
|
||||
HUserAvatar {
|
||||
id: avatar
|
||||
radius: 0
|
||||
userId: messageArea.writingUserId
|
||||
|
||||
mxc:
|
||||
messageArea.writingUserInfo ?
|
||||
messageArea.writingUserInfo.avatar_url :
|
||||
""
|
||||
|
||||
displayName:
|
||||
messageArea.writingUserInfo ?
|
||||
messageArea.writingUserInfo.display_name :
|
||||
""
|
||||
}
|
||||
|
||||
HScrollView {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
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 {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,9 +6,10 @@ import "../../.."
|
||||
import "../../../Base"
|
||||
|
||||
HTextArea {
|
||||
id: textArea
|
||||
id: area
|
||||
|
||||
property HListView eventList
|
||||
property bool autoCompletionOpen
|
||||
|
||||
property string indent: " "
|
||||
|
||||
@@ -20,12 +21,12 @@ HTextArea {
|
||||
ModelStore.get("accounts").find(writingUserId)
|
||||
|
||||
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:
|
||||
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 lineTextUntilCursor:
|
||||
@@ -50,8 +51,13 @@ HTextArea {
|
||||
return obj
|
||||
}
|
||||
|
||||
signal autoCompletePrevious()
|
||||
signal autoCompleteNext()
|
||||
signal extraCharacterCloseAutoCompletion()
|
||||
signal cancelAutoCompletion()
|
||||
|
||||
function setTyping(typing) {
|
||||
if (! textArea.enabled) return
|
||||
if (! area.enabled) return
|
||||
|
||||
py.callClientCoro(
|
||||
writingUserId, "room_typing", [chat.roomId, typing, 5000],
|
||||
@@ -76,7 +82,7 @@ HTextArea {
|
||||
}
|
||||
|
||||
const add = indent.repeat(indents)
|
||||
textArea.insertAtCursor("\n" + add)
|
||||
area.insertAtCursor("\n" + add)
|
||||
}
|
||||
|
||||
function sendText() {
|
||||
@@ -85,7 +91,7 @@ HTextArea {
|
||||
const args = [chat.roomId, toSend, chat.replyToEventId]
|
||||
py.callClientCoro(writingUserId, "send_text", args)
|
||||
|
||||
textArea.clear()
|
||||
area.clear()
|
||||
clearReplyTo()
|
||||
}
|
||||
|
||||
@@ -174,9 +180,11 @@ HTextArea {
|
||||
},
|
||||
)
|
||||
|
||||
Keys.onEscapePressed: clearReplyTo()
|
||||
Keys.onEscapePressed:
|
||||
autoCompletionOpen ? cancelAutoCompletion() : clearReplyTo()
|
||||
|
||||
Keys.onReturnPressed: ev => {
|
||||
extraCharacterCloseAutoCompletion()
|
||||
ev.accepted = true
|
||||
|
||||
ev.modifiers & Qt.ShiftModifier ||
|
||||
@@ -189,18 +197,43 @@ HTextArea {
|
||||
Keys.onEnterPressed: ev => Keys.returnPressed(ev)
|
||||
|
||||
Keys.onMenuPressed: ev => {
|
||||
extraCharacterCloseAutoCompletion()
|
||||
|
||||
if (eventList && eventList.currentItem)
|
||||
eventList.currentItem.openContextMenu()
|
||||
}
|
||||
|
||||
Keys.onBacktabPressed: ev => {
|
||||
ev.accepted = true
|
||||
autoCompletePrevious()
|
||||
}
|
||||
|
||||
Keys.onTabPressed: ev => {
|
||||
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 => {
|
||||
if (ev.text) extraCharacterCloseAutoCompletion()
|
||||
|
||||
if (ev.matches(StandardKey.Copy) &&
|
||||
! textArea.selectedText &&
|
||||
! area.selectedText &&
|
||||
eventList &&
|
||||
(eventList.selectedCount || eventList.currentIndex !== -1))
|
||||
{
|
||||
@@ -212,10 +245,10 @@ HTextArea {
|
||||
// FIXME: buggy
|
||||
// if (ev.modifiers === Qt.NoModifier &&
|
||||
// ev.key === Qt.Key_Backspace &&
|
||||
// ! textArea.selectedText)
|
||||
// ! area.selectedText)
|
||||
// {
|
||||
// ev.accepted = true
|
||||
// textArea.remove(
|
||||
// area.remove(
|
||||
// cursorPosition - deleteCharsOnBackspace,
|
||||
// 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: {
|
||||
stackView.pop(stackView.initialItem)
|
||||
py.callCoro("set_substring_filter", [modelSyncId, text])
|
||||
py.callCoro("set_string_filter", [modelSyncId, text])
|
||||
}
|
||||
|
||||
onActiveFocusChanged: {
|
||||
|
Reference in New Issue
Block a user