From cabeb1464c85d911eea427bd48e8188facde8e56 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 20 Nov 2020 01:22:36 +0100 Subject: [PATCH] WIP Qml completer --- resources/qml/Completer.qml | 107 +++++++++++++++++++++++++++++++++ resources/qml/MessageInput.qml | 58 +++++++++++++++--- resources/res.qrc | 15 ++--- src/timeline/InputBar.cpp | 6 ++ src/timeline/InputBar.h | 2 + 5 files changed, 174 insertions(+), 14 deletions(-) create mode 100644 resources/qml/Completer.qml diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml new file mode 100644 index 00000000..d53eae62 --- /dev/null +++ b/resources/qml/Completer.qml @@ -0,0 +1,107 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.2 +import im.nheko 1.0 + +Popup { + id: popup + + property int currentIndex: -1 + property string completerName + property var completer + + function up() { + currentIndex = currentIndex - 1; + if (currentIndex == -2) + currentIndex = repeater.count - 1; + + } + + function down() { + currentIndex = currentIndex + 1; + if (currentIndex >= repeater.count) + currentIndex = -1; + + } + + function currentCompletion() { + if (currentIndex > -1 && currentIndex < repeater.count) + return completer.completionAt(currentIndex); + else + return null; + } + + onCompleterNameChanged: { + if (completerName) + completer = TimelineManager.timeline.input.completerFor(completerName); + + } + padding: 0 + onAboutToShow: currentIndex = -1 + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Repeater { + id: repeater + + model: completer + + delegate: Rectangle { + color: model.index == popup.currentIndex ? colors.window : colors.base + height: del.implicitHeight + 4 + width: del.implicitWidth + 4 + + RowLayout { + id: del + + anchors.centerIn: parent + + Avatar { + height: 24 + width: 24 + displayName: model.displayName + url: model.avatarUrl.replace("mxc://", "image://MxcImage/") + } + + Label { + text: model.displayName + color: colors.text + } + + } + + } + + } + + } + + enter: Transition { + NumberAnimation { + property: "opacity" + from: 0 + to: 1 + duration: 100 + } + + } + + exit: Transition { + NumberAnimation { + property: "opacity" + from: 1 + to: 0 + duration: 100 + } + + } + + background: Rectangle { + color: colors.base + implicitHeight: popup.contentHeight + implicitWidth: popup.contentWidth + } + +} diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index a5c84a17..a4a47a3e 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -68,28 +68,72 @@ Rectangle { TextArea { id: textArea + property int completerTriggeredAt: -1 + placeholderText: qsTr("Write a message...") placeholderTextColor: colors.buttonText color: colors.text wrapMode: TextEdit.Wrap onTextChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) - onCursorPositionChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) + onCursorPositionChanged: { + TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text); + if (cursorPosition < completerTriggeredAt) { + completerTriggeredAt = -1; + popup.close(); + } + } onSelectionStartChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) onSelectionEndChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) + // Ensure that we get escape key press events first. + Keys.onShortcutOverride: event.accepted = (completerTriggeredAt != -1 && (event.key === Qt.Key_Escape || event.key === Qt.Key_Tab || event.key === Qt.Key_Enter)) Keys.onPressed: { if (event.matches(StandardKey.Paste)) { TimelineManager.timeline.input.paste(false); event.accepted = true; + } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_U) { + textArea.clear(); + } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_P) { + textArea.text = TimelineManager.timeline.input.previousText(); + } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_N) { + textArea.text = TimelineManager.timeline.input.nextText(); + } else if (event.key == Qt.Key_At) { + completerTriggeredAt = cursorPosition + 1; + popup.completerName = "user"; + popup.open(); + } else if (event.key == Qt.Key_Escape && popup.opened) { + completerTriggeredAt = -1; + event.accepted = true; + popup.close(); + } else if (event.matches(StandardKey.InsertParagraphSeparator) && popup.opened) { + var currentCompletion = popup.currentCompletion(); + popup.close(); + if (currentCompletion) { + textArea.remove(completerTriggeredAt - 1, cursorPosition); + textArea.insert(cursorPosition, currentCompletion); + event.accepted = true; + return ; + } + } else if (event.key == Qt.Key_Tab && popup.opened) { + event.accepted = true; + popup.down(); + } else if (event.key == Qt.Key_Up && popup.opened) { + event.accepted = true; + popup.up(); + } else if (event.key == Qt.Key_Down && popup.opened) { + event.accepted = true; + popup.down(); } else if (event.matches(StandardKey.InsertParagraphSeparator)) { TimelineManager.timeline.input.send(); textArea.clear(); event.accepted = true; - } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_U) - textArea.clear(); - else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_P) - textArea.text = TimelineManager.timeline.input.previousText(); - else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_N) - textArea.text = TimelineManager.timeline.input.nextText(); + } + } + + Completer { + id: popup + + x: textArea.positionToRectangle(textArea.completerTriggeredAt).x + y: textArea.positionToRectangle(textArea.completerTriggeredAt).y - height } Connections { diff --git a/resources/res.qrc b/resources/res.qrc index 02f31498..a01907ec 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -123,21 +123,22 @@ qtquickcontrols2.conf qml/TimelineView.qml - qml/TopBar.qml - qml/MessageView.qml - qml/MessageInput.qml - qml/TypingIndicator.qml - qml/ReplyPopup.qml qml/ActiveCallBar.qml qml/Avatar.qml + qml/Completer.qml + qml/EncryptionIndicator.qml qml/ImageButton.qml qml/MatrixText.qml + qml/MessageInput.qml + qml/MessageView.qml qml/NhekoBusyIndicator.qml - qml/StatusIndicator.qml - qml/EncryptionIndicator.qml qml/Reactions.qml + qml/ReplyPopup.qml qml/ScrollHelper.qml + qml/StatusIndicator.qml qml/TimelineRow.qml + qml/TopBar.qml + qml/TypingIndicator.qml qml/VideoCall.qml qml/emoji/EmojiButton.qml qml/emoji/EmojiPicker.qml diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index 1eaaaa64..82649faa 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -163,6 +163,12 @@ InputBar::nextText() return text(); } +QObject * +InputBar::completerFor(QString completerName) +{ + return nullptr; +} + void InputBar::send() { diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index 5e66e86f..939e8dad 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -41,6 +41,8 @@ public slots: bool uploading() const { return uploading_; } void callButton(); + QObject *completerFor(QString completerName); + private slots: void startTyping(); void stopTyping();