diff --git a/src/qml/Component/ChatBox/AttachmentPane.qml b/src/qml/Component/ChatBox/AttachmentPane.qml index b98f36e6c..ae31cff60 100644 --- a/src/qml/Component/ChatBox/AttachmentPane.qml +++ b/src/qml/Component/ChatBox/AttachmentPane.qml @@ -10,188 +10,115 @@ import org.kde.kirigami 2.15 as Kirigami import org.kde.neochat 1.0 -Loader { - id: attachmentPaneLoader +ColumnLayout { + id: root - readonly property var attachmentMimetype: FileType.mimeTypeForUrl(attachmentPaneLoader.attachmentPath) + signal attachmentCancelled() + + property string attachmentPath + + readonly property var attachmentMimetype: FileType.mimeTypeForUrl(attachmentPath) readonly property bool hasImage: attachmentMimetype.valid && FileType.supportedImageFormats.includes(attachmentMimetype.preferredSuffix) - readonly property string attachmentPath: currentRoom.chatBoxAttachmentPath readonly property string baseFileName: attachmentPath.substring(attachmentPath.lastIndexOf('/') + 1, attachmentPath.length) - active: visible - sourceComponent: Component { - QQC2.Pane { - id: attachmentPane - Kirigami.Theme.colorSet: Kirigami.Theme.View + RowLayout { + spacing: Kirigami.Units.smallSpacing - contentItem: Item { - property real spacing: attachmentPane.spacing > 0 ? attachmentPane.spacing : toolBar.spacing - implicitWidth: Math.max(image.implicitWidth, imageBusyIndicator.implicitWidth, fileInfoLayout.implicitWidth, toolBar.implicitWidth) - implicitHeight: Math.max( - (hasImage ? Math.max(image.preferredHeight, imageBusyIndicator.implicitHeight) + spacing : 0) - + fileInfoLayout.implicitHeight, - toolBar.implicitHeight - ) + QQC2.Label { + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + text: i18n("Attachment:") + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + } + QQC2.ToolButton { + id: editImageButton + visible: hasImage + icon.name: "document-edit" + text: i18n("Edit") + display: QQC2.AbstractButton.IconOnly - Image { - id: image - property real preferredHeight: Math.min(implicitHeight, Kirigami.Units.gridUnit * 8) - height: preferredHeight - anchors { - horizontalCenter: parent.horizontalCenter - bottom: fileInfoLayout.top - bottomMargin: parent.spacing - } - width: Math.min(implicitWidth, attachmentPane.availableWidth) - asynchronous: true - cache: false // Cache is not needed. Images will rarely be shown repeatedly. - smooth: height === preferredHeight && parent.height === parent.implicitHeight // Don't smooth until height animation stops - source: hasImage ? attachmentPaneLoader.attachmentPath : "" - visible: hasImage - fillMode: Image.PreserveAspectFit - - onSourceChanged: { - // Reset source size height, which affect implicitHeight - sourceSize.height = -1 - } - - onSourceSizeChanged: { - if (implicitHeight > Kirigami.Units.gridUnit * 8) { - // This can save a lot of RAM when loading large images. - // It also improves visual quality for large images. - sourceSize.height = Kirigami.Units.gridUnit * 8 - } - } - - Behavior on height { - NumberAnimation { - property: "height" - duration: Kirigami.Units.shortDuration - easing.type: Easing.OutCubic - } - } - } - - QQC2.BusyIndicator { - id: imageBusyIndicator - anchors { - horizontalCenter: parent.horizontalCenter - top: parent.top - bottom: fileInfoLayout.top - bottomMargin: parent.spacing - } - visible: running - running: image.visible && image.progress < 1 - } - - RowLayout { - id: fileInfoLayout - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: undefined - anchors.bottom: parent.bottom - spacing: parent.spacing - - Kirigami.Icon { - id: mimetypeIcon - implicitHeight: Kirigami.Units.fontMetrics.roundedIconSize(fileLabel.implicitHeight) - implicitWidth: implicitHeight - source: attachmentMimetype.iconName - } - - QQC2.Label { - id: fileLabel - text: baseFileName - } - - states: State { - when: !hasImage - AnchorChanges { - target: fileInfoLayout - anchors.bottom: undefined - anchors.verticalCenter: parent.verticalCenter - } - } - } - - // Using a toolbar to get a button spacing consistent with what the QQC2 style normally has - // Also has some accessibility info - QQC2.ToolBar { - id: toolBar - width: parent.width - anchors.top: parent.top - - leftPadding: 0 - rightPadding: 0 - topPadding: 0 - bottomPadding: 0 - - Kirigami.Theme.inherit: true - Kirigami.Theme.colorSet: Kirigami.Theme.View - - contentItem: RowLayout { - spacing: parent.spacing - QQC2.Label { - Layout.leftMargin: -attachmentPane.leftPadding - Layout.topMargin: -attachmentPane.topPadding - leftPadding: cancelAttachmentButton.leftPadding + 1 + attachmentPane.leftPadding - rightPadding: cancelAttachmentButton.rightPadding + 1 - topPadding: cancelAttachmentButton.topPadding + attachmentPane.topPadding - bottomPadding: cancelAttachmentButton.bottomPadding - text: i18n("Attachment:") - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - background: Kirigami.ShadowedRectangle { - property real cornerRadius: cancelAttachmentButton.background.hasOwnProperty("radius") ? - Math.min(cancelAttachmentButton.background.radius, height/2) : 0 - corners.bottomLeftRadius: toolBar.mirrored ? cornerRadius : 0 - corners.bottomRightRadius: toolBar.mirrored ? 0 : cornerRadius - color: Kirigami.Theme.backgroundColor - opacity: 0.75 - } - } - Item { - Layout.fillWidth: true - } - QQC2.ToolButton { - id: editImageButton - visible: hasImage - icon.name: "document-edit" - text: i18n("Edit") - display: QQC2.AbstractButton.IconOnly - - Component { - id: imageEditorPage - ImageEditorPage { - imagePath: attachmentPaneLoader.attachmentPath - } - } - onClicked: { - let imageEditor = applicationWindow().pageStack.layers.push(imageEditorPage); - imageEditor.newPathChanged.connect(function(newPath) { - applicationWindow().pageStack.layers.pop(); - attachmentPaneLoader.attachmentPath = newPath; - }); - } - QQC2.ToolTip.text: text - QQC2.ToolTip.visible: hovered - } - QQC2.ToolButton { - id: cancelAttachmentButton - icon.name: "dialog-close" - text: i18n("Cancel sending Image") - display: QQC2.AbstractButton.IconOnly - onClicked: currentRoom.chatBoxAttachmentPath = ""; - QQC2.ToolTip.text: text - QQC2.ToolTip.visible: hovered - } - } - background: null + Component { + id: imageEditorPage + ImageEditorPage { + imagePath: root.attachmentPath } } - background: Rectangle { - color: Kirigami.Theme.backgroundColor + onClicked: { + let imageEditor = applicationWindow().pageStack.layers.push(imageEditorPage); + imageEditor.newPathChanged.connect(function(newPath) { + applicationWindow().pageStack.layers.pop(); + root.attachmentPath = newPath; + }); + } + QQC2.ToolTip.text: text + QQC2.ToolTip.visible: hovered + } + QQC2.ToolButton { + id: cancelAttachmentButton + display: QQC2.AbstractButton.IconOnly + action: Kirigami.Action { + text: i18n("Cancel sending attachment") + icon.name: "dialog-close" + onTriggered: attachmentCancelled(); + shortcut: "Escape" + } + QQC2.ToolTip.text: text + QQC2.ToolTip.visible: hovered + } + } + + Image { + id: image + Layout.alignment: Qt.AlignHCenter + + asynchronous: true + cache: false // Cache is not needed. Images will rarely be shown repeatedly. + source: hasImage ? root.attachmentPath : "" + visible: hasImage + fillMode: Image.PreserveAspectFit + + onSourceChanged: { + // Reset source size height, which affect implicitHeight + sourceSize.height = -1 + } + + onSourceSizeChanged: { + if (implicitHeight > Kirigami.Units.gridUnit * 8) { + // This can save a lot of RAM when loading large images. + // It also improves visual quality for large images. + sourceSize.height = Kirigami.Units.gridUnit * 8 + } + } + + Behavior on height { + NumberAnimation { + duration: Kirigami.Units.shortDuration + easing.type: Easing.OutCubic } } } + QQC2.BusyIndicator { + id: imageBusyIndicator + + visible: running + running: image.visible && image.progress < 1 + } + RowLayout { + id: fileInfoLayout + Layout.alignment: Qt.AlignHCenter + spacing: parent.spacing + + Kirigami.Icon { + id: mimetypeIcon + implicitWidth: Kirigami.Units.iconSizes.smallMedium + implicitHeight: Kirigami.Units.iconSizes.smallMedium + source: attachmentMimetype.iconName + } + QQC2.Label { + id: fileLabel + text: baseFileName + } + } } diff --git a/src/qml/Component/ChatBox/ChatBar.qml b/src/qml/Component/ChatBox/ChatBar.qml index e650be292..52b9c7e78 100644 --- a/src/qml/Component/ChatBox/ChatBar.qml +++ b/src/qml/Component/ChatBox/ChatBar.qml @@ -5,205 +5,62 @@ import QtQuick 2.15 import QtQuick.Layouts 1.15 import QtQuick.Controls 2.15 as QQC2 -import QtQuick.Window 2.15 import org.kde.kirigami 2.18 as Kirigami import org.kde.neochat 1.0 -QQC2.ToolBar { - id: chatBar - property alias inputFieldText: inputField.text - property alias textField: inputField - property alias cursorPosition: inputField.cursorPosition +QQC2.Control { + id: root + + property alias textField: textField + property bool isReplying: currentRoom.chatBoxReplyId.length > 0 + property bool isEditing: currentRoom.chatBoxEditId.length > 0 + property bool replyPaneVisible: isReplying || isEditing + property NeoChatUser replyUser: currentRoom.chatBoxReplyUser ?? currentRoom.chatBoxEditUser + property bool attachmentPaneVisible: currentRoom.chatBoxAttachmentPath.length > 0 - signal inputFieldForceActiveFocusTriggered() signal messageSent() - onInputFieldForceActiveFocusTriggered: { - inputField.forceActiveFocus(); - // set the cursor to the end of the text - inputField.cursorPosition = inputField.length; - } + property list actions : [ + Kirigami.Action { + id: attachmentAction - position: QQC2.ToolBar.Footer + property bool isBusy: currentRoom && currentRoom.hasFileUploading - Kirigami.Theme.colorSet: Kirigami.Theme.View + // Matrix does not allow sending attachments in replies + visible: currentRoom.chatBoxReplyId.length === 0 && currentRoom.chatBoxAttachmentPath.length === 0 + icon.name: "mail-attachment" + text: i18n("Attach an image or file") + displayHint: Kirigami.DisplayHint.IconOnly - // Using a custom background because some styles like Material - // or Fusion might have ugly colors for a TextArea placed inside - // of a toolbar. ToolBar is otherwise the closest QQC2 component - // to what we want because of the padding and spacing values. - background: Rectangle { - color: Kirigami.Theme.backgroundColor - } - - contentItem: RowLayout { - spacing: chatBar.spacing - - QQC2.ScrollView { - Layout.fillHeight: true - Layout.fillWidth: true - Layout.minimumHeight: inputField.implicitHeight - // lineSpacing is height+leading, so subtract leading once since leading only exists between lines. - Layout.maximumHeight: fontMetrics.lineSpacing * 8 - fontMetrics.leading - + inputField.topPadding + inputField.bottomPadding - - // HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890) - QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff - - FontMetrics { - id: fontMetrics - font: inputField.font - } - - QQC2.TextArea { - id: inputField - focus: true - /* Some QQC2 styles will have their own predefined backgrounds for TextAreas. - * Make sure there is no background since we are using the ToolBar background. - * - * This could cause a problem if the QQC2 style was designed around TextArea - * background colors being very different from the QPalette::Base color. - * Luckily, none of the Qt QQC2 styles do that and neither do KDE's QQC2 styles. - */ - background: MouseArea { - acceptedButtons: Qt.NoButton - cursorShape: Qt.IBeamCursor - z: 1 - } - - leftPadding: mirrored ? 0 : Kirigami.Units.largeSpacing - rightPadding: !mirrored ? 0 : Kirigami.Units.largeSpacing - topPadding: 0 - bottomPadding: 0 - - placeholderText: readOnly ? i18n("This room is encrypted. Build libQuotient with encryption enabled to send encrypted messages.") : currentRoom.chatBoxEditId.length > 0 ? i18n("Edit Message") : currentRoom.usesEncryption ? i18n("Send an encrypted message…") : currentRoom.chatBoxAttachmentPath.length > 0 ? i18n("Set an attachment caption...") : i18n("Send a message…") - verticalAlignment: TextEdit.AlignVCenter - horizontalAlignment: TextEdit.AlignLeft - wrapMode: Text.Wrap - readOnly: currentRoom.usesEncryption && !Controller.encryptionSupported - - Kirigami.Theme.colorSet: Kirigami.Theme.View - Kirigami.Theme.inherit: false - - color: Kirigami.Theme.textColor - selectionColor: Kirigami.Theme.highlightColor - selectedTextColor: Kirigami.Theme.highlightedTextColor - hoverEnabled: !Kirigami.Settings.tabletMode - - selectByMouse: !Kirigami.Settings.tabletMode - - Keys.onEnterPressed: { - if (completionMenu.visible) { - completionMenu.complete() - } else if (event.modifiers & Qt.ShiftModifier) { - inputField.insert(cursorPosition, "\n") - } else { - chatBar.postMessage(); - } - } - Keys.onReturnPressed: { - if (completionMenu.visible) { - completionMenu.complete() - } else if (event.modifiers & Qt.ShiftModifier) { - inputField.insert(cursorPosition, "\n") - } else { - chatBar.postMessage(); - } - } - - Keys.onTabPressed: { - if (completionMenu.visible) { - completionMenu.complete() - } - } - - Keys.onPressed: { - if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) { - chatBar.pasteImage(); - } else if (event.key === Qt.Key_Up && event.modifiers & Qt.ControlModifier) { - let replyEvent = messageEventModel.getLatestMessageFromIndex(0) - if (replyEvent && replyEvent["event_id"]) { - currentRoom.chatBoxReplyId = replyEvent["event_id"] + onTriggered: { + if (Clipboard.hasImage) { + attachDialog.open() + } else { + var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay) + fileDialog.chosen.connect((path) => { + if (!path) { + return; } - } else if (event.key === Qt.Key_Up && inputField.text.length === 0) { - let editEvent = messageEventModel.getLastLocalUserMessageEventId() - if (editEvent) { - currentRoom.chatBoxEditId = editEvent["event_id"] - } - } else if (event.key === Qt.Key_Up && completionMenu.visible) { - completionMenu.decrementIndex() - } else if (event.key === Qt.Key_Down && completionMenu.visible) { - completionMenu.incrementIndex() - } else if (event.key === Qt.Key_Backspace && inputField.text.length <= 1) { - currentRoom.sendTypingNotification(false) - repeatTimer.stop() - } - } - - Timer { - id: repeatTimer - interval: 5000 - } - - onTextChanged: { - if (!repeatTimer.running && Config.typingNotifications) { - var textExists = text.length > 0 - currentRoom.sendTypingNotification(textExists) - textExists ? repeatTimer.start() : repeatTimer.stop() - } - currentRoom.chatBoxText = text + currentRoom.chatBoxAttachmentPath = path; + }) + fileDialog.open() } } - } - Item { - visible: currentRoom.chatBoxReplyId.length === 0 && (currentRoom.chatBoxAttachmentPath.length === 0 || uploadingBusySpinner.running) - implicitWidth: uploadButton.implicitWidth - implicitHeight: uploadButton.implicitHeight - QQC2.ToolButton { - id: uploadButton - anchors.fill: parent - // Matrix does not allow sending attachments in replies - visible: currentRoom.chatBoxReplyId.length === 0 && currentRoom.chatBoxAttachmentPath.length === 0 && !uploadingBusySpinner.running - icon.name: "mail-attachment" - text: i18n("Attach an image or file") - display: QQC2.AbstractButton.IconOnly + tooltip: text + }, + Kirigami.Action { + id: emojiAction - onClicked: { - if (Clipboard.hasImage) { - attachDialog.open() - } else { - var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay) - fileDialog.chosen.connect((path) => { - if (!path) { - return; - } - currentRoom.chatBoxAttachmentPath = path; - }) - fileDialog.open() - } - } + property bool isBusy: false - QQC2.ToolTip.text: text - QQC2.ToolTip.visible: hovered - } - QQC2.BusyIndicator { - id: uploadingBusySpinner - anchors.fill: parent - visible: running - running: currentRoom && currentRoom.hasFileUploading - } - } - - QQC2.ToolButton { - id: emojiButton icon.name: "smiley" text: i18n("Add an Emoji") - display: QQC2.AbstractButton.IconOnly + displayHint: Kirigami.DisplayHint.IconOnly checkable: true - onClicked: { + onTriggered: { if (emojiDialog.visible) { emojiDialog.close() } else { @@ -211,29 +68,266 @@ QQC2.ToolBar { } } - QQC2.ToolTip.text: text - QQC2.ToolTip.visible: hovered - } + tooltip: text + }, + Kirigami.Action { + id: sendAction + + property bool isBusy: false - QQC2.ToolButton { - id: sendButton icon.name: "document-send" text: i18n("Send message") - display: QQC2.AbstractButton.IconOnly + displayHint: Kirigami.DisplayHint.IconOnly + checkable: true - onClicked: { - chatBar.postMessage() + onTriggered: { + root.postMessage() } - QQC2.ToolTip.text: text - QQC2.ToolTip.visible: hovered + tooltip: text + } + ] + + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + + contentItem: QQC2.ScrollView { + id: chatBarScrollView + + property var textFieldHeight: textField.height + + property var visualLeftPadding: (root.width - chatBoxMaxWidth) / 2 - (root.width > chatBoxMaxWidth ? Kirigami.Units.largeSpacing : 0) + property var visualRightPadding: (root.width - chatBoxMaxWidth) / 2 + (root.width > chatBoxMaxWidth ? Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing : 0) + leftPadding: LayoutMirroring.enabled ? visualRightPadding : visualLeftPadding + rightPadding: LayoutMirroring.enabled ? visualLeftPadding : visualRightPadding + + // HACK: This is to stop the ScrollBar flickering on and off as the height is increased + QQC2.ScrollBar.vertical.policy: chatBarHeightAnimation.running && implicitHeight <= height ? QQC2.ScrollBar.AlwaysOff : QQC2.ScrollBar.AsNeeded + + Behavior on implicitHeight { + NumberAnimation { + id: chatBarHeightAnimation + duration: Kirigami.Units.shortDuration + easing.type: Easing.InOutCubic + } + } + + QQC2.TextArea{ + id: textField + + topPadding: Kirigami.Units.largeSpacing + (paneLoader.visible ? paneLoader.height : 0) + bottomPadding: Kirigami.Units.largeSpacing + leftPadding: LayoutMirroring.enabled ? actionsRow.width : (root.width > chatBoxMaxWidth ? 0 : Kirigami.Units.largeSpacing) + rightPadding: LayoutMirroring.enabled ? (root.width > chatBoxMaxWidth ? 0 : Kirigami.Units.largeSpacing) : actionsRow.width + + placeholderText: readOnly ? i18n("This room is encrypted. Build libQuotient with encryption enabled to send encrypted messages.") : currentRoom.chatBoxEditId.length > 0 ? i18n("Edit Message") : currentRoom.usesEncryption ? i18n("Send an encrypted message…") : currentRoom.chatBoxAttachmentPath.length > 0 ? i18n("Set an attachment caption...") : i18n("Send a message…") + verticalAlignment: TextEdit.AlignVCenter + wrapMode: Text.Wrap + readOnly: (currentRoom.usesEncryption && !Controller.encryptionSupported) + + Timer { + id: repeatTimer + interval: 5000 + } + + onTextChanged: { + if (!repeatTimer.running && Config.typingNotifications) { + var textExists = text.length > 0 + currentRoom.sendTypingNotification(textExists) + textExists ? repeatTimer.start() : repeatTimer.stop() + } + currentRoom.chatBoxText = text + } + onCursorRectangleChanged: chatBarScrollView.ensureVisible(cursorRectangle) + + Keys.onEnterPressed: { + if (completionMenu.visible) { + completionMenu.complete() + } else if (event.modifiers & Qt.ShiftModifier) { + textField.insert(cursorPosition, "\n") + } else { + chatBar.postMessage(); + } + } + Keys.onReturnPressed: { + if (completionMenu.visible) { + completionMenu.complete() + } else if (event.modifiers & Qt.ShiftModifier) { + textField.insert(cursorPosition, "\n") + } else { + chatBar.postMessage(); + } + } + Keys.onTabPressed: { + if (completionMenu.visible) { + completionMenu.complete() + } + } + Keys.onPressed: { + if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) { + chatBar.pasteImage(); + } else if (event.key === Qt.Key_Up && event.modifiers & Qt.ControlModifier) { + let replyEvent = messageEventModel.getLatestMessageFromIndex(0) + if (replyEvent && replyEvent["event_id"]) { + currentRoom.chatBoxReplyId = replyEvent["event_id"] + } + } else if (event.key === Qt.Key_Up && textField.text.length === 0) { + let editEvent = messageEventModel.getLastLocalUserMessageEventId() + if (editEvent) { + currentRoom.chatBoxEditId = editEvent["event_id"] + } + } else if (event.key === Qt.Key_Up && completionMenu.visible) { + completionMenu.decrementIndex() + } else if (event.key === Qt.Key_Down && completionMenu.visible) { + completionMenu.incrementIndex() + } else if (event.key === Qt.Key_Backspace && textField.text.length <= 1) { + currentRoom.sendTypingNotification(false) + repeatTimer.stop() + } + } + + Loader { + id: paneLoader + + anchors.top: parent.top + anchors.left: parent.left + anchors.leftMargin: root.width > chatBoxMaxWidth ? 0 : Kirigami.Units.largeSpacing + anchors.right: parent.right + anchors.rightMargin: root.width > chatBoxMaxWidth ? 0 : (chatBarScrollView.QQC2.ScrollBar.vertical.visible ? Kirigami.Units.largeSpacing * 3.5 : Kirigami.Units.largeSpacing) + + active: visible + visible: root.replyPaneVisible || root.attachmentPaneVisible + sourceComponent: root.replyPaneVisible ? replyPane : attachmentPane + } + Component { + id: replyPane + ReplyPane { + userName: root.replyUser ? root.replyUser.displayName : "" + userColor: root.replyUser ? root.replyUser.color : "" + userAvatar: root.replyUser ? "image://mxc/" + currentRoom.getUser(root.replyUser.id).avatarMediaId : "" + isReply: root.isReplying + text: isEditing ? currentRoom.chatBoxEditMessage : currentRoom.chatBoxReplyMessage + } + } + Component { + id: attachmentPane + AttachmentPane { + attachmentPath: currentRoom.chatBoxAttachmentPath + + onAttachmentCancelled: { + currentRoom.chatBoxAttachmentPath = ""; + root.forceActiveFocus() + } + } + } + + background: MouseArea { + acceptedButtons: Qt.NoButton + cursorShape: Qt.IBeamCursor + z: 1 + } + } + + /** + * Because of the paneLoader we have to manage the scroll + * position manually or it doesn't keep the cursor visible properly all the time. + */ + function ensureVisible(r) { + // Find the child that is the Flickable created by ScrollView. + let flickable = undefined; + for (var index in children) { + if (children[index] instanceof Flickable) { + flickable = children[index]; + } + } + + if (flickable) { + if (flickable.contentX >= r.x) { + flickable.contentX = r.x; + } else if (flickable.contentX + width <= r.x + r.width) { + flickable.contentX = r.x + r.width - width; + } if (flickable.contentY >= r.y) { + flickable.contentY = r.y; + } else if (flickable.contentY + height <= r.y + r.height) { + flickable.contentY = r.y + r.height - height + textField.bottomPadding; + } + } + } + } + + QQC2.ToolButton { + id: cancelButton + anchors.top: parent.top + anchors.right: parent.right + anchors.rightMargin: (root.width - chatBoxMaxWidth) / 2 + Kirigami.Units.largeSpacing + (chatBarScrollView.QQC2.ScrollBar.vertical.visible && !(root.width > chatBoxMaxWidth) ? Kirigami.Units.largeSpacing * 2.5 : 0) + + visible: root.replyPaneVisible + display: QQC2.AbstractButton.IconOnly + action: Kirigami.Action { + text: root.isReplying ? i18nc("@action:button", "Cancel reply") : i18nc("@action:button", "Cancel edit") + icon.name: "dialog-close" + onTriggered: { + currentRoom.chatBoxReplyId = ""; + currentRoom.chatBoxEditId = ""; + currentRoom.chatBoxAttachmentPath = ""; + root.forceActiveFocus() + } + shortcut: "Escape" + } + QQC2.ToolTip.text: text + QQC2.ToolTip.visible: hovered + } + Row { + id: actionsRow + padding: Kirigami.Units.smallSpacing + spacing: Kirigami.Units.smallSpacing + anchors.right: parent.right + property var requiredMargin: (root.width - chatBoxMaxWidth) / 2 + Kirigami.Units.largeSpacing + (chatBarScrollView.QQC2.ScrollBar.vertical.visible && !(root.width > chatBoxMaxWidth) ? Kirigami.Units.largeSpacing * 2.5 : 0) + anchors.leftMargin: layoutDirection === Qt.RightToLeft ? requiredMargin : 0 + anchors.rightMargin: layoutDirection === Qt.RightToLeft ? 0 : requiredMargin + anchors.bottom: parent.bottom + anchors.bottomMargin: Kirigami.Units.largeSpacing - 2 + + Repeater { + model: root.actions + Kirigami.Icon { + implicitWidth: Kirigami.Units.iconSizes.smallMedium + implicitHeight: Kirigami.Units.iconSizes.smallMedium + + source: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source) + active: actionArea.containsPress + visible: modelData.visible + enabled: modelData.enabled + MouseArea { + id: actionArea + anchors.fill: parent + onClicked: modelData.trigger() + cursorShape: Qt.PointingHandCursor + } + + QQC2.ToolTip.visible: modelData.tooltip !== "" && hoverHandler.hovered + QQC2.ToolTip.text: modelData.tooltip + HoverHandler { id: hoverHandler } + + QQC2.BusyIndicator { + anchors.fill: parent + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + visible: running + running: modelData.isBusy + } + } } } EmojiDialog { id: emojiDialog x: parent.width - implicitWidth - y: -implicitHeight - Kirigami.Units.smallSpacing + y: -implicitHeight // - Kirigami.Units.smallSpacing modal: false includeCustom: true @@ -243,6 +337,10 @@ QQC2.ToolBar { onClosed: if (emojiButton.checked) emojiButton.checked = false } + background: Rectangle { + color: Kirigami.Theme.backgroundColor + } + CompletionMenu { id: completionMenu height: implicitHeight @@ -262,22 +360,34 @@ QQC2.ToolBar { target: currentRoom function onChatBoxEditIdChanged() { if (currentRoom.chatBoxEditMessage.length > 0) { - chatBar.inputFieldText = currentRoom.chatBoxEditMessage + textField.text = currentRoom.chatBoxEditMessage } } } ChatDocumentHandler { id: documentHandler - document: inputField.textDocument - cursorPosition: inputField.cursorPosition - selectionStart: inputField.selectionStart - selectionEnd: inputField.selectionEnd + document: textField.textDocument + cursorPosition: textField.cursorPosition + selectionStart: textField.selectionStart + selectionEnd: textField.selectionEnd Component.onCompleted: { RoomManager.chatDocumentHandler = documentHandler; } } + function forceActiveFocus() { + textField.forceActiveFocus(); + // set the cursor to the end of the text + textField.cursorPosition = textField.length; + } + + function insertText(text) { + let initialCursorPosition = textField.cursorPosition; + + textField.text = textField.text.substr(0, initialCursorPosition) + text + textField.text.substr(initialCursorPosition) + textField.cursorPosition = initialCursorPosition + text.length + } function pasteImage() { let localPath = Clipboard.saveImage(); @@ -291,7 +401,7 @@ QQC2.ToolBar { actionsHandler.handleMessage(); repeatTimer.stop() currentRoom.markAllMessagesAsRead(); - inputField.clear(); + textField.clear(); currentRoom.chatBoxReplyId = ""; currentRoom.chatBoxEditId = ""; messageSent() diff --git a/src/qml/Component/ChatBox/ChatBox.qml b/src/qml/Component/ChatBox/ChatBox.qml index 5b2a6db62..0755ee073 100644 --- a/src/qml/Component/ChatBox/ChatBox.qml +++ b/src/qml/Component/ChatBox/ChatBox.qml @@ -5,108 +5,52 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 as QQC2 import QtQuick.Layouts 1.15 -import org.kde.kirigami 2.15 as Kirigami +import org.kde.kirigami 2.15 as Kirigami import org.kde.neochat 1.0 ColumnLayout { id: chatBox - property alias inputFieldText: chatBar.inputFieldText - signal messageSent() + property alias chatBar: chatBar + + readonly property int extraWidth: width >= Kirigami.Units.gridUnit * 47 ? Math.min((width - Kirigami.Units.gridUnit * 47), Kirigami.Units.gridUnit * 20) : 0 + readonly property int chatBoxMaxWidth: Config.compactLayout ? width : Math.min(width, Kirigami.Units.gridUnit * 39 + extraWidth) + spacing: 0 - Kirigami.Separator { - id: connectionPaneSeparator - visible: connectionPane.visible + Kirigami.InlineMessage { Layout.fillWidth: true - } + Layout.leftMargin: 1 // So we can see the border + Layout.rightMargin: 1 // So we can see the border - QQC2.Pane { - id: connectionPane - padding: fontMetrics.lineSpacing * 0.25 - FontMetrics { - id: fontMetrics - font: networkLabel.font - } - spacing: 0 - Kirigami.Theme.colorSet: Kirigami.Theme.View - background: Rectangle { - color: Kirigami.Theme.backgroundColor - } + text: i18n("NeoChat is offline. Please check your network connection.") visible: !Controller.isOnline - Layout.fillWidth: true - QQC2.Label { - id: networkLabel - width: parent.width - wrapMode: Text.Wrap - text: i18n("NeoChat is offline. Please check your network connection.") - } } Kirigami.Separator { - id: replySeparator - visible: replyPane.visible - Layout.fillWidth: true - } - - ReplyPane { - id: replyPane - visible: currentRoom.chatBoxReplyId.length > 0 || currentRoom.chatBoxEditId.length > 0 - Layout.fillWidth: true - - onReplyCancelled: { - chatBox.focusInputField() - } - } - - Kirigami.Separator { - id: attachmentSeparator - visible: attachmentPane.visible - Layout.fillWidth: true - } - - AttachmentPane { - id: attachmentPane - visible: currentRoom.chatBoxAttachmentPath.length > 0 - Layout.fillWidth: true - } - - Kirigami.Separator { - id: chatBarSeparator - visible: chatBar.visible - Layout.fillWidth: true } ChatBar { id: chatBar + visible: currentRoom.canSendEvent("m.room.message") Layout.fillWidth: true + Layout.minimumHeight: implicitHeight + Kirigami.Units.largeSpacing + // lineSpacing is height+leading, so subtract leading once since leading only exists between lines. + Layout.maximumHeight: chatBarFontMetrics.lineSpacing * 8 - chatBarFontMetrics.leading + textField.topPadding + textField.bottomPadding + + FontMetrics { + id: chatBarFontMetrics + font: chatBar.textField.font + } onMessageSent: { chatBox.messageSent(); } - - Behavior on implicitHeight { - NumberAnimation { - property: "implicitHeight" - duration: Kirigami.Units.shortDuration - easing.type: Easing.OutCubic - } - } - } - - function insertText(str) { - let index = chatBar.cursorPosition; - chatBox.inputFieldText = inputFieldText.substr(0, chatBar.cursorPosition) + str + inputFieldText.substr(chatBar.cursorPosition); - chatBar.cursorPosition = index + str.length; - } - - function focusInputField() { - chatBar.inputFieldForceActiveFocusTriggered() } } diff --git a/src/qml/Component/ChatBox/ReplyPane.qml b/src/qml/Component/ChatBox/ReplyPane.qml index 436024f85..25d4e9726 100644 --- a/src/qml/Component/ChatBox/ReplyPane.qml +++ b/src/qml/Component/ChatBox/ReplyPane.qml @@ -10,108 +10,83 @@ import org.kde.kirigami 2.14 as Kirigami import org.kde.neochat 1.0 -Loader { - id: replyPane - property NeoChatUser user: currentRoom.chatBoxReplyUser ?? currentRoom.chatBoxEditUser +GridLayout { + id: root + property string userName + property color userColor: Kirigami.Theme.highlightColor + property var userAvatar: "" + property bool isReply + property var text - signal replyCancelled() + rows: 3 + columns: 3 + rowSpacing: Kirigami.Units.smallSpacing + columnSpacing: Kirigami.Units.largeSpacing - active: visible - sourceComponent: QQC2.Pane { - id: replyPane + QQC2.Label { + id: replyLabel + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + Layout.columnSpan: 3 + topPadding: Kirigami.Units.smallSpacing - Kirigami.Theme.colorSet: Kirigami.Theme.View + text: isReply ? i18n("Replying to:") : i18n("Editing message:") + } + Rectangle { + id: verticalBorder - spacing: leftPadding + Layout.fillHeight: true + Layout.rowSpan: 2 - contentItem: RowLayout { - Layout.fillWidth: true - spacing: replyPane.spacing + implicitWidth: Kirigami.Units.smallSpacing + color: userColor + } + Kirigami.Avatar { + id: replyAvatar - FontMetrics { - id: fontMetrics - font: textArea.font - } + implicitWidth: Kirigami.Units.iconSizes.small + implicitHeight: Kirigami.Units.iconSizes.small - Kirigami.Avatar { - id: avatar - Layout.alignment: textContentLayout.height > avatar.height ? Qt.AlignHCenter | Qt.AlignTop : Qt.AlignCenter - Layout.preferredWidth: Layout.preferredHeight - Layout.preferredHeight: fontMetrics.lineSpacing * 2 - fontMetrics.leading - source: user ? "image://mxc/" + currentRoom.getUser(user.id).avatarMediaId : "" - name: user ? user.displayName : "" - color: user ? user.color : "transparent" - visible: Boolean(user) - } + source: userAvatar + name: userName + color: userColor + } + QQC2.Label { + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft - ColumnLayout { - id: textContentLayout - Layout.alignment: Qt.AlignCenter - Layout.fillWidth: true - spacing: fontMetrics.leading - QQC2.Label { - Layout.fillWidth: true - textFormat: Text.StyledText - elide: Text.ElideRight - text: { - let heading = "%1" - let userName = user ? "" + currentRoom.htmlSafeMemberName(user.id) + "" : "" - if (currentRoom.chatBoxEditId.length > 0) { - heading = heading.arg(i18n("Editing message:")) + "
" - } else { - heading = heading.arg(i18n("Replying to %1:", userName)) - } + color: userColor + text: userName + elide: Text.ElideRight + } + QQC2.TextArea { + id: textArea - return heading - } - } - //TODO edit user mentions - QQC2.ScrollView { - Layout.alignment: Qt.AlignLeft | Qt.AlignTop - Layout.fillWidth: true - Layout.maximumHeight: fontMetrics.lineSpacing * 8 - fontMetrics.leading + Layout.fillWidth: true + Layout.columnSpan: 2 - // HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890) - QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff - - QQC2.TextArea { - id: textArea - leftPadding: 0 - rightPadding: 0 - topPadding: 0 - bottomPadding: 0 - text: "" + (currentRoom.chatBoxEditId.length > 0 ? currentRoom.chatBoxEditMessage : currentRoom.chatBoxReplyMessage) - selectByMouse: true - selectByKeyboard: true - readOnly: true - wrapMode: QQC2.Label.Wrap - textFormat: TextEdit.RichText - background: Item {} - HoverHandler { - cursorShape: textArea.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor - } - } - } - } - - QQC2.ToolButton { - display: QQC2.AbstractButton.IconOnly - action: Kirigami.Action { - text: i18nc("@action:button", "Cancel reply") - icon.name: "dialog-close" - onTriggered: { - currentRoom.chatBoxReplyId = ""; - currentRoom.chatBoxEditId = ""; - } - shortcut: "Escape" - } - QQC2.ToolTip.text: text - QQC2.ToolTip.visible: hovered - } + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + text: "" + replyTextMetrics.elidedText + selectByMouse: true + selectByKeyboard: true + readOnly: true + wrapMode: QQC2.Label.Wrap + textFormat: TextEdit.RichText + background: Item {} + HoverHandler { + cursorShape: textArea.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor } - background: Rectangle { - color: Kirigami.Theme.backgroundColor + TextMetrics { + id: replyTextMetrics + + text: root.text + font: textArea.font + elide: Qt.ElideRight + elideWidth: textArea.width * 2 - Kirigami.Units.smallSpacing * 2 } } } diff --git a/src/qml/Page/RoomPage.qml b/src/qml/Page/RoomPage.qml index 25e06a406..9936c33b2 100644 --- a/src/qml/Page/RoomPage.qml +++ b/src/qml/Page/RoomPage.qml @@ -50,6 +50,7 @@ Kirigami.ScrollablePage { applicationWindow().hoverLinkIndicator.text = ""; messageListView.positionViewAtBeginning(); hasScrolledUpBefore = false; + chatBox.chatBar.forceActiveFocus(); } Connections { @@ -137,8 +138,8 @@ Kirigami.ScrollablePage { switchRoomUp(); } else if (!(event.modifiers & Qt.ControlModifier) && event.key < Qt.Key_Escape) { event.accepted = true; - chatBox.addText(event.text); - chatBox.focusInputField(); + chatBox.chatBar.insertText(event.text); + chatBox.chatBar.forceActiveFocus(); return; } } @@ -349,7 +350,7 @@ Kirigami.ScrollablePage { visible: currentRoom && currentRoom.hasUnreadMessages && currentRoom.readMarkerLoaded action: Kirigami.Action { onTriggered: { - chatBox.focusInputField(); + chatBox.chatBar.forceActiveFocus(); messageListView.goToEvent(currentRoom.readMarkerEventId) } icon.name: "go-up" @@ -373,7 +374,7 @@ Kirigami.ScrollablePage { visible: !messageListView.atYEnd action: Kirigami.Action { onTriggered: { - chatBox.focusInputField(); + chatBox.chatBar.forceActiveFocus(); goToLastMessage(); currentRoom.markAllMessagesAsRead(); } @@ -519,13 +520,14 @@ Kirigami.ScrollablePage { QQC2.ToolTip.visible: hovered QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay icon.name: "preferences-desktop-emoticons" + onClicked: emojiDialog.open(); EmojiDialog { id: emojiDialog showQuickReaction: true onChosen: { page.currentRoom.toggleReaction(hoverActions.event.eventId, emoji); - chatBox.focusInputField(); + chatBox.chatBar.forceActiveFocus(); } } } @@ -538,7 +540,7 @@ Kirigami.ScrollablePage { onClicked: { currentRoom.chatBoxEditId = hoverActions.event.eventId; currentRoom.chatBoxReplyId = ""; - chatBox.focusInputField(); + chatBox.chatBar.forceActiveFocus(); } } QQC2.Button { @@ -549,7 +551,7 @@ Kirigami.ScrollablePage { onClicked: { currentRoom.chatBoxReplyId = hoverActions.event.eventId; currentRoom.chatBoxEditId = ""; - chatBox.focusInputField(); + chatBox.chatBar.forceActiveFocus(); } } }