diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index a47344b13..919dc16a8 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -121,8 +121,9 @@ macro(add_qml_tests) endmacro() add_executable(qmltest qmltest.cpp + chatkeyhelpertesthelper.h chatmarkdownhelpertestwrapper.h - chattextitemhelpertestwrapper.h + chattextitemhelpertesthelper.h ) qt_add_qml_module(qmltest URI NeoChatTestUtils) @@ -137,4 +138,5 @@ target_link_libraries(qmltest add_qml_tests( chattextitemhelpertest.qml chatmarkdownhelpertest.qml + chatkeyhelpertest.qml ) diff --git a/autotests/chatkeyhelpertest.qml b/autotests/chatkeyhelpertest.qml new file mode 100644 index 000000000..92043a47c --- /dev/null +++ b/autotests/chatkeyhelpertest.qml @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2024 Carl Schwan +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtTest + +import org.kde.neochat.libneochat + +import NeoChatTestUtils + +TestCase { + name: "ChatTextItemHelperTest" + + TextEdit { + id: textEdit + + Keys.onUpPressed: (event) => { + event.accepted = true; + testHelper.keyHelper.up(); + } + + Keys.onDownPressed: (event) => { + event.accepted = true; + testHelper.keyHelper.down(); + } + } + + ChatTextItemHelper { + id: textItemHelper + + textItem: textEdit + } + + ChatKeyHelperTestHelper { + id: testHelper + + textItem: textItemHelper + } + + SignalSpy { + id: spyItem + target: textItemHelper + signalName: "textItemChanged" + } + + SignalSpy { + id: spyUp + target: testHelper.keyHelper + signalName: "unhandledUp" + } + + SignalSpy { + id: spyDown + target: testHelper.keyHelper + signalName: "unhandledDown" + } + + SignalSpy { + id: spyDelete + target: testHelper.keyHelper + signalName: "unhandledDelete" + } + + SignalSpy { + id: spyBackSpace + target: testHelper.keyHelper + signalName: "unhandledBackspace" + } + + function init(): void { + textEdit.clear(); + spyItem.clear(); + spyUp.clear(); + spyDown.clear(); + spyDelete.clear(); + spyBackSpace.clear(); + textEdit.forceActiveFocus(); + } + + function test_upDown(): void { + textEdit.insert(0, "line 1\nline 2\nline 3") + textEdit.cursorPosition = 0; + keyClick(Qt.Key_Up); + compare(spyUp.count, 1); + compare(spyDown.count, 0); + keyClick(Qt.Key_Down); + compare(spyUp.count, 1); + compare(spyDown.count, 0); + keyClick(Qt.Key_Down); + compare(spyUp.count, 1); + compare(spyDown.count, 0); + keyClick(Qt.Key_Down); + compare(spyUp.count, 1); + compare(spyDown.count, 1); + } +} diff --git a/autotests/chatkeyhelpertesthelper.h b/autotests/chatkeyhelpertesthelper.h new file mode 100644 index 000000000..50be4e9b0 --- /dev/null +++ b/autotests/chatkeyhelpertesthelper.h @@ -0,0 +1,57 @@ +// 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 +#include + +#include "chatkeyhelper.h" +#include "chattextitemhelper.h" + +class ChatKeyHelperTestHelper : public QObject +{ + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(ChatTextItemHelper *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged) + + Q_PROPERTY(ChatKeyHelper *keyHelper READ keyHelper CONSTANT) + +public: + explicit ChatKeyHelperTestHelper(QObject *parent = nullptr) + : QObject(parent) + , m_keyHelper(new ChatKeyHelper(this)) + { + } + + ChatTextItemHelper *textItem() const + { + return m_textItem; + } + void setTextItem(ChatTextItemHelper *textItem) + { + if (textItem == m_textItem) { + return; + } + m_textItem = textItem; + m_keyHelper->setTextItem(textItem); + Q_EMIT textItemChanged(); + } + + ChatKeyHelper *keyHelper() const + { + return m_keyHelper; + } + +Q_SIGNALS: + void textItemChanged(); + +private: + QPointer m_textItem; + QPointer m_keyHelper; +}; diff --git a/autotests/chatmarkdownhelpertest.qml b/autotests/chatmarkdownhelpertest.qml index 96af8b4a9..9ed992e3b 100644 --- a/autotests/chatmarkdownhelpertest.qml +++ b/autotests/chatmarkdownhelpertest.qml @@ -53,13 +53,10 @@ TestCase { function test_item(): void { spyItem.clear(); compare(chatMarkdownHelper.textItem, textEdit); - compare(spyItem.count, 0); chatMarkdownHelper.textItem = textEdit2; compare(chatMarkdownHelper.textItem, textEdit2); - compare(spyItem.count, 1); chatMarkdownHelper.textItem = textEdit; compare(chatMarkdownHelper.textItem, textEdit); - compare(spyItem.count, 2); } function test_textFormat_data() { diff --git a/autotests/chatmarkdownhelpertestwrapper.h b/autotests/chatmarkdownhelpertestwrapper.h index b16273370..e5b054b3d 100644 --- a/autotests/chatmarkdownhelpertestwrapper.h +++ b/autotests/chatmarkdownhelpertestwrapper.h @@ -28,19 +28,18 @@ public: , m_chatMarkdownHelper(new ChatMarkdownHelper(this)) , m_textItem(new ChatTextItemHelper(this)) { + m_chatMarkdownHelper->setTextItem(m_textItem); + connect(m_chatMarkdownHelper, &ChatMarkdownHelper::textItemChanged, this, &ChatMarkdownHelperTestWrapper::textItemChanged); connect(m_chatMarkdownHelper, &ChatMarkdownHelper::unhandledBlockFormat, this, &ChatMarkdownHelperTestWrapper::unhandledBlockFormat); } QQuickItem *textItem() const { - return m_chatMarkdownHelper->textItem()->textItem(); + return m_textItem->textItem(); } void setTextItem(QQuickItem *textItem) { - auto textItemWrapper = new ChatTextItemHelper(this); - textItemWrapper->setTextItem(textItem); - m_chatMarkdownHelper->setTextItem(textItemWrapper); m_textItem->setTextItem(textItem); } @@ -50,7 +49,6 @@ public: if (!doc) { return false; } - qWarning() << text << doc->toPlainText(); return text == doc->toPlainText(); } @@ -60,7 +58,6 @@ public: if (cursor.isNull()) { return false; } - qWarning() << RichFormat::formatsAtCursor(cursor) << formats; return RichFormat::formatsAtCursor(cursor) == formats; } diff --git a/autotests/chattextitemhelpertest.qml b/autotests/chattextitemhelpertest.qml index 7406cac58..97de07d24 100644 --- a/autotests/chattextitemhelpertest.qml +++ b/autotests/chattextitemhelpertest.qml @@ -4,6 +4,8 @@ import QtQuick import QtTest +import org.kde.neochat.libneochat + import NeoChatTestUtils TestCase { @@ -17,60 +19,151 @@ TestCase { id: textEdit2 } - ChatTextItemHelperTestWrapper { - id: chatTextItemHelper + ChatTextItemHelper { + id: textItemHelper textItem: textEdit } + ChatTextItemHelperTestHelper { + id: testHelper + + textItem: textItemHelper + } + SignalSpy { id: spyItem - target: chatTextItemHelper + target: textItemHelper signalName: "textItemChanged" } SignalSpy { id: spyContentsChanged - target: chatTextItemHelper + target: textItemHelper signalName: "contentsChanged" } SignalSpy { id: spyContentsChange - target: chatTextItemHelper + target: textItemHelper signalName: "contentsChange" } SignalSpy { id: spyCursor - target: chatTextItemHelper + target: textItemHelper signalName: "cursorPositionChanged" } - function test_item(): void { + function init(): void { + testHelper.setFixedChars("", ""); + textEdit.clear(); + textEdit2.clear(); spyItem.clear(); - compare(chatTextItemHelper.textItem, textEdit); + spyContentsChange.clear(); + spyContentsChanged.clear(); + spyCursor.clear(); + } + + function test_item(): void { + compare(textItemHelper.textItem, textEdit); compare(spyItem.count, 0); - chatTextItemHelper.textItem = textEdit2; - compare(chatTextItemHelper.textItem, textEdit2); + textItemHelper.textItem = textEdit2; + compare(textItemHelper.textItem, textEdit2); compare(spyItem.count, 1); - chatTextItemHelper.textItem = textEdit; - compare(chatTextItemHelper.textItem, textEdit); + textItemHelper.textItem = textEdit; + compare(textItemHelper.textItem, textEdit); compare(spyItem.count, 2); } + function test_fixedChars(): void { + textEdit.forceActiveFocus(); + testHelper.setFixedChars("1", "2"); + compare(textEdit.text, "12"); + compare(textEdit.cursorPosition, 1); + compare(spyCursor.count, 0); + keyClick("b"); + compare(textEdit.text, "1b2"); + compare(textEdit.cursorPosition, 2); + compare(spyCursor.count, 1); + keyClick(Qt.Key_Left); + compare(textEdit.text, "1b2"); + compare(textEdit.cursorPosition, 1); + compare(spyCursor.count, 2); + keyClick(Qt.Key_Left); + compare(textEdit.text, "1b2"); + compare(textEdit.cursorPosition, 1); + compare(spyCursor.count, 3); + keyClick(Qt.Key_Right); + compare(textEdit.text, "1b2"); + compare(textEdit.cursorPosition, 2); + compare(spyCursor.count, 4); + keyClick(Qt.Key_Right); + compare(textEdit.text, "1b2"); + compare(textEdit.cursorPosition, 2); + compare(spyCursor.count, 5); + } + function test_document(): void { // We can't get to the QTextDocument from QML so we have to use a helper function. - compare(chatTextItemHelper.compareDocuments(textEdit.textDocument), true); + compare(testHelper.compareDocuments(textEdit.textDocument), true); + + textEdit.insert(0, "test text"); + compare(testHelper.lineCount(), 1); + textEdit.insert(textEdit.text.length, "\ntest text"); + compare(testHelper.lineCount(), 2); + textEdit.clear() + compare(textEdit.text.length, 0); + } + + function test_takeFirstBlock(): void { + textEdit.insert(0, "test text"); + compare(testHelper.firstBlockText(), "test text"); + compare(textEdit.text.length, 0); + textEdit.insert(0, "test text\nmore test text"); + compare(testHelper.firstBlockText(), "test text"); + compare(textEdit.text, "more test text"); + compare(testHelper.firstBlockText(), "more test text"); + compare(textEdit.text, ""); + compare(textEdit.text.length, 0); + } + + function test_fillFragments(): void { + textEdit.insert(0, "before fragment\nmid fragment\nafter fragment"); + compare(testHelper.checkFragments("before fragment\nmid fragment", "after fragment", ""), true); + textEdit.clear(); + textEdit.insert(0, "before fragment\nmid fragment\nafter fragment"); + textEdit.cursorPosition = 16; + compare(testHelper.checkFragments("before fragment", "mid fragment", "after fragment"), true); + textEdit.clear(); + textEdit.insert(0, "before fragment\nmid fragment\nafter fragment"); + textEdit.cursorPosition = 29; + compare(testHelper.checkFragments("before fragment\nmid fragment", "after fragment", ""), true); + textEdit.clear(); + } + + function test_insertFragment(): void { + testHelper.insertFragment("test text"); + compare(textEdit.text, "test text"); + compare(textEdit.cursorPosition, 9); + testHelper.insertFragment("beginning ", 1); + compare(textEdit.text, "beginning test text"); + compare(textEdit.cursorPosition, 10); + testHelper.insertFragment(" end", 2); + compare(textEdit.text, "beginning test text end"); + compare(textEdit.cursorPosition, 23); + textEdit.clear(); + + testHelper.insertFragment("test text", 0, true); + compare(textEdit.text, "test text"); + compare(textEdit.cursorPosition, 0); } 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(chatTextItemHelper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true); - compare(textEdit.cursorPosition, chatTextItemHelper.cursorPosition()); + compare(testHelper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true); + compare(textEdit.cursorPosition, testHelper.cursorPosition()); + // Check we get the appropriate content and cursor change signals when inserting text. textEdit.insert(0, "test text") compare(spyContentsChange.count, 1); compare(spyContentsChange.signalArguments[0][0], 0); @@ -78,50 +171,127 @@ TestCase { compare(spyContentsChange.signalArguments[0][2], 9); compare(spyContentsChanged.count, 1); compare(spyCursor.count, 1); - compare(chatTextItemHelper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true); - compare(textEdit.cursorPosition, chatTextItemHelper.cursorPosition()); + compare(spyCursor.signalArguments[0][0], true); + compare(testHelper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true); + compare(textEdit.cursorPosition, testHelper.cursorPosition()); + // Check we get only get a cursor change signal when moving the cursor. + textEdit.cursorPosition = 4; + compare(spyContentsChanged.count, 1); + compare(spyCursor.count, 2); + compare(spyCursor.signalArguments[1][0], false); textEdit.selectAll(); compare(spyContentsChanged.count, 1); - compare(spyCursor.count, 1); - compare(chatTextItemHelper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true); - compare(textEdit.cursorPosition, chatTextItemHelper.cursorPosition()); + compare(spyCursor.count, 2); + compare(testHelper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true); + compare(textEdit.cursorPosition, testHelper.cursorPosition()); + // Check we get the appropriate content and cursor change signals when removing text. 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); - + compare(spyCursor.count, 3); + compare(spyCursor.signalArguments[2][0], true); } function test_setCursor(): void { - spyCursor.clear(); textEdit.insert(0, "test text"); compare(textEdit.cursorPosition, 9); compare(spyCursor.count, 1); - chatTextItemHelper.setCursorPosition(5); + testHelper.setCursorPosition(5); compare(textEdit.cursorPosition, 5); compare(spyCursor.count, 2); - chatTextItemHelper.setCursorPosition(1); + testHelper.setCursorPosition(1); compare(textEdit.cursorPosition, 1); compare(spyCursor.count, 3); textEdit.cursorVisible = false; compare(textEdit.cursorVisible, false); - chatTextItemHelper.setCursorVisible(true); + testHelper.setCursorVisible(true); compare(textEdit.cursorVisible, true); - chatTextItemHelper.setCursorVisible(false); + testHelper.setCursorVisible(false); compare(textEdit.cursorVisible, false); + } + function test_setCursorFromTextItem(): void { + textEdit.insert(0, "line 1\nline 2"); + textEdit2.insert(0, "line 1\nline 2"); + testHelper.setCursorFromTextItem(textEdit2, false, 0); + compare(textEdit.cursorPosition, 7); + testHelper.setCursorFromTextItem(textEdit2, true, 7); + compare(textEdit.cursorPosition, 0); + testHelper.setCursorFromTextItem(textEdit2, false, 1); + compare(textEdit.cursorPosition, 8); + testHelper.setCursorFromTextItem(textEdit2, true, 8); + compare(textEdit.cursorPosition, 1); + + testHelper.setFixedChars("1", "2"); + testHelper.setCursorFromTextItem(textEdit2, false, 0); + compare(textEdit.cursorPosition, 8); + testHelper.setCursorFromTextItem(textEdit2, true, 7); + compare(textEdit.cursorPosition, 1); + } + + function test_mergeFormat(): void { + textEdit.insert(0, "lots of text"); + testHelper.setCursorPosition(0); + testHelper.mergeFormatOnCursor(RichFormat.Bold); + compare(testHelper.checkFormatsAtCursor([RichFormat.Bold]), true); + testHelper.mergeFormatOnCursor(RichFormat.Italic); + compare(testHelper.checkFormatsAtCursor([RichFormat.Bold, RichFormat.Italic]), true); + testHelper.setCursorPosition(6); + compare(testHelper.checkFormatsAtCursor([]), true); + testHelper.mergeFormatOnCursor(RichFormat.Underline); + compare(testHelper.checkFormatsAtCursor([RichFormat.Underline]), true); + testHelper.setCursorPosition(9); + compare(testHelper.checkFormatsAtCursor([]), true); + testHelper.mergeFormatOnCursor(RichFormat.Strikethrough); + compare(testHelper.checkFormatsAtCursor([RichFormat.Strikethrough]), true); + compare(testHelper.markdownText(), "***lots*** _of_ ~~text~~"); textEdit.clear(); - compare(spyCursor.count, 4); + + textEdit.insert(0, "heading"); + testHelper.mergeFormatOnCursor(RichFormat.Heading1); + compare(testHelper.checkFormatsAtCursor([RichFormat.Bold, RichFormat.Heading1]), true); + testHelper.mergeFormatOnCursor(RichFormat.Heading2); + compare(testHelper.checkFormatsAtCursor([RichFormat.Bold, RichFormat.Heading2]), true); + testHelper.mergeFormatOnCursor(RichFormat.Paragraph); + compare(testHelper.checkFormatsAtCursor([]), true); + textEdit.clear(); + + textEdit.insert(0, "text"); + testHelper.mergeFormatOnCursor(RichFormat.UnorderedList); + compare(testHelper.checkFormatsAtCursor([RichFormat.UnorderedList]), true); + compare(testHelper.markdownText(), "- text"); + testHelper.mergeFormatOnCursor(RichFormat.OrderedList); + compare(testHelper.checkFormatsAtCursor([RichFormat.OrderedList]), true); + compare(testHelper.markdownText(), "1. text"); + textEdit.clear(); + } + + function test_list(): void { + compare(testHelper.canIndentListMoreAtCursor(), true); + testHelper.indentListMoreAtCursor(); + compare(testHelper.canIndentListMoreAtCursor(), true); + testHelper.indentListMoreAtCursor(); + compare(testHelper.canIndentListMoreAtCursor(), true); + testHelper.indentListMoreAtCursor(); + compare(testHelper.canIndentListMoreAtCursor(), false); + + compare(testHelper.canIndentListLessAtCursor(), true); + testHelper.indentListLessAtCursor(); + compare(testHelper.canIndentListLessAtCursor(), true); + testHelper.indentListLessAtCursor(); + compare(testHelper.canIndentListLessAtCursor(), true); + testHelper.indentListLessAtCursor(); + compare(testHelper.canIndentListLessAtCursor(), false); } function test_forceActiveFocus(): void { textEdit2.forceActiveFocus(); compare(textEdit.activeFocus, false); - chatTextItemHelper.forceActiveFocus(); + testHelper.forceActiveFocus(); compare(textEdit.activeFocus, true); } } diff --git a/autotests/chattextitemhelpertesthelper.h b/autotests/chattextitemhelpertesthelper.h new file mode 100644 index 000000000..7da1af3a9 --- /dev/null +++ b/autotests/chattextitemhelpertesthelper.h @@ -0,0 +1,215 @@ +// 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 +#include + +#include "chattextitemhelper.h" + +class ChatTextItemHelperTestHelper : public QObject +{ + Q_OBJECT + QML_ELEMENT + + /** + * @brief The QML text Item the TextItemHelper is handling. + */ + Q_PROPERTY(ChatTextItemHelper *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged) + +public: + explicit ChatTextItemHelperTestHelper(QObject *parent = nullptr) + : QObject(parent) + { + } + + ChatTextItemHelper *textItem() const + { + return m_textItem; + } + void setTextItem(ChatTextItemHelper *textItem) + { + if (textItem == m_textItem) { + return; + } + m_textItem = textItem; + Q_EMIT textItemChanged(); + } + + Q_INVOKABLE void setFixedChars(const QString &startChars, const QString &endChars) + { + m_textItem->setFixedChars(startChars, endChars); + } + + Q_INVOKABLE bool compareDocuments(QQuickTextDocument *document) + { + if (!m_textItem) { + return false; + } + return document->textDocument() == m_textItem->document(); + } + + Q_INVOKABLE int lineCount() + { + if (!m_textItem) { + return -1; + } + return m_textItem->lineCount(); + } + + Q_INVOKABLE QString firstBlockText() + { + if (!m_textItem) { + return {}; + } + return m_textItem->takeFirstBlock().toPlainText(); + } + + Q_INVOKABLE bool checkFragments(const QString &before, const QString &mid, const QString &after) + { + if (!m_textItem) { + return false; + } + + bool hasBefore = false; + QTextDocumentFragment midFragment; + std::optional afterFragment = std::nullopt; + m_textItem->fillFragments(hasBefore, midFragment, afterFragment); + + return hasBefore && m_textItem->document()->toPlainText() == before && midFragment.toPlainText() == mid && after.isEmpty() + ? !afterFragment + : afterFragment->toPlainText() == after; + } + + Q_INVOKABLE void insertFragment(const QString &text, ChatTextItemHelper::InsertPosition position = ChatTextItemHelper::Cursor, bool keepPosition = false) + { + if (!m_textItem) { + return; + } + const auto fragment = QTextDocumentFragment::fromPlainText(text); + m_textItem->insertFragment(fragment, position, keepPosition); + } + + Q_INVOKABLE bool compareCursor(int cursorPosition, int selectionStart, int selectionEnd) + { + if (!m_textItem) { + return false; + } + const auto cursor = m_textItem->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 int cursorPosition() const + { + if (!m_textItem) { + return -1; + } + return *m_textItem->cursorPosition(); + } + + Q_INVOKABLE void setCursorPosition(int pos) + { + if (!m_textItem) { + return; + } + m_textItem->setCursorPosition(pos); + } + + Q_INVOKABLE void setCursorVisible(bool visible) + { + if (!m_textItem) { + return; + } + m_textItem->setCursorVisible(visible); + } + + Q_INVOKABLE void setCursorFromTextItem(QQuickItem *item, bool infront, int cursorPos) + { + if (!m_textItem) { + return; + } + const auto textItem = new ChatTextItemHelper(); + textItem->setTextItem(item); + textItem->setCursorPosition(cursorPos); + m_textItem->setCursorFromTextItem(textItem, infront); + } + + Q_INVOKABLE void mergeFormatOnCursor(RichFormat::Format format) + { + if (!m_textItem) { + return; + } + m_textItem->mergeFormatOnCursor(format); + } + + Q_INVOKABLE bool checkFormatsAtCursor(QList formats) + { + const auto cursor = m_textItem->textCursor(); + if (cursor.isNull()) { + return false; + } + return RichFormat::formatsAtCursor(cursor) == formats; + } + + Q_INVOKABLE bool canIndentListMoreAtCursor() const + { + if (!m_textItem) { + return false; + } + return m_textItem->canIndentListMoreAtCursor(); + } + Q_INVOKABLE bool canIndentListLessAtCursor() const + { + if (!m_textItem) { + return false; + } + return m_textItem->canIndentListLessAtCursor(); + } + Q_INVOKABLE void indentListMoreAtCursor() + { + if (!m_textItem) { + return; + } + m_textItem->indentListMoreAtCursor(); + } + Q_INVOKABLE void indentListLessAtCursor() + { + if (!m_textItem) { + return; + } + m_textItem->indentListLessAtCursor(); + } + + Q_INVOKABLE void forceActiveFocus() const + { + if (!m_textItem) { + return; + } + m_textItem->forceActiveFocus(); + } + + Q_INVOKABLE QString markdownText() const + { + if (!m_textItem) { + return {}; + } + return m_textItem->markdownText(); + } + +Q_SIGNALS: + void textItemChanged(); + +private: + QPointer m_textItem; +}; diff --git a/autotests/chattextitemhelpertestwrapper.h b/autotests/chattextitemhelpertestwrapper.h deleted file mode 100644 index a0c5ea3d3..000000000 --- a/autotests/chattextitemhelpertestwrapper.h +++ /dev/null @@ -1,89 +0,0 @@ -// 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 "chattextitemhelper.h" - -class ChatTextItemHelperTestWrapper : public QObject -{ - Q_OBJECT - QML_ELEMENT - - /** - * @brief The QML text Item the TextItemHelper is handling. - */ - Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged) - -public: - explicit ChatTextItemHelperTestWrapper(QObject *parent = nullptr) - : QObject(parent) - , m_textItemWrapper(new ChatTextItemHelper(this)) - { - Q_ASSERT(m_textItemWrapper); - connect(m_textItemWrapper, &ChatTextItemHelper::textItemChanged, this, &ChatTextItemHelperTestWrapper::textItemChanged); - connect(m_textItemWrapper, &ChatTextItemHelper::contentsChange, this, &ChatTextItemHelperTestWrapper::contentsChange); - connect(m_textItemWrapper, &ChatTextItemHelper::contentsChanged, this, &ChatTextItemHelperTestWrapper::contentsChanged); - connect(m_textItemWrapper, &ChatTextItemHelper::cursorPositionChanged, this, &ChatTextItemHelperTestWrapper::cursorPositionChanged); - } - - 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 int cursorPosition() const - { - return m_textItemWrapper->cursorPosition(); - } - - 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 contentsChange(int position, int charsRemoved, int charsAdded); - void contentsChanged(); - void cursorPositionChanged(); - -private: - QPointer m_textItemWrapper; -}; diff --git a/src/chatbar/RichEditBar.qml b/src/chatbar/RichEditBar.qml index bb112ef1a..a1f32cc3a 100644 --- a/src/chatbar/RichEditBar.qml +++ b/src/chatbar/RichEditBar.qml @@ -23,14 +23,6 @@ QQC2.ToolBar { required property MessageContent.ChatBarMessageContentModel contentModel - Connections { - target: contentModel - - function onFocusRowChanged() { - console.warn("focus changed", contentModel.focusRow, contentModel.focusType) - } - } - required property real maxAvailableWidth readonly property real uncompressedImplicitWidth: textFormatRow.implicitWidth + diff --git a/src/chatbar/StylePicker.qml b/src/chatbar/StylePicker.qml index 9b9ea188f..3781b3ef2 100644 --- a/src/chatbar/StylePicker.qml +++ b/src/chatbar/StylePicker.qml @@ -87,7 +87,7 @@ QQC2.Popup { radius: Kirigami.Units.cornerRadius border { width: 1 - color: styleDelegate.hovered || root.chatButtonHelper.currentStyle === styleDelegate.index ? + color: styleDelegate.hovered || (root.chatButtonHelper.currentStyle === styleDelegate.index) ? Kirigami.Theme.highlightColor : Kirigami.ColorUtils.linearInterpolation( Kirigami.Theme.backgroundColor, diff --git a/src/chatbar/chatbuttonhelper.cpp b/src/chatbar/chatbuttonhelper.cpp index 347fc38ba..e12d6f2ce 100644 --- a/src/chatbar/chatbuttonhelper.cpp +++ b/src/chatbar/chatbuttonhelper.cpp @@ -44,7 +44,11 @@ bool ChatButtonHelper::bold() const if (!m_textItem) { return false; } - return m_textItem->formatsAtCursor().contains(RichFormat::Bold); + const auto cursor = m_textItem->textCursor(); + if (cursor.isNull()) { + return false; + } + return RichFormat::formatsAtCursor(cursor).contains(RichFormat::Bold); } bool ChatButtonHelper::italic() const @@ -52,7 +56,11 @@ bool ChatButtonHelper::italic() const if (!m_textItem) { return false; } - return m_textItem->formatsAtCursor().contains(RichFormat::Italic); + const auto cursor = m_textItem->textCursor(); + if (cursor.isNull()) { + return false; + } + return RichFormat::formatsAtCursor(cursor).contains(RichFormat::Italic); } bool ChatButtonHelper::underline() const @@ -60,7 +68,11 @@ bool ChatButtonHelper::underline() const if (!m_textItem) { return false; } - return m_textItem->formatsAtCursor().contains(RichFormat::Underline); + const auto cursor = m_textItem->textCursor(); + if (cursor.isNull()) { + return false; + } + return RichFormat::formatsAtCursor(cursor).contains(RichFormat::Underline); } bool ChatButtonHelper::strikethrough() const @@ -68,7 +80,11 @@ bool ChatButtonHelper::strikethrough() const if (!m_textItem) { return false; } - return m_textItem->formatsAtCursor().contains(RichFormat::Strikethrough); + const auto cursor = m_textItem->textCursor(); + if (cursor.isNull()) { + return false; + } + return RichFormat::formatsAtCursor(cursor).contains(RichFormat::Strikethrough); } bool ChatButtonHelper::unorderedList() const @@ -76,7 +92,11 @@ bool ChatButtonHelper::unorderedList() const if (!m_textItem) { return false; } - return m_textItem->formatsAtCursor().contains(RichFormat::UnorderedList); + const auto cursor = m_textItem->textCursor(); + if (cursor.isNull()) { + return false; + } + return RichFormat::formatsAtCursor(cursor).contains(RichFormat::UnorderedList); } bool ChatButtonHelper::orderedList() const @@ -84,7 +104,11 @@ bool ChatButtonHelper::orderedList() const if (!m_textItem) { return false; } - return m_textItem->formatsAtCursor().contains(RichFormat::OrderedList); + const auto cursor = m_textItem->textCursor(); + if (cursor.isNull()) { + return false; + } + return RichFormat::formatsAtCursor(cursor).contains(RichFormat::OrderedList); } RichFormat::Format ChatButtonHelper::currentStyle() const diff --git a/src/libneochat/chatkeyhelper.cpp b/src/libneochat/chatkeyhelper.cpp index a7c4bb1ef..fb2479f29 100644 --- a/src/libneochat/chatkeyhelper.cpp +++ b/src/libneochat/chatkeyhelper.cpp @@ -73,7 +73,7 @@ void ChatKeyHelper::tab() if (cursor.isNull()) { return; } - if (cursor.currentList()) { + if (cursor.currentList() && m_textItem->canIndentListMoreAtCursor()) { m_textItem->indentListMoreAtCursor(); return; } @@ -100,7 +100,7 @@ void ChatKeyHelper::backspace() return; } if (cursor.position() <= m_textItem->fixedStartChars().length()) { - if (cursor.currentList()) { + if (cursor.currentList() && m_textItem->canIndentListLessAtCursor()) { m_textItem->indentListLessAtCursor(); return; } diff --git a/src/libneochat/chatmarkdownhelper.cpp b/src/libneochat/chatmarkdownhelper.cpp index 2dda8af31..a880ebe4f 100644 --- a/src/libneochat/chatmarkdownhelper.cpp +++ b/src/libneochat/chatmarkdownhelper.cpp @@ -36,7 +36,7 @@ const QList syntax = { MarkdownSyntax{.sequence = "`"_L1, .closable = true, .format = RichFormat::InlineCode}, MarkdownSyntax{.sequence = "```"_L1, .lineStart = true, .format = RichFormat::Code}, MarkdownSyntax{.sequence = "~~"_L1, .closable = true, .format = RichFormat::Strikethrough}, - MarkdownSyntax{.sequence = "__"_L1, .closable = true, .format = RichFormat::Underline}, + MarkdownSyntax{.sequence = "_"_L1, .closable = true, .format = RichFormat::Underline}, }; std::optional checkSequence(const QString ¤tString, const QString &nextChar, bool lineStart = false) @@ -104,12 +104,10 @@ void ChatMarkdownHelper::setTextItem(ChatTextItemHelper *textItem) m_textItem = textItem; if (m_textItem) { - connect(m_textItem, &ChatTextItemHelper::textItemChanged, this, &ChatMarkdownHelper::textItemChanged); - connect(m_textItem, &ChatTextItemHelper::textItemChanged, this, [this]() { - m_startPos = m_textItem->cursorPosition(); - m_endPos = m_startPos; - if (m_startPos == 0) { - m_currentState = Pre; + connect(m_textItem, &ChatTextItemHelper::textItemChanged, this, &ChatMarkdownHelper::updateStart); + connect(m_textItem, &ChatTextItemHelper::cursorPositionChanged, this, [this](bool fromContentsChange) { + if (!fromContentsChange) { + updateStart(); } }); connect(m_textItem, &ChatTextItemHelper::contentsChange, this, &ChatMarkdownHelper::checkMarkdown); @@ -118,6 +116,15 @@ void ChatMarkdownHelper::setTextItem(ChatTextItemHelper *textItem) Q_EMIT textItemChanged(); } +void ChatMarkdownHelper::updateStart() +{ + m_startPos = *m_textItem->cursorPosition(); + m_endPos = m_startPos; + if (m_startPos == 0) { + m_currentState = Pre; + } +} + void ChatMarkdownHelper::checkMarkdown(int position, int charsRemoved, int charsAdded) { auto cursor = m_textItem->textCursor(); diff --git a/src/libneochat/chatmarkdownhelper.h b/src/libneochat/chatmarkdownhelper.h index cab1aed6c..1cf785df7 100644 --- a/src/libneochat/chatmarkdownhelper.h +++ b/src/libneochat/chatmarkdownhelper.h @@ -47,6 +47,7 @@ private: State m_currentState = None; int m_startPos = 0; int m_endPos = 0; + void updateStart(); QHash m_currentFormats; diff --git a/src/libneochat/chattextitemhelper.cpp b/src/libneochat/chattextitemhelper.cpp index 649e7a8b7..c7b89088f 100644 --- a/src/libneochat/chattextitemhelper.cpp +++ b/src/libneochat/chattextitemhelper.cpp @@ -53,6 +53,9 @@ void ChatTextItemHelper::setTextItem(QQuickItem *textItem) connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(itemCursorPositionChanged())); if (const auto doc = document()) { connect(doc, &QTextDocument::contentsChanged, this, &ChatTextItemHelper::contentsChanged); + connect(doc, &QTextDocument::contentsChange, this, [this]() { + m_contentsJustChanged = true; + }); connect(doc, &QTextDocument::contentsChange, this, &ChatTextItemHelper::contentsChange); m_highlighter->setDocument(doc); } @@ -122,19 +125,30 @@ void ChatTextItemHelper::initializeChars() return; } + m_initializingChars = true; + + cursor.beginEditBlock(); + int finalCursorPos = cursor.position(); if (doc->isEmpty() && !m_initialText.isEmpty()) { cursor.insertText(m_initialText); + finalCursorPos = cursor.position(); } if (!m_fixedStartChars.isEmpty() && doc->characterAt(0) != m_fixedStartChars) { cursor.movePosition(QTextCursor::Start); - cursor.insertText(m_fixedEndChars); + cursor.insertText(m_fixedStartChars); + finalCursorPos += m_fixedStartChars.length(); } if (!m_fixedStartChars.isEmpty() && doc->characterAt(doc->characterCount()) != m_fixedStartChars) { cursor.movePosition(QTextCursor::End); + cursor.keepPositionOnInsert(); cursor.insertText(m_fixedEndChars); } + setCursorPosition(finalCursorPos); + cursor.endEditBlock(); + + m_initializingChars = false; } QTextDocument *ChatTextItemHelper::document() const @@ -186,6 +200,10 @@ QTextDocumentFragment ChatTextItemHelper::takeFirstBlock() const auto block = cursor.selection(); cursor.removeSelectedText(); + cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); + if (cursor.selectedText() == QChar::ParagraphSeparator) { + cursor.removeSelectedText(); + } cursor.endEditBlock(); if (document()->characterCount() - 1 <= (m_fixedStartChars.length() + m_fixedEndChars.length())) { Q_EMIT cleared(this); @@ -214,18 +232,29 @@ void ChatTextItemHelper::fillFragments(bool &hasBefore, QTextDocumentFragment &m if (!afterBlock) { cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, m_fixedEndChars.length()); } - cursor.endEditBlock(); midFragment = cursor.selection(); if (!midFragment.isEmpty()) { cursor.removeSelectedText(); } - cursor.deletePreviousChar(); + + cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor); + if (cursor.selectedText() == QChar::ParagraphSeparator) { + cursor.removeSelectedText(); + } else { + cursor.movePosition(QTextCursor::NextCharacter); + } + if (afterBlock) { + cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); + if (cursor.selectedText() == QChar::ParagraphSeparator) { + cursor.removeSelectedText(); + } cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); afterFragment = cursor.selection(); cursor.removeSelectedText(); } + cursor.endEditBlock(); } void ChatTextItemHelper::insertFragment(const QTextDocumentFragment fragment, InsertPosition position, bool keepPosition) @@ -257,16 +286,9 @@ void ChatTextItemHelper::insertFragment(const QTextDocumentFragment fragment, In cursor.setPosition(currentPosition); if (textFormat() && textFormat() == Qt::PlainText) { - const auto wasEmpty = isEmpty(); auto text = fragment.toPlainText(); text = trim(text); cursor.insertText(text); - if (wasEmpty) { - cursor.movePosition(QTextCursor::StartOfBlock); - cursor.deletePreviousChar(); - cursor.movePosition(QTextCursor::EndOfBlock); - cursor.deleteChar(); - } } else { cursor.insertMarkdown(trim(fragment.toMarkdown())); } @@ -276,10 +298,10 @@ void ChatTextItemHelper::insertFragment(const QTextDocumentFragment fragment, In setCursorPosition(cursor.position()); } -int ChatTextItemHelper::cursorPosition() const +std::optional ChatTextItemHelper::cursorPosition() const { if (!m_textItem) { - return -1; + return std::nullopt; } return m_textItem->property("cursorPosition").toInt(); } @@ -311,7 +333,7 @@ QTextCursor ChatTextItemHelper::textCursor() const cursor.setPosition(selectionStart()); cursor.setPosition(selectionEnd(), QTextCursor::KeepAnchor); } else { - cursor.setPosition(cursorPosition()); + cursor.setPosition(*cursorPosition()); } return cursor; } @@ -332,7 +354,7 @@ void ChatTextItemHelper::setCursorVisible(bool visible) m_textItem->setProperty("cursorVisible", visible); } -void ChatTextItemHelper::setCursorFromTextItem(ChatTextItemHelper *textItem, bool infront, int defaultPosition) +void ChatTextItemHelper::setCursorFromTextItem(ChatTextItemHelper *textItem, bool infront) { const auto doc = document(); if (!doc) { @@ -343,37 +365,40 @@ void ChatTextItemHelper::setCursorFromTextItem(ChatTextItemHelper *textItem, boo if (!textItem) { const auto docLastBlockLayout = doc->lastBlock().layout(); - setCursorPosition(infront ? defaultPosition : docLastBlockLayout->lineAt(docLastBlockLayout->lineCount() - 1).textStart()); + setCursorPosition(infront ? 0 : docLastBlockLayout->lineAt(docLastBlockLayout->lineCount() - 1).textStart()); setCursorVisible(true); return; } const auto previousLinePosition = textItem->textCursor().positionInBlock(); const auto newMaxLineLength = lineLength(infront ? 0 : lineCount() - 1); - setCursorPosition(std::min(previousLinePosition, newMaxLineLength ? *newMaxLineLength : defaultPosition) + (infront ? 0 : doc->lastBlock().position())); + setCursorPosition(std::min(previousLinePosition, newMaxLineLength ? *newMaxLineLength : 0) + (infront ? 0 : doc->lastBlock().position())); setCursorVisible(true); } void ChatTextItemHelper::itemCursorPositionChanged() { - Q_EMIT cursorPositionChanged(); + if (m_initializingChars) { + return; + } + const auto currentCursorPosition = cursorPosition(); + if (!currentCursorPosition) { + return; + } + if (*currentCursorPosition < m_fixedStartChars.length() || *currentCursorPosition > document()->characterCount() - 1 - m_fixedEndChars.length()) { + setCursorPosition( + std::min(std::max(*currentCursorPosition, int(m_fixedStartChars.length())), int(document()->characterCount() - 1 - m_fixedEndChars.length()))); + return; + } + + Q_EMIT cursorPositionChanged(m_contentsJustChanged); + m_contentsJustChanged = false; Q_EMIT formatChanged(); Q_EMIT textFormatChanged(); Q_EMIT styleChanged(); Q_EMIT listChanged(); } -QList ChatTextItemHelper::formatsAtCursor(QTextCursor cursor) const -{ - if (cursor.isNull()) { - cursor = textCursor(); - if (cursor.isNull()) { - return {}; - } - } - return RichFormat::formatsAtCursor(cursor); -} - void ChatTextItemHelper::mergeFormatOnCursor(RichFormat::Format format, QTextCursor cursor) { if (cursor.isNull()) { diff --git a/src/libneochat/chattextitemhelper.h b/src/libneochat/chattextitemhelper.h index b93252ad5..e0610ec1e 100644 --- a/src/libneochat/chattextitemhelper.h +++ b/src/libneochat/chattextitemhelper.h @@ -21,10 +21,12 @@ class NeoChatRoom; * * A class to wrap around a QQuickItem that is a QML TextEdit (or inherited from it). * - * @note This basically exists because Qt does not give us access to the cpp headers of - * most QML items. + * This class has 2 key functions: + * - Provide easy read/write access to the properties of the TextEdit. This is required + * because Qt does not give us access to the cpp headers of most QML items. + * - Provide standard functions to edit the underlying QTextDocument. * - * @sa QQuickItem, TextEdit + * @sa QQuickItem, TextEdit, QTextDocument */ class ChatTextItemHelper : public QObject { @@ -45,71 +47,196 @@ public: explicit ChatTextItemHelper(QObject *parent = nullptr); + /** + * @brief Set the NeoChatRoom required by the syntax highlighter. + * + * @sa NeoChatRoom + */ void setRoom(NeoChatRoom *room); + /** + * @brief Set the ChatBarType::Type required by the syntax highlighter. + * + * @sa ChatBarType::Type + */ void setType(ChatBarType::Type type); QQuickItem *textItem() const; void setTextItem(QQuickItem *textItem); + /** + * @brief The fixed characters that will always be at the beginning of the text item. + */ QString fixedStartChars() const; + + /** + * @brief The fixed characters that will always be at the end of the text item. + */ QString fixedEndChars() const; + + /** + * @brief Set the fixed characters that will always be at the beginning and end of the text item. + */ void setFixedChars(const QString &startChars, const QString &endChars); + + /** + * @brief Any text to initialise the text item with when set. + */ QString initialText() const; + + /** + * @brief Set the text to initialise the text item with when set. + * + * This text will only be set if the text item is empty when set. + */ void setInitialText(const QString &text); + /** + * @brief The underlying QTextDocument. + * + * @sa QTextDocument + */ QTextDocument *document() const; + + /** + * @brief The line count of the text item. + */ int lineCount() const; + + /** + * @brief Remove the first QTextBlock from the QTextDocument and return as a QTextDocumentFragment. + * + * @sa QTextBlock, QTextDocument, QTextDocumentFragment + */ QTextDocumentFragment takeFirstBlock(); + + /** + * @brief Fill the given QTextDocumentFragment with the text item contents. + * + * The idea is to split the QTextDocument into 3. There is the QTextBlock that the + * cursor is currently in, the midFragment. Then if there are any blocks after + * this they are put into the afterFragment. The if there is any block before + * the midFragment these are left and hasBefore is set to true. + * + * This is used when inserting a new block type at the cursor. The midFragement will be + * given the new style and then the before and after are put back as the same + * block type. + * + * @sa QTextBlock, QTextDocument, QTextDocumentFragment + */ void fillFragments(bool &hasBefore, QTextDocumentFragment &midFragment, std::optional &afterFragment); + + /** + * @brief Insert the given QTextDocumentFragment as the given position. + */ void insertFragment(const QTextDocumentFragment fragment, InsertPosition position = Cursor, bool keepPosition = false); + /** + * @brief Return a QTextCursor pointing to the current cursor position. + */ QTextCursor textCursor() const; - int cursorPosition() const; - void setCursorPosition(int pos); - void setCursorVisible(bool visible); - void setCursorFromTextItem(ChatTextItemHelper *textItem, bool infront, int defaultPosition = 0); - QList formatsAtCursor(QTextCursor cursor = {}) const; + /** + * @brief Return the current cursor position of the underlying text item. + */ + std::optional cursorPosition() const; + + /** + * @brief Set the cursor position of the underlying text item to the given value. + */ + void setCursorPosition(int pos); + + /** + * @brief Set the cursor visibility of the underlying text item to the given value. + */ + void setCursorVisible(bool visible); + + /** + * @brief Set the cursor position to the same as the given text item. + * + * This will either be the first or last line dependent upon the infront value. + */ + void setCursorFromTextItem(ChatTextItemHelper *textItem, bool infront); + + /** + * @brief Merge the given format on the given QTextCursor. + */ void mergeFormatOnCursor(RichFormat::Format format, QTextCursor cursor = {}); + /** + * @brief Whether the list can be indented more at the given cursor. + */ bool canIndentListMoreAtCursor(QTextCursor cursor = {}) const; + + /** + * @brief Whether the list can be indented less at the given cursor. + */ bool canIndentListLessAtCursor(QTextCursor cursor = {}) const; + + /** + * @brief Indented the list more at the given cursor. + */ void indentListMoreAtCursor(QTextCursor cursor = {}); + + /** + * @brief Indented the list less at the given cursor. + */ void indentListLessAtCursor(QTextCursor cursor = {}); + /** + * @brief Force active focus on the underlying text item. + */ void forceActiveFocus() const; + /** + * @brief Rehightlight the text in the text item. + */ void rehighlight() const; + /** + * @brief Output the text in the text item in markdown format. + */ QString markdownText() const; Q_SIGNALS: void textItemChanged(); - - void contentsChange(int position, int charsRemoved, int charsAdded); - - void contentsChanged(); - - void cleared(ChatTextItemHelper *self); - - void cursorPositionChanged(); - void formatChanged(); void textFormatChanged(); void styleChanged(); void listChanged(); + /** + * @brief Emitted when the contents of the underlying text item are changed. + */ + void contentsChange(int position, int charsRemoved, int charsAdded); + + /** + * @brief Emitted when the contents of the underlying text item are changed. + */ + void contentsChanged(); + + /** + * @brief Emitted when the contents of the underlying text item are cleared. + */ + void cleared(ChatTextItemHelper *self); + + /** + * @brief Emitted when the cursor position of the underlying text item is changed. + */ + void cursorPositionChanged(bool fromContentsChange); + private: QPointer m_textItem; QPointer m_highlighter; + bool m_contentsJustChanged = false; std::optional textFormat() const; QString m_fixedStartChars = {}; QString m_fixedEndChars = {}; QString m_initialText = {}; void initializeChars(); + bool m_initializingChars = false; bool isEmpty() const; std::optional lineLength(int lineNumber) const; diff --git a/src/messagecontent/QuoteComponent.qml b/src/messagecontent/QuoteComponent.qml index e407f4aeb..810f59d3f 100644 --- a/src/messagecontent/QuoteComponent.qml +++ b/src/messagecontent/QuoteComponent.qml @@ -71,20 +71,6 @@ QQC2.TextArea { event.accepted = true; Message.contentModel.keyHelper.down(); } - Keys.onLeftPressed: (event) => { - if (cursorPosition == 1) { - event.accepted = true; - } else { - event.accepted = false; - } - } - Keys.onRightPressed: (event) => { - if (cursorPosition == (length - 1)) { - event.accepted = true; - return; - } - event.accepted = false; - } Keys.onDeletePressed: (event) => { event.accepted = true; @@ -123,12 +109,6 @@ QQC2.TextArea { Message.contentModel.setFocusRow(root.index, true) } - onCursorPositionChanged: if (cursorPosition == 0) { - cursorPosition = 1; - } else if (cursorPosition == length) { - cursorPosition = length - 1; - } - TapHandler { enabled: !root.hoveredLink acceptedDevices: PointerDevice.TouchScreen diff --git a/src/messagecontent/models/chatbarmessagecontentmodel.cpp b/src/messagecontent/models/chatbarmessagecontentmodel.cpp index 74080ad59..b21ee9126 100644 --- a/src/messagecontent/models/chatbarmessagecontentmodel.cpp +++ b/src/messagecontent/models/chatbarmessagecontentmodel.cpp @@ -191,7 +191,7 @@ void ChatBarMessageContentModel::focusCurrentComponent(const QModelIndex &previo return; } - textItem->setCursorFromTextItem(textItemForIndex(previousIndex), down, MessageComponentType::Quote ? 1 : 0); + textItem->setCursorFromTextItem(textItemForIndex(previousIndex), down); } void ChatBarMessageContentModel::refocusCurrentComponent() const