Add more tests
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
96
autotests/chatkeyhelpertest.qml
Normal file
96
autotests/chatkeyhelpertest.qml
Normal file
@@ -0,0 +1,96 @@
|
||||
// SPDX-FileCopyrightText: 2024 Carl Schwan <carl@carlschwan.eu>
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
57
autotests/chatkeyhelpertesthelper.h
Normal file
57
autotests/chatkeyhelpertesthelper.h
Normal file
@@ -0,0 +1,57 @@
|
||||
// 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 <QTextDocumentFragment>
|
||||
#include <qquickitem.h>
|
||||
|
||||
#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<ChatTextItemHelper> m_textItem;
|
||||
QPointer<ChatKeyHelper> m_keyHelper;
|
||||
};
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
215
autotests/chattextitemhelpertesthelper.h
Normal file
215
autotests/chattextitemhelpertesthelper.h
Normal file
@@ -0,0 +1,215 @@
|
||||
// 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 <QTextDocumentFragment>
|
||||
#include <qquickitem.h>
|
||||
|
||||
#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<QTextDocumentFragment> 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<RichFormat::Format> 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<ChatTextItemHelper> m_textItem;
|
||||
};
|
||||
@@ -1,89 +0,0 @@
|
||||
// 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 "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<ChatTextItemHelper> m_textItemWrapper;
|
||||
};
|
||||
@@ -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 +
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ const QList<MarkdownSyntax> 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<bool> 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();
|
||||
|
||||
@@ -47,6 +47,7 @@ private:
|
||||
State m_currentState = None;
|
||||
int m_startPos = 0;
|
||||
int m_endPos = 0;
|
||||
void updateStart();
|
||||
|
||||
QHash<RichFormat::Format, int> m_currentFormats;
|
||||
|
||||
|
||||
@@ -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<int> 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<RichFormat::Format> 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()) {
|
||||
|
||||
@@ -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<QTextDocumentFragment> &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<RichFormat::Format> formatsAtCursor(QTextCursor cursor = {}) const;
|
||||
/**
|
||||
* @brief Return the current cursor position of the underlying text item.
|
||||
*/
|
||||
std::optional<int> 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<QQuickItem> m_textItem;
|
||||
QPointer<ChatBarSyntaxHighlighter> m_highlighter;
|
||||
|
||||
bool m_contentsJustChanged = false;
|
||||
std::optional<Qt::TextFormat> textFormat() const;
|
||||
|
||||
QString m_fixedStartChars = {};
|
||||
QString m_fixedEndChars = {};
|
||||
QString m_initialText = {};
|
||||
void initializeChars();
|
||||
bool m_initializingChars = false;
|
||||
|
||||
bool isEmpty() const;
|
||||
std::optional<int> lineLength(int lineNumber) const;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user