Spearate completion from ChatDocumentHandler
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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);
|
||||
|
||||
@@ -150,7 +150,6 @@ QQC2.Control {
|
||||
CompletionMenu {
|
||||
id: completionMenu
|
||||
chatDocumentHandler: contentModel.focusedDocumentHandler
|
||||
connection: root.connection
|
||||
|
||||
x: 1
|
||||
y: -height
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ target_sources(LibNeoChat PRIVATE
|
||||
neochatdatetime.cpp
|
||||
nestedlisthelper_p.h
|
||||
nestedlisthelper.cpp
|
||||
qmltextitemwrapper.cpp
|
||||
roomlastmessageprovider.cpp
|
||||
spacehierarchycache.cpp
|
||||
texthandler.cpp
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
98
src/libneochat/qmltextitemwrapper.cpp
Normal file
98
src/libneochat/qmltextitemwrapper.cpp
Normal 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"
|
||||
51
src/libneochat/qmltextitemwrapper.h
Normal file
51
src/libneochat/qmltextitemwrapper.h
Normal 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();
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user