Add tests for ChatMarkdownHelper and rework how formats are applied to make it more robust.
This commit is contained in:
@@ -120,7 +120,10 @@ macro(add_qml_tests)
|
||||
endforeach()
|
||||
endmacro()
|
||||
|
||||
add_executable(qmltest qmltest.cpp qmltextitemwrappertestwrapper.h)
|
||||
add_executable(qmltest qmltest.cpp
|
||||
chatmarkdownhelpertestwrapper.h
|
||||
qmltextitemwrappertestwrapper.h
|
||||
)
|
||||
qt_add_qml_module(qmltest URI NeoChatTestUtils)
|
||||
|
||||
target_link_libraries(qmltest
|
||||
@@ -133,5 +136,6 @@ target_link_libraries(qmltest
|
||||
|
||||
add_qml_tests(
|
||||
chatdocumenthelpertest.qml
|
||||
chatmarkdownhelpertest.qml
|
||||
qmltextitemwrappertest.qml
|
||||
)
|
||||
|
||||
102
autotests/chatmarkdownhelpertest.qml
Normal file
102
autotests/chatmarkdownhelpertest.qml
Normal file
@@ -0,0 +1,102 @@
|
||||
// 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: "ChatMarkdownHelperTest"
|
||||
|
||||
TextEdit {
|
||||
id: textEdit
|
||||
|
||||
textFormat: TextEdit.RichText
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
id: textEdit2
|
||||
}
|
||||
|
||||
ChatMarkdownHelperTestWrapper {
|
||||
id: chatMarkdownHelper
|
||||
|
||||
textItem: textEdit
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: spyItem
|
||||
target: chatMarkdownHelper
|
||||
signalName: "textItemChanged"
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: spyUnhandledFormat
|
||||
target: chatMarkdownHelper
|
||||
signalName: "unhandledBlockFormat"
|
||||
}
|
||||
|
||||
function initTestCase(): void {
|
||||
textEdit.forceActiveFocus();
|
||||
}
|
||||
|
||||
function cleanup(): void {
|
||||
chatMarkdownHelper.clear();
|
||||
compare(chatMarkdownHelper.checkText(""), true);
|
||||
compare(chatMarkdownHelper.checkFormats([]), true);
|
||||
compare(textEdit.cursorPosition, 0);
|
||||
}
|
||||
|
||||
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() {
|
||||
return [
|
||||
{tag: "bold", input: "**b** ", outText: ["*", "**", "b", "b*", "b**", "b "], outFormats: [[], [], [RichFormat.Bold], [RichFormat.Bold], [RichFormat.Bold], []], unhandled: 0},
|
||||
{tag: "italic", input: "*i* ", outText: ["*", "i", "i*", "i "], outFormats: [[], [RichFormat.Italic], [RichFormat.Italic], []], unhandled: 0},
|
||||
{tag: "heading 1", input: "# h", outText: ["#", "# ", "h"], outFormats: [[], [], [RichFormat.Bold, RichFormat.Heading1]], unhandled: 0},
|
||||
{tag: "heading 2", input: "## h", outText: ["#", "##", "## ", "h"], outFormats: [[], [], [], [RichFormat.Bold, RichFormat.Heading2]], unhandled: 0},
|
||||
{tag: "heading 3", input: "### h", outText: ["#", "##", "###", "### ", "h"], outFormats: [[], [], [], [], [RichFormat.Bold, RichFormat.Heading3]], unhandled: 0},
|
||||
{tag: "heading 4", input: "#### h", outText: ["#", "##", "###", "####", "#### ", "h"], outFormats: [[], [], [], [], [], [RichFormat.Bold, RichFormat.Heading4]], unhandled: 0},
|
||||
{tag: "heading 5", input: "##### h", outText: ["#", "##", "###", "####", "#####", "##### ", "h"], outFormats: [[], [], [], [], [], [], [RichFormat.Bold, RichFormat.Heading5]], unhandled: 0},
|
||||
{tag: "heading 6", input: "###### h", outText: ["#", "##", "###", "####", "#####", "######", "###### ", "h"], outFormats: [[], [], [], [], [], [] ,[], [RichFormat.Bold, RichFormat.Heading6]], unhandled: 0},
|
||||
{tag: "quote", input: "> q", outText: [">", "> ", "q"], outFormats: [[], [], []], unhandled: 1},
|
||||
{tag: "unorderedlist 1", input: "* l", outText: ["*", "* ", "l"], outFormats: [[], [], [RichFormat.UnorderedList]], unhandled: 0},
|
||||
{tag: "unorderedlist 2", input: "- l", outText: ["-", "- ", "l"], outFormats: [[], [], [RichFormat.UnorderedList]], unhandled: 0},
|
||||
{tag: "orderedlist 1", input: "1. l", outText: ["1", "1.", "1. ", "l"], outFormats: [[], [], [], [RichFormat.OrderedList]], unhandled: 0},
|
||||
{tag: "orderedlist 2", input: "1) l", outText: ["1", "1)", "1) ", "l"], outFormats: [[], [], [], [RichFormat.OrderedList]], unhandled: 0},
|
||||
{tag: "inline code", input: "`c` ", outText: ["`", "c", "c`", "c "], outFormats: [[], [RichFormat.InlineCode], [RichFormat.InlineCode], []], unhandled: 0},
|
||||
{tag: "code", input: "``` ", outText: ["`", "``", "```", " "], outFormats: [[], [], [], []], unhandled: 1},
|
||||
{tag: "strikethrough", input: "~~s~~ ", outText: ["~", "~~", "s", "s~", "s~~", "s "], outFormats: [[], [], [RichFormat.Strikethrough], [RichFormat.Strikethrough], [RichFormat.Strikethrough], []], unhandled: 0},
|
||||
{tag: "underline", input: "__u__ ", outText: ["_", "__", "u", "u_", "u__", "u "], outFormats: [[], [], [RichFormat.Underline], [RichFormat.Underline], [RichFormat.Underline], []], unhandled: 0},
|
||||
{tag: "multiple closable", input: "***__~~t~~__*** ", outText: ["*", "**", "*", "_", "__", "~", "~~", "t", "t~", "t~~", "t_", "t__", "t*", "t**", "t*", "t "], outFormats: [[], [], [RichFormat.Bold], [RichFormat.Bold, RichFormat.Italic], [RichFormat.Bold, RichFormat.Italic], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline, RichFormat.Strikethrough], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline, RichFormat.Strikethrough], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline, RichFormat.Strikethrough], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline], [RichFormat.Bold, RichFormat.Italic], [RichFormat.Bold, RichFormat.Italic], [RichFormat.Italic], []], unhandled: 0},
|
||||
{tag: "nonclosable closable", input: "* **b** ", outText: ["*", "* ", "*", "**", "b", "b*", "b**", "b "], outFormats: [[], [], [RichFormat.UnorderedList], [RichFormat.UnorderedList], [RichFormat.Bold, RichFormat.UnorderedList], [RichFormat.Bold, RichFormat.UnorderedList], [RichFormat.Bold, RichFormat.UnorderedList], [RichFormat.UnorderedList]], unhandled: 0},
|
||||
{tag: "not at line start", input: " 1) ", outText: [" ", " 1", " 1)", " 1) "], outFormats: [[], [], [], []], unhandled: 0},
|
||||
]
|
||||
}
|
||||
|
||||
function test_textFormat(data): void {
|
||||
spyUnhandledFormat.clear();
|
||||
compare(spyUnhandledFormat.count, 0);
|
||||
|
||||
for (let i = 0; i < data.input.length; i++) {
|
||||
keyClick(data.input[i]);
|
||||
compare(chatMarkdownHelper.checkText(data.outText[i]), true);
|
||||
compare(chatMarkdownHelper.checkFormats(data.outFormats[i]), true);
|
||||
}
|
||||
|
||||
compare(spyUnhandledFormat.count, data.unhandled);
|
||||
}
|
||||
}
|
||||
84
autotests/chatmarkdownhelpertestwrapper.h
Normal file
84
autotests/chatmarkdownhelpertestwrapper.h
Normal file
@@ -0,0 +1,84 @@
|
||||
// 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 <QTextCursor>
|
||||
#include <qtextcursor.h>
|
||||
|
||||
#include "chatmarkdownhelper.h"
|
||||
#include "enums/richformat.h"
|
||||
#include "qmltextitemwrapper.h"
|
||||
|
||||
class ChatMarkdownHelperTestWrapper : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
/**
|
||||
* @brief The QML text Item the ChatDocumentHandler is handling.
|
||||
*/
|
||||
Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
|
||||
|
||||
public:
|
||||
explicit ChatMarkdownHelperTestWrapper(QObject *parent = nullptr)
|
||||
: QObject(parent)
|
||||
, m_chatMarkdownHelper(new ChatMarkdownHelper(this))
|
||||
, m_textItem(new QmlTextItemWrapper(this))
|
||||
{
|
||||
connect(m_chatMarkdownHelper, &ChatMarkdownHelper::textItemChanged, this, &ChatMarkdownHelperTestWrapper::textItemChanged);
|
||||
connect(m_chatMarkdownHelper, &ChatMarkdownHelper::unhandledBlockFormat, this, &ChatMarkdownHelperTestWrapper::unhandledBlockFormat);
|
||||
}
|
||||
|
||||
QQuickItem *textItem() const
|
||||
{
|
||||
return m_chatMarkdownHelper->textItem();
|
||||
}
|
||||
void setTextItem(QQuickItem *textItem)
|
||||
{
|
||||
m_chatMarkdownHelper->setTextItem(textItem);
|
||||
m_textItem->setTextItem(textItem);
|
||||
}
|
||||
|
||||
Q_INVOKABLE bool checkText(const QString &text)
|
||||
{
|
||||
const auto doc = m_textItem->document();
|
||||
if (!doc) {
|
||||
return false;
|
||||
}
|
||||
qWarning() << text << doc->toPlainText();
|
||||
return text == doc->toPlainText();
|
||||
}
|
||||
|
||||
Q_INVOKABLE bool checkFormats(QList<RichFormat::Format> formats)
|
||||
{
|
||||
const auto cursor = m_textItem->textCursor();
|
||||
if (cursor.isNull()) {
|
||||
return false;
|
||||
}
|
||||
qWarning() << RichFormat::formatsAtCursor(cursor) << formats;
|
||||
return RichFormat::formatsAtCursor(cursor) == formats;
|
||||
}
|
||||
|
||||
Q_INVOKABLE void clear()
|
||||
{
|
||||
auto cursor = m_textItem->textCursor();
|
||||
if (cursor.isNull()) {
|
||||
return;
|
||||
}
|
||||
cursor.select(QTextCursor::Document);
|
||||
cursor.removeSelectedText();
|
||||
cursor.setBlockCharFormat(RichFormat::charFormatForFormat(RichFormat::Paragraph));
|
||||
cursor.setBlockFormat(RichFormat::blockFormatForFormat(RichFormat::Paragraph));
|
||||
}
|
||||
|
||||
Q_SIGNALS:
|
||||
void textItemChanged();
|
||||
void unhandledBlockFormat(RichFormat::Format format);
|
||||
|
||||
private:
|
||||
QPointer<ChatMarkdownHelper> m_chatMarkdownHelper;
|
||||
QPointer<QmlTextItemWrapper> m_textItem;
|
||||
};
|
||||
@@ -70,6 +70,7 @@ TestCase {
|
||||
spyCursor.clear();
|
||||
// We can't get to the QTextCursor from QML so we have to use a helper function.
|
||||
compare(qmlTextItemWrapper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
|
||||
compare(textEdit.cursorPosition, qmlTextItemWrapper.cursorPosition());
|
||||
textEdit.insert(0, "test text")
|
||||
compare(spyContentsChange.count, 1);
|
||||
compare(spyContentsChange.signalArguments[0][0], 0);
|
||||
@@ -78,10 +79,12 @@ TestCase {
|
||||
compare(spyContentsChanged.count, 1);
|
||||
compare(spyCursor.count, 1);
|
||||
compare(qmlTextItemWrapper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
|
||||
compare(textEdit.cursorPosition, qmlTextItemWrapper.cursorPosition());
|
||||
textEdit.selectAll();
|
||||
compare(spyContentsChanged.count, 1);
|
||||
compare(spyCursor.count, 1);
|
||||
compare(qmlTextItemWrapper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
|
||||
compare(textEdit.cursorPosition, qmlTextItemWrapper.cursorPosition());
|
||||
textEdit.clear();
|
||||
compare(spyContentsChange.count, 2);
|
||||
compare(spyContentsChange.signalArguments[1][0], 0);
|
||||
|
||||
@@ -61,6 +61,11 @@ public:
|
||||
return posSame && startSame && endSame;
|
||||
}
|
||||
|
||||
Q_INVOKABLE int cursorPosition() const
|
||||
{
|
||||
return m_textItemWrapper->cursorPosition();
|
||||
}
|
||||
|
||||
Q_INVOKABLE void setCursorPosition(int pos)
|
||||
{
|
||||
m_textItemWrapper->setCursorPosition(pos);
|
||||
|
||||
@@ -136,6 +136,8 @@ ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
|
||||
, m_markdownHelper(new ChatMarkdownHelper(this))
|
||||
, m_highlighter(new SyntaxHighlighter(this))
|
||||
{
|
||||
connectTextItem();
|
||||
|
||||
connect(this, &ChatDocumentHandler::formatChanged, m_markdownHelper, &ChatMarkdownHelper::handleExternalFormatChange);
|
||||
}
|
||||
|
||||
@@ -176,6 +178,7 @@ QQuickItem *ChatDocumentHandler::textItem() const
|
||||
void ChatDocumentHandler::setTextItem(QQuickItem *textItem)
|
||||
{
|
||||
m_textItem->setTextItem(textItem);
|
||||
m_markdownHelper->setTextItem(textItem);
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::connectTextItem()
|
||||
@@ -209,6 +212,10 @@ void ChatDocumentHandler::connectTextItem()
|
||||
}
|
||||
}
|
||||
});
|
||||
connect(m_textItem, &QmlTextItemWrapper::formatChanged, this, &ChatDocumentHandler::formatChanged);
|
||||
connect(m_textItem, &QmlTextItemWrapper::textFormatChanged, this, &ChatDocumentHandler::textFormatChanged);
|
||||
connect(m_textItem, &QmlTextItemWrapper::styleChanged, this, &ChatDocumentHandler::styleChanged);
|
||||
connect(m_textItem, &QmlTextItemWrapper::listChanged, this, &ChatDocumentHandler::listChanged);
|
||||
}
|
||||
|
||||
ChatDocumentHandler *ChatDocumentHandler::previousDocumentHandler() const
|
||||
@@ -756,65 +763,36 @@ void ChatDocumentHandler::regenerateColorScheme()
|
||||
|
||||
void ChatDocumentHandler::setFormat(RichFormat::Format format)
|
||||
{
|
||||
switch (RichFormat::typeForFormat(format)) {
|
||||
case RichFormat::Text:
|
||||
setTextFormat(format);
|
||||
return;
|
||||
case RichFormat::List:
|
||||
setListFormat(format);
|
||||
return;
|
||||
case RichFormat::Style:
|
||||
setStyleFormat(format);
|
||||
return;
|
||||
default:
|
||||
QTextCursor cursor = m_textItem->textCursor();
|
||||
if (cursor.isNull()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::indentListMore()
|
||||
{
|
||||
m_nestedListHelper.handleOnIndentMore(m_textItem->textCursor());
|
||||
Q_EMIT currentListStyleChanged();
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::indentListLess()
|
||||
{
|
||||
m_nestedListHelper.handleOnIndentLess(m_textItem->textCursor());
|
||||
Q_EMIT currentListStyleChanged();
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::setListFormat(RichFormat::Format format)
|
||||
{
|
||||
m_nestedListHelper.handleOnBulletType(RichFormat::listStyleForFormat(format), m_textItem->textCursor());
|
||||
Q_EMIT currentListStyleChanged();
|
||||
}
|
||||
|
||||
bool ChatDocumentHandler::canIndentList() const
|
||||
{
|
||||
return m_nestedListHelper.canIndent(m_textItem->textCursor()) && m_textItem->textCursor().blockFormat().headingLevel() == 0;
|
||||
}
|
||||
|
||||
bool ChatDocumentHandler::canDedentList() const
|
||||
{
|
||||
return m_nestedListHelper.canDedent(m_textItem->textCursor()) && m_textItem->textCursor().blockFormat().headingLevel() == 0;
|
||||
m_textItem->mergeFormatOnCursor(format, cursor);
|
||||
}
|
||||
|
||||
int ChatDocumentHandler::currentListStyle() const
|
||||
{
|
||||
if (!m_textItem->textCursor().currentList()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return -m_textItem->textCursor().currentList()->format().style();
|
||||
return m_textItem->currentListStyle();
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::setTextFormat(RichFormat::Format format)
|
||||
bool ChatDocumentHandler::canIndentListMore() const
|
||||
{
|
||||
if (RichFormat::typeForFormat(format) != RichFormat::Text) {
|
||||
return;
|
||||
}
|
||||
mergeFormatOnWordOrSelection(RichFormat::charFormatForFormat(format, RichFormat::hasFormat(m_textItem->textCursor(), format)));
|
||||
Q_EMIT formatChanged();
|
||||
return m_textItem->canIndentListMore();
|
||||
}
|
||||
|
||||
bool ChatDocumentHandler::canIndentListLess() const
|
||||
{
|
||||
return m_textItem->canIndentListLess();
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::indentListMore()
|
||||
{
|
||||
m_textItem->indentListMore();
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::indentListLess()
|
||||
{
|
||||
m_textItem->indentListLess();
|
||||
}
|
||||
|
||||
RichFormat::Format ChatDocumentHandler::style() const
|
||||
@@ -822,48 +800,6 @@ RichFormat::Format ChatDocumentHandler::style() const
|
||||
return static_cast<RichFormat::Format>(m_textItem->textCursor().blockFormat().headingLevel());
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::setStyleFormat(RichFormat::Format format)
|
||||
{
|
||||
// Paragraph is special because it is normally a Block format but if we're already
|
||||
// in a Paragraph it clears any existing style.
|
||||
if (!(RichFormat::typeForFormat(format) == RichFormat::Style || format == RichFormat::Paragraph)) {
|
||||
return;
|
||||
}
|
||||
|
||||
QTextCursor cursor = m_textItem->textCursor();
|
||||
if (cursor.isNull()) {
|
||||
return;
|
||||
}
|
||||
cursor.beginEditBlock();
|
||||
|
||||
cursor.mergeBlockFormat(RichFormat::blockFormatForFormat(format));
|
||||
|
||||
// Applying style to the current line or selection
|
||||
QTextCursor selectCursor = cursor;
|
||||
if (selectCursor.hasSelection()) {
|
||||
QTextCursor top = selectCursor;
|
||||
top.setPosition(qMin(top.anchor(), top.position()));
|
||||
top.movePosition(QTextCursor::StartOfBlock);
|
||||
|
||||
QTextCursor bottom = selectCursor;
|
||||
bottom.setPosition(qMax(bottom.anchor(), bottom.position()));
|
||||
bottom.movePosition(QTextCursor::EndOfBlock);
|
||||
|
||||
selectCursor.setPosition(top.position(), QTextCursor::MoveAnchor);
|
||||
selectCursor.setPosition(bottom.position(), QTextCursor::KeepAnchor);
|
||||
} else {
|
||||
selectCursor.select(QTextCursor::BlockUnderCursor);
|
||||
}
|
||||
|
||||
const auto chrfmt = RichFormat::charFormatForFormat(format);
|
||||
selectCursor.mergeCharFormat(chrfmt);
|
||||
cursor.mergeBlockCharFormat(chrfmt);
|
||||
cursor.endEditBlock();
|
||||
|
||||
Q_EMIT formatChanged();
|
||||
Q_EMIT styleChanged();
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::tab()
|
||||
{
|
||||
QTextCursor cursor = m_textItem->textCursor();
|
||||
|
||||
@@ -97,14 +97,14 @@ class ChatDocumentHandler : public QObject
|
||||
|
||||
Q_PROPERTY(QColor textColor READ textColor WRITE setTextColor NOTIFY textColorChanged)
|
||||
|
||||
Q_PROPERTY(bool bold READ bold NOTIFY formatChanged)
|
||||
Q_PROPERTY(bool italic READ italic NOTIFY formatChanged)
|
||||
Q_PROPERTY(bool underline READ underline NOTIFY formatChanged)
|
||||
Q_PROPERTY(bool strikethrough READ strikethrough NOTIFY formatChanged)
|
||||
Q_PROPERTY(bool bold READ bold NOTIFY textFormatChanged)
|
||||
Q_PROPERTY(bool italic READ italic NOTIFY textFormatChanged)
|
||||
Q_PROPERTY(bool underline READ underline NOTIFY textFormatChanged)
|
||||
Q_PROPERTY(bool strikethrough READ strikethrough NOTIFY textFormatChanged)
|
||||
|
||||
Q_PROPERTY(RichFormat::Format style READ style NOTIFY styleChanged)
|
||||
|
||||
Q_PROPERTY(int currentListStyle READ currentListStyle NOTIFY currentListStyleChanged)
|
||||
Q_PROPERTY(int currentListStyle READ currentListStyle NOTIFY listChanged)
|
||||
|
||||
public:
|
||||
enum InsertPosition {
|
||||
@@ -161,11 +161,10 @@ public:
|
||||
bool strikethrough() const;
|
||||
|
||||
Q_INVOKABLE void setFormat(RichFormat::Format format);
|
||||
void setFormatOnCursor(RichFormat::Format format, const QTextCursor &cursor);
|
||||
|
||||
bool canIndentList() const;
|
||||
bool canDedentList() const;
|
||||
int currentListStyle() const;
|
||||
bool canIndentListMore() const;
|
||||
bool canIndentListLess() const;
|
||||
Q_INVOKABLE void indentListLess();
|
||||
Q_INVOKABLE void indentListMore();
|
||||
|
||||
@@ -195,11 +194,12 @@ Q_SIGNALS:
|
||||
|
||||
void textColorChanged();
|
||||
|
||||
void checkableChanged();
|
||||
void currentListStyleChanged();
|
||||
|
||||
void formatChanged();
|
||||
void textFormatChanged();
|
||||
void styleChanged();
|
||||
void listChanged();
|
||||
|
||||
void contentsChanged();
|
||||
|
||||
@@ -220,10 +220,6 @@ private:
|
||||
QString m_initialText = {};
|
||||
void initializeChars();
|
||||
|
||||
void setTextFormat(RichFormat::Format format);
|
||||
void setStyleFormat(RichFormat::Format format);
|
||||
void setListFormat(RichFormat::Format format);
|
||||
|
||||
QPointer<ChatMarkdownHelper> m_markdownHelper;
|
||||
std::optional<QTextCharFormat> m_pendingFormat = std::nullopt;
|
||||
std::optional<QTextCharFormat> m_pendingOverrideFormat = std::nullopt;
|
||||
@@ -236,7 +232,6 @@ private:
|
||||
std::optional<Qt::TextFormat> textFormat() const;
|
||||
void mergeFormatOnWordOrSelection(const QTextCharFormat &format);
|
||||
void selectLinkText(QTextCursor *cursor) const;
|
||||
NestedListHelper m_nestedListHelper;
|
||||
QColor linkColor();
|
||||
QColor mLinkColor;
|
||||
void regenerateColorScheme();
|
||||
|
||||
@@ -3,11 +3,15 @@
|
||||
|
||||
#include "chatmarkdownhelper.h"
|
||||
|
||||
#include <QTextCursor>
|
||||
#include <QTextDocument>
|
||||
#include <qtextcursor.h>
|
||||
|
||||
#include "chatdocumenthandler.h"
|
||||
#include "qmltextitemwrapper.h"
|
||||
#include "richformat.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
struct MarkdownSyntax {
|
||||
QLatin1String sequence;
|
||||
bool closable = false;
|
||||
@@ -15,7 +19,7 @@ struct MarkdownSyntax {
|
||||
RichFormat::Format format;
|
||||
};
|
||||
|
||||
static const QList<MarkdownSyntax> syntax = {
|
||||
const QList<MarkdownSyntax> syntax = {
|
||||
MarkdownSyntax{.sequence = "*"_L1, .closable = true, .format = RichFormat::Italic},
|
||||
MarkdownSyntax{.sequence = "**"_L1, .closable = true, .format = RichFormat::Bold},
|
||||
MarkdownSyntax{.sequence = "# "_L1, .lineStart = true, .format = RichFormat::Heading1},
|
||||
@@ -35,7 +39,7 @@ static const QList<MarkdownSyntax> syntax = {
|
||||
MarkdownSyntax{.sequence = "__"_L1, .closable = true, .format = RichFormat::Underline},
|
||||
};
|
||||
|
||||
static std::optional<bool> checkSequence(const QString ¤tString, const QString &nextChar, bool lineStart = false)
|
||||
std::optional<bool> checkSequence(const QString ¤tString, const QString &nextChar, bool lineStart = false)
|
||||
{
|
||||
QList<MarkdownSyntax> partialMatches;
|
||||
std::optional<MarkdownSyntax> fullMatch = std::nullopt;
|
||||
@@ -65,7 +69,7 @@ static std::optional<bool> checkSequence(const QString ¤tString, const QSt
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static std::optional<MarkdownSyntax> syntaxForSequence(const QString &sequence)
|
||||
std::optional<MarkdownSyntax> syntaxForSequence(const QString &sequence)
|
||||
{
|
||||
const auto it = std::find_if(syntax.cbegin(), syntax.cend(), [sequence](const MarkdownSyntax &syntax) {
|
||||
return syntax.sequence == sequence;
|
||||
@@ -75,55 +79,41 @@ static std::optional<MarkdownSyntax> syntaxForSequence(const QString &sequence)
|
||||
}
|
||||
return *it;
|
||||
}
|
||||
}
|
||||
|
||||
ChatMarkdownHelper::ChatMarkdownHelper(ChatDocumentHandler *parent)
|
||||
ChatMarkdownHelper::ChatMarkdownHelper(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_textItem(new QmlTextItemWrapper(this))
|
||||
{
|
||||
Q_ASSERT(parent);
|
||||
|
||||
connectDocument();
|
||||
connect(parent, &ChatDocumentHandler::textItemChanged, this, &ChatMarkdownHelper::connectDocument);
|
||||
connectTextItem();
|
||||
}
|
||||
|
||||
QTextDocument *ChatMarkdownHelper::document() const
|
||||
QQuickItem *ChatMarkdownHelper::textItem() const
|
||||
{
|
||||
const auto documentHandler = qobject_cast<ChatDocumentHandler *>(parent());
|
||||
if (!documentHandler) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!documentHandler->textItem()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto quickDocument = qvariant_cast<QQuickTextDocument *>(documentHandler->textItem()->property("textDocument"));
|
||||
return quickDocument ? quickDocument->textDocument() : nullptr;
|
||||
return m_textItem->textItem();
|
||||
}
|
||||
|
||||
void ChatMarkdownHelper::connectDocument()
|
||||
void ChatMarkdownHelper::setTextItem(QQuickItem *textItem)
|
||||
{
|
||||
disconnect();
|
||||
m_textItem->setTextItem(textItem);
|
||||
}
|
||||
|
||||
if (document()) {
|
||||
m_startPos = qobject_cast<ChatDocumentHandler *>(parent())->textItem()->property("cursorPosition").toInt();
|
||||
void ChatMarkdownHelper::connectTextItem()
|
||||
{
|
||||
connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, &ChatMarkdownHelper::textItemChanged);
|
||||
connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, [this]() {
|
||||
m_startPos = m_textItem->cursorPosition();
|
||||
m_endPos = m_startPos;
|
||||
if (m_startPos == 0) {
|
||||
m_currentState = Pre;
|
||||
}
|
||||
|
||||
connect(document(), &QTextDocument::contentsChange, this, &ChatMarkdownHelper::checkMarkdown);
|
||||
}
|
||||
});
|
||||
connect(m_textItem, &QmlTextItemWrapper::textDocumentContentsChange, this, &ChatMarkdownHelper::checkMarkdown);
|
||||
}
|
||||
|
||||
void ChatMarkdownHelper::checkMarkdown(int position, int charsRemoved, int charsAdded)
|
||||
{
|
||||
qWarning() << "1" << m_currentState << m_startPos << m_endPos;
|
||||
|
||||
if (!document()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto cursor = QTextCursor(document());
|
||||
auto cursor = m_textItem->textCursor();
|
||||
if (cursor.isNull()) {
|
||||
return;
|
||||
}
|
||||
@@ -137,7 +127,6 @@ void ChatMarkdownHelper::checkMarkdown(int position, int charsRemoved, int chars
|
||||
cursor.setPosition(m_endPos + (cursor.atBlockEnd() ? 0 : 1), QTextCursor::KeepAnchor);
|
||||
const auto nextChar = cursor.selectedText();
|
||||
m_currentState = m_startPos == 0 || nextChar == u' ' ? Pre : None;
|
||||
qWarning() << "2" << m_currentState << m_startPos << m_endPos;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -151,7 +140,7 @@ void ChatMarkdownHelper::checkMarkdown(int position, int charsRemoved, int chars
|
||||
cursor.setPosition(m_startPos);
|
||||
|
||||
const auto result = checkSequence(currentMarkdown, nextChar, cursor.atBlockStart());
|
||||
qWarning() << result;
|
||||
qWarning() << m_startPos << m_endPos << result;
|
||||
|
||||
switch (m_currentState) {
|
||||
case None:
|
||||
@@ -186,16 +175,15 @@ void ChatMarkdownHelper::checkMarkdown(int position, int charsRemoved, int chars
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
qWarning() << "2" << m_currentState << m_startPos << m_endPos;
|
||||
}
|
||||
|
||||
void ChatMarkdownHelper::complete()
|
||||
{
|
||||
auto cursor = QTextCursor(document());
|
||||
auto cursor = m_textItem->textCursor();
|
||||
if (cursor.isNull()) {
|
||||
return;
|
||||
}
|
||||
cursor.beginEditBlock();
|
||||
cursor.setPosition(m_startPos);
|
||||
cursor.setPosition(m_endPos, QTextCursor::KeepAnchor);
|
||||
const auto syntax = syntaxForSequence(cursor.selectedText());
|
||||
@@ -207,29 +195,38 @@ void ChatMarkdownHelper::complete()
|
||||
m_currentFormats.insert(syntax->format, m_startPos);
|
||||
}
|
||||
|
||||
++m_startPos;
|
||||
cursor.setPosition(m_startPos);
|
||||
cursor.setPosition(m_startPos + 1, QTextCursor::KeepAnchor);
|
||||
const auto nextChar = cursor.selectedText();
|
||||
const auto result = checkSequence({}, nextChar, cursor.atBlockStart());
|
||||
m_currentState = result ? Started : Pre;
|
||||
|
||||
const auto documentHandler = qobject_cast<ChatDocumentHandler *>(parent());
|
||||
// cursor.setPosition(m_startPos + 1);
|
||||
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
|
||||
if (syntax) {
|
||||
documentHandler->textItem()->setProperty("cursorPosition", m_startPos);
|
||||
documentHandler->setFormat(syntax->format);
|
||||
const auto formatType = RichFormat::typeForFormat(syntax->format);
|
||||
if (formatType == RichFormat::Block) {
|
||||
Q_EMIT unhandledBlockFormat(syntax->format);
|
||||
} else {
|
||||
m_textItem->mergeFormatOnCursor(syntax->format, cursor);
|
||||
}
|
||||
}
|
||||
|
||||
m_currentState = Pre;
|
||||
m_endPos = m_startPos;
|
||||
m_startPos = result ? m_startPos : m_startPos + 1;
|
||||
m_endPos = result ? m_startPos + 1 : m_startPos;
|
||||
|
||||
documentHandler->textItem()->setProperty("cursorPosition", m_startPos);
|
||||
cursor.endEditBlock();
|
||||
qWarning() << m_currentState << m_startPos << m_endPos << m_textItem->cursorPosition();
|
||||
}
|
||||
|
||||
void ChatMarkdownHelper::handleExternalFormatChange()
|
||||
{
|
||||
auto cursor = QTextCursor(document());
|
||||
auto cursor = m_textItem->textCursor();
|
||||
if (cursor.isNull()) {
|
||||
return;
|
||||
}
|
||||
cursor.setPosition(m_startPos);
|
||||
m_currentState = RichFormat::formatsAtCursor(cursor).length() > 0 ? Pre : None;
|
||||
qWarning() << "3" << m_currentState << m_startPos << m_endPos;
|
||||
}
|
||||
|
||||
#include "moc_chatmarkdownhelper.cpp"
|
||||
|
||||
@@ -4,24 +4,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
|
||||
#include "enums/richformat.h"
|
||||
|
||||
class QQuickItem;
|
||||
class QTextDocument;
|
||||
|
||||
class ChatDocumentHandler;
|
||||
class QmlTextItemWrapper;
|
||||
|
||||
class ChatMarkdownHelper : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
public:
|
||||
explicit ChatMarkdownHelper(ChatDocumentHandler *parent);
|
||||
explicit ChatMarkdownHelper(QObject *parent = nullptr);
|
||||
|
||||
QQuickItem *textItem() const;
|
||||
void setTextItem(QQuickItem *textItem);
|
||||
|
||||
void handleExternalFormatChange();
|
||||
|
||||
Q_SIGNALS:
|
||||
void textItemChanged();
|
||||
void unhandledBlockFormat(RichFormat::Format format);
|
||||
|
||||
private:
|
||||
enum State {
|
||||
None,
|
||||
@@ -29,8 +35,8 @@ private:
|
||||
Started,
|
||||
};
|
||||
|
||||
QTextDocument *document() const;
|
||||
void connectDocument();
|
||||
QPointer<QmlTextItemWrapper> m_textItem;
|
||||
void connectTextItem();
|
||||
|
||||
State m_currentState = None;
|
||||
int m_startPos = 0;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <QTextBlockFormat>
|
||||
#include <QTextCharFormat>
|
||||
#include <QTextCursor>
|
||||
#include <qtextformat.h>
|
||||
|
||||
QString RichFormat::styleString(Format format)
|
||||
{
|
||||
@@ -68,7 +67,7 @@ QTextListFormat::Style RichFormat::listStyleForFormat(Format format)
|
||||
}
|
||||
}
|
||||
|
||||
QTextCharFormat RichFormat::charFormatForFormat(Format format, bool invert)
|
||||
QTextCharFormat RichFormat::charFormatForFormat(Format format, bool invert, const QColor &highlightColor)
|
||||
{
|
||||
QTextCharFormat charFormat;
|
||||
if (format == Bold || headingLevelForFormat(format) > 0) {
|
||||
@@ -84,6 +83,16 @@ QTextCharFormat RichFormat::charFormatForFormat(Format format, bool invert)
|
||||
if (format == Strikethrough) {
|
||||
charFormat.setFontStrikeOut(!invert);
|
||||
}
|
||||
if (format == InlineCode) {
|
||||
if (invert) {
|
||||
charFormat.setFont({});
|
||||
charFormat.setBackground({});
|
||||
} else {
|
||||
charFormat.setFontFamilies({u"monospace"_s});
|
||||
charFormat.setFontFixedPitch(!invert);
|
||||
charFormat.setBackground(highlightColor);
|
||||
}
|
||||
}
|
||||
if (headingLevelForFormat(format) > 0) {
|
||||
// Apparently, 4 is maximum for FontSizeAdjustment; otherwise level=1 and
|
||||
// level=2 look the same
|
||||
@@ -144,6 +153,8 @@ bool RichFormat::hasFormat(QTextCursor cursor, Format format)
|
||||
return cursor.charFormat().fontStrikeOut();
|
||||
case Underline:
|
||||
return cursor.charFormat().fontUnderline();
|
||||
case InlineCode:
|
||||
return cursor.charFormat().fontFixedPitch();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -167,6 +178,9 @@ QList<RichFormat::Format> RichFormat::formatsAtCursor(const QTextCursor &cursor)
|
||||
if (cursor.charFormat().fontStrikeOut()) {
|
||||
formats += Strikethrough;
|
||||
}
|
||||
if (cursor.charFormat().fontFixedPitch()) {
|
||||
formats += InlineCode;
|
||||
}
|
||||
if (cursor.blockFormat().headingLevel() > 0 && cursor.blockFormat().headingLevel() <= 6) {
|
||||
formats += formatForHeadingLevel(cursor.blockFormat().headingLevel());
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ public:
|
||||
*
|
||||
* @sa Format, QTextCharFormat
|
||||
*/
|
||||
static QTextCharFormat charFormatForFormat(Format format, bool invert = false);
|
||||
static QTextCharFormat charFormatForFormat(Format format, bool invert = false, const QColor &highlightColor = {});
|
||||
|
||||
/**
|
||||
* @brief Return the QTextBlockFormat for the Format.
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <QQuickTextDocument>
|
||||
#include <QTextCursor>
|
||||
|
||||
#include <Kirigami/Platform/PlatformTheme>
|
||||
|
||||
QmlTextItemWrapper::QmlTextItemWrapper(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
@@ -112,6 +114,133 @@ void QmlTextItemWrapper::textDocCursorChanged()
|
||||
Q_EMIT textDocumentCursorPositionChanged();
|
||||
}
|
||||
|
||||
void QmlTextItemWrapper::mergeFormatOnCursor(RichFormat::Format format, const QTextCursor &cursor)
|
||||
{
|
||||
if (cursor.isNull()) {
|
||||
return;
|
||||
}
|
||||
switch (RichFormat::typeForFormat(format)) {
|
||||
case RichFormat::Text:
|
||||
mergeTextFormatOnCursor(format, cursor);
|
||||
return;
|
||||
case RichFormat::List:
|
||||
mergeListFormatOnCursor(format, cursor);
|
||||
return;
|
||||
case RichFormat::Style:
|
||||
mergeStyleFormatOnCursor(format, cursor);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void QmlTextItemWrapper::mergeTextFormatOnCursor(RichFormat::Format format, QTextCursor cursor)
|
||||
{
|
||||
if (RichFormat::typeForFormat(format) != RichFormat::Text) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
|
||||
const auto charFormat = RichFormat::charFormatForFormat(format, RichFormat::hasFormat(cursor, format), theme->alternateBackgroundColor());
|
||||
if (!cursor.hasSelection()) {
|
||||
cursor.select(QTextCursor::WordUnderCursor);
|
||||
}
|
||||
cursor.mergeCharFormat(charFormat);
|
||||
Q_EMIT formatChanged();
|
||||
Q_EMIT textFormatChanged();
|
||||
}
|
||||
|
||||
void QmlTextItemWrapper::mergeStyleFormatOnCursor(RichFormat::Format format, QTextCursor cursor)
|
||||
{
|
||||
// Paragraph is special because it is normally a Block format but if we're already
|
||||
// in a Paragraph it clears any existing style.
|
||||
if (!(RichFormat::typeForFormat(format) == RichFormat::Style || format == RichFormat::Paragraph)) {
|
||||
return;
|
||||
}
|
||||
|
||||
cursor.beginEditBlock();
|
||||
cursor.mergeBlockFormat(RichFormat::blockFormatForFormat(format));
|
||||
|
||||
// Applying style to the current line or selection
|
||||
QTextCursor selectCursor = cursor;
|
||||
if (selectCursor.hasSelection()) {
|
||||
QTextCursor top = selectCursor;
|
||||
top.setPosition(qMin(top.anchor(), top.position()));
|
||||
top.movePosition(QTextCursor::StartOfBlock);
|
||||
|
||||
QTextCursor bottom = selectCursor;
|
||||
bottom.setPosition(qMax(bottom.anchor(), bottom.position()));
|
||||
bottom.movePosition(QTextCursor::EndOfBlock);
|
||||
|
||||
selectCursor.setPosition(top.position(), QTextCursor::MoveAnchor);
|
||||
selectCursor.setPosition(bottom.position(), QTextCursor::KeepAnchor);
|
||||
} else {
|
||||
selectCursor.select(QTextCursor::BlockUnderCursor);
|
||||
}
|
||||
|
||||
const auto chrfmt = RichFormat::charFormatForFormat(format);
|
||||
selectCursor.mergeCharFormat(chrfmt);
|
||||
cursor.mergeBlockCharFormat(chrfmt);
|
||||
cursor.endEditBlock();
|
||||
|
||||
Q_EMIT formatChanged();
|
||||
Q_EMIT styleChanged();
|
||||
}
|
||||
|
||||
void QmlTextItemWrapper::mergeListFormatOnCursor(RichFormat::Format format, const QTextCursor &cursor)
|
||||
{
|
||||
m_nestedListHelper.handleOnBulletType(RichFormat::listStyleForFormat(format), cursor);
|
||||
Q_EMIT formatChanged();
|
||||
Q_EMIT listChanged();
|
||||
}
|
||||
|
||||
int QmlTextItemWrapper::currentListStyle() const
|
||||
{
|
||||
auto cursor = textCursor();
|
||||
if (cursor.isNull() || !textCursor().currentList()) {
|
||||
return 0;
|
||||
}
|
||||
return -cursor.currentList()->format().style();
|
||||
}
|
||||
|
||||
bool QmlTextItemWrapper::canIndentListMore() const
|
||||
{
|
||||
auto cursor = textCursor();
|
||||
if (cursor.isNull()) {
|
||||
return false;
|
||||
}
|
||||
return m_nestedListHelper.canIndent(cursor) && cursor.blockFormat().headingLevel() == 0;
|
||||
}
|
||||
|
||||
bool QmlTextItemWrapper::canIndentListLess() const
|
||||
{
|
||||
auto cursor = textCursor();
|
||||
if (cursor.isNull()) {
|
||||
return false;
|
||||
}
|
||||
return m_nestedListHelper.canDedent(cursor) && cursor.blockFormat().headingLevel() == 0;
|
||||
}
|
||||
|
||||
void QmlTextItemWrapper::indentListMore()
|
||||
{
|
||||
auto cursor = textCursor();
|
||||
if (cursor.isNull()) {
|
||||
return;
|
||||
}
|
||||
m_nestedListHelper.handleOnIndentMore(cursor);
|
||||
Q_EMIT listChanged();
|
||||
}
|
||||
|
||||
void QmlTextItemWrapper::indentListLess()
|
||||
{
|
||||
auto cursor = textCursor();
|
||||
if (cursor.isNull()) {
|
||||
return;
|
||||
}
|
||||
m_nestedListHelper.handleOnIndentLess(cursor);
|
||||
Q_EMIT listChanged();
|
||||
}
|
||||
|
||||
void QmlTextItemWrapper::forceActiveFocus() const
|
||||
{
|
||||
if (!m_textItem) {
|
||||
|
||||
@@ -6,12 +6,15 @@
|
||||
#include <QObject>
|
||||
#include <QQuickItem>
|
||||
|
||||
#include "enums/richformat.h"
|
||||
#include "nestedlisthelper_p.h"
|
||||
|
||||
class QTextDocument;
|
||||
|
||||
/**
|
||||
* @class QmlTextItemWrapper
|
||||
*
|
||||
* A class to wrap around a QQuickItem that is a QML TextEdit (or inherited from it) and provide easy acess to its properties.
|
||||
* 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.
|
||||
@@ -31,9 +34,18 @@ public:
|
||||
QTextDocument *document() const;
|
||||
|
||||
QTextCursor textCursor() const;
|
||||
int cursorPosition() const;
|
||||
void setCursorPosition(int pos);
|
||||
void setCursorVisible(bool visible);
|
||||
|
||||
void mergeFormatOnCursor(RichFormat::Format format, const QTextCursor &cursor);
|
||||
|
||||
int currentListStyle() const;
|
||||
bool canIndentListMore() const;
|
||||
bool canIndentListLess() const;
|
||||
void indentListMore();
|
||||
void indentListLess();
|
||||
|
||||
void forceActiveFocus() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
@@ -45,13 +57,22 @@ Q_SIGNALS:
|
||||
|
||||
void textDocumentCursorPositionChanged();
|
||||
|
||||
void formatChanged();
|
||||
void textFormatChanged();
|
||||
void styleChanged();
|
||||
void listChanged();
|
||||
|
||||
private:
|
||||
QPointer<QQuickItem> m_textItem;
|
||||
|
||||
int cursorPosition() const;
|
||||
int selectionStart() const;
|
||||
int selectionEnd() const;
|
||||
|
||||
void mergeTextFormatOnCursor(RichFormat::Format format, QTextCursor cursor);
|
||||
void mergeStyleFormatOnCursor(RichFormat::Format format, QTextCursor cursor);
|
||||
void mergeListFormatOnCursor(RichFormat::Format format, const QTextCursor &cursor);
|
||||
NestedListHelper m_nestedListHelper;
|
||||
|
||||
private Q_SLOTS:
|
||||
void textDocCursorChanged();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user