Move ChatDocumentHandler to use QmlTextItemWrapper and create test

This commit is contained in:
James Graham
2025-12-26 16:40:15 +00:00
parent 416d85af3b
commit f31e9062e6
8 changed files with 341 additions and 233 deletions

View File

@@ -120,7 +120,8 @@ macro(add_qml_tests)
endforeach() endforeach()
endmacro() endmacro()
add_executable(qmltest qmltest.cpp) add_executable(qmltest qmltest.cpp qmltextitemwrappertestwrapper.h)
qt_add_qml_module(qmltest URI NeoChatTestUtils)
target_link_libraries(qmltest target_link_libraries(qmltest
PRIVATE PRIVATE
@@ -132,4 +133,5 @@ target_link_libraries(qmltest
add_qml_tests( add_qml_tests(
chatdocumenthelpertest.qml chatdocumenthelpertest.qml
qmltextitemwrappertest.qml
) )

View File

@@ -6,4 +6,4 @@
#include <quicktest.h> #include <quicktest.h>
QUICK_TEST_MAIN(Kirigami) QUICK_TEST_MAIN(NeoChat)

View File

@@ -0,0 +1,124 @@
// SPDX-FileCopyrightText: 2024 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick
import QtTest
import NeoChatTestUtils
TestCase {
name: "QmlTextItemWrapperTest"
TextEdit {
id: textEdit
}
TextEdit {
id: textEdit2
}
QmlTextItemWrapperTestWrapper {
id: qmlTextItemWrapper
textItem: textEdit
}
SignalSpy {
id: spyItem
target: qmlTextItemWrapper
signalName: "textItemChanged"
}
SignalSpy {
id: spyContentsChanged
target: qmlTextItemWrapper
signalName: "textDocumentContentsChanged"
}
SignalSpy {
id: spyContentsChange
target: qmlTextItemWrapper
signalName: "textDocumentContentsChange"
}
SignalSpy {
id: spyCursor
target: qmlTextItemWrapper
signalName: "textDocumentCursorPositionChanged"
}
function test_item(): void {
spyItem.clear();
compare(qmlTextItemWrapper.textItem, textEdit);
compare(spyItem.count, 0);
qmlTextItemWrapper.textItem = textEdit2;
compare(qmlTextItemWrapper.textItem, textEdit2);
compare(spyItem.count, 1);
qmlTextItemWrapper.textItem = textEdit;
compare(qmlTextItemWrapper.textItem, textEdit);
compare(spyItem.count, 2);
}
function test_document(): void {
// We can't get to the QTextDocument from QML so we have to use a helper function.
compare(qmlTextItemWrapper.compareDocuments(textEdit.textDocument), true);
}
function test_cursor(): void {
spyContentsChange.clear();
spyContentsChanged.clear();
spyCursor.clear();
// We can't get to the QTextCursor from QML so we have to use a helper function.
compare(qmlTextItemWrapper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
textEdit.insert(0, "test text")
compare(spyContentsChange.count, 1);
compare(spyContentsChange.signalArguments[0][0], 0);
compare(spyContentsChange.signalArguments[0][1], 0);
compare(spyContentsChange.signalArguments[0][2], 9);
compare(spyContentsChanged.count, 1);
compare(spyCursor.count, 1);
compare(qmlTextItemWrapper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
textEdit.selectAll();
compare(spyContentsChanged.count, 1);
compare(spyCursor.count, 1);
compare(qmlTextItemWrapper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
textEdit.clear();
compare(spyContentsChange.count, 2);
compare(spyContentsChange.signalArguments[1][0], 0);
compare(spyContentsChange.signalArguments[1][1], 9);
compare(spyContentsChange.signalArguments[1][2], 0);
compare(spyContentsChanged.count, 2);
compare(spyCursor.count, 2);
}
function test_setCursor(): void {
spyCursor.clear();
textEdit.insert(0, "test text");
compare(textEdit.cursorPosition, 9);
compare(spyCursor.count, 1);
qmlTextItemWrapper.setCursorPosition(5);
compare(textEdit.cursorPosition, 5);
compare(spyCursor.count, 2);
qmlTextItemWrapper.setCursorPosition(1);
compare(textEdit.cursorPosition, 1);
compare(spyCursor.count, 3);
textEdit.cursorVisible = false;
compare(textEdit.cursorVisible, false);
qmlTextItemWrapper.setCursorVisible(true);
compare(textEdit.cursorVisible, true);
qmlTextItemWrapper.setCursorVisible(false);
compare(textEdit.cursorVisible, false);
textEdit.clear();
compare(spyCursor.count, 4);
}
function test_forceActiveFocus(): void {
textEdit2.forceActiveFocus();
compare(textEdit.activeFocus, false);
qmlTextItemWrapper.forceActiveFocus();
compare(textEdit.activeFocus, true);
}
}

View File

@@ -0,0 +1,87 @@
// 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>
#include <QQuickTextDocument>
#include <QTextCursor>
#include "qmltextitemwrapper.h"
class QmlTextItemWrapperTestWrapper : public QObject
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The QML text Item the ChatDocumentHandler is handling.
*/
Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
public:
explicit QmlTextItemWrapperTestWrapper(QObject *parent = nullptr)
: QObject(parent)
, m_textItemWrapper(new QmlTextItemWrapper(this))
{
Q_ASSERT(m_textItemWrapper);
connect(m_textItemWrapper, &QmlTextItemWrapper::textItemChanged, this, &QmlTextItemWrapperTestWrapper::textItemChanged);
connect(m_textItemWrapper, &QmlTextItemWrapper::textDocumentContentsChange, this, &QmlTextItemWrapperTestWrapper::textDocumentContentsChange);
connect(m_textItemWrapper, &QmlTextItemWrapper::textDocumentContentsChanged, this, &QmlTextItemWrapperTestWrapper::textDocumentContentsChanged);
connect(m_textItemWrapper,
&QmlTextItemWrapper::textDocumentCursorPositionChanged,
this,
&QmlTextItemWrapperTestWrapper::textDocumentCursorPositionChanged);
}
QQuickItem *textItem() const
{
return m_textItemWrapper->textItem();
}
void setTextItem(QQuickItem *textItem)
{
m_textItemWrapper->setTextItem(textItem);
}
Q_INVOKABLE bool compareDocuments(QQuickTextDocument *document)
{
return document->textDocument() == m_textItemWrapper->document();
}
Q_INVOKABLE bool compareCursor(int cursorPosition, int selectionStart, int selectionEnd)
{
const auto cursor = m_textItemWrapper->textCursor();
if (cursor.isNull()) {
return false;
}
const auto posSame = cursor.position() == cursorPosition;
const auto startSame = cursor.selectionStart() == selectionStart;
const auto endSame = cursor.selectionEnd() == selectionEnd;
return posSame && startSame && endSame;
}
Q_INVOKABLE void setCursorPosition(int pos)
{
m_textItemWrapper->setCursorPosition(pos);
}
Q_INVOKABLE void setCursorVisible(bool visible)
{
m_textItemWrapper->setCursorVisible(visible);
}
Q_INVOKABLE void forceActiveFocus() const
{
m_textItemWrapper->forceActiveFocus();
}
Q_SIGNALS:
void textItemChanged();
void textDocumentContentsChange(int position, int charsRemoved, int charsAdded);
void textDocumentContentsChanged();
void textDocumentCursorPositionChanged();
private:
QPointer<QmlTextItemWrapper> m_textItemWrapper;
};

View File

@@ -32,6 +32,7 @@
#include "chatdocumenthandler_logging.h" #include "chatdocumenthandler_logging.h"
#include "chatmarkdownhelper.h" #include "chatmarkdownhelper.h"
#include "eventhandler.h" #include "eventhandler.h"
#include "qmltextitemwrapper.h"
using namespace Qt::StringLiterals; using namespace Qt::StringLiterals;
@@ -131,6 +132,7 @@ private:
ChatDocumentHandler::ChatDocumentHandler(QObject *parent) ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
: QObject(parent) : QObject(parent)
, m_textItem(new QmlTextItemWrapper(this))
, m_markdownHelper(new ChatMarkdownHelper(this)) , m_markdownHelper(new ChatMarkdownHelper(this))
, m_highlighter(new SyntaxHighlighter(this)) , m_highlighter(new SyntaxHighlighter(this))
{ {
@@ -168,53 +170,45 @@ void ChatDocumentHandler::setRoom(NeoChatRoom *room)
QQuickItem *ChatDocumentHandler::textItem() const QQuickItem *ChatDocumentHandler::textItem() const
{ {
return m_textItem; return m_textItem->textItem();
} }
void ChatDocumentHandler::setTextItem(QQuickItem *textItem) void ChatDocumentHandler::setTextItem(QQuickItem *textItem)
{ {
if (textItem == m_textItem) { m_textItem->setTextItem(textItem);
return; }
}
if (m_textItem) { void ChatDocumentHandler::connectTextItem()
m_textItem->disconnect(this); {
if (const auto textDoc = document()) { Q_ASSERT(m_textItem);
textDoc->disconnect(this); connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, [this]() {
m_highlighter->setDocument(m_textItem->document());
initializeChars();
});
connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, &ChatDocumentHandler::textItemChanged);
connect(m_textItem, &QmlTextItemWrapper::textDocumentContentsChanged, this, &ChatDocumentHandler::contentsChanged);
connect(m_textItem, &QmlTextItemWrapper::textDocumentContentsChanged, this, &ChatDocumentHandler::atFirstLineChanged);
connect(m_textItem, &QmlTextItemWrapper::textDocumentContentsChanged, this, &ChatDocumentHandler::atLastLineChanged);
connect(m_textItem, &QmlTextItemWrapper::textDocumentCursorPositionChanged, this, &ChatDocumentHandler::atFirstLineChanged);
connect(m_textItem, &QmlTextItemWrapper::textDocumentCursorPositionChanged, this, &ChatDocumentHandler::atLastLineChanged);
connect(m_textItem, &QmlTextItemWrapper::textDocumentContentsChange, this, [this](int position) {
auto cursor = m_textItem->textCursor();
if (cursor.isNull()) {
return;
} }
} cursor.setPosition(position);
cursor.movePosition(QTextCursor::NextWord, QTextCursor::KeepAnchor);
m_textItem = textItem; if (!cursor.selectedText().isEmpty()) {
m_highlighter->setDocument(document()); if (m_pendingFormat) {
cursor.mergeCharFormat(*m_pendingFormat);
if (m_textItem) { m_pendingFormat = std::nullopt;
connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(updateCursor())); }
if (document()) { if (m_pendingOverrideFormat) {
connect(document(), &QTextDocument::contentsChanged, this, &ChatDocumentHandler::contentsChanged); cursor.setCharFormat(*m_pendingOverrideFormat);
connect(document(), &QTextDocument::contentsChanged, this, &ChatDocumentHandler::updateCursor); m_pendingOverrideFormat = std::nullopt;
connect(document(), &QTextDocument::contentsChange, this, [this](int position) { }
auto cursor = textCursor();
if (cursor.isNull()) {
return;
}
cursor.setPosition(position);
cursor.movePosition(QTextCursor::NextWord, QTextCursor::KeepAnchor);
if (!cursor.selectedText().isEmpty()) {
if (m_pendingFormat) {
cursor.mergeCharFormat(*m_pendingFormat);
m_pendingFormat = std::nullopt;
}
if (m_pendingOverrideFormat) {
cursor.setCharFormat(*m_pendingOverrideFormat);
m_pendingOverrideFormat = std::nullopt;
}
}
});
initializeChars();
} }
} });
Q_EMIT textItemChanged();
} }
ChatDocumentHandler *ChatDocumentHandler::previousDocumentHandler() const ChatDocumentHandler *ChatDocumentHandler::previousDocumentHandler() const
@@ -279,7 +273,7 @@ void ChatDocumentHandler::setInitialText(const QString &text)
void ChatDocumentHandler::initializeChars() void ChatDocumentHandler::initializeChars()
{ {
const auto doc = document(); const auto doc = m_textItem->document();
if (!doc) { if (!doc) {
return; return;
} }
@@ -304,61 +298,6 @@ void ChatDocumentHandler::initializeChars()
} }
} }
QTextDocument *ChatDocumentHandler::document() const
{
if (!m_textItem) {
return nullptr;
}
const auto quickDocument = qvariant_cast<QQuickTextDocument *>(m_textItem->property("textDocument"));
return quickDocument ? quickDocument->textDocument() : nullptr;
}
int ChatDocumentHandler::cursorPosition() const
{
if (!m_textItem) {
return -1;
}
return m_textItem->property("cursorPosition").toInt();
}
void ChatDocumentHandler::updateCursor()
{
Q_EMIT atFirstLineChanged();
Q_EMIT atLastLineChanged();
}
int ChatDocumentHandler::selectionStart() const
{
if (!m_textItem) {
return -1;
}
return m_textItem->property("selectionStart").toInt();
}
int ChatDocumentHandler::selectionEnd() const
{
if (!m_textItem) {
return -1;
}
return m_textItem->property("selectionEnd").toInt();
}
QTextCursor ChatDocumentHandler::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;
}
bool ChatDocumentHandler::isEmpty() const bool ChatDocumentHandler::isEmpty() const
{ {
return htmlText().length() == 0; return htmlText().length() == 0;
@@ -366,7 +305,7 @@ bool ChatDocumentHandler::isEmpty() const
bool ChatDocumentHandler::atFirstLine() const bool ChatDocumentHandler::atFirstLine() const
{ {
const auto cursor = textCursor(); const auto cursor = m_textItem->textCursor();
if (cursor.isNull()) { if (cursor.isNull()) {
return false; return false;
} }
@@ -375,8 +314,8 @@ bool ChatDocumentHandler::atFirstLine() const
bool ChatDocumentHandler::atLastLine() const bool ChatDocumentHandler::atLastLine() const
{ {
const auto cursor = textCursor(); const auto cursor = m_textItem->textCursor();
const auto doc = document(); const auto doc = m_textItem->document();
if (cursor.isNull() || !doc) { if (cursor.isNull() || !doc) {
return false; return false;
} }
@@ -386,31 +325,30 @@ bool ChatDocumentHandler::atLastLine() const
void ChatDocumentHandler::setCursorFromDocumentHandler(ChatDocumentHandler *previousDocumentHandler, bool infront, int defaultPosition) void ChatDocumentHandler::setCursorFromDocumentHandler(ChatDocumentHandler *previousDocumentHandler, bool infront, int defaultPosition)
{ {
const auto doc = document(); const auto doc = m_textItem->document();
const auto item = textItem(); if (!doc) {
if (!doc || !item) {
return; return;
} }
item->forceActiveFocus(); m_textItem->forceActiveFocus();
if (!previousDocumentHandler) { if (!previousDocumentHandler) {
const auto docLastBlockLayout = doc->lastBlock().layout(); const auto docLastBlockLayout = doc->lastBlock().layout();
item->setProperty("cursorPosition", infront ? defaultPosition : docLastBlockLayout->lineAt(docLastBlockLayout->lineCount() - 1).textStart()); m_textItem->setCursorPosition(infront ? defaultPosition : docLastBlockLayout->lineAt(docLastBlockLayout->lineCount() - 1).textStart());
item->setProperty("cursorVisible", true); m_textItem->setCursorVisible(true);
return; return;
} }
const auto previousLinePosition = previousDocumentHandler->cursorPositionInLine(); const auto previousLinePosition = previousDocumentHandler->cursorPositionInLine();
const auto newMaxLineLength = lineLength(infront ? 0 : lineCount() - 1); const auto newMaxLineLength = lineLength(infront ? 0 : lineCount() - 1);
item->setProperty("cursorPosition", m_textItem->setCursorPosition(std::min(previousLinePosition, newMaxLineLength ? *newMaxLineLength : defaultPosition)
std::min(previousLinePosition, newMaxLineLength ? *newMaxLineLength : defaultPosition) + (infront ? 0 : doc->lastBlock().position())); + (infront ? 0 : doc->lastBlock().position()));
item->setProperty("cursorVisible", true); m_textItem->setCursorVisible(true);
} }
int ChatDocumentHandler::lineCount() const int ChatDocumentHandler::lineCount() const
{ {
if (const auto doc = document()) { if (const auto doc = m_textItem->document()) {
return doc->lineCount(); return doc->lineCount();
} }
return 0; return 0;
@@ -418,7 +356,7 @@ int ChatDocumentHandler::lineCount() const
std::optional<int> ChatDocumentHandler::lineLength(int lineNumber) const std::optional<int> ChatDocumentHandler::lineLength(int lineNumber) const
{ {
const auto doc = document(); const auto doc = m_textItem->document();
if (!doc || lineNumber < 0 || lineNumber >= doc->lineCount()) { if (!doc || lineNumber < 0 || lineNumber >= doc->lineCount()) {
return std::nullopt; return std::nullopt;
} }
@@ -429,7 +367,7 @@ std::optional<int> ChatDocumentHandler::lineLength(int lineNumber) const
int ChatDocumentHandler::cursorPositionInLine() const int ChatDocumentHandler::cursorPositionInLine() const
{ {
const auto cursor = textCursor(); const auto cursor = m_textItem->textCursor();
if (cursor.isNull()) { if (cursor.isNull()) {
return false; return false;
} }
@@ -438,7 +376,7 @@ int ChatDocumentHandler::cursorPositionInLine() const
QTextDocumentFragment ChatDocumentHandler::takeFirstBlock() QTextDocumentFragment ChatDocumentHandler::takeFirstBlock()
{ {
auto cursor = textCursor(); auto cursor = m_textItem->textCursor();
if (cursor.isNull()) { if (cursor.isNull()) {
return {}; return {};
} }
@@ -446,14 +384,14 @@ QTextDocumentFragment ChatDocumentHandler::takeFirstBlock()
cursor.movePosition(QTextCursor::Start); cursor.movePosition(QTextCursor::Start);
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, m_fixedStartChars.length()); cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, m_fixedStartChars.length());
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
if (document()->blockCount() <= 1) { if (m_textItem->document()->blockCount() <= 1) {
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, m_fixedEndChars.length()); cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, m_fixedEndChars.length());
} }
const auto block = cursor.selection(); const auto block = cursor.selection();
cursor.removeSelectedText(); cursor.removeSelectedText();
cursor.endEditBlock(); cursor.endEditBlock();
if (document()->characterCount() - 1 <= (m_fixedStartChars.length() + m_fixedEndChars.length())) { if (m_textItem->document()->characterCount() - 1 <= (m_fixedStartChars.length() + m_fixedEndChars.length())) {
Q_EMIT removeMe(this); Q_EMIT removeMe(this);
} }
return block; return block;
@@ -461,7 +399,7 @@ QTextDocumentFragment ChatDocumentHandler::takeFirstBlock()
void ChatDocumentHandler::fillFragments(bool &hasBefore, QTextDocumentFragment &midFragment, std::optional<QTextDocumentFragment> &afterFragment) void ChatDocumentHandler::fillFragments(bool &hasBefore, QTextDocumentFragment &midFragment, std::optional<QTextDocumentFragment> &afterFragment)
{ {
auto cursor = textCursor(); auto cursor = m_textItem->textCursor();
if (cursor.isNull()) { if (cursor.isNull()) {
return; return;
} }
@@ -469,7 +407,7 @@ void ChatDocumentHandler::fillFragments(bool &hasBefore, QTextDocumentFragment &
if (cursor.blockNumber() > 0) { if (cursor.blockNumber() > 0) {
hasBefore = true; hasBefore = true;
} }
auto afterBlock = cursor.blockNumber() < document()->blockCount() - 1; auto afterBlock = cursor.blockNumber() < m_textItem->document()->blockCount() - 1;
cursor.beginEditBlock(); cursor.beginEditBlock();
cursor.movePosition(QTextCursor::StartOfBlock); cursor.movePosition(QTextCursor::StartOfBlock);
@@ -496,7 +434,7 @@ void ChatDocumentHandler::fillFragments(bool &hasBefore, QTextDocumentFragment &
void ChatDocumentHandler::insertFragment(const QTextDocumentFragment fragment, InsertPosition position, bool keepPosition) void ChatDocumentHandler::insertFragment(const QTextDocumentFragment fragment, InsertPosition position, bool keepPosition)
{ {
auto cursor = textCursor(); auto cursor = m_textItem->textCursor();
if (cursor.isNull()) { if (cursor.isNull()) {
return; return;
} }
@@ -507,7 +445,7 @@ void ChatDocumentHandler::insertFragment(const QTextDocumentFragment fragment, I
currentPosition = 0; currentPosition = 0;
break; break;
case End: case End:
currentPosition = document()->characterCount() - 1; currentPosition = m_textItem->document()->characterCount() - 1;
break; break;
case Cursor: case Cursor:
currentPosition = cursor.position(); currentPosition = cursor.position();
@@ -536,18 +474,16 @@ void ChatDocumentHandler::insertFragment(const QTextDocumentFragment fragment, I
if (keepPosition) { if (keepPosition) {
cursor.setPosition(currentPosition); cursor.setPosition(currentPosition);
} }
if (textItem()) { m_textItem->setCursorPosition(cursor.position());
textItem()->setProperty("cursorPosition", cursor.position());
}
} }
QString ChatDocumentHandler::getText() const QString ChatDocumentHandler::getText() const
{ {
if (!document()) { if (!m_textItem->document()) {
qCWarning(ChatDocumentHandling) << "getText called with no QQuickTextDocument available."; qCWarning(ChatDocumentHandling) << "getText called with no QQuickTextDocument available.";
return {}; return {};
} }
return document()->toPlainText(); return m_textItem->document()->toPlainText();
} }
void ChatDocumentHandler::pushMention(const Mention mention) const void ChatDocumentHandler::pushMention(const Mention mention) const
@@ -584,7 +520,7 @@ void ChatDocumentHandler::updateMentions(const QString &editId)
const int end = position + name.length(); const int end = position + name.length();
linkSize += match.capturedLength(0) - name.length(); linkSize += match.capturedLength(0) - name.length();
QTextCursor cursor(document()); QTextCursor cursor(m_textItem->document());
cursor.setPosition(position); cursor.setPosition(position);
cursor.setPosition(end, QTextCursor::KeepAnchor); cursor.setPosition(end, QTextCursor::KeepAnchor);
cursor.setKeepPositionOnInsert(true); cursor.setKeepPositionOnInsert(true);
@@ -606,40 +542,40 @@ void ChatDocumentHandler::setTextColor(const QColor &color)
bool ChatDocumentHandler::bold() const bool ChatDocumentHandler::bold() const
{ {
QTextCursor cursor = textCursor(); QTextCursor cursor = m_textItem->textCursor();
if (cursor.isNull()) { if (cursor.isNull()) {
return false; return false;
} }
return textCursor().charFormat().fontWeight() == QFont::Bold; return cursor.charFormat().fontWeight() == QFont::Bold;
} }
bool ChatDocumentHandler::italic() const bool ChatDocumentHandler::italic() const
{ {
QTextCursor cursor = textCursor(); QTextCursor cursor = m_textItem->textCursor();
if (cursor.isNull()) if (cursor.isNull())
return false; return false;
return textCursor().charFormat().fontItalic(); return cursor.charFormat().fontItalic();
} }
bool ChatDocumentHandler::underline() const bool ChatDocumentHandler::underline() const
{ {
QTextCursor cursor = textCursor(); QTextCursor cursor = m_textItem->textCursor();
if (cursor.isNull()) if (cursor.isNull())
return false; return false;
return textCursor().charFormat().fontUnderline(); return cursor.charFormat().fontUnderline();
} }
bool ChatDocumentHandler::strikethrough() const bool ChatDocumentHandler::strikethrough() const
{ {
QTextCursor cursor = textCursor(); QTextCursor cursor = m_textItem->textCursor();
if (cursor.isNull()) if (cursor.isNull())
return false; return false;
return textCursor().charFormat().fontStrikeOut(); return cursor.charFormat().fontStrikeOut();
} }
QColor ChatDocumentHandler::textColor() const QColor ChatDocumentHandler::textColor() const
{ {
QTextCursor cursor = textCursor(); QTextCursor cursor = m_textItem->textCursor();
if (cursor.isNull()) if (cursor.isNull())
return QColor(Qt::black); return QColor(Qt::black);
QTextCharFormat format = cursor.charFormat(); QTextCharFormat format = cursor.charFormat();
@@ -657,7 +593,7 @@ std::optional<Qt::TextFormat> ChatDocumentHandler::textFormat() const
void ChatDocumentHandler::mergeFormatOnWordOrSelection(const QTextCharFormat &format) void ChatDocumentHandler::mergeFormatOnWordOrSelection(const QTextCharFormat &format)
{ {
QTextCursor cursor = textCursor(); QTextCursor cursor = m_textItem->textCursor();
if (!cursor.hasSelection()) { if (!cursor.hasSelection()) {
cursor.select(QTextCursor::WordUnderCursor); cursor.select(QTextCursor::WordUnderCursor);
} }
@@ -670,7 +606,7 @@ void ChatDocumentHandler::mergeFormatOnWordOrSelection(const QTextCharFormat &fo
QString ChatDocumentHandler::currentLinkText() const QString ChatDocumentHandler::currentLinkText() const
{ {
QTextCursor cursor = textCursor(); QTextCursor cursor = m_textItem->textCursor();
selectLinkText(&cursor); selectLinkText(&cursor);
return cursor.selectedText(); return cursor.selectedText();
} }
@@ -717,70 +653,9 @@ void ChatDocumentHandler::selectLinkText(QTextCursor *cursor) const
} }
} }
void ChatDocumentHandler::insertImage(const QUrl &url)
{
if (!url.isLocalFile()) {
return;
}
QImage image;
if (!image.load(url.path())) {
return;
}
// Ensure we are putting the image in a new line and not in a list has it
// breaks the Qt rendering
textCursor().insertHtml(QStringLiteral("<br />"));
while (canDedentList()) {
m_nestedListHelper.handleOnIndentLess(textCursor());
}
textCursor().insertHtml(QStringLiteral("<img width=\"500\" src=\"") + url.path() + QStringLiteral("\"\\>"));
}
void ChatDocumentHandler::insertTable(int rows, int columns)
{
QString htmlText;
QTextCursor cursor = textCursor();
QTextTableFormat tableFormat;
tableFormat.setBorder(1);
const int numberOfColumns(columns);
QList<QTextLength> constrains;
constrains.reserve(numberOfColumns);
const QTextLength::Type type = QTextLength::PercentageLength;
const int length = 100; // 100% of window width
const QTextLength textlength(type, length / numberOfColumns);
for (int i = 0; i < numberOfColumns; ++i) {
constrains.append(textlength);
}
tableFormat.setColumnWidthConstraints(constrains);
tableFormat.setAlignment(Qt::AlignLeft);
tableFormat.setCellSpacing(0);
tableFormat.setCellPadding(4);
tableFormat.setBorderCollapse(true);
tableFormat.setBorder(0.5);
tableFormat.setTopMargin(20);
Q_ASSERT(cursor.document());
QTextTable *table = cursor.insertTable(rows, numberOfColumns, tableFormat);
// fill table with whitespace
for (int i = 0, rows = table->rows(); i < rows; i++) {
for (int j = 0, columns = table->columns(); j < columns; j++) {
auto cell = table->cellAt(i, j);
Q_ASSERT(cell.isValid());
cell.firstCursorPosition().insertText(QStringLiteral(" "));
}
}
return;
}
void ChatDocumentHandler::insertCompletion(const QString &text, const QUrl &link) void ChatDocumentHandler::insertCompletion(const QString &text, const QUrl &link)
{ {
QTextCursor cursor = textCursor(); QTextCursor cursor = m_textItem->textCursor();
if (cursor.isNull()) { if (cursor.isNull()) {
return; return;
} }
@@ -813,7 +688,7 @@ void ChatDocumentHandler::insertCompletion(const QString &text, const QUrl &link
void ChatDocumentHandler::updateLink(const QString &linkUrl, const QString &linkText) void ChatDocumentHandler::updateLink(const QString &linkUrl, const QString &linkText)
{ {
auto cursor = textCursor(); auto cursor = m_textItem->textCursor();
selectLinkText(&cursor); selectLinkText(&cursor);
cursor.beginEditBlock(); cursor.beginEditBlock();
@@ -898,39 +773,39 @@ void ChatDocumentHandler::setFormat(RichFormat::Format format)
void ChatDocumentHandler::indentListMore() void ChatDocumentHandler::indentListMore()
{ {
m_nestedListHelper.handleOnIndentMore(textCursor()); m_nestedListHelper.handleOnIndentMore(m_textItem->textCursor());
Q_EMIT currentListStyleChanged(); Q_EMIT currentListStyleChanged();
} }
void ChatDocumentHandler::indentListLess() void ChatDocumentHandler::indentListLess()
{ {
m_nestedListHelper.handleOnIndentLess(textCursor()); m_nestedListHelper.handleOnIndentLess(m_textItem->textCursor());
Q_EMIT currentListStyleChanged(); Q_EMIT currentListStyleChanged();
} }
void ChatDocumentHandler::setListFormat(RichFormat::Format format) void ChatDocumentHandler::setListFormat(RichFormat::Format format)
{ {
m_nestedListHelper.handleOnBulletType(RichFormat::listStyleForFormat(format), textCursor()); m_nestedListHelper.handleOnBulletType(RichFormat::listStyleForFormat(format), m_textItem->textCursor());
Q_EMIT currentListStyleChanged(); Q_EMIT currentListStyleChanged();
} }
bool ChatDocumentHandler::canIndentList() const bool ChatDocumentHandler::canIndentList() const
{ {
return m_nestedListHelper.canIndent(textCursor()) && textCursor().blockFormat().headingLevel() == 0; return m_nestedListHelper.canIndent(m_textItem->textCursor()) && m_textItem->textCursor().blockFormat().headingLevel() == 0;
} }
bool ChatDocumentHandler::canDedentList() const bool ChatDocumentHandler::canDedentList() const
{ {
return m_nestedListHelper.canDedent(textCursor()) && textCursor().blockFormat().headingLevel() == 0; return m_nestedListHelper.canDedent(m_textItem->textCursor()) && m_textItem->textCursor().blockFormat().headingLevel() == 0;
} }
int ChatDocumentHandler::currentListStyle() const int ChatDocumentHandler::currentListStyle() const
{ {
if (!textCursor().currentList()) { if (!m_textItem->textCursor().currentList()) {
return 0; return 0;
} }
return -textCursor().currentList()->format().style(); return -m_textItem->textCursor().currentList()->format().style();
} }
void ChatDocumentHandler::setTextFormat(RichFormat::Format format) void ChatDocumentHandler::setTextFormat(RichFormat::Format format)
@@ -938,13 +813,13 @@ void ChatDocumentHandler::setTextFormat(RichFormat::Format format)
if (RichFormat::typeForFormat(format) != RichFormat::Text) { if (RichFormat::typeForFormat(format) != RichFormat::Text) {
return; return;
} }
mergeFormatOnWordOrSelection(RichFormat::charFormatForFormat(format, RichFormat::hasFormat(textCursor(), format))); mergeFormatOnWordOrSelection(RichFormat::charFormatForFormat(format, RichFormat::hasFormat(m_textItem->textCursor(), format)));
Q_EMIT formatChanged(); Q_EMIT formatChanged();
} }
RichFormat::Format ChatDocumentHandler::style() const RichFormat::Format ChatDocumentHandler::style() const
{ {
return static_cast<RichFormat::Format>(textCursor().blockFormat().headingLevel()); return static_cast<RichFormat::Format>(m_textItem->textCursor().blockFormat().headingLevel());
} }
void ChatDocumentHandler::setStyleFormat(RichFormat::Format format) void ChatDocumentHandler::setStyleFormat(RichFormat::Format format)
@@ -955,9 +830,7 @@ void ChatDocumentHandler::setStyleFormat(RichFormat::Format format)
return; return;
} }
qWarning() << format; QTextCursor cursor = m_textItem->textCursor();
QTextCursor cursor = textCursor();
if (cursor.isNull()) { if (cursor.isNull()) {
return; return;
} }
@@ -993,7 +866,7 @@ void ChatDocumentHandler::setStyleFormat(RichFormat::Format format)
void ChatDocumentHandler::tab() void ChatDocumentHandler::tab()
{ {
QTextCursor cursor = textCursor(); QTextCursor cursor = m_textItem->textCursor();
if (cursor.isNull()) { if (cursor.isNull()) {
return; return;
} }
@@ -1006,11 +879,11 @@ void ChatDocumentHandler::tab()
void ChatDocumentHandler::deleteChar() void ChatDocumentHandler::deleteChar()
{ {
QTextCursor cursor = textCursor(); QTextCursor cursor = m_textItem->textCursor();
if (cursor.isNull()) { if (cursor.isNull()) {
return; return;
} }
if (cursor.position() >= document()->characterCount() - m_fixedEndChars.length() - 1) { if (cursor.position() >= m_textItem->document()->characterCount() - m_fixedEndChars.length() - 1) {
if (const auto nextHandler = nextDocumentHandler()) { if (const auto nextHandler = nextDocumentHandler()) {
insertFragment(nextHandler->takeFirstBlock(), Cursor, true); insertFragment(nextHandler->takeFirstBlock(), Cursor, true);
} }
@@ -1021,7 +894,7 @@ void ChatDocumentHandler::deleteChar()
void ChatDocumentHandler::backspace() void ChatDocumentHandler::backspace()
{ {
QTextCursor cursor = textCursor(); QTextCursor cursor = m_textItem->textCursor();
if (cursor.isNull()) { if (cursor.isNull()) {
return; return;
} }
@@ -1043,7 +916,7 @@ void ChatDocumentHandler::backspace()
void ChatDocumentHandler::insertReturn() void ChatDocumentHandler::insertReturn()
{ {
QTextCursor cursor = textCursor(); QTextCursor cursor = m_textItem->textCursor();
if (cursor.isNull()) { if (cursor.isNull()) {
return; return;
} }
@@ -1052,12 +925,12 @@ void ChatDocumentHandler::insertReturn()
void ChatDocumentHandler::insertText(const QString &text) void ChatDocumentHandler::insertText(const QString &text)
{ {
textCursor().insertText(text); m_textItem->textCursor().insertText(text);
} }
QString ChatDocumentHandler::currentLinkUrl() const QString ChatDocumentHandler::currentLinkUrl() const
{ {
return textCursor().charFormat().anchorHref(); return m_textItem->textCursor().charFormat().anchorHref();
} }
void ChatDocumentHandler::dumpHtml() void ChatDocumentHandler::dumpHtml()
@@ -1067,7 +940,7 @@ void ChatDocumentHandler::dumpHtml()
QString ChatDocumentHandler::htmlText() const QString ChatDocumentHandler::htmlText() const
{ {
const auto doc = document(); const auto doc = m_textItem->document();
if (!doc) { if (!doc) {
return {}; return {};
} }

View File

@@ -19,6 +19,7 @@
class QTextDocument; class QTextDocument;
class QmlTextItemWrapper;
class NeoChatRoom; class NeoChatRoom;
class SyntaxHighlighter; class SyntaxHighlighter;
@@ -179,8 +180,6 @@ public:
Q_INVOKABLE QString currentLinkUrl() const; Q_INVOKABLE QString currentLinkUrl() const;
Q_INVOKABLE QString currentLinkText() const; Q_INVOKABLE QString currentLinkText() const;
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 insertTable(int rows, int columns);
Q_INVOKABLE void insertCompletion(const QString &text, const QUrl &link); Q_INVOKABLE void insertCompletion(const QString &text, const QUrl &link);
Q_INVOKABLE void dumpHtml(); Q_INVOKABLE void dumpHtml();
@@ -210,8 +209,8 @@ Q_SIGNALS:
private: private:
ChatBarType::Type m_type = ChatBarType::None; ChatBarType::Type m_type = ChatBarType::None;
QPointer<NeoChatRoom> m_room; QPointer<NeoChatRoom> m_room;
QPointer<QQuickItem> m_textItem; QPointer<QmlTextItemWrapper> m_textItem;
QTextDocument *document() const; void connectTextItem();
QPointer<ChatDocumentHandler> m_previousDocumentHandler; QPointer<ChatDocumentHandler> m_previousDocumentHandler;
QPointer<ChatDocumentHandler> m_nextDocumentHandler; QPointer<ChatDocumentHandler> m_nextDocumentHandler;
@@ -221,11 +220,6 @@ private:
QString m_initialText = {}; QString m_initialText = {};
void initializeChars(); void initializeChars();
int cursorPosition() const;
int selectionStart() const;
int selectionEnd() const;
QTextCursor textCursor() const;
void setTextFormat(RichFormat::Format format); void setTextFormat(RichFormat::Format format);
void setStyleFormat(RichFormat::Format format); void setStyleFormat(RichFormat::Format format);
void setListFormat(RichFormat::Format format); void setListFormat(RichFormat::Format format);
@@ -248,7 +242,4 @@ private:
void regenerateColorScheme(); void regenerateColorScheme();
QString trim(QString string) const; QString trim(QString string) const;
private Q_SLOTS:
void updateCursor();
}; };

View File

@@ -35,6 +35,7 @@ void QmlTextItemWrapper::setTextItem(QQuickItem *textItem)
connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(textDocCursorChanged())); connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(textDocCursorChanged()));
if (document()) { if (document()) {
connect(document(), &QTextDocument::contentsChanged, this, &QmlTextItemWrapper::textDocumentContentsChanged); connect(document(), &QTextDocument::contentsChanged, this, &QmlTextItemWrapper::textDocumentContentsChanged);
connect(document(), &QTextDocument::contentsChange, this, &QmlTextItemWrapper::textDocumentContentsChange);
} }
} }
@@ -90,9 +91,33 @@ QTextCursor QmlTextItemWrapper::textCursor() const
return cursor; return cursor;
} }
void QmlTextItemWrapper::setCursorPosition(int pos)
{
if (!m_textItem) {
return;
}
m_textItem->setProperty("cursorPosition", pos);
}
void QmlTextItemWrapper::setCursorVisible(bool visible)
{
if (!m_textItem) {
return;
}
m_textItem->setProperty("cursorVisible", visible);
}
void QmlTextItemWrapper::textDocCursorChanged() void QmlTextItemWrapper::textDocCursorChanged()
{ {
Q_EMIT textDocumentCursorPositionChanged(); Q_EMIT textDocumentCursorPositionChanged();
} }
void QmlTextItemWrapper::forceActiveFocus() const
{
if (!m_textItem) {
return;
}
m_textItem->forceActiveFocus();
}
#include "moc_qmltextitemwrapper.cpp" #include "moc_qmltextitemwrapper.cpp"

View File

@@ -13,7 +13,7 @@ class QTextDocument;
* *
* A class to wrap around a QQuickItem that is a QML TextEdit (or inherited from it) and provide easy acess to its properties. * 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 * @note This basically exists because Qt does not give us access to the cpp headers of
* most QML items. * most QML items.
* *
* @sa QQuickItem, TextEdit * @sa QQuickItem, TextEdit
@@ -31,10 +31,16 @@ public:
QTextDocument *document() const; QTextDocument *document() const;
QTextCursor textCursor() const; QTextCursor textCursor() const;
void setCursorPosition(int pos);
void setCursorVisible(bool visible);
void forceActiveFocus() const;
Q_SIGNALS: Q_SIGNALS:
void textItemChanged(); void textItemChanged();
void textDocumentContentsChange(int position, int charsRemoved, int charsAdded);
void textDocumentContentsChanged(); void textDocumentContentsChanged();
void textDocumentCursorPositionChanged(); void textDocumentCursorPositionChanged();