Simpify the API for ChatDocumentHandler

Simpify the API for ChatDocumentHandler by taking the text item and grabbing everything else needed from there
This commit is contained in:
James Graham
2025-08-11 19:23:40 +01:00
parent 5f7ff209d3
commit bc82ceeb5f
4 changed files with 80 additions and 187 deletions

View File

@@ -500,13 +500,8 @@ QQC2.Control {
ChatDocumentHandler { ChatDocumentHandler {
id: documentHandler id: documentHandler
type: ChatBarType.Room type: ChatBarType.Room
textItem: textField
room: root.currentRoom room: root.currentRoom
document: textField.textDocument
cursorPosition: textField.cursorPosition
selectionStart: textField.selectionStart
selectionEnd: textField.selectionEnd
mentionColor: Kirigami.Theme.linkColor
errorColor: Kirigami.Theme.negativeTextColor
} }
Component { Component {

View File

@@ -1,16 +1,19 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org> // SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
#include "chatdocumenthandler.h" #include "chatdocumenthandler.h"
#include <QQmlFile> #include <QQmlFile>
#include <QQmlFileSelector> #include <QQmlFileSelector>
#include <QQuickTextDocument>
#include <QStringBuilder> #include <QStringBuilder>
#include <QSyntaxHighlighter> #include <QSyntaxHighlighter>
#include <QTextBlock> #include <QTextBlock>
#include <QTextDocument> #include <QTextDocument>
#include <QTimer> #include <QTimer>
#include <Kirigami/Platform/PlatformTheme>
#include <Sonnet/BackgroundChecker> #include <Sonnet/BackgroundChecker>
#include <Sonnet/Settings> #include <Sonnet/Settings>
@@ -33,10 +36,16 @@ public:
SyntaxHighlighter(QObject *parent) SyntaxHighlighter(QObject *parent)
: QSyntaxHighlighter(parent) : QSyntaxHighlighter(parent)
{ {
mentionFormat.setFontWeight(QFont::Bold); m_theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
mentionFormat.setForeground(Qt::blue); connect(m_theme, &Kirigami::Platform::PlatformTheme::colorsChanged, this, [this]() {
mentionFormat.setForeground(m_theme->linkColor());
errorFormat.setForeground(m_theme->negativeTextColor());
});
errorFormat.setForeground(Qt::red); mentionFormat.setFontWeight(QFont::Bold);
mentionFormat.setForeground(m_theme->linkColor());
errorFormat.setForeground(m_theme->negativeTextColor());
errorFormat.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline); errorFormat.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
connect(checker, &Sonnet::BackgroundChecker::misspelling, this, [this](const QString &word, int start) { connect(checker, &Sonnet::BackgroundChecker::misspelling, this, [this](const QString &word, int start) {
@@ -101,29 +110,22 @@ public:
}), }),
mentions->end()); mentions->end());
} }
private:
Kirigami::Platform::PlatformTheme *m_theme = nullptr;
}; };
ChatDocumentHandler::ChatDocumentHandler(QObject *parent) ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
: QObject(parent) : QObject(parent)
, m_document(nullptr)
, m_cursorPosition(-1)
, m_highlighter(new SyntaxHighlighter(this)) , m_highlighter(new SyntaxHighlighter(this))
, m_completionModel(new CompletionModel(this)) , m_completionModel(new CompletionModel(this))
{ {
connect(this, &ChatDocumentHandler::documentChanged, this, [this]() { }
if (!m_document) {
m_highlighter->setDocument(nullptr); void ChatDocumentHandler::updateCompletion() const
return; {
} int start = completionStartIndex();
m_highlighter->setDocument(m_document->textDocument()); m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
});
connect(this, &ChatDocumentHandler::cursorPositionChanged, this, [this]() {
if (!m_room) {
return;
}
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
});
} }
int ChatDocumentHandler::completionStartIndex() const int ChatDocumentHandler::completionStartIndex() const
@@ -160,38 +162,49 @@ void ChatDocumentHandler::setType(ChatBarType::Type type)
Q_EMIT typeChanged(); Q_EMIT typeChanged();
} }
QQuickTextDocument *ChatDocumentHandler::document() const QQuickItem *ChatDocumentHandler::textItem() const
{ {
return m_document; return m_textItem;
} }
void ChatDocumentHandler::setDocument(QQuickTextDocument *document) void ChatDocumentHandler::setTextItem(QQuickItem *textItem)
{ {
if (document == m_document) { if (textItem == m_textItem) {
return; return;
} }
if (m_document) { if (m_textItem) {
m_document->textDocument()->disconnect(this); m_textItem->disconnect(this);
if (const auto textDoc = document()) {
textDoc->disconnect(this);
}
} }
m_document = document;
Q_EMIT documentChanged(); m_textItem = textItem;
m_highlighter->setDocument(document());
if (m_textItem) {
connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(updateCompletion()));
}
Q_EMIT textItemChanged();
}
QTextDocument *ChatDocumentHandler::document() const
{
if (!m_textItem) {
return nullptr;
}
const auto quickDocument = qvariant_cast<QQuickTextDocument *>(m_textItem->property("textDocument"));
return quickDocument ? quickDocument->textDocument() : nullptr;
} }
int ChatDocumentHandler::cursorPosition() const int ChatDocumentHandler::cursorPosition() const
{ {
return m_cursorPosition; if (!m_textItem) {
} return -1;
void ChatDocumentHandler::setCursorPosition(int position)
{
if (position == m_cursorPosition) {
return;
} }
if (m_room) { return m_textItem->property("cursorPosition").toInt();
m_cursorPosition = position;
}
Q_EMIT cursorPositionChanged();
} }
NeoChatRoom *ChatDocumentHandler::room() const NeoChatRoom *ChatDocumentHandler::room() const
@@ -207,8 +220,8 @@ void ChatDocumentHandler::setRoom(NeoChatRoom *room)
if (m_room && m_type != ChatBarType::None) { if (m_room && m_type != ChatBarType::None) {
m_room->cacheForType(m_type)->disconnect(this); m_room->cacheForType(m_type)->disconnect(this);
if (!m_room->isSpace() && m_document && m_type == ChatBarType::Room) { if (!m_room->isSpace() && document() && m_type == ChatBarType::Room) {
m_room->mainCache()->setSavedText(document()->textDocument()->toPlainText()); m_room->mainCache()->setSavedText(document()->toPlainText());
} }
} }
@@ -220,8 +233,8 @@ void ChatDocumentHandler::setRoom(NeoChatRoom *room)
int start = completionStartIndex(); int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start)); m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
}); });
if (!m_room->isSpace() && m_document && m_type == ChatBarType::Room) { if (!m_room->isSpace() && document() && m_type == ChatBarType::Room) {
document()->textDocument()->setPlainText(room->mainCache()->savedText()); document()->setPlainText(room->mainCache()->savedText());
m_room->mainCache()->setText(room->mainCache()->savedText()); m_room->mainCache()->setText(room->mainCache()->savedText());
} }
} }
@@ -239,7 +252,7 @@ ChatBarCache *ChatDocumentHandler::chatBarCache() const
void ChatDocumentHandler::complete(int index) void ChatDocumentHandler::complete(int index)
{ {
if (m_document == nullptr) { if (document() == nullptr) {
qCWarning(ChatDocumentHandling) << "complete called with m_document set to nullptr."; qCWarning(ChatDocumentHandling) << "complete called with m_document set to nullptr.";
return; return;
} }
@@ -256,7 +269,7 @@ void ChatDocumentHandler::complete(int index)
auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString(); auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString();
auto text = getText(); auto text = getText();
auto at = text.indexOf(QLatin1Char('@'), fromIndex); auto at = text.indexOf(QLatin1Char('@'), fromIndex);
QTextCursor cursor(document()->textDocument()); QTextCursor cursor(document());
cursor.setPosition(at); cursor.setPosition(at);
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor); cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
cursor.insertText(name + u" "_s); cursor.insertText(name + u" "_s);
@@ -269,7 +282,7 @@ void ChatDocumentHandler::complete(int index)
auto command = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedTextRole).toString(); auto command = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedTextRole).toString();
auto text = getText(); auto text = getText();
auto at = text.indexOf(QLatin1Char('/'), fromIndex); auto at = text.indexOf(QLatin1Char('/'), fromIndex);
QTextCursor cursor(document()->textDocument()); QTextCursor cursor(document());
cursor.setPosition(at); cursor.setPosition(at);
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor); cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
cursor.insertText(u"/%1 "_s.arg(command)); cursor.insertText(u"/%1 "_s.arg(command));
@@ -277,7 +290,7 @@ void ChatDocumentHandler::complete(int index)
auto alias = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString(); auto alias = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString();
auto text = getText(); auto text = getText();
auto at = text.indexOf(QLatin1Char('#'), fromIndex); auto at = text.indexOf(QLatin1Char('#'), fromIndex);
QTextCursor cursor(document()->textDocument()); QTextCursor cursor(document());
cursor.setPosition(at); cursor.setPosition(at);
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor); cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
cursor.insertText(alias + u" "_s); cursor.insertText(alias + u" "_s);
@@ -290,7 +303,7 @@ void ChatDocumentHandler::complete(int index)
auto shortcode = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedTextRole).toString(); auto shortcode = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedTextRole).toString();
auto text = getText(); auto text = getText();
auto at = text.indexOf(QLatin1Char(':'), fromIndex); auto at = text.indexOf(QLatin1Char(':'), fromIndex);
QTextCursor cursor(document()->textDocument()); QTextCursor cursor(document());
cursor.setPosition(at); cursor.setPosition(at);
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor); cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
cursor.insertText(shortcode); cursor.insertText(shortcode);
@@ -302,36 +315,6 @@ CompletionModel *ChatDocumentHandler::completionModel() const
return m_completionModel; return m_completionModel;
} }
int ChatDocumentHandler::selectionStart() const
{
return m_selectionStart;
}
void ChatDocumentHandler::setSelectionStart(int position)
{
if (position == m_selectionStart) {
return;
}
m_selectionStart = position;
Q_EMIT selectionStartChanged();
}
int ChatDocumentHandler::selectionEnd() const
{
return m_selectionEnd;
}
void ChatDocumentHandler::setSelectionEnd(int position)
{
if (position == m_selectionEnd) {
return;
}
m_selectionEnd = position;
Q_EMIT selectionEndChanged();
}
QString ChatDocumentHandler::getText() const QString ChatDocumentHandler::getText() const
{ {
if (!m_room || m_type == ChatBarType::None) { if (!m_room || m_type == ChatBarType::None) {
@@ -350,42 +333,8 @@ void ChatDocumentHandler::pushMention(const Mention mention) const
m_room->cacheForType(m_type)->mentions()->push_back(mention); m_room->cacheForType(m_type)->mentions()->push_back(mention);
} }
QColor ChatDocumentHandler::mentionColor() const void ChatDocumentHandler::updateMentions(const QString &editId)
{ {
return m_mentionColor;
}
void ChatDocumentHandler::setMentionColor(const QColor &color)
{
if (m_mentionColor == color) {
return;
}
m_mentionColor = color;
m_highlighter->mentionFormat.setForeground(m_mentionColor);
m_highlighter->rehighlight();
Q_EMIT mentionColorChanged();
}
QColor ChatDocumentHandler::errorColor() const
{
return m_errorColor;
}
void ChatDocumentHandler::setErrorColor(const QColor &color)
{
if (m_errorColor == color) {
return;
}
m_errorColor = color;
m_highlighter->errorFormat.setForeground(m_errorColor);
m_highlighter->rehighlight();
Q_EMIT errorColorChanged();
}
void ChatDocumentHandler::updateMentions(QQuickTextDocument *document, const QString &editId)
{
setDocument(document);
if (editId.isEmpty() || m_type == ChatBarType::None || !m_room) { if (editId.isEmpty() || m_type == ChatBarType::None || !m_room) {
return; return;
} }
@@ -409,7 +358,7 @@ void ChatDocumentHandler::updateMentions(QQuickTextDocument *document, const QSt
const int end = position + name.length(); const int end = position + name.length();
linkSize += match.capturedLength(0) - name.length(); linkSize += match.capturedLength(0) - name.length();
QTextCursor cursor(this->document()->textDocument()); QTextCursor cursor(document());
cursor.setPosition(position); cursor.setPosition(position);
cursor.setPosition(end, QTextCursor::KeepAnchor); cursor.setPosition(end, QTextCursor::KeepAnchor);
cursor.setKeepPositionOnInsert(true); cursor.setKeepPositionOnInsert(true);

View File

@@ -1,11 +1,11 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu> // SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <QQmlEngine> #include <QQmlEngine>
#include <QQuickTextDocument>
#include <QTextCursor> #include <QTextCursor>
#include "chatbarcache.h" #include "chatbarcache.h"
@@ -13,6 +13,8 @@
#include "models/completionmodel.h" #include "models/completionmodel.h"
#include "neochatroom.h" #include "neochatroom.h"
class QTextDocument;
class NeoChatRoom; class NeoChatRoom;
class SyntaxHighlighter; class SyntaxHighlighter;
@@ -69,24 +71,9 @@ class ChatDocumentHandler : public QObject
Q_PROPERTY(ChatBarType::Type type READ type WRITE setType NOTIFY typeChanged) Q_PROPERTY(ChatBarType::Type type READ type WRITE setType NOTIFY typeChanged)
/** /**
* @brief The QQuickTextDocument that is being handled. * @brief The QML text Item the ChatDocumentHandler is handling.
*/ */
Q_PROPERTY(QQuickTextDocument *document READ document WRITE setDocument NOTIFY documentChanged) Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
/**
* @brief The current saved cursor position.
*/
Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY cursorPositionChanged)
/**
* @brief The start position of any currently selected text.
*/
Q_PROPERTY(int selectionStart READ selectionStart WRITE setSelectionStart NOTIFY selectionStartChanged)
/**
* @brief The end position of any currently selected text.
*/
Q_PROPERTY(int selectionEnd READ selectionEnd WRITE setSelectionEnd NOTIFY selectionEndChanged)
/** /**
* @brief The current CompletionModel. * @brief The current CompletionModel.
@@ -101,33 +88,14 @@ class ChatDocumentHandler : public QObject
*/ */
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged) Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
/**
* @brief The color to highlight user mentions.
*/
Q_PROPERTY(QColor mentionColor READ mentionColor WRITE setMentionColor NOTIFY mentionColorChanged)
/**
* @brief The color to highlight spelling errors.
*/
Q_PROPERTY(QColor errorColor READ errorColor WRITE setErrorColor NOTIFY errorColorChanged)
public: public:
explicit ChatDocumentHandler(QObject *parent = nullptr); explicit ChatDocumentHandler(QObject *parent = nullptr);
ChatBarType::Type type() const; ChatBarType::Type type() const;
void setType(ChatBarType::Type type); void setType(ChatBarType::Type type);
[[nodiscard]] QQuickTextDocument *document() const; QQuickItem *textItem() const;
void setDocument(QQuickTextDocument *document); void setTextItem(QQuickItem *textItem);
[[nodiscard]] int cursorPosition() const;
void setCursorPosition(int position);
[[nodiscard]] int selectionStart() const;
void setSelectionStart(int position);
[[nodiscard]] int selectionEnd() const;
void setSelectionEnd(int position);
[[nodiscard]] NeoChatRoom *room() const; [[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room); void setRoom(NeoChatRoom *room);
@@ -138,41 +106,27 @@ public:
CompletionModel *completionModel() const; CompletionModel *completionModel() const;
[[nodiscard]] QColor mentionColor() const;
void setMentionColor(const QColor &color);
[[nodiscard]] QColor errorColor() const;
void setErrorColor(const QColor &color);
/** /**
* @brief Update the mentions in @p document when editing a message. * @brief Update the mentions in @p document when editing a message.
*/ */
Q_INVOKABLE void updateMentions(QQuickTextDocument *document, const QString &editId); Q_INVOKABLE void updateMentions(const QString &editId);
Q_SIGNALS: Q_SIGNALS:
void typeChanged(); void typeChanged();
void documentChanged(); void textItemChanged();
void cursorPositionChanged();
void roomChanged(); void roomChanged();
void selectionStartChanged();
void selectionEndChanged();
void errorColorChanged();
void mentionColorChanged();
private: private:
int completionStartIndex() const;
ChatBarType::Type m_type = ChatBarType::None; ChatBarType::Type m_type = ChatBarType::None;
QPointer<QQuickTextDocument> m_document; QPointer<QQuickItem> m_textItem;
QTextDocument *document() const;
void updateCompletion() const;
int completionStartIndex() const;
QPointer<NeoChatRoom> m_room; QPointer<NeoChatRoom> m_room;
QColor m_mentionColor; int cursorPosition() const;
QColor m_errorColor;
int m_cursorPosition;
int m_selectionStart;
int m_selectionEnd;
QString getText() const; QString getText() const;
void pushMention(const Mention mention) const; void pushMention(const Mention mention) const;

View File

@@ -125,13 +125,8 @@ QQC2.Control {
ChatDocumentHandler { ChatDocumentHandler {
id: documentHandler id: documentHandler
type: root.chatBarCache.isEditing ? ChatBarType.Edit : ChatBarType.Thread type: root.chatBarCache.isEditing ? ChatBarType.Edit : ChatBarType.Thread
document: textArea.textDocument textItem: textArea
cursorPosition: textArea.cursorPosition
selectionStart: textArea.selectionStart
selectionEnd: textArea.selectionEnd
room: root.Message.room room: root.Message.room
mentionColor: Kirigami.Theme.linkColor
errorColor: Kirigami.Theme.negativeTextColor
} }
TextMetrics { TextMetrics {
@@ -264,7 +259,7 @@ QQC2.Control {
documentHandler.document; documentHandler.document;
if (chatBarCache?.isEditing && chatBarCache.relationMessage.length > 0) { if (chatBarCache?.isEditing && chatBarCache.relationMessage.length > 0) {
textArea.text = chatBarCache.relationMessage; textArea.text = chatBarCache.relationMessage;
documentHandler.updateMentions(textArea.textDocument, chatBarCache.editId); documentHandler.updateMentions(chatBarCache.editId);
textArea.forceActiveFocus(); textArea.forceActiveFocus();
textArea.cursorPosition = textArea.text.length; textArea.cursorPosition = textArea.text.length;
} }