matrixion/resources/qml/MessageInput.qml

461 lines
19 KiB
QML
Raw Normal View History

// SPDX-FileCopyrightText: Nheko Contributors
2021-03-14 04:45:20 +03:00
//
2021-03-05 02:35:15 +03:00
// SPDX-License-Identifier: GPL-3.0-or-later
2021-07-15 21:37:52 +03:00
import "./emoji"
2020-12-30 23:03:07 +03:00
import "./voip"
2023-02-25 19:03:30 +03:00
import "./ui"
2021-02-21 04:11:50 +03:00
import QtQuick 2.12
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import QtQuick.Window 2.13
2020-11-01 01:24:07 +03:00
import im.nheko 1.0
Rectangle {
2021-02-21 04:11:50 +03:00
id: inputBar
2023-06-02 02:45:24 +03:00
property bool showAllButtons: width > 450 || (messageInput.length == 0 && !messageInput.inputMethodComposing)
readonly property string text: messageInput.text
Layout.fillWidth: true
Layout.minimumHeight: 40
2023-06-02 02:45:24 +03:00
Layout.preferredHeight: row.implicitHeight
color: palette.window
2020-12-18 20:49:24 +03:00
Component {
id: placeCallDialog
PlaceCall {
}
}
Component {
id: screenShareDialog
ScreenShare {
}
}
RowLayout {
2021-02-21 04:11:50 +03:00
id: row
anchors.fill: parent
spacing: 0
2023-06-02 02:45:24 +03:00
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
ImageButton {
Layout.alignment: Qt.AlignBottom
2023-06-02 02:45:24 +03:00
Layout.margins: 8
ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : (CallManager.isOnCallOnOtherDevice ? qsTr("Already on a call") : qsTr("Place a call"))
ToolTip.visible: hovered
2023-10-26 17:43:09 +03:00
Layout.preferredHeight: 22
2023-06-02 02:45:24 +03:00
hoverEnabled: true
2021-11-14 04:23:10 +03:00
image: CallManager.isOnCall ? ":/icons/icons/ui/end-call.svg" : ":/icons/icons/ui/place-call.svg"
2023-06-02 02:45:24 +03:00
opacity: (CallManager.haveCallInvite || CallManager.isOnCallOnOtherDevice) ? 0.3 : 1
visible: CallManager.callsSupported && showAllButtons
2023-10-26 17:43:09 +03:00
Layout.preferredWidth: 22
2023-06-02 02:45:24 +03:00
2020-12-18 20:49:24 +03:00
onClicked: {
if (room) {
2020-12-18 20:49:24 +03:00
if (CallManager.haveCallInvite) {
2023-06-02 02:45:24 +03:00
return;
2021-01-12 01:51:39 +03:00
} else if (CallManager.isOnCall) {
2020-12-18 20:49:24 +03:00
CallManager.hangUp();
2023-06-02 02:45:24 +03:00
} else if (CallManager.isOnCallOnOtherDevice) {
return;
2023-06-02 02:45:24 +03:00
} else {
2020-12-18 20:49:24 +03:00
var dialog = placeCallDialog.createObject(timelineRoot);
2020-12-30 23:03:07 +03:00
dialog.open();
timelineRoot.destroyOnClose(dialog);
2020-12-18 20:49:24 +03:00
}
}
}
}
ImageButton {
Layout.alignment: Qt.AlignBottom
2023-06-02 02:45:24 +03:00
Layout.margins: 8
ToolTip.text: qsTr("Send a file")
ToolTip.visible: hovered
2023-10-26 17:43:09 +03:00
Layout.preferredHeight: 22
2023-06-02 02:45:24 +03:00
hoverEnabled: true
2021-11-14 04:23:10 +03:00
image: ":/icons/icons/ui/attach.svg"
2023-06-02 02:45:24 +03:00
visible: showAllButtons
2023-10-26 17:43:09 +03:00
Layout.preferredWidth: 22
2023-06-02 02:45:24 +03:00
onClicked: room.input.openFileSelection()
2020-11-15 06:52:49 +03:00
Rectangle {
anchors.fill: parent
color: palette.window
visible: room && room.input.uploading
2020-11-15 06:52:49 +03:00
2023-02-25 19:03:30 +03:00
Spinner {
anchors.centerIn: parent
height: parent.height / 2
2020-11-15 06:52:49 +03:00
running: parent.visible
}
}
}
2021-02-21 04:11:50 +03:00
ScrollView {
id: textInput
Layout.alignment: Qt.AlignVCenter
2023-06-02 02:45:24 +03:00
Layout.fillWidth: true
Layout.maximumHeight: Window.height / 4
Layout.minimumHeight: fontMetrics.lineSpacing
Layout.preferredHeight: contentHeight
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
contentWidth: availableWidth
TextArea {
id: messageInput
2020-11-01 01:24:07 +03:00
2021-08-21 07:01:10 +03:00
property int completerTriggeredAt: 0
2023-06-02 02:45:24 +03:00
property string lastChar
2020-11-20 03:22:36 +03:00
2020-11-24 19:32:45 +03:00
function insertCompletion(completion) {
messageInput.remove(completerTriggeredAt, cursorPosition);
messageInput.insert(cursorPosition, completion);
2020-11-24 19:32:45 +03:00
}
2020-11-25 21:03:22 +03:00
function openCompleter(pos, type) {
2023-06-02 02:45:24 +03:00
if (popup.opened)
return;
2020-11-25 21:03:22 +03:00
completerTriggeredAt = pos;
2022-02-21 06:06:49 +03:00
completer.completerName = type;
2020-11-25 21:03:22 +03:00
popup.open();
2023-06-02 02:45:24 +03:00
completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition) + messageInput.preeditText);
2020-11-25 21:03:22 +03:00
}
function positionCursorAtEnd() {
cursorPosition = messageInput.length;
}
function positionCursorAtStart() {
cursorPosition = 0;
}
2023-06-02 02:45:24 +03:00
background: null
bottomPadding: 8
color: palette.text
focus: true
leftPadding: inputBar.showAllButtons ? 0 : 8
padding: 0
placeholderText: qsTr("Write a message...")
placeholderTextColor: palette.buttonText
2023-06-02 02:45:24 +03:00
selectByMouse: true
topPadding: 8
verticalAlignment: TextEdit.AlignVCenter
2023-06-02 02:45:24 +03:00
width: textInput.width
wrapMode: TextEdit.Wrap
2023-06-02 02:45:24 +03:00
Keys.onPressed: event => {
2020-11-01 01:24:07 +03:00
if (event.matches(StandardKey.Paste)) {
2022-06-13 14:16:49 +03:00
event.accepted = room.input.tryPasteAttachment(false);
} else if (event.key == Qt.Key_Space) {
// close popup if user enters space after colon
if (cursorPosition == completerTriggeredAt + 1)
popup.close();
2022-03-01 03:59:06 +03:00
if (popup.opened && completer.count <= 0)
popup.close();
2020-11-20 03:22:36 +03:00
} else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_U) {
messageInput.clear();
2020-11-20 03:22:36 +03:00
} else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_P) {
messageInput.text = room.input.previousText();
2020-11-20 03:22:36 +03:00
} else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_N) {
messageInput.text = room.input.nextText();
2020-11-20 03:22:36 +03:00
} else if (event.key == Qt.Key_Escape && popup.opened) {
2022-02-21 06:06:49 +03:00
completer.completerName = "";
2021-08-21 06:29:27 +03:00
popup.close();
2020-11-20 03:22:36 +03:00
event.accepted = true;
2021-08-21 06:29:27 +03:00
} else if (event.matches(StandardKey.SelectAll) && popup.opened) {
2022-02-21 06:06:49 +03:00
completer.completerName = "";
2020-11-20 03:22:36 +03:00
popup.close();
2022-03-01 03:59:06 +03:00
} else if (event.matches(StandardKey.InsertLineSeparator)) {
2023-06-02 02:45:24 +03:00
if (popup.opened)
popup.close();
if (Settings.invertEnterKey && (!Qt.inputMethod.visible || Qt.platform.os === "windows")) {
2022-11-05 01:42:35 +03:00
room.input.send();
event.accepted = true;
}
2020-11-25 04:20:42 +03:00
} else if (event.matches(StandardKey.InsertParagraphSeparator)) {
if (popup.opened) {
2022-02-21 06:06:49 +03:00
var currentCompletion = completer.currentCompletion();
completer.completerName = "";
2020-11-25 04:20:42 +03:00
popup.close();
if (currentCompletion) {
messageInput.insertCompletion(currentCompletion);
2020-11-25 04:20:42 +03:00
event.accepted = true;
2022-03-01 03:59:06 +03:00
return;
2020-11-25 04:20:42 +03:00
}
2020-11-20 03:22:36 +03:00
}
if (!Settings.invertEnterKey && (!Qt.inputMethod.visible || Qt.platform.os === "windows")) {
room.input.send();
event.accepted = true;
}
2022-01-09 02:28:03 +03:00
} else if (event.key == Qt.Key_Tab && (event.modifiers == Qt.NoModifier || event.modifiers == Qt.ShiftModifier)) {
2020-11-20 03:22:36 +03:00
event.accepted = true;
2020-11-25 21:03:22 +03:00
if (popup.opened) {
if (event.modifiers & Qt.ShiftModifier)
2022-02-21 06:06:49 +03:00
completer.down();
else
2022-02-21 06:06:49 +03:00
completer.up();
2020-11-25 21:03:22 +03:00
} else {
var pos = cursorPosition - 1;
while (pos > -1) {
var t = messageInput.getText(pos, pos + 1);
2020-11-25 21:03:22 +03:00
console.log('"' + t + '"');
if (t == '@') {
messageInput.openCompleter(pos, "user");
2023-06-02 02:45:24 +03:00
return;
} else if (t == ' ' || t == '\t') {
messageInput.openCompleter(pos + 1, "user");
2023-06-02 02:45:24 +03:00
return;
2020-11-25 21:03:22 +03:00
} else if (t == ':') {
messageInput.openCompleter(pos, "emoji");
2023-06-02 02:45:24 +03:00
return;
} else if (t == '~') {
messageInput.openCompleter(pos, "customEmoji");
2023-06-02 02:45:24 +03:00
return;
2020-11-25 21:03:22 +03:00
}
pos = pos - 1;
}
// At start of input
messageInput.openCompleter(0, "user");
2020-11-25 21:03:22 +03:00
}
2020-11-20 03:22:36 +03:00
} else if (event.key == Qt.Key_Up && popup.opened) {
event.accepted = true;
2022-02-21 06:06:49 +03:00
completer.up();
} else if ((event.key == Qt.Key_Down || event.key == Qt.Key_Backtab) && popup.opened) {
2020-11-20 03:22:36 +03:00
event.accepted = true;
2022-02-21 06:06:49 +03:00
completer.down();
} else if (event.key == Qt.Key_Up && event.modifiers == Qt.NoModifier) {
2021-02-25 01:51:05 +03:00
if (cursorPosition == 0) {
event.accepted = true;
var idx = room.edit ? room.idToIndex(room.edit) + 1 : 0;
2021-02-25 01:51:05 +03:00
while (true) {
var id = room.indexToId(idx);
if (!id || room.getDump(id, "").isEditable) {
room.edit = id;
2021-02-25 01:51:05 +03:00
cursorPosition = 0;
Qt.callLater(positionCursorAtEnd);
2021-02-25 01:51:05 +03:00
break;
}
idx++;
}
} else if (positionAt(0, cursorRectangle.y + cursorRectangle.height / 2) === 0) {
2021-02-25 01:51:05 +03:00
event.accepted = true;
positionCursorAtStart();
2021-02-25 01:51:05 +03:00
}
} else if (event.key == Qt.Key_Down && event.modifiers == Qt.NoModifier) {
if (cursorPosition == messageInput.length && room.edit) {
2021-02-25 01:51:05 +03:00
event.accepted = true;
var idx = room.idToIndex(room.edit) - 1;
2021-02-25 01:51:05 +03:00
while (true) {
var id = room.indexToId(idx);
if (!id || room.getDump(id, "").isEditable) {
room.edit = id;
Qt.callLater(positionCursorAtStart);
2021-02-25 01:51:05 +03:00
break;
}
idx--;
}
} else if (positionAt(width, cursorRectangle.y + cursorRectangle.height / 2) === messageInput.length) {
event.accepted = true;
positionCursorAtEnd();
2021-02-25 01:51:05 +03:00
}
2020-11-20 03:22:36 +03:00
}
}
2023-06-02 02:45:24 +03:00
// Ensure that we get escape key press events first.
Keys.onShortcutOverride: event => event.accepted = (popup.opened && (event.key === Qt.Key_Escape || event.key === Qt.Key_Tab || event.key === Qt.Key_Enter || event.key === Qt.Key_Space))
onCursorPositionChanged: {
if (!room)
return;
room.input.updateState(selectionStart, selectionEnd, cursorPosition, text);
if (popup.opened && cursorPosition <= completerTriggeredAt)
popup.close();
if (popup.opened)
completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition) + messageInput.preeditText);
}
onPreeditTextChanged: {
if (popup.opened)
completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition) + messageInput.preeditText);
}
onSelectionEndChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
onSelectionStartChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
onTextChanged: {
if (room)
room.input.updateState(selectionStart, selectionEnd, cursorPosition, text);
forceActiveFocus();
if (cursorPosition > 0)
lastChar = text.charAt(cursorPosition - 1);
else
lastChar = '';
if (lastChar == '@') {
messageInput.openCompleter(selectionStart - 1, "user");
} else if (lastChar == ':') {
messageInput.openCompleter(selectionStart - 1, "emoji");
} else if (lastChar == '#') {
messageInput.openCompleter(selectionStart - 1, "roomAliases");
} else if (lastChar == "/" && cursorPosition == 1) {
messageInput.openCompleter(selectionStart - 1, "command");
}
}
2020-11-20 03:22:36 +03:00
2020-11-20 04:38:08 +03:00
Connections {
function onRoomChanged() {
messageInput.clear();
if (room)
messageInput.append(room.input.text);
2022-02-21 06:06:49 +03:00
completer.completerName = "";
messageInput.forceActiveFocus();
2020-11-20 04:38:08 +03:00
}
target: timelineView
2020-11-20 04:38:08 +03:00
}
2020-11-24 19:32:45 +03:00
Connections {
function onCompletionClicked(completion) {
messageInput.insertCompletion(completion);
}
2022-02-21 06:06:49 +03:00
target: completer
2020-11-24 19:32:45 +03:00
}
2022-02-21 06:06:49 +03:00
Popup {
2020-11-20 03:22:36 +03:00
id: popup
2022-02-21 06:06:49 +03:00
background: null
2022-03-01 03:59:06 +03:00
padding: 0
2023-06-02 02:45:24 +03:00
x: messageInput.positionToRectangle(messageInput.completerTriggeredAt).x
y: messageInput.positionToRectangle(messageInput.completerTriggeredAt).y - height
2022-02-21 06:06:49 +03:00
enter: Transition {
NumberAnimation {
2023-06-02 02:45:24 +03:00
duration: 100
2022-02-21 06:06:49 +03:00
from: 0
2023-06-02 02:45:24 +03:00
property: "opacity"
2022-02-21 06:06:49 +03:00
to: 1
}
}
exit: Transition {
NumberAnimation {
2023-06-02 02:45:24 +03:00
duration: 100
2022-02-21 06:06:49 +03:00
from: 1
2023-06-02 02:45:24 +03:00
property: "opacity"
2022-02-21 06:06:49 +03:00
to: 0
}
}
2020-11-15 06:52:49 +03:00
2023-06-02 02:45:24 +03:00
Completer {
id: completer
anchors.fill: parent
rowMargin: 2
rowSpacing: 0
}
}
2020-11-15 06:52:49 +03:00
Connections {
function onTextChanged(newText) {
messageInput.text = newText;
messageInput.cursorPosition = newText.length;
}
ignoreUnknownSignals: true
target: room ? room.input : null
2020-11-01 01:24:07 +03:00
}
Connections {
2023-06-02 02:45:24 +03:00
function onEditChanged() {
messageInput.forceActiveFocus();
}
2023-06-02 02:45:24 +03:00
function onReplyChanged() {
messageInput.forceActiveFocus();
}
2022-09-30 04:27:05 +03:00
function onThreadChanged() {
messageInput.forceActiveFocus();
}
ignoreUnknownSignals: true
target: room
}
Connections {
function onFocusInput() {
messageInput.forceActiveFocus();
}
target: TimelineManager
}
MouseArea {
2023-06-02 02:45:24 +03:00
acceptedButtons: Qt.MiddleButton
// workaround for wrong cursor shape on some platforms
anchors.fill: parent
cursorShape: Qt.IBeamCursor
2023-06-02 02:45:24 +03:00
onPressed: mouse => mouse.accepted = room.input.tryPasteAttachment(true)
}
2021-01-17 06:05:02 +03:00
}
}
2021-07-15 21:37:52 +03:00
ImageButton {
id: stickerButton
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
Layout.margins: 8
2023-06-02 02:45:24 +03:00
ToolTip.text: qsTr("Stickers")
ToolTip.visible: hovered
2023-10-26 17:43:09 +03:00
Layout.preferredHeight: 22
2023-06-02 02:45:24 +03:00
hoverEnabled: true
2021-07-15 21:37:52 +03:00
image: ":/icons/icons/ui/sticky-note-solid.svg"
2023-06-02 02:45:24 +03:00
visible: showAllButtons
2023-10-26 17:43:09 +03:00
Layout.preferredWidth: 22
2023-06-02 02:45:24 +03:00
onClicked: stickerPopup.visible ? stickerPopup.close() : stickerPopup.show(stickerButton, room.roomId, function (row) {
room.input.sticker(row);
TimelineManager.focusMessageInput();
})
2021-07-15 21:37:52 +03:00
StickerPicker {
id: stickerPopup
2023-05-25 20:07:13 +03:00
emoji: false
2021-07-15 21:37:52 +03:00
}
}
ImageButton {
2020-11-16 20:41:29 +03:00
id: emojiButton
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
2021-02-21 04:11:50 +03:00
Layout.margins: 8
2023-06-02 02:45:24 +03:00
ToolTip.text: qsTr("Emoji")
ToolTip.visible: hovered
2023-10-26 17:43:09 +03:00
Layout.preferredHeight: 22
2023-06-02 02:45:24 +03:00
hoverEnabled: true
2021-11-14 04:23:10 +03:00
image: ":/icons/icons/ui/smile.svg"
2023-10-26 17:43:09 +03:00
Layout.preferredWidth: 22
2023-06-02 02:45:24 +03:00
onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, room.roomId, function (plaintext, markdown) {
messageInput.insert(messageInput.cursorPosition, markdown);
TimelineManager.focusMessageInput();
})
2023-05-25 20:07:13 +03:00
StickerPicker {
2023-05-25 20:20:25 +03:00
id: emojiPopup
2023-05-25 20:07:13 +03:00
emoji: true
}
}
ImageButton {
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
2021-02-21 04:11:50 +03:00
Layout.margins: 8
2022-03-20 00:30:35 +03:00
Layout.rightMargin: 8
2020-11-16 01:14:47 +03:00
ToolTip.text: qsTr("Send")
2023-06-02 02:45:24 +03:00
ToolTip.visible: hovered
2023-10-26 17:43:09 +03:00
Layout.preferredHeight: 22
2023-06-02 02:45:24 +03:00
hoverEnabled: true
image: ":/icons/icons/ui/send.svg"
2023-10-26 17:43:09 +03:00
Layout.preferredWidth: 22
2023-06-02 02:45:24 +03:00
2020-11-15 06:52:49 +03:00
onClicked: {
room.input.send();
2020-11-15 06:52:49 +03:00
}
}
}
Label {
anchors.centerIn: parent
color: palette.placeholderText
text: qsTr("You don't have permission to send messages in this room")
2023-06-02 02:45:24 +03:00
visible: room ? (!room.permissions.canSend(MtxEvent.TextMessage)) : false
}
}