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

@@ -134,7 +134,7 @@ HBox {
onTextEdited: {
py.callCoro(
"set_substring_filter", ["filtered_homeservers", text],
"set_string_filter", ["filtered_homeservers", text],
)
serverList.currentIndex = -1
}

View File

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

View File

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

View 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 {} }
}

View File

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