Skip to content
Snippets Groups Projects
MessageInput.qml 19.1 KiB
Newer Older
// SPDX-FileCopyrightText: Nheko Contributors
Nicolas Werner's avatar
Nicolas Werner committed
// SPDX-License-Identifier: GPL-3.0-or-later

Nicolas Werner's avatar
Nicolas Werner committed
import "./emoji"
trilene's avatar
trilene committed
import "./voip"
import "./ui"
import QtQuick 2.12
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import QtQuick.Window 2.13
Nicolas Werner's avatar
Nicolas Werner committed
import im.nheko 1.0

    id: inputBar

Nicolas Werner's avatar
Nicolas Werner committed
    property bool showAllButtons: width > 450 || (messageInput.length == 0 && !messageInput.inputMethodComposing)
    readonly property string text: messageInput.text

    Layout.fillWidth: true
    Layout.minimumHeight: 40
Nicolas Werner's avatar
Nicolas Werner committed
    Layout.preferredHeight: row.implicitHeight
    color: palette.window
trilene's avatar
trilene committed
    Component {
        id: placeCallDialog

        PlaceCall {
        }
    }
    Component {
        id: screenShareDialog

        ScreenShare {
        }
    }
Nicolas Werner's avatar
Nicolas Werner committed
        visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false

        ImageButton {
            Layout.alignment: Qt.AlignBottom
Nicolas Werner's avatar
Nicolas Werner committed
            Layout.margins: 8
            ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : (CallManager.isOnCallOnOtherDevice ? qsTr("Already on a call") : qsTr("Place a call"))
            ToolTip.visible: hovered
Nicolas Werner's avatar
Nicolas Werner committed
            Layout.preferredHeight: 22
Nicolas Werner's avatar
Nicolas Werner committed
            hoverEnabled: true
            image: CallManager.isOnCall ? ":/icons/icons/ui/end-call.svg" : ":/icons/icons/ui/place-call.svg"
Nicolas Werner's avatar
Nicolas Werner committed
            opacity: (CallManager.haveCallInvite || CallManager.isOnCallOnOtherDevice) ? 0.3 : 1
            visible: CallManager.callsSupported && showAllButtons
Nicolas Werner's avatar
Nicolas Werner committed
            Layout.preferredWidth: 22
Nicolas Werner's avatar
Nicolas Werner committed

trilene's avatar
trilene committed
            onClicked: {
trilene's avatar
trilene committed
                    if (CallManager.haveCallInvite) {
Nicolas Werner's avatar
Nicolas Werner committed
                        return;
                    } else if (CallManager.isOnCall) {
trilene's avatar
trilene committed
                        CallManager.hangUp();
Nicolas Werner's avatar
Nicolas Werner committed
                    } else if (CallManager.isOnCallOnOtherDevice) {
Nicolas Werner's avatar
Nicolas Werner committed
                    } else {
trilene's avatar
trilene committed
                        var dialog = placeCallDialog.createObject(timelineRoot);
trilene's avatar
trilene committed
                        dialog.open();
Nicolas Werner's avatar
Nicolas Werner committed
                        timelineRoot.destroyOnClose(dialog);
        }
        ImageButton {
            Layout.alignment: Qt.AlignBottom
Nicolas Werner's avatar
Nicolas Werner committed
            Layout.margins: 8
            ToolTip.text: qsTr("Send a file")
            ToolTip.visible: hovered
Nicolas Werner's avatar
Nicolas Werner committed
            Layout.preferredHeight: 22
Nicolas Werner's avatar
Nicolas Werner committed
            hoverEnabled: true
            image: ":/icons/icons/ui/attach.svg"
Nicolas Werner's avatar
Nicolas Werner committed
            visible: showAllButtons
Nicolas Werner's avatar
Nicolas Werner committed
            Layout.preferredWidth: 22
Nicolas Werner's avatar
Nicolas Werner committed

            onClicked: room.input.openFileSelection()
Nicolas Werner's avatar
Nicolas Werner committed

            Rectangle {
                anchors.fill: parent
                color: palette.window
                visible: room && room.input.uploading
                Spinner {
                    anchors.centerIn: parent
                    height: parent.height / 2
Nicolas Werner's avatar
Nicolas Werner committed
                    running: parent.visible
                }
            }
        ScrollView {
            Layout.alignment: Qt.AlignVCenter
Nicolas Werner's avatar
Nicolas Werner committed
            Layout.fillWidth: true
            Layout.maximumHeight: Window.height / 4
            Layout.minimumHeight: fontMetrics.lineSpacing
            Layout.preferredHeight: contentHeight
            ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
            contentWidth: availableWidth
                property int completerTriggeredAt: 0
Nicolas Werner's avatar
Nicolas Werner committed
                property string lastChar
                function insertCompletion(completion) {
                    messageInput.remove(completerTriggeredAt, cursorPosition);
                    messageInput.insert(cursorPosition, completion);
Nicolas Werner's avatar
Nicolas Werner committed
                function openCompleter(pos, type) {
Nicolas Werner's avatar
Nicolas Werner committed
                    if (popup.opened)
                        return;
Nicolas Werner's avatar
Nicolas Werner committed
                    completerTriggeredAt = pos;
Nicolas Werner's avatar
Nicolas Werner committed
                    completer.completerName = type;
Nicolas Werner's avatar
Nicolas Werner committed
                    popup.open();
Nicolas Werner's avatar
Nicolas Werner committed
                    completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition) + messageInput.preeditText);
                function positionCursorAtEnd() {
                    cursorPosition = messageInput.length;
                }
                function positionCursorAtStart() {
                    cursorPosition = 0;
                }

Nicolas Werner's avatar
Nicolas Werner committed
                background: null
                bottomPadding: 8
                color: palette.text
                focus: true
                leftPadding: inputBar.showAllButtons ? 0 : 8
                padding: 0
                placeholderText: qsTr("Write a message...")
                placeholderTextColor: palette.buttonText
Nicolas Werner's avatar
Nicolas Werner committed
                selectByMouse: true
                topPadding: 8
                verticalAlignment: TextEdit.AlignVCenter
Nicolas Werner's avatar
Nicolas Werner committed
                width: textInput.width
                wrapMode: TextEdit.Wrap
Nicolas Werner's avatar
Nicolas Werner committed
                Keys.onPressed: event => {
Nicolas Werner's avatar
Nicolas Werner committed
                    if (event.matches(StandardKey.Paste)) {
                        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)
                        if (popup.opened && completer.count <= 0)
Nicolas Werner's avatar
Nicolas Werner committed
                    } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_U) {
Nicolas Werner's avatar
Nicolas Werner committed
                    } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_P) {
                        messageInput.text = room.input.previousText();
Nicolas Werner's avatar
Nicolas Werner committed
                    } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_N) {
                        messageInput.text = room.input.nextText();
Nicolas Werner's avatar
Nicolas Werner committed
                    } else if (event.key == Qt.Key_Escape && popup.opened) {
Nicolas Werner's avatar
Nicolas Werner committed
                        completer.completerName = "";
                        popup.close();
Nicolas Werner's avatar
Nicolas Werner committed
                        event.accepted = true;
                    } else if (event.matches(StandardKey.SelectAll) && popup.opened) {
Nicolas Werner's avatar
Nicolas Werner committed
                        completer.completerName = "";
Nicolas Werner's avatar
Nicolas Werner committed
                        popup.close();
                    } else if (event.matches(StandardKey.InsertLineSeparator)) {
Nicolas Werner's avatar
Nicolas Werner committed
                        if (popup.opened)
                            popup.close();
                        if (Settings.invertEnterKey && (!Qt.inputMethod.visible || Qt.platform.os === "windows")) {
LordMZTE's avatar
LordMZTE committed
                            room.input.send();
                            event.accepted = true;
                        }
                    } else if (event.matches(StandardKey.InsertParagraphSeparator)) {
                        if (popup.opened) {
Nicolas Werner's avatar
Nicolas Werner committed
                            var currentCompletion = completer.currentCompletion();
                            completer.completerName = "";
                            popup.close();
                            if (currentCompletion) {
                                messageInput.insertCompletion(currentCompletion);
                                event.accepted = true;
Nicolas Werner's avatar
Nicolas Werner committed
                        }
                        if (!Settings.invertEnterKey && (!Qt.inputMethod.visible || Qt.platform.os === "windows")) {
                            room.input.send();
                            event.accepted = true;
                        }
                    } else if (event.key == Qt.Key_Tab && (event.modifiers == Qt.NoModifier || event.modifiers == Qt.ShiftModifier)) {
Nicolas Werner's avatar
Nicolas Werner committed
                        event.accepted = true;
Nicolas Werner's avatar
Nicolas Werner committed
                        if (popup.opened) {
                            if (event.modifiers & Qt.ShiftModifier)
Nicolas Werner's avatar
Nicolas Werner committed
                                completer.down();
Nicolas Werner's avatar
Nicolas Werner committed
                                completer.up();
Nicolas Werner's avatar
Nicolas Werner committed
                        } else {
                            var pos = cursorPosition - 1;
                            while (pos > -1) {
                                var t = messageInput.getText(pos, pos + 1);
Nicolas Werner's avatar
Nicolas Werner committed
                                console.log('"' + t + '"');
                                    messageInput.openCompleter(pos, "user");
Nicolas Werner's avatar
Nicolas Werner committed
                                    return;
                                } else if (t == ' ' || t == '\t') {
                                    messageInput.openCompleter(pos + 1, "user");
Nicolas Werner's avatar
Nicolas Werner committed
                                    return;
Nicolas Werner's avatar
Nicolas Werner committed
                                } else if (t == ':') {
                                    messageInput.openCompleter(pos, "emoji");
Nicolas Werner's avatar
Nicolas Werner committed
                                    return;
                                } else if (t == '~') {
                                    messageInput.openCompleter(pos, "customEmoji");
Nicolas Werner's avatar
Nicolas Werner committed
                                    return;
Nicolas Werner's avatar
Nicolas Werner committed
                                }
                                pos = pos - 1;
                            }
                            // At start of input
                            messageInput.openCompleter(0, "user");
Nicolas Werner's avatar
Nicolas Werner committed
                    } else if (event.key == Qt.Key_Up && popup.opened) {
                        event.accepted = true;
Nicolas Werner's avatar
Nicolas Werner committed
                        completer.up();
                    } else if ((event.key == Qt.Key_Down || event.key == Qt.Key_Backtab) && popup.opened) {
Nicolas Werner's avatar
Nicolas Werner committed
                        event.accepted = true;
Nicolas Werner's avatar
Nicolas Werner committed
                        completer.down();
                    } else if (event.key == Qt.Key_Up && (event.modifiers == Qt.NoModifier || event.modifiers == Qt.KeypadModifier)) {
                        if (cursorPosition == 0) {
                            event.accepted = true;
                            var idx = room.edit ? room.idToIndex(room.edit) + 1 : 0;
                            while (true) {
                                var id = room.indexToId(idx);
                                if (!id || room.getDump(id, "").isEditable) {
                                    room.edit = id;
                                    cursorPosition = 0;
                                    Qt.callLater(positionCursorAtEnd);
                        } else if (positionAt(0, cursorRectangle.y + cursorRectangle.height / 2) === 0) {
                            event.accepted = true;
                            positionCursorAtStart();
                    } else if (event.key == Qt.Key_Down && (event.modifiers == Qt.NoModifier || event.modifiers == Qt.KeypadModifier)) {
                        if (cursorPosition == messageInput.length && room.edit) {
                            event.accepted = true;
                            var idx = room.idToIndex(room.edit) - 1;
                            while (true) {
                                var id = room.indexToId(idx);
                                if (!id || room.getDump(id, "").isEditable) {
                                    room.edit = id;
                                    Qt.callLater(positionCursorAtStart);
                        } else if (positionAt(width, cursorRectangle.y + cursorRectangle.height / 2) === messageInput.length) {
                            event.accepted = true;
                            positionCursorAtEnd();
Nicolas Werner's avatar
Nicolas Werner committed
                // 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");
                    }
                }
Nicolas Werner's avatar
Nicolas Werner committed
                Connections {
                    function onRoomChanged() {
                        if (room)
                            messageInput.append(room.input.text);
Nicolas Werner's avatar
Nicolas Werner committed
                        completer.completerName = "";
                        messageInput.forceActiveFocus();
                    target: timelineView
                Connections {
                    function onCompletionClicked(completion) {
                        messageInput.insertCompletion(completion);
                    }

Nicolas Werner's avatar
Nicolas Werner committed
                    target: completer
Nicolas Werner's avatar
Nicolas Werner committed
                Popup {
Nicolas Werner's avatar
Nicolas Werner committed
                    id: popup

Nicolas Werner's avatar
Nicolas Werner committed
                    background: null
Nicolas Werner's avatar
Nicolas Werner committed
                    x: messageInput.positionToRectangle(messageInput.completerTriggeredAt).x
                    y: messageInput.positionToRectangle(messageInput.completerTriggeredAt).y - height
Nicolas Werner's avatar
Nicolas Werner committed

                    enter: Transition {
                        NumberAnimation {
Nicolas Werner's avatar
Nicolas Werner committed
                            duration: 100
Nicolas Werner's avatar
Nicolas Werner committed
                            from: 0
Nicolas Werner's avatar
Nicolas Werner committed
                            property: "opacity"
Nicolas Werner's avatar
Nicolas Werner committed
                            to: 1
                        }
                    }
                    exit: Transition {
                        NumberAnimation {
Nicolas Werner's avatar
Nicolas Werner committed
                            duration: 100
Nicolas Werner's avatar
Nicolas Werner committed
                            from: 1
Nicolas Werner's avatar
Nicolas Werner committed
                            property: "opacity"
                    contentItem: Completer {
Nicolas Werner's avatar
Nicolas Werner committed
                        id: completer

                        rowMargin: 2
                        rowSpacing: 0
                    }
                }
Nicolas Werner's avatar
Nicolas Werner committed
                Connections {
                    function onTextChanged(newText) {
                        messageInput.text = newText;
                        messageInput.cursorPosition = newText.length;
                    }

                    ignoreUnknownSignals: true
                    target: room ? room.input : null
Nicolas Werner's avatar
Nicolas Werner committed
                }
Nicolas Werner's avatar
Nicolas Werner committed
                    function onEditChanged() {
                        messageInput.forceActiveFocus();
                    }
Nicolas Werner's avatar
Nicolas Werner committed
                    function onReplyChanged() {
                        messageInput.forceActiveFocus();
                    }
Nicolas Werner's avatar
Nicolas Werner committed
                    function onThreadChanged() {
                        messageInput.forceActiveFocus();
                    }

                    ignoreUnknownSignals: true
                    function onFocusInput() {
                        messageInput.forceActiveFocus();
                    }

                    target: TimelineManager
                }
Nicolas Werner's avatar
Nicolas Werner committed
                    acceptedButtons: Qt.MiddleButton
                    // workaround for wrong cursor shape on some platforms
                    anchors.fill: parent
                    cursorShape: Qt.IBeamCursor
Nicolas Werner's avatar
Nicolas Werner committed
                    onPressed: mouse => mouse.accepted = room.input.tryPasteAttachment(true)
                }
Nicolas Werner's avatar
Nicolas Werner committed
        ImageButton {
            id: stickerButton

            Layout.alignment: Qt.AlignRight | Qt.AlignBottom
            Layout.margins: 8
Nicolas Werner's avatar
Nicolas Werner committed
            ToolTip.text: qsTr("Stickers")
            ToolTip.visible: hovered
Nicolas Werner's avatar
Nicolas Werner committed
            Layout.preferredHeight: 22
Nicolas Werner's avatar
Nicolas Werner committed
            hoverEnabled: true
Nicolas Werner's avatar
Nicolas Werner committed
            image: ":/icons/icons/ui/sticky-note-solid.svg"
Nicolas Werner's avatar
Nicolas Werner committed
            visible: showAllButtons
Nicolas Werner's avatar
Nicolas Werner committed
            Layout.preferredWidth: 22
Nicolas Werner's avatar
Nicolas Werner committed

            onClicked: stickerPopup.visible ? stickerPopup.close() : stickerPopup.show(stickerButton, room.roomId, function (row) {
                    room.input.sticker(row);
                    TimelineManager.focusMessageInput();
                })
Nicolas Werner's avatar
Nicolas Werner committed

            StickerPicker {
                id: stickerPopup

            id: emojiButton

            Layout.alignment: Qt.AlignRight | Qt.AlignBottom
            Layout.margins: 8
Nicolas Werner's avatar
Nicolas Werner committed
            ToolTip.text: qsTr("Emoji")
            ToolTip.visible: hovered
Nicolas Werner's avatar
Nicolas Werner committed
            Layout.preferredHeight: 22
Nicolas Werner's avatar
Nicolas Werner committed
            hoverEnabled: true
            image: ":/icons/icons/ui/smile.svg"
Nicolas Werner's avatar
Nicolas Werner committed
            Layout.preferredWidth: 22
Nicolas Werner's avatar
Nicolas Werner committed

            onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, room.roomId, function (plaintext, markdown) {
                    messageInput.insert(messageInput.cursorPosition, markdown);
                    TimelineManager.focusMessageInput();
                })
                id: emojiPopup
        }
        ImageButton {
            Layout.alignment: Qt.AlignRight | Qt.AlignBottom
            Layout.margins: 8
Malte E's avatar
Malte E committed
            Layout.rightMargin: 8
            ToolTip.text: qsTr("Send")
Nicolas Werner's avatar
Nicolas Werner committed
            ToolTip.visible: hovered
Nicolas Werner's avatar
Nicolas Werner committed
            Layout.preferredHeight: 22
Nicolas Werner's avatar
Nicolas Werner committed
            hoverEnabled: true
            image: ":/icons/icons/ui/send.svg"
Nicolas Werner's avatar
Nicolas Werner committed
            Layout.preferredWidth: 22
Nicolas Werner's avatar
Nicolas Werner committed

Nicolas Werner's avatar
Nicolas Werner committed
            onClicked: {
Nicolas Werner's avatar
Nicolas Werner committed
            }
        color: palette.placeholderText
        text: qsTr("You don't have permission to send messages in this room")
Nicolas Werner's avatar
Nicolas Werner committed
        visible: room ? (!room.permissions.canSend(MtxEvent.TextMessage)) : false