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,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
}
}
}

View File

@@ -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<Kirigami.Action> 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()

View File

@@ -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()
}
}

View File

@@ -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 = "<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))
}
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: "<style> a{color:" + Kirigami.Theme.linkColor + ";}.user-pill{}</style>" + (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: "<style> a{color:" + Kirigami.Theme.linkColor + ";}.user-pill{}</style>" + 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
}
}
}

View File

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