Spearate completion from ChatDocumentHandler

This commit is contained in:
James Graham
2025-12-23 16:22:09 +00:00
parent 02bed79265
commit 416d85af3b
13 changed files with 283 additions and 210 deletions

View File

@@ -45,12 +45,6 @@ ecm_add_test(
TEST_NAME chatbarcachetest
)
ecm_add_test(
chatdocumenthandlertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME chatdocumenthandlertest
)
ecm_add_test(
timelinemessagemodeltest.cpp
LINK_LIBRARIES neochat Qt::Test

View File

@@ -1,36 +0,0 @@
// SPDX-FileCopyrightText: 2023 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 <QObject>
#include <QTest>
#include "chatdocumenthandler.h"
#include "neochatconfig.h"
class ChatDocumentHandlerTest : public QObject
{
Q_OBJECT
private:
ChatDocumentHandler emptyHandler;
private Q_SLOTS:
void initTestCase();
void nullComplete();
};
void ChatDocumentHandlerTest::initTestCase()
{
// HACK: this is to stop KStatusNotifierItem SEGFAULTING on cleanup.
NeoChatConfig::self()->setSystemTray(false);
}
void ChatDocumentHandlerTest::nullComplete()
{
QTest::ignoreMessage(QtWarningMsg, "complete called with m_document set to nullptr.");
emptyHandler.complete(0);
}
QTEST_MAIN(ChatDocumentHandlerTest)
#include "chatdocumenthandlertest.moc"

View File

@@ -14,14 +14,13 @@ TestCase {
}
TextEdit {
id: TextEdit
id: textEdit
}
function test_empty(): void {
compare(documentHandler.type, LibNeoChat.ChatBarType.None);
compare(documentHandler.room, null);
compare(documentHandler.textItem, null);
compare(documentHandler.completionModel instanceof LibNeoChat.CompletionModel, true);
compare(documentHandler.atFirstLine, false);
compare(documentHandler.atLastLine, false);
compare(documentHandler.bold, false);

View File

@@ -150,7 +150,6 @@ QQC2.Control {
CompletionMenu {
id: completionMenu
chatDocumentHandler: contentModel.focusedDocumentHandler
connection: root.connection
x: 1
y: -height

View File

@@ -16,12 +16,7 @@ import org.kde.neochat
QQC2.Popup {
id: root
required property NeoChatConnection connection
required property var chatDocumentHandler
onChatDocumentHandlerChanged: if (chatDocumentHandler) {
chatDocumentHandler.completionModel.roomListModel = RoomManager.roomListModel;
chatDocumentHandler.completionModel.userListModel = RoomManager.userListModel;
}
visible: completions.count > 0
@@ -38,7 +33,7 @@ QQC2.Popup {
}
function complete() {
root.chatDocumentHandler.complete(completions.currentIndex);
root.chatDocumentHandler.insertCompletion(completions.currentItem.replacedText, completions.currentItem.hRef)
}
leftPadding: 0
@@ -65,7 +60,11 @@ QQC2.Popup {
ListView {
id: completions
model: root.chatDocumentHandler.completionModel
model: CompletionModel {
textItem: root.chatDocumentHandler.textItem
roomListModel: RoomManager.roomListModel
userListModel: RoomManager.userListModel
}
currentIndex: 0
keyNavigationWraps: true
highlightMoveDuration: 100
@@ -77,6 +76,8 @@ QQC2.Popup {
required property string displayName
required property string subtitle
required property string iconName
required property string replacedText
required property url hRef
text: displayName
@@ -96,7 +97,7 @@ QQC2.Popup {
subtitleItem.textFormat: Text.PlainText
}
}
onClicked: root.chatDocumentHandler.complete(completionDelegate.index)
onClicked: root.chatDocumentHandler.insertCompletion(replacedText, hRef)
}
}
}

View File

@@ -21,6 +21,7 @@ target_sources(LibNeoChat PRIVATE
neochatdatetime.cpp
nestedlisthelper_p.h
nestedlisthelper.cpp
qmltextitemwrapper.cpp
roomlastmessageprovider.cpp
spacehierarchycache.cpp
texthandler.cpp

View File

@@ -10,6 +10,7 @@
#include <QStringBuilder>
#include <QSyntaxHighlighter>
#include <QTextBlock>
#include <QTextBoundaryFinder>
#include <QTextDocument>
#include <QTextDocumentFragment>
#include <QTextList>
@@ -84,6 +85,7 @@ public:
setFormat(error.first, error.second.size(), errorFormat);
}
}
auto handler = dynamic_cast<ChatDocumentHandler *>(parent());
auto room = handler->room();
if (!room) {
@@ -109,6 +111,7 @@ public:
mention.cursor.setPosition(mention.cursor.anchor() + mention.text.size(), QTextCursor::KeepAnchor);
}
qWarning() << mention.cursor.selectedText() << mention.text;
if (mention.cursor.selectedText() != mention.text) {
return true;
}
@@ -128,29 +131,12 @@ private:
ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
: QObject(parent)
, m_markdownHelper(new ChatMarkdownHelper(this))
, m_highlighter(new SyntaxHighlighter(this))
, m_completionModel(new CompletionModel(this))
{
m_markdownHelper = new ChatMarkdownHelper(this);
connect(this, &ChatDocumentHandler::formatChanged, m_markdownHelper, &ChatMarkdownHelper::handleExternalFormatChange);
}
int ChatDocumentHandler::completionStartIndex() const
{
const qsizetype cursor = cursorPosition();
const auto &text = getText();
auto start = std::min(cursor, text.size()) - 1;
while (start > -1) {
if (text.at(start) == QLatin1Char(' ')) {
start++;
break;
}
start--;
}
return start;
}
ChatBarType::Type ChatDocumentHandler::type() const
{
return m_type;
@@ -177,7 +163,6 @@ void ChatDocumentHandler::setRoom(NeoChatRoom *room)
}
m_room = room;
m_completionModel->setRoom(m_room);
Q_EMIT roomChanged();
}
@@ -200,8 +185,8 @@ void ChatDocumentHandler::setTextItem(QQuickItem *textItem)
}
m_textItem = textItem;
m_highlighter->setDocument(document());
if (m_textItem) {
connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(updateCursor()));
if (document()) {
@@ -338,9 +323,6 @@ int ChatDocumentHandler::cursorPosition() const
void ChatDocumentHandler::updateCursor()
{
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
Q_EMIT atFirstLineChanged();
Q_EMIT atLastLineChanged();
}
@@ -559,71 +541,6 @@ void ChatDocumentHandler::insertFragment(const QTextDocumentFragment fragment, I
}
}
void ChatDocumentHandler::complete(int index)
{
if (document() == nullptr) {
qCWarning(ChatDocumentHandling) << "complete called with m_document set to nullptr.";
return;
}
if (m_completionModel->autoCompletionType() == CompletionModel::None) {
qCWarning(ChatDocumentHandling) << "complete called with m_completionModel->autoCompletionType() == CompletionModel::None.";
return;
}
// Ensure we only search for the beginning of the current completion identifier
const auto fromIndex = qMax(completionStartIndex(), 0);
if (m_completionModel->autoCompletionType() == CompletionModel::User) {
auto name = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::DisplayNameRole).toString();
auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString();
auto text = getText();
auto at = text.indexOf(QLatin1Char('@'), fromIndex);
QTextCursor cursor(document());
cursor.setPosition(at);
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
cursor.insertText(name + u" "_s);
cursor.setPosition(at);
cursor.setPosition(cursor.position() + name.size(), QTextCursor::KeepAnchor);
cursor.setKeepPositionOnInsert(true);
pushMention({cursor, name, 0, 0, id});
m_highlighter->rehighlight();
} else if (m_completionModel->autoCompletionType() == CompletionModel::Command) {
auto command = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedTextRole).toString();
auto text = getText();
auto at = text.indexOf(QLatin1Char('/'), fromIndex);
QTextCursor cursor(document());
cursor.setPosition(at);
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
cursor.insertText(u"/%1 "_s.arg(command));
} else if (m_completionModel->autoCompletionType() == CompletionModel::Room) {
auto alias = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString();
auto text = getText();
auto at = text.indexOf(QLatin1Char('#'), fromIndex);
QTextCursor cursor(document());
cursor.setPosition(at);
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
cursor.insertText(alias + u" "_s);
cursor.setPosition(at);
cursor.setPosition(cursor.position() + alias.size(), QTextCursor::KeepAnchor);
cursor.setKeepPositionOnInsert(true);
pushMention({cursor, alias, 0, 0, alias});
m_highlighter->rehighlight();
} else if (m_completionModel->autoCompletionType() == CompletionModel::Emoji) {
auto shortcode = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedTextRole).toString();
auto text = getText();
auto at = text.indexOf(QLatin1Char(':'), fromIndex);
QTextCursor cursor(document());
cursor.setPosition(at);
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
cursor.insertText(shortcode);
}
}
CompletionModel *ChatDocumentHandler::completionModel() const
{
return m_completionModel;
}
QString ChatDocumentHandler::getText() const
{
if (!document()) {
@@ -861,6 +778,39 @@ void ChatDocumentHandler::insertTable(int rows, int columns)
return;
}
void ChatDocumentHandler::insertCompletion(const QString &text, const QUrl &link)
{
QTextCursor cursor = textCursor();
if (cursor.isNull()) {
return;
}
cursor.beginEditBlock();
while (!cursor.selectedText().startsWith(u' ') && !cursor.atBlockStart()) {
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
}
if (cursor.selectedText().startsWith(u' ')) {
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
}
cursor.removeSelectedText();
const int start = cursor.position();
const auto insertString = u"%1 %2"_s.arg(text, link.isEmpty() ? QString() : u" "_s);
cursor.insertText(insertString);
cursor.setPosition(start);
cursor.setPosition(start + text.size(), QTextCursor::KeepAnchor);
cursor.setKeepPositionOnInsert(true);
cursor.endEditBlock();
if (!link.isEmpty()) {
pushMention({
.cursor = cursor,
.text = text,
.id = link.toString(),
});
}
m_highlighter->rehighlight();
}
void ChatDocumentHandler::updateLink(const QString &linkUrl, const QString &linkText)
{
auto cursor = textCursor();

View File

@@ -14,7 +14,6 @@
#include "chatmarkdownhelper.h"
#include "enums/chatbartype.h"
#include "enums/richformat.h"
#include "models/completionmodel.h"
#include "neochatroom.h"
#include "nestedlisthelper_p.h"
@@ -85,14 +84,6 @@ class ChatDocumentHandler : public QObject
*/
Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
/**
* @brief The current CompletionModel.
*
* This is typically provided to a qml component to visualise the current
* completion results.
*/
Q_PROPERTY(CompletionModel *completionModel READ completionModel CONSTANT)
/**
* @brief Whether the cursor is currently on the first line.
*/
@@ -155,10 +146,6 @@ public:
QTextDocumentFragment takeFirstBlock();
void fillFragments(bool &hasBefore, QTextDocumentFragment &midFragment, std::optional<QTextDocumentFragment> &afterFragment);
Q_INVOKABLE void complete(int index);
CompletionModel *completionModel() const;
/**
* @brief Update the mentions in @p document when editing a message.
*/
@@ -194,6 +181,7 @@ public:
Q_INVOKABLE void updateLink(const QString &linkUrl, const QString &linkText);
Q_INVOKABLE void insertImage(const QUrl &imagePath);
Q_INVOKABLE void insertTable(int rows, int columns);
Q_INVOKABLE void insertCompletion(const QString &text, const QUrl &link);
Q_INVOKABLE void dumpHtml();
Q_INVOKABLE QString htmlText() const;
@@ -248,9 +236,6 @@ private:
SyntaxHighlighter *m_highlighter = nullptr;
int completionStartIndex() const;
CompletionModel *m_completionModel = nullptr;
QString getText() const;
void pushMention(const Mention mention) const;

View File

@@ -2,35 +2,55 @@
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "completionmodel.h"
#include <QDebug>
#include <QTextCursor>
#include "completionproxymodel.h"
#include "models/actionsmodel.h"
#include "models/customemojimodel.h"
#include "models/emojimodel.h"
#include "neochatroom.h"
#include "qmltextitemwrapper.h"
#include "userlistmodel.h"
CompletionModel::CompletionModel(QObject *parent)
: QAbstractListModel(parent)
, m_textItem(new QmlTextItemWrapper(this))
, m_filterModel(new CompletionProxyModel(this))
, m_emojiModel(new QConcatenateTablesProxyModel(this))
{
connect(this, &CompletionModel::textChanged, this, &CompletionModel::updateCompletion);
connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, &CompletionModel::textItemChanged);
connect(m_textItem, &QmlTextItemWrapper::textDocumentCursorPositionChanged, this, &CompletionModel::updateTextStart);
connect(m_textItem, &QmlTextItemWrapper::textDocumentContentsChanged, this, &CompletionModel::updateCompletion);
m_emojiModel->addSourceModel(&CustomEmojiModel::instance());
m_emojiModel->addSourceModel(&EmojiModel::instance());
}
QString CompletionModel::text() const
QQuickItem *CompletionModel::textItem() const
{
return m_text;
return m_textItem->textItem();
}
void CompletionModel::setText(const QString &text, const QString &fullText)
void CompletionModel::setTextItem(QQuickItem *textItem)
{
m_text = text;
m_fullText = fullText;
Q_EMIT textChanged();
m_textItem->setTextItem(textItem);
}
void CompletionModel::updateTextStart()
{
auto cursor = m_textItem->textCursor();
if (cursor.isNull()) {
return;
}
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
while (cursor.selectedText() != u' ' && !cursor.atBlockStart()) {
cursor.movePosition(QTextCursor::PreviousCharacter);
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
}
m_textStart = cursor.position() == 0 && cursor.selectedText() != u' ' ? 0 : cursor.position() + 1;
updateCompletion();
}
int CompletionModel::rowCount(const QModelIndex &parent) const
@@ -58,6 +78,12 @@ QVariant CompletionModel::data(const QModelIndex &index, int role) const
if (role == IconNameRole) {
return m_filterModel->data(filterIndex, UserListModel::AvatarRole);
}
if (role == ReplacedTextRole) {
return m_filterModel->data(filterIndex, UserListModel::DisplayNameRole);
}
if (role == HRefRole) {
return u"https://matrix.to/#/%1"_s.arg(m_filterModel->data(filterIndex, UserListModel::UserIdRole).toString());
}
}
if (m_autoCompletionType == Command) {
@@ -85,6 +111,12 @@ QVariant CompletionModel::data(const QModelIndex &index, int role) const
if (role == IconNameRole) {
return m_filterModel->data(filterIndex, RoomListModel::AvatarRole).toString();
}
if (role == ReplacedTextRole) {
return m_filterModel->data(filterIndex, RoomListModel::CanonicalAliasRole);
}
if (role == HRefRole) {
return u"https://matrix.to/#/%1"_s.arg(m_filterModel->data(filterIndex, RoomListModel::CanonicalAliasRole).toString());
}
}
if (m_autoCompletionType == Emoji) {
if (role == DisplayNameRole) {
@@ -111,44 +143,57 @@ QHash<int, QByteArray> CompletionModel::roleNames() const
{SubtitleRole, "subtitle"},
{IconNameRole, "iconName"},
{ReplacedTextRole, "replacedText"},
{HRefRole, "hRef"},
};
}
void CompletionModel::updateCompletion()
{
if (text().startsWith(QLatin1Char('@'))) {
auto cursor = m_textItem->textCursor();
if (cursor.isNull()) {
return;
}
cursor.setPosition(m_textStart);
while (!cursor.selectedText().endsWith(u' ') && !cursor.atBlockEnd()) {
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
}
const auto text = cursor.selectedText().trimmed();
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
const auto fullText = cursor.selectedText();
if (text.startsWith(QLatin1Char('@'))) {
m_filterModel->setSourceModel(m_userListModel);
m_filterModel->setFilterRole(UserListModel::UserIdRole);
m_filterModel->setSecondaryFilterRole(UserListModel::DisplayNameRole);
m_filterModel->setFullText(m_fullText);
m_filterModel->setFilterText(m_text);
m_filterModel->setFullText(fullText);
m_filterModel->setFilterText(text);
m_autoCompletionType = User;
m_filterModel->invalidate();
} else if (text().startsWith(QLatin1Char('/'))) {
} else if (text.startsWith(QLatin1Char('/'))) {
m_filterModel->setSourceModel(&ActionsModel::instance());
m_filterModel->setFilterRole(ActionsModel::Prefix);
m_filterModel->setSecondaryFilterRole(-1);
m_filterModel->setFullText(m_fullText);
m_filterModel->setFilterText(m_text.mid(1));
m_filterModel->setFullText(fullText);
m_filterModel->setFilterText(text.mid(1));
m_autoCompletionType = Command;
m_filterModel->invalidate();
} else if (text().startsWith(QLatin1Char('#'))) {
} else if (text.startsWith(QLatin1Char('#'))) {
m_autoCompletionType = Room;
m_filterModel->setSourceModel(m_roomListModel);
m_filterModel->setFilterRole(RoomListModel::CanonicalAliasRole);
m_filterModel->setSecondaryFilterRole(RoomListModel::DisplayNameRole);
m_filterModel->setFullText(m_fullText);
m_filterModel->setFilterText(m_text);
m_filterModel->setFullText(fullText);
m_filterModel->setFilterText(text);
m_filterModel->invalidate();
} else if (text().startsWith(QLatin1Char(':')) && text().size() > 1 && !text()[1].isUpper()
&& (m_fullText.indexOf(QLatin1Char(':'), 1) == -1
|| (m_fullText.indexOf(QLatin1Char(' ')) != -1 && m_fullText.indexOf(QLatin1Char(':'), 1) > m_fullText.indexOf(QLatin1Char(' '), 1)))) {
} else if (text.startsWith(QLatin1Char(':')) && text.size() > 1 && !text[1].isUpper()
&& (fullText.indexOf(QLatin1Char(':'), 1) == -1
|| (fullText.indexOf(QLatin1Char(' ')) != -1 && fullText.indexOf(QLatin1Char(':'), 1) > fullText.indexOf(QLatin1Char(' '), 1)))) {
m_filterModel->setSourceModel(m_emojiModel);
m_autoCompletionType = Emoji;
m_filterModel->setFilterRole(CustomEmojiModel::Name);
m_filterModel->setSecondaryFilterRole(EmojiModel::DescriptionRole);
m_filterModel->setFullText(m_fullText);
m_filterModel->setFilterText(m_text);
m_filterModel->setFullText(fullText);
m_filterModel->setFilterText(text);
m_filterModel->invalidate();
} else {
m_autoCompletionType = None;
@@ -157,17 +202,6 @@ void CompletionModel::updateCompletion()
endResetModel();
}
NeoChatRoom *CompletionModel::room() const
{
return m_room;
}
void CompletionModel::setRoom(NeoChatRoom *room)
{
m_room = room;
Q_EMIT roomChanged();
}
CompletionModel::AutoCompletionType CompletionModel::autoCompletionType() const
{
return m_autoCompletionType;

View File

@@ -5,13 +5,14 @@
#include <QConcatenateTablesProxyModel>
#include <QQmlEngine>
#include <QQuickItem>
#include <QSortFilterProxyModel>
#include "roomlistmodel.h"
class CompletionProxyModel;
class UserListModel;
class NeoChatRoom;
class QmlTextItemWrapper;
class RoomListModel;
/**
@@ -28,14 +29,9 @@ class CompletionModel : public QAbstractListModel
QML_ELEMENT
/**
* @brief The current text to search for completions.
* @brief The QML text Item that completions are being provided for.
*/
Q_PROPERTY(QString text READ text NOTIFY textChanged)
/**
* @brief The current room that the model is getting completions for.
*/
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
/**
* @brief The current type of completion being done on the entered text.
@@ -72,14 +68,18 @@ public:
*/
enum Roles {
DisplayNameRole = Qt::DisplayRole, /**< The main text to show. */
SubtitleRole, /**< The subtitle text to show. */
SubtitleRole = Qt::UserRole, /**< The subtitle text to show. */
IconNameRole, /**< The icon to show. */
ReplacedTextRole, /**< The text to replace the input text with for the completion. */
HRefRole, /**< The hyperlink if applicable for the completion. */
};
Q_ENUM(Roles)
explicit CompletionModel(QObject *parent = nullptr);
QQuickItem *textItem() const;
void setTextItem(QQuickItem *textItem);
/**
* @brief Get the given role value at the given index.
*
@@ -101,12 +101,6 @@ public:
*/
QHash<int, QByteArray> roleNames() const override;
QString text() const;
void setText(const QString &text, const QString &fullText);
NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
RoomListModel *roomListModel() const;
void setRoomListModel(RoomListModel *roomListModel);
@@ -117,17 +111,20 @@ public:
void setAutoCompletionType(AutoCompletionType autoCompletionType);
Q_SIGNALS:
void textChanged();
void textItemChanged();
void roomChanged();
void autoCompletionTypeChanged();
void roomListModelChanged();
void userListModelChanged();
private:
QString m_text;
QString m_fullText;
QPointer<QmlTextItemWrapper> m_textItem;
int m_textStart = 0;
void updateTextStart();
CompletionProxyModel *m_filterModel;
QPointer<NeoChatRoom> m_room;
AutoCompletionType m_autoCompletionType = None;
void updateCompletion();

View File

@@ -0,0 +1,98 @@
// 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 "qmltextitemwrapper.h"
#include <QQuickTextDocument>
#include <QTextCursor>
QmlTextItemWrapper::QmlTextItemWrapper(QObject *parent)
: QObject(parent)
{
}
QQuickItem *QmlTextItemWrapper::textItem() const
{
return m_textItem;
}
void QmlTextItemWrapper::setTextItem(QQuickItem *textItem)
{
if (textItem == m_textItem) {
return;
}
if (m_textItem) {
m_textItem->disconnect(this);
if (const auto textDoc = document()) {
textDoc->disconnect(this);
}
}
m_textItem = textItem;
if (m_textItem) {
connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(textDocCursorChanged()));
if (document()) {
connect(document(), &QTextDocument::contentsChanged, this, &QmlTextItemWrapper::textDocumentContentsChanged);
}
}
Q_EMIT textItemChanged();
}
QTextDocument *QmlTextItemWrapper::document() const
{
if (!m_textItem) {
return nullptr;
}
const auto quickDocument = qvariant_cast<QQuickTextDocument *>(textItem()->property("textDocument"));
return quickDocument ? quickDocument->textDocument() : nullptr;
}
int QmlTextItemWrapper::cursorPosition() const
{
if (!m_textItem) {
return -1;
}
return m_textItem->property("cursorPosition").toInt();
}
int QmlTextItemWrapper::selectionStart() const
{
if (!m_textItem) {
return -1;
}
return m_textItem->property("selectionStart").toInt();
}
int QmlTextItemWrapper::selectionEnd() const
{
if (!m_textItem) {
return -1;
}
return m_textItem->property("selectionEnd").toInt();
}
QTextCursor QmlTextItemWrapper::textCursor() const
{
if (!document()) {
return QTextCursor();
}
QTextCursor cursor = QTextCursor(document());
if (selectionStart() != selectionEnd()) {
cursor.setPosition(selectionStart());
cursor.setPosition(selectionEnd(), QTextCursor::KeepAnchor);
} else {
cursor.setPosition(cursorPosition());
}
return cursor;
}
void QmlTextItemWrapper::textDocCursorChanged()
{
Q_EMIT textDocumentCursorPositionChanged();
}
#include "moc_qmltextitemwrapper.cpp"

View File

@@ -0,0 +1,51 @@
// 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 <QQuickItem>
class QTextDocument;
/**
* @class QmlTextItemWrapper
*
* A class to wrap around a QQuickItem that is a QML TextEdit (or inherited from it) and provide easy acess to its properties.
*
* This basically exists because Qt does not give us access to the cpp headers of
* most QML items.
*
* @sa QQuickItem, TextEdit
*/
class QmlTextItemWrapper : public QObject
{
Q_OBJECT
public:
explicit QmlTextItemWrapper(QObject *parent);
QQuickItem *textItem() const;
void setTextItem(QQuickItem *textItem);
QTextDocument *document() const;
QTextCursor textCursor() const;
Q_SIGNALS:
void textItemChanged();
void textDocumentContentsChanged();
void textDocumentCursorPositionChanged();
private:
QPointer<QQuickItem> m_textItem;
int cursorPosition() const;
int selectionStart() const;
int selectionEnd() const;
private Q_SLOTS:
void textDocCursorChanged();
};

View File

@@ -110,7 +110,7 @@ QQC2.Control {
height: implicitHeight
y: -height - 5
z: 10
connection: root.Message.room.connection as NeoChatConnection
chatDocumentHandler: documentHandler
margins: 0
Behavior on height {