Separate ChatButtonHelper from ChatDocumentHandler

This commit is contained in:
James Graham
2025-12-29 16:24:24 +00:00
parent 45163944d0
commit 22d7d90cf4
19 changed files with 606 additions and 434 deletions

View File

@@ -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);
} }
} }

View File

@@ -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);
} }

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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
) )

View File

@@ -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) {

View File

@@ -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();
} }

View 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"

View 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;
};

View File

@@ -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();

View File

@@ -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;
}; };

View File

@@ -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()

View File

@@ -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;

View File

@@ -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());

View File

@@ -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)

View File

@@ -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();

View File

@@ -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();
}; };

View File

@@ -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();
} }

View File

@@ -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;