From f31e9062e657d99fd94b5bae211b2a12bf2f7837 Mon Sep 17 00:00:00 2001 From: James Graham Date: Fri, 26 Dec 2025 16:40:15 +0000 Subject: [PATCH] Move ChatDocumentHandler to use QmlTextItemWrapper and create test --- autotests/CMakeLists.txt | 4 +- autotests/qmltest.cpp | 2 +- autotests/qmltextitemwrappertest.qml | 124 +++++++++ autotests/qmltextitemwrappertestwrapper.h | 87 ++++++ src/libneochat/chatdocumenthandler.cpp | 309 +++++++--------------- src/libneochat/chatdocumenthandler.h | 15 +- src/libneochat/qmltextitemwrapper.cpp | 25 ++ src/libneochat/qmltextitemwrapper.h | 8 +- 8 files changed, 341 insertions(+), 233 deletions(-) create mode 100644 autotests/qmltextitemwrappertest.qml create mode 100644 autotests/qmltextitemwrappertestwrapper.h diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index d1067db72..1e20ad284 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -120,7 +120,8 @@ macro(add_qml_tests) endforeach() endmacro() -add_executable(qmltest qmltest.cpp) +add_executable(qmltest qmltest.cpp qmltextitemwrappertestwrapper.h) +qt_add_qml_module(qmltest URI NeoChatTestUtils) target_link_libraries(qmltest PRIVATE @@ -132,4 +133,5 @@ target_link_libraries(qmltest add_qml_tests( chatdocumenthelpertest.qml + qmltextitemwrappertest.qml ) diff --git a/autotests/qmltest.cpp b/autotests/qmltest.cpp index c07a73ec4..af01e6751 100644 --- a/autotests/qmltest.cpp +++ b/autotests/qmltest.cpp @@ -6,4 +6,4 @@ #include -QUICK_TEST_MAIN(Kirigami) +QUICK_TEST_MAIN(NeoChat) diff --git a/autotests/qmltextitemwrappertest.qml b/autotests/qmltextitemwrappertest.qml new file mode 100644 index 000000000..09ad7bd9e --- /dev/null +++ b/autotests/qmltextitemwrappertest.qml @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: 2024 Carl Schwan +// 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); + } +} diff --git a/autotests/qmltextitemwrappertestwrapper.h b/autotests/qmltextitemwrappertestwrapper.h new file mode 100644 index 000000000..cb7170bbf --- /dev/null +++ b/autotests/qmltextitemwrappertestwrapper.h @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2025 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#pragma once + +#include +#include +#include +#include + +#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 m_textItemWrapper; +}; diff --git a/src/libneochat/chatdocumenthandler.cpp b/src/libneochat/chatdocumenthandler.cpp index 2a659b193..b5656be12 100644 --- a/src/libneochat/chatdocumenthandler.cpp +++ b/src/libneochat/chatdocumenthandler.cpp @@ -32,6 +32,7 @@ #include "chatdocumenthandler_logging.h" #include "chatmarkdownhelper.h" #include "eventhandler.h" +#include "qmltextitemwrapper.h" using namespace Qt::StringLiterals; @@ -131,6 +132,7 @@ private: ChatDocumentHandler::ChatDocumentHandler(QObject *parent) : QObject(parent) + , m_textItem(new QmlTextItemWrapper(this)) , m_markdownHelper(new ChatMarkdownHelper(this)) , m_highlighter(new SyntaxHighlighter(this)) { @@ -168,53 +170,45 @@ void ChatDocumentHandler::setRoom(NeoChatRoom *room) QQuickItem *ChatDocumentHandler::textItem() const { - return m_textItem; + return m_textItem->textItem(); } void ChatDocumentHandler::setTextItem(QQuickItem *textItem) { - if (textItem == m_textItem) { - return; - } + m_textItem->setTextItem(textItem); +} - if (m_textItem) { - m_textItem->disconnect(this); - if (const auto textDoc = document()) { - textDoc->disconnect(this); +void ChatDocumentHandler::connectTextItem() +{ + Q_ASSERT(m_textItem); + 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; } - } - - m_textItem = textItem; - m_highlighter->setDocument(document()); - - if (m_textItem) { - connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(updateCursor())); - if (document()) { - connect(document(), &QTextDocument::contentsChanged, this, &ChatDocumentHandler::contentsChanged); - connect(document(), &QTextDocument::contentsChanged, this, &ChatDocumentHandler::updateCursor); - 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(); + 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; + } } - } - - Q_EMIT textItemChanged(); + }); } ChatDocumentHandler *ChatDocumentHandler::previousDocumentHandler() const @@ -279,7 +273,7 @@ void ChatDocumentHandler::setInitialText(const QString &text) void ChatDocumentHandler::initializeChars() { - const auto doc = document(); + const auto doc = m_textItem->document(); if (!doc) { return; } @@ -304,61 +298,6 @@ void ChatDocumentHandler::initializeChars() } } -QTextDocument *ChatDocumentHandler::document() const -{ - if (!m_textItem) { - return nullptr; - } - const auto quickDocument = qvariant_cast(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 { return htmlText().length() == 0; @@ -366,7 +305,7 @@ bool ChatDocumentHandler::isEmpty() const bool ChatDocumentHandler::atFirstLine() const { - const auto cursor = textCursor(); + const auto cursor = m_textItem->textCursor(); if (cursor.isNull()) { return false; } @@ -375,8 +314,8 @@ bool ChatDocumentHandler::atFirstLine() const bool ChatDocumentHandler::atLastLine() const { - const auto cursor = textCursor(); - const auto doc = document(); + const auto cursor = m_textItem->textCursor(); + const auto doc = m_textItem->document(); if (cursor.isNull() || !doc) { return false; } @@ -386,31 +325,30 @@ bool ChatDocumentHandler::atLastLine() const void ChatDocumentHandler::setCursorFromDocumentHandler(ChatDocumentHandler *previousDocumentHandler, bool infront, int defaultPosition) { - const auto doc = document(); - const auto item = textItem(); - if (!doc || !item) { + const auto doc = m_textItem->document(); + if (!doc) { return; } - item->forceActiveFocus(); + m_textItem->forceActiveFocus(); if (!previousDocumentHandler) { const auto docLastBlockLayout = doc->lastBlock().layout(); - item->setProperty("cursorPosition", infront ? defaultPosition : docLastBlockLayout->lineAt(docLastBlockLayout->lineCount() - 1).textStart()); - item->setProperty("cursorVisible", true); + m_textItem->setCursorPosition(infront ? defaultPosition : docLastBlockLayout->lineAt(docLastBlockLayout->lineCount() - 1).textStart()); + m_textItem->setCursorVisible(true); return; } const auto previousLinePosition = previousDocumentHandler->cursorPositionInLine(); const auto newMaxLineLength = lineLength(infront ? 0 : lineCount() - 1); - item->setProperty("cursorPosition", - std::min(previousLinePosition, newMaxLineLength ? *newMaxLineLength : defaultPosition) + (infront ? 0 : doc->lastBlock().position())); - item->setProperty("cursorVisible", true); + m_textItem->setCursorPosition(std::min(previousLinePosition, newMaxLineLength ? *newMaxLineLength : defaultPosition) + + (infront ? 0 : doc->lastBlock().position())); + m_textItem->setCursorVisible(true); } int ChatDocumentHandler::lineCount() const { - if (const auto doc = document()) { + if (const auto doc = m_textItem->document()) { return doc->lineCount(); } return 0; @@ -418,7 +356,7 @@ int ChatDocumentHandler::lineCount() const std::optional ChatDocumentHandler::lineLength(int lineNumber) const { - const auto doc = document(); + const auto doc = m_textItem->document(); if (!doc || lineNumber < 0 || lineNumber >= doc->lineCount()) { return std::nullopt; } @@ -429,7 +367,7 @@ std::optional ChatDocumentHandler::lineLength(int lineNumber) const int ChatDocumentHandler::cursorPositionInLine() const { - const auto cursor = textCursor(); + const auto cursor = m_textItem->textCursor(); if (cursor.isNull()) { return false; } @@ -438,7 +376,7 @@ int ChatDocumentHandler::cursorPositionInLine() const QTextDocumentFragment ChatDocumentHandler::takeFirstBlock() { - auto cursor = textCursor(); + auto cursor = m_textItem->textCursor(); if (cursor.isNull()) { return {}; } @@ -446,14 +384,14 @@ QTextDocumentFragment ChatDocumentHandler::takeFirstBlock() cursor.movePosition(QTextCursor::Start); cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, m_fixedStartChars.length()); 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()); } const auto block = cursor.selection(); cursor.removeSelectedText(); 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); } return block; @@ -461,7 +399,7 @@ QTextDocumentFragment ChatDocumentHandler::takeFirstBlock() void ChatDocumentHandler::fillFragments(bool &hasBefore, QTextDocumentFragment &midFragment, std::optional &afterFragment) { - auto cursor = textCursor(); + auto cursor = m_textItem->textCursor(); if (cursor.isNull()) { return; } @@ -469,7 +407,7 @@ void ChatDocumentHandler::fillFragments(bool &hasBefore, QTextDocumentFragment & if (cursor.blockNumber() > 0) { hasBefore = true; } - auto afterBlock = cursor.blockNumber() < document()->blockCount() - 1; + auto afterBlock = cursor.blockNumber() < m_textItem->document()->blockCount() - 1; cursor.beginEditBlock(); cursor.movePosition(QTextCursor::StartOfBlock); @@ -496,7 +434,7 @@ void ChatDocumentHandler::fillFragments(bool &hasBefore, QTextDocumentFragment & void ChatDocumentHandler::insertFragment(const QTextDocumentFragment fragment, InsertPosition position, bool keepPosition) { - auto cursor = textCursor(); + auto cursor = m_textItem->textCursor(); if (cursor.isNull()) { return; } @@ -507,7 +445,7 @@ void ChatDocumentHandler::insertFragment(const QTextDocumentFragment fragment, I currentPosition = 0; break; case End: - currentPosition = document()->characterCount() - 1; + currentPosition = m_textItem->document()->characterCount() - 1; break; case Cursor: currentPosition = cursor.position(); @@ -536,18 +474,16 @@ void ChatDocumentHandler::insertFragment(const QTextDocumentFragment fragment, I if (keepPosition) { cursor.setPosition(currentPosition); } - if (textItem()) { - textItem()->setProperty("cursorPosition", cursor.position()); - } + m_textItem->setCursorPosition(cursor.position()); } QString ChatDocumentHandler::getText() const { - if (!document()) { + if (!m_textItem->document()) { qCWarning(ChatDocumentHandling) << "getText called with no QQuickTextDocument available."; return {}; } - return document()->toPlainText(); + return m_textItem->document()->toPlainText(); } void ChatDocumentHandler::pushMention(const Mention mention) const @@ -584,7 +520,7 @@ void ChatDocumentHandler::updateMentions(const QString &editId) const int end = position + name.length(); linkSize += match.capturedLength(0) - name.length(); - QTextCursor cursor(document()); + QTextCursor cursor(m_textItem->document()); cursor.setPosition(position); cursor.setPosition(end, QTextCursor::KeepAnchor); cursor.setKeepPositionOnInsert(true); @@ -606,40 +542,40 @@ void ChatDocumentHandler::setTextColor(const QColor &color) bool ChatDocumentHandler::bold() const { - QTextCursor cursor = textCursor(); + QTextCursor cursor = m_textItem->textCursor(); if (cursor.isNull()) { return false; } - return textCursor().charFormat().fontWeight() == QFont::Bold; + return cursor.charFormat().fontWeight() == QFont::Bold; } bool ChatDocumentHandler::italic() const { - QTextCursor cursor = textCursor(); + QTextCursor cursor = m_textItem->textCursor(); if (cursor.isNull()) return false; - return textCursor().charFormat().fontItalic(); + return cursor.charFormat().fontItalic(); } bool ChatDocumentHandler::underline() const { - QTextCursor cursor = textCursor(); + QTextCursor cursor = m_textItem->textCursor(); if (cursor.isNull()) return false; - return textCursor().charFormat().fontUnderline(); + return cursor.charFormat().fontUnderline(); } bool ChatDocumentHandler::strikethrough() const { - QTextCursor cursor = textCursor(); + QTextCursor cursor = m_textItem->textCursor(); if (cursor.isNull()) return false; - return textCursor().charFormat().fontStrikeOut(); + return cursor.charFormat().fontStrikeOut(); } QColor ChatDocumentHandler::textColor() const { - QTextCursor cursor = textCursor(); + QTextCursor cursor = m_textItem->textCursor(); if (cursor.isNull()) return QColor(Qt::black); QTextCharFormat format = cursor.charFormat(); @@ -657,7 +593,7 @@ std::optional ChatDocumentHandler::textFormat() const void ChatDocumentHandler::mergeFormatOnWordOrSelection(const QTextCharFormat &format) { - QTextCursor cursor = textCursor(); + QTextCursor cursor = m_textItem->textCursor(); if (!cursor.hasSelection()) { cursor.select(QTextCursor::WordUnderCursor); } @@ -670,7 +606,7 @@ void ChatDocumentHandler::mergeFormatOnWordOrSelection(const QTextCharFormat &fo QString ChatDocumentHandler::currentLinkText() const { - QTextCursor cursor = textCursor(); + QTextCursor cursor = m_textItem->textCursor(); selectLinkText(&cursor); 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("
")); - - while (canDedentList()) { - m_nestedListHelper.handleOnIndentLess(textCursor()); - } - - textCursor().insertHtml(QStringLiteral("")); -} - -void ChatDocumentHandler::insertTable(int rows, int columns) -{ - QString htmlText; - - QTextCursor cursor = textCursor(); - QTextTableFormat tableFormat; - tableFormat.setBorder(1); - const int numberOfColumns(columns); - QList 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) { - QTextCursor cursor = textCursor(); + QTextCursor cursor = m_textItem->textCursor(); if (cursor.isNull()) { return; } @@ -813,7 +688,7 @@ void ChatDocumentHandler::insertCompletion(const QString &text, const QUrl &link void ChatDocumentHandler::updateLink(const QString &linkUrl, const QString &linkText) { - auto cursor = textCursor(); + auto cursor = m_textItem->textCursor(); selectLinkText(&cursor); cursor.beginEditBlock(); @@ -898,39 +773,39 @@ void ChatDocumentHandler::setFormat(RichFormat::Format format) void ChatDocumentHandler::indentListMore() { - m_nestedListHelper.handleOnIndentMore(textCursor()); + m_nestedListHelper.handleOnIndentMore(m_textItem->textCursor()); Q_EMIT currentListStyleChanged(); } void ChatDocumentHandler::indentListLess() { - m_nestedListHelper.handleOnIndentLess(textCursor()); + m_nestedListHelper.handleOnIndentLess(m_textItem->textCursor()); Q_EMIT currentListStyleChanged(); } 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(); } 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 { - 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 { - if (!textCursor().currentList()) { + if (!m_textItem->textCursor().currentList()) { return 0; } - return -textCursor().currentList()->format().style(); + return -m_textItem->textCursor().currentList()->format().style(); } void ChatDocumentHandler::setTextFormat(RichFormat::Format format) @@ -938,13 +813,13 @@ void ChatDocumentHandler::setTextFormat(RichFormat::Format format) if (RichFormat::typeForFormat(format) != RichFormat::Text) { return; } - mergeFormatOnWordOrSelection(RichFormat::charFormatForFormat(format, RichFormat::hasFormat(textCursor(), format))); + mergeFormatOnWordOrSelection(RichFormat::charFormatForFormat(format, RichFormat::hasFormat(m_textItem->textCursor(), format))); Q_EMIT formatChanged(); } RichFormat::Format ChatDocumentHandler::style() const { - return static_cast(textCursor().blockFormat().headingLevel()); + return static_cast(m_textItem->textCursor().blockFormat().headingLevel()); } void ChatDocumentHandler::setStyleFormat(RichFormat::Format format) @@ -955,9 +830,7 @@ void ChatDocumentHandler::setStyleFormat(RichFormat::Format format) return; } - qWarning() << format; - - QTextCursor cursor = textCursor(); + QTextCursor cursor = m_textItem->textCursor(); if (cursor.isNull()) { return; } @@ -993,7 +866,7 @@ void ChatDocumentHandler::setStyleFormat(RichFormat::Format format) void ChatDocumentHandler::tab() { - QTextCursor cursor = textCursor(); + QTextCursor cursor = m_textItem->textCursor(); if (cursor.isNull()) { return; } @@ -1006,11 +879,11 @@ void ChatDocumentHandler::tab() void ChatDocumentHandler::deleteChar() { - QTextCursor cursor = textCursor(); + QTextCursor cursor = m_textItem->textCursor(); if (cursor.isNull()) { 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()) { insertFragment(nextHandler->takeFirstBlock(), Cursor, true); } @@ -1021,7 +894,7 @@ void ChatDocumentHandler::deleteChar() void ChatDocumentHandler::backspace() { - QTextCursor cursor = textCursor(); + QTextCursor cursor = m_textItem->textCursor(); if (cursor.isNull()) { return; } @@ -1043,7 +916,7 @@ void ChatDocumentHandler::backspace() void ChatDocumentHandler::insertReturn() { - QTextCursor cursor = textCursor(); + QTextCursor cursor = m_textItem->textCursor(); if (cursor.isNull()) { return; } @@ -1052,12 +925,12 @@ void ChatDocumentHandler::insertReturn() void ChatDocumentHandler::insertText(const QString &text) { - textCursor().insertText(text); + m_textItem->textCursor().insertText(text); } QString ChatDocumentHandler::currentLinkUrl() const { - return textCursor().charFormat().anchorHref(); + return m_textItem->textCursor().charFormat().anchorHref(); } void ChatDocumentHandler::dumpHtml() @@ -1067,7 +940,7 @@ void ChatDocumentHandler::dumpHtml() QString ChatDocumentHandler::htmlText() const { - const auto doc = document(); + const auto doc = m_textItem->document(); if (!doc) { return {}; } diff --git a/src/libneochat/chatdocumenthandler.h b/src/libneochat/chatdocumenthandler.h index 3d82de288..ba89c9bbc 100644 --- a/src/libneochat/chatdocumenthandler.h +++ b/src/libneochat/chatdocumenthandler.h @@ -19,6 +19,7 @@ class QTextDocument; +class QmlTextItemWrapper; class NeoChatRoom; class SyntaxHighlighter; @@ -179,8 +180,6 @@ public: Q_INVOKABLE QString currentLinkUrl() const; Q_INVOKABLE QString currentLinkText() const; 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(); @@ -210,8 +209,8 @@ Q_SIGNALS: private: ChatBarType::Type m_type = ChatBarType::None; QPointer m_room; - QPointer m_textItem; - QTextDocument *document() const; + QPointer m_textItem; + void connectTextItem(); QPointer m_previousDocumentHandler; QPointer m_nextDocumentHandler; @@ -221,11 +220,6 @@ private: QString m_initialText = {}; void initializeChars(); - int cursorPosition() const; - int selectionStart() const; - int selectionEnd() const; - QTextCursor textCursor() const; - void setTextFormat(RichFormat::Format format); void setStyleFormat(RichFormat::Format format); void setListFormat(RichFormat::Format format); @@ -248,7 +242,4 @@ private: void regenerateColorScheme(); QString trim(QString string) const; - -private Q_SLOTS: - void updateCursor(); }; diff --git a/src/libneochat/qmltextitemwrapper.cpp b/src/libneochat/qmltextitemwrapper.cpp index 0caea0a5c..a70b02e13 100644 --- a/src/libneochat/qmltextitemwrapper.cpp +++ b/src/libneochat/qmltextitemwrapper.cpp @@ -35,6 +35,7 @@ void QmlTextItemWrapper::setTextItem(QQuickItem *textItem) connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(textDocCursorChanged())); if (document()) { connect(document(), &QTextDocument::contentsChanged, this, &QmlTextItemWrapper::textDocumentContentsChanged); + connect(document(), &QTextDocument::contentsChange, this, &QmlTextItemWrapper::textDocumentContentsChange); } } @@ -90,9 +91,33 @@ QTextCursor QmlTextItemWrapper::textCursor() const 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() { Q_EMIT textDocumentCursorPositionChanged(); } +void QmlTextItemWrapper::forceActiveFocus() const +{ + if (!m_textItem) { + return; + } + m_textItem->forceActiveFocus(); +} + #include "moc_qmltextitemwrapper.cpp" diff --git a/src/libneochat/qmltextitemwrapper.h b/src/libneochat/qmltextitemwrapper.h index b3f1f78fd..df947a31c 100644 --- a/src/libneochat/qmltextitemwrapper.h +++ b/src/libneochat/qmltextitemwrapper.h @@ -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. * - * 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. * * @sa QQuickItem, TextEdit @@ -31,10 +31,16 @@ public: QTextDocument *document() const; QTextCursor textCursor() const; + void setCursorPosition(int pos); + void setCursorVisible(bool visible); + + void forceActiveFocus() const; Q_SIGNALS: void textItemChanged(); + void textDocumentContentsChange(int position, int charsRemoved, int charsAdded); + void textDocumentContentsChanged(); void textDocumentCursorPositionChanged();