diff --git a/src/chatbar/RichEditBar.qml b/src/chatbar/RichEditBar.qml index a1f32cc3a..90452b17b 100644 --- a/src/chatbar/RichEditBar.qml +++ b/src/chatbar/RichEditBar.qml @@ -73,7 +73,7 @@ QQC2.ToolBar { onActivated: boldButton.clicked() } icon.name: "format-text-bold" - enabled: root.contentModel.focusType !== LibNeoChat.MessageComponentType.Code + enabled: chatButtonHelper.richFormatEnabled text: i18nc("@action:button", "Bold") display: QQC2.AbstractButton.IconOnly checkable: true @@ -94,7 +94,7 @@ QQC2.ToolBar { onActivated: italicButton.clicked() } icon.name: "format-text-italic" - enabled: root.contentModel.focusType !== LibNeoChat.MessageComponentType.Code + enabled: chatButtonHelper.richFormatEnabled text: i18nc("@action:button", "Italic") display: QQC2.AbstractButton.IconOnly checkable: true @@ -115,7 +115,7 @@ QQC2.ToolBar { onActivated: underlineButton.clicked() } icon.name: "format-text-underline" - enabled: root.contentModel.focusType !== LibNeoChat.MessageComponentType.Code + enabled: chatButtonHelper.richFormatEnabled text: i18nc("@action:button", "Underline") display: QQC2.AbstractButton.IconOnly checkable: true @@ -131,7 +131,7 @@ QQC2.ToolBar { } QQC2.ToolButton { icon.name: "format-text-strikethrough" - enabled: root.contentModel.focusType !== LibNeoChat.MessageComponentType.Code + enabled: chatButtonHelper.richFormatEnabled text: i18nc("@action:button", "Strikethrough") display: QQC2.AbstractButton.IconOnly checkable: true @@ -150,7 +150,7 @@ QQC2.ToolBar { id: compressedTextFormatButton visible: root.maxAvailableWidth < root.listCompressedImplicitWidth icon.name: "dialog-text-and-font" - enabled: root.contentModel.focusType !== LibNeoChat.MessageComponentType.Code + enabled: chatButtonHelper.richFormatEnabled text: i18nc("@action:button", "Format Text") display: QQC2.AbstractButton.IconOnly checkable: true @@ -218,7 +218,7 @@ QQC2.ToolBar { visible: root.maxAvailableWidth > root.uncompressedImplicitWidth QQC2.ToolButton { icon.name: "format-list-unordered" - enabled: root.contentModel.focusType !== LibNeoChat.MessageComponentType.Code + enabled: chatButtonHelper.richFormatEnabled text: i18nc("@action:button", "Unordered List") display: QQC2.AbstractButton.IconOnly checkable: true @@ -234,7 +234,7 @@ QQC2.ToolBar { } QQC2.ToolButton { icon.name: "format-list-ordered" - enabled: root.contentModel.focusType !== LibNeoChat.MessageComponentType.Code + enabled: chatButtonHelper.richFormatEnabled text: i18nc("@action:button", "Ordered List") display: QQC2.AbstractButton.IconOnly checkable: true @@ -251,7 +251,7 @@ QQC2.ToolBar { QQC2.ToolButton { id: indentAction icon.name: "format-indent-more" - enabled: root.contentModel.focusType !== LibNeoChat.MessageComponentType.Code && root.chatButtonHelper.canIndentListMore + enabled: chatButtonHelper.richFormatEnabled && root.chatButtonHelper.canIndentListMore text: i18nc("@action:button", "Increase List Level") display: QQC2.AbstractButton.IconOnly onClicked: { @@ -266,7 +266,7 @@ QQC2.ToolBar { QQC2.ToolButton { id: dedentAction icon.name: "format-indent-less" - enabled: root.contentModel.focusType !== LibNeoChat.MessageComponentType.Code && root.chatButtonHelper.canIndentListLess + enabled: chatButtonHelper.richFormatEnabled && root.chatButtonHelper.canIndentListLess text: i18nc("@action:button", "Decrease List Level") display: QQC2.AbstractButton.IconOnly onClicked: { @@ -281,7 +281,7 @@ QQC2.ToolBar { } QQC2.ToolButton { id: compressedListButton - enabled: root.contentModel.focusType !== LibNeoChat.MessageComponentType.Code + enabled: chatButtonHelper.richFormatEnabled visible: root.maxAvailableWidth < root.uncompressedImplicitWidth icon.name: "format-list-unordered" text: i18nc("@action:button", "List Style") @@ -315,6 +315,7 @@ QQC2.ToolBar { QQC2.MenuItem { icon.name: "format-indent-more" text: i18nc("@action:button", "Increase List Level") + enabled: root.chatButtonHelper.canIndentListMore onTriggered: { root.chatButtonHelper.indentListMore(); root.clicked(); @@ -323,6 +324,7 @@ QQC2.ToolBar { QQC2.MenuItem { icon.name: "format-indent-less" text: i18nc("@action:button", "Decrease List Level") + enabled: root.chatButtonHelper.canIndentListLess onTriggered: { root.chatButtonHelper.indentListLess(); root.clicked(); @@ -338,6 +340,7 @@ QQC2.ToolBar { id: styleButton icon.name: "typewriter" text: i18nc("@action:button", "Text Style") + enabled: root.chatButtonHelper.styleFormatEnabled display: QQC2.AbstractButton.IconOnly checkable: true checked: styleMenu.visible @@ -422,10 +425,29 @@ QQC2.ToolBar { display: QQC2.AbstractButton.IconOnly onClicked: { + if (!root.contentModel.hasRichFormatting) { + fileDialog(); + return; + } + + let warningDialog = Qt.createComponent('org.kde.kirigami', 'PromptDialog').createObject(QQC2.Overlay.overlay, { + dialogType: Kirigami.PromptDialog.Warning, + title: i18n("Attach an image or file?"), + subtitle: i18n("Attachments can only have plain text captions, all rich formatting will be removed"), + standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel + }); + warningDialog.onAccepted.connect(() => { + attachmentButton.fileDialog(); + }); + warningDialog.open(); + } + + function fileDialog(): void { let dialog = (LibNeoChat.Clipboard.hasImage ? attachDialog : openFileDialog).createObject(QQC2.Overlay.overlay); dialog.chosen.connect(path => root.contentModel.addAttachment(path)); dialog.open(); } + QQC2.ToolTip.visible: hovered QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay QQC2.ToolTip.text: text diff --git a/src/chatbar/chatbuttonhelper.cpp b/src/chatbar/chatbuttonhelper.cpp index e12d6f2ce..e2035bb8e 100644 --- a/src/chatbar/chatbuttonhelper.cpp +++ b/src/chatbar/chatbuttonhelper.cpp @@ -5,7 +5,11 @@ #include +#include "chatbarcache.h" +#include "chattextitemhelper.h" +#include "enums/chatbartype.h" #include "enums/richformat.h" +#include "neochatroom.h" ChatButtonHelper::ChatButtonHelper(QObject *parent) : QObject(parent) @@ -30,13 +34,51 @@ void ChatButtonHelper::setTextItem(ChatTextItemHelper *textItem) m_textItem = textItem; if (m_textItem) { - connect(m_textItem, &ChatTextItemHelper::formatChanged, this, &ChatButtonHelper::linkChanged); - connect(m_textItem, &ChatTextItemHelper::textFormatChanged, this, &ChatButtonHelper::textFormatChanged); + connect(m_textItem, &ChatTextItemHelper::roomChanged, this, [this]() { + if (m_textItem->room() && m_textItem->type() != ChatBarType::None) { + const auto cache = m_textItem->room()->cacheForType(m_textItem->type()); + connect(cache, &ChatBarCache::attachmentPathChanged, this, &ChatButtonHelper::richFormatEnabledChanged); + } + }); + connect(m_textItem, &ChatTextItemHelper::textFormatChanged, this, &ChatButtonHelper::richFormatEnabledChanged); + connect(m_textItem, &ChatTextItemHelper::charFormatChanged, this, &ChatButtonHelper::charFormatChanged); connect(m_textItem, &ChatTextItemHelper::styleChanged, this, &ChatButtonHelper::styleChanged); connect(m_textItem, &ChatTextItemHelper::listChanged, this, &ChatButtonHelper::listChanged); } Q_EMIT textItemChanged(); + Q_EMIT richFormatEnabledChanged(); +} + +bool ChatButtonHelper::richFormatEnabled() const +{ + if (!m_textItem) { + return false; + } + const auto styleAvailable = styleFormatEnabled(); + if (!styleAvailable) { + return false; + } + const auto format = m_textItem->textFormat(); + if (format) { + return format != Qt::PlainText; + } + return false; +} + +bool ChatButtonHelper::styleFormatEnabled() const +{ + if (!m_textItem) { + return false; + } + const auto room = m_textItem->room(); + if (!room) { + return false; + } + if (const auto cache = room->cacheForType(m_textItem->type())) { + return cache->attachmentPath().isEmpty(); + } + return true; } bool ChatButtonHelper::bold() const diff --git a/src/chatbar/chatbuttonhelper.h b/src/chatbar/chatbuttonhelper.h index b12be3299..dd1aafc8b 100644 --- a/src/chatbar/chatbuttonhelper.h +++ b/src/chatbar/chatbuttonhelper.h @@ -26,22 +26,32 @@ class ChatButtonHelper : public QObject /** * @brief Whether the text format at the current cursor is bold. */ - Q_PROPERTY(bool bold READ bold NOTIFY textFormatChanged) + Q_PROPERTY(bool richFormatEnabled READ richFormatEnabled NOTIFY richFormatEnabledChanged) + + /** + * @brief Whether the text format at the current cursor is bold. + */ + Q_PROPERTY(bool styleFormatEnabled READ styleFormatEnabled NOTIFY richFormatEnabledChanged) + + /** + * @brief Whether the text format at the current cursor is bold. + */ + Q_PROPERTY(bool bold READ bold NOTIFY charFormatChanged) /** * @brief Whether the text format at the current cursor is italic. */ - Q_PROPERTY(bool italic READ italic NOTIFY textFormatChanged) + Q_PROPERTY(bool italic READ italic NOTIFY charFormatChanged) /** * @brief Whether the text format at the current cursor is underlined. */ - Q_PROPERTY(bool underline READ underline NOTIFY textFormatChanged) + Q_PROPERTY(bool underline READ underline NOTIFY charFormatChanged) /** * @brief Whether the text format at the current cursor is struckthrough. */ - Q_PROPERTY(bool strikethrough READ strikethrough NOTIFY textFormatChanged) + Q_PROPERTY(bool strikethrough READ strikethrough NOTIFY charFormatChanged) /** * @brief Whether the format at the current cursor includes RichFormat::UnorderedList. @@ -71,12 +81,12 @@ class ChatButtonHelper : public QObject /** * @brief The link url at the current cursor position. */ - Q_PROPERTY(QString currentLinkUrl READ currentLinkUrl NOTIFY linkChanged) + Q_PROPERTY(QString currentLinkUrl READ currentLinkUrl NOTIFY charFormatChanged) /** * @brief The link url at the current cursor position. */ - Q_PROPERTY(QString currentLinkText READ currentLinkText NOTIFY linkChanged) + Q_PROPERTY(QString currentLinkText READ currentLinkText NOTIFY charFormatChanged) public: explicit ChatButtonHelper(QObject *parent = nullptr); @@ -84,6 +94,8 @@ public: ChatTextItemHelper *textItem() const; void setTextItem(ChatTextItemHelper *textItem); + bool richFormatEnabled() const; + bool styleFormatEnabled() const; bool bold() const; bool italic() const; bool underline() const; @@ -128,11 +140,10 @@ public: Q_SIGNALS: void textItemChanged(); - void formatChanged(); - void textFormatChanged(); + void richFormatEnabledChanged(); + void charFormatChanged(); void styleChanged(); void listChanged(); - void linkChanged(); private: QPointer m_textItem; diff --git a/src/libneochat/chattextitemhelper.cpp b/src/libneochat/chattextitemhelper.cpp index c7b89088f..8564ced18 100644 --- a/src/libneochat/chattextitemhelper.cpp +++ b/src/libneochat/chattextitemhelper.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "chattextitemhelper.h" +#include "chatbartype.h" #include "richformat.h" #include @@ -13,20 +14,50 @@ #include "chatbarsyntaxhighlighter.h" #include "neochatroom.h" +// Because we can't get access to the private header we foward declare this so the SIGNAL() macro works in setTextItem. +namespace QQuickTextEdit +{ +enum TextFormat : unsigned int; +} + ChatTextItemHelper::ChatTextItemHelper(QObject *parent) : QObject(parent) , m_highlighter(new ChatBarSyntaxHighlighter(this)) { } +NeoChatRoom *ChatTextItemHelper::room() const +{ + if (!m_highlighter) { + return nullptr; + } + return m_highlighter->room; +} + void ChatTextItemHelper::setRoom(NeoChatRoom *room) { + if (!m_highlighter) { + return; + } m_highlighter->room = room; + Q_EMIT roomChanged(); +} + +ChatBarType::Type ChatTextItemHelper::type() const +{ + if (!m_highlighter) { + return ChatBarType::None; + } + return m_highlighter->type; } void ChatTextItemHelper::setType(ChatBarType::Type type) { + if (!m_highlighter) { + return; + } m_highlighter->type = type; + Q_EMIT typeChanged(); } QQuickItem *ChatTextItemHelper::textItem() const @@ -51,6 +82,7 @@ void ChatTextItemHelper::setTextItem(QQuickItem *textItem) if (m_textItem) { connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(itemCursorPositionChanged())); + connect(m_textItem, SIGNAL(textFormatChanged(QQuickTextEdit::TextFormat)), this, SLOT(itemTextFormatChanged())); if (const auto doc = document()) { connect(doc, &QTextDocument::contentsChanged, this, &ChatTextItemHelper::contentsChanged); connect(doc, &QTextDocument::contentsChange, this, [this]() { @@ -63,8 +95,8 @@ void ChatTextItemHelper::setTextItem(QQuickItem *textItem) } Q_EMIT textItemChanged(); - Q_EMIT formatChanged(); Q_EMIT textFormatChanged(); + Q_EMIT charFormatChanged(); Q_EMIT styleChanged(); Q_EMIT listChanged(); } @@ -74,10 +106,14 @@ std::optional ChatTextItemHelper::textFormat() const if (!m_textItem) { return std::nullopt; } - return static_cast(m_textItem->property("textFormat").toInt()); } +void ChatTextItemHelper::itemTextFormatChanged() +{ + Q_EMIT textFormatChanged(); +} + QString ChatTextItemHelper::fixedStartChars() const { return m_fixedStartChars; @@ -393,8 +429,8 @@ void ChatTextItemHelper::itemCursorPositionChanged() Q_EMIT cursorPositionChanged(m_contentsJustChanged); m_contentsJustChanged = false; - Q_EMIT formatChanged(); Q_EMIT textFormatChanged(); + Q_EMIT charFormatChanged(); Q_EMIT styleChanged(); Q_EMIT listChanged(); } @@ -438,8 +474,7 @@ void ChatTextItemHelper::mergeTextFormatOnCursor(RichFormat::Format format, QTex cursor.select(QTextCursor::WordUnderCursor); } cursor.mergeCharFormat(charFormat); - Q_EMIT formatChanged(); - Q_EMIT textFormatChanged(); + Q_EMIT charFormatChanged(); } void ChatTextItemHelper::mergeStyleFormatOnCursor(RichFormat::Format format, QTextCursor cursor) @@ -475,14 +510,12 @@ void ChatTextItemHelper::mergeStyleFormatOnCursor(RichFormat::Format format, QTe cursor.mergeBlockCharFormat(chrfmt); cursor.endEditBlock(); - Q_EMIT formatChanged(); Q_EMIT styleChanged(); } void ChatTextItemHelper::mergeListFormatOnCursor(RichFormat::Format format, const QTextCursor &cursor) { m_nestedListHelper.handleOnBulletType(RichFormat::listStyleForFormat(format), cursor); - Q_EMIT formatChanged(); Q_EMIT listChanged(); } @@ -545,6 +578,11 @@ void ChatTextItemHelper::rehighlight() const m_highlighter->rehighlight(); } +bool ChatTextItemHelper::hasRichFormatting() const +{ + return markdownText() != plainText(); +} + QString ChatTextItemHelper::markdownText() const { const auto doc = document(); @@ -554,6 +592,15 @@ QString ChatTextItemHelper::markdownText() const return trim(doc->toMarkdown()); } +QString ChatTextItemHelper::plainText() const +{ + const auto doc = document(); + if (!doc) { + return {}; + } + return trim(doc->toPlainText()); +} + QString ChatTextItemHelper::trim(QString string) const { while (string.startsWith(u"\n"_s)) { diff --git a/src/libneochat/chattextitemhelper.h b/src/libneochat/chattextitemhelper.h index fe7053837..4231afd6f 100644 --- a/src/libneochat/chattextitemhelper.h +++ b/src/libneochat/chattextitemhelper.h @@ -47,6 +47,13 @@ public: explicit ChatTextItemHelper(QObject *parent = nullptr); + /** + * @brief Get the NeoChatRoom used by the syntax highlighter. + * + * @sa NeoChatRoom + */ + NeoChatRoom *room() const; + /** * @brief Set the NeoChatRoom required by the syntax highlighter. * @@ -54,6 +61,13 @@ public: */ void setRoom(NeoChatRoom *room); + /** + * @brief Get the ChatBarType::Type used by the syntax highlighter. + * + * @sa ChatBarType::Type + */ + ChatBarType::Type type() const; + /** * @brief Set the ChatBarType::Type required by the syntax highlighter. * @@ -64,6 +78,11 @@ public: QQuickItem *textItem() const; void setTextItem(QQuickItem *textItem); + /** + * @brief The text format of the wrapped item. + */ + std::optional textFormat() const; + /** * @brief Whether a completion has started based on recent text entry. */ @@ -205,16 +224,52 @@ public: */ void rehighlight() const; + /** + * @brief Whether there is any rich formatting in the item text. + */ + bool hasRichFormatting() const; + /** * @brief Output the text in the text item in markdown format. */ QString markdownText() const; + /** + * @brief Output the text in the text item in plain text format. + */ + QString plainText() const; + Q_SIGNALS: + /** + * @brief Emitted when the NeoChatRoom used by the syntax highlighter is changed. + */ + void roomChanged(); + + /** + * @brief Emitted when the ChatBarType::Type used by the syntax highlighter is changed. + */ + void typeChanged(); + void textItemChanged(); - void formatChanged(); + + /** + * @brief Emitted when the textFormat property of the underlying text item is changed. + */ void textFormatChanged(); + + /** + * @brief Emitted when the char format at the current cursor is changed. + */ + void charFormatChanged(); + + /** + * @brief Emitted when the style at the current cursor is changed. + */ void styleChanged(); + + /** + * @brief Emitted when the list format at the current cursor is changed. + */ void listChanged(); /** @@ -242,7 +297,6 @@ private: QPointer m_highlighter; bool m_contentsJustChanged = false; - std::optional textFormat() const; QString m_fixedStartChars = {}; QString m_fixedEndChars = {}; @@ -263,5 +317,6 @@ private: QString trim(QString string) const; private Q_SLOTS: + void itemTextFormatChanged(); void itemCursorPositionChanged(); }; diff --git a/src/messagecontent/models/chatbarmessagecontentmodel.cpp b/src/messagecontent/models/chatbarmessagecontentmodel.cpp index de3c559dd..10e341689 100644 --- a/src/messagecontent/models/chatbarmessagecontentmodel.cpp +++ b/src/messagecontent/models/chatbarmessagecontentmodel.cpp @@ -91,12 +91,15 @@ ChatBarMessageContentModel::ChatBarMessageContentModel(QObject *parent) initializeModel(); } -void ChatBarMessageContentModel::initializeModel() +void ChatBarMessageContentModel::initializeModel(const QString &initialText) { + updateReplyModel(); + beginInsertRows({}, rowCount(), rowCount()); const auto textItem = new ChatTextItemHelper(this); textItem->setRoom(m_room); textItem->setType(m_type); + textItem->setInitialText(initialText); connectTextItem(textItem); m_components += MessageComponent{ .type = MessageComponentType::Text, @@ -248,6 +251,10 @@ ChatTextItemHelper *ChatBarMessageContentModel::focusedTextItem() const void ChatBarMessageContentModel::connectTextItem(ChatTextItemHelper *chattextitemhelper) { connect(chattextitemhelper, &ChatTextItemHelper::contentsChanged, this, &ChatBarMessageContentModel::updateCache); + connect(chattextitemhelper, &ChatTextItemHelper::contentsChanged, this, &ChatBarMessageContentModel::hasRichFormattingChanged); + connect(chattextitemhelper, &ChatTextItemHelper::charFormatChanged, this, &ChatBarMessageContentModel::hasRichFormattingChanged); + connect(chattextitemhelper, &ChatTextItemHelper::styleChanged, this, &ChatBarMessageContentModel::hasRichFormattingChanged); + connect(chattextitemhelper, &ChatTextItemHelper::listChanged, this, &ChatBarMessageContentModel::hasRichFormattingChanged); connect(chattextitemhelper, &ChatTextItemHelper::cleared, this, [this](ChatTextItemHelper *helper) { removeComponent(helper); }); @@ -281,12 +288,37 @@ QModelIndex ChatBarMessageContentModel::indexForTextItem(ChatTextItemHelper *tex return {}; } +bool ChatBarMessageContentModel::hasRichFormatting() const +{ + for (const auto &component : m_components) { + if (component.type != MessageComponentType::Text) { + return true; + } + if (const auto textItem = textItemForComponent(component)) { + if (textItem->hasRichFormatting()) { + return true; + } + } + } + return false; +} + void ChatBarMessageContentModel::addAttachment(const QUrl &path) { if (m_type == ChatBarType::None || !m_room) { return; } + QString plainText; + for (const auto &component : m_components) { + if (const auto textItem = textItemForComponent(component)) { + plainText += u"%1%2"_s.arg(plainText.isEmpty() ? u""_s : u"\n"_s, textItem->plainText()); + } + } + + clearModel(); + initializeModel(plainText); + auto it = insertComponent(m_components.first().type == MessageComponentType::Reply ? 1 : 0, MessageComponentType::typeForPath(path), { @@ -295,19 +327,7 @@ void ChatBarMessageContentModel::addAttachment(const QUrl &path) {"animated"_L1, false}, }); it->display = path.fileName(); - ++it; Q_EMIT dataChanged(index(std::distance(m_components.begin(), it)), index(std::distance(m_components.begin(), it)), {DisplayRole}); - - bool textKept = false; - while (it != m_components.end()) { - if (it->type != MessageComponentType::Text || textKept) { - it = removeComponent(it); - } else { - textKept = true; - ++it; - } - } - m_room->cacheForType(m_type)->setAttachmentPath(path.toString()); } @@ -338,6 +358,8 @@ ChatBarMessageContentModel::insertComponent(int row, MessageComponentType::Type .attributes = attributes, }); endInsertRows(); + Q_EMIT hasRichFormattingChanged(); + return it; } @@ -454,6 +476,8 @@ ChatBarMessageContentModel::ComponentIt ChatBarMessageContentModel::removeCompon it = m_components.erase(it); endRemoveRows(); + Q_EMIT hasRichFormattingChanged(); + return it; } @@ -566,6 +590,9 @@ void ChatBarMessageContentModel::postMessage() std::optional ChatBarMessageContentModel::getReplyEventId() { + if (!m_room) { + return std::nullopt; + } return m_room->mainCache()->isReplying() ? std::make_optional(m_room->mainCache()->replyId()) : std::nullopt; } diff --git a/src/messagecontent/models/chatbarmessagecontentmodel.h b/src/messagecontent/models/chatbarmessagecontentmodel.h index cde18d3c6..632a12bcc 100644 --- a/src/messagecontent/models/chatbarmessagecontentmodel.h +++ b/src/messagecontent/models/chatbarmessagecontentmodel.h @@ -59,6 +59,13 @@ class ChatBarMessageContentModel : public MessageContentModel */ Q_PROPERTY(ChatTextItemHelper *focusedTextItem READ focusedTextItem NOTIFY focusRowChanged) + /** + * @brief Whether there is any rich formatting in any of the model components. + * + * If true the contents of the model will change if an attachment is added. + */ + Q_PROPERTY(bool hasRichFormatting READ hasRichFormatting NOTIFY hasRichFormattingChanged) + public: explicit ChatBarMessageContentModel(QObject *parent = nullptr); @@ -75,6 +82,7 @@ public: Q_INVOKABLE void insertComponentAtCursor(MessageComponentType::Type type); + bool hasRichFormatting() const; Q_INVOKABLE void addAttachment(const QUrl &path); Q_INVOKABLE void removeComponent(int row, bool removeLast = false); @@ -86,11 +94,12 @@ public: Q_SIGNALS: void typeChanged(); void focusRowChanged(); + void hasRichFormattingChanged(); private: ChatBarType::Type m_type = ChatBarType::None; - void initializeModel(); + void initializeModel(const QString &initialText = {}); std::optional getReplyEventId() override;