From 22d7d90cf4344f93bc949af570466524336bcb28 Mon Sep 17 00:00:00 2001 From: James Graham Date: Mon, 29 Dec 2025 16:24:24 +0000 Subject: [PATCH] Separate ChatButtonHelper from ChatDocumentHandler --- autotests/chatdocumenthelpertest.qml | 1 - autotests/chatmarkdownhelpertestwrapper.h | 6 +- autotests/qmltextitemwrappertest.qml | 6 +- autotests/qmltextitemwrappertestwrapper.h | 15 +- src/chatbar/CMakeLists.txt | 1 + src/chatbar/RichEditBar.qml | 67 +++-- src/chatbar/StylePicker.qml | 3 +- src/chatbar/chatbuttonhelper.cpp | 258 +++++++++++++++++ src/chatbar/chatbuttonhelper.h | 135 +++++++++ src/libneochat/chatdocumenthandler.cpp | 259 +----------------- src/libneochat/chatdocumenthandler.h | 44 --- src/libneochat/chatmarkdownhelper.cpp | 45 +-- src/libneochat/chatmarkdownhelper.h | 5 +- src/libneochat/models/completionmodel.cpp | 4 +- src/libneochat/nestedlisthelper.cpp | 45 ++- src/libneochat/qmltextitemwrapper.cpp | 82 ++++-- src/libneochat/qmltextitemwrapper.h | 20 +- .../models/chatbarmessagecontentmodel.cpp | 28 +- .../models/chatbarmessagecontentmodel.h | 16 ++ 19 files changed, 606 insertions(+), 434 deletions(-) create mode 100644 src/chatbar/chatbuttonhelper.cpp create mode 100644 src/chatbar/chatbuttonhelper.h diff --git a/autotests/chatdocumenthelpertest.qml b/autotests/chatdocumenthelpertest.qml index 5218841d8..df077898d 100644 --- a/autotests/chatdocumenthelpertest.qml +++ b/autotests/chatdocumenthelpertest.qml @@ -28,6 +28,5 @@ TestCase { compare(documentHandler.underline, false); compare(documentHandler.strikethrough, false); compare(documentHandler.style, 0); - compare(documentHandler.currentListStyle, 0); } } diff --git a/autotests/chatmarkdownhelpertestwrapper.h b/autotests/chatmarkdownhelpertestwrapper.h index d2e4ecbb0..1dc8a4e57 100644 --- a/autotests/chatmarkdownhelpertestwrapper.h +++ b/autotests/chatmarkdownhelpertestwrapper.h @@ -34,11 +34,13 @@ public: QQuickItem *textItem() const { - return m_chatMarkdownHelper->textItem(); + return m_chatMarkdownHelper->textItem()->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); } diff --git a/autotests/qmltextitemwrappertest.qml b/autotests/qmltextitemwrappertest.qml index 95ddf88cc..cd5497bfc 100644 --- a/autotests/qmltextitemwrappertest.qml +++ b/autotests/qmltextitemwrappertest.qml @@ -32,19 +32,19 @@ TestCase { SignalSpy { id: spyContentsChanged target: qmlTextItemWrapper - signalName: "textDocumentContentsChanged" + signalName: "contentsChanged" } SignalSpy { id: spyContentsChange target: qmlTextItemWrapper - signalName: "textDocumentContentsChange" + signalName: "contentsChange" } SignalSpy { id: spyCursor target: qmlTextItemWrapper - signalName: "textDocumentCursorPositionChanged" + signalName: "cursorPositionChanged" } function test_item(): void { diff --git a/autotests/qmltextitemwrappertestwrapper.h b/autotests/qmltextitemwrappertestwrapper.h index 34c63ab03..216a429ec 100644 --- a/autotests/qmltextitemwrappertestwrapper.h +++ b/autotests/qmltextitemwrappertestwrapper.h @@ -27,12 +27,9 @@ public: { Q_ASSERT(m_textItemWrapper); connect(m_textItemWrapper, &QmlTextItemWrapper::textItemChanged, this, &QmlTextItemWrapperTestWrapper::textItemChanged); - connect(m_textItemWrapper, &QmlTextItemWrapper::textDocumentContentsChange, this, &QmlTextItemWrapperTestWrapper::textDocumentContentsChange); - connect(m_textItemWrapper, &QmlTextItemWrapper::textDocumentContentsChanged, this, &QmlTextItemWrapperTestWrapper::textDocumentContentsChanged); - connect(m_textItemWrapper, - &QmlTextItemWrapper::textDocumentCursorPositionChanged, - this, - &QmlTextItemWrapperTestWrapper::textDocumentCursorPositionChanged); + connect(m_textItemWrapper, &QmlTextItemWrapper::contentsChange, this, &QmlTextItemWrapperTestWrapper::contentsChange); + connect(m_textItemWrapper, &QmlTextItemWrapper::contentsChanged, this, &QmlTextItemWrapperTestWrapper::contentsChanged); + connect(m_textItemWrapper, &QmlTextItemWrapper::cursorPositionChanged, this, &QmlTextItemWrapperTestWrapper::cursorPositionChanged); } QQuickItem *textItem() const @@ -83,9 +80,9 @@ public: Q_SIGNALS: void textItemChanged(); - void textDocumentContentsChange(int position, int charsRemoved, int charsAdded); - void textDocumentContentsChanged(); - void textDocumentCursorPositionChanged(); + void contentsChange(int position, int charsRemoved, int charsAdded); + void contentsChanged(); + void cursorPositionChanged(); private: QPointer m_textItemWrapper; diff --git a/src/chatbar/CMakeLists.txt b/src/chatbar/CMakeLists.txt index 132a4fd7e..a2d3525fc 100644 --- a/src/chatbar/CMakeLists.txt +++ b/src/chatbar/CMakeLists.txt @@ -25,6 +25,7 @@ ecm_add_qml_module(Chatbar GENERATE_PLUGIN_SOURCE NewPollDialog.qml TableDialog.qml SOURCES + chatbuttonhelper.cpp styledelegatehelper.cpp ) diff --git a/src/chatbar/RichEditBar.qml b/src/chatbar/RichEditBar.qml index 0ee1b937d..ef4734f51 100644 --- a/src/chatbar/RichEditBar.qml +++ b/src/chatbar/RichEditBar.qml @@ -64,6 +64,10 @@ QQC2.ToolBar { buttonRow.spacing * 9 + 3 + readonly property ChatButtonHelper chatButtonHelper: ChatButtonHelper { + textItem: contentModel.currentTextItem + } + signal clicked RowLayout { @@ -82,9 +86,9 @@ QQC2.ToolBar { text: i18nc("@action:button", "Bold") display: QQC2.AbstractButton.IconOnly checkable: true - checked: root.focusedDocumentHandler.bold + checked: root.chatButtonHelper.bold onClicked: { - root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Bold); + root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Bold); root.clicked() } @@ -103,9 +107,9 @@ QQC2.ToolBar { text: i18nc("@action:button", "Italic") display: QQC2.AbstractButton.IconOnly checkable: true - checked: root.focusedDocumentHandler.italic + checked: root.chatButtonHelper.italic onClicked: { - root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Italic); + root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Italic); root.clicked() } @@ -124,9 +128,9 @@ QQC2.ToolBar { text: i18nc("@action:button", "Underline") display: QQC2.AbstractButton.IconOnly checkable: true - checked: root.focusedDocumentHandler.underline + checked: root.chatButtonHelper.underline onClicked: { - root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Underline); + root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Underline); root.clicked(); } @@ -140,9 +144,9 @@ QQC2.ToolBar { text: i18nc("@action:button", "Strikethrough") display: QQC2.AbstractButton.IconOnly checkable: true - checked: root.focusedDocumentHandler.strikethrough + checked: root.chatButtonHelper.strikethrough onClicked: { - root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Strikethrough); + root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Strikethrough); root.clicked() } @@ -172,9 +176,9 @@ QQC2.ToolBar { icon.name: "format-text-bold" text: i18nc("@action:button", "Bold") checkable: true - checked: root.focusedDocumentHandler.bold + checked: root.chatButtonHelper.bold onTriggered: { - root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Bold); + root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Bold); root.clicked(); } } @@ -182,9 +186,9 @@ QQC2.ToolBar { icon.name: "format-text-italic" text: i18nc("@action:button", "Italic") checkable: true - checked: root.focusedDocumentHandler.italic + checked: root.chatButtonHelper.italic onTriggered: { - root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Italic); + root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Italic); root.clicked(); } } @@ -192,9 +196,9 @@ QQC2.ToolBar { icon.name: "format-text-underline" text: i18nc("@action:button", "Underline") checkable: true - checked: root.focusedDocumentHandler.underline + checked: root.chatButtonHelper.underline onTriggered: { - root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Underline); + root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Underline); root.clicked(); } } @@ -202,9 +206,9 @@ QQC2.ToolBar { icon.name: "format-text-strikethrough" text: i18nc("@action:button", "Strikethrough") checkable: true - checked: root.focusedDocumentHandler.strikethrough + checked: root.chatButtonHelper.strikethrough onTriggered: { - root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Strikethrough); + root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Strikethrough); root.clicked(); } } @@ -227,9 +231,9 @@ QQC2.ToolBar { text: i18nc("@action:button", "Unordered List") display: QQC2.AbstractButton.IconOnly checkable: true - checked: root.focusedDocumentHandler.currentListStyle === 1 + checked: root.chatButtonHelper.unorderedList onClicked: { - root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.UnorderedList); + root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.UnorderedList); root.clicked(); } @@ -243,9 +247,9 @@ QQC2.ToolBar { text: i18nc("@action:button", "Ordered List") display: QQC2.AbstractButton.IconOnly checkable: true - checked: root.focusedDocumentHandler.currentListStyle === 4 + checked: root.chatButtonHelper.orderedlist onClicked: { - root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.OrderedList); + root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.OrderedList); root.clicked(); } @@ -256,10 +260,11 @@ QQC2.ToolBar { QQC2.ToolButton { id: indentAction icon.name: "format-indent-more" + enabled: root.contentModel.focusType !== LibNeoChat.MessageComponentType.Code && root.chatButtonHelper.canIndentListMore text: i18nc("@action:button", "Increase List Level") display: QQC2.AbstractButton.IconOnly onClicked: { - root.focusedDocumentHandler.indentListMore(); + root.chatButtonHelper.indentListMore(); root.clicked(); } @@ -270,10 +275,11 @@ QQC2.ToolBar { QQC2.ToolButton { id: dedentAction icon.name: "format-indent-less" + enabled: root.contentModel.focusType !== LibNeoChat.MessageComponentType.Code && root.chatButtonHelper.canIndentListLess text: i18nc("@action:button", "Decrease List Level") display: QQC2.AbstractButton.IconOnly onClicked: { - root.focusedDocumentHandler.indentListLess(); + root.chatButtonHelper.indentListLess(); root.clicked(); } @@ -303,7 +309,7 @@ QQC2.ToolBar { icon.name: "format-list-unordered" text: i18nc("@action:button", "Unordered List") onTriggered: { - root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.UnorderedList); + root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.UnorderedList); root.clicked(); } } @@ -311,7 +317,7 @@ QQC2.ToolBar { icon.name: "format-list-ordered" text: i18nc("@action:button", "Ordered List") onTriggered: { - root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.OrderedList); + root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.OrderedList); root.clicked(); } } @@ -319,7 +325,7 @@ QQC2.ToolBar { icon.name: "format-indent-more" text: i18nc("@action:button", "Increase List Level") onTriggered: { - root.focusedDocumentHandler.indentListMore(); + root.chatButtonHelper.indentListMore(); root.clicked(); } } @@ -327,7 +333,7 @@ QQC2.ToolBar { icon.name: "format-indent-less" text: i18nc("@action:button", "Decrease List Level") onTriggered: { - root.focusedDocumentHandler.indentListLess(); + root.chatButtonHelper.indentListLess(); root.clicked(); } } @@ -355,6 +361,7 @@ QQC2.ToolBar { StylePicker { id: styleMenu chatContentModel: root.contentModel + chatButtonHelper: root.chatButtonHelper onClosed: root.clicked() } @@ -392,11 +399,11 @@ QQC2.ToolBar { display: QQC2.AbstractButton.IconOnly onClicked: { let dialog = linkDialog.createObject(QQC2.Overlay.overlay, { - linkText: root.focusedDocumentHandler.currentLinkText(), - linkUrl: root.focusedDocumentHandler.currentLinkUrl() + linkText: root.chatButtonHelper.currentLinkText, + linkUrl: root.chatButtonHelper.currentLinkUrl }) dialog.onAccepted.connect(() => { - root.focusedDocumentHandler.updateLink(dialog.linkUrl, dialog.linkText) + root.chatButtonHelper.updateLink(dialog.linkUrl, dialog.linkText) root.clicked(); }); dialog.open(); @@ -583,7 +590,7 @@ QQC2.ToolBar { currentRoom: root.room onChosen: emoji => { - root.focusedDocumentHandler.insertText(emoji); + root.chatButtonHelper.insertText(emoji); close(); } onClosed: if (emojiButton.checked) { diff --git a/src/chatbar/StylePicker.qml b/src/chatbar/StylePicker.qml index a50cc2e1f..50ef95d93 100644 --- a/src/chatbar/StylePicker.qml +++ b/src/chatbar/StylePicker.qml @@ -16,6 +16,7 @@ QQC2.Popup { id: root required property MessageContent.ChatBarMessageContentModel chatContentModel + required property ChatButtonHelper chatButtonHelper readonly property LibNeoChat.ChatDocumentHandler focusedDocumentHandler: chatContentModel.focusedDocumentHandler y: -implicitHeight @@ -47,7 +48,7 @@ QQC2.Popup { ) { root.chatContentModel.insertStyleAtCursor(styleDelegate.index); } else { - root.focusedDocumentHandler.setFormat(styleDelegate.index); + root.chatButtonHelper.setFormat(styleDelegate.index); } root.close(); } diff --git a/src/chatbar/chatbuttonhelper.cpp b/src/chatbar/chatbuttonhelper.cpp new file mode 100644 index 000000000..1a64a20dd --- /dev/null +++ b/src/chatbar/chatbuttonhelper.cpp @@ -0,0 +1,258 @@ +// SPDX-FileCopyrightText: 2025 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#include "chatbuttonhelper.h" + +#include + +#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(qmlAttachedPropertiesObject(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" diff --git a/src/chatbar/chatbuttonhelper.h b/src/chatbar/chatbuttonhelper.h new file mode 100644 index 000000000..d024e3f73 --- /dev/null +++ b/src/chatbar/chatbuttonhelper.h @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: 2025 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#pragma once + +#include +#include + +#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 m_textItem; + + void selectLinkText(QTextCursor &cursor) const; +}; diff --git a/src/libneochat/chatdocumenthandler.cpp b/src/libneochat/chatdocumenthandler.cpp index 10803d158..fa4eb7beb 100644 --- a/src/libneochat/chatdocumenthandler.cpp +++ b/src/libneochat/chatdocumenthandler.cpp @@ -112,7 +112,6 @@ public: mention.cursor.setPosition(mention.cursor.anchor() + mention.text.size(), QTextCursor::KeepAnchor); } - qWarning() << mention.cursor.selectedText() << mention.text; if (mention.cursor.selectedText() != mention.text) { return true; } @@ -133,12 +132,9 @@ private: ChatDocumentHandler::ChatDocumentHandler(QObject *parent) : QObject(parent) , m_textItem(new QmlTextItemWrapper(this)) - , m_markdownHelper(new ChatMarkdownHelper(this)) , m_highlighter(new SyntaxHighlighter(this)) { connectTextItem(); - - connect(this, &ChatDocumentHandler::formatChanged, m_markdownHelper, &ChatMarkdownHelper::handleExternalFormatChange); } ChatBarType::Type ChatDocumentHandler::type() const @@ -178,7 +174,6 @@ QQuickItem *ChatDocumentHandler::textItem() const void ChatDocumentHandler::setTextItem(QQuickItem *textItem) { m_textItem->setTextItem(textItem); - m_markdownHelper->setTextItem(textItem); } void ChatDocumentHandler::connectTextItem() @@ -189,33 +184,14 @@ void ChatDocumentHandler::connectTextItem() initializeChars(); }); connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, &ChatDocumentHandler::textItemChanged); - connect(m_textItem, &QmlTextItemWrapper::textDocumentContentsChanged, this, &ChatDocumentHandler::contentsChanged); - connect(m_textItem, &QmlTextItemWrapper::textDocumentContentsChanged, this, &ChatDocumentHandler::atFirstLineChanged); - connect(m_textItem, &QmlTextItemWrapper::textDocumentContentsChanged, this, &ChatDocumentHandler::atLastLineChanged); - connect(m_textItem, &QmlTextItemWrapper::textDocumentCursorPositionChanged, this, &ChatDocumentHandler::atFirstLineChanged); - connect(m_textItem, &QmlTextItemWrapper::textDocumentCursorPositionChanged, 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::contentsChanged, this, &ChatDocumentHandler::contentsChanged); + connect(m_textItem, &QmlTextItemWrapper::contentsChanged, this, &ChatDocumentHandler::atFirstLineChanged); + connect(m_textItem, &QmlTextItemWrapper::contentsChanged, this, &ChatDocumentHandler::atLastLineChanged); + connect(m_textItem, &QmlTextItemWrapper::cursorPositionChanged, this, &ChatDocumentHandler::atFirstLineChanged); + connect(m_textItem, &QmlTextItemWrapper::cursorPositionChanged, this, &ChatDocumentHandler::atLastLineChanged); connect(m_textItem, &QmlTextItemWrapper::formatChanged, this, &ChatDocumentHandler::formatChanged); connect(m_textItem, &QmlTextItemWrapper::textFormatChanged, this, &ChatDocumentHandler::textFormatChanged); connect(m_textItem, &QmlTextItemWrapper::styleChanged, this, &ChatDocumentHandler::styleChanged); - connect(m_textItem, &QmlTextItemWrapper::listChanged, this, &ChatDocumentHandler::listChanged); } 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 ChatDocumentHandler::textFormat() const { if (!m_textItem) { @@ -606,57 +532,6 @@ void ChatDocumentHandler::mergeFormatOnWordOrSelection(const QTextCharFormat &fo } if (cursor.hasSelection()) { 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(); } -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(m_textItem->textCursor().blockFormat().headingLevel()); -} - void ChatDocumentHandler::tab() { QTextCursor cursor = m_textItem->textCursor(); @@ -807,10 +575,10 @@ void ChatDocumentHandler::tab() return; } if (cursor.currentList()) { - indentListMore(); + m_textItem->indentListMoreAtCursor(); return; } - insertText(u" "_s); + cursor.insertText(u" "_s); } void ChatDocumentHandler::deleteChar() @@ -835,9 +603,8 @@ void ChatDocumentHandler::backspace() return; } if (cursor.position() <= m_fixedStartChars.length()) { - qWarning() << "unhandled backspace"; if (cursor.currentList()) { - indentListLess(); + m_textItem->indentListLessAtCursor(); return; } if (const auto previousHandler = previousDocumentHandler()) { @@ -859,16 +626,6 @@ void ChatDocumentHandler::insertReturn() 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() { qWarning() << htmlText(); diff --git a/src/libneochat/chatdocumenthandler.h b/src/libneochat/chatdocumenthandler.h index 909f95226..258d365e4 100644 --- a/src/libneochat/chatdocumenthandler.h +++ b/src/libneochat/chatdocumenthandler.h @@ -95,17 +95,6 @@ class ChatDocumentHandler : public QObject */ 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: enum InsertPosition { Cursor, @@ -152,33 +141,11 @@ public: */ 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 deleteChar(); Q_INVOKABLE void backspace(); Q_INVOKABLE void insertReturn(); - Q_INVOKABLE void insertText(const QString &text); void insertFragment(const QTextDocumentFragment fragment, InsertPosition position = Cursor, bool keepPosition = false); - Q_INVOKABLE QString currentLinkUrl() const; - Q_INVOKABLE QString 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 dumpHtml(); @@ -192,14 +159,11 @@ Q_SIGNALS: void atFirstLineChanged(); void atLastLineChanged(); - void textColorChanged(); - void currentListStyleChanged(); void formatChanged(); void textFormatChanged(); void styleChanged(); - void listChanged(); void contentsChanged(); @@ -220,10 +184,6 @@ private: QString m_initialText = {}; void initializeChars(); - QPointer m_markdownHelper; - std::optional m_pendingFormat = std::nullopt; - std::optional m_pendingOverrideFormat = std::nullopt; - SyntaxHighlighter *m_highlighter = nullptr; QString getText() const; @@ -231,10 +191,6 @@ private: std::optional textFormat() const; void mergeFormatOnWordOrSelection(const QTextCharFormat &format); - void selectLinkText(QTextCursor *cursor) const; - QColor linkColor(); - QColor mLinkColor; - void regenerateColorScheme(); QString trim(QString string) const; }; diff --git a/src/libneochat/chatmarkdownhelper.cpp b/src/libneochat/chatmarkdownhelper.cpp index 5ef8f878c..05ab8995e 100644 --- a/src/libneochat/chatmarkdownhelper.cpp +++ b/src/libneochat/chatmarkdownhelper.cpp @@ -83,32 +83,39 @@ std::optional syntaxForSequence(const QString &sequence) ChatMarkdownHelper::ChatMarkdownHelper(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() -{ - connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, &ChatMarkdownHelper::textItemChanged); - connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, [this]() { - m_startPos = m_textItem->cursorPosition(); - m_endPos = m_startPos; - if (m_startPos == 0) { - m_currentState = Pre; - } - }); - connect(m_textItem, &QmlTextItemWrapper::textDocumentContentsChange, this, &ChatMarkdownHelper::checkMarkdown); + if (m_textItem) { + m_textItem->disconnect(this); + } + + m_textItem = textItem; + + if (m_textItem) { + connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, &ChatMarkdownHelper::textItemChanged); + connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, [this]() { + m_startPos = m_textItem->cursorPosition(); + 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) @@ -140,7 +147,6 @@ void ChatMarkdownHelper::checkMarkdown(int position, int charsRemoved, int chars cursor.setPosition(m_startPos); const auto result = checkSequence(currentMarkdown, nextChar, cursor.atBlockStart()); - qWarning() << m_startPos << m_endPos << result; switch (m_currentState) { case None: @@ -216,7 +222,6 @@ void ChatMarkdownHelper::complete() m_endPos = result ? m_startPos + 1 : m_startPos; cursor.endEditBlock(); - qWarning() << m_currentState << m_startPos << m_endPos << m_textItem->cursorPosition(); } void ChatMarkdownHelper::handleExternalFormatChange() diff --git a/src/libneochat/chatmarkdownhelper.h b/src/libneochat/chatmarkdownhelper.h index 08814f6fb..75b29558e 100644 --- a/src/libneochat/chatmarkdownhelper.h +++ b/src/libneochat/chatmarkdownhelper.h @@ -19,8 +19,8 @@ class ChatMarkdownHelper : public QObject public: explicit ChatMarkdownHelper(QObject *parent = nullptr); - QQuickItem *textItem() const; - void setTextItem(QQuickItem *textItem); + QmlTextItemWrapper *textItem() const; + void setTextItem(QmlTextItemWrapper *textItem); void handleExternalFormatChange(); @@ -36,7 +36,6 @@ private: }; QPointer m_textItem; - void connectTextItem(); State m_currentState = None; int m_startPos = 0; diff --git a/src/libneochat/models/completionmodel.cpp b/src/libneochat/models/completionmodel.cpp index 63c6bed5f..f8a9e6cbc 100644 --- a/src/libneochat/models/completionmodel.cpp +++ b/src/libneochat/models/completionmodel.cpp @@ -20,8 +20,8 @@ CompletionModel::CompletionModel(QObject *parent) , m_emojiModel(new QConcatenateTablesProxyModel(this)) { connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, &CompletionModel::textItemChanged); - connect(m_textItem, &QmlTextItemWrapper::textDocumentCursorPositionChanged, this, &CompletionModel::updateTextStart); - connect(m_textItem, &QmlTextItemWrapper::textDocumentContentsChanged, this, &CompletionModel::updateCompletion); + connect(m_textItem, &QmlTextItemWrapper::cursorPositionChanged, this, &CompletionModel::updateTextStart); + connect(m_textItem, &QmlTextItemWrapper::contentsChanged, this, &CompletionModel::updateCompletion); m_emojiModel->addSourceModel(&CustomEmojiModel::instance()); m_emojiModel->addSourceModel(&EmojiModel::instance()); diff --git a/src/libneochat/nestedlisthelper.cpp b/src/libneochat/nestedlisthelper.cpp index a183335c1..51f4bd2c9 100644 --- a/src/libneochat/nestedlisthelper.cpp +++ b/src/libneochat/nestedlisthelper.cpp @@ -39,39 +39,30 @@ bool NestedListHelper::handleBeforeKeyPressEvent(QKeyEvent *event, const QTextCu 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; - } + const auto block = textCursor.block(); + if (textCursor.isNull() || !block.isValid()) { + return false; } - return false; + + if (!block.textList()) { + return true; + } + + return block.textList()->format().indent() < 3; } 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; - } - } + const auto block = textCursor.block(); + if (textCursor.isNull() || !block.isValid()) { + return false; } - return false; + + if (!block.textList()) { + return false; + } + + return block.textList()->format().indent() > 0; } bool NestedListHelper::handleAfterKeyPressEvent(QKeyEvent *event, const QTextCursor &cursor) diff --git a/src/libneochat/qmltextitemwrapper.cpp b/src/libneochat/qmltextitemwrapper.cpp index bd7f651c5..44efe91f1 100644 --- a/src/libneochat/qmltextitemwrapper.cpp +++ b/src/libneochat/qmltextitemwrapper.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "qmltextitemwrapper.h" +#include "richformat.h" #include #include @@ -34,14 +35,18 @@ void QmlTextItemWrapper::setTextItem(QQuickItem *textItem) m_textItem = textItem; if (m_textItem) { - connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(textDocCursorChanged())); + connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(itemCursorPositionChanged())); if (document()) { - connect(document(), &QTextDocument::contentsChanged, this, &QmlTextItemWrapper::textDocumentContentsChanged); - connect(document(), &QTextDocument::contentsChange, this, &QmlTextItemWrapper::textDocumentContentsChange); + connect(document(), &QTextDocument::contentsChanged, this, &QmlTextItemWrapper::contentsChanged); + connect(document(), &QTextDocument::contentsChange, this, &QmlTextItemWrapper::contentsChange); } } Q_EMIT textItemChanged(); + Q_EMIT formatChanged(); + Q_EMIT textFormatChanged(); + Q_EMIT styleChanged(); + Q_EMIT listChanged(); } QTextDocument *QmlTextItemWrapper::document() const @@ -109,15 +114,33 @@ void QmlTextItemWrapper::setCursorVisible(bool 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 QmlTextItemWrapper::formatsAtCursor(QTextCursor cursor) const { 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)) { case RichFormat::Text: @@ -126,6 +149,10 @@ void QmlTextItemWrapper::mergeFormatOnCursor(RichFormat::Format format, const QT case RichFormat::List: mergeListFormatOnCursor(format, cursor); return; + case RichFormat::Block: + if (format != RichFormat::Paragraph) { + return; + } case RichFormat::Style: mergeStyleFormatOnCursor(format, cursor); return; @@ -194,48 +221,47 @@ void QmlTextItemWrapper::mergeListFormatOnCursor(RichFormat::Format format, cons 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()) { - return false; + cursor = textCursor(); + if (cursor.isNull()) { + return false; + } } 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()) { - return false; + cursor = textCursor(); + if (cursor.isNull()) { + return false; + } } return m_nestedListHelper.canDedent(cursor) && cursor.blockFormat().headingLevel() == 0; } -void QmlTextItemWrapper::indentListMore() +void QmlTextItemWrapper::indentListMoreAtCursor(QTextCursor cursor) { - auto cursor = textCursor(); if (cursor.isNull()) { - return; + cursor = textCursor(); + if (cursor.isNull()) { + return; + } } m_nestedListHelper.handleOnIndentMore(cursor); Q_EMIT listChanged(); } -void QmlTextItemWrapper::indentListLess() +void QmlTextItemWrapper::indentListLessAtCursor(QTextCursor cursor) { - auto cursor = textCursor(); if (cursor.isNull()) { - return; + cursor = textCursor(); + if (cursor.isNull()) { + return; + } } m_nestedListHelper.handleOnIndentLess(cursor); Q_EMIT listChanged(); diff --git a/src/libneochat/qmltextitemwrapper.h b/src/libneochat/qmltextitemwrapper.h index 7755eec11..962327257 100644 --- a/src/libneochat/qmltextitemwrapper.h +++ b/src/libneochat/qmltextitemwrapper.h @@ -38,24 +38,24 @@ public: void setCursorPosition(int pos); void setCursorVisible(bool visible); - void mergeFormatOnCursor(RichFormat::Format format, const QTextCursor &cursor); + QList formatsAtCursor(QTextCursor cursor = {}) const; + void mergeFormatOnCursor(RichFormat::Format format, QTextCursor cursor = {}); - int currentListStyle() const; - bool canIndentListMore() const; - bool canIndentListLess() const; - void indentListMore(); - void indentListLess(); + bool canIndentListMoreAtCursor(QTextCursor cursor = {}) const; + bool canIndentListLessAtCursor(QTextCursor cursor = {}) const; + void indentListMoreAtCursor(QTextCursor cursor = {}); + void indentListLessAtCursor(QTextCursor cursor = {}); void forceActiveFocus() const; Q_SIGNALS: 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 textFormatChanged(); @@ -74,5 +74,5 @@ private: NestedListHelper m_nestedListHelper; private Q_SLOTS: - void textDocCursorChanged(); + void itemCursorPositionChanged(); }; diff --git a/src/messagecontent/models/chatbarmessagecontentmodel.cpp b/src/messagecontent/models/chatbarmessagecontentmodel.cpp index ad0c3ee85..4fdd40458 100644 --- a/src/messagecontent/models/chatbarmessagecontentmodel.cpp +++ b/src/messagecontent/models/chatbarmessagecontentmodel.cpp @@ -9,12 +9,17 @@ #include "chatdocumenthandler.h" #include "enums/chatbartype.h" #include "enums/messagecomponenttype.h" +#include "enums/richformat.h" #include "messagecontentmodel.h" +#include "qmltextitemwrapper.h" ChatBarMessageContentModel::ChatBarMessageContentModel(QObject *parent) : MessageContentModel(parent) + , m_currentTextItem(new QmlTextItemWrapper(this)) + , m_markdownHelper(new ChatMarkdownHelper(this)) { m_editableActive = true; + connectCurentTextItem(); initializeModel(); connect(this, &ChatBarMessageContentModel::roomChanged, this, [this]() { @@ -66,6 +71,19 @@ void ChatBarMessageContentModel::initializeModel() 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) { connect(handler, &ChatDocumentHandler::contentsChanged, this, &ChatBarMessageContentModel::updateCache); @@ -176,7 +194,6 @@ void ChatBarMessageContentModel::setFocusIndex(const QModelIndex &index, bool mo } } - Q_EMIT focusRowChanged(); emitFocusChangeSignals(); } @@ -200,6 +217,11 @@ void ChatBarMessageContentModel::refocusCurrentComponent() const chatDocumentHandler->textItem()->forceActiveFocus(); } +QmlTextItemWrapper *ChatBarMessageContentModel::currentTextItem() const +{ + return m_currentTextItem; +} + ChatDocumentHandler *ChatBarMessageContentModel::focusedDocumentHandler() const { if (!m_currentFocusComponent.isValid()) { @@ -214,6 +236,7 @@ ChatDocumentHandler *ChatBarMessageContentModel::focusedDocumentHandler() const void ChatBarMessageContentModel::emitFocusChangeSignals() { + Q_EMIT focusRowChanged(); 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 (type == MessageComponentType::Text && focusedDocumentHandler()) { - focusedDocumentHandler()->setFormat(RichFormat::Paragraph); + currentTextItem()->mergeFormatOnCursor(RichFormat::Paragraph); } return; } @@ -324,7 +347,6 @@ void ChatBarMessageContentModel::insertComponentAtCursor(MessageComponentType::T insertChatDocumentHandler->insertFragment(midFragment); } m_currentFocusComponent = QPersistentModelIndex(index(insertIt - m_components.begin())); - Q_EMIT focusRowChanged(); emitFocusChangeSignals(); } diff --git a/src/messagecontent/models/chatbarmessagecontentmodel.h b/src/messagecontent/models/chatbarmessagecontentmodel.h index 42a748ba5..166b5044d 100644 --- a/src/messagecontent/models/chatbarmessagecontentmodel.h +++ b/src/messagecontent/models/chatbarmessagecontentmodel.h @@ -12,6 +12,7 @@ #include "enums/richformat.h" #include "messagecomponent.h" #include "models/messagecontentmodel.h" +#include "qmltextitemwrapper.h" /** * @class ChatBarMessageContentModel @@ -38,6 +39,16 @@ class ChatBarMessageContentModel : public MessageContentModel */ 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. */ @@ -54,6 +65,7 @@ public: Q_INVOKABLE void setFocusRow(int focusRow, bool mouse = false); void setFocusIndex(const QModelIndex &index, bool mouse = false); Q_INVOKABLE void refocusCurrentComponent() const; + QmlTextItemWrapper *currentTextItem() const; ChatDocumentHandler *focusedDocumentHandler() const; Q_INVOKABLE void insertStyleAtCursor(RichFormat::Format style); @@ -79,6 +91,10 @@ private: std::optional getReplyEventId() override; + QPointer m_currentTextItem; + void connectCurentTextItem(); + QPointer m_markdownHelper; + void connectHandler(ChatDocumentHandler *handler); ChatDocumentHandler *documentHandlerForComponent(const MessageComponent &component) const; ChatDocumentHandler *documentHandlerForIndex(const QModelIndex &index) const;