Chatbar Refactor

This is mostly a stylistic rework of the chatbar but there are some buxfixes / improvements in here as well. The aim was to make the chatbar so it's size was managed in the same manner as the timeline in both bubble and compactmode in the same manner as network/neochat!476.

The other features are:
- Replies, attachments and edits now look like they are inside the chatbar and use a similar styling to edits in message bubbles
- Replies and edits now part of the message so they arte part of the ScrollView and will scroll away when the text is long
- ~~The emoji picker is now a popup so it doesn't mess with the timeline layout when activated~~ (done in network/neochat!697)
- ~~Emoji dialog is now no longer required as the picker itself is a popup now~~ (no longer the case see above)
- The scrollbar now sits on the right of the chatbar actions rather than weirdly to the left
- The action icons will always stay in the same place even as the chatbar gets taller


Updated\
![2022-12-10_14-13-41.mkv](/uploads/ccbe14843834a19bf98ef0028e023eae/2022-12-10_14-13-41.mkv)

Scrollbar behaviour before
![chatbar_refactor_scrollbar_before](/uploads/2f3b91a79eb302ccf83dd35e51004e6a/chatbar_refactor_scrollbar_before.png)

Scrollbar behaviour after
![chatbar_refactor_scrollbar_after](/uploads/fcab044d8a4338ed9bcff6721b65e89c/chatbar_refactor_scrollbar_after.png)
This commit is contained in:
James Graham
2023-01-24 18:02:19 +00:00
parent 826760a55c
commit eee93e0f1f
5 changed files with 498 additions and 540 deletions

View File

@@ -10,43 +10,72 @@ import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0 import org.kde.neochat 1.0
Loader { ColumnLayout {
id: attachmentPaneLoader 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 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) readonly property string baseFileName: attachmentPath.substring(attachmentPath.lastIndexOf('/') + 1, attachmentPath.length)
active: visible RowLayout {
sourceComponent: Component { spacing: Kirigami.Units.smallSpacing
QQC2.Pane {
id: attachmentPane
Kirigami.Theme.colorSet: Kirigami.Theme.View
contentItem: Item { QQC2.Label {
property real spacing: attachmentPane.spacing > 0 ? attachmentPane.spacing : toolBar.spacing Layout.fillWidth: true
implicitWidth: Math.max(image.implicitWidth, imageBusyIndicator.implicitWidth, fileInfoLayout.implicitWidth, toolBar.implicitWidth) Layout.alignment: Qt.AlignLeft
implicitHeight: Math.max( text: i18n("Attachment:")
(hasImage ? Math.max(image.preferredHeight, imageBusyIndicator.implicitHeight) + spacing : 0) horizontalAlignment: Text.AlignLeft
+ fileInfoLayout.implicitHeight, verticalAlignment: Text.AlignVCenter
toolBar.implicitHeight }
) QQC2.ToolButton {
id: editImageButton
visible: hasImage
icon.name: "document-edit"
text: i18n("Edit")
display: QQC2.AbstractButton.IconOnly
Component {
id: imageEditorPage
ImageEditorPage {
imagePath: root.attachmentPath
}
}
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 { Image {
id: image id: image
property real preferredHeight: Math.min(implicitHeight, Kirigami.Units.gridUnit * 8) Layout.alignment: Qt.AlignHCenter
height: preferredHeight
anchors {
horizontalCenter: parent.horizontalCenter
bottom: fileInfoLayout.top
bottomMargin: parent.spacing
}
width: Math.min(implicitWidth, attachmentPane.availableWidth)
asynchronous: true asynchronous: true
cache: false // Cache is not needed. Images will rarely be shown repeatedly. 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 ? root.attachmentPath : ""
source: hasImage ? attachmentPaneLoader.attachmentPath : ""
visible: hasImage visible: hasImage
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
@@ -65,133 +94,31 @@ Loader {
Behavior on height { Behavior on height {
NumberAnimation { NumberAnimation {
property: "height"
duration: Kirigami.Units.shortDuration duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic easing.type: Easing.OutCubic
} }
} }
} }
QQC2.BusyIndicator { QQC2.BusyIndicator {
id: imageBusyIndicator id: imageBusyIndicator
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
bottom: fileInfoLayout.top
bottomMargin: parent.spacing
}
visible: running visible: running
running: image.visible && image.progress < 1 running: image.visible && image.progress < 1
} }
RowLayout { RowLayout {
id: fileInfoLayout id: fileInfoLayout
anchors.horizontalCenter: parent.horizontalCenter Layout.alignment: Qt.AlignHCenter
anchors.verticalCenter: undefined
anchors.bottom: parent.bottom
spacing: parent.spacing spacing: parent.spacing
Kirigami.Icon { Kirigami.Icon {
id: mimetypeIcon id: mimetypeIcon
implicitHeight: Kirigami.Units.fontMetrics.roundedIconSize(fileLabel.implicitHeight) implicitWidth: Kirigami.Units.iconSizes.smallMedium
implicitWidth: implicitHeight implicitHeight: Kirigami.Units.iconSizes.smallMedium
source: attachmentMimetype.iconName source: attachmentMimetype.iconName
} }
QQC2.Label { QQC2.Label {
id: fileLabel id: fileLabel
text: baseFileName 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
}
}
background: Rectangle {
color: Kirigami.Theme.backgroundColor
}
}
} }
} }

View File

@@ -5,172 +5,35 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15 as QQC2 import QtQuick.Controls 2.15 as QQC2
import QtQuick.Window 2.15
import org.kde.kirigami 2.18 as Kirigami import org.kde.kirigami 2.18 as Kirigami
import org.kde.neochat 1.0 import org.kde.neochat 1.0
QQC2.ToolBar { QQC2.Control {
id: chatBar id: root
property alias inputFieldText: inputField.text
property alias textField: inputField property alias textField: textField
property alias cursorPosition: inputField.cursorPosition 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() signal messageSent()
onInputFieldForceActiveFocusTriggered: { property list<Kirigami.Action> actions : [
inputField.forceActiveFocus(); Kirigami.Action {
// set the cursor to the end of the text id: attachmentAction
inputField.cursorPosition = inputField.length;
}
position: QQC2.ToolBar.Footer property bool isBusy: currentRoom && currentRoom.hasFileUploading
Kirigami.Theme.colorSet: Kirigami.Theme.View
// 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"]
}
} 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
}
}
}
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 // Matrix does not allow sending attachments in replies
visible: currentRoom.chatBoxReplyId.length === 0 && currentRoom.chatBoxAttachmentPath.length === 0 && !uploadingBusySpinner.running visible: currentRoom.chatBoxReplyId.length === 0 && currentRoom.chatBoxAttachmentPath.length === 0
icon.name: "mail-attachment" icon.name: "mail-attachment"
text: i18n("Attach an image or file") text: i18n("Attach an image or file")
display: QQC2.AbstractButton.IconOnly displayHint: Kirigami.DisplayHint.IconOnly
onClicked: { onTriggered: {
if (Clipboard.hasImage) { if (Clipboard.hasImage) {
attachDialog.open() attachDialog.open()
} else { } else {
@@ -185,25 +48,19 @@ QQC2.ToolBar {
} }
} }
QQC2.ToolTip.text: text tooltip: text
QQC2.ToolTip.visible: hovered },
} Kirigami.Action {
QQC2.BusyIndicator { id: emojiAction
id: uploadingBusySpinner
anchors.fill: parent property bool isBusy: false
visible: running
running: currentRoom && currentRoom.hasFileUploading
}
}
QQC2.ToolButton {
id: emojiButton
icon.name: "smiley" icon.name: "smiley"
text: i18n("Add an Emoji") text: i18n("Add an Emoji")
display: QQC2.AbstractButton.IconOnly displayHint: Kirigami.DisplayHint.IconOnly
checkable: true checkable: true
onClicked: { onTriggered: {
if (emojiDialog.visible) { if (emojiDialog.visible) {
emojiDialog.close() emojiDialog.close()
} else { } else {
@@ -211,29 +68,266 @@ QQC2.ToolBar {
} }
} }
QQC2.ToolTip.text: text tooltip: text
QQC2.ToolTip.visible: hovered },
Kirigami.Action {
id: sendAction
property bool isBusy: false
icon.name: "document-send"
text: i18n("Send message")
displayHint: Kirigami.DisplayHint.IconOnly
checkable: true
onTriggered: {
root.postMessage()
}
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 { QQC2.ToolButton {
id: sendButton id: cancelButton
icon.name: "document-send" anchors.top: parent.top
text: i18n("Send message") 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 display: QQC2.AbstractButton.IconOnly
action: Kirigami.Action {
onClicked: { text: root.isReplying ? i18nc("@action:button", "Cancel reply") : i18nc("@action:button", "Cancel edit")
chatBar.postMessage() icon.name: "dialog-close"
onTriggered: {
currentRoom.chatBoxReplyId = "";
currentRoom.chatBoxEditId = "";
currentRoom.chatBoxAttachmentPath = "";
root.forceActiveFocus()
}
shortcut: "Escape"
} }
QQC2.ToolTip.text: text QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered 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 { EmojiDialog {
id: emojiDialog id: emojiDialog
x: parent.width - implicitWidth x: parent.width - implicitWidth
y: -implicitHeight - Kirigami.Units.smallSpacing y: -implicitHeight // - Kirigami.Units.smallSpacing
modal: false modal: false
includeCustom: true includeCustom: true
@@ -243,6 +337,10 @@ QQC2.ToolBar {
onClosed: if (emojiButton.checked) emojiButton.checked = false onClosed: if (emojiButton.checked) emojiButton.checked = false
} }
background: Rectangle {
color: Kirigami.Theme.backgroundColor
}
CompletionMenu { CompletionMenu {
id: completionMenu id: completionMenu
height: implicitHeight height: implicitHeight
@@ -262,22 +360,34 @@ QQC2.ToolBar {
target: currentRoom target: currentRoom
function onChatBoxEditIdChanged() { function onChatBoxEditIdChanged() {
if (currentRoom.chatBoxEditMessage.length > 0) { if (currentRoom.chatBoxEditMessage.length > 0) {
chatBar.inputFieldText = currentRoom.chatBoxEditMessage textField.text = currentRoom.chatBoxEditMessage
} }
} }
} }
ChatDocumentHandler { ChatDocumentHandler {
id: documentHandler id: documentHandler
document: inputField.textDocument document: textField.textDocument
cursorPosition: inputField.cursorPosition cursorPosition: textField.cursorPosition
selectionStart: inputField.selectionStart selectionStart: textField.selectionStart
selectionEnd: inputField.selectionEnd selectionEnd: textField.selectionEnd
Component.onCompleted: { Component.onCompleted: {
RoomManager.chatDocumentHandler = documentHandler; 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() { function pasteImage() {
let localPath = Clipboard.saveImage(); let localPath = Clipboard.saveImage();
@@ -291,7 +401,7 @@ QQC2.ToolBar {
actionsHandler.handleMessage(); actionsHandler.handleMessage();
repeatTimer.stop() repeatTimer.stop()
currentRoom.markAllMessagesAsRead(); currentRoom.markAllMessagesAsRead();
inputField.clear(); textField.clear();
currentRoom.chatBoxReplyId = ""; currentRoom.chatBoxReplyId = "";
currentRoom.chatBoxEditId = ""; currentRoom.chatBoxEditId = "";
messageSent() messageSent()

View File

@@ -5,108 +5,52 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2 import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15 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 import org.kde.neochat 1.0
ColumnLayout { ColumnLayout {
id: chatBox id: chatBox
property alias inputFieldText: chatBar.inputFieldText
signal messageSent() 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 spacing: 0
Kirigami.Separator { Kirigami.InlineMessage {
id: connectionPaneSeparator
visible: connectionPane.visible
Layout.fillWidth: true 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
}
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.") text: i18n("NeoChat is offline. Please check your network connection.")
} visible: !Controller.isOnline
} }
Kirigami.Separator { 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 Layout.fillWidth: true
} }
ChatBar { ChatBar {
id: chatBar id: chatBar
visible: currentRoom.canSendEvent("m.room.message") visible: currentRoom.canSendEvent("m.room.message")
Layout.fillWidth: true 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: { onMessageSent: {
chatBox.messageSent(); 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()
} }
} }

View File

@@ -10,77 +10,66 @@ import org.kde.kirigami 2.14 as Kirigami
import org.kde.neochat 1.0 import org.kde.neochat 1.0
Loader { GridLayout {
id: replyPane id: root
property NeoChatUser user: currentRoom.chatBoxReplyUser ?? currentRoom.chatBoxEditUser 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 QQC2.Label {
sourceComponent: QQC2.Pane { id: replyLabel
id: replyPane
Kirigami.Theme.colorSet: Kirigami.Theme.View
spacing: leftPadding
contentItem: RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: replyPane.spacing Layout.alignment: Qt.AlignLeft
Layout.columnSpan: 3
topPadding: Kirigami.Units.smallSpacing
FontMetrics { text: isReply ? i18n("Replying to:") : i18n("Editing message:")
id: fontMetrics
font: textArea.font
} }
Rectangle {
id: verticalBorder
Layout.fillHeight: true
Layout.rowSpan: 2
implicitWidth: Kirigami.Units.smallSpacing
color: userColor
}
Kirigami.Avatar { Kirigami.Avatar {
id: avatar id: replyAvatar
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)
}
ColumnLayout { implicitWidth: Kirigami.Units.iconSizes.small
id: textContentLayout implicitHeight: Kirigami.Units.iconSizes.small
Layout.alignment: Qt.AlignCenter
Layout.fillWidth: true source: userAvatar
spacing: fontMetrics.leading name: userName
color: userColor
}
QQC2.Label { QQC2.Label {
Layout.fillWidth: true Layout.fillWidth: true
textFormat: Text.StyledText Layout.alignment: Qt.AlignLeft
color: userColor
text: userName
elide: Text.ElideRight elide: Text.ElideRight
text: {
let heading = "<b>%1</b>"
let userName = user ? "<font color=\""+ user.color +"\">" + currentRoom.htmlSafeMemberName(user.id) + "</font>" : ""
if (currentRoom.chatBoxEditId.length > 0) {
heading = heading.arg(i18n("Editing message:")) + "<br/>"
} else {
heading = heading.arg(i18n("Replying to %1:", userName))
} }
return heading
}
}
//TODO edit user mentions
QQC2.ScrollView {
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
Layout.maximumHeight: fontMetrics.lineSpacing * 8 - fontMetrics.leading
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
QQC2.TextArea { QQC2.TextArea {
id: textArea id: textArea
Layout.fillWidth: true
Layout.columnSpan: 2
leftPadding: 0 leftPadding: 0
rightPadding: 0 rightPadding: 0
topPadding: 0 topPadding: 0
bottomPadding: 0 bottomPadding: 0
text: "<style> a{color:" + Kirigami.Theme.linkColor + ";}.user-pill{}</style>" + (currentRoom.chatBoxEditId.length > 0 ? currentRoom.chatBoxEditMessage : currentRoom.chatBoxReplyMessage) text: "<style> a{color:" + Kirigami.Theme.linkColor + ";}.user-pill{}</style>" + replyTextMetrics.elidedText
selectByMouse: true selectByMouse: true
selectByKeyboard: true selectByKeyboard: true
readOnly: true readOnly: true
@@ -90,28 +79,14 @@ Loader {
HoverHandler { HoverHandler {
cursorShape: textArea.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor cursorShape: textArea.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
} }
}
}
}
QQC2.ToolButton { TextMetrics {
display: QQC2.AbstractButton.IconOnly id: replyTextMetrics
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
}
}
background: Rectangle { text: root.text
color: Kirigami.Theme.backgroundColor font: textArea.font
elide: Qt.ElideRight
elideWidth: textArea.width * 2 - Kirigami.Units.smallSpacing * 2
} }
} }
} }

View File

@@ -50,6 +50,7 @@ Kirigami.ScrollablePage {
applicationWindow().hoverLinkIndicator.text = ""; applicationWindow().hoverLinkIndicator.text = "";
messageListView.positionViewAtBeginning(); messageListView.positionViewAtBeginning();
hasScrolledUpBefore = false; hasScrolledUpBefore = false;
chatBox.chatBar.forceActiveFocus();
} }
Connections { Connections {
@@ -137,8 +138,8 @@ Kirigami.ScrollablePage {
switchRoomUp(); switchRoomUp();
} else if (!(event.modifiers & Qt.ControlModifier) && event.key < Qt.Key_Escape) { } else if (!(event.modifiers & Qt.ControlModifier) && event.key < Qt.Key_Escape) {
event.accepted = true; event.accepted = true;
chatBox.addText(event.text); chatBox.chatBar.insertText(event.text);
chatBox.focusInputField(); chatBox.chatBar.forceActiveFocus();
return; return;
} }
} }
@@ -349,7 +350,7 @@ Kirigami.ScrollablePage {
visible: currentRoom && currentRoom.hasUnreadMessages && currentRoom.readMarkerLoaded visible: currentRoom && currentRoom.hasUnreadMessages && currentRoom.readMarkerLoaded
action: Kirigami.Action { action: Kirigami.Action {
onTriggered: { onTriggered: {
chatBox.focusInputField(); chatBox.chatBar.forceActiveFocus();
messageListView.goToEvent(currentRoom.readMarkerEventId) messageListView.goToEvent(currentRoom.readMarkerEventId)
} }
icon.name: "go-up" icon.name: "go-up"
@@ -373,7 +374,7 @@ Kirigami.ScrollablePage {
visible: !messageListView.atYEnd visible: !messageListView.atYEnd
action: Kirigami.Action { action: Kirigami.Action {
onTriggered: { onTriggered: {
chatBox.focusInputField(); chatBox.chatBar.forceActiveFocus();
goToLastMessage(); goToLastMessage();
currentRoom.markAllMessagesAsRead(); currentRoom.markAllMessagesAsRead();
} }
@@ -519,13 +520,14 @@ Kirigami.ScrollablePage {
QQC2.ToolTip.visible: hovered QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
icon.name: "preferences-desktop-emoticons" icon.name: "preferences-desktop-emoticons"
onClicked: emojiDialog.open(); onClicked: emojiDialog.open();
EmojiDialog { EmojiDialog {
id: emojiDialog id: emojiDialog
showQuickReaction: true showQuickReaction: true
onChosen: { onChosen: {
page.currentRoom.toggleReaction(hoverActions.event.eventId, emoji); page.currentRoom.toggleReaction(hoverActions.event.eventId, emoji);
chatBox.focusInputField(); chatBox.chatBar.forceActiveFocus();
} }
} }
} }
@@ -538,7 +540,7 @@ Kirigami.ScrollablePage {
onClicked: { onClicked: {
currentRoom.chatBoxEditId = hoverActions.event.eventId; currentRoom.chatBoxEditId = hoverActions.event.eventId;
currentRoom.chatBoxReplyId = ""; currentRoom.chatBoxReplyId = "";
chatBox.focusInputField(); chatBox.chatBar.forceActiveFocus();
} }
} }
QQC2.Button { QQC2.Button {
@@ -549,7 +551,7 @@ Kirigami.ScrollablePage {
onClicked: { onClicked: {
currentRoom.chatBoxReplyId = hoverActions.event.eventId; currentRoom.chatBoxReplyId = hoverActions.event.eventId;
currentRoom.chatBoxEditId = ""; currentRoom.chatBoxEditId = "";
chatBox.focusInputField(); chatBox.chatBar.forceActiveFocus();
} }
} }
} }