Spearate completion from ChatDocumentHandler
This commit is contained in:
@@ -45,12 +45,6 @@ ecm_add_test(
|
|||||||
TEST_NAME chatbarcachetest
|
TEST_NAME chatbarcachetest
|
||||||
)
|
)
|
||||||
|
|
||||||
ecm_add_test(
|
|
||||||
chatdocumenthandlertest.cpp
|
|
||||||
LINK_LIBRARIES neochat Qt::Test
|
|
||||||
TEST_NAME chatdocumenthandlertest
|
|
||||||
)
|
|
||||||
|
|
||||||
ecm_add_test(
|
ecm_add_test(
|
||||||
timelinemessagemodeltest.cpp
|
timelinemessagemodeltest.cpp
|
||||||
LINK_LIBRARIES neochat Qt::Test
|
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 {
|
TextEdit {
|
||||||
id: TextEdit
|
id: textEdit
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_empty(): void {
|
function test_empty(): void {
|
||||||
compare(documentHandler.type, LibNeoChat.ChatBarType.None);
|
compare(documentHandler.type, LibNeoChat.ChatBarType.None);
|
||||||
compare(documentHandler.room, null);
|
compare(documentHandler.room, null);
|
||||||
compare(documentHandler.textItem, null);
|
compare(documentHandler.textItem, null);
|
||||||
compare(documentHandler.completionModel instanceof LibNeoChat.CompletionModel, true);
|
|
||||||
compare(documentHandler.atFirstLine, false);
|
compare(documentHandler.atFirstLine, false);
|
||||||
compare(documentHandler.atLastLine, false);
|
compare(documentHandler.atLastLine, false);
|
||||||
compare(documentHandler.bold, false);
|
compare(documentHandler.bold, false);
|
||||||
|
|||||||
@@ -150,7 +150,6 @@ QQC2.Control {
|
|||||||
CompletionMenu {
|
CompletionMenu {
|
||||||
id: completionMenu
|
id: completionMenu
|
||||||
chatDocumentHandler: contentModel.focusedDocumentHandler
|
chatDocumentHandler: contentModel.focusedDocumentHandler
|
||||||
connection: root.connection
|
|
||||||
|
|
||||||
x: 1
|
x: 1
|
||||||
y: -height
|
y: -height
|
||||||
|
|||||||
@@ -16,12 +16,7 @@ import org.kde.neochat
|
|||||||
QQC2.Popup {
|
QQC2.Popup {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property NeoChatConnection connection
|
|
||||||
required property var chatDocumentHandler
|
required property var chatDocumentHandler
|
||||||
onChatDocumentHandlerChanged: if (chatDocumentHandler) {
|
|
||||||
chatDocumentHandler.completionModel.roomListModel = RoomManager.roomListModel;
|
|
||||||
chatDocumentHandler.completionModel.userListModel = RoomManager.userListModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
visible: completions.count > 0
|
visible: completions.count > 0
|
||||||
|
|
||||||
@@ -38,7 +33,7 @@ QQC2.Popup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function complete() {
|
function complete() {
|
||||||
root.chatDocumentHandler.complete(completions.currentIndex);
|
root.chatDocumentHandler.insertCompletion(completions.currentItem.replacedText, completions.currentItem.hRef)
|
||||||
}
|
}
|
||||||
|
|
||||||
leftPadding: 0
|
leftPadding: 0
|
||||||
@@ -65,7 +60,11 @@ QQC2.Popup {
|
|||||||
ListView {
|
ListView {
|
||||||
id: completions
|
id: completions
|
||||||
|
|
||||||
model: root.chatDocumentHandler.completionModel
|
model: CompletionModel {
|
||||||
|
textItem: root.chatDocumentHandler.textItem
|
||||||
|
roomListModel: RoomManager.roomListModel
|
||||||
|
userListModel: RoomManager.userListModel
|
||||||
|
}
|
||||||
currentIndex: 0
|
currentIndex: 0
|
||||||
keyNavigationWraps: true
|
keyNavigationWraps: true
|
||||||
highlightMoveDuration: 100
|
highlightMoveDuration: 100
|
||||||
@@ -77,6 +76,8 @@ QQC2.Popup {
|
|||||||
required property string displayName
|
required property string displayName
|
||||||
required property string subtitle
|
required property string subtitle
|
||||||
required property string iconName
|
required property string iconName
|
||||||
|
required property string replacedText
|
||||||
|
required property url hRef
|
||||||
|
|
||||||
text: displayName
|
text: displayName
|
||||||
|
|
||||||
@@ -96,7 +97,7 @@ QQC2.Popup {
|
|||||||
subtitleItem.textFormat: Text.PlainText
|
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
|
neochatdatetime.cpp
|
||||||
nestedlisthelper_p.h
|
nestedlisthelper_p.h
|
||||||
nestedlisthelper.cpp
|
nestedlisthelper.cpp
|
||||||
|
qmltextitemwrapper.cpp
|
||||||
roomlastmessageprovider.cpp
|
roomlastmessageprovider.cpp
|
||||||
spacehierarchycache.cpp
|
spacehierarchycache.cpp
|
||||||
texthandler.cpp
|
texthandler.cpp
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <QStringBuilder>
|
#include <QStringBuilder>
|
||||||
#include <QSyntaxHighlighter>
|
#include <QSyntaxHighlighter>
|
||||||
#include <QTextBlock>
|
#include <QTextBlock>
|
||||||
|
#include <QTextBoundaryFinder>
|
||||||
#include <QTextDocument>
|
#include <QTextDocument>
|
||||||
#include <QTextDocumentFragment>
|
#include <QTextDocumentFragment>
|
||||||
#include <QTextList>
|
#include <QTextList>
|
||||||
@@ -84,6 +85,7 @@ public:
|
|||||||
setFormat(error.first, error.second.size(), errorFormat);
|
setFormat(error.first, error.second.size(), errorFormat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto handler = dynamic_cast<ChatDocumentHandler *>(parent());
|
auto handler = dynamic_cast<ChatDocumentHandler *>(parent());
|
||||||
auto room = handler->room();
|
auto room = handler->room();
|
||||||
if (!room) {
|
if (!room) {
|
||||||
@@ -109,6 +111,7 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -128,29 +131,12 @@ private:
|
|||||||
|
|
||||||
ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
|
ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
|
, m_markdownHelper(new ChatMarkdownHelper(this))
|
||||||
, m_highlighter(new SyntaxHighlighter(this))
|
, m_highlighter(new SyntaxHighlighter(this))
|
||||||
, m_completionModel(new CompletionModel(this))
|
|
||||||
{
|
{
|
||||||
m_markdownHelper = new ChatMarkdownHelper(this);
|
|
||||||
connect(this, &ChatDocumentHandler::formatChanged, m_markdownHelper, &ChatMarkdownHelper::handleExternalFormatChange);
|
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
|
ChatBarType::Type ChatDocumentHandler::type() const
|
||||||
{
|
{
|
||||||
return m_type;
|
return m_type;
|
||||||
@@ -177,7 +163,6 @@ void ChatDocumentHandler::setRoom(NeoChatRoom *room)
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_room = room;
|
m_room = room;
|
||||||
m_completionModel->setRoom(m_room);
|
|
||||||
Q_EMIT roomChanged();
|
Q_EMIT roomChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,8 +185,8 @@ void ChatDocumentHandler::setTextItem(QQuickItem *textItem)
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_textItem = textItem;
|
m_textItem = textItem;
|
||||||
|
|
||||||
m_highlighter->setDocument(document());
|
m_highlighter->setDocument(document());
|
||||||
|
|
||||||
if (m_textItem) {
|
if (m_textItem) {
|
||||||
connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(updateCursor()));
|
connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(updateCursor()));
|
||||||
if (document()) {
|
if (document()) {
|
||||||
@@ -338,9 +323,6 @@ int ChatDocumentHandler::cursorPosition() const
|
|||||||
|
|
||||||
void ChatDocumentHandler::updateCursor()
|
void ChatDocumentHandler::updateCursor()
|
||||||
{
|
{
|
||||||
int start = completionStartIndex();
|
|
||||||
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
|
|
||||||
|
|
||||||
Q_EMIT atFirstLineChanged();
|
Q_EMIT atFirstLineChanged();
|
||||||
Q_EMIT atLastLineChanged();
|
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
|
QString ChatDocumentHandler::getText() const
|
||||||
{
|
{
|
||||||
if (!document()) {
|
if (!document()) {
|
||||||
@@ -861,6 +778,39 @@ void ChatDocumentHandler::insertTable(int rows, int columns)
|
|||||||
return;
|
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)
|
void ChatDocumentHandler::updateLink(const QString &linkUrl, const QString &linkText)
|
||||||
{
|
{
|
||||||
auto cursor = textCursor();
|
auto cursor = textCursor();
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
#include "chatmarkdownhelper.h"
|
#include "chatmarkdownhelper.h"
|
||||||
#include "enums/chatbartype.h"
|
#include "enums/chatbartype.h"
|
||||||
#include "enums/richformat.h"
|
#include "enums/richformat.h"
|
||||||
#include "models/completionmodel.h"
|
|
||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
#include "nestedlisthelper_p.h"
|
#include "nestedlisthelper_p.h"
|
||||||
|
|
||||||
@@ -85,14 +84,6 @@ class ChatDocumentHandler : public QObject
|
|||||||
*/
|
*/
|
||||||
Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
|
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.
|
* @brief Whether the cursor is currently on the first line.
|
||||||
*/
|
*/
|
||||||
@@ -155,10 +146,6 @@ public:
|
|||||||
QTextDocumentFragment takeFirstBlock();
|
QTextDocumentFragment takeFirstBlock();
|
||||||
void fillFragments(bool &hasBefore, QTextDocumentFragment &midFragment, std::optional<QTextDocumentFragment> &afterFragment);
|
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.
|
* @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 updateLink(const QString &linkUrl, const QString &linkText);
|
||||||
Q_INVOKABLE void insertImage(const QUrl &imagePath);
|
Q_INVOKABLE void insertImage(const QUrl &imagePath);
|
||||||
Q_INVOKABLE void insertTable(int rows, int columns);
|
Q_INVOKABLE void insertTable(int rows, int columns);
|
||||||
|
Q_INVOKABLE void insertCompletion(const QString &text, const QUrl &link);
|
||||||
|
|
||||||
Q_INVOKABLE void dumpHtml();
|
Q_INVOKABLE void dumpHtml();
|
||||||
Q_INVOKABLE QString htmlText() const;
|
Q_INVOKABLE QString htmlText() const;
|
||||||
@@ -248,9 +236,6 @@ private:
|
|||||||
|
|
||||||
SyntaxHighlighter *m_highlighter = nullptr;
|
SyntaxHighlighter *m_highlighter = nullptr;
|
||||||
|
|
||||||
int completionStartIndex() const;
|
|
||||||
|
|
||||||
CompletionModel *m_completionModel = nullptr;
|
|
||||||
QString getText() const;
|
QString getText() const;
|
||||||
void pushMention(const Mention mention) const;
|
void pushMention(const Mention mention) const;
|
||||||
|
|
||||||
|
|||||||
@@ -2,35 +2,55 @@
|
|||||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
#include "completionmodel.h"
|
#include "completionmodel.h"
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QTextCursor>
|
||||||
|
|
||||||
#include "completionproxymodel.h"
|
#include "completionproxymodel.h"
|
||||||
#include "models/actionsmodel.h"
|
#include "models/actionsmodel.h"
|
||||||
#include "models/customemojimodel.h"
|
#include "models/customemojimodel.h"
|
||||||
#include "models/emojimodel.h"
|
#include "models/emojimodel.h"
|
||||||
#include "neochatroom.h"
|
#include "qmltextitemwrapper.h"
|
||||||
#include "userlistmodel.h"
|
#include "userlistmodel.h"
|
||||||
|
|
||||||
CompletionModel::CompletionModel(QObject *parent)
|
CompletionModel::CompletionModel(QObject *parent)
|
||||||
: QAbstractListModel(parent)
|
: QAbstractListModel(parent)
|
||||||
|
, m_textItem(new QmlTextItemWrapper(this))
|
||||||
, m_filterModel(new CompletionProxyModel(this))
|
, m_filterModel(new CompletionProxyModel(this))
|
||||||
, m_emojiModel(new QConcatenateTablesProxyModel(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(&CustomEmojiModel::instance());
|
||||||
m_emojiModel->addSourceModel(&EmojiModel::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_textItem->setTextItem(textItem);
|
||||||
m_fullText = fullText;
|
}
|
||||||
Q_EMIT textChanged();
|
|
||||||
|
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
|
int CompletionModel::rowCount(const QModelIndex &parent) const
|
||||||
@@ -58,6 +78,12 @@ QVariant CompletionModel::data(const QModelIndex &index, int role) const
|
|||||||
if (role == IconNameRole) {
|
if (role == IconNameRole) {
|
||||||
return m_filterModel->data(filterIndex, UserListModel::AvatarRole);
|
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) {
|
if (m_autoCompletionType == Command) {
|
||||||
@@ -85,6 +111,12 @@ QVariant CompletionModel::data(const QModelIndex &index, int role) const
|
|||||||
if (role == IconNameRole) {
|
if (role == IconNameRole) {
|
||||||
return m_filterModel->data(filterIndex, RoomListModel::AvatarRole).toString();
|
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 (m_autoCompletionType == Emoji) {
|
||||||
if (role == DisplayNameRole) {
|
if (role == DisplayNameRole) {
|
||||||
@@ -111,44 +143,57 @@ QHash<int, QByteArray> CompletionModel::roleNames() const
|
|||||||
{SubtitleRole, "subtitle"},
|
{SubtitleRole, "subtitle"},
|
||||||
{IconNameRole, "iconName"},
|
{IconNameRole, "iconName"},
|
||||||
{ReplacedTextRole, "replacedText"},
|
{ReplacedTextRole, "replacedText"},
|
||||||
|
{HRefRole, "hRef"},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void CompletionModel::updateCompletion()
|
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->setSourceModel(m_userListModel);
|
||||||
m_filterModel->setFilterRole(UserListModel::UserIdRole);
|
m_filterModel->setFilterRole(UserListModel::UserIdRole);
|
||||||
m_filterModel->setSecondaryFilterRole(UserListModel::DisplayNameRole);
|
m_filterModel->setSecondaryFilterRole(UserListModel::DisplayNameRole);
|
||||||
m_filterModel->setFullText(m_fullText);
|
m_filterModel->setFullText(fullText);
|
||||||
m_filterModel->setFilterText(m_text);
|
m_filterModel->setFilterText(text);
|
||||||
m_autoCompletionType = User;
|
m_autoCompletionType = User;
|
||||||
m_filterModel->invalidate();
|
m_filterModel->invalidate();
|
||||||
} else if (text().startsWith(QLatin1Char('/'))) {
|
} else if (text.startsWith(QLatin1Char('/'))) {
|
||||||
m_filterModel->setSourceModel(&ActionsModel::instance());
|
m_filterModel->setSourceModel(&ActionsModel::instance());
|
||||||
m_filterModel->setFilterRole(ActionsModel::Prefix);
|
m_filterModel->setFilterRole(ActionsModel::Prefix);
|
||||||
m_filterModel->setSecondaryFilterRole(-1);
|
m_filterModel->setSecondaryFilterRole(-1);
|
||||||
m_filterModel->setFullText(m_fullText);
|
m_filterModel->setFullText(fullText);
|
||||||
m_filterModel->setFilterText(m_text.mid(1));
|
m_filterModel->setFilterText(text.mid(1));
|
||||||
m_autoCompletionType = Command;
|
m_autoCompletionType = Command;
|
||||||
m_filterModel->invalidate();
|
m_filterModel->invalidate();
|
||||||
} else if (text().startsWith(QLatin1Char('#'))) {
|
} else if (text.startsWith(QLatin1Char('#'))) {
|
||||||
m_autoCompletionType = Room;
|
m_autoCompletionType = Room;
|
||||||
m_filterModel->setSourceModel(m_roomListModel);
|
m_filterModel->setSourceModel(m_roomListModel);
|
||||||
m_filterModel->setFilterRole(RoomListModel::CanonicalAliasRole);
|
m_filterModel->setFilterRole(RoomListModel::CanonicalAliasRole);
|
||||||
m_filterModel->setSecondaryFilterRole(RoomListModel::DisplayNameRole);
|
m_filterModel->setSecondaryFilterRole(RoomListModel::DisplayNameRole);
|
||||||
m_filterModel->setFullText(m_fullText);
|
m_filterModel->setFullText(fullText);
|
||||||
m_filterModel->setFilterText(m_text);
|
m_filterModel->setFilterText(text);
|
||||||
m_filterModel->invalidate();
|
m_filterModel->invalidate();
|
||||||
} else if (text().startsWith(QLatin1Char(':')) && text().size() > 1 && !text()[1].isUpper()
|
} else if (text.startsWith(QLatin1Char(':')) && text.size() > 1 && !text[1].isUpper()
|
||||||
&& (m_fullText.indexOf(QLatin1Char(':'), 1) == -1
|
&& (fullText.indexOf(QLatin1Char(':'), 1) == -1
|
||||||
|| (m_fullText.indexOf(QLatin1Char(' ')) != -1 && m_fullText.indexOf(QLatin1Char(':'), 1) > m_fullText.indexOf(QLatin1Char(' '), 1)))) {
|
|| (fullText.indexOf(QLatin1Char(' ')) != -1 && fullText.indexOf(QLatin1Char(':'), 1) > fullText.indexOf(QLatin1Char(' '), 1)))) {
|
||||||
m_filterModel->setSourceModel(m_emojiModel);
|
m_filterModel->setSourceModel(m_emojiModel);
|
||||||
m_autoCompletionType = Emoji;
|
m_autoCompletionType = Emoji;
|
||||||
m_filterModel->setFilterRole(CustomEmojiModel::Name);
|
m_filterModel->setFilterRole(CustomEmojiModel::Name);
|
||||||
m_filterModel->setSecondaryFilterRole(EmojiModel::DescriptionRole);
|
m_filterModel->setSecondaryFilterRole(EmojiModel::DescriptionRole);
|
||||||
m_filterModel->setFullText(m_fullText);
|
m_filterModel->setFullText(fullText);
|
||||||
m_filterModel->setFilterText(m_text);
|
m_filterModel->setFilterText(text);
|
||||||
m_filterModel->invalidate();
|
m_filterModel->invalidate();
|
||||||
} else {
|
} else {
|
||||||
m_autoCompletionType = None;
|
m_autoCompletionType = None;
|
||||||
@@ -157,17 +202,6 @@ void CompletionModel::updateCompletion()
|
|||||||
endResetModel();
|
endResetModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
NeoChatRoom *CompletionModel::room() const
|
|
||||||
{
|
|
||||||
return m_room;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CompletionModel::setRoom(NeoChatRoom *room)
|
|
||||||
{
|
|
||||||
m_room = room;
|
|
||||||
Q_EMIT roomChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
CompletionModel::AutoCompletionType CompletionModel::autoCompletionType() const
|
CompletionModel::AutoCompletionType CompletionModel::autoCompletionType() const
|
||||||
{
|
{
|
||||||
return m_autoCompletionType;
|
return m_autoCompletionType;
|
||||||
|
|||||||
@@ -5,13 +5,14 @@
|
|||||||
|
|
||||||
#include <QConcatenateTablesProxyModel>
|
#include <QConcatenateTablesProxyModel>
|
||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
|
#include <QQuickItem>
|
||||||
#include <QSortFilterProxyModel>
|
#include <QSortFilterProxyModel>
|
||||||
|
|
||||||
#include "roomlistmodel.h"
|
#include "roomlistmodel.h"
|
||||||
|
|
||||||
class CompletionProxyModel;
|
class CompletionProxyModel;
|
||||||
class UserListModel;
|
class UserListModel;
|
||||||
class NeoChatRoom;
|
class QmlTextItemWrapper;
|
||||||
class RoomListModel;
|
class RoomListModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,14 +29,9 @@ class CompletionModel : public QAbstractListModel
|
|||||||
QML_ELEMENT
|
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)
|
Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The current room that the model is getting completions for.
|
|
||||||
*/
|
|
||||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The current type of completion being done on the entered text.
|
* @brief The current type of completion being done on the entered text.
|
||||||
@@ -72,14 +68,18 @@ public:
|
|||||||
*/
|
*/
|
||||||
enum Roles {
|
enum Roles {
|
||||||
DisplayNameRole = Qt::DisplayRole, /**< The main text to show. */
|
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. */
|
IconNameRole, /**< The icon to show. */
|
||||||
ReplacedTextRole, /**< The text to replace the input text with for the completion. */
|
ReplacedTextRole, /**< The text to replace the input text with for the completion. */
|
||||||
|
HRefRole, /**< The hyperlink if applicable for the completion. */
|
||||||
};
|
};
|
||||||
Q_ENUM(Roles)
|
Q_ENUM(Roles)
|
||||||
|
|
||||||
explicit CompletionModel(QObject *parent = nullptr);
|
explicit CompletionModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QQuickItem *textItem() const;
|
||||||
|
void setTextItem(QQuickItem *textItem);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get the given role value at the given index.
|
* @brief Get the given role value at the given index.
|
||||||
*
|
*
|
||||||
@@ -101,12 +101,6 @@ public:
|
|||||||
*/
|
*/
|
||||||
QHash<int, QByteArray> roleNames() const override;
|
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;
|
RoomListModel *roomListModel() const;
|
||||||
void setRoomListModel(RoomListModel *roomListModel);
|
void setRoomListModel(RoomListModel *roomListModel);
|
||||||
|
|
||||||
@@ -117,17 +111,20 @@ public:
|
|||||||
void setAutoCompletionType(AutoCompletionType autoCompletionType);
|
void setAutoCompletionType(AutoCompletionType autoCompletionType);
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void textChanged();
|
void textItemChanged();
|
||||||
|
|
||||||
void roomChanged();
|
void roomChanged();
|
||||||
void autoCompletionTypeChanged();
|
void autoCompletionTypeChanged();
|
||||||
void roomListModelChanged();
|
void roomListModelChanged();
|
||||||
void userListModelChanged();
|
void userListModelChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString m_text;
|
QPointer<QmlTextItemWrapper> m_textItem;
|
||||||
QString m_fullText;
|
|
||||||
|
int m_textStart = 0;
|
||||||
|
void updateTextStart();
|
||||||
|
|
||||||
CompletionProxyModel *m_filterModel;
|
CompletionProxyModel *m_filterModel;
|
||||||
QPointer<NeoChatRoom> m_room;
|
|
||||||
AutoCompletionType m_autoCompletionType = None;
|
AutoCompletionType m_autoCompletionType = None;
|
||||||
|
|
||||||
void updateCompletion();
|
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
|
height: implicitHeight
|
||||||
y: -height - 5
|
y: -height - 5
|
||||||
z: 10
|
z: 10
|
||||||
connection: root.Message.room.connection as NeoChatConnection
|
|
||||||
chatDocumentHandler: documentHandler
|
chatDocumentHandler: documentHandler
|
||||||
margins: 0
|
margins: 0
|
||||||
Behavior on height {
|
Behavior on height {
|
||||||
|
|||||||
Reference in New Issue
Block a user