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();