Separate ChatButtonHelper from ChatDocumentHandler
This commit is contained in:
@@ -28,6 +28,5 @@ TestCase {
|
|||||||
compare(documentHandler.underline, false);
|
compare(documentHandler.underline, false);
|
||||||
compare(documentHandler.strikethrough, false);
|
compare(documentHandler.strikethrough, false);
|
||||||
compare(documentHandler.style, 0);
|
compare(documentHandler.style, 0);
|
||||||
compare(documentHandler.currentListStyle, 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,11 +34,13 @@ public:
|
|||||||
|
|
||||||
QQuickItem *textItem() const
|
QQuickItem *textItem() const
|
||||||
{
|
{
|
||||||
return m_chatMarkdownHelper->textItem();
|
return m_chatMarkdownHelper->textItem()->textItem();
|
||||||
}
|
}
|
||||||
void setTextItem(QQuickItem *textItem)
|
void setTextItem(QQuickItem *textItem)
|
||||||
{
|
{
|
||||||
m_chatMarkdownHelper->setTextItem(textItem);
|
auto textItemWrapper = new QmlTextItemWrapper(this);
|
||||||
|
textItemWrapper->setTextItem(textItem);
|
||||||
|
m_chatMarkdownHelper->setTextItem(textItemWrapper);
|
||||||
m_textItem->setTextItem(textItem);
|
m_textItem->setTextItem(textItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,19 +32,19 @@ TestCase {
|
|||||||
SignalSpy {
|
SignalSpy {
|
||||||
id: spyContentsChanged
|
id: spyContentsChanged
|
||||||
target: qmlTextItemWrapper
|
target: qmlTextItemWrapper
|
||||||
signalName: "textDocumentContentsChanged"
|
signalName: "contentsChanged"
|
||||||
}
|
}
|
||||||
|
|
||||||
SignalSpy {
|
SignalSpy {
|
||||||
id: spyContentsChange
|
id: spyContentsChange
|
||||||
target: qmlTextItemWrapper
|
target: qmlTextItemWrapper
|
||||||
signalName: "textDocumentContentsChange"
|
signalName: "contentsChange"
|
||||||
}
|
}
|
||||||
|
|
||||||
SignalSpy {
|
SignalSpy {
|
||||||
id: spyCursor
|
id: spyCursor
|
||||||
target: qmlTextItemWrapper
|
target: qmlTextItemWrapper
|
||||||
signalName: "textDocumentCursorPositionChanged"
|
signalName: "cursorPositionChanged"
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_item(): void {
|
function test_item(): void {
|
||||||
|
|||||||
@@ -27,12 +27,9 @@ public:
|
|||||||
{
|
{
|
||||||
Q_ASSERT(m_textItemWrapper);
|
Q_ASSERT(m_textItemWrapper);
|
||||||
connect(m_textItemWrapper, &QmlTextItemWrapper::textItemChanged, this, &QmlTextItemWrapperTestWrapper::textItemChanged);
|
connect(m_textItemWrapper, &QmlTextItemWrapper::textItemChanged, this, &QmlTextItemWrapperTestWrapper::textItemChanged);
|
||||||
connect(m_textItemWrapper, &QmlTextItemWrapper::textDocumentContentsChange, this, &QmlTextItemWrapperTestWrapper::textDocumentContentsChange);
|
connect(m_textItemWrapper, &QmlTextItemWrapper::contentsChange, this, &QmlTextItemWrapperTestWrapper::contentsChange);
|
||||||
connect(m_textItemWrapper, &QmlTextItemWrapper::textDocumentContentsChanged, this, &QmlTextItemWrapperTestWrapper::textDocumentContentsChanged);
|
connect(m_textItemWrapper, &QmlTextItemWrapper::contentsChanged, this, &QmlTextItemWrapperTestWrapper::contentsChanged);
|
||||||
connect(m_textItemWrapper,
|
connect(m_textItemWrapper, &QmlTextItemWrapper::cursorPositionChanged, this, &QmlTextItemWrapperTestWrapper::cursorPositionChanged);
|
||||||
&QmlTextItemWrapper::textDocumentCursorPositionChanged,
|
|
||||||
this,
|
|
||||||
&QmlTextItemWrapperTestWrapper::textDocumentCursorPositionChanged);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QQuickItem *textItem() const
|
QQuickItem *textItem() const
|
||||||
@@ -83,9 +80,9 @@ public:
|
|||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void textItemChanged();
|
void textItemChanged();
|
||||||
void textDocumentContentsChange(int position, int charsRemoved, int charsAdded);
|
void contentsChange(int position, int charsRemoved, int charsAdded);
|
||||||
void textDocumentContentsChanged();
|
void contentsChanged();
|
||||||
void textDocumentCursorPositionChanged();
|
void cursorPositionChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QPointer<QmlTextItemWrapper> m_textItemWrapper;
|
QPointer<QmlTextItemWrapper> m_textItemWrapper;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ ecm_add_qml_module(Chatbar GENERATE_PLUGIN_SOURCE
|
|||||||
NewPollDialog.qml
|
NewPollDialog.qml
|
||||||
TableDialog.qml
|
TableDialog.qml
|
||||||
SOURCES
|
SOURCES
|
||||||
|
chatbuttonhelper.cpp
|
||||||
styledelegatehelper.cpp
|
styledelegatehelper.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ QQC2.ToolBar {
|
|||||||
buttonRow.spacing * 9 +
|
buttonRow.spacing * 9 +
|
||||||
3
|
3
|
||||||
|
|
||||||
|
readonly property ChatButtonHelper chatButtonHelper: ChatButtonHelper {
|
||||||
|
textItem: contentModel.currentTextItem
|
||||||
|
}
|
||||||
|
|
||||||
signal clicked
|
signal clicked
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
@@ -82,9 +86,9 @@ QQC2.ToolBar {
|
|||||||
text: i18nc("@action:button", "Bold")
|
text: i18nc("@action:button", "Bold")
|
||||||
display: QQC2.AbstractButton.IconOnly
|
display: QQC2.AbstractButton.IconOnly
|
||||||
checkable: true
|
checkable: true
|
||||||
checked: root.focusedDocumentHandler.bold
|
checked: root.chatButtonHelper.bold
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Bold);
|
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Bold);
|
||||||
root.clicked()
|
root.clicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,9 +107,9 @@ QQC2.ToolBar {
|
|||||||
text: i18nc("@action:button", "Italic")
|
text: i18nc("@action:button", "Italic")
|
||||||
display: QQC2.AbstractButton.IconOnly
|
display: QQC2.AbstractButton.IconOnly
|
||||||
checkable: true
|
checkable: true
|
||||||
checked: root.focusedDocumentHandler.italic
|
checked: root.chatButtonHelper.italic
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Italic);
|
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Italic);
|
||||||
root.clicked()
|
root.clicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,9 +128,9 @@ QQC2.ToolBar {
|
|||||||
text: i18nc("@action:button", "Underline")
|
text: i18nc("@action:button", "Underline")
|
||||||
display: QQC2.AbstractButton.IconOnly
|
display: QQC2.AbstractButton.IconOnly
|
||||||
checkable: true
|
checkable: true
|
||||||
checked: root.focusedDocumentHandler.underline
|
checked: root.chatButtonHelper.underline
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Underline);
|
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Underline);
|
||||||
root.clicked();
|
root.clicked();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,9 +144,9 @@ QQC2.ToolBar {
|
|||||||
text: i18nc("@action:button", "Strikethrough")
|
text: i18nc("@action:button", "Strikethrough")
|
||||||
display: QQC2.AbstractButton.IconOnly
|
display: QQC2.AbstractButton.IconOnly
|
||||||
checkable: true
|
checkable: true
|
||||||
checked: root.focusedDocumentHandler.strikethrough
|
checked: root.chatButtonHelper.strikethrough
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Strikethrough);
|
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Strikethrough);
|
||||||
root.clicked()
|
root.clicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,9 +176,9 @@ 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.focusedDocumentHandler.bold
|
checked: root.chatButtonHelper.bold
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Bold);
|
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Bold);
|
||||||
root.clicked();
|
root.clicked();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,9 +186,9 @@ QQC2.ToolBar {
|
|||||||
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.focusedDocumentHandler.italic
|
checked: root.chatButtonHelper.italic
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Italic);
|
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Italic);
|
||||||
root.clicked();
|
root.clicked();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,9 +196,9 @@ QQC2.ToolBar {
|
|||||||
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.focusedDocumentHandler.underline
|
checked: root.chatButtonHelper.underline
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Underline);
|
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Underline);
|
||||||
root.clicked();
|
root.clicked();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,9 +206,9 @@ QQC2.ToolBar {
|
|||||||
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.focusedDocumentHandler.strikethrough
|
checked: root.chatButtonHelper.strikethrough
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Strikethrough);
|
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Strikethrough);
|
||||||
root.clicked();
|
root.clicked();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,9 +231,9 @@ QQC2.ToolBar {
|
|||||||
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.focusedDocumentHandler.currentListStyle === 1
|
checked: root.chatButtonHelper.unorderedList
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.UnorderedList);
|
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.UnorderedList);
|
||||||
root.clicked();
|
root.clicked();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,9 +247,9 @@ QQC2.ToolBar {
|
|||||||
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.focusedDocumentHandler.currentListStyle === 4
|
checked: root.chatButtonHelper.orderedlist
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.OrderedList);
|
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.OrderedList);
|
||||||
root.clicked();
|
root.clicked();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,10 +260,11 @@ QQC2.ToolBar {
|
|||||||
QQC2.ToolButton {
|
QQC2.ToolButton {
|
||||||
id: indentAction
|
id: indentAction
|
||||||
icon.name: "format-indent-more"
|
icon.name: "format-indent-more"
|
||||||
|
enabled: root.contentModel.focusType !== LibNeoChat.MessageComponentType.Code && root.chatButtonHelper.canIndentListMore
|
||||||
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.focusedDocumentHandler.indentListMore();
|
root.chatButtonHelper.indentListMore();
|
||||||
root.clicked();
|
root.clicked();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,10 +275,11 @@ QQC2.ToolBar {
|
|||||||
QQC2.ToolButton {
|
QQC2.ToolButton {
|
||||||
id: dedentAction
|
id: dedentAction
|
||||||
icon.name: "format-indent-less"
|
icon.name: "format-indent-less"
|
||||||
|
enabled: root.contentModel.focusType !== LibNeoChat.MessageComponentType.Code && root.chatButtonHelper.canIndentListLess
|
||||||
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.focusedDocumentHandler.indentListLess();
|
root.chatButtonHelper.indentListLess();
|
||||||
root.clicked();
|
root.clicked();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,7 +309,7 @@ QQC2.ToolBar {
|
|||||||
icon.name: "format-list-unordered"
|
icon.name: "format-list-unordered"
|
||||||
text: i18nc("@action:button", "Unordered List")
|
text: i18nc("@action:button", "Unordered List")
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.UnorderedList);
|
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.UnorderedList);
|
||||||
root.clicked();
|
root.clicked();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,7 +317,7 @@ QQC2.ToolBar {
|
|||||||
icon.name: "format-list-ordered"
|
icon.name: "format-list-ordered"
|
||||||
text: i18nc("@action:button", "Ordered List")
|
text: i18nc("@action:button", "Ordered List")
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.OrderedList);
|
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.OrderedList);
|
||||||
root.clicked();
|
root.clicked();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -319,7 +325,7 @@ QQC2.ToolBar {
|
|||||||
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: {
|
onTriggered: {
|
||||||
root.focusedDocumentHandler.indentListMore();
|
root.chatButtonHelper.indentListMore();
|
||||||
root.clicked();
|
root.clicked();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -327,7 +333,7 @@ QQC2.ToolBar {
|
|||||||
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: {
|
onTriggered: {
|
||||||
root.focusedDocumentHandler.indentListLess();
|
root.chatButtonHelper.indentListLess();
|
||||||
root.clicked();
|
root.clicked();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -355,6 +361,7 @@ QQC2.ToolBar {
|
|||||||
StylePicker {
|
StylePicker {
|
||||||
id: styleMenu
|
id: styleMenu
|
||||||
chatContentModel: root.contentModel
|
chatContentModel: root.contentModel
|
||||||
|
chatButtonHelper: root.chatButtonHelper
|
||||||
|
|
||||||
onClosed: root.clicked()
|
onClosed: root.clicked()
|
||||||
}
|
}
|
||||||
@@ -392,11 +399,11 @@ 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.focusedDocumentHandler.currentLinkText(),
|
linkText: root.chatButtonHelper.currentLinkText,
|
||||||
linkUrl: root.focusedDocumentHandler.currentLinkUrl()
|
linkUrl: root.chatButtonHelper.currentLinkUrl
|
||||||
})
|
})
|
||||||
dialog.onAccepted.connect(() => {
|
dialog.onAccepted.connect(() => {
|
||||||
root.focusedDocumentHandler.updateLink(dialog.linkUrl, dialog.linkText)
|
root.chatButtonHelper.updateLink(dialog.linkUrl, dialog.linkText)
|
||||||
root.clicked();
|
root.clicked();
|
||||||
});
|
});
|
||||||
dialog.open();
|
dialog.open();
|
||||||
@@ -583,7 +590,7 @@ QQC2.ToolBar {
|
|||||||
currentRoom: root.room
|
currentRoom: root.room
|
||||||
|
|
||||||
onChosen: emoji => {
|
onChosen: emoji => {
|
||||||
root.focusedDocumentHandler.insertText(emoji);
|
root.chatButtonHelper.insertText(emoji);
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
onClosed: if (emojiButton.checked) {
|
onClosed: if (emojiButton.checked) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ QQC2.Popup {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property MessageContent.ChatBarMessageContentModel chatContentModel
|
required property MessageContent.ChatBarMessageContentModel chatContentModel
|
||||||
|
required property ChatButtonHelper chatButtonHelper
|
||||||
readonly property LibNeoChat.ChatDocumentHandler focusedDocumentHandler: chatContentModel.focusedDocumentHandler
|
readonly property LibNeoChat.ChatDocumentHandler focusedDocumentHandler: chatContentModel.focusedDocumentHandler
|
||||||
|
|
||||||
y: -implicitHeight
|
y: -implicitHeight
|
||||||
@@ -47,7 +48,7 @@ QQC2.Popup {
|
|||||||
) {
|
) {
|
||||||
root.chatContentModel.insertStyleAtCursor(styleDelegate.index);
|
root.chatContentModel.insertStyleAtCursor(styleDelegate.index);
|
||||||
} else {
|
} else {
|
||||||
root.focusedDocumentHandler.setFormat(styleDelegate.index);
|
root.chatButtonHelper.setFormat(styleDelegate.index);
|
||||||
}
|
}
|
||||||
root.close();
|
root.close();
|
||||||
}
|
}
|
||||||
|
|||||||
258
src/chatbar/chatbuttonhelper.cpp
Normal file
258
src/chatbar/chatbuttonhelper.cpp
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
// 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 "chatbuttonhelper.h"
|
||||||
|
|
||||||
|
#include <Kirigami/Platform/PlatformTheme>
|
||||||
|
|
||||||
|
#include "enums/richformat.h"
|
||||||
|
|
||||||
|
ChatButtonHelper::ChatButtonHelper(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QmlTextItemWrapper *ChatButtonHelper::textItem() const
|
||||||
|
{
|
||||||
|
return m_textItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatButtonHelper::setTextItem(QmlTextItemWrapper *textItem)
|
||||||
|
{
|
||||||
|
if (textItem == m_textItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_textItem) {
|
||||||
|
m_textItem->disconnect(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_textItem = textItem;
|
||||||
|
|
||||||
|
if (m_textItem) {
|
||||||
|
connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, &ChatButtonHelper::textItemChanged);
|
||||||
|
connect(m_textItem, &QmlTextItemWrapper::formatChanged, this, &ChatButtonHelper::linkChanged);
|
||||||
|
connect(m_textItem, &QmlTextItemWrapper::textFormatChanged, this, &ChatButtonHelper::textFormatChanged);
|
||||||
|
connect(m_textItem, &QmlTextItemWrapper::styleChanged, this, &ChatButtonHelper::styleChanged);
|
||||||
|
connect(m_textItem, &QmlTextItemWrapper::listChanged, this, &ChatButtonHelper::listChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_EMIT textItemChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatButtonHelper::bold() const
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return m_textItem->formatsAtCursor().contains(RichFormat::Bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatButtonHelper::italic() const
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return m_textItem->formatsAtCursor().contains(RichFormat::Italic);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatButtonHelper::underline() const
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return m_textItem->formatsAtCursor().contains(RichFormat::Underline);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatButtonHelper::strikethrough() const
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return m_textItem->formatsAtCursor().contains(RichFormat::Strikethrough);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatButtonHelper::unorderedList() const
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return m_textItem->formatsAtCursor().contains(RichFormat::UnorderedList);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatButtonHelper::orderedList() const
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return m_textItem->formatsAtCursor().contains(RichFormat::OrderedList);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatButtonHelper::setFormat(RichFormat::Format format)
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_textItem->mergeFormatOnCursor(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatButtonHelper::canIndentListMore() const
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return m_textItem->canIndentListMoreAtCursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatButtonHelper::canIndentListLess() const
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return m_textItem->canIndentListLessAtCursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatButtonHelper::indentListMore()
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_textItem->indentListMoreAtCursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatButtonHelper::indentListLess()
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_textItem->indentListLessAtCursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatButtonHelper::insertText(const QString &text)
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_textItem->textCursor().insertText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatButtonHelper::currentLinkUrl() const
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return m_textItem->textCursor().charFormat().anchorHref();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatButtonHelper::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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatButtonHelper::currentLinkText() const
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
QTextCursor cursor = m_textItem->textCursor();
|
||||||
|
selectLinkText(cursor);
|
||||||
|
return cursor.selectedText();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatButtonHelper::updateLink(const QString &linkUrl, const QString &linkText)
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto cursor = m_textItem->textCursor();
|
||||||
|
selectLinkText(cursor);
|
||||||
|
|
||||||
|
cursor.beginEditBlock();
|
||||||
|
|
||||||
|
if (!cursor.hasSelection()) {
|
||||||
|
cursor.select(QTextCursor::WordUnderCursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto originalFormat = cursor.charFormat();
|
||||||
|
auto 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);
|
||||||
|
|
||||||
|
const auto theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
|
||||||
|
format.setUnderlineColor(theme->linkColor());
|
||||||
|
format.setForeground(theme->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);
|
||||||
|
if (cursor.atBlockEnd()) {
|
||||||
|
cursor.insertText(u" "_s, originalFormat);
|
||||||
|
}
|
||||||
|
cursor.endEditBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "moc_chatbuttonhelper.cpp"
|
||||||
135
src/chatbar/chatbuttonhelper.h
Normal file
135
src/chatbar/chatbuttonhelper.h
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
// 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 <QObject>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
|
||||||
|
#include "qmltextitemwrapper.h"
|
||||||
|
|
||||||
|
class ChatButtonHelper : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The text item that the helper is interfacing with.
|
||||||
|
*
|
||||||
|
* This is a QQuickItem that is a TextEdit (or inherited from) wrapped in a QmlTextItemWrapper
|
||||||
|
* to provide easy access to properties and basic QTextDocument manipulation.
|
||||||
|
*
|
||||||
|
* @sa TextEdit, QTextDocument, QmlTextItemWrapper
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(QmlTextItemWrapper *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the text format at the current cursor is bold.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(bool bold READ bold NOTIFY textFormatChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the text format at the current cursor is italic.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(bool italic READ italic NOTIFY textFormatChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the text format at the current cursor is underlined.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(bool underline READ underline NOTIFY textFormatChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the text format at the current cursor is struckthrough.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(bool strikethrough READ strikethrough NOTIFY textFormatChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the format at the current cursor includes RichFormat::UnorderedList.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(bool unorderedList READ unorderedList NOTIFY listChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the format at the current cursor includes RichFormat::OrderedList.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(bool orderedList READ orderedList NOTIFY listChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the list at the current cursor can be indented one level more.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(bool canIndentListMore READ canIndentListMore NOTIFY listChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the list at the current cursor can be indented one level less.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(bool canIndentListLess READ canIndentListLess NOTIFY listChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The link url at the current cursor position.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(QString currentLinkUrl READ currentLinkUrl NOTIFY linkChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The link url at the current cursor position.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(QString currentLinkText READ currentLinkText NOTIFY linkChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ChatButtonHelper(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QmlTextItemWrapper *textItem() const;
|
||||||
|
void setTextItem(QmlTextItemWrapper *textItem);
|
||||||
|
|
||||||
|
bool bold() const;
|
||||||
|
bool italic() const;
|
||||||
|
bool underline() const;
|
||||||
|
bool strikethrough() const;
|
||||||
|
bool unorderedList() const;
|
||||||
|
bool orderedList() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Apply the given format at the current cursor position.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void setFormat(RichFormat::Format format);
|
||||||
|
|
||||||
|
bool canIndentListMore() const;
|
||||||
|
bool canIndentListLess() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Indent the list at the current cursor one level more.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void indentListMore();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Indent the list at the current cursor one level less.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void indentListLess();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Insert text at the current cursor position.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void insertText(const QString &text);
|
||||||
|
|
||||||
|
QString currentLinkUrl() const;
|
||||||
|
QString currentLinkText() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update the link at the current cursor position.
|
||||||
|
*
|
||||||
|
* This will replace any selected text of the word next to the cursor with the
|
||||||
|
* given text and will link to the given url.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void updateLink(const QString &linkUrl, const QString &linkText);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void textItemChanged();
|
||||||
|
void formatChanged();
|
||||||
|
void textFormatChanged();
|
||||||
|
void styleChanged();
|
||||||
|
void listChanged();
|
||||||
|
void linkChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QPointer<QmlTextItemWrapper> m_textItem;
|
||||||
|
|
||||||
|
void selectLinkText(QTextCursor &cursor) const;
|
||||||
|
};
|
||||||
@@ -112,7 +112,6 @@ public:
|
|||||||
mention.cursor.setPosition(mention.cursor.anchor() + mention.text.size(), QTextCursor::KeepAnchor);
|
mention.cursor.setPosition(mention.cursor.anchor() + mention.text.size(), QTextCursor::KeepAnchor);
|
||||||
}
|
}
|
||||||
|
|
||||||
qWarning() << mention.cursor.selectedText() << mention.text;
|
|
||||||
if (mention.cursor.selectedText() != mention.text) {
|
if (mention.cursor.selectedText() != mention.text) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -133,12 +132,9 @@ private:
|
|||||||
ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
|
ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_textItem(new QmlTextItemWrapper(this))
|
, m_textItem(new QmlTextItemWrapper(this))
|
||||||
, m_markdownHelper(new ChatMarkdownHelper(this))
|
|
||||||
, m_highlighter(new SyntaxHighlighter(this))
|
, m_highlighter(new SyntaxHighlighter(this))
|
||||||
{
|
{
|
||||||
connectTextItem();
|
connectTextItem();
|
||||||
|
|
||||||
connect(this, &ChatDocumentHandler::formatChanged, m_markdownHelper, &ChatMarkdownHelper::handleExternalFormatChange);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatBarType::Type ChatDocumentHandler::type() const
|
ChatBarType::Type ChatDocumentHandler::type() const
|
||||||
@@ -178,7 +174,6 @@ QQuickItem *ChatDocumentHandler::textItem() const
|
|||||||
void ChatDocumentHandler::setTextItem(QQuickItem *textItem)
|
void ChatDocumentHandler::setTextItem(QQuickItem *textItem)
|
||||||
{
|
{
|
||||||
m_textItem->setTextItem(textItem);
|
m_textItem->setTextItem(textItem);
|
||||||
m_markdownHelper->setTextItem(textItem);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatDocumentHandler::connectTextItem()
|
void ChatDocumentHandler::connectTextItem()
|
||||||
@@ -189,33 +184,14 @@ void ChatDocumentHandler::connectTextItem()
|
|||||||
initializeChars();
|
initializeChars();
|
||||||
});
|
});
|
||||||
connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, &ChatDocumentHandler::textItemChanged);
|
connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, &ChatDocumentHandler::textItemChanged);
|
||||||
connect(m_textItem, &QmlTextItemWrapper::textDocumentContentsChanged, this, &ChatDocumentHandler::contentsChanged);
|
connect(m_textItem, &QmlTextItemWrapper::contentsChanged, this, &ChatDocumentHandler::contentsChanged);
|
||||||
connect(m_textItem, &QmlTextItemWrapper::textDocumentContentsChanged, this, &ChatDocumentHandler::atFirstLineChanged);
|
connect(m_textItem, &QmlTextItemWrapper::contentsChanged, this, &ChatDocumentHandler::atFirstLineChanged);
|
||||||
connect(m_textItem, &QmlTextItemWrapper::textDocumentContentsChanged, this, &ChatDocumentHandler::atLastLineChanged);
|
connect(m_textItem, &QmlTextItemWrapper::contentsChanged, this, &ChatDocumentHandler::atLastLineChanged);
|
||||||
connect(m_textItem, &QmlTextItemWrapper::textDocumentCursorPositionChanged, this, &ChatDocumentHandler::atFirstLineChanged);
|
connect(m_textItem, &QmlTextItemWrapper::cursorPositionChanged, this, &ChatDocumentHandler::atFirstLineChanged);
|
||||||
connect(m_textItem, &QmlTextItemWrapper::textDocumentCursorPositionChanged, this, &ChatDocumentHandler::atLastLineChanged);
|
connect(m_textItem, &QmlTextItemWrapper::cursorPositionChanged, this, &ChatDocumentHandler::atLastLineChanged);
|
||||||
connect(m_textItem, &QmlTextItemWrapper::textDocumentContentsChange, this, [this](int position) {
|
|
||||||
auto cursor = m_textItem->textCursor();
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cursor.setPosition(position);
|
|
||||||
cursor.movePosition(QTextCursor::NextWord, QTextCursor::KeepAnchor);
|
|
||||||
if (!cursor.selectedText().isEmpty()) {
|
|
||||||
if (m_pendingFormat) {
|
|
||||||
cursor.mergeCharFormat(*m_pendingFormat);
|
|
||||||
m_pendingFormat = std::nullopt;
|
|
||||||
}
|
|
||||||
if (m_pendingOverrideFormat) {
|
|
||||||
cursor.setCharFormat(*m_pendingOverrideFormat);
|
|
||||||
m_pendingOverrideFormat = std::nullopt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
connect(m_textItem, &QmlTextItemWrapper::formatChanged, this, &ChatDocumentHandler::formatChanged);
|
connect(m_textItem, &QmlTextItemWrapper::formatChanged, this, &ChatDocumentHandler::formatChanged);
|
||||||
connect(m_textItem, &QmlTextItemWrapper::textFormatChanged, this, &ChatDocumentHandler::textFormatChanged);
|
connect(m_textItem, &QmlTextItemWrapper::textFormatChanged, this, &ChatDocumentHandler::textFormatChanged);
|
||||||
connect(m_textItem, &QmlTextItemWrapper::styleChanged, this, &ChatDocumentHandler::styleChanged);
|
connect(m_textItem, &QmlTextItemWrapper::styleChanged, this, &ChatDocumentHandler::styleChanged);
|
||||||
connect(m_textItem, &QmlTextItemWrapper::listChanged, this, &ChatDocumentHandler::listChanged);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatDocumentHandler *ChatDocumentHandler::previousDocumentHandler() const
|
ChatDocumentHandler *ChatDocumentHandler::previousDocumentHandler() const
|
||||||
@@ -539,56 +515,6 @@ void ChatDocumentHandler::updateMentions(const QString &editId)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatDocumentHandler::setTextColor(const QColor &color)
|
|
||||||
{
|
|
||||||
QTextCharFormat format;
|
|
||||||
format.setForeground(QBrush(color));
|
|
||||||
mergeFormatOnWordOrSelection(format);
|
|
||||||
Q_EMIT textColorChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatDocumentHandler::bold() const
|
|
||||||
{
|
|
||||||
QTextCursor cursor = m_textItem->textCursor();
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return cursor.charFormat().fontWeight() == QFont::Bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatDocumentHandler::italic() const
|
|
||||||
{
|
|
||||||
QTextCursor cursor = m_textItem->textCursor();
|
|
||||||
if (cursor.isNull())
|
|
||||||
return false;
|
|
||||||
return cursor.charFormat().fontItalic();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatDocumentHandler::underline() const
|
|
||||||
{
|
|
||||||
QTextCursor cursor = m_textItem->textCursor();
|
|
||||||
if (cursor.isNull())
|
|
||||||
return false;
|
|
||||||
return cursor.charFormat().fontUnderline();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatDocumentHandler::strikethrough() const
|
|
||||||
{
|
|
||||||
QTextCursor cursor = m_textItem->textCursor();
|
|
||||||
if (cursor.isNull())
|
|
||||||
return false;
|
|
||||||
return cursor.charFormat().fontStrikeOut();
|
|
||||||
}
|
|
||||||
|
|
||||||
QColor ChatDocumentHandler::textColor() const
|
|
||||||
{
|
|
||||||
QTextCursor cursor = m_textItem->textCursor();
|
|
||||||
if (cursor.isNull())
|
|
||||||
return QColor(Qt::black);
|
|
||||||
QTextCharFormat format = cursor.charFormat();
|
|
||||||
return format.foreground().color();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<Qt::TextFormat> ChatDocumentHandler::textFormat() const
|
std::optional<Qt::TextFormat> ChatDocumentHandler::textFormat() const
|
||||||
{
|
{
|
||||||
if (!m_textItem) {
|
if (!m_textItem) {
|
||||||
@@ -606,57 +532,6 @@ void ChatDocumentHandler::mergeFormatOnWordOrSelection(const QTextCharFormat &fo
|
|||||||
}
|
}
|
||||||
if (cursor.hasSelection()) {
|
if (cursor.hasSelection()) {
|
||||||
cursor.mergeCharFormat(format);
|
cursor.mergeCharFormat(format);
|
||||||
} else {
|
|
||||||
m_pendingFormat = format.toCharFormat();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatDocumentHandler::currentLinkText() const
|
|
||||||
{
|
|
||||||
QTextCursor cursor = m_textItem->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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -693,113 +568,6 @@ void ChatDocumentHandler::insertCompletion(const QString &text, const QUrl &link
|
|||||||
m_highlighter->rehighlight();
|
m_highlighter->rehighlight();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatDocumentHandler::updateLink(const QString &linkUrl, const QString &linkText)
|
|
||||||
{
|
|
||||||
auto cursor = m_textItem->textCursor();
|
|
||||||
selectLinkText(&cursor);
|
|
||||||
|
|
||||||
cursor.beginEditBlock();
|
|
||||||
|
|
||||||
if (!cursor.hasSelection()) {
|
|
||||||
cursor.select(QTextCursor::WordUnderCursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto originalFormat = cursor.charFormat();
|
|
||||||
auto 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();
|
|
||||||
|
|
||||||
m_pendingOverrideFormat = originalFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::setFormat(RichFormat::Format format)
|
|
||||||
{
|
|
||||||
QTextCursor cursor = m_textItem->textCursor();
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_textItem->mergeFormatOnCursor(format, cursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChatDocumentHandler::currentListStyle() const
|
|
||||||
{
|
|
||||||
return m_textItem->currentListStyle();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatDocumentHandler::canIndentListMore() const
|
|
||||||
{
|
|
||||||
return m_textItem->canIndentListMore();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatDocumentHandler::canIndentListLess() const
|
|
||||||
{
|
|
||||||
return m_textItem->canIndentListLess();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::indentListMore()
|
|
||||||
{
|
|
||||||
m_textItem->indentListMore();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::indentListLess()
|
|
||||||
{
|
|
||||||
m_textItem->indentListLess();
|
|
||||||
}
|
|
||||||
|
|
||||||
RichFormat::Format ChatDocumentHandler::style() const
|
|
||||||
{
|
|
||||||
return static_cast<RichFormat::Format>(m_textItem->textCursor().blockFormat().headingLevel());
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::tab()
|
void ChatDocumentHandler::tab()
|
||||||
{
|
{
|
||||||
QTextCursor cursor = m_textItem->textCursor();
|
QTextCursor cursor = m_textItem->textCursor();
|
||||||
@@ -807,10 +575,10 @@ void ChatDocumentHandler::tab()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (cursor.currentList()) {
|
if (cursor.currentList()) {
|
||||||
indentListMore();
|
m_textItem->indentListMoreAtCursor();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
insertText(u" "_s);
|
cursor.insertText(u" "_s);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatDocumentHandler::deleteChar()
|
void ChatDocumentHandler::deleteChar()
|
||||||
@@ -835,9 +603,8 @@ void ChatDocumentHandler::backspace()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (cursor.position() <= m_fixedStartChars.length()) {
|
if (cursor.position() <= m_fixedStartChars.length()) {
|
||||||
qWarning() << "unhandled backspace";
|
|
||||||
if (cursor.currentList()) {
|
if (cursor.currentList()) {
|
||||||
indentListLess();
|
m_textItem->indentListLessAtCursor();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (const auto previousHandler = previousDocumentHandler()) {
|
if (const auto previousHandler = previousDocumentHandler()) {
|
||||||
@@ -859,16 +626,6 @@ void ChatDocumentHandler::insertReturn()
|
|||||||
cursor.insertBlock();
|
cursor.insertBlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatDocumentHandler::insertText(const QString &text)
|
|
||||||
{
|
|
||||||
m_textItem->textCursor().insertText(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatDocumentHandler::currentLinkUrl() const
|
|
||||||
{
|
|
||||||
return m_textItem->textCursor().charFormat().anchorHref();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::dumpHtml()
|
void ChatDocumentHandler::dumpHtml()
|
||||||
{
|
{
|
||||||
qWarning() << htmlText();
|
qWarning() << htmlText();
|
||||||
|
|||||||
@@ -95,17 +95,6 @@ class ChatDocumentHandler : public QObject
|
|||||||
*/
|
*/
|
||||||
Q_PROPERTY(bool atLastLine READ atLastLine NOTIFY atLastLineChanged)
|
Q_PROPERTY(bool atLastLine READ atLastLine NOTIFY atLastLineChanged)
|
||||||
|
|
||||||
Q_PROPERTY(QColor textColor READ textColor WRITE setTextColor NOTIFY textColorChanged)
|
|
||||||
|
|
||||||
Q_PROPERTY(bool bold READ bold NOTIFY textFormatChanged)
|
|
||||||
Q_PROPERTY(bool italic READ italic NOTIFY textFormatChanged)
|
|
||||||
Q_PROPERTY(bool underline READ underline NOTIFY textFormatChanged)
|
|
||||||
Q_PROPERTY(bool strikethrough READ strikethrough NOTIFY textFormatChanged)
|
|
||||||
|
|
||||||
Q_PROPERTY(RichFormat::Format style READ style NOTIFY styleChanged)
|
|
||||||
|
|
||||||
Q_PROPERTY(int currentListStyle READ currentListStyle NOTIFY listChanged)
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum InsertPosition {
|
enum InsertPosition {
|
||||||
Cursor,
|
Cursor,
|
||||||
@@ -152,33 +141,11 @@ public:
|
|||||||
*/
|
*/
|
||||||
Q_INVOKABLE void updateMentions(const QString &editId);
|
Q_INVOKABLE void updateMentions(const QString &editId);
|
||||||
|
|
||||||
QColor textColor() const;
|
|
||||||
void setTextColor(const QColor &color);
|
|
||||||
|
|
||||||
bool bold() const;
|
|
||||||
bool italic() const;
|
|
||||||
bool underline() const;
|
|
||||||
bool strikethrough() const;
|
|
||||||
|
|
||||||
Q_INVOKABLE void setFormat(RichFormat::Format format);
|
|
||||||
|
|
||||||
int currentListStyle() const;
|
|
||||||
bool canIndentListMore() const;
|
|
||||||
bool canIndentListLess() const;
|
|
||||||
Q_INVOKABLE void indentListLess();
|
|
||||||
Q_INVOKABLE void indentListMore();
|
|
||||||
|
|
||||||
RichFormat::Format style() const;
|
|
||||||
|
|
||||||
Q_INVOKABLE void tab();
|
Q_INVOKABLE void tab();
|
||||||
Q_INVOKABLE void deleteChar();
|
Q_INVOKABLE void deleteChar();
|
||||||
Q_INVOKABLE void backspace();
|
Q_INVOKABLE void backspace();
|
||||||
Q_INVOKABLE void insertReturn();
|
Q_INVOKABLE void insertReturn();
|
||||||
Q_INVOKABLE void insertText(const QString &text);
|
|
||||||
void insertFragment(const QTextDocumentFragment fragment, InsertPosition position = Cursor, bool keepPosition = false);
|
void insertFragment(const QTextDocumentFragment fragment, InsertPosition position = Cursor, bool keepPosition = false);
|
||||||
Q_INVOKABLE QString currentLinkUrl() const;
|
|
||||||
Q_INVOKABLE QString currentLinkText() const;
|
|
||||||
Q_INVOKABLE void updateLink(const QString &linkUrl, const QString &linkText);
|
|
||||||
Q_INVOKABLE void insertCompletion(const QString &text, const QUrl &link);
|
Q_INVOKABLE void insertCompletion(const QString &text, const QUrl &link);
|
||||||
|
|
||||||
Q_INVOKABLE void dumpHtml();
|
Q_INVOKABLE void dumpHtml();
|
||||||
@@ -192,14 +159,11 @@ Q_SIGNALS:
|
|||||||
void atFirstLineChanged();
|
void atFirstLineChanged();
|
||||||
void atLastLineChanged();
|
void atLastLineChanged();
|
||||||
|
|
||||||
void textColorChanged();
|
|
||||||
|
|
||||||
void currentListStyleChanged();
|
void currentListStyleChanged();
|
||||||
|
|
||||||
void formatChanged();
|
void formatChanged();
|
||||||
void textFormatChanged();
|
void textFormatChanged();
|
||||||
void styleChanged();
|
void styleChanged();
|
||||||
void listChanged();
|
|
||||||
|
|
||||||
void contentsChanged();
|
void contentsChanged();
|
||||||
|
|
||||||
@@ -220,10 +184,6 @@ private:
|
|||||||
QString m_initialText = {};
|
QString m_initialText = {};
|
||||||
void initializeChars();
|
void initializeChars();
|
||||||
|
|
||||||
QPointer<ChatMarkdownHelper> m_markdownHelper;
|
|
||||||
std::optional<QTextCharFormat> m_pendingFormat = std::nullopt;
|
|
||||||
std::optional<QTextCharFormat> m_pendingOverrideFormat = std::nullopt;
|
|
||||||
|
|
||||||
SyntaxHighlighter *m_highlighter = nullptr;
|
SyntaxHighlighter *m_highlighter = nullptr;
|
||||||
|
|
||||||
QString getText() const;
|
QString getText() const;
|
||||||
@@ -231,10 +191,6 @@ private:
|
|||||||
|
|
||||||
std::optional<Qt::TextFormat> textFormat() const;
|
std::optional<Qt::TextFormat> textFormat() const;
|
||||||
void mergeFormatOnWordOrSelection(const QTextCharFormat &format);
|
void mergeFormatOnWordOrSelection(const QTextCharFormat &format);
|
||||||
void selectLinkText(QTextCursor *cursor) const;
|
|
||||||
QColor linkColor();
|
|
||||||
QColor mLinkColor;
|
|
||||||
void regenerateColorScheme();
|
|
||||||
|
|
||||||
QString trim(QString string) const;
|
QString trim(QString string) const;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -83,32 +83,39 @@ std::optional<MarkdownSyntax> syntaxForSequence(const QString &sequence)
|
|||||||
|
|
||||||
ChatMarkdownHelper::ChatMarkdownHelper(QObject *parent)
|
ChatMarkdownHelper::ChatMarkdownHelper(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_textItem(new QmlTextItemWrapper(this))
|
|
||||||
{
|
{
|
||||||
connectTextItem();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QQuickItem *ChatMarkdownHelper::textItem() const
|
QmlTextItemWrapper *ChatMarkdownHelper::textItem() const
|
||||||
{
|
{
|
||||||
return m_textItem->textItem();
|
return m_textItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatMarkdownHelper::setTextItem(QQuickItem *textItem)
|
void ChatMarkdownHelper::setTextItem(QmlTextItemWrapper *textItem)
|
||||||
{
|
{
|
||||||
m_textItem->setTextItem(textItem);
|
if (textItem == m_textItem) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
void ChatMarkdownHelper::connectTextItem()
|
if (m_textItem) {
|
||||||
{
|
m_textItem->disconnect(this);
|
||||||
connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, &ChatMarkdownHelper::textItemChanged);
|
}
|
||||||
connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, [this]() {
|
|
||||||
m_startPos = m_textItem->cursorPosition();
|
m_textItem = textItem;
|
||||||
m_endPos = m_startPos;
|
|
||||||
if (m_startPos == 0) {
|
if (m_textItem) {
|
||||||
m_currentState = Pre;
|
connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, &ChatMarkdownHelper::textItemChanged);
|
||||||
}
|
connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, [this]() {
|
||||||
});
|
m_startPos = m_textItem->cursorPosition();
|
||||||
connect(m_textItem, &QmlTextItemWrapper::textDocumentContentsChange, this, &ChatMarkdownHelper::checkMarkdown);
|
m_endPos = m_startPos;
|
||||||
|
if (m_startPos == 0) {
|
||||||
|
m_currentState = Pre;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(m_textItem, &QmlTextItemWrapper::contentsChange, this, &ChatMarkdownHelper::checkMarkdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_EMIT textItemChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatMarkdownHelper::checkMarkdown(int position, int charsRemoved, int charsAdded)
|
void ChatMarkdownHelper::checkMarkdown(int position, int charsRemoved, int charsAdded)
|
||||||
@@ -140,7 +147,6 @@ void ChatMarkdownHelper::checkMarkdown(int position, int charsRemoved, int chars
|
|||||||
cursor.setPosition(m_startPos);
|
cursor.setPosition(m_startPos);
|
||||||
|
|
||||||
const auto result = checkSequence(currentMarkdown, nextChar, cursor.atBlockStart());
|
const auto result = checkSequence(currentMarkdown, nextChar, cursor.atBlockStart());
|
||||||
qWarning() << m_startPos << m_endPos << result;
|
|
||||||
|
|
||||||
switch (m_currentState) {
|
switch (m_currentState) {
|
||||||
case None:
|
case None:
|
||||||
@@ -216,7 +222,6 @@ void ChatMarkdownHelper::complete()
|
|||||||
m_endPos = result ? m_startPos + 1 : m_startPos;
|
m_endPos = result ? m_startPos + 1 : m_startPos;
|
||||||
|
|
||||||
cursor.endEditBlock();
|
cursor.endEditBlock();
|
||||||
qWarning() << m_currentState << m_startPos << m_endPos << m_textItem->cursorPosition();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatMarkdownHelper::handleExternalFormatChange()
|
void ChatMarkdownHelper::handleExternalFormatChange()
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ class ChatMarkdownHelper : public QObject
|
|||||||
public:
|
public:
|
||||||
explicit ChatMarkdownHelper(QObject *parent = nullptr);
|
explicit ChatMarkdownHelper(QObject *parent = nullptr);
|
||||||
|
|
||||||
QQuickItem *textItem() const;
|
QmlTextItemWrapper *textItem() const;
|
||||||
void setTextItem(QQuickItem *textItem);
|
void setTextItem(QmlTextItemWrapper *textItem);
|
||||||
|
|
||||||
void handleExternalFormatChange();
|
void handleExternalFormatChange();
|
||||||
|
|
||||||
@@ -36,7 +36,6 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
QPointer<QmlTextItemWrapper> m_textItem;
|
QPointer<QmlTextItemWrapper> m_textItem;
|
||||||
void connectTextItem();
|
|
||||||
|
|
||||||
State m_currentState = None;
|
State m_currentState = None;
|
||||||
int m_startPos = 0;
|
int m_startPos = 0;
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ CompletionModel::CompletionModel(QObject *parent)
|
|||||||
, m_emojiModel(new QConcatenateTablesProxyModel(this))
|
, m_emojiModel(new QConcatenateTablesProxyModel(this))
|
||||||
{
|
{
|
||||||
connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, &CompletionModel::textItemChanged);
|
connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, &CompletionModel::textItemChanged);
|
||||||
connect(m_textItem, &QmlTextItemWrapper::textDocumentCursorPositionChanged, this, &CompletionModel::updateTextStart);
|
connect(m_textItem, &QmlTextItemWrapper::cursorPositionChanged, this, &CompletionModel::updateTextStart);
|
||||||
connect(m_textItem, &QmlTextItemWrapper::textDocumentContentsChanged, this, &CompletionModel::updateCompletion);
|
connect(m_textItem, &QmlTextItemWrapper::contentsChanged, this, &CompletionModel::updateCompletion);
|
||||||
|
|
||||||
m_emojiModel->addSourceModel(&CustomEmojiModel::instance());
|
m_emojiModel->addSourceModel(&CustomEmojiModel::instance());
|
||||||
m_emojiModel->addSourceModel(&EmojiModel::instance());
|
m_emojiModel->addSourceModel(&EmojiModel::instance());
|
||||||
|
|||||||
@@ -39,39 +39,30 @@ bool NestedListHelper::handleBeforeKeyPressEvent(QKeyEvent *event, const QTextCu
|
|||||||
|
|
||||||
bool NestedListHelper::canIndent(const QTextCursor &textCursor) const
|
bool NestedListHelper::canIndent(const QTextCursor &textCursor) const
|
||||||
{
|
{
|
||||||
if ((textCursor.block().isValid())
|
const auto block = textCursor.block();
|
||||||
// && ( textEdit->textCursor().block().previous().isValid() )
|
if (textCursor.isNull() || !block.isValid()) {
|
||||||
) {
|
return false;
|
||||||
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;
|
|
||||||
|
if (!block.textList()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return block.textList()->format().indent() < 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NestedListHelper::canDedent(const QTextCursor &textCursor) const
|
bool NestedListHelper::canDedent(const QTextCursor &textCursor) const
|
||||||
{
|
{
|
||||||
QTextBlock thisBlock = textCursor.block();
|
const auto block = textCursor.block();
|
||||||
QTextBlock nextBlock = thisBlock.next();
|
if (textCursor.isNull() || !block.isValid()) {
|
||||||
if (thisBlock.isValid()) {
|
return false;
|
||||||
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;
|
|
||||||
|
if (!block.textList()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return block.textList()->format().indent() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NestedListHelper::handleAfterKeyPressEvent(QKeyEvent *event, const QTextCursor &cursor)
|
bool NestedListHelper::handleAfterKeyPressEvent(QKeyEvent *event, const QTextCursor &cursor)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// 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
|
||||||
|
|
||||||
#include "qmltextitemwrapper.h"
|
#include "qmltextitemwrapper.h"
|
||||||
|
#include "richformat.h"
|
||||||
|
|
||||||
#include <QQuickTextDocument>
|
#include <QQuickTextDocument>
|
||||||
#include <QTextCursor>
|
#include <QTextCursor>
|
||||||
@@ -34,14 +35,18 @@ void QmlTextItemWrapper::setTextItem(QQuickItem *textItem)
|
|||||||
m_textItem = textItem;
|
m_textItem = textItem;
|
||||||
|
|
||||||
if (m_textItem) {
|
if (m_textItem) {
|
||||||
connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(textDocCursorChanged()));
|
connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(itemCursorPositionChanged()));
|
||||||
if (document()) {
|
if (document()) {
|
||||||
connect(document(), &QTextDocument::contentsChanged, this, &QmlTextItemWrapper::textDocumentContentsChanged);
|
connect(document(), &QTextDocument::contentsChanged, this, &QmlTextItemWrapper::contentsChanged);
|
||||||
connect(document(), &QTextDocument::contentsChange, this, &QmlTextItemWrapper::textDocumentContentsChange);
|
connect(document(), &QTextDocument::contentsChange, this, &QmlTextItemWrapper::contentsChange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Q_EMIT textItemChanged();
|
Q_EMIT textItemChanged();
|
||||||
|
Q_EMIT formatChanged();
|
||||||
|
Q_EMIT textFormatChanged();
|
||||||
|
Q_EMIT styleChanged();
|
||||||
|
Q_EMIT listChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
QTextDocument *QmlTextItemWrapper::document() const
|
QTextDocument *QmlTextItemWrapper::document() const
|
||||||
@@ -109,15 +114,33 @@ void QmlTextItemWrapper::setCursorVisible(bool visible)
|
|||||||
m_textItem->setProperty("cursorVisible", visible);
|
m_textItem->setProperty("cursorVisible", visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
void QmlTextItemWrapper::textDocCursorChanged()
|
void QmlTextItemWrapper::itemCursorPositionChanged()
|
||||||
{
|
{
|
||||||
Q_EMIT textDocumentCursorPositionChanged();
|
Q_EMIT cursorPositionChanged();
|
||||||
|
Q_EMIT formatChanged();
|
||||||
|
Q_EMIT textFormatChanged();
|
||||||
|
Q_EMIT styleChanged();
|
||||||
|
Q_EMIT listChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void QmlTextItemWrapper::mergeFormatOnCursor(RichFormat::Format format, const QTextCursor &cursor)
|
QList<RichFormat::Format> QmlTextItemWrapper::formatsAtCursor(QTextCursor cursor) const
|
||||||
{
|
{
|
||||||
if (cursor.isNull()) {
|
if (cursor.isNull()) {
|
||||||
return;
|
cursor = textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return RichFormat::formatsAtCursor(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void QmlTextItemWrapper::mergeFormatOnCursor(RichFormat::Format format, QTextCursor cursor)
|
||||||
|
{
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
cursor = textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
switch (RichFormat::typeForFormat(format)) {
|
switch (RichFormat::typeForFormat(format)) {
|
||||||
case RichFormat::Text:
|
case RichFormat::Text:
|
||||||
@@ -126,6 +149,10 @@ void QmlTextItemWrapper::mergeFormatOnCursor(RichFormat::Format format, const QT
|
|||||||
case RichFormat::List:
|
case RichFormat::List:
|
||||||
mergeListFormatOnCursor(format, cursor);
|
mergeListFormatOnCursor(format, cursor);
|
||||||
return;
|
return;
|
||||||
|
case RichFormat::Block:
|
||||||
|
if (format != RichFormat::Paragraph) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
case RichFormat::Style:
|
case RichFormat::Style:
|
||||||
mergeStyleFormatOnCursor(format, cursor);
|
mergeStyleFormatOnCursor(format, cursor);
|
||||||
return;
|
return;
|
||||||
@@ -194,48 +221,47 @@ void QmlTextItemWrapper::mergeListFormatOnCursor(RichFormat::Format format, cons
|
|||||||
Q_EMIT listChanged();
|
Q_EMIT listChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
int QmlTextItemWrapper::currentListStyle() const
|
bool QmlTextItemWrapper::canIndentListMoreAtCursor(QTextCursor cursor) const
|
||||||
{
|
{
|
||||||
auto cursor = textCursor();
|
|
||||||
if (cursor.isNull() || !textCursor().currentList()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return -cursor.currentList()->format().style();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool QmlTextItemWrapper::canIndentListMore() const
|
|
||||||
{
|
|
||||||
auto cursor = textCursor();
|
|
||||||
if (cursor.isNull()) {
|
if (cursor.isNull()) {
|
||||||
return false;
|
cursor = textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return m_nestedListHelper.canIndent(cursor) && cursor.blockFormat().headingLevel() == 0;
|
return m_nestedListHelper.canIndent(cursor) && cursor.blockFormat().headingLevel() == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool QmlTextItemWrapper::canIndentListLess() const
|
bool QmlTextItemWrapper::canIndentListLessAtCursor(QTextCursor cursor) const
|
||||||
{
|
{
|
||||||
auto cursor = textCursor();
|
|
||||||
if (cursor.isNull()) {
|
if (cursor.isNull()) {
|
||||||
return false;
|
cursor = textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return m_nestedListHelper.canDedent(cursor) && cursor.blockFormat().headingLevel() == 0;
|
return m_nestedListHelper.canDedent(cursor) && cursor.blockFormat().headingLevel() == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void QmlTextItemWrapper::indentListMore()
|
void QmlTextItemWrapper::indentListMoreAtCursor(QTextCursor cursor)
|
||||||
{
|
{
|
||||||
auto cursor = textCursor();
|
|
||||||
if (cursor.isNull()) {
|
if (cursor.isNull()) {
|
||||||
return;
|
cursor = textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
m_nestedListHelper.handleOnIndentMore(cursor);
|
m_nestedListHelper.handleOnIndentMore(cursor);
|
||||||
Q_EMIT listChanged();
|
Q_EMIT listChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void QmlTextItemWrapper::indentListLess()
|
void QmlTextItemWrapper::indentListLessAtCursor(QTextCursor cursor)
|
||||||
{
|
{
|
||||||
auto cursor = textCursor();
|
|
||||||
if (cursor.isNull()) {
|
if (cursor.isNull()) {
|
||||||
return;
|
cursor = textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
m_nestedListHelper.handleOnIndentLess(cursor);
|
m_nestedListHelper.handleOnIndentLess(cursor);
|
||||||
Q_EMIT listChanged();
|
Q_EMIT listChanged();
|
||||||
|
|||||||
@@ -38,24 +38,24 @@ public:
|
|||||||
void setCursorPosition(int pos);
|
void setCursorPosition(int pos);
|
||||||
void setCursorVisible(bool visible);
|
void setCursorVisible(bool visible);
|
||||||
|
|
||||||
void mergeFormatOnCursor(RichFormat::Format format, const QTextCursor &cursor);
|
QList<RichFormat::Format> formatsAtCursor(QTextCursor cursor = {}) const;
|
||||||
|
void mergeFormatOnCursor(RichFormat::Format format, QTextCursor cursor = {});
|
||||||
|
|
||||||
int currentListStyle() const;
|
bool canIndentListMoreAtCursor(QTextCursor cursor = {}) const;
|
||||||
bool canIndentListMore() const;
|
bool canIndentListLessAtCursor(QTextCursor cursor = {}) const;
|
||||||
bool canIndentListLess() const;
|
void indentListMoreAtCursor(QTextCursor cursor = {});
|
||||||
void indentListMore();
|
void indentListLessAtCursor(QTextCursor cursor = {});
|
||||||
void indentListLess();
|
|
||||||
|
|
||||||
void forceActiveFocus() const;
|
void forceActiveFocus() const;
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void textItemChanged();
|
void textItemChanged();
|
||||||
|
|
||||||
void textDocumentContentsChange(int position, int charsRemoved, int charsAdded);
|
void contentsChange(int position, int charsRemoved, int charsAdded);
|
||||||
|
|
||||||
void textDocumentContentsChanged();
|
void contentsChanged();
|
||||||
|
|
||||||
void textDocumentCursorPositionChanged();
|
void cursorPositionChanged();
|
||||||
|
|
||||||
void formatChanged();
|
void formatChanged();
|
||||||
void textFormatChanged();
|
void textFormatChanged();
|
||||||
@@ -74,5 +74,5 @@ private:
|
|||||||
NestedListHelper m_nestedListHelper;
|
NestedListHelper m_nestedListHelper;
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void textDocCursorChanged();
|
void itemCursorPositionChanged();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,12 +9,17 @@
|
|||||||
#include "chatdocumenthandler.h"
|
#include "chatdocumenthandler.h"
|
||||||
#include "enums/chatbartype.h"
|
#include "enums/chatbartype.h"
|
||||||
#include "enums/messagecomponenttype.h"
|
#include "enums/messagecomponenttype.h"
|
||||||
|
#include "enums/richformat.h"
|
||||||
#include "messagecontentmodel.h"
|
#include "messagecontentmodel.h"
|
||||||
|
#include "qmltextitemwrapper.h"
|
||||||
|
|
||||||
ChatBarMessageContentModel::ChatBarMessageContentModel(QObject *parent)
|
ChatBarMessageContentModel::ChatBarMessageContentModel(QObject *parent)
|
||||||
: MessageContentModel(parent)
|
: MessageContentModel(parent)
|
||||||
|
, m_currentTextItem(new QmlTextItemWrapper(this))
|
||||||
|
, m_markdownHelper(new ChatMarkdownHelper(this))
|
||||||
{
|
{
|
||||||
m_editableActive = true;
|
m_editableActive = true;
|
||||||
|
connectCurentTextItem();
|
||||||
initializeModel();
|
initializeModel();
|
||||||
|
|
||||||
connect(this, &ChatBarMessageContentModel::roomChanged, this, [this]() {
|
connect(this, &ChatBarMessageContentModel::roomChanged, this, [this]() {
|
||||||
@@ -66,6 +71,19 @@ void ChatBarMessageContentModel::initializeModel()
|
|||||||
Q_EMIT focusRowChanged();
|
Q_EMIT focusRowChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ChatBarMessageContentModel::connectCurentTextItem()
|
||||||
|
{
|
||||||
|
if (const auto docHandler = focusedDocumentHandler()) {
|
||||||
|
m_currentTextItem->setTextItem(docHandler->textItem());
|
||||||
|
}
|
||||||
|
connect(this, &ChatBarMessageContentModel::focusRowChanged, this, [this]() {
|
||||||
|
if (const auto docHandler = focusedDocumentHandler()) {
|
||||||
|
m_currentTextItem->setTextItem(docHandler->textItem());
|
||||||
|
m_markdownHelper->setTextItem(m_currentTextItem);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void ChatBarMessageContentModel::connectHandler(ChatDocumentHandler *handler)
|
void ChatBarMessageContentModel::connectHandler(ChatDocumentHandler *handler)
|
||||||
{
|
{
|
||||||
connect(handler, &ChatDocumentHandler::contentsChanged, this, &ChatBarMessageContentModel::updateCache);
|
connect(handler, &ChatDocumentHandler::contentsChanged, this, &ChatBarMessageContentModel::updateCache);
|
||||||
@@ -176,7 +194,6 @@ void ChatBarMessageContentModel::setFocusIndex(const QModelIndex &index, bool mo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Q_EMIT focusRowChanged();
|
|
||||||
emitFocusChangeSignals();
|
emitFocusChangeSignals();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +217,11 @@ void ChatBarMessageContentModel::refocusCurrentComponent() const
|
|||||||
chatDocumentHandler->textItem()->forceActiveFocus();
|
chatDocumentHandler->textItem()->forceActiveFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QmlTextItemWrapper *ChatBarMessageContentModel::currentTextItem() const
|
||||||
|
{
|
||||||
|
return m_currentTextItem;
|
||||||
|
}
|
||||||
|
|
||||||
ChatDocumentHandler *ChatBarMessageContentModel::focusedDocumentHandler() const
|
ChatDocumentHandler *ChatBarMessageContentModel::focusedDocumentHandler() const
|
||||||
{
|
{
|
||||||
if (!m_currentFocusComponent.isValid()) {
|
if (!m_currentFocusComponent.isValid()) {
|
||||||
@@ -214,6 +236,7 @@ ChatDocumentHandler *ChatBarMessageContentModel::focusedDocumentHandler() const
|
|||||||
|
|
||||||
void ChatBarMessageContentModel::emitFocusChangeSignals()
|
void ChatBarMessageContentModel::emitFocusChangeSignals()
|
||||||
{
|
{
|
||||||
|
Q_EMIT focusRowChanged();
|
||||||
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {CurrentFocusRole});
|
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {CurrentFocusRole});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,7 +321,7 @@ void ChatBarMessageContentModel::insertComponentAtCursor(MessageComponentType::T
|
|||||||
{
|
{
|
||||||
if (m_components[m_currentFocusComponent.row()].type == type) {
|
if (m_components[m_currentFocusComponent.row()].type == type) {
|
||||||
if (type == MessageComponentType::Text && focusedDocumentHandler()) {
|
if (type == MessageComponentType::Text && focusedDocumentHandler()) {
|
||||||
focusedDocumentHandler()->setFormat(RichFormat::Paragraph);
|
currentTextItem()->mergeFormatOnCursor(RichFormat::Paragraph);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -324,7 +347,6 @@ void ChatBarMessageContentModel::insertComponentAtCursor(MessageComponentType::T
|
|||||||
insertChatDocumentHandler->insertFragment(midFragment);
|
insertChatDocumentHandler->insertFragment(midFragment);
|
||||||
}
|
}
|
||||||
m_currentFocusComponent = QPersistentModelIndex(index(insertIt - m_components.begin()));
|
m_currentFocusComponent = QPersistentModelIndex(index(insertIt - m_components.begin()));
|
||||||
Q_EMIT focusRowChanged();
|
|
||||||
emitFocusChangeSignals();
|
emitFocusChangeSignals();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "enums/richformat.h"
|
#include "enums/richformat.h"
|
||||||
#include "messagecomponent.h"
|
#include "messagecomponent.h"
|
||||||
#include "models/messagecontentmodel.h"
|
#include "models/messagecontentmodel.h"
|
||||||
|
#include "qmltextitemwrapper.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class ChatBarMessageContentModel
|
* @class ChatBarMessageContentModel
|
||||||
@@ -38,6 +39,16 @@ class ChatBarMessageContentModel : public MessageContentModel
|
|||||||
*/
|
*/
|
||||||
Q_PROPERTY(MessageComponentType::Type focusType READ focusType NOTIFY focusRowChanged)
|
Q_PROPERTY(MessageComponentType::Type focusType READ focusType NOTIFY focusRowChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The text item that the helper is interfacing with.
|
||||||
|
*
|
||||||
|
* This is a QQuickItem that is a TextEdit (or inherited from) wrapped in a QmlTextItemWrapper
|
||||||
|
* to provide easy access to properties and basic QTextDocument manipulation.
|
||||||
|
*
|
||||||
|
* @sa TextEdit, QTextDocument, QmlTextItemWrapper
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(QmlTextItemWrapper *currentTextItem READ currentTextItem NOTIFY focusRowChanged)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The ChatDocumentHandler of the model component that currently has focus.
|
* @brief The ChatDocumentHandler of the model component that currently has focus.
|
||||||
*/
|
*/
|
||||||
@@ -54,6 +65,7 @@ public:
|
|||||||
Q_INVOKABLE void setFocusRow(int focusRow, bool mouse = false);
|
Q_INVOKABLE void setFocusRow(int focusRow, bool mouse = false);
|
||||||
void setFocusIndex(const QModelIndex &index, bool mouse = false);
|
void setFocusIndex(const QModelIndex &index, bool mouse = false);
|
||||||
Q_INVOKABLE void refocusCurrentComponent() const;
|
Q_INVOKABLE void refocusCurrentComponent() const;
|
||||||
|
QmlTextItemWrapper *currentTextItem() const;
|
||||||
ChatDocumentHandler *focusedDocumentHandler() const;
|
ChatDocumentHandler *focusedDocumentHandler() const;
|
||||||
|
|
||||||
Q_INVOKABLE void insertStyleAtCursor(RichFormat::Format style);
|
Q_INVOKABLE void insertStyleAtCursor(RichFormat::Format style);
|
||||||
@@ -79,6 +91,10 @@ private:
|
|||||||
|
|
||||||
std::optional<QString> getReplyEventId() override;
|
std::optional<QString> getReplyEventId() override;
|
||||||
|
|
||||||
|
QPointer<QmlTextItemWrapper> m_currentTextItem;
|
||||||
|
void connectCurentTextItem();
|
||||||
|
QPointer<ChatMarkdownHelper> m_markdownHelper;
|
||||||
|
|
||||||
void connectHandler(ChatDocumentHandler *handler);
|
void connectHandler(ChatDocumentHandler *handler);
|
||||||
ChatDocumentHandler *documentHandlerForComponent(const MessageComponent &component) const;
|
ChatDocumentHandler *documentHandlerForComponent(const MessageComponent &component) const;
|
||||||
ChatDocumentHandler *documentHandlerForIndex(const QModelIndex &index) const;
|
ChatDocumentHandler *documentHandlerForIndex(const QModelIndex &index) const;
|
||||||
|
|||||||
Reference in New Issue
Block a user