Add ChatBarMessageContentModel and hook up
This commit is contained in:
@@ -35,7 +35,7 @@ QQC2.Control {
|
|||||||
|
|
||||||
required property NeoChatConnection connection
|
required property NeoChatConnection connection
|
||||||
|
|
||||||
onActiveFocusChanged: textField.forceActiveFocus()
|
onActiveFocusChanged: chatContentView.itemAt(contentModel.index(contentModel.focusRow, 0)).forceActiveFocus()
|
||||||
|
|
||||||
onCurrentRoomChanged: {
|
onCurrentRoomChanged: {
|
||||||
_private.chatBarCache = currentRoom.mainCache
|
_private.chatBarCache = currentRoom.mainCache
|
||||||
@@ -75,6 +75,9 @@ QQC2.Control {
|
|||||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||||
Kirigami.Theme.inherit: false
|
Kirigami.Theme.inherit: false
|
||||||
|
|
||||||
|
Message.room: root.currentRoom
|
||||||
|
Message.contentModel: contentModel
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: Kirigami.Theme.backgroundColor
|
color: Kirigami.Theme.backgroundColor
|
||||||
Kirigami.Separator {
|
Kirigami.Separator {
|
||||||
@@ -84,233 +87,39 @@ QQC2.Control {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
height: Math.max(Math.min(chatScrollView.contentHeight + bottomPadding + topPadding, Kirigami.Units.gridUnit * 10), Kirigami.Units.gridUnit * 5)
|
||||||
leftPadding: rightPadding
|
leftPadding: rightPadding
|
||||||
rightPadding: (root.width - chatBarSizeHelper.availableWidth) / 2
|
rightPadding: (root.width - chatBarSizeHelper.availableWidth) / 2
|
||||||
topPadding: 0
|
topPadding: Kirigami.Units.smallSpacing
|
||||||
bottomPadding: 0
|
bottomPadding: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
contentItem: ColumnLayout {
|
contentItem: QQC2.ScrollView {
|
||||||
spacing: 0
|
id: chatScrollView
|
||||||
Item {
|
ColumnLayout {
|
||||||
// Required to adjust for the top separator
|
|
||||||
Layout.preferredHeight: 1
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
Loader {
|
|
||||||
id: replyLoader
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.margins: Kirigami.Units.largeSpacing
|
|
||||||
Layout.preferredHeight: active ? (item as Item).implicitHeight : 0
|
|
||||||
|
|
||||||
active: visible
|
|
||||||
visible: root.currentRoom.mainCache.replyId.length > 0
|
|
||||||
sourceComponent: replyPane
|
|
||||||
}
|
|
||||||
RowLayout {
|
|
||||||
visible: replyLoader.visible && !root.currentRoom.mainCache.relationAuthorIsPresent
|
|
||||||
spacing: Kirigami.Units.smallSpacing
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
Kirigami.Icon {
|
Repeater {
|
||||||
source: "help-hint-symbolic"
|
id: chatContentView
|
||||||
color: Kirigami.Theme.disabledTextColor
|
model: ChatBarMessageContentModel {
|
||||||
|
id: contentModel
|
||||||
Layout.preferredWidth: Kirigami.Units.iconSizes.small
|
type: ChatBarType.Room
|
||||||
Layout.preferredHeight: Kirigami.Units.iconSizes.small
|
room: root.currentRoom
|
||||||
}
|
|
||||||
QQC2.Label {
|
|
||||||
text: i18nc("@info", "The user you're replying to has left the room, and can't be notified.")
|
|
||||||
color: Kirigami.Theme.disabledTextColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loader {
|
|
||||||
id: attachLoader
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.margins: Kirigami.Units.largeSpacing
|
|
||||||
Layout.preferredHeight: active ? (item as Item).implicitHeight : 0
|
|
||||||
|
|
||||||
active: visible
|
|
||||||
visible: root.currentRoom.mainCache.attachmentPath.length > 0
|
|
||||||
sourceComponent: attachmentPane
|
|
||||||
}
|
|
||||||
RowLayout {
|
|
||||||
QQC2.ScrollView {
|
|
||||||
id: chatBarScrollView
|
|
||||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
|
||||||
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
|
||||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
|
||||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.maximumHeight: Kirigami.Units.gridUnit * 8
|
|
||||||
Layout.minimumHeight: Kirigami.Units.gridUnit * 3
|
|
||||||
|
|
||||||
// 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 {
|
delegate: MessageComponentChooser {}
|
||||||
id: textField
|
|
||||||
|
|
||||||
placeholderText: root.currentRoom.usesEncryption ? i18nc("@placeholder", "Send an encrypted message…") : root.currentRoom.mainCache.attachmentPath.length > 0 ? i18nc("@placeholder", "Set an attachment caption…") : i18nc("@placeholder", "Send a message…")
|
|
||||||
verticalAlignment: TextEdit.AlignVCenter
|
|
||||||
wrapMode: TextEdit.Wrap
|
|
||||||
persistentSelection: true
|
|
||||||
|
|
||||||
Accessible.description: placeholderText
|
|
||||||
|
|
||||||
Kirigami.SpellCheck.enabled: false
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: repeatTimer
|
|
||||||
interval: 5000
|
|
||||||
}
|
|
||||||
|
|
||||||
onTextChanged: {
|
|
||||||
if (!repeatTimer.running && NeoChatConfig.typingNotifications) {
|
|
||||||
var textExists = text.length > 0;
|
|
||||||
root.currentRoom.sendTypingNotification(textExists);
|
|
||||||
textExists ? repeatTimer.start() : repeatTimer.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onSelectedTextChanged: {
|
|
||||||
if (selectedText.length > 0) {
|
|
||||||
quickFormatBar.selectionStart = selectionStart;
|
|
||||||
quickFormatBar.selectionEnd = selectionEnd;
|
|
||||||
quickFormatBar.open();
|
|
||||||
} else if (quickFormatBar.visible) {
|
|
||||||
quickFormatBar.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QuickFormatBar {
|
|
||||||
id: quickFormatBar
|
|
||||||
|
|
||||||
x: textField.cursorRectangle.x
|
|
||||||
y: textField.cursorRectangle.y - height
|
|
||||||
|
|
||||||
onFormattingSelected: (format, selectionStart, selectionEnd) => _private.formatText(format, selectionStart, selectionEnd)
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onEnterPressed: event => {
|
|
||||||
const controlIsPressed = event.modifiers & Qt.ControlModifier;
|
|
||||||
if (completionMenu.visible) {
|
|
||||||
completionMenu.complete();
|
|
||||||
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile || NeoChatConfig.sendMessageWith === 1 && !controlIsPressed || NeoChatConfig.sendMessageWith === 0 && controlIsPressed) {
|
|
||||||
textField.insert(cursorPosition, "\n");
|
|
||||||
} else if (NeoChatConfig.sendMessageWith === 0 && !controlIsPressed || NeoChatConfig.sendMessageWith === 1 && controlIsPressed) {
|
|
||||||
_private.postMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Keys.onReturnPressed: event => {
|
|
||||||
const controlIsPressed = event.modifiers & Qt.ControlModifier;
|
|
||||||
if (completionMenu.visible) {
|
|
||||||
completionMenu.complete();
|
|
||||||
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile || NeoChatConfig.sendMessageWith === 1 && !controlIsPressed || NeoChatConfig.sendMessageWith === 0 && controlIsPressed) {
|
|
||||||
textField.insert(cursorPosition, "\n");
|
|
||||||
} else if (NeoChatConfig.sendMessageWith === 0 && !controlIsPressed || NeoChatConfig.sendMessageWith === 1 && controlIsPressed) {
|
|
||||||
_private.postMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Keys.onTabPressed: {
|
|
||||||
if (completionMenu.visible) {
|
|
||||||
completionMenu.complete();
|
|
||||||
} else {
|
|
||||||
contextDrawer.handle.children[0].forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Keys.onPressed: event => {
|
|
||||||
if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
|
|
||||||
event.accepted = _private.pasteImage();
|
|
||||||
} else if (event.key === Qt.Key_Up && event.modifiers & Qt.ControlModifier) {
|
|
||||||
root.currentRoom.replyLastMessage();
|
|
||||||
} else if (event.key === Qt.Key_Up && textField.text.length === 0) {
|
|
||||||
root.currentRoom.editLastMessage();
|
|
||||||
} 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 || event.key === Qt.Key_Delete) {
|
|
||||||
if (textField.text == selectedText || textField.text.length <= 1) {
|
|
||||||
root.currentRoom.sendTypingNotification(false);
|
|
||||||
repeatTimer.stop();
|
|
||||||
}
|
|
||||||
if (quickFormatBar.visible && selectedText.length > 0) {
|
|
||||||
quickFormatBar.close();
|
|
||||||
}
|
|
||||||
} else if (event.key === Qt.Key_Escape && completionMenu.visible) {
|
|
||||||
completionMenu.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Keys.onShortcutOverride: event => {
|
|
||||||
if ((_private.chatBarCache.isReplying || _private.chatBarCache.attachmentPath.length > 0) && event.key === Qt.Key_Escape) {
|
|
||||||
_private.chatBarCache.attachmentPath = "";
|
|
||||||
_private.chatBarCache.replyId = "";
|
|
||||||
_private.chatBarCache.threadId = "";
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
background: MouseArea {
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
cursorShape: Qt.IBeamCursor
|
|
||||||
z: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
RowLayout {
|
RichEditBar {
|
||||||
id: actionsRow
|
id: richEditBar
|
||||||
spacing: 0
|
maxAvailableWidth: chatBarSizeHelper.availableWidth - Kirigami.Units.largeSpacing * 2
|
||||||
Layout.alignment: Qt.AlignBottom
|
|
||||||
Layout.bottomMargin: Kirigami.Units.smallSpacing * 4
|
|
||||||
|
|
||||||
Repeater {
|
room: root.currentRoom
|
||||||
model: root.actions
|
contentModel: chatContentView.model
|
||||||
delegate: QQC2.ToolButton {
|
|
||||||
id: actionDelegate
|
|
||||||
required property BusyAction modelData
|
|
||||||
icon.name: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
|
|
||||||
onClicked: if (!pieProgress.visible) {
|
|
||||||
modelData.trigger()
|
|
||||||
}
|
|
||||||
|
|
||||||
padding: Kirigami.Units.smallSpacing
|
onClicked: contentModel.refocusCurrentComponent()
|
||||||
|
|
||||||
QQC2.ToolTip.visible: hovered
|
|
||||||
QQC2.ToolTip.text: modelData.tooltip
|
|
||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
|
||||||
|
|
||||||
PieProgressBar {
|
|
||||||
id: pieProgress
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: actionDelegate.modelData.isBusy
|
|
||||||
progress: root.currentRoom.fileUploadingProgress
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RichEditBar {
|
|
||||||
id: richEditBar
|
|
||||||
Layout.maximumWidth: chatBarSizeHelper.availableWidth - Kirigami.Units.largeSpacing * 2
|
|
||||||
Layout.margins: Kirigami.Units.largeSpacing
|
|
||||||
Layout.alignment:Qt.AlignHCenter
|
|
||||||
maxAvailableWidth: chatBarSizeHelper.availableWidth - Kirigami.Units.largeSpacing * 2
|
|
||||||
|
|
||||||
room: root.currentRoom
|
|
||||||
documentHandler: documentHandler
|
|
||||||
|
|
||||||
onRequestPostMessage: _private.postMessage()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LibNeoChat.DelegateSizeHelper {
|
LibNeoChat.DelegateSizeHelper {
|
||||||
id: chatBarSizeHelper
|
id: chatBarSizeHelper
|
||||||
parentItem: root
|
parentItem: root
|
||||||
@@ -321,49 +130,6 @@ QQC2.Control {
|
|||||||
maxWidth: NeoChatConfig.compactLayout ? root.width - Kirigami.Units.largeSpacing * 2 : Kirigami.Units.gridUnit * 60
|
maxWidth: NeoChatConfig.compactLayout ? root.width - Kirigami.Units.largeSpacing * 2 : Kirigami.Units.gridUnit * 60
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
|
||||||
id: replyPane
|
|
||||||
Item {
|
|
||||||
implicitHeight: replyComponent.implicitHeight
|
|
||||||
ReplyComponent {
|
|
||||||
id: replyComponent
|
|
||||||
replyContentModel: ContentProvider.contentModelForEvent(root.currentRoom, _private.chatBarCache.replyId, true)
|
|
||||||
Message.maxContentWidth: (replyLoader.item as Item).width
|
|
||||||
|
|
||||||
// When the user replies to a message and the preview is loaded, make sure the text field is focused again
|
|
||||||
Component.onCompleted: textField.forceActiveFocus(Qt.OtherFocusReason)
|
|
||||||
}
|
|
||||||
QQC2.Button {
|
|
||||||
id: cancelButton
|
|
||||||
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.right: parent.right
|
|
||||||
|
|
||||||
display: QQC2.AbstractButton.IconOnly
|
|
||||||
text: i18nc("@action:button", "Cancel reply")
|
|
||||||
icon.name: "dialog-close"
|
|
||||||
onClicked: {
|
|
||||||
_private.chatBarCache.replyId = "";
|
|
||||||
_private.chatBarCache.attachmentPath = "";
|
|
||||||
}
|
|
||||||
QQC2.ToolTip.text: text
|
|
||||||
QQC2.ToolTip.visible: hovered
|
|
||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Component {
|
|
||||||
id: attachmentPane
|
|
||||||
AttachmentPane {
|
|
||||||
attachmentPath: _private.chatBarCache.attachmentPath
|
|
||||||
|
|
||||||
onAttachmentCancelled: {
|
|
||||||
_private.chatBarCache.attachmentPath = "";
|
|
||||||
root.forceActiveFocus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: _private
|
id: _private
|
||||||
property ChatBarCache chatBarCache
|
property ChatBarCache chatBarCache
|
||||||
@@ -371,53 +137,6 @@ QQC2.Control {
|
|||||||
richEditBar.chatBarCache = chatBarCache
|
richEditBar.chatBarCache = chatBarCache
|
||||||
}
|
}
|
||||||
|
|
||||||
function postMessage() {
|
|
||||||
_private.chatBarCache.postMessage();
|
|
||||||
repeatTimer.stop();
|
|
||||||
textField.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatText(format, selectionStart, selectionEnd) {
|
|
||||||
let index = textField.cursorPosition;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* There cannot be white space at the beginning or end of the string for the
|
|
||||||
* formatting to work so move the sectionStart and sectionEnd markers past any whitespace.
|
|
||||||
*/
|
|
||||||
let innerText = textField.text.substr(selectionStart, selectionEnd - selectionStart);
|
|
||||||
if (innerText.charAt(innerText.length - 1) === " ") {
|
|
||||||
let trimmedRightString = innerText.replace(/\s*$/, "");
|
|
||||||
let trimDifference = innerText.length - trimmedRightString.length;
|
|
||||||
selectionEnd -= trimDifference;
|
|
||||||
}
|
|
||||||
if (innerText.charAt(0) === " ") {
|
|
||||||
let trimmedLeftString = innerText.replace(/^\s*/, "");
|
|
||||||
let trimDifference = innerText.length - trimmedLeftString.length;
|
|
||||||
selectionStart = selectionStart + trimDifference;
|
|
||||||
}
|
|
||||||
let startText = textField.text.substr(0, selectionStart);
|
|
||||||
// Needs updating with the new selectionStart and selectionEnd with white space trimmed.
|
|
||||||
innerText = textField.text.substr(selectionStart, selectionEnd - selectionStart);
|
|
||||||
let endText = textField.text.substr(selectionEnd);
|
|
||||||
textField.text = "";
|
|
||||||
textField.text = startText + format.start + innerText + format.end + format.extra + endText;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Put the cursor where it was when the popup was opened accounting for the
|
|
||||||
* new markup.
|
|
||||||
*
|
|
||||||
* The exception is for a hyperlink where it is placed ready to start typing
|
|
||||||
* the url.
|
|
||||||
*/
|
|
||||||
if (format.extra !== "") {
|
|
||||||
textField.cursorPosition = selectionEnd + format.start.length + format.end.length;
|
|
||||||
} else if (index == selectionStart) {
|
|
||||||
textField.cursorPosition = index;
|
|
||||||
} else {
|
|
||||||
textField.cursorPosition = index + format.start.length + format.end.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pasteImage() {
|
function pasteImage() {
|
||||||
let localPath = Clipboard.saveImage();
|
let localPath = Clipboard.saveImage();
|
||||||
if (localPath.length === 0) {
|
if (localPath.length === 0) {
|
||||||
@@ -428,16 +147,9 @@ QQC2.Control {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatDocumentHandler {
|
|
||||||
id: documentHandler
|
|
||||||
type: ChatBarType.Room
|
|
||||||
textItem: textField
|
|
||||||
room: root.currentRoom
|
|
||||||
}
|
|
||||||
|
|
||||||
CompletionMenu {
|
CompletionMenu {
|
||||||
id: completionMenu
|
id: completionMenu
|
||||||
chatDocumentHandler: documentHandler
|
chatDocumentHandler: contentModel.focusedDocumentHandler
|
||||||
connection: root.connection
|
connection: root.connection
|
||||||
|
|
||||||
x: 1
|
x: 1
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ QQC2.Popup {
|
|||||||
|
|
||||||
required property NeoChatConnection connection
|
required property NeoChatConnection connection
|
||||||
required property var chatDocumentHandler
|
required property var chatDocumentHandler
|
||||||
|
onChatDocumentHandlerChanged: if (chatDocumentHandler) {
|
||||||
|
chatDocumentHandler.completionModel.roomListModel = RoomManager.roomListModel;
|
||||||
|
chatDocumentHandler.completionModel.userListModel = RoomManager.userListModel;
|
||||||
|
}
|
||||||
|
|
||||||
visible: completions.count > 0
|
visible: completions.count > 0
|
||||||
|
|
||||||
@@ -25,11 +29,6 @@ QQC2.Popup {
|
|||||||
root.open();
|
root.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
chatDocumentHandler.completionModel.roomListModel = RoomManager.roomListModel;
|
|
||||||
chatDocumentHandler.completionModel.userListModel = RoomManager.userListModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
function incrementIndex() {
|
function incrementIndex() {
|
||||||
completions.incrementCurrentIndex();
|
completions.incrementCurrentIndex();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
import QtCore
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls as QQC2
|
import QtQuick.Controls as QQC2
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
@@ -8,6 +9,7 @@ import QtQuick.Layouts
|
|||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
import org.kde.neochat.libneochat as LibNeoChat
|
import org.kde.neochat.libneochat as LibNeoChat
|
||||||
|
import org.kde.neochat.messagecontent as MessageContent
|
||||||
|
|
||||||
QQC2.ToolBar {
|
QQC2.ToolBar {
|
||||||
id: root
|
id: root
|
||||||
@@ -19,13 +21,23 @@ QQC2.ToolBar {
|
|||||||
|
|
||||||
property LibNeoChat.ChatBarCache chatBarCache
|
property LibNeoChat.ChatBarCache chatBarCache
|
||||||
|
|
||||||
required property LibNeoChat.ChatDocumentHandler documentHandler
|
required property MessageContent.ChatBarMessageContentModel contentModel
|
||||||
|
readonly property LibNeoChat.ChatDocumentHandler focusedDocumentHandler: contentModel.focusedDocumentHandler
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: contentModel
|
||||||
|
|
||||||
|
function onFocusRowChanged() {
|
||||||
|
console.warn("focus changed", contentModel.focusRow, contentModel.focusType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
required property real maxAvailableWidth
|
required property real maxAvailableWidth
|
||||||
|
|
||||||
readonly property real uncompressedImplicitWidth: textFormatRow.implicitWidth +
|
readonly property real uncompressedImplicitWidth: textFormatRow.implicitWidth +
|
||||||
listRow.implicitWidth +
|
listRow.implicitWidth +
|
||||||
styleButton.implicitWidth +
|
styleButton.implicitWidth +
|
||||||
|
codeButton.implicitWidth +
|
||||||
emojiButton.implicitWidth +
|
emojiButton.implicitWidth +
|
||||||
linkButton.implicitWidth +
|
linkButton.implicitWidth +
|
||||||
sendRow.implicitWidth +
|
sendRow.implicitWidth +
|
||||||
@@ -36,6 +48,7 @@ QQC2.ToolBar {
|
|||||||
readonly property real listCompressedImplicitWidth: textFormatRow.implicitWidth +
|
readonly property real listCompressedImplicitWidth: textFormatRow.implicitWidth +
|
||||||
compressedListButton.implicitWidth +
|
compressedListButton.implicitWidth +
|
||||||
styleButton.implicitWidth +
|
styleButton.implicitWidth +
|
||||||
|
codeButton.implicitWidth +
|
||||||
emojiButton.implicitWidth +
|
emojiButton.implicitWidth +
|
||||||
linkButton.implicitWidth +
|
linkButton.implicitWidth +
|
||||||
sendRow.implicitWidth +
|
sendRow.implicitWidth +
|
||||||
@@ -46,6 +59,7 @@ QQC2.ToolBar {
|
|||||||
readonly property real textFormatCompressedImplicitWidth: compressedTextFormatButton.implicitWidth +
|
readonly property real textFormatCompressedImplicitWidth: compressedTextFormatButton.implicitWidth +
|
||||||
compressedListButton.implicitWidth +
|
compressedListButton.implicitWidth +
|
||||||
styleButton.implicitWidth +
|
styleButton.implicitWidth +
|
||||||
|
codeButton.implicitWidth +
|
||||||
emojiButton.implicitWidth +
|
emojiButton.implicitWidth +
|
||||||
linkButton.implicitWidth +
|
linkButton.implicitWidth +
|
||||||
sendRow.implicitWidth +
|
sendRow.implicitWidth +
|
||||||
@@ -53,7 +67,7 @@ QQC2.ToolBar {
|
|||||||
buttonRow.spacing * 9 +
|
buttonRow.spacing * 9 +
|
||||||
3
|
3
|
||||||
|
|
||||||
signal requestPostMessage
|
signal clicked
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: buttonRow
|
id: buttonRow
|
||||||
@@ -67,11 +81,15 @@ QQC2.ToolBar {
|
|||||||
onActivated: boldButton.clicked()
|
onActivated: boldButton.clicked()
|
||||||
}
|
}
|
||||||
icon.name: "format-text-bold"
|
icon.name: "format-text-bold"
|
||||||
|
enabled: root.contentModel.focusType !== LibNeoChat.MessageComponentType.Code
|
||||||
text: i18nc("@action:button", "Bold")
|
text: i18nc("@action:button", "Bold")
|
||||||
display: QQC2.AbstractButton.IconOnly
|
display: QQC2.AbstractButton.IconOnly
|
||||||
checkable: true
|
checkable: true
|
||||||
checked: root.documentHandler.bold
|
checked: root.focusedDocumentHandler.bold
|
||||||
onClicked: root.documentHandler.bold = checked;
|
onClicked: {
|
||||||
|
root.focusedDocumentHandler.bold = checked;
|
||||||
|
root.clicked()
|
||||||
|
}
|
||||||
|
|
||||||
QQC2.ToolTip.text: text
|
QQC2.ToolTip.text: text
|
||||||
QQC2.ToolTip.visible: hovered
|
QQC2.ToolTip.visible: hovered
|
||||||
@@ -84,11 +102,15 @@ QQC2.ToolBar {
|
|||||||
onActivated: italicButton.clicked()
|
onActivated: italicButton.clicked()
|
||||||
}
|
}
|
||||||
icon.name: "format-text-italic"
|
icon.name: "format-text-italic"
|
||||||
|
enabled: root.contentModel.focusType !== LibNeoChat.MessageComponentType.Code
|
||||||
text: i18nc("@action:button", "Italic")
|
text: i18nc("@action:button", "Italic")
|
||||||
display: QQC2.AbstractButton.IconOnly
|
display: QQC2.AbstractButton.IconOnly
|
||||||
checkable: true
|
checkable: true
|
||||||
checked: root.documentHandler.italic
|
checked: root.focusedDocumentHandler.italic
|
||||||
onClicked: root.documentHandler.italic = checked;
|
onClicked: {
|
||||||
|
root.focusedDocumentHandler.italic = checked;
|
||||||
|
root.clicked()
|
||||||
|
}
|
||||||
|
|
||||||
QQC2.ToolTip.text: text
|
QQC2.ToolTip.text: text
|
||||||
QQC2.ToolTip.visible: hovered
|
QQC2.ToolTip.visible: hovered
|
||||||
@@ -101,11 +123,15 @@ QQC2.ToolBar {
|
|||||||
onActivated: underlineButton.clicked()
|
onActivated: underlineButton.clicked()
|
||||||
}
|
}
|
||||||
icon.name: "format-text-underline"
|
icon.name: "format-text-underline"
|
||||||
|
enabled: root.contentModel.focusType !== LibNeoChat.MessageComponentType.Code
|
||||||
text: i18nc("@action:button", "Underline")
|
text: i18nc("@action:button", "Underline")
|
||||||
display: QQC2.AbstractButton.IconOnly
|
display: QQC2.AbstractButton.IconOnly
|
||||||
checkable: true
|
checkable: true
|
||||||
checked: root.documentHandler.underline
|
checked: root.focusedDocumentHandler.underline
|
||||||
onClicked: root.documentHandler.underline = checked;
|
onClicked: {
|
||||||
|
root.focusedDocumentHandler.underline = checked;
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
|
|
||||||
QQC2.ToolTip.text: text
|
QQC2.ToolTip.text: text
|
||||||
QQC2.ToolTip.visible: hovered
|
QQC2.ToolTip.visible: hovered
|
||||||
@@ -113,11 +139,15 @@ QQC2.ToolBar {
|
|||||||
}
|
}
|
||||||
QQC2.ToolButton {
|
QQC2.ToolButton {
|
||||||
icon.name: "format-text-strikethrough"
|
icon.name: "format-text-strikethrough"
|
||||||
|
enabled: root.contentModel.focusType !== LibNeoChat.MessageComponentType.Code
|
||||||
text: i18nc("@action:button", "Strikethrough")
|
text: i18nc("@action:button", "Strikethrough")
|
||||||
display: QQC2.AbstractButton.IconOnly
|
display: QQC2.AbstractButton.IconOnly
|
||||||
checkable: true
|
checkable: true
|
||||||
checked: root.documentHandler.strikethrough
|
checked: root.focusedDocumentHandler.strikethrough
|
||||||
onClicked: root.documentHandler.strikethrough = checked;
|
onClicked: {
|
||||||
|
root.focusedDocumentHandler.strikethrough = checked;
|
||||||
|
root.clicked()
|
||||||
|
}
|
||||||
|
|
||||||
QQC2.ToolTip.text: text
|
QQC2.ToolTip.text: text
|
||||||
QQC2.ToolTip.visible: hovered
|
QQC2.ToolTip.visible: hovered
|
||||||
@@ -128,6 +158,7 @@ QQC2.ToolBar {
|
|||||||
id: compressedTextFormatButton
|
id: compressedTextFormatButton
|
||||||
visible: root.maxAvailableWidth < root.listCompressedImplicitWidth
|
visible: root.maxAvailableWidth < root.listCompressedImplicitWidth
|
||||||
icon.name: "dialog-text-and-font"
|
icon.name: "dialog-text-and-font"
|
||||||
|
enabled: root.contentModel.focusType !== LibNeoChat.MessageComponentType.Code
|
||||||
text: i18nc("@action:button", "Format Text")
|
text: i18nc("@action:button", "Format Text")
|
||||||
display: QQC2.AbstractButton.IconOnly
|
display: QQC2.AbstractButton.IconOnly
|
||||||
checkable: true
|
checkable: true
|
||||||
@@ -144,29 +175,41 @@ QQC2.ToolBar {
|
|||||||
icon.name: "format-text-bold"
|
icon.name: "format-text-bold"
|
||||||
text: i18nc("@action:button", "Bold")
|
text: i18nc("@action:button", "Bold")
|
||||||
checkable: true
|
checkable: true
|
||||||
checked: root.documentHandler.bold
|
checked: root.focusedDocumentHandler.bold
|
||||||
onTriggered: root.documentHandler.bold = checked;
|
onTriggered: {
|
||||||
|
root.focusedDocumentHandler.bold = checked;
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
icon.name: "format-text-italic"
|
icon.name: "format-text-italic"
|
||||||
text: i18nc("@action:button", "Italic")
|
text: i18nc("@action:button", "Italic")
|
||||||
checkable: true
|
checkable: true
|
||||||
checked: root.documentHandler.italic
|
checked: root.focusedDocumentHandler.italic
|
||||||
onTriggered: root.documentHandler.italic = checked;
|
onTriggered: {
|
||||||
|
root.focusedDocumentHandler.italic = checked;
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
icon.name: "format-text-underline"
|
icon.name: "format-text-underline"
|
||||||
text: i18nc("@action:button", "Underline")
|
text: i18nc("@action:button", "Underline")
|
||||||
checkable: true
|
checkable: true
|
||||||
checked: root.documentHandler.underline
|
checked: root.focusedDocumentHandler.underline
|
||||||
onTriggered: root.documentHandler.underline = checked;
|
onTriggered: {
|
||||||
|
root.focusedDocumentHandler.underline = checked;
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
icon.name: "format-text-strikethrough"
|
icon.name: "format-text-strikethrough"
|
||||||
text: i18nc("@action:button", "Strikethrough")
|
text: i18nc("@action:button", "Strikethrough")
|
||||||
checkable: true
|
checkable: true
|
||||||
checked: root.documentHandler.strikethrough
|
checked: root.focusedDocumentHandler.strikethrough
|
||||||
onTriggered: root.documentHandler.strikethrough = checked;
|
onTriggered: {
|
||||||
|
root.focusedDocumentHandler.strikethrough = checked;
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,12 +226,14 @@ QQC2.ToolBar {
|
|||||||
visible: root.maxAvailableWidth > root.uncompressedImplicitWidth
|
visible: root.maxAvailableWidth > root.uncompressedImplicitWidth
|
||||||
QQC2.ToolButton {
|
QQC2.ToolButton {
|
||||||
icon.name: "format-list-unordered"
|
icon.name: "format-list-unordered"
|
||||||
|
enabled: root.contentModel.focusType !== LibNeoChat.MessageComponentType.Code
|
||||||
text: i18nc("@action:button", "Unordered List")
|
text: i18nc("@action:button", "Unordered List")
|
||||||
display: QQC2.AbstractButton.IconOnly
|
display: QQC2.AbstractButton.IconOnly
|
||||||
checkable: true
|
checkable: true
|
||||||
checked: root.documentHandler.currentListStyle === 1
|
checked: root.focusedDocumentHandler.currentListStyle === 1
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.documentHandler.setListStyle(root.documentHandler.currentListStyle === 1 ? 0 : 1)
|
root.focusedDocumentHandler.setListStyle(root.focusedDocumentHandler.currentListStyle === 1 ? 0 : 1);
|
||||||
|
root.clicked();
|
||||||
}
|
}
|
||||||
|
|
||||||
QQC2.ToolTip.text: text
|
QQC2.ToolTip.text: text
|
||||||
@@ -197,11 +242,15 @@ QQC2.ToolBar {
|
|||||||
}
|
}
|
||||||
QQC2.ToolButton {
|
QQC2.ToolButton {
|
||||||
icon.name: "format-list-ordered"
|
icon.name: "format-list-ordered"
|
||||||
|
enabled: root.contentModel.focusType !== LibNeoChat.MessageComponentType.Code
|
||||||
text: i18nc("@action:button", "Ordered List")
|
text: i18nc("@action:button", "Ordered List")
|
||||||
display: QQC2.AbstractButton.IconOnly
|
display: QQC2.AbstractButton.IconOnly
|
||||||
checkable: true
|
checkable: true
|
||||||
checked: root.documentHandler.currentListStyle === 4
|
checked: root.focusedDocumentHandler.currentListStyle === 4
|
||||||
onClicked: root.documentHandler.setListStyle(root.documentHandler.currentListStyle === 4 ? 0 : 4)
|
onClicked: {
|
||||||
|
root.focusedDocumentHandler.setListStyle(root.focusedDocumentHandler.currentListStyle === 4 ? 0 : 4);
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
|
|
||||||
QQC2.ToolTip.text: text
|
QQC2.ToolTip.text: text
|
||||||
QQC2.ToolTip.visible: hovered
|
QQC2.ToolTip.visible: hovered
|
||||||
@@ -213,7 +262,8 @@ QQC2.ToolBar {
|
|||||||
text: i18nc("@action:button", "Increase List Level")
|
text: i18nc("@action:button", "Increase List Level")
|
||||||
display: QQC2.AbstractButton.IconOnly
|
display: QQC2.AbstractButton.IconOnly
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.documentHandler.indentListMore();
|
root.focusedDocumentHandler.indentListMore();
|
||||||
|
root.clicked();
|
||||||
}
|
}
|
||||||
|
|
||||||
QQC2.ToolTip.text: text
|
QQC2.ToolTip.text: text
|
||||||
@@ -226,7 +276,8 @@ QQC2.ToolBar {
|
|||||||
text: i18nc("@action:button", "Decrease List Level")
|
text: i18nc("@action:button", "Decrease List Level")
|
||||||
display: QQC2.AbstractButton.IconOnly
|
display: QQC2.AbstractButton.IconOnly
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.documentHandler.indentListLess();
|
root.focusedDocumentHandler.indentListLess();
|
||||||
|
root.clicked();
|
||||||
}
|
}
|
||||||
|
|
||||||
QQC2.ToolTip.text: text
|
QQC2.ToolTip.text: text
|
||||||
@@ -236,6 +287,7 @@ QQC2.ToolBar {
|
|||||||
}
|
}
|
||||||
QQC2.ToolButton {
|
QQC2.ToolButton {
|
||||||
id: compressedListButton
|
id: compressedListButton
|
||||||
|
enabled: root.contentModel.focusType !== LibNeoChat.MessageComponentType.Code
|
||||||
visible: root.maxAvailableWidth < root.uncompressedImplicitWidth
|
visible: root.maxAvailableWidth < root.uncompressedImplicitWidth
|
||||||
icon.name: "format-list-unordered"
|
icon.name: "format-list-unordered"
|
||||||
text: i18nc("@action:button", "List Style")
|
text: i18nc("@action:button", "List Style")
|
||||||
@@ -253,22 +305,34 @@ QQC2.ToolBar {
|
|||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
icon.name: "format-list-unordered"
|
icon.name: "format-list-unordered"
|
||||||
text: i18nc("@action:button", "Unordered List")
|
text: i18nc("@action:button", "Unordered List")
|
||||||
onTriggered: root.documentHandler.setListStyle(root.documentHandler.currentListStyle === 1 ? 0 : 1);
|
onTriggered: {
|
||||||
|
root.focusedDocumentHandler.setListStyle(root.focusedDocumentHandler.currentListStyle === 1 ? 0 : 1);
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
icon.name: "format-list-ordered"
|
icon.name: "format-list-ordered"
|
||||||
text: i18nc("@action:button", "Ordered List")
|
text: i18nc("@action:button", "Ordered List")
|
||||||
onTriggered: root.documentHandler.setListStyle(root.documentHandler.currentListStyle === 4 ? 0 : 4);
|
onTriggered: {
|
||||||
|
root.focusedDocumentHandler.setListStyle(root.focusedDocumentHandler.currentListStyle === 4 ? 0 : 4);
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
icon.name: "format-indent-more"
|
icon.name: "format-indent-more"
|
||||||
text: i18nc("@action:button", "Increase List Level")
|
text: i18nc("@action:button", "Increase List Level")
|
||||||
onTriggered: root.documentHandler.indentListMore();
|
onTriggered: {
|
||||||
|
root.focusedDocumentHandler.indentListMore();
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
icon.name: "format-indent-less"
|
icon.name: "format-indent-less"
|
||||||
text: i18nc("@action:button", "Decrease List Level")
|
text: i18nc("@action:button", "Decrease List Level")
|
||||||
onTriggered: root.documentHandler.indentListLess();
|
onTriggered: {
|
||||||
|
root.focusedDocumentHandler.indentListLess();
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,31 +357,56 @@ QQC2.ToolBar {
|
|||||||
|
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
text: i18nc("@item:inmenu no heading", "Paragraph")
|
text: i18nc("@item:inmenu no heading", "Paragraph")
|
||||||
onTriggered: root.documentHandler.setHeadingLevel(0);
|
onTriggered: root.contentModel.insertComponentAtCursor(LibNeoChat.MessageComponentType.Text);
|
||||||
}
|
}
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
text: i18nc("@item:inmenu heading level 1 (largest)", "Heading 1")
|
text: i18nc("@item:inmenu heading level 1 (largest)", "Heading 1")
|
||||||
onTriggered: root.documentHandler.setHeadingLevel(1);
|
onTriggered: {
|
||||||
|
root.focusedDocumentHandler.style = LibNeoChat.ChatDocumentHandler.Heading1;
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
text: i18nc("@item:inmenu heading level 2", "Heading 2")
|
text: i18nc("@item:inmenu heading level 2", "Heading 2")
|
||||||
onTriggered: root.documentHandler.setHeadingLevel(2);
|
onTriggered: {
|
||||||
|
root.focusedDocumentHandler.style = LibNeoChat.ChatDocumentHandler.Heading2;
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
text: i18nc("@item:inmenu heading level 3", "Heading 3")
|
text: i18nc("@item:inmenu heading level 3", "Heading 3")
|
||||||
onTriggered: root.documentHandler.setHeadingLevel(3);
|
onTriggered: {
|
||||||
|
root.focusedDocumentHandler.style = LibNeoChat.ChatDocumentHandler.Heading3;
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
text: i18nc("@item:inmenu heading level 4", "Heading 4")
|
text: i18nc("@item:inmenu heading level 4", "Heading 4")
|
||||||
onTriggered: root.documentHandler.setHeadingLevel(4);
|
onTriggered: {
|
||||||
|
root.focusedDocumentHandler.style = LibNeoChat.ChatDocumentHandler.Heading4;
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
text: i18nc("@item:inmenu heading level 5", "Heading 5")
|
text: i18nc("@item:inmenu heading level 5", "Heading 5")
|
||||||
onTriggered: root.documentHandler.setHeadingLevel(5);
|
onTriggered: {
|
||||||
|
root.focusedDocumentHandler.style = LibNeoChat.ChatDocumentHandler.Heading5;
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
text: i18nc("@item:inmenu heading level 6 (smallest)", "Heading 6")
|
text: i18nc("@item:inmenu heading level 6 (smallest)", "Heading 6")
|
||||||
onTriggered: root.documentHandler.setHeadingLevel(6);
|
onTriggered: {
|
||||||
|
root.focusedDocumentHandler.style = LibNeoChat.ChatDocumentHandler.Heading6;
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QQC2.MenuItem {
|
||||||
|
text: i18nc("@item:inmenu", "Quote")
|
||||||
|
onTriggered: {
|
||||||
|
root.contentModel.insertComponentAtCursor(LibNeoChat.MessageComponentType.Quote);
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,6 +414,20 @@ QQC2.ToolBar {
|
|||||||
QQC2.ToolTip.visible: hovered
|
QQC2.ToolTip.visible: hovered
|
||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
}
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: codeButton
|
||||||
|
icon.name: "format-text-code"
|
||||||
|
text: i18n("Code")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.contentModel.insertComponentAtCursor(LibNeoChat.MessageComponentType.Code);
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
}
|
||||||
Kirigami.Separator {
|
Kirigami.Separator {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.margins: 0
|
Layout.margins: 0
|
||||||
@@ -354,10 +457,13 @@ QQC2.ToolBar {
|
|||||||
display: QQC2.AbstractButton.IconOnly
|
display: QQC2.AbstractButton.IconOnly
|
||||||
onClicked: {
|
onClicked: {
|
||||||
let dialog = linkDialog.createObject(QQC2.Overlay.overlay, {
|
let dialog = linkDialog.createObject(QQC2.Overlay.overlay, {
|
||||||
linkText: root.documentHandler.currentLinkText(),
|
linkText: root.focusedDocumentHandler.currentLinkText(),
|
||||||
linkUrl: root.documentHandler.currentLinkUrl()
|
linkUrl: root.focusedDocumentHandler.currentLinkUrl()
|
||||||
})
|
})
|
||||||
dialog.onAccepted.connect(() => { documentHandler.updateLink(dialog.linkUrl, dialog.linkText) });
|
dialog.onAccepted.connect(() => {
|
||||||
|
root.focusedDocumentHandler.updateLink(dialog.linkUrl, dialog.linkText)
|
||||||
|
root.clicked();
|
||||||
|
});
|
||||||
dialog.open();
|
dialog.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,7 +490,7 @@ QQC2.ToolBar {
|
|||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
let dialog = (LibNeoChat.Clipboard.hasImage ? attachDialog : openFileDialog).createObject(QQC2.Overlay.overlay);
|
let dialog = (LibNeoChat.Clipboard.hasImage ? attachDialog : openFileDialog).createObject(QQC2.Overlay.overlay);
|
||||||
dialog.chosen.connect(path => root.chatBarCache.attachmentPath = path);
|
dialog.chosen.connect(path => root.contentModel.addAttachment(path));
|
||||||
dialog.open();
|
dialog.open();
|
||||||
}
|
}
|
||||||
QQC2.ToolTip.visible: hovered
|
QQC2.ToolTip.visible: hovered
|
||||||
@@ -482,9 +588,8 @@ QQC2.ToolBar {
|
|||||||
icon.name: "document-send"
|
icon.name: "document-send"
|
||||||
text: i18n("Send message")
|
text: i18n("Send message")
|
||||||
display: QQC2.AbstractButton.IconOnly
|
display: QQC2.AbstractButton.IconOnly
|
||||||
checkable: true
|
|
||||||
|
|
||||||
onClicked: root.requestPostMessage()
|
onClicked: root.contentModel.postMessage();
|
||||||
QQC2.ToolTip.visible: hovered
|
QQC2.ToolTip.visible: hovered
|
||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
QQC2.ToolTip.text: text
|
QQC2.ToolTip.text: text
|
||||||
@@ -543,7 +648,7 @@ QQC2.ToolBar {
|
|||||||
currentRoom: root.room
|
currentRoom: root.room
|
||||||
|
|
||||||
onChosen: emoji => {
|
onChosen: emoji => {
|
||||||
root.documentHandler.insertText(emoji);
|
root.focusedDocumentHandler.insertText(emoji);
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
onClosed: if (emojiButton.checked) {
|
onClosed: if (emojiButton.checked) {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ target_sources(LibNeoChat PRIVATE
|
|||||||
utils.cpp
|
utils.cpp
|
||||||
voicerecorder.cpp
|
voicerecorder.cpp
|
||||||
enums/chatbartype.h
|
enums/chatbartype.h
|
||||||
enums/messagecomponenttype.h
|
enums/messagecomponenttype.cpp
|
||||||
enums/messagetype.h
|
enums/messagetype.h
|
||||||
enums/powerlevel.cpp
|
enums/powerlevel.cpp
|
||||||
enums/pushrule.h
|
enums/pushrule.h
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <QSyntaxHighlighter>
|
#include <QSyntaxHighlighter>
|
||||||
#include <QTextBlock>
|
#include <QTextBlock>
|
||||||
#include <QTextDocument>
|
#include <QTextDocument>
|
||||||
|
#include <QTextDocumentFragment>
|
||||||
#include <QTextList>
|
#include <QTextList>
|
||||||
#include <QTextTable>
|
#include <QTextTable>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
@@ -20,17 +21,21 @@
|
|||||||
|
|
||||||
#include <Sonnet/BackgroundChecker>
|
#include <Sonnet/BackgroundChecker>
|
||||||
#include <Sonnet/Settings>
|
#include <Sonnet/Settings>
|
||||||
|
#include <qlogging.h>
|
||||||
|
#include <qnamespace.h>
|
||||||
|
#include <qtextcursor.h>
|
||||||
|
#include <sched.h>
|
||||||
|
|
||||||
#include "chatbartype.h"
|
#include "chatbartype.h"
|
||||||
#include "chatdocumenthandler_logging.h"
|
#include "chatdocumenthandler_logging.h"
|
||||||
#include "eventhandler.h"
|
#include "eventhandler.h"
|
||||||
#include "utils.h"
|
|
||||||
|
|
||||||
using namespace Qt::StringLiterals;
|
using namespace Qt::StringLiterals;
|
||||||
|
|
||||||
class SyntaxHighlighter : public QSyntaxHighlighter
|
class SyntaxHighlighter : public QSyntaxHighlighter
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
QPointer<NeoChatRoom> room;
|
||||||
QTextCharFormat mentionFormat;
|
QTextCharFormat mentionFormat;
|
||||||
QTextCharFormat errorFormat;
|
QTextCharFormat errorFormat;
|
||||||
Sonnet::BackgroundChecker checker;
|
Sonnet::BackgroundChecker checker;
|
||||||
@@ -82,11 +87,10 @@ public:
|
|||||||
if (!room) {
|
if (!room) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto chatchache = handler->chatBarCache();
|
if (!room) {
|
||||||
if (!chatchache) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto mentions = chatchache->mentions();
|
auto mentions = room->cacheForType(handler->type())->mentions();
|
||||||
mentions->erase(std::remove_if(mentions->begin(),
|
mentions->erase(std::remove_if(mentions->begin(),
|
||||||
mentions->end(),
|
mentions->end(),
|
||||||
[this](auto &mention) {
|
[this](auto &mention) {
|
||||||
@@ -127,18 +131,8 @@ ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatDocumentHandler::updateCompletion() const
|
|
||||||
{
|
|
||||||
int start = completionStartIndex();
|
|
||||||
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChatDocumentHandler::completionStartIndex() const
|
int ChatDocumentHandler::completionStartIndex() const
|
||||||
{
|
{
|
||||||
if (!m_room) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const qsizetype cursor = cursorPosition();
|
const qsizetype cursor = cursorPosition();
|
||||||
const auto &text = getText();
|
const auto &text = getText();
|
||||||
|
|
||||||
@@ -189,21 +183,108 @@ void ChatDocumentHandler::setTextItem(QQuickItem *textItem)
|
|||||||
|
|
||||||
m_highlighter->setDocument(document());
|
m_highlighter->setDocument(document());
|
||||||
if (m_textItem) {
|
if (m_textItem) {
|
||||||
connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(updateCompletion()));
|
connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(updateCursor()));
|
||||||
if (document()) {
|
if (document()) {
|
||||||
|
connect(document(), &QTextDocument::contentsChanged, this, &ChatDocumentHandler::contentsChanged);
|
||||||
connect(document(), &QTextDocument::contentsChanged, this, [this]() {
|
connect(document(), &QTextDocument::contentsChanged, this, [this]() {
|
||||||
if (m_room) {
|
if (m_room) {
|
||||||
m_room->cacheForType(m_type)->setText(getText());
|
updateCursor();
|
||||||
int start = completionStartIndex();
|
|
||||||
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
initializeChars();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Q_EMIT textItemChanged();
|
Q_EMIT textItemChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ChatDocumentHandler *ChatDocumentHandler::previousDocumentHandler() const
|
||||||
|
{
|
||||||
|
return m_previousDocumentHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::setPreviousDocumentHandler(ChatDocumentHandler *previousDocumentHandler)
|
||||||
|
{
|
||||||
|
m_previousDocumentHandler = previousDocumentHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatDocumentHandler *ChatDocumentHandler::nextDocumentHandler() const
|
||||||
|
{
|
||||||
|
return m_nextDocumentHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::setNextDocumentHandler(ChatDocumentHandler *nextDocumentHandler)
|
||||||
|
{
|
||||||
|
m_nextDocumentHandler = nextDocumentHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatDocumentHandler::fixedStartChars() const
|
||||||
|
{
|
||||||
|
return m_fixedStartChars;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::setFixedStartChars(const QString &chars)
|
||||||
|
{
|
||||||
|
if (chars == m_fixedStartChars) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_fixedStartChars = chars;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatDocumentHandler::fixedEndChars() const
|
||||||
|
{
|
||||||
|
return m_fixedEndChars;
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::setFixedEndChars(const QString &chars)
|
||||||
|
{
|
||||||
|
if (chars == m_fixedEndChars) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_fixedEndChars = chars;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatDocumentHandler::initialText() const
|
||||||
|
{
|
||||||
|
return m_initialText;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::setInitialText(const QString &text)
|
||||||
|
{
|
||||||
|
if (text == m_initialText) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_initialText = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::initializeChars()
|
||||||
|
{
|
||||||
|
const auto doc = document();
|
||||||
|
if (!doc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextCursor cursor = QTextCursor(doc);
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc->isEmpty() && !m_initialText.isEmpty()) {
|
||||||
|
cursor.insertText(m_initialText);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_fixedStartChars.isEmpty() && doc->characterAt(0) != m_fixedStartChars) {
|
||||||
|
cursor.movePosition(QTextCursor::Start);
|
||||||
|
cursor.insertText(m_fixedEndChars);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_fixedStartChars.isEmpty() && doc->characterAt(doc->characterCount()) != m_fixedStartChars) {
|
||||||
|
cursor.movePosition(QTextCursor::End);
|
||||||
|
cursor.insertText(m_fixedEndChars);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QTextDocument *ChatDocumentHandler::document() const
|
QTextDocument *ChatDocumentHandler::document() const
|
||||||
{
|
{
|
||||||
if (!m_textItem) {
|
if (!m_textItem) {
|
||||||
@@ -221,6 +302,16 @@ int ChatDocumentHandler::cursorPosition() const
|
|||||||
return m_textItem->property("cursorPosition").toInt();
|
return m_textItem->property("cursorPosition").toInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::updateCursor()
|
||||||
|
{
|
||||||
|
int start = completionStartIndex();
|
||||||
|
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
|
||||||
|
|
||||||
|
Q_EMIT formatChanged();
|
||||||
|
Q_EMIT atFirstLineChanged();
|
||||||
|
Q_EMIT atLastLineChanged();
|
||||||
|
}
|
||||||
|
|
||||||
int ChatDocumentHandler::selectionStart() const
|
int ChatDocumentHandler::selectionStart() const
|
||||||
{
|
{
|
||||||
if (!m_textItem) {
|
if (!m_textItem) {
|
||||||
@@ -248,46 +339,191 @@ void ChatDocumentHandler::setRoom(NeoChatRoom *room)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_room && m_type != ChatBarType::None) {
|
|
||||||
m_room->cacheForType(m_type)->disconnect(this);
|
|
||||||
if (!m_room->isSpace() && document() && m_type == ChatBarType::Room) {
|
|
||||||
m_room->mainCache()->setSavedText(document()->toPlainText());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_room = room;
|
m_room = room;
|
||||||
|
|
||||||
m_completionModel->setRoom(m_room);
|
m_completionModel->setRoom(m_room);
|
||||||
if (m_room && m_type != ChatBarType::None) {
|
|
||||||
connect(m_room->cacheForType(m_type), &ChatBarCache::textChanged, this, [this]() {
|
|
||||||
int start = completionStartIndex();
|
|
||||||
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
|
|
||||||
Q_EMIT fontFamilyChanged();
|
|
||||||
Q_EMIT textColorChanged();
|
|
||||||
Q_EMIT alignmentChanged();
|
|
||||||
Q_EMIT boldChanged();
|
|
||||||
Q_EMIT italicChanged();
|
|
||||||
Q_EMIT underlineChanged();
|
|
||||||
Q_EMIT checkableChanged();
|
|
||||||
Q_EMIT strikethroughChanged();
|
|
||||||
Q_EMIT fontSizeChanged();
|
|
||||||
Q_EMIT fileUrlChanged();
|
|
||||||
});
|
|
||||||
if (!m_room->isSpace() && document() && m_type == ChatBarType::Room) {
|
|
||||||
document()->setPlainText(room->mainCache()->savedText());
|
|
||||||
m_room->mainCache()->setText(room->mainCache()->savedText());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Q_EMIT roomChanged();
|
Q_EMIT roomChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatBarCache *ChatDocumentHandler::chatBarCache() const
|
bool ChatDocumentHandler::isEmpty() const
|
||||||
{
|
{
|
||||||
if (!m_room || m_type == ChatBarType::None) {
|
return htmlText().length() == 0;
|
||||||
return nullptr;
|
}
|
||||||
|
|
||||||
|
bool ChatDocumentHandler::atFirstLine() const
|
||||||
|
{
|
||||||
|
const auto cursor = textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return cursor.blockNumber() == 0 && cursor.block().layout()->lineForTextPosition(cursor.positionInBlock()).lineNumber() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatDocumentHandler::atLastLine() const
|
||||||
|
{
|
||||||
|
const auto cursor = textCursor();
|
||||||
|
const auto doc = document();
|
||||||
|
if (cursor.isNull() || !doc) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return cursor.blockNumber() == doc->blockCount() - 1
|
||||||
|
&& cursor.block().layout()->lineForTextPosition(cursor.positionInBlock()).lineNumber() == (cursor.block().layout()->lineCount() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::setCursorFromDocumentHandler(ChatDocumentHandler *previousDocumentHandler, bool infront, int defaultPosition)
|
||||||
|
{
|
||||||
|
const auto doc = document();
|
||||||
|
const auto item = textItem();
|
||||||
|
if (!doc || !item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
item->forceActiveFocus();
|
||||||
|
|
||||||
|
if (!previousDocumentHandler) {
|
||||||
|
const auto docLastBlockLayout = doc->lastBlock().layout();
|
||||||
|
item->setProperty("cursorPosition", infront ? defaultPosition : docLastBlockLayout->lineAt(docLastBlockLayout->lineCount() - 1).textStart());
|
||||||
|
item->setProperty("cursorVisible", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto previousLinePosition = previousDocumentHandler->cursorPositionInLine();
|
||||||
|
const auto newMaxLineLength = lineLength(infront ? 0 : lineCount() - 1);
|
||||||
|
item->setProperty("cursorPosition",
|
||||||
|
std::min(previousLinePosition, newMaxLineLength ? *newMaxLineLength : defaultPosition) + (infront ? 0 : doc->lastBlock().position()));
|
||||||
|
item->setProperty("cursorVisible", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChatDocumentHandler::lineCount() const
|
||||||
|
{
|
||||||
|
if (const auto doc = document()) {
|
||||||
|
return doc->lineCount();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<int> ChatDocumentHandler::lineLength(int lineNumber) const
|
||||||
|
{
|
||||||
|
const auto doc = document();
|
||||||
|
if (!doc || lineNumber < 0 || lineNumber >= doc->lineCount()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
const auto block = doc->findBlockByLineNumber(lineNumber);
|
||||||
|
const auto lineNumInBlock = lineNumber - block.firstLineNumber();
|
||||||
|
return block.layout()->lineAt(lineNumInBlock).textLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChatDocumentHandler::cursorPositionInLine() const
|
||||||
|
{
|
||||||
|
const auto cursor = textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return cursor.positionInBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextDocumentFragment ChatDocumentHandler::takeFirstBlock()
|
||||||
|
{
|
||||||
|
auto cursor = textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
cursor.beginEditBlock();
|
||||||
|
cursor.movePosition(QTextCursor::Start);
|
||||||
|
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, m_fixedStartChars.length());
|
||||||
|
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||||
|
if (document()->blockCount() <= 1) {
|
||||||
|
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, m_fixedEndChars.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto block = cursor.selection();
|
||||||
|
cursor.removeSelectedText();
|
||||||
|
cursor.endEditBlock();
|
||||||
|
if (document()->characterCount() - 1 <= (m_fixedStartChars.length() + m_fixedEndChars.length())) {
|
||||||
|
Q_EMIT removeMe(this);
|
||||||
|
}
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::fillFragments(bool &hasBefore, QTextDocumentFragment &midFragment, std::optional<QTextDocumentFragment> &afterFragment)
|
||||||
|
{
|
||||||
|
auto cursor = textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursor.blockNumber() > 0) {
|
||||||
|
hasBefore = true;
|
||||||
|
}
|
||||||
|
auto afterBlock = cursor.blockNumber() < document()->blockCount() - 1;
|
||||||
|
|
||||||
|
cursor.beginEditBlock();
|
||||||
|
cursor.movePosition(QTextCursor::StartOfBlock);
|
||||||
|
if (!hasBefore) {
|
||||||
|
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, m_fixedStartChars.length());
|
||||||
|
}
|
||||||
|
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||||
|
if (!afterBlock) {
|
||||||
|
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, m_fixedEndChars.length());
|
||||||
|
}
|
||||||
|
cursor.endEditBlock();
|
||||||
|
|
||||||
|
midFragment = cursor.selection();
|
||||||
|
if (!midFragment.isEmpty()) {
|
||||||
|
cursor.removeSelectedText();
|
||||||
|
}
|
||||||
|
cursor.deletePreviousChar();
|
||||||
|
if (afterBlock) {
|
||||||
|
cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
|
||||||
|
afterFragment = cursor.selection();
|
||||||
|
cursor.removeSelectedText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::insertFragment(const QTextDocumentFragment fragment, InsertPosition position, bool keepPosition)
|
||||||
|
{
|
||||||
|
auto cursor = textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int currentPosition;
|
||||||
|
switch (position) {
|
||||||
|
case Start:
|
||||||
|
currentPosition = 0;
|
||||||
|
break;
|
||||||
|
case End:
|
||||||
|
currentPosition = document()->characterCount() - 1;
|
||||||
|
break;
|
||||||
|
case Cursor:
|
||||||
|
currentPosition = cursor.position();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cursor.setPosition(currentPosition);
|
||||||
|
if (textFormat() && textFormat() == Qt::PlainText) {
|
||||||
|
const auto wasEmpty = isEmpty();
|
||||||
|
auto text = fragment.toPlainText();
|
||||||
|
while (text.startsWith(u"\n"_s)) {
|
||||||
|
text.removeFirst();
|
||||||
|
}
|
||||||
|
while (text.endsWith(u"\n"_s)) {
|
||||||
|
text.removeLast();
|
||||||
|
}
|
||||||
|
cursor.insertText(fragment.toPlainText());
|
||||||
|
if (wasEmpty) {
|
||||||
|
cursor.movePosition(QTextCursor::StartOfBlock);
|
||||||
|
cursor.deletePreviousChar();
|
||||||
|
cursor.movePosition(QTextCursor::EndOfBlock);
|
||||||
|
cursor.deleteChar();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cursor.insertMarkdown(trim(fragment.toMarkdown()));
|
||||||
|
}
|
||||||
|
if (keepPosition) {
|
||||||
|
cursor.setPosition(currentPosition);
|
||||||
|
}
|
||||||
|
if (textItem()) {
|
||||||
|
textItem()->setProperty("cursorPosition", cursor.position());
|
||||||
}
|
}
|
||||||
return m_room->cacheForType(m_type);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatDocumentHandler::complete(int index)
|
void ChatDocumentHandler::complete(int index)
|
||||||
@@ -436,7 +672,7 @@ void ChatDocumentHandler::setStrikethrough(bool strikethrough)
|
|||||||
QTextCharFormat format;
|
QTextCharFormat format;
|
||||||
format.setFontStrikeOut(strikethrough);
|
format.setFontStrikeOut(strikethrough);
|
||||||
mergeFormatOnWordOrSelection(format);
|
mergeFormatOnWordOrSelection(format);
|
||||||
Q_EMIT underlineChanged();
|
Q_EMIT formatChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatDocumentHandler::setTextColor(const QColor &color)
|
void ChatDocumentHandler::setTextColor(const QColor &color)
|
||||||
@@ -478,7 +714,7 @@ void ChatDocumentHandler::setBold(bool bold)
|
|||||||
QTextCharFormat format;
|
QTextCharFormat format;
|
||||||
format.setFontWeight(bold ? QFont::Bold : QFont::Normal);
|
format.setFontWeight(bold ? QFont::Bold : QFont::Normal);
|
||||||
mergeFormatOnWordOrSelection(format);
|
mergeFormatOnWordOrSelection(format);
|
||||||
Q_EMIT boldChanged();
|
Q_EMIT formatChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatDocumentHandler::italic() const
|
bool ChatDocumentHandler::italic() const
|
||||||
@@ -494,7 +730,7 @@ void ChatDocumentHandler::setItalic(bool italic)
|
|||||||
QTextCharFormat format;
|
QTextCharFormat format;
|
||||||
format.setFontItalic(italic);
|
format.setFontItalic(italic);
|
||||||
mergeFormatOnWordOrSelection(format);
|
mergeFormatOnWordOrSelection(format);
|
||||||
Q_EMIT italicChanged();
|
Q_EMIT formatChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatDocumentHandler::underline() const
|
bool ChatDocumentHandler::underline() const
|
||||||
@@ -510,7 +746,7 @@ void ChatDocumentHandler::setUnderline(bool underline)
|
|||||||
QTextCharFormat format;
|
QTextCharFormat format;
|
||||||
format.setFontUnderline(underline);
|
format.setFontUnderline(underline);
|
||||||
mergeFormatOnWordOrSelection(format);
|
mergeFormatOnWordOrSelection(format);
|
||||||
Q_EMIT underlineChanged();
|
Q_EMIT formatChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatDocumentHandler::strikethrough() const
|
bool ChatDocumentHandler::strikethrough() const
|
||||||
@@ -549,11 +785,11 @@ QColor ChatDocumentHandler::textColor() const
|
|||||||
|
|
||||||
QTextCursor ChatDocumentHandler::textCursor() const
|
QTextCursor ChatDocumentHandler::textCursor() const
|
||||||
{
|
{
|
||||||
QTextDocument *doc = document();
|
if (!document()) {
|
||||||
if (!doc)
|
|
||||||
return QTextCursor();
|
return QTextCursor();
|
||||||
|
}
|
||||||
|
|
||||||
QTextCursor cursor = QTextCursor(doc);
|
QTextCursor cursor = QTextCursor(document());
|
||||||
if (selectionStart() != selectionEnd()) {
|
if (selectionStart() != selectionEnd()) {
|
||||||
cursor.setPosition(selectionStart());
|
cursor.setPosition(selectionStart());
|
||||||
cursor.setPosition(selectionEnd(), QTextCursor::KeepAnchor);
|
cursor.setPosition(selectionEnd(), QTextCursor::KeepAnchor);
|
||||||
@@ -563,6 +799,15 @@ QTextCursor ChatDocumentHandler::textCursor() const
|
|||||||
return cursor;
|
return cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<Qt::TextFormat> ChatDocumentHandler::textFormat() const
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return static_cast<Qt::TextFormat>(m_textItem->property("textFormat").toInt());
|
||||||
|
}
|
||||||
|
|
||||||
void ChatDocumentHandler::mergeFormatOnWordOrSelection(const QTextCharFormat &format)
|
void ChatDocumentHandler::mergeFormatOnWordOrSelection(const QTextCharFormat &format)
|
||||||
{
|
{
|
||||||
QTextCursor cursor = textCursor();
|
QTextCursor cursor = textCursor();
|
||||||
@@ -747,11 +992,6 @@ void ChatDocumentHandler::regenerateColorScheme()
|
|||||||
// TODO update existing link
|
// TODO update existing link
|
||||||
}
|
}
|
||||||
|
|
||||||
int ChatDocumentHandler::currentHeadingLevel() const
|
|
||||||
{
|
|
||||||
return textCursor().blockFormat().headingLevel();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::indentListMore()
|
void ChatDocumentHandler::indentListMore()
|
||||||
{
|
{
|
||||||
m_nestedListHelper.handleOnIndentMore(textCursor());
|
m_nestedListHelper.handleOnIndentMore(textCursor());
|
||||||
@@ -768,22 +1008,46 @@ void ChatDocumentHandler::setListStyle(int styleIndex)
|
|||||||
Q_EMIT currentListStyleChanged();
|
Q_EMIT currentListStyleChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatDocumentHandler::setHeadingLevel(int level)
|
bool ChatDocumentHandler::canIndentList() const
|
||||||
{
|
{
|
||||||
const int boundedLevel = qBound(0, 6, level);
|
return m_nestedListHelper.canIndent(textCursor()) && textCursor().blockFormat().headingLevel() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatDocumentHandler::canDedentList() const
|
||||||
|
{
|
||||||
|
return m_nestedListHelper.canDedent(textCursor()) && textCursor().blockFormat().headingLevel() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChatDocumentHandler::currentListStyle() const
|
||||||
|
{
|
||||||
|
if (!textCursor().currentList()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -textCursor().currentList()->format().style();
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatDocumentHandler::Style ChatDocumentHandler::style() const
|
||||||
|
{
|
||||||
|
return static_cast<Style>(textCursor().blockFormat().headingLevel());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::setStyle(ChatDocumentHandler::Style style)
|
||||||
|
{
|
||||||
|
const int headingLevel = style <= 6 ? style : 0;
|
||||||
// Apparently, 5 is maximum for FontSizeAdjustment; otherwise level=1 and
|
// Apparently, 5 is maximum for FontSizeAdjustment; otherwise level=1 and
|
||||||
// level=2 look the same
|
// level=2 look the same
|
||||||
const int sizeAdjustment = boundedLevel > 0 ? 5 - boundedLevel : 0;
|
const int sizeAdjustment = headingLevel > 0 ? 5 - headingLevel : 0;
|
||||||
|
|
||||||
QTextCursor cursor = textCursor();
|
QTextCursor cursor = textCursor();
|
||||||
cursor.beginEditBlock();
|
cursor.beginEditBlock();
|
||||||
|
|
||||||
QTextBlockFormat blkfmt;
|
QTextBlockFormat blkfmt;
|
||||||
blkfmt.setHeadingLevel(boundedLevel);
|
blkfmt.setHeadingLevel(headingLevel);
|
||||||
cursor.mergeBlockFormat(blkfmt);
|
cursor.mergeBlockFormat(blkfmt);
|
||||||
|
|
||||||
QTextCharFormat chrfmt;
|
QTextCharFormat chrfmt;
|
||||||
chrfmt.setFontWeight(boundedLevel > 0 ? QFont::Bold : QFont::Normal);
|
chrfmt.setFontWeight(headingLevel > 0 ? QFont::Bold : QFont::Normal);
|
||||||
chrfmt.setProperty(QTextFormat::FontSizeAdjustment, sizeAdjustment);
|
chrfmt.setProperty(QTextFormat::FontSizeAdjustment, sizeAdjustment);
|
||||||
// Applying style to the current line or selection
|
// Applying style to the current line or selection
|
||||||
QTextCursor selectCursor = cursor;
|
QTextCursor selectCursor = cursor;
|
||||||
@@ -805,28 +1069,8 @@ void ChatDocumentHandler::setHeadingLevel(int level)
|
|||||||
|
|
||||||
cursor.mergeBlockCharFormat(chrfmt);
|
cursor.mergeBlockCharFormat(chrfmt);
|
||||||
cursor.endEditBlock();
|
cursor.endEditBlock();
|
||||||
// richTextComposer()->setTextCursor(cursor);
|
|
||||||
// richTextComposer()->setFocus();
|
|
||||||
// richTextComposer()->activateRichText();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatDocumentHandler::canIndentList() const
|
Q_EMIT styleChanged();
|
||||||
{
|
|
||||||
return m_nestedListHelper.canIndent(textCursor()) && textCursor().blockFormat().headingLevel() == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatDocumentHandler::canDedentList() const
|
|
||||||
{
|
|
||||||
return m_nestedListHelper.canDedent(textCursor()) && textCursor().blockFormat().headingLevel() == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChatDocumentHandler::currentListStyle() const
|
|
||||||
{
|
|
||||||
if (!textCursor().currentList()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return -textCursor().currentList()->format().style();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int ChatDocumentHandler::fontSize() const
|
int ChatDocumentHandler::fontSize() const
|
||||||
@@ -857,6 +1101,38 @@ QUrl ChatDocumentHandler::fileUrl() const
|
|||||||
return m_fileUrl;
|
return m_fileUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::deleteChar()
|
||||||
|
{
|
||||||
|
QTextCursor cursor = textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cursor.position() >= document()->characterCount() - m_fixedEndChars.length() - 1) {
|
||||||
|
if (const auto nextHandler = nextDocumentHandler()) {
|
||||||
|
insertFragment(nextHandler->takeFirstBlock(), Cursor, true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cursor.deleteChar();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::backspace()
|
||||||
|
{
|
||||||
|
QTextCursor cursor = textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cursor.position() <= m_fixedStartChars.length()) {
|
||||||
|
if (const auto previousHandler = previousDocumentHandler()) {
|
||||||
|
previousHandler->insertFragment(takeFirstBlock(), End, true);
|
||||||
|
} else {
|
||||||
|
Q_EMIT unhandledBackspaceAtBeginning(this);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cursor.deletePreviousChar();
|
||||||
|
}
|
||||||
|
|
||||||
void ChatDocumentHandler::insertText(const QString &text)
|
void ChatDocumentHandler::insertText(const QString &text)
|
||||||
{
|
{
|
||||||
textCursor().insertText(text);
|
textCursor().insertText(text);
|
||||||
@@ -872,16 +1148,24 @@ void ChatDocumentHandler::dumpHtml()
|
|||||||
qWarning() << htmlText();
|
qWarning() << htmlText();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatDocumentHandler::htmlText()
|
QString ChatDocumentHandler::htmlText() const
|
||||||
{
|
{
|
||||||
auto text = document()->toMarkdown();
|
const auto doc = document();
|
||||||
while (text.startsWith(u"\n"_s)) {
|
if (!doc) {
|
||||||
text.remove(0, 1);
|
return {};
|
||||||
}
|
}
|
||||||
while (text.endsWith(u"\n"_s)) {
|
return trim(doc->toMarkdown());
|
||||||
text.remove(text.size() - 1, text.size());
|
}
|
||||||
|
|
||||||
|
QString ChatDocumentHandler::trim(QString string) const
|
||||||
|
{
|
||||||
|
while (string.startsWith(u"\n"_s)) {
|
||||||
|
string.removeFirst();
|
||||||
}
|
}
|
||||||
return text;
|
while (string.endsWith(u"\n"_s)) {
|
||||||
|
string.removeLast();
|
||||||
|
}
|
||||||
|
return string;
|
||||||
}
|
}
|
||||||
|
|
||||||
#include "moc_chatdocumenthandler.cpp"
|
#include "moc_chatdocumenthandler.cpp"
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
#include <QTextCursor>
|
#include <QTextCursor>
|
||||||
|
#include <qnamespace.h>
|
||||||
|
#include <qtextdocumentfragment.h>
|
||||||
|
|
||||||
#include "chatbarcache.h"
|
#include "chatbarcache.h"
|
||||||
#include "enums/chatbartype.h"
|
#include "enums/chatbartype.h"
|
||||||
@@ -71,6 +73,11 @@ class ChatDocumentHandler : public QObject
|
|||||||
*/
|
*/
|
||||||
Q_PROPERTY(ChatBarType::Type type READ type WRITE setType NOTIFY typeChanged)
|
Q_PROPERTY(ChatBarType::Type type READ type WRITE setType NOTIFY typeChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The current room that the text document is being handled for.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The QML text Item the ChatDocumentHandler is handling.
|
* @brief The QML text Item the ChatDocumentHandler is handling.
|
||||||
*/
|
*/
|
||||||
@@ -85,23 +92,30 @@ class ChatDocumentHandler : public QObject
|
|||||||
Q_PROPERTY(CompletionModel *completionModel READ completionModel CONSTANT)
|
Q_PROPERTY(CompletionModel *completionModel READ completionModel CONSTANT)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The current room that the text document is being handled for.
|
* @brief Whether the cursor is cuurently on the first line.
|
||||||
*/
|
*/
|
||||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
Q_PROPERTY(bool atFirstLine READ atFirstLine NOTIFY atFirstLineChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the cursor is cuurently on the last line.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(bool atLastLine READ atLastLine NOTIFY atLastLineChanged)
|
||||||
|
|
||||||
Q_PROPERTY(QColor textColor READ textColor WRITE setTextColor NOTIFY textColorChanged)
|
Q_PROPERTY(QColor textColor READ textColor WRITE setTextColor NOTIFY textColorChanged)
|
||||||
Q_PROPERTY(QString fontFamily READ fontFamily WRITE setFontFamily NOTIFY fontFamilyChanged)
|
Q_PROPERTY(QString fontFamily READ fontFamily WRITE setFontFamily NOTIFY fontFamilyChanged)
|
||||||
Q_PROPERTY(Qt::Alignment alignment READ alignment WRITE setAlignment NOTIFY alignmentChanged)
|
Q_PROPERTY(Qt::Alignment alignment READ alignment WRITE setAlignment NOTIFY alignmentChanged)
|
||||||
|
|
||||||
Q_PROPERTY(bool bold READ bold WRITE setBold NOTIFY boldChanged)
|
Q_PROPERTY(bool bold READ bold WRITE setBold NOTIFY formatChanged)
|
||||||
Q_PROPERTY(bool italic READ italic WRITE setItalic NOTIFY italicChanged)
|
Q_PROPERTY(bool italic READ italic WRITE setItalic NOTIFY formatChanged)
|
||||||
Q_PROPERTY(bool underline READ underline WRITE setUnderline NOTIFY underlineChanged)
|
Q_PROPERTY(bool underline READ underline WRITE setUnderline NOTIFY formatChanged)
|
||||||
Q_PROPERTY(bool strikethrough READ strikethrough WRITE setStrikethrough NOTIFY strikethroughChanged)
|
Q_PROPERTY(bool strikethrough READ strikethrough WRITE setStrikethrough NOTIFY formatChanged)
|
||||||
|
|
||||||
Q_PROPERTY(bool canIndentList READ canIndentList NOTIFY cursorPositionChanged)
|
Q_PROPERTY(ChatDocumentHandler::Style style READ style WRITE setStyle NOTIFY styleChanged)
|
||||||
Q_PROPERTY(bool canDedentList READ canDedentList NOTIFY cursorPositionChanged)
|
|
||||||
|
// Q_PROPERTY(bool canIndentList READ canIndentList NOTIFY cursorPositionChanged)
|
||||||
|
// Q_PROPERTY(bool canDedentList READ canDedentList NOTIFY cursorPositionChanged)
|
||||||
Q_PROPERTY(int currentListStyle READ currentListStyle NOTIFY currentListStyleChanged)
|
Q_PROPERTY(int currentListStyle READ currentListStyle NOTIFY currentListStyleChanged)
|
||||||
Q_PROPERTY(int currentHeadingLevel READ currentHeadingLevel NOTIFY cursorPositionChanged)
|
// Q_PROPERTY(int currentHeadingLevel READ currentHeadingLevel NOTIFY cursorPositionChanged)
|
||||||
|
|
||||||
// Q_PROPERTY(bool list READ list WRITE setList NOTIFY listChanged)
|
// Q_PROPERTY(bool list READ list WRITE setList NOTIFY listChanged)
|
||||||
|
|
||||||
@@ -112,18 +126,64 @@ class ChatDocumentHandler : public QObject
|
|||||||
Q_PROPERTY(QUrl fileUrl READ fileUrl NOTIFY fileUrlChanged)
|
Q_PROPERTY(QUrl fileUrl READ fileUrl NOTIFY fileUrlChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
enum InsertPosition {
|
||||||
|
Cursor,
|
||||||
|
Start,
|
||||||
|
End,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Enum to define available styles.
|
||||||
|
*
|
||||||
|
* @note The Paragraph and Heading values are intentially fixed to match heading
|
||||||
|
* level values returned by QTextBlockFormat::headingLevel().
|
||||||
|
*
|
||||||
|
* @sa QTextBlockFormat::headingLevel()
|
||||||
|
*/
|
||||||
|
enum Style {
|
||||||
|
Paragraph = 0,
|
||||||
|
Heading1 = 1,
|
||||||
|
Heading2 = 2,
|
||||||
|
Heading3 = 3,
|
||||||
|
Heading4 = 4,
|
||||||
|
Heading5 = 5,
|
||||||
|
Heading6 = 6,
|
||||||
|
};
|
||||||
|
Q_ENUM(Style);
|
||||||
|
|
||||||
explicit ChatDocumentHandler(QObject *parent = nullptr);
|
explicit ChatDocumentHandler(QObject *parent = nullptr);
|
||||||
|
|
||||||
ChatBarType::Type type() const;
|
ChatBarType::Type type() const;
|
||||||
void setType(ChatBarType::Type type);
|
void setType(ChatBarType::Type type);
|
||||||
|
|
||||||
QQuickItem *textItem() const;
|
|
||||||
void setTextItem(QQuickItem *textItem);
|
|
||||||
|
|
||||||
[[nodiscard]] NeoChatRoom *room() const;
|
[[nodiscard]] NeoChatRoom *room() const;
|
||||||
void setRoom(NeoChatRoom *room);
|
void setRoom(NeoChatRoom *room);
|
||||||
|
|
||||||
ChatBarCache *chatBarCache() const;
|
QQuickItem *textItem() const;
|
||||||
|
void setTextItem(QQuickItem *textItem);
|
||||||
|
|
||||||
|
ChatDocumentHandler *previousDocumentHandler() const;
|
||||||
|
void setPreviousDocumentHandler(ChatDocumentHandler *previousDocumentHandler);
|
||||||
|
|
||||||
|
ChatDocumentHandler *nextDocumentHandler() const;
|
||||||
|
void setNextDocumentHandler(ChatDocumentHandler *nextDocumentHandler);
|
||||||
|
|
||||||
|
QString fixedStartChars() const;
|
||||||
|
void setFixedStartChars(const QString &chars);
|
||||||
|
QString fixedEndChars() const;
|
||||||
|
void setFixedEndChars(const QString &chars);
|
||||||
|
QString initialText() const;
|
||||||
|
void setInitialText(const QString &text);
|
||||||
|
|
||||||
|
bool isEmpty() const;
|
||||||
|
bool atFirstLine() const;
|
||||||
|
bool atLastLine() const;
|
||||||
|
void setCursorFromDocumentHandler(ChatDocumentHandler *previousDocumentHandler, bool infront, int defaultPosition = 0);
|
||||||
|
int lineCount() const;
|
||||||
|
std::optional<int> lineLength(int lineNumber) const;
|
||||||
|
int cursorPositionInLine() const;
|
||||||
|
QTextDocumentFragment takeFirstBlock();
|
||||||
|
void fillFragments(bool &hasBefore, QTextDocumentFragment &midFragment, std::optional<QTextDocumentFragment> &afterFragment);
|
||||||
|
|
||||||
Q_INVOKABLE void complete(int index);
|
Q_INVOKABLE void complete(int index);
|
||||||
|
|
||||||
@@ -145,7 +205,6 @@ public:
|
|||||||
|
|
||||||
bool bold() const;
|
bool bold() const;
|
||||||
void setBold(bool bold);
|
void setBold(bool bold);
|
||||||
|
|
||||||
bool italic() const;
|
bool italic() const;
|
||||||
void setItalic(bool italic);
|
void setItalic(bool italic);
|
||||||
|
|
||||||
@@ -159,7 +218,8 @@ public:
|
|||||||
bool canDedentList() const;
|
bool canDedentList() const;
|
||||||
int currentListStyle() const;
|
int currentListStyle() const;
|
||||||
|
|
||||||
int currentHeadingLevel() const;
|
Style style() const;
|
||||||
|
void setStyle(Style style);
|
||||||
|
|
||||||
// bool list() const;
|
// bool list() const;
|
||||||
// void setList(bool list);
|
// void setList(bool list);
|
||||||
@@ -171,7 +231,10 @@ public:
|
|||||||
QString fileType() const;
|
QString fileType() const;
|
||||||
QUrl fileUrl() const;
|
QUrl fileUrl() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE void deleteChar();
|
||||||
|
Q_INVOKABLE void backspace();
|
||||||
Q_INVOKABLE void insertText(const QString &text);
|
Q_INVOKABLE void insertText(const QString &text);
|
||||||
|
void insertFragment(const QTextDocumentFragment fragment, InsertPosition position = Cursor, bool keepPosition = false);
|
||||||
Q_INVOKABLE QString currentLinkUrl() const;
|
Q_INVOKABLE QString currentLinkUrl() const;
|
||||||
Q_INVOKABLE QString currentLinkText() const;
|
Q_INVOKABLE QString currentLinkText() const;
|
||||||
Q_INVOKABLE void updateLink(const QString &linkUrl, const QString &linkText);
|
Q_INVOKABLE void updateLink(const QString &linkUrl, const QString &linkText);
|
||||||
@@ -182,16 +245,18 @@ public:
|
|||||||
Q_INVOKABLE void indentListMore();
|
Q_INVOKABLE void indentListMore();
|
||||||
|
|
||||||
Q_INVOKABLE void setListStyle(int styleIndex);
|
Q_INVOKABLE void setListStyle(int styleIndex);
|
||||||
Q_INVOKABLE void setHeadingLevel(int level);
|
|
||||||
|
|
||||||
Q_INVOKABLE void dumpHtml();
|
Q_INVOKABLE void dumpHtml();
|
||||||
Q_INVOKABLE QString htmlText();
|
Q_INVOKABLE QString htmlText() const;
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void typeChanged();
|
void typeChanged();
|
||||||
void textItemChanged();
|
void textItemChanged();
|
||||||
void roomChanged();
|
void roomChanged();
|
||||||
|
|
||||||
|
void atFirstLineChanged();
|
||||||
|
void atLastLineChanged();
|
||||||
|
|
||||||
void fontFamilyChanged();
|
void fontFamilyChanged();
|
||||||
void textColorChanged();
|
void textColorChanged();
|
||||||
void alignmentChanged();
|
void alignmentChanged();
|
||||||
@@ -205,11 +270,27 @@ Q_SIGNALS:
|
|||||||
void fontSizeChanged();
|
void fontSizeChanged();
|
||||||
void fileUrlChanged();
|
void fileUrlChanged();
|
||||||
|
|
||||||
|
void formatChanged();
|
||||||
|
void styleChanged();
|
||||||
|
|
||||||
|
void contentsChanged();
|
||||||
|
|
||||||
|
void unhandledBackspaceAtBeginning(ChatDocumentHandler *self);
|
||||||
|
void removeMe(ChatDocumentHandler *self);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ChatBarType::Type m_type = ChatBarType::None;
|
ChatBarType::Type m_type = ChatBarType::None;
|
||||||
QPointer<QQuickItem> m_textItem;
|
QPointer<QQuickItem> m_textItem;
|
||||||
QTextDocument *document() const;
|
QTextDocument *document() const;
|
||||||
|
|
||||||
|
QPointer<ChatDocumentHandler> m_previousDocumentHandler;
|
||||||
|
QPointer<ChatDocumentHandler> m_nextDocumentHandler;
|
||||||
|
|
||||||
|
QString m_fixedStartChars = {};
|
||||||
|
QString m_fixedEndChars = {};
|
||||||
|
QString m_initialText = {};
|
||||||
|
void initializeChars();
|
||||||
|
|
||||||
int completionStartIndex() const;
|
int completionStartIndex() const;
|
||||||
|
|
||||||
QPointer<NeoChatRoom> m_room;
|
QPointer<NeoChatRoom> m_room;
|
||||||
@@ -226,6 +307,7 @@ private:
|
|||||||
|
|
||||||
CompletionModel *m_completionModel = nullptr;
|
CompletionModel *m_completionModel = nullptr;
|
||||||
QTextCursor textCursor() const;
|
QTextCursor textCursor() const;
|
||||||
|
std::optional<Qt::TextFormat> textFormat() const;
|
||||||
void mergeFormatOnWordOrSelection(const QTextCharFormat &format);
|
void mergeFormatOnWordOrSelection(const QTextCharFormat &format);
|
||||||
void selectLinkText(QTextCursor *cursor) const;
|
void selectLinkText(QTextCursor *cursor) const;
|
||||||
NestedListHelper m_nestedListHelper;
|
NestedListHelper m_nestedListHelper;
|
||||||
@@ -233,4 +315,9 @@ private:
|
|||||||
QColor mLinkColor;
|
QColor mLinkColor;
|
||||||
void regenerateColorScheme();
|
void regenerateColorScheme();
|
||||||
QUrl m_fileUrl;
|
QUrl m_fileUrl;
|
||||||
|
|
||||||
|
QString trim(QString string) const;
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void updateCursor();
|
||||||
};
|
};
|
||||||
|
|||||||
132
src/libneochat/enums/messagecomponenttype.cpp
Normal file
132
src/libneochat/enums/messagecomponenttype.cpp
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#include "messagecomponenttype.h"
|
||||||
|
|
||||||
|
#include <QMimeDatabase>
|
||||||
|
|
||||||
|
#include <Quotient/events/encryptedevent.h>
|
||||||
|
#include <Quotient/events/roommessageevent.h>
|
||||||
|
#include <Quotient/events/stickerevent.h>
|
||||||
|
|
||||||
|
#include "events/pollevent.h"
|
||||||
|
|
||||||
|
const QList<MessageComponentType::Type> MessageComponentType::textTypes = {
|
||||||
|
Text,
|
||||||
|
Code,
|
||||||
|
Quote,
|
||||||
|
};
|
||||||
|
|
||||||
|
const QList<MessageComponentType::Type> MessageComponentType::fileTypes = {
|
||||||
|
File,
|
||||||
|
Image,
|
||||||
|
Video,
|
||||||
|
Audio,
|
||||||
|
};
|
||||||
|
|
||||||
|
MessageComponentType::Type MessageComponentType::typeForEvent(const Quotient::RoomEvent &event, bool isInReply)
|
||||||
|
{
|
||||||
|
using namespace Quotient;
|
||||||
|
|
||||||
|
if (event.isRedacted()) {
|
||||||
|
return MessageComponentType::Text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto e = eventCast<const RoomMessageEvent>(&event)) {
|
||||||
|
if (e->rawMsgtype() == u"m.key.verification.request"_s) {
|
||||||
|
return MessageComponentType::Verification;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (e->msgtype()) {
|
||||||
|
case MessageEventType::Emote:
|
||||||
|
return MessageComponentType::Text;
|
||||||
|
case MessageEventType::Notice:
|
||||||
|
return MessageComponentType::Text;
|
||||||
|
case MessageEventType::Image:
|
||||||
|
return MessageComponentType::Image;
|
||||||
|
case MessageEventType::Audio:
|
||||||
|
return MessageComponentType::Audio;
|
||||||
|
case MessageEventType::Video:
|
||||||
|
return MessageComponentType::Video;
|
||||||
|
case MessageEventType::Location:
|
||||||
|
return MessageComponentType::Location;
|
||||||
|
case MessageEventType::File:
|
||||||
|
return MessageComponentType::File;
|
||||||
|
default:
|
||||||
|
return MessageComponentType::Text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (is<const StickerEvent>(event)) {
|
||||||
|
return MessageComponentType::Image;
|
||||||
|
}
|
||||||
|
if (event.isStateEvent()) {
|
||||||
|
if (event.matrixType() == u"org.matrix.msc3672.beacon_info"_s) {
|
||||||
|
return MessageComponentType::LiveLocation;
|
||||||
|
}
|
||||||
|
// In the (unlikely) case that this is a reply to a state event, we do want to show something
|
||||||
|
return isInReply ? MessageComponentType::Text : MessageComponentType::Other;
|
||||||
|
}
|
||||||
|
if (is<const EncryptedEvent>(event)) {
|
||||||
|
return MessageComponentType::Encrypted;
|
||||||
|
}
|
||||||
|
if (is<PollStartEvent>(event)) {
|
||||||
|
const auto pollEvent = eventCast<const PollStartEvent>(&event);
|
||||||
|
if (pollEvent->isRedacted()) {
|
||||||
|
return MessageComponentType::Text;
|
||||||
|
}
|
||||||
|
return MessageComponentType::Poll;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the (unlikely) case that this is a reply to an unusual event, we do want to show something
|
||||||
|
return isInReply ? MessageComponentType::Text : MessageComponentType::Other;
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageComponentType::Type MessageComponentType::typeForString(const QString &string)
|
||||||
|
{
|
||||||
|
if (string.isEmpty()) {
|
||||||
|
return Text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.startsWith(u'>')) {
|
||||||
|
return Quote;
|
||||||
|
}
|
||||||
|
if (string.startsWith(u"```"_s) && string.endsWith(u"```"_s)) {
|
||||||
|
return Code;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Text;
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageComponentType::Type MessageComponentType::typeForTag(const QString &tag)
|
||||||
|
{
|
||||||
|
if (tag == u"pre"_s || tag == u"pre"_s) {
|
||||||
|
return Code;
|
||||||
|
}
|
||||||
|
if (tag == u"blockquote"_s) {
|
||||||
|
return Quote;
|
||||||
|
}
|
||||||
|
return Text;
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageComponentType::Type MessageComponentType::typeForPath(const QUrl &path)
|
||||||
|
{
|
||||||
|
auto mime = QMimeDatabase().mimeTypeForUrl(path);
|
||||||
|
if (mime.name().startsWith("image/"_L1)) {
|
||||||
|
return Image;
|
||||||
|
} else if (mime.name().startsWith("audio/"_L1)) {
|
||||||
|
return Audio;
|
||||||
|
} else if (mime.name().startsWith("video/"_L1)) {
|
||||||
|
return Video;
|
||||||
|
}
|
||||||
|
return File;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MessageComponentType::isTextType(const MessageComponentType::Type &type)
|
||||||
|
{
|
||||||
|
return textTypes.contains(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MessageComponentType::isFileType(const MessageComponentType::Type &type)
|
||||||
|
{
|
||||||
|
return fileTypes.contains(type);
|
||||||
|
}
|
||||||
@@ -6,12 +6,10 @@
|
|||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
|
|
||||||
#include <Quotient/events/encryptedevent.h>
|
namespace Quotient
|
||||||
#include <Quotient/events/roomevent.h>
|
{
|
||||||
#include <Quotient/events/roommessageevent.h>
|
class RoomEvent;
|
||||||
#include <Quotient/events/stickerevent.h>
|
}
|
||||||
|
|
||||||
#include "events/pollevent.h"
|
|
||||||
|
|
||||||
using namespace Qt::StringLiterals;
|
using namespace Qt::StringLiterals;
|
||||||
|
|
||||||
@@ -74,62 +72,14 @@ public:
|
|||||||
*
|
*
|
||||||
* @sa Type
|
* @sa Type
|
||||||
*/
|
*/
|
||||||
static Type typeForEvent(const Quotient::RoomEvent &event, bool isInReply = false)
|
static Type typeForEvent(const Quotient::RoomEvent &event, bool isInReply = false);
|
||||||
{
|
|
||||||
using namespace Quotient;
|
|
||||||
|
|
||||||
if (event.isRedacted()) {
|
/**
|
||||||
return MessageComponentType::Text;
|
* @brief Return MessageComponentType for the given string.
|
||||||
}
|
*
|
||||||
|
* @sa Type
|
||||||
if (const auto e = eventCast<const RoomMessageEvent>(&event)) {
|
*/
|
||||||
if (e->rawMsgtype() == u"m.key.verification.request"_s) {
|
static Type typeForString(const QString &string);
|
||||||
return MessageComponentType::Verification;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (e->msgtype()) {
|
|
||||||
case MessageEventType::Emote:
|
|
||||||
return MessageComponentType::Text;
|
|
||||||
case MessageEventType::Notice:
|
|
||||||
return MessageComponentType::Text;
|
|
||||||
case MessageEventType::Image:
|
|
||||||
return MessageComponentType::Image;
|
|
||||||
case MessageEventType::Audio:
|
|
||||||
return MessageComponentType::Audio;
|
|
||||||
case MessageEventType::Video:
|
|
||||||
return MessageComponentType::Video;
|
|
||||||
case MessageEventType::Location:
|
|
||||||
return MessageComponentType::Location;
|
|
||||||
case MessageEventType::File:
|
|
||||||
return MessageComponentType::File;
|
|
||||||
default:
|
|
||||||
return MessageComponentType::Text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (is<const StickerEvent>(event)) {
|
|
||||||
return MessageComponentType::Image;
|
|
||||||
}
|
|
||||||
if (event.isStateEvent()) {
|
|
||||||
if (event.matrixType() == u"org.matrix.msc3672.beacon_info"_s) {
|
|
||||||
return MessageComponentType::LiveLocation;
|
|
||||||
}
|
|
||||||
// In the (unlikely) case that this is a reply to a state event, we do want to show something
|
|
||||||
return isInReply ? MessageComponentType::Text : MessageComponentType::Other;
|
|
||||||
}
|
|
||||||
if (is<const EncryptedEvent>(event)) {
|
|
||||||
return MessageComponentType::Encrypted;
|
|
||||||
}
|
|
||||||
if (is<PollStartEvent>(event)) {
|
|
||||||
const auto pollEvent = eventCast<const PollStartEvent>(&event);
|
|
||||||
if (pollEvent->isRedacted()) {
|
|
||||||
return MessageComponentType::Text;
|
|
||||||
}
|
|
||||||
return MessageComponentType::Poll;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In the (unlikely) case that this is a reply to an unusual event, we do want to show something
|
|
||||||
return isInReply ? MessageComponentType::Text : MessageComponentType::Other;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Return MessageComponentType for the given html tag.
|
* @brief Return MessageComponentType for the given html tag.
|
||||||
@@ -138,14 +88,30 @@ public:
|
|||||||
*
|
*
|
||||||
* @sa Type
|
* @sa Type
|
||||||
*/
|
*/
|
||||||
static Type typeForTag(const QString &tag)
|
static Type typeForTag(const QString &tag);
|
||||||
{
|
|
||||||
if (tag == u"pre"_s || tag == u"pre"_s) {
|
/**
|
||||||
return Code;
|
* @brief Return MessageComponentType for the file with the given path.
|
||||||
}
|
*
|
||||||
if (tag == u"blockquote"_s) {
|
* @sa Type
|
||||||
return Quote;
|
*/
|
||||||
}
|
static Type typeForPath(const QUrl &path);
|
||||||
return Text;
|
|
||||||
}
|
/**
|
||||||
|
* @brief Return if the given MessageComponentType is a text type.
|
||||||
|
*
|
||||||
|
* @sa Type
|
||||||
|
*/
|
||||||
|
static bool isTextType(const MessageComponentType::Type &type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return if the given MessageComponentType is a file type.
|
||||||
|
*
|
||||||
|
* @sa Type
|
||||||
|
*/
|
||||||
|
static bool isFileType(const MessageComponentType::Type &type);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static const QList<MessageComponentType::Type> textTypes;
|
||||||
|
static const QList<MessageComponentType::Type> fileTypes;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
struct MessageComponent {
|
struct MessageComponent {
|
||||||
MessageComponentType::Type type = MessageComponentType::Other;
|
MessageComponentType::Type type = MessageComponentType::Other;
|
||||||
QString display;
|
QString display;
|
||||||
QVariantMap attributes;
|
QVariantMap attributes = {};
|
||||||
|
|
||||||
bool operator==(const MessageComponent &right) const
|
bool operator==(const MessageComponent &right) const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ ecm_add_qml_module(MessageContent GENERATE_PLUGIN_SOURCE
|
|||||||
contentprovider.cpp
|
contentprovider.cpp
|
||||||
mediasizehelper.cpp
|
mediasizehelper.cpp
|
||||||
pollhandler.cpp
|
pollhandler.cpp
|
||||||
|
models/chatbarmessagecontentmodel.cpp
|
||||||
models/itinerarymodel.cpp
|
models/itinerarymodel.cpp
|
||||||
models/linemodel.cpp
|
models/linemodel.cpp
|
||||||
models/messagecontentmodel.cpp
|
models/messagecontentmodel.cpp
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ import org.kde.neochat
|
|||||||
QQC2.Control {
|
QQC2.Control {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The index of the delegate in the model.
|
||||||
|
*/
|
||||||
|
required property int index
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The matrix ID of the message event.
|
* @brief The matrix ID of the message event.
|
||||||
*/
|
*/
|
||||||
@@ -37,10 +42,29 @@ QQC2.Control {
|
|||||||
*/
|
*/
|
||||||
required property string display
|
required property string display
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the component should be editable.
|
||||||
|
*/
|
||||||
|
required property bool editable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The attributes of the component.
|
* @brief The attributes of the component.
|
||||||
*/
|
*/
|
||||||
required property var componentAttributes
|
required property var componentAttributes
|
||||||
|
readonly property ChatDocumentHandler chatDocumentHandler: componentAttributes?.chatDocumentHandler ?? null
|
||||||
|
onChatDocumentHandlerChanged: if (chatDocumentHandler) {
|
||||||
|
chatDocumentHandler.type = ChatBarType.Room;
|
||||||
|
chatDocumentHandler.room = root.Message.room;
|
||||||
|
chatDocumentHandler.textItem = codeText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the component is currently focussed.
|
||||||
|
*/
|
||||||
|
required property bool currentFocus
|
||||||
|
onCurrentFocusChanged: if (currentFocus && !codeText.focus) {
|
||||||
|
codeText.forceActiveFocus();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The user selected text has changed.
|
* @brief The user selected text has changed.
|
||||||
@@ -52,6 +76,8 @@ QQC2.Control {
|
|||||||
Layout.maximumWidth: Message.maxContentWidth
|
Layout.maximumWidth: Message.maxContentWidth
|
||||||
Layout.maximumHeight: Kirigami.Units.gridUnit * 20
|
Layout.maximumHeight: Kirigami.Units.gridUnit * 20
|
||||||
|
|
||||||
|
width: ListView.view?.width ?? -1
|
||||||
|
|
||||||
topPadding: 0
|
topPadding: 0
|
||||||
bottomPadding: 0
|
bottomPadding: 0
|
||||||
leftPadding: 0
|
leftPadding: 0
|
||||||
@@ -66,12 +92,44 @@ QQC2.Control {
|
|||||||
|
|
||||||
QQC2.TextArea {
|
QQC2.TextArea {
|
||||||
id: codeText
|
id: codeText
|
||||||
|
|
||||||
|
Keys.onUpPressed: (event) => {
|
||||||
|
event.accepted = false;
|
||||||
|
if (root.chatDocumentHandler.atFirstLine) {
|
||||||
|
Message.contentModel.focusRow = root.index - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Keys.onDownPressed: (event) => {
|
||||||
|
event.accepted = false;
|
||||||
|
if (root.chatDocumentHandler.atLastLine) {
|
||||||
|
Message.contentModel.focusRow = root.index + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onDeletePressed: (event) => {
|
||||||
|
event.accepted = true;
|
||||||
|
root.chatDocumentHandler.deleteChar();
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: (event) => {
|
||||||
|
if (event.key == Qt.Key_Backspace && cursorPosition == 0) {
|
||||||
|
event.accepted = true;
|
||||||
|
root.chatDocumentHandler.backspace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.accepted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocusChanged: if (focus && !root.currentFocus) {
|
||||||
|
Message.contentModel.setFocusRow(root.index, true)
|
||||||
|
}
|
||||||
|
|
||||||
topPadding: Kirigami.Units.smallSpacing
|
topPadding: Kirigami.Units.smallSpacing
|
||||||
bottomPadding: Kirigami.Units.smallSpacing
|
bottomPadding: Kirigami.Units.smallSpacing
|
||||||
leftPadding: lineNumberColumn.width + lineNumberColumn.anchors.leftMargin + Kirigami.Units.smallSpacing * 2
|
leftPadding: lineNumberColumn.width + lineNumberColumn.anchors.leftMargin + Kirigami.Units.smallSpacing * 2
|
||||||
|
|
||||||
text: root.display
|
text: root.editable ? "" : root.display
|
||||||
readOnly: true
|
readOnly: !root.editable
|
||||||
textFormat: TextEdit.PlainText
|
textFormat: TextEdit.PlainText
|
||||||
wrapMode: TextEdit.Wrap
|
wrapMode: TextEdit.Wrap
|
||||||
color: Kirigami.Theme.textColor
|
color: Kirigami.Theme.textColor
|
||||||
@@ -149,7 +207,7 @@ QQC2.Control {
|
|||||||
right: parent.right
|
right: parent.right
|
||||||
rightMargin: (codeScrollView.QQC2.ScrollBar.vertical.visible ? codeScrollView.QQC2.ScrollBar.vertical.width : 0) + Kirigami.Units.smallSpacing
|
rightMargin: (codeScrollView.QQC2.ScrollBar.vertical.visible ? codeScrollView.QQC2.ScrollBar.vertical.width : 0) + Kirigami.Units.smallSpacing
|
||||||
}
|
}
|
||||||
visible: root.hovered
|
visible: root.hovered && !root.editable
|
||||||
spacing: Kirigami.Units.mediumSpacing
|
spacing: Kirigami.Units.mediumSpacing
|
||||||
|
|
||||||
QQC2.Button {
|
QQC2.Button {
|
||||||
|
|||||||
@@ -9,9 +9,14 @@ import org.kde.kirigami as Kirigami
|
|||||||
|
|
||||||
import org.kde.neochat
|
import org.kde.neochat
|
||||||
|
|
||||||
QQC2.Control {
|
QQC2.TextArea {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The index of the delegate in the model.
|
||||||
|
*/
|
||||||
|
required property int index
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The matrix ID of the message event.
|
* @brief The matrix ID of the message event.
|
||||||
*/
|
*/
|
||||||
@@ -31,50 +36,117 @@ QQC2.Control {
|
|||||||
*/
|
*/
|
||||||
required property string display
|
required property string display
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the component should be editable.
|
||||||
|
*/
|
||||||
|
required property bool editable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The attributes of the component.
|
||||||
|
*/
|
||||||
|
required property var componentAttributes
|
||||||
|
readonly property ChatDocumentHandler chatDocumentHandler: componentAttributes?.chatDocumentHandler ?? null
|
||||||
|
onChatDocumentHandlerChanged: if (chatDocumentHandler) {
|
||||||
|
chatDocumentHandler.type = ChatBarType.Room;
|
||||||
|
chatDocumentHandler.room = root.Message.room;
|
||||||
|
chatDocumentHandler.textItem = root;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the component is currently focussed.
|
||||||
|
*/
|
||||||
|
required property bool currentFocus
|
||||||
|
onCurrentFocusChanged: if (currentFocus && !focus) {
|
||||||
|
forceActiveFocus();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The user selected text has changed.
|
* @brief The user selected text has changed.
|
||||||
*/
|
*/
|
||||||
signal selectedTextChanged(string selectedText)
|
signal selectedTextChanged(string selectedText)
|
||||||
|
|
||||||
|
Keys.onUpPressed: (event) => {
|
||||||
|
event.accepted = false;
|
||||||
|
if (root.chatDocumentHandler.atFirstLine) {
|
||||||
|
Message.contentModel.focusRow = root.index - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Keys.onDownPressed: (event) => {
|
||||||
|
event.accepted = false;
|
||||||
|
if (root.chatDocumentHandler.atLastLine) {
|
||||||
|
Message.contentModel.focusRow = root.index + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Keys.onLeftPressed: (event) => {
|
||||||
|
if (cursorPosition == 1) {
|
||||||
|
event.accepted = true;
|
||||||
|
} else {
|
||||||
|
event.accepted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Keys.onRightPressed: (event) => {
|
||||||
|
if (cursorPosition == (length - 1)) {
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.accepted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onDeletePressed: (event) => {
|
||||||
|
event.accepted = true;
|
||||||
|
chatDocumentHandler.deleteChar();
|
||||||
|
}
|
||||||
|
Keys.onPressed: (event) => {
|
||||||
|
if (event.key == Qt.Key_Backspace) {
|
||||||
|
event.accepted = true;
|
||||||
|
chatDocumentHandler.backspace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.accepted = false;
|
||||||
|
}
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.maximumWidth: Message.maxContentWidth
|
Layout.maximumWidth: Message.maxContentWidth
|
||||||
|
|
||||||
topPadding: 0
|
topPadding: Kirigami.Units.smallSpacing
|
||||||
bottomPadding: 0
|
bottomPadding: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
contentItem: TextEdit {
|
text: root.editable ? "" : root.display
|
||||||
id: quoteText
|
selectByMouse: true
|
||||||
Layout.fillWidth: true
|
persistentSelection: true
|
||||||
topPadding: Kirigami.Units.smallSpacing
|
readOnly: !root.editable
|
||||||
bottomPadding: Kirigami.Units.smallSpacing
|
textFormat: TextEdit.RichText
|
||||||
|
wrapMode: TextEdit.Wrap
|
||||||
|
color: Kirigami.Theme.textColor
|
||||||
|
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
||||||
|
selectionColor: Kirigami.Theme.highlightColor
|
||||||
|
font.italic: true
|
||||||
|
font.pointSize: Kirigami.Theme.defaultFont.pointSize * NeoChatConfig.fontScale
|
||||||
|
|
||||||
text: root.display
|
onSelectedTextChanged: root.selectedTextChanged(selectedText)
|
||||||
readOnly: true
|
|
||||||
textFormat: TextEdit.RichText
|
|
||||||
wrapMode: TextEdit.Wrap
|
|
||||||
color: Kirigami.Theme.textColor
|
|
||||||
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
|
||||||
selectionColor: Kirigami.Theme.highlightColor
|
|
||||||
|
|
||||||
font.italic: true
|
onFocusChanged: if (focus && !currentFocus) {
|
||||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * NeoChatConfig.fontScale
|
Message.contentModel.setFocusRow(root.index, true)
|
||||||
|
}
|
||||||
|
|
||||||
onSelectedTextChanged: root.selectedTextChanged(selectedText)
|
onCursorPositionChanged: if (cursorPosition == 0) {
|
||||||
|
cursorPosition = 1;
|
||||||
|
} else if (cursorPosition == length) {
|
||||||
|
cursorPosition = length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
TapHandler {
|
TapHandler {
|
||||||
enabled: !quoteText.hoveredLink
|
enabled: !root.hoveredLink
|
||||||
acceptedDevices: PointerDevice.TouchScreen
|
acceptedDevices: PointerDevice.TouchScreen
|
||||||
acceptedButtons: Qt.LeftButton
|
acceptedButtons: Qt.LeftButton
|
||||||
onLongPressed: {
|
onLongPressed: {
|
||||||
const event = root.Message.room.findEvent(root.eventId);
|
const event = root.Message.room.findEvent(root.eventId);
|
||||||
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
|
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: Kirigami.Theme.backgroundColor
|
color: Kirigami.Theme.alternateBackgroundColor
|
||||||
radius: Kirigami.Units.cornerRadius
|
radius: Kirigami.Units.cornerRadius
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ RowLayout {
|
|||||||
*/
|
*/
|
||||||
required property var replyContentModel
|
required property var replyContentModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the component should be editable.
|
||||||
|
*/
|
||||||
|
required property bool editable
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
spacing: Kirigami.Units.largeSpacing
|
spacing: Kirigami.Units.largeSpacing
|
||||||
@@ -55,6 +60,21 @@ RowLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
QQC2.Button {
|
||||||
|
id: cancelButton
|
||||||
|
|
||||||
|
anchors.top: root.top
|
||||||
|
anchors.right: root.right
|
||||||
|
|
||||||
|
visible: root.editable
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
text: i18nc("@action:button", "Cancel reply")
|
||||||
|
icon.name: "dialog-close"
|
||||||
|
onClicked: root.Message.room.mainCache.replyId = ""
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
HoverHandler {
|
HoverHandler {
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,10 +40,29 @@ TextEdit {
|
|||||||
*/
|
*/
|
||||||
required property string display
|
required property string display
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the component should be editable.
|
||||||
|
*/
|
||||||
|
required property bool editable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The attributes of the component.
|
* @brief The attributes of the component.
|
||||||
*/
|
*/
|
||||||
required property var componentAttributes
|
required property var componentAttributes
|
||||||
|
readonly property ChatDocumentHandler chatDocumentHandler: componentAttributes?.chatDocumentHandler ?? null
|
||||||
|
onChatDocumentHandlerChanged: if (chatDocumentHandler) {
|
||||||
|
chatDocumentHandler.type = ChatBarType.Room;
|
||||||
|
chatDocumentHandler.room = root.Message.room;
|
||||||
|
chatDocumentHandler.textItem = root;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the component is currently focussed.
|
||||||
|
*/
|
||||||
|
required property bool currentFocus
|
||||||
|
onCurrentFocusChanged: if (currentFocus && !focus) {
|
||||||
|
forceActiveFocus();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Whether the message contains a spoiler
|
* @brief Whether the message contains a spoiler
|
||||||
@@ -56,12 +75,46 @@ TextEdit {
|
|||||||
property bool isReply: false
|
property bool isReply: false
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.maximumWidth: Message.maxContentWidth
|
Layout.maximumWidth: Message.maxContentWidth
|
||||||
|
|
||||||
|
Keys.onUpPressed: (event) => {
|
||||||
|
event.accepted = false;
|
||||||
|
if (chatDocumentHandler.atFirstLine) {
|
||||||
|
Message.contentModel.focusRow = root.index - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Keys.onDownPressed: (event) => {
|
||||||
|
event.accepted = false;
|
||||||
|
if (chatDocumentHandler.atLastLine) {
|
||||||
|
Message.contentModel.focusRow = root.index + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onDeletePressed: (event) => {
|
||||||
|
event.accepted = true;
|
||||||
|
chatDocumentHandler.deleteChar();
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: (event) => {
|
||||||
|
if (event.key == Qt.Key_Backspace && cursorPosition == 0) {
|
||||||
|
event.accepted = true;
|
||||||
|
chatDocumentHandler.backspace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.accepted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocusChanged: if (focus && !root.currentFocus) {
|
||||||
|
Message.contentModel.setFocusRow(root.index, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView.onReused: Qt.binding(() => !hasSpoiler.test(display))
|
||||||
|
|
||||||
|
leftPadding: Kirigami.Units.smallSpacing
|
||||||
|
rightPadding: Kirigami.Units.smallSpacing
|
||||||
persistentSelection: true
|
persistentSelection: true
|
||||||
|
|
||||||
text: display
|
text: root.editable ? "" : display
|
||||||
|
|
||||||
color: Kirigami.Theme.textColor
|
color: Kirigami.Theme.textColor
|
||||||
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
||||||
@@ -73,7 +126,7 @@ TextEdit {
|
|||||||
family: QmlUtils.isEmoji(display) ? 'emoji' : Kirigami.Theme.defaultFont.family
|
family: QmlUtils.isEmoji(display) ? 'emoji' : Kirigami.Theme.defaultFont.family
|
||||||
}
|
}
|
||||||
selectByMouse: !Kirigami.Settings.isMobile
|
selectByMouse: !Kirigami.Settings.isMobile
|
||||||
readOnly: true
|
readOnly: !root.editable
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
textFormat: Text.RichText
|
textFormat: Text.RichText
|
||||||
|
|
||||||
|
|||||||
490
src/messagecontent/models/chatbarmessagecontentmodel.cpp
Normal file
490
src/messagecontent/models/chatbarmessagecontentmodel.cpp
Normal file
@@ -0,0 +1,490 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#include "chatbarmessagecontentmodel.h"
|
||||||
|
|
||||||
|
#include <QTextDocumentFragment>
|
||||||
|
|
||||||
|
#include "chatbarcache.h"
|
||||||
|
#include "chatdocumenthandler.h"
|
||||||
|
#include "enums/chatbartype.h"
|
||||||
|
#include "enums/messagecomponenttype.h"
|
||||||
|
#include "messagecontentmodel.h"
|
||||||
|
|
||||||
|
ChatBarMessageContentModel::ChatBarMessageContentModel(QObject *parent)
|
||||||
|
: MessageContentModel(parent)
|
||||||
|
{
|
||||||
|
m_editableActive = true;
|
||||||
|
initializeModel();
|
||||||
|
|
||||||
|
connect(this, &ChatBarMessageContentModel::roomChanged, this, [this]() {
|
||||||
|
if (m_type == ChatBarType::None || !m_room) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(m_room->cacheForType(m_type), &ChatBarCache::relationIdChanged, this, &ChatBarMessageContentModel::updateReplyModel);
|
||||||
|
clearModel();
|
||||||
|
|
||||||
|
beginResetModel();
|
||||||
|
|
||||||
|
if (m_room->cacheForType(m_type)->attachmentPath().length() > 0) {
|
||||||
|
addAttachment(QUrl(m_room->cacheForType(m_type)->attachmentPath()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto textSections = m_room->cacheForType(m_type)->text().split(u"\n\n"_s);
|
||||||
|
for (const auto §ion : textSections) {
|
||||||
|
const auto type = MessageComponentType::typeForString(section);
|
||||||
|
auto cleanText = section;
|
||||||
|
if (type == MessageComponentType::Code) {
|
||||||
|
cleanText.remove(0, 4);
|
||||||
|
cleanText.remove(cleanText.length() - 4, 4);
|
||||||
|
} else if (type == MessageComponentType::Quote) {
|
||||||
|
cleanText.remove(0, 2);
|
||||||
|
}
|
||||||
|
insertComponent(rowCount(), type, {}, cleanText);
|
||||||
|
}
|
||||||
|
m_currentFocusComponent = QPersistentModelIndex(index(rowCount() - 1));
|
||||||
|
endResetModel();
|
||||||
|
|
||||||
|
Q_EMIT focusRowChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatBarMessageContentModel::initializeModel()
|
||||||
|
{
|
||||||
|
beginInsertRows({}, rowCount(), rowCount());
|
||||||
|
const auto documentHandler = new ChatDocumentHandler();
|
||||||
|
connectHandler(documentHandler);
|
||||||
|
m_components += MessageComponent{
|
||||||
|
.type = MessageComponentType::Text,
|
||||||
|
.display = {},
|
||||||
|
.attributes = {{"chatDocumentHandler"_L1, QVariant::fromValue<ChatDocumentHandler *>(documentHandler)}},
|
||||||
|
};
|
||||||
|
m_currentFocusComponent = QPersistentModelIndex(index(0));
|
||||||
|
endInsertRows();
|
||||||
|
|
||||||
|
Q_EMIT focusRowChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatBarMessageContentModel::connectHandler(ChatDocumentHandler *handler)
|
||||||
|
{
|
||||||
|
connect(handler, &ChatDocumentHandler::contentsChanged, this, &ChatBarMessageContentModel::updateCache);
|
||||||
|
connect(handler, &ChatDocumentHandler::unhandledBackspaceAtBeginning, this, [this](ChatDocumentHandler *handler) {
|
||||||
|
const auto index = indexForDocumentHandler(handler);
|
||||||
|
if (index.isValid()) {
|
||||||
|
if (index.row() > 0 && MessageComponentType::isFileType(m_components[index.row() - 1].type)) {
|
||||||
|
removeAttachment();
|
||||||
|
} else if (m_components[index.row()].type == MessageComponentType::Code || m_components[index.row()].type == MessageComponentType::Quote) {
|
||||||
|
insertComponentAtCursor(MessageComponentType::Text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(handler, &ChatDocumentHandler::removeMe, this, [this](ChatDocumentHandler *handler) {
|
||||||
|
removeComponent(handler);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatDocumentHandler *ChatBarMessageContentModel::documentHandlerForComponent(const MessageComponent &component) const
|
||||||
|
{
|
||||||
|
if (const auto chatDocumentHandler = qvariant_cast<ChatDocumentHandler *>(component.attributes["chatDocumentHandler"_L1])) {
|
||||||
|
return chatDocumentHandler;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatDocumentHandler *ChatBarMessageContentModel::documentHandlerForIndex(const QModelIndex &index) const
|
||||||
|
{
|
||||||
|
return documentHandlerForComponent(m_components[index.row()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
QModelIndex ChatBarMessageContentModel::indexForDocumentHandler(ChatDocumentHandler *handler) const
|
||||||
|
{
|
||||||
|
for (auto it = m_components.begin(); it != m_components.end(); ++it) {
|
||||||
|
const auto currentIndex = index(it - m_components.begin());
|
||||||
|
if (documentHandlerForIndex(currentIndex) == handler) {
|
||||||
|
return currentIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatBarMessageContentModel::updateDocumentHandlerRefs(const ComponentIt &it)
|
||||||
|
{
|
||||||
|
if (it == m_components.end()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto handler = documentHandlerForComponent(*it);
|
||||||
|
if (!handler) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it != m_components.begin()) {
|
||||||
|
if (const auto beforeHandler = documentHandlerForComponent(*(it - 1))) {
|
||||||
|
beforeHandler->setNextDocumentHandler(handler);
|
||||||
|
handler->setPreviousDocumentHandler(beforeHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (it + 1 != m_components.end()) {
|
||||||
|
if (const auto afterHandler = documentHandlerForComponent(*(it + 1))) {
|
||||||
|
afterHandler->setPreviousDocumentHandler(handler);
|
||||||
|
handler->setNextDocumentHandler(afterHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatBarType::Type ChatBarMessageContentModel::type() const
|
||||||
|
{
|
||||||
|
return m_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatBarMessageContentModel::setType(ChatBarType::Type type)
|
||||||
|
{
|
||||||
|
if (type == m_type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_type = type;
|
||||||
|
Q_EMIT typeChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChatBarMessageContentModel::focusRow() const
|
||||||
|
{
|
||||||
|
return m_currentFocusComponent.row();
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageComponentType::Type ChatBarMessageContentModel::focusType() const
|
||||||
|
{
|
||||||
|
return static_cast<MessageComponentType::Type>(m_currentFocusComponent.data(ComponentTypeRole).toInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatBarMessageContentModel::setFocusRow(int focusRow, bool mouse)
|
||||||
|
{
|
||||||
|
if (focusRow == m_currentFocusComponent.row() || focusRow < 0 || focusRow >= rowCount()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFocusIndex(index(focusRow), mouse);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatBarMessageContentModel::setFocusIndex(const QModelIndex &index, bool mouse)
|
||||||
|
{
|
||||||
|
const auto oldIndex = std::exchange(m_currentFocusComponent, QPersistentModelIndex(index));
|
||||||
|
|
||||||
|
if (m_currentFocusComponent.isValid()) {
|
||||||
|
if (!mouse) {
|
||||||
|
focusCurrentComponent(oldIndex, m_currentFocusComponent.row() > oldIndex.row());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_EMIT focusRowChanged();
|
||||||
|
emitFocusChangeSignals();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatBarMessageContentModel::focusCurrentComponent(const QModelIndex &previousIndex, bool down)
|
||||||
|
{
|
||||||
|
const auto chatDocumentHandler = focusedDocumentHandler();
|
||||||
|
if (!chatDocumentHandler) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
chatDocumentHandler->setCursorFromDocumentHandler(documentHandlerForIndex(previousIndex), down, MessageComponentType::Quote ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatBarMessageContentModel::refocusCurrentComponent() const
|
||||||
|
{
|
||||||
|
const auto chatDocumentHandler = focusedDocumentHandler();
|
||||||
|
if (!chatDocumentHandler) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
chatDocumentHandler->textItem()->forceActiveFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatDocumentHandler *ChatBarMessageContentModel::focusedDocumentHandler() const
|
||||||
|
{
|
||||||
|
if (!m_currentFocusComponent.isValid()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto chatDocumentHandler = documentHandlerForIndex(m_currentFocusComponent)) {
|
||||||
|
return chatDocumentHandler;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatBarMessageContentModel::emitFocusChangeSignals()
|
||||||
|
{
|
||||||
|
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {CurrentFocusRole});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatBarMessageContentModel::addAttachment(const QUrl &path)
|
||||||
|
{
|
||||||
|
if (m_type == ChatBarType::None || !m_room) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto it = insertComponent(m_components.first().type == MessageComponentType::Reply ? 1 : 0,
|
||||||
|
MessageComponentType::typeForPath(path),
|
||||||
|
{
|
||||||
|
{"filename"_L1, path.fileName()},
|
||||||
|
{"source"_L1, path},
|
||||||
|
{"animated"_L1, false},
|
||||||
|
});
|
||||||
|
it->display = path.fileName();
|
||||||
|
++it;
|
||||||
|
Q_EMIT dataChanged(index(std::distance(m_components.begin(), it)), index(std::distance(m_components.begin(), it)), {DisplayRole});
|
||||||
|
|
||||||
|
bool textKept = false;
|
||||||
|
while (it != m_components.end()) {
|
||||||
|
if (it->type != MessageComponentType::Text || textKept) {
|
||||||
|
it = removeComponent(it);
|
||||||
|
} else {
|
||||||
|
textKept = true;
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_room->cacheForType(m_type)->setAttachmentPath(path.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatBarMessageContentModel::ComponentIt
|
||||||
|
ChatBarMessageContentModel::insertComponent(int row, MessageComponentType::Type type, QVariantMap attributes, const QString &intialText)
|
||||||
|
{
|
||||||
|
if (row < 0 || row > rowCount()) {
|
||||||
|
return m_components.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MessageComponentType::isTextType(type)) {
|
||||||
|
const auto documentHandler = new ChatDocumentHandler();
|
||||||
|
documentHandler->setInitialText(intialText);
|
||||||
|
if (type == MessageComponentType::Quote) {
|
||||||
|
documentHandler->setFixedStartChars(u"\""_s);
|
||||||
|
documentHandler->setFixedEndChars(u"\""_s);
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes.insert("chatDocumentHandler"_L1, QVariant::fromValue<ChatDocumentHandler *>(documentHandler));
|
||||||
|
connectHandler(documentHandler);
|
||||||
|
}
|
||||||
|
beginInsertRows({}, row, row);
|
||||||
|
const auto it = m_components.insert(row,
|
||||||
|
MessageComponent{
|
||||||
|
.type = type,
|
||||||
|
.display = {},
|
||||||
|
.attributes = attributes,
|
||||||
|
});
|
||||||
|
updateDocumentHandlerRefs(it);
|
||||||
|
endInsertRows();
|
||||||
|
return it;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatBarMessageContentModel::insertComponentAtCursor(MessageComponentType::Type type)
|
||||||
|
{
|
||||||
|
if (m_components[m_currentFocusComponent.row()].type == type) {
|
||||||
|
if (type == MessageComponentType::Text && focusedDocumentHandler()) {
|
||||||
|
focusedDocumentHandler()->setStyle(ChatDocumentHandler::Paragraph);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasBefore = false;
|
||||||
|
QTextDocumentFragment midFragment;
|
||||||
|
std::optional<QTextDocumentFragment> afterFragment = std::nullopt;
|
||||||
|
|
||||||
|
if (const auto currentChatDocumentHandler = focusedDocumentHandler()) {
|
||||||
|
currentChatDocumentHandler->fillFragments(hasBefore, midFragment, afterFragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto currentType = m_components[m_currentFocusComponent.row()].type;
|
||||||
|
int insertRow = m_currentFocusComponent.row() + (hasBefore ? 1 : 0);
|
||||||
|
|
||||||
|
if (!hasBefore) {
|
||||||
|
removeComponent(insertRow, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto insertIt = insertComponent(insertRow, type);
|
||||||
|
if (insertIt != m_components.end()) {
|
||||||
|
if (const auto insertChatDocumentHandler = documentHandlerForComponent(*insertIt)) {
|
||||||
|
insertChatDocumentHandler->insertFragment(midFragment);
|
||||||
|
}
|
||||||
|
m_currentFocusComponent = QPersistentModelIndex(index(insertIt - m_components.begin()));
|
||||||
|
Q_EMIT focusRowChanged();
|
||||||
|
emitFocusChangeSignals();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (afterFragment) {
|
||||||
|
const auto afterIt = insertComponent(insertRow + 1, currentType);
|
||||||
|
if (afterIt != m_components.end()) {
|
||||||
|
if (const auto afterChatDocumentHandler = documentHandlerForComponent(*afterIt)) {
|
||||||
|
afterChatDocumentHandler->insertFragment(*afterFragment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatBarMessageContentModel::removeComponent(int row, bool removeLast)
|
||||||
|
{
|
||||||
|
if (row < 0 || row >= rowCount() || (rowCount() == 1 && !removeLast)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeComponent(m_components.begin() + row);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatBarMessageContentModel::removeAttachment()
|
||||||
|
{
|
||||||
|
if (!hasComponentType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video})) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto mediaRow = 0;
|
||||||
|
if (MessageComponentType::isFileType(m_components[1].type)) {
|
||||||
|
mediaRow = 1;
|
||||||
|
}
|
||||||
|
removeComponent(mediaRow);
|
||||||
|
if (m_room) {
|
||||||
|
m_room->cacheForType(m_type)->setAttachmentPath({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatBarMessageContentModel::ComponentIt ChatBarMessageContentModel::removeComponent(ComponentIt it)
|
||||||
|
{
|
||||||
|
if (it == m_components.end()) {
|
||||||
|
return it;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto row = std::distance(m_components.begin(), it);
|
||||||
|
beginRemoveRows({}, row, row);
|
||||||
|
if (rowCount() == 1) {
|
||||||
|
setFocusIndex({});
|
||||||
|
} else if (m_currentFocusComponent.row() == row) {
|
||||||
|
int newFocusRow;
|
||||||
|
if (row > 0) {
|
||||||
|
newFocusRow = row - 1;
|
||||||
|
} else {
|
||||||
|
newFocusRow = row + 1;
|
||||||
|
}
|
||||||
|
setFocusRow(newFocusRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto chatDocumentHandler = documentHandlerForIndex(index(row))) {
|
||||||
|
const auto beforeHandler = chatDocumentHandler->previousDocumentHandler();
|
||||||
|
const auto afterHandler = chatDocumentHandler->nextDocumentHandler();
|
||||||
|
if (beforeHandler && afterHandler) {
|
||||||
|
beforeHandler->setNextDocumentHandler(afterHandler);
|
||||||
|
afterHandler->setPreviousDocumentHandler(beforeHandler);
|
||||||
|
} else if (beforeHandler) {
|
||||||
|
beforeHandler->setNextDocumentHandler(nullptr);
|
||||||
|
} else if (afterHandler) {
|
||||||
|
afterHandler->setPreviousDocumentHandler(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_components[row].attributes.remove("chatDocumentHandler"_L1);
|
||||||
|
chatDocumentHandler->disconnect(this);
|
||||||
|
chatDocumentHandler->deleteLater();
|
||||||
|
}
|
||||||
|
it = m_components.erase(it);
|
||||||
|
endRemoveRows();
|
||||||
|
|
||||||
|
return it;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatBarMessageContentModel::removeComponent(ChatDocumentHandler *handler)
|
||||||
|
{
|
||||||
|
const auto index = indexForDocumentHandler(handler);
|
||||||
|
if (index.isValid()) {
|
||||||
|
removeComponent(index.row());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatBarMessageContentModel::updateCache() const
|
||||||
|
{
|
||||||
|
if (m_type == ChatBarType::None || !m_room) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_room->cacheForType(m_type)->setText(messageText());
|
||||||
|
}
|
||||||
|
|
||||||
|
inline QString formatQuote(const QString &input)
|
||||||
|
{
|
||||||
|
QString stringOut;
|
||||||
|
auto splitString = input.split(u"\n\n"_s, Qt::SkipEmptyParts);
|
||||||
|
for (auto &string : splitString) {
|
||||||
|
if (string.startsWith(u'*')) {
|
||||||
|
string.removeFirst();
|
||||||
|
}
|
||||||
|
if (string.startsWith(u'\"')) {
|
||||||
|
string.removeFirst();
|
||||||
|
}
|
||||||
|
if (string.endsWith(u'*')) {
|
||||||
|
string.removeLast();
|
||||||
|
}
|
||||||
|
if (string.endsWith(u'\"')) {
|
||||||
|
string.removeLast();
|
||||||
|
}
|
||||||
|
if (!stringOut.isEmpty()) {
|
||||||
|
stringOut += u"\n\n"_s;
|
||||||
|
}
|
||||||
|
stringOut += u"> "_s + string;
|
||||||
|
}
|
||||||
|
return stringOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline QString formatCode(const QString &input)
|
||||||
|
{
|
||||||
|
return u"```\n%1\n```"_s.arg(input).replace(u"\n\n"_s, u"\n"_s);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatBarMessageContentModel::messageText() const
|
||||||
|
{
|
||||||
|
QString text;
|
||||||
|
for (const auto &component : m_components) {
|
||||||
|
if (MessageComponentType::isTextType(component.type)) {
|
||||||
|
if (const auto chatDocumentHandler = documentHandlerForComponent(component)) {
|
||||||
|
auto newText = chatDocumentHandler->htmlText();
|
||||||
|
if (component.type == MessageComponentType::Quote) {
|
||||||
|
newText = formatQuote(newText);
|
||||||
|
} else if (component.type == MessageComponentType::Code) {
|
||||||
|
newText = formatCode(newText);
|
||||||
|
}
|
||||||
|
if (!text.isEmpty()) {
|
||||||
|
text += u"\n\n"_s;
|
||||||
|
}
|
||||||
|
text += newText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatBarMessageContentModel::postMessage()
|
||||||
|
{
|
||||||
|
if (m_type == ChatBarType::None || !m_room) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qWarning() << m_room->cacheForType(m_type)->text();
|
||||||
|
m_room->cacheForType(m_type)->postMessage();
|
||||||
|
clearModel();
|
||||||
|
initializeModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<QString> ChatBarMessageContentModel::getReplyEventId()
|
||||||
|
{
|
||||||
|
return m_room->mainCache()->isReplying() ? std::make_optional(m_room->mainCache()->replyId()) : std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatBarMessageContentModel::clearModel()
|
||||||
|
{
|
||||||
|
beginResetModel();
|
||||||
|
for (const auto &component : m_components) {
|
||||||
|
if (const auto chatDocumentHandler = documentHandlerForComponent(component)) {
|
||||||
|
chatDocumentHandler->disconnect(this);
|
||||||
|
chatDocumentHandler->deleteLater();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_components.clear();
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "moc_chatbarmessagecontentmodel.cpp"
|
||||||
96
src/messagecontent/models/chatbarmessagecontentmodel.h
Normal file
96
src/messagecontent/models/chatbarmessagecontentmodel.h
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
#include <qabstractitemmodel.h>
|
||||||
|
|
||||||
|
#include "chatdocumenthandler.h"
|
||||||
|
#include "enums/messagecomponenttype.h"
|
||||||
|
#include "messagecomponent.h"
|
||||||
|
#include "models/messagecontentmodel.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class ChatBarMessageContentModel
|
||||||
|
*
|
||||||
|
* Inherited from MessageContentModel this visulaises the content of a Quotient::RoomMessageEvent.
|
||||||
|
*/
|
||||||
|
class ChatBarMessageContentModel : public MessageContentModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The QQuickTextDocument that is being handled.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(ChatBarType::Type type READ type WRITE setType NOTIFY typeChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The row of the model component that currently has focus.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(int focusRow READ focusRow WRITE setFocusRow NOTIFY focusRowChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The MessageComponentType of the focussed row.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(MessageComponentType::Type focusType READ focusType NOTIFY focusRowChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The ChatDocumentHandler of the model component that currently has focus.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(ChatDocumentHandler *focusedDocumentHandler READ focusedDocumentHandler NOTIFY focusRowChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ChatBarMessageContentModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
ChatBarType::Type type() const;
|
||||||
|
void setType(ChatBarType::Type type);
|
||||||
|
|
||||||
|
int focusRow() const;
|
||||||
|
MessageComponentType::Type focusType() const;
|
||||||
|
Q_INVOKABLE void setFocusRow(int focusRow, bool mouse = false);
|
||||||
|
void setFocusIndex(const QModelIndex &index, bool mouse = false);
|
||||||
|
Q_INVOKABLE void refocusCurrentComponent() const;
|
||||||
|
ChatDocumentHandler *focusedDocumentHandler() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE void insertComponentAtCursor(MessageComponentType::Type type);
|
||||||
|
|
||||||
|
Q_INVOKABLE void addAttachment(const QUrl &path);
|
||||||
|
|
||||||
|
Q_INVOKABLE void removeComponent(int row, bool removeLast = false);
|
||||||
|
|
||||||
|
Q_INVOKABLE void removeAttachment();
|
||||||
|
|
||||||
|
Q_INVOKABLE void postMessage();
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void typeChanged();
|
||||||
|
void focusRowChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
ChatBarType::Type m_type = ChatBarType::None;
|
||||||
|
|
||||||
|
void initializeModel();
|
||||||
|
|
||||||
|
std::optional<QString> getReplyEventId() override;
|
||||||
|
|
||||||
|
void connectHandler(ChatDocumentHandler *handler);
|
||||||
|
ChatDocumentHandler *documentHandlerForComponent(const MessageComponent &component) const;
|
||||||
|
ChatDocumentHandler *documentHandlerForIndex(const QModelIndex &index) const;
|
||||||
|
QModelIndex indexForDocumentHandler(ChatDocumentHandler *handler) const;
|
||||||
|
void updateDocumentHandlerRefs(const ComponentIt &it);
|
||||||
|
|
||||||
|
ComponentIt insertComponent(int row, MessageComponentType::Type type, QVariantMap attributes = {}, const QString &intialText = {});
|
||||||
|
ComponentIt removeComponent(ComponentIt it);
|
||||||
|
void removeComponent(ChatDocumentHandler *handler);
|
||||||
|
|
||||||
|
void focusCurrentComponent(const QModelIndex &previousIndex, bool down);
|
||||||
|
void emitFocusChangeSignals();
|
||||||
|
|
||||||
|
void updateCache() const;
|
||||||
|
QString messageText() const;
|
||||||
|
|
||||||
|
void clearModel();
|
||||||
|
};
|
||||||
@@ -25,7 +25,7 @@ using namespace Quotient;
|
|||||||
bool EventMessageContentModel::m_threadsEnabled = false;
|
bool EventMessageContentModel::m_threadsEnabled = false;
|
||||||
|
|
||||||
EventMessageContentModel::EventMessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply, bool isPending, MessageContentModel *parent)
|
EventMessageContentModel::EventMessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply, bool isPending, MessageContentModel *parent)
|
||||||
: MessageContentModel(room, parent, eventId)
|
: MessageContentModel(room, eventId, parent)
|
||||||
, m_currentState(isPending ? Pending : Unknown)
|
, m_currentState(isPending ? Pending : Unknown)
|
||||||
, m_isReply(isReply)
|
, m_isReply(isReply)
|
||||||
{
|
{
|
||||||
@@ -313,44 +313,23 @@ QList<MessageComponent> EventMessageContentModel::messageContentComponents(bool
|
|||||||
return newComponents;
|
return newComponents;
|
||||||
}
|
}
|
||||||
|
|
||||||
void EventMessageContentModel::updateReplyModel()
|
std::optional<QString> EventMessageContentModel::getReplyEventId()
|
||||||
{
|
{
|
||||||
const auto event = m_room->getEvent(m_eventId);
|
if (m_isReply) {
|
||||||
if (event.first == nullptr || m_isReply) {
|
return std::nullopt;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(m_room->getEvent(m_eventId).first);
|
||||||
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
|
|
||||||
if (roomMessageEvent == nullptr) {
|
if (roomMessageEvent == nullptr) {
|
||||||
return;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
if (!roomMessageEvent->isReply(!m_threadsEnabled)) {
|
if (!roomMessageEvent->isReply(!m_threadsEnabled)) {
|
||||||
if (m_replyModel) {
|
if (m_replyModel) {
|
||||||
m_replyModel->disconnect(this);
|
m_replyModel->disconnect(this);
|
||||||
m_replyModel->deleteLater();
|
m_replyModel->deleteLater();
|
||||||
}
|
}
|
||||||
return;
|
return std::nullopt;
|
||||||
}
|
|
||||||
|
|
||||||
m_replyModel = new EventMessageContentModel(m_room, roomMessageEvent->replyEventId(!m_threadsEnabled), true, false, this);
|
|
||||||
|
|
||||||
bool hasModel = hasComponentType(MessageComponentType::Reply);
|
|
||||||
if (m_replyModel && !hasModel) {
|
|
||||||
int insertRow = 0;
|
|
||||||
if (m_components.first().type == MessageComponentType::Author) {
|
|
||||||
insertRow = 1;
|
|
||||||
}
|
|
||||||
beginInsertRows({}, insertRow, insertRow);
|
|
||||||
m_components.insert(insertRow, MessageComponent{MessageComponentType::Reply, QString(), {}});
|
|
||||||
} else if (!m_replyModel && hasModel) {
|
|
||||||
int removeRow = 0;
|
|
||||||
if (m_components.first().type == MessageComponentType::Author) {
|
|
||||||
removeRow = 1;
|
|
||||||
}
|
|
||||||
beginRemoveRows({}, removeRow, removeRow);
|
|
||||||
m_components.removeAt(removeRow);
|
|
||||||
endRemoveRows();
|
|
||||||
}
|
}
|
||||||
|
return roomMessageEvent->isReply(!m_threadsEnabled) ? std::make_optional(roomMessageEvent->replyEventId(!m_threadsEnabled)) : std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<MessageComponent> EventMessageContentModel::componentsForType(MessageComponentType::Type type)
|
QList<MessageComponent> EventMessageContentModel::componentsForType(MessageComponentType::Type type)
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ private:
|
|||||||
void resetContent(bool isEditing = false, bool isThreading = false);
|
void resetContent(bool isEditing = false, bool isThreading = false);
|
||||||
QList<MessageComponent> messageContentComponents(bool isEditing = false, bool isThreading = false);
|
QList<MessageComponent> messageContentComponents(bool isEditing = false, bool isThreading = false);
|
||||||
|
|
||||||
void updateReplyModel();
|
std::optional<QString> getReplyEventId() override;
|
||||||
|
|
||||||
QList<MessageComponent> componentsForType(MessageComponentType::Type type);
|
QList<MessageComponent> componentsForType(MessageComponentType::Type type);
|
||||||
|
|
||||||
|
|||||||
@@ -15,21 +15,29 @@
|
|||||||
|
|
||||||
using namespace Quotient;
|
using namespace Quotient;
|
||||||
|
|
||||||
MessageContentModel::MessageContentModel(NeoChatRoom *room, MessageContentModel *parent, const QString &eventId)
|
MessageContentModel::MessageContentModel(QObject *parent)
|
||||||
|
: QAbstractListModel(parent)
|
||||||
|
{
|
||||||
|
initializeModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &eventId, MessageContentModel *parent)
|
||||||
: QAbstractListModel(parent)
|
: QAbstractListModel(parent)
|
||||||
, m_room(room)
|
|
||||||
, m_eventId(eventId)
|
, m_eventId(eventId)
|
||||||
{
|
{
|
||||||
connect(qGuiApp->styleHints(), &QStyleHints::colorSchemeChanged, this, &MessageContentModel::updateSpoilers);
|
connect(qGuiApp->styleHints(), &QStyleHints::colorSchemeChanged, this, &MessageContentModel::updateSpoilers);
|
||||||
|
|
||||||
|
setRoom(room);
|
||||||
initializeModel();
|
initializeModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MessageContentModel::initializeModel()
|
void MessageContentModel::initializeModel()
|
||||||
{
|
{
|
||||||
Q_ASSERT(m_room != nullptr);
|
|
||||||
|
|
||||||
connect(this, &MessageContentModel::componentsUpdated, this, [this]() {
|
connect(this, &MessageContentModel::componentsUpdated, this, [this]() {
|
||||||
|
if (!m_room) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (m_room->urlPreviewEnabled()) {
|
if (m_room->urlPreviewEnabled()) {
|
||||||
forEachComponentOfType({MessageComponentType::Text, MessageComponentType::Quote}, m_linkPreviewAddFunction);
|
forEachComponentOfType({MessageComponentType::Text, MessageComponentType::Quote}, m_linkPreviewAddFunction);
|
||||||
} else {
|
} else {
|
||||||
@@ -42,37 +50,60 @@ void MessageContentModel::initializeModel()
|
|||||||
forEachComponentOfType(MessageComponentType::File, m_fileFunction);
|
forEachComponentOfType(MessageComponentType::File, m_fileFunction);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
connect(m_room, &NeoChatRoom::newFileTransfer, this, [this](const QString &eventId) {
|
}
|
||||||
if (eventId == m_eventId) {
|
|
||||||
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
|
NeoChatRoom *MessageContentModel::room() const
|
||||||
m_fileInfoFunction);
|
{
|
||||||
}
|
return m_room;
|
||||||
});
|
}
|
||||||
connect(m_room, &NeoChatRoom::fileTransferProgress, this, [this](const QString &eventId) {
|
|
||||||
if (eventId == m_eventId) {
|
void MessageContentModel::setRoom(NeoChatRoom *room)
|
||||||
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
|
{
|
||||||
m_fileInfoFunction);
|
if (room == m_room) {
|
||||||
}
|
return;
|
||||||
});
|
}
|
||||||
connect(m_room, &NeoChatRoom::fileTransferCompleted, this, [this](const QString &eventId) {
|
|
||||||
if (m_room != nullptr && eventId == m_eventId) {
|
if (m_room) {
|
||||||
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
|
m_room->disconnect(this);
|
||||||
m_fileInfoFunction);
|
}
|
||||||
}
|
|
||||||
});
|
m_room = room;
|
||||||
connect(m_room, &NeoChatRoom::fileTransferFailed, this, [this](const QString &eventId, const QString &errorMessage) {
|
|
||||||
if (eventId == m_eventId) {
|
if (m_room) {
|
||||||
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
|
connect(m_room, &NeoChatRoom::newFileTransfer, this, [this](const QString &eventId) {
|
||||||
m_fileInfoFunction);
|
if (eventId == m_eventId) {
|
||||||
if (errorMessage.isEmpty()) {
|
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
|
||||||
Q_EMIT m_room->showMessage(MessageType::Error, i18nc("@info", "Failed to download file."));
|
m_fileInfoFunction);
|
||||||
} else {
|
|
||||||
Q_EMIT m_room->showMessage(MessageType::Error,
|
|
||||||
i18nc("@info Failed to download file: [error message]", "Failed to download file:<br />%1", errorMessage));
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
connect(m_room, &NeoChatRoom::fileTransferProgress, this, [this](const QString &eventId) {
|
||||||
connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, &MessageContentModel::componentsUpdated);
|
if (eventId == m_eventId) {
|
||||||
|
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
|
||||||
|
m_fileInfoFunction);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(m_room, &NeoChatRoom::fileTransferCompleted, this, [this](const QString &eventId) {
|
||||||
|
if (m_room != nullptr && eventId == m_eventId) {
|
||||||
|
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
|
||||||
|
m_fileInfoFunction);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(m_room, &NeoChatRoom::fileTransferFailed, this, [this](const QString &eventId, const QString &errorMessage) {
|
||||||
|
if (eventId == m_eventId) {
|
||||||
|
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
|
||||||
|
m_fileInfoFunction);
|
||||||
|
if (errorMessage.isEmpty()) {
|
||||||
|
Q_EMIT m_room->showMessage(MessageType::Error, i18nc("@info", "Failed to download file."));
|
||||||
|
} else {
|
||||||
|
Q_EMIT m_room->showMessage(MessageType::Error,
|
||||||
|
i18nc("@info Failed to download file: [error message]", "Failed to download file:<br />%1", errorMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, &MessageContentModel::componentsUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_EMIT roomChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString MessageContentModel::eventId() const
|
QString MessageContentModel::eventId() const
|
||||||
@@ -170,6 +201,12 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
|||||||
}
|
}
|
||||||
return QVariant::fromValue<ChatBarCache *>(m_room->editCache());
|
return QVariant::fromValue<ChatBarCache *>(m_room->editCache());
|
||||||
}
|
}
|
||||||
|
if (role == Editable) {
|
||||||
|
return m_editableActive;
|
||||||
|
}
|
||||||
|
if (role == CurrentFocusRole) {
|
||||||
|
return index.row() == m_currentFocusComponent.row();
|
||||||
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -202,6 +239,8 @@ QHash<int, QByteArray> MessageContentModel::roleNamesStatic()
|
|||||||
roles[MessageContentModel::ThreadRootRole] = "threadRoot";
|
roles[MessageContentModel::ThreadRootRole] = "threadRoot";
|
||||||
roles[MessageContentModel::LinkPreviewerRole] = "linkPreviewer";
|
roles[MessageContentModel::LinkPreviewerRole] = "linkPreviewer";
|
||||||
roles[MessageContentModel::ChatBarCacheRole] = "chatBarCache";
|
roles[MessageContentModel::ChatBarCacheRole] = "chatBarCache";
|
||||||
|
roles[MessageContentModel::Editable] = "editable";
|
||||||
|
roles[MessageContentModel::CurrentFocusRole] = "currentFocus";
|
||||||
return roles;
|
return roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,6 +254,16 @@ bool MessageContentModel::hasComponentType(MessageComponentType::Type type)
|
|||||||
!= m_components.cend();
|
!= m_components.cend();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool MessageContentModel::hasComponentType(QList<MessageComponentType::Type> types)
|
||||||
|
{
|
||||||
|
for (const auto &type : types) {
|
||||||
|
if (hasComponentType(type)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void MessageContentModel::forEachComponentOfType(MessageComponentType::Type type,
|
void MessageContentModel::forEachComponentOfType(MessageComponentType::Type type,
|
||||||
std::function<MessageContentModel::ComponentIt(MessageContentModel::ComponentIt)> function)
|
std::function<MessageContentModel::ComponentIt(MessageContentModel::ComponentIt)> function)
|
||||||
{
|
{
|
||||||
@@ -237,6 +286,54 @@ void MessageContentModel::forEachComponentOfType(QList<MessageComponentType::Typ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<QString> MessageContentModel::getReplyEventId()
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MessageContentModel::updateReplyModel()
|
||||||
|
{
|
||||||
|
const auto eventId = getReplyEventId();
|
||||||
|
if (!eventId) {
|
||||||
|
if (m_replyModel) {
|
||||||
|
m_replyModel->disconnect(this);
|
||||||
|
m_replyModel->deleteLater();
|
||||||
|
}
|
||||||
|
if (hasComponentType(MessageComponentType::Reply)) {
|
||||||
|
forEachComponentOfType(MessageComponentType::Reply, [this](ComponentIt it) {
|
||||||
|
beginRemoveRows({}, std::distance(m_components.begin(), it), std::distance(m_components.begin(), it));
|
||||||
|
it = m_components.erase(it);
|
||||||
|
endRemoveRows();
|
||||||
|
return it;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_replyModel && m_replyModel->eventId() == eventId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_replyModel = new EventMessageContentModel(m_room, *eventId, true, false, this);
|
||||||
|
|
||||||
|
bool hasModel = hasComponentType(MessageComponentType::Reply);
|
||||||
|
if (!hasModel) {
|
||||||
|
int insertRow = 0;
|
||||||
|
if (m_components.first().type == MessageComponentType::Author) {
|
||||||
|
insertRow = 1;
|
||||||
|
}
|
||||||
|
beginInsertRows({}, insertRow, insertRow);
|
||||||
|
m_components.insert(insertRow, MessageComponent{MessageComponentType::Reply, QString(), {}});
|
||||||
|
endInsertRows();
|
||||||
|
} else {
|
||||||
|
forEachComponentOfType(MessageComponentType::Reply, [this](ComponentIt it) {
|
||||||
|
const auto replyIndex = index(std::distance(m_components.begin(), it));
|
||||||
|
dataChanged(replyIndex, replyIndex, {ReplyContentModelRole});
|
||||||
|
return ++it;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MessageComponent MessageContentModel::linkPreviewComponent(const QUrl &link)
|
MessageComponent MessageContentModel::linkPreviewComponent(const QUrl &link)
|
||||||
{
|
{
|
||||||
const auto linkPreviewer = dynamic_cast<NeoChatConnection *>(m_room->connection())->previewerForLink(link);
|
const auto linkPreviewer = dynamic_cast<NeoChatConnection *>(m_room->connection())->previewerForLink(link);
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
#include <QQmlEngine>
|
|
||||||
#include <QImageReader>
|
#include <QImageReader>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
#ifndef Q_OS_ANDROID
|
#ifndef Q_OS_ANDROID
|
||||||
#include <KSyntaxHighlighting/Definition>
|
#include <KSyntaxHighlighting/Definition>
|
||||||
@@ -35,8 +36,15 @@ class MessageContentModel : public QAbstractListModel
|
|||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
QML_UNCREATABLE("")
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The room the chat bar is for.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The author if the message.
|
||||||
|
*/
|
||||||
Q_PROPERTY(NeochatRoomMember *author READ author NOTIFY authorChanged)
|
Q_PROPERTY(NeochatRoomMember *author READ author NOTIFY authorChanged)
|
||||||
Q_PROPERTY(QString eventId READ eventId CONSTANT)
|
Q_PROPERTY(QString eventId READ eventId CONSTANT)
|
||||||
|
|
||||||
@@ -59,10 +67,16 @@ public:
|
|||||||
ThreadRootRole, /**< The thread root event ID for the event. */
|
ThreadRootRole, /**< The thread root event ID for the event. */
|
||||||
LinkPreviewerRole, /**< The link preview details. */
|
LinkPreviewerRole, /**< The link preview details. */
|
||||||
ChatBarCacheRole, /**< The ChatBarCache to use. */
|
ChatBarCacheRole, /**< The ChatBarCache to use. */
|
||||||
|
Editable, /**< Whether the component can be edited. */
|
||||||
|
CurrentFocusRole, /**< Whteher the delegate should have focus. */
|
||||||
};
|
};
|
||||||
Q_ENUM(Roles)
|
Q_ENUM(Roles)
|
||||||
|
|
||||||
explicit MessageContentModel(NeoChatRoom *room, MessageContentModel *parent = nullptr, const QString &eventId = {});
|
explicit MessageContentModel(QObject *parent = nullptr);
|
||||||
|
explicit MessageContentModel(NeoChatRoom *room, const QString &eventId, MessageContentModel *parent = nullptr);
|
||||||
|
|
||||||
|
NeoChatRoom *room() const;
|
||||||
|
void setRoom(NeoChatRoom *room);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get the given role value at the given index.
|
* @brief Get the given role value at the given index.
|
||||||
@@ -109,6 +123,7 @@ public:
|
|||||||
Q_INVOKABLE void toggleSpoiler(QModelIndex index);
|
Q_INVOKABLE void toggleSpoiler(QModelIndex index);
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
|
void roomChanged();
|
||||||
void authorChanged();
|
void authorChanged();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -123,7 +138,7 @@ Q_SIGNALS:
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
QPointer<NeoChatRoom> m_room;
|
QPointer<NeoChatRoom> m_room;
|
||||||
QString m_eventId;
|
QString m_eventId = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief NeoChatDateTime for the message.
|
* @brief NeoChatDateTime for the message.
|
||||||
@@ -150,14 +165,25 @@ protected:
|
|||||||
|
|
||||||
QList<MessageComponent> m_components;
|
QList<MessageComponent> m_components;
|
||||||
bool hasComponentType(MessageComponentType::Type type);
|
bool hasComponentType(MessageComponentType::Type type);
|
||||||
|
bool hasComponentType(QList<MessageComponentType::Type> types);
|
||||||
void forEachComponentOfType(MessageComponentType::Type type, std::function<ComponentIt(ComponentIt)> function);
|
void forEachComponentOfType(MessageComponentType::Type type, std::function<ComponentIt(ComponentIt)> function);
|
||||||
void forEachComponentOfType(QList<MessageComponentType::Type> types, std::function<ComponentIt(ComponentIt)> function);
|
void forEachComponentOfType(QList<MessageComponentType::Type> types, std::function<ComponentIt(ComponentIt)> function);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The ID for the event that the message is replying to, if any.
|
||||||
|
*
|
||||||
|
* The default implementation returns a std::nullopt.
|
||||||
|
*/
|
||||||
|
virtual std::optional<QString> getReplyEventId();
|
||||||
|
void updateReplyModel();
|
||||||
QPointer<MessageContentModel> m_replyModel;
|
QPointer<MessageContentModel> m_replyModel;
|
||||||
QPointer<ReactionModel> m_reactionModel = nullptr;
|
QPointer<ReactionModel> m_reactionModel = nullptr;
|
||||||
QPointer<ItineraryModel> m_itineraryModel = nullptr;
|
QPointer<ItineraryModel> m_itineraryModel = nullptr;
|
||||||
bool m_emptyItinerary = false;
|
bool m_emptyItinerary = false;
|
||||||
|
|
||||||
|
bool m_editableActive = false;
|
||||||
|
QPersistentModelIndex m_currentFocusComponent = {};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void initializeModel();
|
void initializeModel();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user