Start implementing rich-text editor
This commit is contained in:
committed by
James Graham
parent
1f723d1fdf
commit
9cbe9f7280
@@ -70,7 +70,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
|||||||
qml/AttachmentPane.qml
|
qml/AttachmentPane.qml
|
||||||
qml/QuickFormatBar.qml
|
qml/QuickFormatBar.qml
|
||||||
qml/UserDetailDialog.qml
|
qml/UserDetailDialog.qml
|
||||||
qml/OpenFileDialog.qml
|
|
||||||
qml/KeyVerificationDialog.qml
|
qml/KeyVerificationDialog.qml
|
||||||
qml/ConfirmLogoutDialog.qml
|
qml/ConfirmLogoutDialog.qml
|
||||||
qml/VerificationMessage.qml
|
qml/VerificationMessage.qml
|
||||||
@@ -79,7 +78,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
|||||||
qml/EmojiSas.qml
|
qml/EmojiSas.qml
|
||||||
qml/VerificationCanceled.qml
|
qml/VerificationCanceled.qml
|
||||||
qml/MessageSourceSheet.qml
|
qml/MessageSourceSheet.qml
|
||||||
qml/LocationChooser.qml
|
|
||||||
qml/InvitationView.qml
|
qml/InvitationView.qml
|
||||||
qml/AvatarTabButton.qml
|
qml/AvatarTabButton.qml
|
||||||
qml/OsmLocationPlugin.qml
|
qml/OsmLocationPlugin.qml
|
||||||
@@ -105,7 +103,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
|||||||
qml/HoverLinkIndicator.qml
|
qml/HoverLinkIndicator.qml
|
||||||
qml/AvatarNotification.qml
|
qml/AvatarNotification.qml
|
||||||
qml/ReasonDialog.qml
|
qml/ReasonDialog.qml
|
||||||
qml/NewPollDialog.qml
|
|
||||||
qml/UserMenu.qml
|
qml/UserMenu.qml
|
||||||
qml/MeetingDialog.qml
|
qml/MeetingDialog.qml
|
||||||
qml/SeenByDialog.qml
|
qml/SeenByDialog.qml
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ ecm_add_qml_module(Chatbar GENERATE_PLUGIN_SOURCE
|
|||||||
QML_FILES
|
QML_FILES
|
||||||
AttachDialog.qml
|
AttachDialog.qml
|
||||||
ChatBar.qml
|
ChatBar.qml
|
||||||
|
RichEditBar.qml
|
||||||
CompletionMenu.qml
|
CompletionMenu.qml
|
||||||
EmojiDelegate.qml
|
EmojiDelegate.qml
|
||||||
EmojiGrid.qml
|
EmojiGrid.qml
|
||||||
@@ -17,4 +18,9 @@ ecm_add_qml_module(Chatbar GENERATE_PLUGIN_SOURCE
|
|||||||
EmojiTonesPicker.qml
|
EmojiTonesPicker.qml
|
||||||
ImageEditorPage.qml
|
ImageEditorPage.qml
|
||||||
VoiceMessageDialog.qml
|
VoiceMessageDialog.qml
|
||||||
|
ImageDialog.qml
|
||||||
|
LinkDialog.qml
|
||||||
|
LocationChooser.qml
|
||||||
|
NewPollDialog.qml
|
||||||
|
TableDialog.qml
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -70,117 +70,6 @@ QQC2.Control {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The list of actions in the ChatBar.
|
|
||||||
*
|
|
||||||
* Each of these will be visualised in the ChatBar so new actions can be added
|
|
||||||
* by appending to this list.
|
|
||||||
*/
|
|
||||||
property list<BusyAction> actions: [
|
|
||||||
BusyAction {
|
|
||||||
id: attachmentAction
|
|
||||||
|
|
||||||
isBusy: root.currentRoom && root.currentRoom.hasFileUploading
|
|
||||||
|
|
||||||
// Matrix does not allow sending attachments in replies
|
|
||||||
visible: _private.chatBarCache.replyId.length === 0 && _private.chatBarCache.attachmentPath.length === 0
|
|
||||||
icon.name: "mail-attachment"
|
|
||||||
text: i18nc("@action:button", "Attach an image or file")
|
|
||||||
displayHint: Kirigami.DisplayHint.IconOnly
|
|
||||||
|
|
||||||
onTriggered: {
|
|
||||||
if (Clipboard.hasImage) {
|
|
||||||
let dialog = attachDialog.createObject(root.QQC2.Overlay.overlay) as AttachDialog;
|
|
||||||
dialog.chosen.connect(path => _private.chatBarCache.attachmentPath = path);
|
|
||||||
dialog.open();
|
|
||||||
} else {
|
|
||||||
let dialog = openFileDialog.createObject(root.QQC2.Overlay.overlay) as OpenFileDialog;
|
|
||||||
dialog.chosen.connect(path => _private.chatBarCache.attachmentPath = path);
|
|
||||||
dialog.open();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tooltip: text
|
|
||||||
},
|
|
||||||
BusyAction {
|
|
||||||
id: emojiAction
|
|
||||||
|
|
||||||
isBusy: false
|
|
||||||
|
|
||||||
visible: !Kirigami.Settings.isMobile
|
|
||||||
icon.name: "smiley"
|
|
||||||
text: i18nc("@action:button", "Emojis & Stickers")
|
|
||||||
displayHint: Kirigami.DisplayHint.IconOnly
|
|
||||||
checkable: true
|
|
||||||
|
|
||||||
onTriggered: {
|
|
||||||
if (emojiDialog.visible) {
|
|
||||||
emojiDialog.close();
|
|
||||||
} else {
|
|
||||||
emojiDialog.open();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tooltip: text
|
|
||||||
},
|
|
||||||
BusyAction {
|
|
||||||
id: mapButton
|
|
||||||
icon.name: "mark-location-symbolic"
|
|
||||||
isBusy: false
|
|
||||||
text: i18nc("@action:button", "Send a Location")
|
|
||||||
displayHint: QQC2.AbstractButton.IconOnly
|
|
||||||
|
|
||||||
onTriggered: {
|
|
||||||
(locationChooser.createObject(QQC2.Overlay.overlay, {
|
|
||||||
room: root.currentRoom
|
|
||||||
}) as LocationChooser).open();
|
|
||||||
}
|
|
||||||
tooltip: text
|
|
||||||
},
|
|
||||||
BusyAction {
|
|
||||||
id: pollButton
|
|
||||||
icon.name: "amarok_playcount"
|
|
||||||
isBusy: false
|
|
||||||
text: i18nc("@action:button", "Create a Poll")
|
|
||||||
displayHint: QQC2.AbstractButton.IconOnly
|
|
||||||
|
|
||||||
onTriggered: {
|
|
||||||
(newPollDialog.createObject(QQC2.Overlay.overlay, {
|
|
||||||
room: root.currentRoom
|
|
||||||
}) as NewPollDialog).open();
|
|
||||||
}
|
|
||||||
tooltip: text
|
|
||||||
},
|
|
||||||
BusyAction {
|
|
||||||
icon.name: "microphone"
|
|
||||||
isBusy: false
|
|
||||||
text: i18nc("@action:button", "Send a Voice Message")
|
|
||||||
displayHint: QQC2.AbstractButton.IconOnly
|
|
||||||
onTriggered: {
|
|
||||||
let dialog = voiceMessageDialog.createObject(root, {
|
|
||||||
room: root.currentRoom
|
|
||||||
}) as VoiceMessageDialog;
|
|
||||||
dialog.open();
|
|
||||||
}
|
|
||||||
tooltip: text
|
|
||||||
},
|
|
||||||
BusyAction {
|
|
||||||
id: sendAction
|
|
||||||
|
|
||||||
isBusy: false
|
|
||||||
|
|
||||||
icon.name: "document-send"
|
|
||||||
text: i18nc("@action:button", "Send message")
|
|
||||||
displayHint: Kirigami.DisplayHint.IconOnly
|
|
||||||
checkable: true
|
|
||||||
|
|
||||||
onTriggered: {
|
|
||||||
_private.postMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
tooltip: text
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||||
@@ -274,9 +163,7 @@ QQC2.Control {
|
|||||||
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…")
|
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
|
verticalAlignment: TextEdit.AlignVCenter
|
||||||
wrapMode: TextEdit.Wrap
|
wrapMode: TextEdit.Wrap
|
||||||
// This has to stay PlainText or else formatting starts breaking in strange ways
|
persistentSelection: true
|
||||||
textFormat: TextEdit.PlainText
|
|
||||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * NeoChatConfig.fontScale
|
|
||||||
|
|
||||||
Accessible.description: placeholderText
|
Accessible.description: placeholderText
|
||||||
|
|
||||||
@@ -411,6 +298,18 @@ QQC2.Control {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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
|
||||||
@@ -468,11 +367,13 @@ QQC2.Control {
|
|||||||
QtObject {
|
QtObject {
|
||||||
id: _private
|
id: _private
|
||||||
property ChatBarCache chatBarCache
|
property ChatBarCache chatBarCache
|
||||||
|
onChatBarCacheChanged: {
|
||||||
|
richEditBar.chatBarCache = chatBarCache
|
||||||
|
}
|
||||||
|
|
||||||
function postMessage() {
|
function postMessage() {
|
||||||
_private.chatBarCache.postMessage();
|
_private.chatBarCache.postMessage();
|
||||||
repeatTimer.stop();
|
repeatTimer.stop();
|
||||||
root.currentRoom.markAllMessagesAsRead();
|
|
||||||
textField.clear();
|
textField.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,38 +435,6 @@ QQC2.Control {
|
|||||||
room: root.currentRoom
|
room: root.currentRoom
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
|
||||||
id: openFileDialog
|
|
||||||
|
|
||||||
OpenFileDialog {
|
|
||||||
parentWindow: Window.window
|
|
||||||
currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: attachDialog
|
|
||||||
|
|
||||||
AttachDialog {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: locationChooser
|
|
||||||
LocationChooser {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: newPollDialog
|
|
||||||
NewPollDialog {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: voiceMessageDialog
|
|
||||||
VoiceMessageDialog {}
|
|
||||||
}
|
|
||||||
|
|
||||||
CompletionMenu {
|
CompletionMenu {
|
||||||
id: completionMenu
|
id: completionMenu
|
||||||
chatDocumentHandler: documentHandler
|
chatDocumentHandler: documentHandler
|
||||||
@@ -582,32 +451,4 @@ QQC2.Control {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EmojiDialog {
|
|
||||||
id: emojiDialog
|
|
||||||
|
|
||||||
x: root.width - width
|
|
||||||
y: -implicitHeight
|
|
||||||
|
|
||||||
modal: false
|
|
||||||
includeCustom: true
|
|
||||||
closeOnChosen: false
|
|
||||||
|
|
||||||
currentRoom: root.currentRoom
|
|
||||||
|
|
||||||
onChosen: emoji => root.insertText(emoji)
|
|
||||||
onClosed: if (emojiAction.checked) {
|
|
||||||
emojiAction.checked = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function insertText(text) {
|
|
||||||
let initialCursorPosition = textField.cursorPosition;
|
|
||||||
textField.text = textField.text.substr(0, initialCursorPosition) + text + textField.text.substr(initialCursorPosition);
|
|
||||||
textField.cursorPosition = initialCursorPosition + text.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
component BusyAction : Kirigami.Action {
|
|
||||||
required property bool isBusy
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
55
src/chatbar/ImageDialog.qml
Normal file
55
src/chatbar/ImageDialog.qml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 Carl Schwan <carl@carlschwan.eu>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtCore
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
import org.kde.kirigamiaddons.formcard as FormCard
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Dialogs
|
||||||
|
|
||||||
|
FormCard.FormCardDialog {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property alias imagePath: imageField.path
|
||||||
|
|
||||||
|
title: i18nc("@title:window", "Insert Image")
|
||||||
|
standardButtons: QQC2.Dialog.Ok | QQC2.Dialog.Cancel
|
||||||
|
|
||||||
|
FileDialog {
|
||||||
|
id: fileDialog
|
||||||
|
|
||||||
|
title: i18nc("@title:window", "Select an image")
|
||||||
|
currentFolder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
|
||||||
|
fileMode: FileDialog.OpenFile
|
||||||
|
nameFilters: [i18n("Image files (*.jpg *.jpeg *.png *.svg *.webp)"), i18n("All files (*)")]
|
||||||
|
onAccepted: imageField.path = selectedFile
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormButtonDelegate {
|
||||||
|
id: imageField
|
||||||
|
|
||||||
|
property url path
|
||||||
|
|
||||||
|
text: i18nc("@label:textbox", "Image Location:")
|
||||||
|
description: path.toString().length > 0 ? path.toString().split('/').slice(-1)[0] : ''
|
||||||
|
|
||||||
|
onClicked: fileDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
visible: imageField.path.toString().length > 0
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 200
|
||||||
|
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.fill: parent
|
||||||
|
source: imageField.path
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
horizontalAlignment: Image.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/chatbar/LinkDialog.qml
Normal file
33
src/chatbar/LinkDialog.qml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 Carl Schwan <carl@carlschwan.eu>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
import org.kde.kirigamiaddons.formcard as FormCard
|
||||||
|
|
||||||
|
FormCard.FormCardDialog {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property alias linkText: linkTextField.text
|
||||||
|
property alias linkUrl: linkUrlField.text
|
||||||
|
|
||||||
|
title: i18nc("@title:window", "Insert Link")
|
||||||
|
standardButtons: QQC2.Dialog.Ok | QQC2.Dialog.Cancel
|
||||||
|
|
||||||
|
FormCard.FormTextFieldDelegate {
|
||||||
|
id: linkTextField
|
||||||
|
|
||||||
|
label: i18nc("@label:textbox", "Link Text:")
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
||||||
|
FormCard.FormTextFieldDelegate {
|
||||||
|
id: linkUrlField
|
||||||
|
|
||||||
|
label: i18nc("@label:textbox", "Link URL:")
|
||||||
|
}
|
||||||
|
}
|
||||||
564
src/chatbar/RichEditBar.qml
Normal file
564
src/chatbar/RichEditBar.qml
Normal file
@@ -0,0 +1,564 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
|
import org.kde.neochat.libneochat as LibNeoChat
|
||||||
|
|
||||||
|
QQC2.ToolBar {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The current room that user is viewing.
|
||||||
|
*/
|
||||||
|
required property LibNeoChat.NeoChatRoom room
|
||||||
|
|
||||||
|
property LibNeoChat.ChatBarCache chatBarCache
|
||||||
|
|
||||||
|
required property LibNeoChat.ChatDocumentHandler documentHandler
|
||||||
|
|
||||||
|
required property real maxAvailableWidth
|
||||||
|
|
||||||
|
readonly property real uncompressedImplicitWidth: textFormatRow.implicitWidth +
|
||||||
|
listRow.implicitWidth +
|
||||||
|
styleButton.implicitWidth +
|
||||||
|
emojiButton.implicitWidth +
|
||||||
|
linkButton.implicitWidth +
|
||||||
|
sendRow.implicitWidth +
|
||||||
|
sendButton.implicitWidth +
|
||||||
|
buttonRow.spacing * 9 +
|
||||||
|
3
|
||||||
|
|
||||||
|
readonly property real listCompressedImplicitWidth: textFormatRow.implicitWidth +
|
||||||
|
compressedListButton.implicitWidth +
|
||||||
|
styleButton.implicitWidth +
|
||||||
|
emojiButton.implicitWidth +
|
||||||
|
linkButton.implicitWidth +
|
||||||
|
sendRow.implicitWidth +
|
||||||
|
sendButton.implicitWidth +
|
||||||
|
buttonRow.spacing * 9 +
|
||||||
|
3
|
||||||
|
|
||||||
|
readonly property real textFormatCompressedImplicitWidth: compressedTextFormatButton.implicitWidth +
|
||||||
|
compressedListButton.implicitWidth +
|
||||||
|
styleButton.implicitWidth +
|
||||||
|
emojiButton.implicitWidth +
|
||||||
|
linkButton.implicitWidth +
|
||||||
|
sendRow.implicitWidth +
|
||||||
|
sendButton.implicitWidth +
|
||||||
|
buttonRow.spacing * 9 +
|
||||||
|
3
|
||||||
|
|
||||||
|
signal requestPostMessage
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: buttonRow
|
||||||
|
RowLayout {
|
||||||
|
id: textFormatRow
|
||||||
|
visible: root.maxAvailableWidth > root.listCompressedImplicitWidth
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: boldButton
|
||||||
|
Shortcut {
|
||||||
|
sequence: "Ctrl+B"
|
||||||
|
onActivated: boldButton.clicked()
|
||||||
|
}
|
||||||
|
icon.name: "format-text-bold"
|
||||||
|
text: i18nc("@action:button", "Bold")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
checkable: true
|
||||||
|
checked: root.documentHandler.bold
|
||||||
|
onClicked: root.documentHandler.bold = checked;
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: italicButton
|
||||||
|
Shortcut {
|
||||||
|
sequence: "Ctrl+I"
|
||||||
|
onActivated: italicButton.clicked()
|
||||||
|
}
|
||||||
|
icon.name: "format-text-italic"
|
||||||
|
text: i18nc("@action:button", "Italic")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
checkable: true
|
||||||
|
checked: root.documentHandler.italic
|
||||||
|
onClicked: root.documentHandler.italic = checked;
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: underlineButton
|
||||||
|
Shortcut {
|
||||||
|
sequence: "Ctrl+U"
|
||||||
|
onActivated: underlineButton.clicked()
|
||||||
|
}
|
||||||
|
icon.name: "format-text-underline"
|
||||||
|
text: i18nc("@action:button", "Underline")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
checkable: true
|
||||||
|
checked: root.documentHandler.underline
|
||||||
|
onClicked: root.documentHandler.underline = checked;
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
icon.name: "format-text-strikethrough"
|
||||||
|
text: i18nc("@action:button", "Strikethrough")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
checkable: true
|
||||||
|
checked: root.documentHandler.strikethrough
|
||||||
|
onClicked: root.documentHandler.strikethrough = checked;
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: compressedTextFormatButton
|
||||||
|
visible: root.maxAvailableWidth < root.listCompressedImplicitWidth
|
||||||
|
icon.name: "dialog-text-and-font"
|
||||||
|
text: i18nc("@action:button", "Format Text")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
checkable: true
|
||||||
|
checked: compressedTextFormatMenu.visible
|
||||||
|
onClicked: {
|
||||||
|
compressedTextFormatMenu.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Menu {
|
||||||
|
id: compressedTextFormatMenu
|
||||||
|
y: -implicitHeight
|
||||||
|
|
||||||
|
QQC2.MenuItem {
|
||||||
|
icon.name: "format-text-bold"
|
||||||
|
text: i18nc("@action:button", "Bold")
|
||||||
|
checkable: true
|
||||||
|
checked: root.documentHandler.bold
|
||||||
|
onTriggered: root.documentHandler.bold = checked;
|
||||||
|
}
|
||||||
|
QQC2.MenuItem {
|
||||||
|
icon.name: "format-text-italic"
|
||||||
|
text: i18nc("@action:button", "Italic")
|
||||||
|
checkable: true
|
||||||
|
checked: root.documentHandler.italic
|
||||||
|
onTriggered: root.documentHandler.italic = checked;
|
||||||
|
}
|
||||||
|
QQC2.MenuItem {
|
||||||
|
icon.name: "format-text-underline"
|
||||||
|
text: i18nc("@action:button", "Underline")
|
||||||
|
checkable: true
|
||||||
|
checked: root.documentHandler.underline
|
||||||
|
onTriggered: root.documentHandler.underline = checked;
|
||||||
|
}
|
||||||
|
QQC2.MenuItem {
|
||||||
|
icon.name: "format-text-strikethrough"
|
||||||
|
text: i18nc("@action:button", "Strikethrough")
|
||||||
|
checkable: true
|
||||||
|
checked: root.documentHandler.strikethrough
|
||||||
|
onTriggered: root.documentHandler.strikethrough = checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
Kirigami.Separator {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.margins: 0
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
id: listRow
|
||||||
|
visible: root.maxAvailableWidth > root.uncompressedImplicitWidth
|
||||||
|
QQC2.ToolButton {
|
||||||
|
icon.name: "format-list-unordered"
|
||||||
|
text: i18nc("@action:button", "Unordered List")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
checkable: true
|
||||||
|
checked: root.documentHandler.currentListStyle === 1
|
||||||
|
onClicked: {
|
||||||
|
root.documentHandler.setListStyle(root.documentHandler.currentListStyle === 1 ? 0 : 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
icon.name: "format-list-ordered"
|
||||||
|
text: i18nc("@action:button", "Ordered List")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
checkable: true
|
||||||
|
checked: root.documentHandler.currentListStyle === 4
|
||||||
|
onClicked: root.documentHandler.setListStyle(root.documentHandler.currentListStyle === 4 ? 0 : 4)
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: indentAction
|
||||||
|
icon.name: "format-indent-more"
|
||||||
|
text: i18nc("@action:button", "Increase List Level")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
onClicked: {
|
||||||
|
root.documentHandler.indentListMore();
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: dedentAction
|
||||||
|
icon.name: "format-indent-less"
|
||||||
|
text: i18nc("@action:button", "Decrease List Level")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
onClicked: {
|
||||||
|
root.documentHandler.indentListLess();
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: compressedListButton
|
||||||
|
visible: root.maxAvailableWidth < root.uncompressedImplicitWidth
|
||||||
|
icon.name: "format-list-unordered"
|
||||||
|
text: i18nc("@action:button", "List Style")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
checkable: true
|
||||||
|
checked: compressedListMenu.visible
|
||||||
|
onClicked: {
|
||||||
|
compressedListMenu.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Menu {
|
||||||
|
id: compressedListMenu
|
||||||
|
y: -implicitHeight
|
||||||
|
|
||||||
|
QQC2.MenuItem {
|
||||||
|
icon.name: "format-list-unordered"
|
||||||
|
text: i18nc("@action:button", "Unordered List")
|
||||||
|
onTriggered: root.documentHandler.setListStyle(root.documentHandler.currentListStyle === 1 ? 0 : 1);
|
||||||
|
}
|
||||||
|
QQC2.MenuItem {
|
||||||
|
icon.name: "format-list-ordered"
|
||||||
|
text: i18nc("@action:button", "Ordered List")
|
||||||
|
onTriggered: root.documentHandler.setListStyle(root.documentHandler.currentListStyle === 4 ? 0 : 4);
|
||||||
|
}
|
||||||
|
QQC2.MenuItem {
|
||||||
|
icon.name: "format-indent-more"
|
||||||
|
text: i18nc("@action:button", "Increase List Level")
|
||||||
|
onTriggered: root.documentHandler.indentListMore();
|
||||||
|
}
|
||||||
|
QQC2.MenuItem {
|
||||||
|
icon.name: "format-indent-less"
|
||||||
|
text: i18nc("@action:button", "Decrease List Level")
|
||||||
|
onTriggered: root.documentHandler.indentListLess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: styleButton
|
||||||
|
icon.name: "typewriter"
|
||||||
|
text: i18nc("@action:button", "Text Style")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
checkable: true
|
||||||
|
checked: styleMenu.visible
|
||||||
|
onClicked: {
|
||||||
|
styleMenu.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Menu {
|
||||||
|
id: styleMenu
|
||||||
|
y: -implicitHeight
|
||||||
|
|
||||||
|
QQC2.MenuItem {
|
||||||
|
text: i18nc("@item:inmenu no heading", "Paragraph")
|
||||||
|
onTriggered: root.documentHandler.setHeadingLevel(0);
|
||||||
|
}
|
||||||
|
QQC2.MenuItem {
|
||||||
|
text: i18nc("@item:inmenu heading level 1 (largest)", "Heading 1")
|
||||||
|
onTriggered: root.documentHandler.setHeadingLevel(1);
|
||||||
|
}
|
||||||
|
QQC2.MenuItem {
|
||||||
|
text: i18nc("@item:inmenu heading level 2", "Heading 2")
|
||||||
|
onTriggered: root.documentHandler.setHeadingLevel(2);
|
||||||
|
}
|
||||||
|
QQC2.MenuItem {
|
||||||
|
text: i18nc("@item:inmenu heading level 3", "Heading 3")
|
||||||
|
onTriggered: root.documentHandler.setHeadingLevel(3);
|
||||||
|
}
|
||||||
|
QQC2.MenuItem {
|
||||||
|
text: i18nc("@item:inmenu heading level 4", "Heading 4")
|
||||||
|
onTriggered: root.documentHandler.setHeadingLevel(4);
|
||||||
|
}
|
||||||
|
QQC2.MenuItem {
|
||||||
|
text: i18nc("@item:inmenu heading level 5", "Heading 5")
|
||||||
|
onTriggered: root.documentHandler.setHeadingLevel(5);
|
||||||
|
}
|
||||||
|
QQC2.MenuItem {
|
||||||
|
text: i18nc("@item:inmenu heading level 6 (smallest)", "Heading 6")
|
||||||
|
onTriggered: root.documentHandler.setHeadingLevel(6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
Kirigami.Separator {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.margins: 0
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: emojiButton
|
||||||
|
|
||||||
|
property bool isBusy: false
|
||||||
|
|
||||||
|
visible: !Kirigami.Settings.isMobile
|
||||||
|
icon.name: "smiley"
|
||||||
|
text: i18n("Emojis & Stickers")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
checkable: true
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
let dialog = emojiDialog.createObject(root).open();
|
||||||
|
}
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: linkButton
|
||||||
|
icon.name: "insert-link-symbolic"
|
||||||
|
text: i18nc("@action:button", "Insert link")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
onClicked: {
|
||||||
|
let dialog = linkDialog.createObject(QQC2.Overlay.overlay, {
|
||||||
|
linkText: root.documentHandler.currentLinkText(),
|
||||||
|
linkUrl: root.documentHandler.currentLinkUrl()
|
||||||
|
})
|
||||||
|
dialog.onAccepted.connect(() => { documentHandler.updateLink(dialog.linkUrl, dialog.linkText) });
|
||||||
|
dialog.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
Kirigami.Separator {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.margins: 0
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
id: sendRow
|
||||||
|
visible: root.maxAvailableWidth > root.textFormatCompressedImplicitWidth
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: attachmentButton
|
||||||
|
|
||||||
|
property bool isBusy: root.room && root.room.hasFileUploading
|
||||||
|
|
||||||
|
visible: root.chatBarCache.attachmentPath.length === 0
|
||||||
|
icon.name: "mail-attachment"
|
||||||
|
text: i18n("Attach an image or file")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
let dialog = (LibNeoChat.Clipboard.hasImage ? attachDialog : openFileDialog).createObject(QQC2.Overlay.overlay);
|
||||||
|
dialog.chosen.connect(path => root.chatBarCache.attachmentPath = path);
|
||||||
|
dialog.open();
|
||||||
|
}
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: mapButton
|
||||||
|
icon.name: "globe"
|
||||||
|
property bool isBusy: false
|
||||||
|
text: i18n("Send a Location")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
locationChooser.createObject(QQC2.ApplicationWindow.overlay, {
|
||||||
|
room: root.room
|
||||||
|
}).open();
|
||||||
|
}
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: pollButton
|
||||||
|
icon.name: "amarok_playcount"
|
||||||
|
property bool isBusy: false
|
||||||
|
text: i18nc("@action:button", "Create a Poll")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
newPollDialog.createObject(QQC2.Overlay.overlay, {
|
||||||
|
room: root.room
|
||||||
|
}).open();
|
||||||
|
}
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: compressedSendButton
|
||||||
|
visible: root.maxAvailableWidth < root.textFormatCompressedImplicitWidth
|
||||||
|
icon.name: "overflow-menu"
|
||||||
|
text: i18nc("@action:button", "Send Other")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
checkable: true
|
||||||
|
checked: compressedSendMenu.visible
|
||||||
|
onClicked: {
|
||||||
|
compressedSendMenu.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Menu {
|
||||||
|
id: compressedSendMenu
|
||||||
|
y: -implicitHeight
|
||||||
|
|
||||||
|
QQC2.MenuItem {
|
||||||
|
visible: root.chatBarCache.attachmentPath.length === 0
|
||||||
|
icon.name: "mail-attachment"
|
||||||
|
text: i18n("Attach an image or file")
|
||||||
|
onTriggered: {
|
||||||
|
let dialog = (LibNeoChat.Clipboard.hasImage ? attachDialog : openFileDialog).createObject(QQC2.Overlay.overlay);
|
||||||
|
dialog.chosen.connect(path => root.chatBarCache.attachmentPath = path);
|
||||||
|
dialog.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QQC2.MenuItem {
|
||||||
|
icon.name: "globe"
|
||||||
|
text: i18n("Send a Location")
|
||||||
|
onTriggered: {
|
||||||
|
locationChooser.createObject(QQC2.ApplicationWindow.overlay, {
|
||||||
|
room: root.room
|
||||||
|
}).open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QQC2.MenuItem {
|
||||||
|
icon.name: "amarok_playcount"
|
||||||
|
text: i18nc("@action:button", "Create a Poll")
|
||||||
|
onTriggered: {
|
||||||
|
newPollDialog.createObject(QQC2.Overlay.overlay, {
|
||||||
|
room: root.room
|
||||||
|
}).open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: sendButton
|
||||||
|
|
||||||
|
property bool isBusy: false
|
||||||
|
|
||||||
|
icon.name: "document-send"
|
||||||
|
text: i18n("Send message")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
checkable: true
|
||||||
|
|
||||||
|
onClicked: root.requestPostMessage()
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Kirigami.ShadowedRectangle {
|
||||||
|
color: Kirigami.Theme.backgroundColor
|
||||||
|
radius: 5
|
||||||
|
|
||||||
|
shadow {
|
||||||
|
size: 15
|
||||||
|
yOffset: 3
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
border {
|
||||||
|
color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.2)
|
||||||
|
width: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Theme.inherit: false
|
||||||
|
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: linkDialog
|
||||||
|
LinkDialog {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: attachDialog
|
||||||
|
AttachDialog {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: openFileDialog
|
||||||
|
LibNeoChat.OpenFileDialog {
|
||||||
|
parentWindow: Window.window
|
||||||
|
currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: emojiDialog
|
||||||
|
EmojiDialog {
|
||||||
|
x: root.width - width
|
||||||
|
y: -implicitHeight
|
||||||
|
|
||||||
|
modal: false
|
||||||
|
includeCustom: true
|
||||||
|
closeOnChosen: false
|
||||||
|
|
||||||
|
currentRoom: root.room
|
||||||
|
|
||||||
|
onChosen: emoji => {
|
||||||
|
root.documentHandler.insertText(emoji);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
onClosed: if (emojiButton.checked) {
|
||||||
|
emojiButton.checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: locationChooser
|
||||||
|
LocationChooser {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: newPollDialog
|
||||||
|
NewPollDialog {}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/chatbar/TableDialog.qml
Normal file
32
src/chatbar/TableDialog.qml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 Carl Schwan <carl@carlschwan.eu>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtCore
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
import org.kde.kirigamiaddons.formcard as FormCard
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Dialogs
|
||||||
|
|
||||||
|
FormCard.FormCardDialog {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property alias rows: rowsSpinBox.value
|
||||||
|
readonly property alias cols: colsSpinBox.value
|
||||||
|
|
||||||
|
title: i18nc("@title:window", "Insert Table")
|
||||||
|
standardButtons: QQC2.Dialog.Ok | QQC2.Dialog.Cancel
|
||||||
|
|
||||||
|
FormCard.FormSpinBoxDelegate {
|
||||||
|
id: rowsSpinBox
|
||||||
|
label: i18nc("@label:textbox", "Number of Rows:")
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
||||||
|
FormCard.FormSpinBoxDelegate {
|
||||||
|
id: colsSpinBox
|
||||||
|
label: i18nc("@label:textbox", "Number of Columns:")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,8 @@ target_sources(LibNeoChat PRIVATE
|
|||||||
filetype.cpp
|
filetype.cpp
|
||||||
linkpreviewer.cpp
|
linkpreviewer.cpp
|
||||||
neochatdatetime.cpp
|
neochatdatetime.cpp
|
||||||
|
nestedlisthelper_p.h
|
||||||
|
nestedlisthelper.cpp
|
||||||
roomlastmessageprovider.cpp
|
roomlastmessageprovider.cpp
|
||||||
spacehierarchycache.cpp
|
spacehierarchycache.cpp
|
||||||
texthandler.cpp
|
texthandler.cpp
|
||||||
@@ -68,6 +70,7 @@ ecm_add_qml_module(LibNeoChat GENERATE_PLUGIN_SOURCE
|
|||||||
qml/SearchPage.qml
|
qml/SearchPage.qml
|
||||||
qml/CreateRoomDialog.qml
|
qml/CreateRoomDialog.qml
|
||||||
qml/CreateSpaceDialog.qml
|
qml/CreateSpaceDialog.qml
|
||||||
|
qml/OpenFileDialog.qml
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
io.github.quotient_im.libquotient
|
io.github.quotient_im.libquotient
|
||||||
)
|
)
|
||||||
@@ -100,6 +103,7 @@ target_link_libraries(LibNeoChat PUBLIC
|
|||||||
Qt::Multimedia
|
Qt::Multimedia
|
||||||
Qt::Quick
|
Qt::Quick
|
||||||
Qt::QuickControls2
|
Qt::QuickControls2
|
||||||
|
KF6::ColorScheme
|
||||||
KF6::ConfigCore
|
KF6::ConfigCore
|
||||||
KF6::CoreAddons
|
KF6::CoreAddons
|
||||||
KF6::I18n
|
KF6::I18n
|
||||||
|
|||||||
@@ -11,15 +11,20 @@
|
|||||||
#include <QSyntaxHighlighter>
|
#include <QSyntaxHighlighter>
|
||||||
#include <QTextBlock>
|
#include <QTextBlock>
|
||||||
#include <QTextDocument>
|
#include <QTextDocument>
|
||||||
|
#include <QTextList>
|
||||||
|
#include <QTextTable>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
#include <Kirigami/Platform/PlatformTheme>
|
#include <Kirigami/Platform/PlatformTheme>
|
||||||
|
#include <KColorScheme>
|
||||||
|
|
||||||
#include <Sonnet/BackgroundChecker>
|
#include <Sonnet/BackgroundChecker>
|
||||||
#include <Sonnet/Settings>
|
#include <Sonnet/Settings>
|
||||||
|
|
||||||
#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;
|
||||||
|
|
||||||
@@ -216,6 +221,22 @@ int ChatDocumentHandler::cursorPosition() const
|
|||||||
return m_textItem->property("cursorPosition").toInt();
|
return m_textItem->property("cursorPosition").toInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int ChatDocumentHandler::selectionStart() const
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return m_textItem->property("selectionStart").toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChatDocumentHandler::selectionEnd() const
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return m_textItem->property("selectionEnd").toInt();
|
||||||
|
}
|
||||||
|
|
||||||
NeoChatRoom *ChatDocumentHandler::room() const
|
NeoChatRoom *ChatDocumentHandler::room() const
|
||||||
{
|
{
|
||||||
return m_room;
|
return m_room;
|
||||||
@@ -241,6 +262,16 @@ void ChatDocumentHandler::setRoom(NeoChatRoom *room)
|
|||||||
connect(m_room->cacheForType(m_type), &ChatBarCache::textChanged, this, [this]() {
|
connect(m_room->cacheForType(m_type), &ChatBarCache::textChanged, this, [this]() {
|
||||||
int start = completionStartIndex();
|
int start = completionStartIndex();
|
||||||
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
|
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) {
|
if (!m_room->isSpace() && document() && m_type == ChatBarType::Room) {
|
||||||
document()->setPlainText(room->mainCache()->savedText());
|
document()->setPlainText(room->mainCache()->savedText());
|
||||||
@@ -379,4 +410,478 @@ void ChatDocumentHandler::updateMentions(const QString &editId)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::setFontSize(int size)
|
||||||
|
{
|
||||||
|
if (size <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
QTextCursor cursor = textCursor();
|
||||||
|
if (cursor.isNull())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!cursor.hasSelection())
|
||||||
|
cursor.select(QTextCursor::WordUnderCursor);
|
||||||
|
|
||||||
|
if (cursor.charFormat().property(QTextFormat::FontPointSize).toInt() == size)
|
||||||
|
return;
|
||||||
|
|
||||||
|
QTextCharFormat format;
|
||||||
|
format.setFontPointSize(size);
|
||||||
|
mergeFormatOnWordOrSelection(format);
|
||||||
|
Q_EMIT fontSizeChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::setStrikethrough(bool strikethrough)
|
||||||
|
{
|
||||||
|
QTextCharFormat format;
|
||||||
|
format.setFontStrikeOut(strikethrough);
|
||||||
|
mergeFormatOnWordOrSelection(format);
|
||||||
|
Q_EMIT underlineChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::setTextColor(const QColor &color)
|
||||||
|
{
|
||||||
|
QTextCharFormat format;
|
||||||
|
format.setForeground(QBrush(color));
|
||||||
|
mergeFormatOnWordOrSelection(format);
|
||||||
|
Q_EMIT textColorChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
Qt::Alignment ChatDocumentHandler::alignment() const
|
||||||
|
{
|
||||||
|
QTextCursor cursor = textCursor();
|
||||||
|
if (cursor.isNull())
|
||||||
|
return Qt::AlignLeft;
|
||||||
|
return textCursor().blockFormat().alignment();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::setAlignment(Qt::Alignment alignment)
|
||||||
|
{
|
||||||
|
QTextBlockFormat format;
|
||||||
|
format.setAlignment(alignment);
|
||||||
|
QTextCursor cursor = textCursor();
|
||||||
|
cursor.mergeBlockFormat(format);
|
||||||
|
Q_EMIT alignmentChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatDocumentHandler::bold() const
|
||||||
|
{
|
||||||
|
QTextCursor cursor = textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return textCursor().charFormat().fontWeight() == QFont::Bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::setBold(bool bold)
|
||||||
|
{
|
||||||
|
QTextCharFormat format;
|
||||||
|
format.setFontWeight(bold ? QFont::Bold : QFont::Normal);
|
||||||
|
mergeFormatOnWordOrSelection(format);
|
||||||
|
Q_EMIT boldChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatDocumentHandler::italic() const
|
||||||
|
{
|
||||||
|
QTextCursor cursor = textCursor();
|
||||||
|
if (cursor.isNull())
|
||||||
|
return false;
|
||||||
|
return textCursor().charFormat().fontItalic();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::setItalic(bool italic)
|
||||||
|
{
|
||||||
|
QTextCharFormat format;
|
||||||
|
format.setFontItalic(italic);
|
||||||
|
mergeFormatOnWordOrSelection(format);
|
||||||
|
Q_EMIT italicChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatDocumentHandler::underline() const
|
||||||
|
{
|
||||||
|
QTextCursor cursor = textCursor();
|
||||||
|
if (cursor.isNull())
|
||||||
|
return false;
|
||||||
|
return textCursor().charFormat().fontUnderline();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::setUnderline(bool underline)
|
||||||
|
{
|
||||||
|
QTextCharFormat format;
|
||||||
|
format.setFontUnderline(underline);
|
||||||
|
mergeFormatOnWordOrSelection(format);
|
||||||
|
Q_EMIT underlineChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatDocumentHandler::strikethrough() const
|
||||||
|
{
|
||||||
|
QTextCursor cursor = textCursor();
|
||||||
|
if (cursor.isNull())
|
||||||
|
return false;
|
||||||
|
return textCursor().charFormat().fontStrikeOut();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatDocumentHandler::fontFamily() const
|
||||||
|
{
|
||||||
|
QTextCursor cursor = textCursor();
|
||||||
|
if (cursor.isNull())
|
||||||
|
return QString();
|
||||||
|
QTextCharFormat format = cursor.charFormat();
|
||||||
|
return format.font().family();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::setFontFamily(const QString &family)
|
||||||
|
{
|
||||||
|
QTextCharFormat format;
|
||||||
|
format.setFontFamilies({family});
|
||||||
|
mergeFormatOnWordOrSelection(format);
|
||||||
|
Q_EMIT fontFamilyChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
QColor ChatDocumentHandler::textColor() const
|
||||||
|
{
|
||||||
|
QTextCursor cursor = textCursor();
|
||||||
|
if (cursor.isNull())
|
||||||
|
return QColor(Qt::black);
|
||||||
|
QTextCharFormat format = cursor.charFormat();
|
||||||
|
return format.foreground().color();
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextCursor ChatDocumentHandler::textCursor() const
|
||||||
|
{
|
||||||
|
QTextDocument *doc = document();
|
||||||
|
if (!doc)
|
||||||
|
return QTextCursor();
|
||||||
|
|
||||||
|
QTextCursor cursor = QTextCursor(doc);
|
||||||
|
if (selectionStart() != selectionEnd()) {
|
||||||
|
cursor.setPosition(selectionStart());
|
||||||
|
cursor.setPosition(selectionEnd(), QTextCursor::KeepAnchor);
|
||||||
|
} else {
|
||||||
|
cursor.setPosition(cursorPosition());
|
||||||
|
}
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::mergeFormatOnWordOrSelection(const QTextCharFormat &format)
|
||||||
|
{
|
||||||
|
QTextCursor cursor = textCursor();
|
||||||
|
if (!cursor.hasSelection())
|
||||||
|
cursor.select(QTextCursor::WordUnderCursor);
|
||||||
|
cursor.mergeCharFormat(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatDocumentHandler::currentLinkText() const
|
||||||
|
{
|
||||||
|
QTextCursor cursor = textCursor();
|
||||||
|
selectLinkText(&cursor);
|
||||||
|
return cursor.selectedText();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::selectLinkText(QTextCursor *cursor) const
|
||||||
|
{
|
||||||
|
// If the cursor is on a link, select the text of the link.
|
||||||
|
if (cursor->charFormat().isAnchor()) {
|
||||||
|
const QString aHref = cursor->charFormat().anchorHref();
|
||||||
|
|
||||||
|
// Move cursor to start of link
|
||||||
|
while (cursor->charFormat().anchorHref() == aHref) {
|
||||||
|
if (cursor->atStart()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cursor->setPosition(cursor->position() - 1);
|
||||||
|
}
|
||||||
|
if (cursor->charFormat().anchorHref() != aHref) {
|
||||||
|
cursor->setPosition(cursor->position() + 1, QTextCursor::KeepAnchor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move selection to the end of the link
|
||||||
|
while (cursor->charFormat().anchorHref() == aHref) {
|
||||||
|
if (cursor->atEnd()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const int oldPosition = cursor->position();
|
||||||
|
cursor->movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
|
||||||
|
// Wordaround Qt Bug. when we have a table.
|
||||||
|
// FIXME selection url
|
||||||
|
if (oldPosition == cursor->position()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cursor->charFormat().anchorHref() != aHref) {
|
||||||
|
cursor->setPosition(cursor->position() - 1, QTextCursor::KeepAnchor);
|
||||||
|
}
|
||||||
|
} else if (cursor->hasSelection()) {
|
||||||
|
// Nothing to do. Using the currently selected text as the link text.
|
||||||
|
} else {
|
||||||
|
// Select current word
|
||||||
|
cursor->movePosition(QTextCursor::StartOfWord);
|
||||||
|
cursor->movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::insertImage(const QUrl &url)
|
||||||
|
{
|
||||||
|
if (!url.isLocalFile()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QImage image;
|
||||||
|
if (!image.load(url.path())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we are putting the image in a new line and not in a list has it
|
||||||
|
// breaks the Qt rendering
|
||||||
|
textCursor().insertHtml(QStringLiteral("<br />"));
|
||||||
|
|
||||||
|
while (canDedentList()) {
|
||||||
|
m_nestedListHelper.handleOnIndentLess(textCursor());
|
||||||
|
}
|
||||||
|
|
||||||
|
textCursor().insertHtml(QStringLiteral("<img width=\"500\" src=\"") + url.path() + QStringLiteral("\"\\>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::insertTable(int rows, int columns)
|
||||||
|
{
|
||||||
|
QString htmlText;
|
||||||
|
|
||||||
|
QTextCursor cursor = textCursor();
|
||||||
|
QTextTableFormat tableFormat;
|
||||||
|
tableFormat.setBorder(1);
|
||||||
|
const int numberOfColumns(columns);
|
||||||
|
QList<QTextLength> constrains;
|
||||||
|
constrains.reserve(numberOfColumns);
|
||||||
|
const QTextLength::Type type = QTextLength::PercentageLength;
|
||||||
|
const int length = 100; // 100% of window width
|
||||||
|
|
||||||
|
const QTextLength textlength(type, length / numberOfColumns);
|
||||||
|
for (int i = 0; i < numberOfColumns; ++i) {
|
||||||
|
constrains.append(textlength);
|
||||||
|
}
|
||||||
|
tableFormat.setColumnWidthConstraints(constrains);
|
||||||
|
tableFormat.setAlignment(Qt::AlignLeft);
|
||||||
|
tableFormat.setCellSpacing(0);
|
||||||
|
tableFormat.setCellPadding(4);
|
||||||
|
tableFormat.setBorderCollapse(true);
|
||||||
|
tableFormat.setBorder(0.5);
|
||||||
|
tableFormat.setTopMargin(20);
|
||||||
|
|
||||||
|
Q_ASSERT(cursor.document());
|
||||||
|
QTextTable *table = cursor.insertTable(rows, numberOfColumns, tableFormat);
|
||||||
|
|
||||||
|
// fill table with whitespace
|
||||||
|
for (int i = 0, rows = table->rows(); i < rows; i++) {
|
||||||
|
for (int j = 0, columns = table->columns(); j < columns; j++) {
|
||||||
|
auto cell = table->cellAt(i, j);
|
||||||
|
Q_ASSERT(cell.isValid());
|
||||||
|
cell.firstCursorPosition().insertText(QStringLiteral(" "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::updateLink(const QString &linkUrl, const QString &linkText)
|
||||||
|
{
|
||||||
|
auto cursor = textCursor();
|
||||||
|
selectLinkText(&cursor);
|
||||||
|
|
||||||
|
cursor.beginEditBlock();
|
||||||
|
|
||||||
|
if (!cursor.hasSelection()) {
|
||||||
|
cursor.select(QTextCursor::WordUnderCursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextCharFormat format = cursor.charFormat();
|
||||||
|
// Save original format to create an extra space with the existing char
|
||||||
|
// format for the block
|
||||||
|
if (!linkUrl.isEmpty()) {
|
||||||
|
// Add link details
|
||||||
|
format.setAnchor(true);
|
||||||
|
format.setAnchorHref(linkUrl);
|
||||||
|
// Workaround for QTBUG-1814:
|
||||||
|
// Link formatting does not get applied immediately when setAnchor(true)
|
||||||
|
// is called. So the formatting needs to be applied manually.
|
||||||
|
format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
|
||||||
|
format.setUnderlineColor(linkColor());
|
||||||
|
format.setForeground(linkColor());
|
||||||
|
} else {
|
||||||
|
// Remove link details
|
||||||
|
format.setAnchor(false);
|
||||||
|
format.setAnchorHref(QString());
|
||||||
|
// Workaround for QTBUG-1814:
|
||||||
|
// Link formatting does not get removed immediately when setAnchor(false)
|
||||||
|
// is called. So the formatting needs to be applied manually.
|
||||||
|
QTextDocument defaultTextDocument;
|
||||||
|
QTextCharFormat defaultCharFormat = defaultTextDocument.begin().charFormat();
|
||||||
|
|
||||||
|
format.setUnderlineStyle(defaultCharFormat.underlineStyle());
|
||||||
|
format.setUnderlineColor(defaultCharFormat.underlineColor());
|
||||||
|
format.setForeground(defaultCharFormat.foreground());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert link text specified in dialog, otherwise write out url.
|
||||||
|
QString _linkText;
|
||||||
|
if (!linkText.isEmpty()) {
|
||||||
|
_linkText = linkText;
|
||||||
|
} else {
|
||||||
|
_linkText = linkUrl;
|
||||||
|
}
|
||||||
|
cursor.insertText(_linkText, format);
|
||||||
|
|
||||||
|
cursor.endEditBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
QColor ChatDocumentHandler::linkColor()
|
||||||
|
{
|
||||||
|
if (mLinkColor.isValid()) {
|
||||||
|
return mLinkColor;
|
||||||
|
}
|
||||||
|
regenerateColorScheme();
|
||||||
|
return mLinkColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::regenerateColorScheme()
|
||||||
|
{
|
||||||
|
mLinkColor = KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::LinkText).color();
|
||||||
|
// TODO update existing link
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChatDocumentHandler::currentHeadingLevel() const
|
||||||
|
{
|
||||||
|
return textCursor().blockFormat().headingLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::indentListMore()
|
||||||
|
{
|
||||||
|
m_nestedListHelper.handleOnIndentMore(textCursor());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::indentListLess()
|
||||||
|
{
|
||||||
|
m_nestedListHelper.handleOnIndentLess(textCursor());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::setListStyle(int styleIndex)
|
||||||
|
{
|
||||||
|
m_nestedListHelper.handleOnBulletType(-styleIndex, textCursor());
|
||||||
|
Q_EMIT currentListStyleChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::setHeadingLevel(int level)
|
||||||
|
{
|
||||||
|
const int boundedLevel = qBound(0, 6, level);
|
||||||
|
// Apparently, 5 is maximum for FontSizeAdjustment; otherwise level=1 and
|
||||||
|
// level=2 look the same
|
||||||
|
const int sizeAdjustment = boundedLevel > 0 ? 5 - boundedLevel : 0;
|
||||||
|
|
||||||
|
QTextCursor cursor = textCursor();
|
||||||
|
cursor.beginEditBlock();
|
||||||
|
|
||||||
|
QTextBlockFormat blkfmt;
|
||||||
|
blkfmt.setHeadingLevel(boundedLevel);
|
||||||
|
cursor.mergeBlockFormat(blkfmt);
|
||||||
|
|
||||||
|
QTextCharFormat chrfmt;
|
||||||
|
chrfmt.setFontWeight(boundedLevel > 0 ? QFont::Bold : QFont::Normal);
|
||||||
|
chrfmt.setProperty(QTextFormat::FontSizeAdjustment, sizeAdjustment);
|
||||||
|
// Applying style to the current line or selection
|
||||||
|
QTextCursor selectCursor = cursor;
|
||||||
|
if (selectCursor.hasSelection()) {
|
||||||
|
QTextCursor top = selectCursor;
|
||||||
|
top.setPosition(qMin(top.anchor(), top.position()));
|
||||||
|
top.movePosition(QTextCursor::StartOfBlock);
|
||||||
|
|
||||||
|
QTextCursor bottom = selectCursor;
|
||||||
|
bottom.setPosition(qMax(bottom.anchor(), bottom.position()));
|
||||||
|
bottom.movePosition(QTextCursor::EndOfBlock);
|
||||||
|
|
||||||
|
selectCursor.setPosition(top.position(), QTextCursor::MoveAnchor);
|
||||||
|
selectCursor.setPosition(bottom.position(), QTextCursor::KeepAnchor);
|
||||||
|
} else {
|
||||||
|
selectCursor.select(QTextCursor::BlockUnderCursor);
|
||||||
|
}
|
||||||
|
selectCursor.mergeCharFormat(chrfmt);
|
||||||
|
|
||||||
|
cursor.mergeBlockCharFormat(chrfmt);
|
||||||
|
cursor.endEditBlock();
|
||||||
|
// richTextComposer()->setTextCursor(cursor);
|
||||||
|
// richTextComposer()->setFocus();
|
||||||
|
// richTextComposer()->activateRichText();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatDocumentHandler::canIndentList() const
|
||||||
|
{
|
||||||
|
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
|
||||||
|
{
|
||||||
|
QTextCursor cursor = textCursor();
|
||||||
|
if (cursor.isNull())
|
||||||
|
return 0;
|
||||||
|
QTextCharFormat format = cursor.charFormat();
|
||||||
|
return format.font().pointSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatDocumentHandler::fileName() const
|
||||||
|
{
|
||||||
|
const QString filePath = QQmlFile::urlToLocalFileOrQrc(m_fileUrl);
|
||||||
|
const QString fileName = QFileInfo(filePath).fileName();
|
||||||
|
if (fileName.isEmpty())
|
||||||
|
return QStringLiteral("untitled.txt");
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatDocumentHandler::fileType() const
|
||||||
|
{
|
||||||
|
return QFileInfo(fileName()).suffix();
|
||||||
|
}
|
||||||
|
|
||||||
|
QUrl ChatDocumentHandler::fileUrl() const
|
||||||
|
{
|
||||||
|
return m_fileUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::insertText(const QString &text)
|
||||||
|
{
|
||||||
|
textCursor().insertText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatDocumentHandler::currentLinkUrl() const
|
||||||
|
{
|
||||||
|
return textCursor().charFormat().anchorHref();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDocumentHandler::dumpHtml()
|
||||||
|
{
|
||||||
|
qWarning() << htmlText();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatDocumentHandler::htmlText()
|
||||||
|
{
|
||||||
|
auto text = document()->toMarkdown();
|
||||||
|
while (text.startsWith(u"\n"_s)) {
|
||||||
|
text.remove(0, 1);
|
||||||
|
}
|
||||||
|
while (text.endsWith(u"\n"_s)) {
|
||||||
|
text.remove(text.size() - 1, text.size());
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
#include "moc_chatdocumenthandler.cpp"
|
#include "moc_chatdocumenthandler.cpp"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "enums/chatbartype.h"
|
#include "enums/chatbartype.h"
|
||||||
#include "models/completionmodel.h"
|
#include "models/completionmodel.h"
|
||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
|
#include "nestedlisthelper_p.h"
|
||||||
|
|
||||||
class QTextDocument;
|
class QTextDocument;
|
||||||
|
|
||||||
@@ -88,6 +89,28 @@ class ChatDocumentHandler : public QObject
|
|||||||
*/
|
*/
|
||||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||||
|
|
||||||
|
Q_PROPERTY(QColor textColor READ textColor WRITE setTextColor NOTIFY textColorChanged)
|
||||||
|
Q_PROPERTY(QString fontFamily READ fontFamily WRITE setFontFamily NOTIFY fontFamilyChanged)
|
||||||
|
Q_PROPERTY(Qt::Alignment alignment READ alignment WRITE setAlignment NOTIFY alignmentChanged)
|
||||||
|
|
||||||
|
Q_PROPERTY(bool bold READ bold WRITE setBold NOTIFY boldChanged)
|
||||||
|
Q_PROPERTY(bool italic READ italic WRITE setItalic NOTIFY italicChanged)
|
||||||
|
Q_PROPERTY(bool underline READ underline WRITE setUnderline NOTIFY underlineChanged)
|
||||||
|
Q_PROPERTY(bool strikethrough READ strikethrough WRITE setStrikethrough NOTIFY strikethroughChanged)
|
||||||
|
|
||||||
|
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 currentHeadingLevel READ currentHeadingLevel NOTIFY cursorPositionChanged)
|
||||||
|
|
||||||
|
// Q_PROPERTY(bool list READ list WRITE setList NOTIFY listChanged)
|
||||||
|
|
||||||
|
Q_PROPERTY(int fontSize READ fontSize WRITE setFontSize NOTIFY fontSizeChanged)
|
||||||
|
|
||||||
|
Q_PROPERTY(QString fileName READ fileName NOTIFY fileUrlChanged)
|
||||||
|
Q_PROPERTY(QString fileType READ fileType NOTIFY fileUrlChanged)
|
||||||
|
Q_PROPERTY(QUrl fileUrl READ fileUrl NOTIFY fileUrlChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ChatDocumentHandler(QObject *parent = nullptr);
|
explicit ChatDocumentHandler(QObject *parent = nullptr);
|
||||||
|
|
||||||
@@ -111,13 +134,76 @@ public:
|
|||||||
*/
|
*/
|
||||||
Q_INVOKABLE void updateMentions(const QString &editId);
|
Q_INVOKABLE void updateMentions(const QString &editId);
|
||||||
|
|
||||||
|
QString fontFamily() const;
|
||||||
|
void setFontFamily(const QString &family);
|
||||||
|
|
||||||
|
QColor textColor() const;
|
||||||
|
void setTextColor(const QColor &color);
|
||||||
|
|
||||||
|
Qt::Alignment alignment() const;
|
||||||
|
void setAlignment(Qt::Alignment alignment);
|
||||||
|
|
||||||
|
bool bold() const;
|
||||||
|
void setBold(bool bold);
|
||||||
|
|
||||||
|
bool italic() const;
|
||||||
|
void setItalic(bool italic);
|
||||||
|
|
||||||
|
bool underline() const;
|
||||||
|
void setUnderline(bool underline);
|
||||||
|
|
||||||
|
bool strikethrough() const;
|
||||||
|
void setStrikethrough(bool strikethrough);
|
||||||
|
|
||||||
|
bool canIndentList() const;
|
||||||
|
bool canDedentList() const;
|
||||||
|
int currentListStyle() const;
|
||||||
|
|
||||||
|
int currentHeadingLevel() const;
|
||||||
|
|
||||||
|
// bool list() const;
|
||||||
|
// void setList(bool list);
|
||||||
|
|
||||||
|
int fontSize() const;
|
||||||
|
void setFontSize(int size);
|
||||||
|
|
||||||
|
QString fileName() const;
|
||||||
|
QString fileType() const;
|
||||||
|
QUrl fileUrl() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE void insertText(const QString &text);
|
||||||
|
Q_INVOKABLE QString currentLinkUrl() const;
|
||||||
|
Q_INVOKABLE QString currentLinkText() const;
|
||||||
|
Q_INVOKABLE void updateLink(const QString &linkUrl, const QString &linkText);
|
||||||
|
Q_INVOKABLE void insertImage(const QUrl &imagePath);
|
||||||
|
Q_INVOKABLE void insertTable(int rows, int columns);
|
||||||
|
|
||||||
|
Q_INVOKABLE void indentListLess();
|
||||||
|
Q_INVOKABLE void indentListMore();
|
||||||
|
|
||||||
|
Q_INVOKABLE void setListStyle(int styleIndex);
|
||||||
|
Q_INVOKABLE void setHeadingLevel(int level);
|
||||||
|
|
||||||
|
Q_INVOKABLE void dumpHtml();
|
||||||
|
Q_INVOKABLE QString htmlText();
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void typeChanged();
|
void typeChanged();
|
||||||
void textItemChanged();
|
void textItemChanged();
|
||||||
void roomChanged();
|
void roomChanged();
|
||||||
|
|
||||||
public Q_SLOTS:
|
void fontFamilyChanged();
|
||||||
void updateCompletion() const;
|
void textColorChanged();
|
||||||
|
void alignmentChanged();
|
||||||
|
|
||||||
|
void boldChanged();
|
||||||
|
void italicChanged();
|
||||||
|
void underlineChanged();
|
||||||
|
void checkableChanged();
|
||||||
|
void strikethroughChanged();
|
||||||
|
void currentListStyleChanged();
|
||||||
|
void fontSizeChanged();
|
||||||
|
void fileUrlChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ChatBarType::Type m_type = ChatBarType::None;
|
ChatBarType::Type m_type = ChatBarType::None;
|
||||||
@@ -129,11 +215,22 @@ private:
|
|||||||
QPointer<NeoChatRoom> m_room;
|
QPointer<NeoChatRoom> m_room;
|
||||||
|
|
||||||
int cursorPosition() const;
|
int cursorPosition() const;
|
||||||
|
int selectionStart() const;
|
||||||
|
int selectionEnd() const;
|
||||||
|
|
||||||
QString getText() const;
|
QString getText() const;
|
||||||
void pushMention(const Mention mention) const;
|
void pushMention(const Mention mention) const;
|
||||||
|
|
||||||
SyntaxHighlighter *m_highlighter = nullptr;
|
SyntaxHighlighter *m_highlighter = nullptr;
|
||||||
|
QQuickItem *m_textArea;
|
||||||
|
|
||||||
CompletionModel *m_completionModel = nullptr;
|
CompletionModel *m_completionModel = nullptr;
|
||||||
|
QTextCursor textCursor() const;
|
||||||
|
void mergeFormatOnWordOrSelection(const QTextCharFormat &format);
|
||||||
|
void selectLinkText(QTextCursor *cursor) const;
|
||||||
|
NestedListHelper m_nestedListHelper;
|
||||||
|
QColor linkColor();
|
||||||
|
QColor mLinkColor;
|
||||||
|
void regenerateColorScheme();
|
||||||
|
QUrl m_fileUrl;
|
||||||
};
|
};
|
||||||
|
|||||||
249
src/libneochat/nestedlisthelper.cpp
Normal file
249
src/libneochat/nestedlisthelper.cpp
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
/**
|
||||||
|
* Nested list helper
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: 2008 Stephen Kelly <steveire@gmail.com>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "nestedlisthelper_p.h"
|
||||||
|
|
||||||
|
#include <QKeyEvent>
|
||||||
|
#include <QTextBlock>
|
||||||
|
#include <QTextCursor>
|
||||||
|
#include <QTextList>
|
||||||
|
|
||||||
|
NestedListHelper::NestedListHelper()
|
||||||
|
{
|
||||||
|
listBottomMargin = 12;
|
||||||
|
listTopMargin = 12;
|
||||||
|
listNoMargin = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NestedListHelper::handleBeforeKeyPressEvent(QKeyEvent *event, const QTextCursor &cursor)
|
||||||
|
{
|
||||||
|
// Only attempt to handle Backspace while on a list
|
||||||
|
if ((event->key() != Qt::Key_Backspace) || (!cursor.currentList())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool handled = false;
|
||||||
|
|
||||||
|
if (!cursor.hasSelection() && cursor.currentList() && event->key() == Qt::Key_Backspace && cursor.atBlockStart()) {
|
||||||
|
handleOnIndentLess(cursor);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NestedListHelper::canIndent(const QTextCursor &textCursor) const
|
||||||
|
{
|
||||||
|
if ((textCursor.block().isValid())
|
||||||
|
// && ( textEdit->textCursor().block().previous().isValid() )
|
||||||
|
) {
|
||||||
|
const QTextBlock block = textCursor.block();
|
||||||
|
const QTextBlock prevBlock = textCursor.block().previous();
|
||||||
|
if (block.textList()) {
|
||||||
|
if (prevBlock.textList()) {
|
||||||
|
return block.textList()->format().indent() <= prevBlock.textList()->format().indent();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NestedListHelper::canDedent(const QTextCursor &textCursor) const
|
||||||
|
{
|
||||||
|
QTextBlock thisBlock = textCursor.block();
|
||||||
|
QTextBlock nextBlock = thisBlock.next();
|
||||||
|
if (thisBlock.isValid()) {
|
||||||
|
int nextBlockIndent = 0;
|
||||||
|
if (nextBlock.isValid() && nextBlock.textList()) {
|
||||||
|
nextBlockIndent = nextBlock.textList()->format().indent();
|
||||||
|
}
|
||||||
|
if (thisBlock.textList()) {
|
||||||
|
const int thisBlockIndent = thisBlock.textList()->format().indent();
|
||||||
|
if (thisBlockIndent >= nextBlockIndent) {
|
||||||
|
return thisBlockIndent > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NestedListHelper::handleAfterKeyPressEvent(QKeyEvent *event, const QTextCursor &cursor)
|
||||||
|
{
|
||||||
|
// Only attempt to handle Backspace and Return
|
||||||
|
if ((event->key() != Qt::Key_Backspace) && (event->key() != Qt::Key_Return)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool handled = false;
|
||||||
|
|
||||||
|
if (!cursor.hasSelection() && cursor.currentList()) {
|
||||||
|
// Check if we're on the last list item.
|
||||||
|
// itemNumber is zero indexed
|
||||||
|
QTextBlock currentBlock = cursor.block();
|
||||||
|
if (cursor.currentList()->count() == cursor.currentList()->itemNumber(currentBlock) + 1) {
|
||||||
|
// Last block in this list, but may have just gained another list below.
|
||||||
|
if (currentBlock.next().textList()) {
|
||||||
|
reformatList(cursor.block());
|
||||||
|
}
|
||||||
|
reformatList(cursor.block());
|
||||||
|
|
||||||
|
// No need to reformatList in this case. reformatList is slow.
|
||||||
|
if ((event->key() == Qt::Key_Return) || (event->key() == Qt::Key_Backspace)) {
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reformatList(cursor.block());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NestedListHelper::processList(QTextList *list)
|
||||||
|
{
|
||||||
|
QTextBlock block = list->item(0);
|
||||||
|
const int thisListIndent = list->format().indent();
|
||||||
|
|
||||||
|
QTextCursor cursor = QTextCursor(block);
|
||||||
|
list = cursor.createList(list->format());
|
||||||
|
bool processingSubList = false;
|
||||||
|
while (block.next().textList() != nullptr) {
|
||||||
|
block = block.next();
|
||||||
|
|
||||||
|
QTextList *nextList = block.textList();
|
||||||
|
const int nextItemIndent = nextList->format().indent();
|
||||||
|
if (nextItemIndent < thisListIndent) {
|
||||||
|
return;
|
||||||
|
} else if (nextItemIndent > thisListIndent) {
|
||||||
|
if (processingSubList) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
processingSubList = true;
|
||||||
|
processList(nextList);
|
||||||
|
} else {
|
||||||
|
processingSubList = false;
|
||||||
|
list->add(block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// delete nextList;
|
||||||
|
// nextList = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NestedListHelper::reformatList(QTextBlock block)
|
||||||
|
{
|
||||||
|
if (block.textList()) {
|
||||||
|
const int minimumIndent = block.textList()->format().indent();
|
||||||
|
|
||||||
|
// Start at the top of the list
|
||||||
|
while (block.previous().textList() != nullptr) {
|
||||||
|
if (block.previous().textList()->format().indent() < minimumIndent) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
block = block.previous();
|
||||||
|
}
|
||||||
|
|
||||||
|
processList(block.textList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextCursor NestedListHelper::topOfSelection(QTextCursor cursor)
|
||||||
|
{
|
||||||
|
if (cursor.hasSelection()) {
|
||||||
|
cursor.setPosition(qMin(cursor.position(), cursor.anchor()));
|
||||||
|
}
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextCursor NestedListHelper::bottomOfSelection(QTextCursor cursor)
|
||||||
|
{
|
||||||
|
if (cursor.hasSelection()) {
|
||||||
|
cursor.setPosition(qMax(cursor.position(), cursor.anchor()));
|
||||||
|
}
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NestedListHelper::handleOnIndentMore(const QTextCursor &textCursor)
|
||||||
|
{
|
||||||
|
QTextCursor cursor = textCursor;
|
||||||
|
|
||||||
|
QTextListFormat listFmt;
|
||||||
|
if (!cursor.currentList()) {
|
||||||
|
QTextListFormat::Style style;
|
||||||
|
cursor = topOfSelection(textCursor);
|
||||||
|
cursor.movePosition(QTextCursor::PreviousBlock);
|
||||||
|
if (cursor.currentList()) {
|
||||||
|
style = cursor.currentList()->format().style();
|
||||||
|
} else {
|
||||||
|
cursor = bottomOfSelection(textCursor);
|
||||||
|
cursor.movePosition(QTextCursor::NextBlock);
|
||||||
|
|
||||||
|
if (cursor.currentList()) {
|
||||||
|
style = cursor.currentList()->format().style();
|
||||||
|
} else {
|
||||||
|
style = QTextListFormat::ListDisc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleOnBulletType(style, textCursor);
|
||||||
|
} else {
|
||||||
|
listFmt = cursor.currentList()->format();
|
||||||
|
listFmt.setIndent(listFmt.indent() + 1);
|
||||||
|
|
||||||
|
cursor.createList(listFmt);
|
||||||
|
reformatList(textCursor.block());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NestedListHelper::handleOnIndentLess(const QTextCursor &textCursor)
|
||||||
|
{
|
||||||
|
QTextCursor cursor = textCursor;
|
||||||
|
QTextList *currentList = cursor.currentList();
|
||||||
|
if (!currentList) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QTextListFormat listFmt = currentList->format();
|
||||||
|
if (listFmt.indent() > 1) {
|
||||||
|
listFmt.setIndent(listFmt.indent() - 1);
|
||||||
|
cursor.createList(listFmt);
|
||||||
|
reformatList(cursor.block());
|
||||||
|
} else {
|
||||||
|
QTextBlockFormat bfmt;
|
||||||
|
bfmt.setObjectIndex(-1);
|
||||||
|
cursor.setBlockFormat(bfmt);
|
||||||
|
reformatList(cursor.block().next());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NestedListHelper::handleOnBulletType(int styleIndex, const QTextCursor &textCursor)
|
||||||
|
{
|
||||||
|
QTextCursor cursor = textCursor;
|
||||||
|
if (styleIndex != 0) {
|
||||||
|
auto style = static_cast<QTextListFormat::Style>(styleIndex);
|
||||||
|
QTextList *currentList = cursor.currentList();
|
||||||
|
QTextListFormat listFmt;
|
||||||
|
|
||||||
|
cursor.beginEditBlock();
|
||||||
|
|
||||||
|
if (currentList) {
|
||||||
|
listFmt = currentList->format();
|
||||||
|
listFmt.setStyle(style);
|
||||||
|
currentList->setFormat(listFmt);
|
||||||
|
} else {
|
||||||
|
listFmt.setStyle(style);
|
||||||
|
cursor.createList(listFmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.endEditBlock();
|
||||||
|
} else {
|
||||||
|
QTextBlockFormat bfmt;
|
||||||
|
bfmt.setObjectIndex(-1);
|
||||||
|
cursor.setBlockFormat(bfmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
reformatList(textCursor.block());
|
||||||
|
}
|
||||||
114
src/libneochat/nestedlisthelper_p.h
Normal file
114
src/libneochat/nestedlisthelper_p.h
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* Nested list helper
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: 2008 Stephen Kelly <steveire@gmail.com>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
class QKeyEvent;
|
||||||
|
class QTextCursor;
|
||||||
|
class QTextBlock;
|
||||||
|
class QTextList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @short Helper class for automatic handling of nested lists in a text edit
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @author Stephen Kelly
|
||||||
|
* @since 4.1
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class NestedListHelper
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Create a helper
|
||||||
|
*
|
||||||
|
* @param te The text edit object to handle lists in.
|
||||||
|
*/
|
||||||
|
NestedListHelper();
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Handles a key press before it is processed by the text edit widget.
|
||||||
|
*
|
||||||
|
* Currently this causes a backspace at the beginning of a line or with a
|
||||||
|
* multi-line selection to decrease the nesting level of the list.
|
||||||
|
*
|
||||||
|
* @param event The event to be handled
|
||||||
|
* @return Whether the event was completely handled by this method.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] bool handleBeforeKeyPressEvent(QKeyEvent *event, const QTextCursor &cursor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Handles a key press after it is processed by the text edit widget.
|
||||||
|
*
|
||||||
|
* Currently this causes a Return at the end of the last list item, or
|
||||||
|
* a Backspace after the last list item to recalculate the spacing
|
||||||
|
* between the list items.
|
||||||
|
*
|
||||||
|
* @param event The event to be handled
|
||||||
|
* @return Whether the event was completely handled by this method.
|
||||||
|
*/
|
||||||
|
bool handleAfterKeyPressEvent(QKeyEvent *event, const QTextCursor &cursor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increases the indent (nesting level) on the current list item or selection.
|
||||||
|
*/
|
||||||
|
void handleOnIndentMore(const QTextCursor &textCursor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decreases the indent (nesting level) on the current list item or selection.
|
||||||
|
*/
|
||||||
|
void handleOnIndentLess(const QTextCursor &textCursor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the style of the current list or creates a new list with
|
||||||
|
* the specified style.
|
||||||
|
*
|
||||||
|
* @param styleIndex The QTextListStyle of the list.
|
||||||
|
*/
|
||||||
|
void handleOnBulletType(int styleIndex, const QTextCursor &textCursor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check whether the current item in the list may be indented.
|
||||||
|
*
|
||||||
|
* An list item must have an item above it on the same or greater level
|
||||||
|
* if it can be indented.
|
||||||
|
*
|
||||||
|
* Also, a block which is currently part of a list can be indented.
|
||||||
|
*
|
||||||
|
* @sa canDedent
|
||||||
|
*
|
||||||
|
* @return Whether the item can be indented.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] bool canIndent(const QTextCursor &textCursor) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Check whether the current item in the list may be dedented.
|
||||||
|
*
|
||||||
|
* An item may be dedented if it is part of a list. Otherwise it can't be.
|
||||||
|
*
|
||||||
|
* @sa canIndent
|
||||||
|
*
|
||||||
|
* @return Whether the item can be dedented.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] bool canDedent(const QTextCursor &textCursor) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
[[nodiscard]] QTextCursor topOfSelection(QTextCursor cursor);
|
||||||
|
[[nodiscard]] QTextCursor bottomOfSelection(QTextCursor cursor);
|
||||||
|
void processList(QTextList *list);
|
||||||
|
void reformatList(QTextBlock block);
|
||||||
|
|
||||||
|
int listBottomMargin;
|
||||||
|
int listTopMargin;
|
||||||
|
int listNoMargin;
|
||||||
|
};
|
||||||
|
|
||||||
|
//@endcond
|
||||||
@@ -52,8 +52,8 @@ void TextHandler::setData(const QString &string)
|
|||||||
QString TextHandler::handleSendText()
|
QString TextHandler::handleSendText()
|
||||||
{
|
{
|
||||||
m_pos = 0;
|
m_pos = 0;
|
||||||
m_dataBuffer = markdownToHTML(m_data);
|
m_dataBuffer = customMarkdownToHtml(m_data);
|
||||||
m_dataBuffer = customMarkdownToHtml(m_dataBuffer);
|
m_dataBuffer = markdownToHTML(m_dataBuffer);
|
||||||
|
|
||||||
m_nextTokenType = nextTokenType(m_dataBuffer, m_pos, m_nextToken, m_nextTokenType);
|
m_nextTokenType = nextTokenType(m_dataBuffer, m_pos, m_nextToken, m_nextTokenType);
|
||||||
|
|
||||||
@@ -802,6 +802,9 @@ QString TextHandler::customMarkdownToHtml(const QString &stringIn)
|
|||||||
// strikethrough
|
// strikethrough
|
||||||
processSyntax(u"~~"_s, u"<del>"_s, u"</del>"_s);
|
processSyntax(u"~~"_s, u"<del>"_s, u"</del>"_s);
|
||||||
|
|
||||||
|
// underline
|
||||||
|
processSyntax(u"_"_s, u"<u>"_s, u"</u>"_s);
|
||||||
|
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ namespace TextRegex
|
|||||||
static const QRegularExpression endTagType{u"[> /]"_s};
|
static const QRegularExpression endTagType{u"[> /]"_s};
|
||||||
static const QRegularExpression endAttributeType{u"[> ]"_s};
|
static const QRegularExpression endAttributeType{u"[> ]"_s};
|
||||||
static const QRegularExpression attributeData{u"['\"](.*?)['\"]"_s};
|
static const QRegularExpression attributeData{u"['\"](.*?)['\"]"_s};
|
||||||
|
static const QRegularExpression htmlBodyContent{u"<body[^>]*>(.*?)</body>"_s, QRegularExpression::DotMatchesEverythingOption};
|
||||||
static const QRegularExpression removeReply{u"> <.*?>.*?\\n\\n"_s, QRegularExpression::DotMatchesEverythingOption};
|
static const QRegularExpression removeReply{u"> <.*?>.*?\\n\\n"_s, QRegularExpression::DotMatchesEverythingOption};
|
||||||
static const QRegularExpression removeRichReply{u"<mx-reply>.*?</mx-reply>"_s, QRegularExpression::DotMatchesEverythingOption};
|
static const QRegularExpression removeRichReply{u"<mx-reply>.*?</mx-reply>"_s, QRegularExpression::DotMatchesEverythingOption};
|
||||||
static const QRegularExpression codePill{u"<pre><code[^>]*>(.*?)</code></pre>"_s, QRegularExpression::DotMatchesEverythingOption};
|
static const QRegularExpression codePill{u"<pre><code[^>]*>(.*?)</code></pre>"_s, QRegularExpression::DotMatchesEverythingOption};
|
||||||
|
|||||||
Reference in New Issue
Block a user