Start implementing rich-text editor

This commit is contained in:
Tobias Fella
2024-04-24 01:17:14 +02:00
committed by James Graham
parent 1f723d1fdf
commit 9cbe9f7280
17 changed files with 1683 additions and 182 deletions

View File

@@ -18,6 +18,8 @@ target_sources(LibNeoChat PRIVATE
filetype.cpp
linkpreviewer.cpp
neochatdatetime.cpp
nestedlisthelper_p.h
nestedlisthelper.cpp
roomlastmessageprovider.cpp
spacehierarchycache.cpp
texthandler.cpp
@@ -68,6 +70,7 @@ ecm_add_qml_module(LibNeoChat GENERATE_PLUGIN_SOURCE
qml/SearchPage.qml
qml/CreateRoomDialog.qml
qml/CreateSpaceDialog.qml
qml/OpenFileDialog.qml
DEPENDENCIES
io.github.quotient_im.libquotient
)
@@ -100,6 +103,7 @@ target_link_libraries(LibNeoChat PUBLIC
Qt::Multimedia
Qt::Quick
Qt::QuickControls2
KF6::ColorScheme
KF6::ConfigCore
KF6::CoreAddons
KF6::I18n

View File

@@ -11,15 +11,20 @@
#include <QSyntaxHighlighter>
#include <QTextBlock>
#include <QTextDocument>
#include <QTextList>
#include <QTextTable>
#include <QTimer>
#include <Kirigami/Platform/PlatformTheme>
#include <KColorScheme>
#include <Sonnet/BackgroundChecker>
#include <Sonnet/Settings>
#include "chatbartype.h"
#include "chatdocumenthandler_logging.h"
#include "eventhandler.h"
#include "utils.h"
using namespace Qt::StringLiterals;
@@ -216,6 +221,22 @@ int ChatDocumentHandler::cursorPosition() const
return m_textItem->property("cursorPosition").toInt();
}
int ChatDocumentHandler::selectionStart() const
{
if (!m_textItem) {
return -1;
}
return m_textItem->property("selectionStart").toInt();
}
int ChatDocumentHandler::selectionEnd() const
{
if (!m_textItem) {
return -1;
}
return m_textItem->property("selectionEnd").toInt();
}
NeoChatRoom *ChatDocumentHandler::room() const
{
return m_room;
@@ -241,6 +262,16 @@ void ChatDocumentHandler::setRoom(NeoChatRoom *room)
connect(m_room->cacheForType(m_type), &ChatBarCache::textChanged, this, [this]() {
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
Q_EMIT fontFamilyChanged();
Q_EMIT textColorChanged();
Q_EMIT alignmentChanged();
Q_EMIT boldChanged();
Q_EMIT italicChanged();
Q_EMIT underlineChanged();
Q_EMIT checkableChanged();
Q_EMIT strikethroughChanged();
Q_EMIT fontSizeChanged();
Q_EMIT fileUrlChanged();
});
if (!m_room->isSpace() && document() && m_type == ChatBarType::Room) {
document()->setPlainText(room->mainCache()->savedText());
@@ -379,4 +410,478 @@ void ChatDocumentHandler::updateMentions(const QString &editId)
}
}
void ChatDocumentHandler::setFontSize(int size)
{
if (size <= 0)
return;
QTextCursor cursor = textCursor();
if (cursor.isNull())
return;
if (!cursor.hasSelection())
cursor.select(QTextCursor::WordUnderCursor);
if (cursor.charFormat().property(QTextFormat::FontPointSize).toInt() == size)
return;
QTextCharFormat format;
format.setFontPointSize(size);
mergeFormatOnWordOrSelection(format);
Q_EMIT fontSizeChanged();
}
void ChatDocumentHandler::setStrikethrough(bool strikethrough)
{
QTextCharFormat format;
format.setFontStrikeOut(strikethrough);
mergeFormatOnWordOrSelection(format);
Q_EMIT underlineChanged();
}
void ChatDocumentHandler::setTextColor(const QColor &color)
{
QTextCharFormat format;
format.setForeground(QBrush(color));
mergeFormatOnWordOrSelection(format);
Q_EMIT textColorChanged();
}
Qt::Alignment ChatDocumentHandler::alignment() const
{
QTextCursor cursor = textCursor();
if (cursor.isNull())
return Qt::AlignLeft;
return textCursor().blockFormat().alignment();
}
void ChatDocumentHandler::setAlignment(Qt::Alignment alignment)
{
QTextBlockFormat format;
format.setAlignment(alignment);
QTextCursor cursor = textCursor();
cursor.mergeBlockFormat(format);
Q_EMIT alignmentChanged();
}
bool ChatDocumentHandler::bold() const
{
QTextCursor cursor = textCursor();
if (cursor.isNull()) {
return false;
}
return textCursor().charFormat().fontWeight() == QFont::Bold;
}
void ChatDocumentHandler::setBold(bool bold)
{
QTextCharFormat format;
format.setFontWeight(bold ? QFont::Bold : QFont::Normal);
mergeFormatOnWordOrSelection(format);
Q_EMIT boldChanged();
}
bool ChatDocumentHandler::italic() const
{
QTextCursor cursor = textCursor();
if (cursor.isNull())
return false;
return textCursor().charFormat().fontItalic();
}
void ChatDocumentHandler::setItalic(bool italic)
{
QTextCharFormat format;
format.setFontItalic(italic);
mergeFormatOnWordOrSelection(format);
Q_EMIT italicChanged();
}
bool ChatDocumentHandler::underline() const
{
QTextCursor cursor = textCursor();
if (cursor.isNull())
return false;
return textCursor().charFormat().fontUnderline();
}
void ChatDocumentHandler::setUnderline(bool underline)
{
QTextCharFormat format;
format.setFontUnderline(underline);
mergeFormatOnWordOrSelection(format);
Q_EMIT underlineChanged();
}
bool ChatDocumentHandler::strikethrough() const
{
QTextCursor cursor = textCursor();
if (cursor.isNull())
return false;
return textCursor().charFormat().fontStrikeOut();
}
QString ChatDocumentHandler::fontFamily() const
{
QTextCursor cursor = textCursor();
if (cursor.isNull())
return QString();
QTextCharFormat format = cursor.charFormat();
return format.font().family();
}
void ChatDocumentHandler::setFontFamily(const QString &family)
{
QTextCharFormat format;
format.setFontFamilies({family});
mergeFormatOnWordOrSelection(format);
Q_EMIT fontFamilyChanged();
}
QColor ChatDocumentHandler::textColor() const
{
QTextCursor cursor = textCursor();
if (cursor.isNull())
return QColor(Qt::black);
QTextCharFormat format = cursor.charFormat();
return format.foreground().color();
}
QTextCursor ChatDocumentHandler::textCursor() const
{
QTextDocument *doc = document();
if (!doc)
return QTextCursor();
QTextCursor cursor = QTextCursor(doc);
if (selectionStart() != selectionEnd()) {
cursor.setPosition(selectionStart());
cursor.setPosition(selectionEnd(), QTextCursor::KeepAnchor);
} else {
cursor.setPosition(cursorPosition());
}
return cursor;
}
void ChatDocumentHandler::mergeFormatOnWordOrSelection(const QTextCharFormat &format)
{
QTextCursor cursor = textCursor();
if (!cursor.hasSelection())
cursor.select(QTextCursor::WordUnderCursor);
cursor.mergeCharFormat(format);
}
QString ChatDocumentHandler::currentLinkText() const
{
QTextCursor cursor = textCursor();
selectLinkText(&cursor);
return cursor.selectedText();
}
void ChatDocumentHandler::selectLinkText(QTextCursor *cursor) const
{
// If the cursor is on a link, select the text of the link.
if (cursor->charFormat().isAnchor()) {
const QString aHref = cursor->charFormat().anchorHref();
// Move cursor to start of link
while (cursor->charFormat().anchorHref() == aHref) {
if (cursor->atStart()) {
break;
}
cursor->setPosition(cursor->position() - 1);
}
if (cursor->charFormat().anchorHref() != aHref) {
cursor->setPosition(cursor->position() + 1, QTextCursor::KeepAnchor);
}
// Move selection to the end of the link
while (cursor->charFormat().anchorHref() == aHref) {
if (cursor->atEnd()) {
break;
}
const int oldPosition = cursor->position();
cursor->movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
// Wordaround Qt Bug. when we have a table.
// FIXME selection url
if (oldPosition == cursor->position()) {
break;
}
}
if (cursor->charFormat().anchorHref() != aHref) {
cursor->setPosition(cursor->position() - 1, QTextCursor::KeepAnchor);
}
} else if (cursor->hasSelection()) {
// Nothing to do. Using the currently selected text as the link text.
} else {
// Select current word
cursor->movePosition(QTextCursor::StartOfWord);
cursor->movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
}
}
void ChatDocumentHandler::insertImage(const QUrl &url)
{
if (!url.isLocalFile()) {
return;
}
QImage image;
if (!image.load(url.path())) {
return;
}
// Ensure we are putting the image in a new line and not in a list has it
// breaks the Qt rendering
textCursor().insertHtml(QStringLiteral("<br />"));
while (canDedentList()) {
m_nestedListHelper.handleOnIndentLess(textCursor());
}
textCursor().insertHtml(QStringLiteral("<img width=\"500\" src=\"") + url.path() + QStringLiteral("\"\\>"));
}
void ChatDocumentHandler::insertTable(int rows, int columns)
{
QString htmlText;
QTextCursor cursor = textCursor();
QTextTableFormat tableFormat;
tableFormat.setBorder(1);
const int numberOfColumns(columns);
QList<QTextLength> constrains;
constrains.reserve(numberOfColumns);
const QTextLength::Type type = QTextLength::PercentageLength;
const int length = 100; // 100% of window width
const QTextLength textlength(type, length / numberOfColumns);
for (int i = 0; i < numberOfColumns; ++i) {
constrains.append(textlength);
}
tableFormat.setColumnWidthConstraints(constrains);
tableFormat.setAlignment(Qt::AlignLeft);
tableFormat.setCellSpacing(0);
tableFormat.setCellPadding(4);
tableFormat.setBorderCollapse(true);
tableFormat.setBorder(0.5);
tableFormat.setTopMargin(20);
Q_ASSERT(cursor.document());
QTextTable *table = cursor.insertTable(rows, numberOfColumns, tableFormat);
// fill table with whitespace
for (int i = 0, rows = table->rows(); i < rows; i++) {
for (int j = 0, columns = table->columns(); j < columns; j++) {
auto cell = table->cellAt(i, j);
Q_ASSERT(cell.isValid());
cell.firstCursorPosition().insertText(QStringLiteral(" "));
}
}
return;
}
void ChatDocumentHandler::updateLink(const QString &linkUrl, const QString &linkText)
{
auto cursor = textCursor();
selectLinkText(&cursor);
cursor.beginEditBlock();
if (!cursor.hasSelection()) {
cursor.select(QTextCursor::WordUnderCursor);
}
QTextCharFormat format = cursor.charFormat();
// Save original format to create an extra space with the existing char
// format for the block
if (!linkUrl.isEmpty()) {
// Add link details
format.setAnchor(true);
format.setAnchorHref(linkUrl);
// Workaround for QTBUG-1814:
// Link formatting does not get applied immediately when setAnchor(true)
// is called. So the formatting needs to be applied manually.
format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
format.setUnderlineColor(linkColor());
format.setForeground(linkColor());
} else {
// Remove link details
format.setAnchor(false);
format.setAnchorHref(QString());
// Workaround for QTBUG-1814:
// Link formatting does not get removed immediately when setAnchor(false)
// is called. So the formatting needs to be applied manually.
QTextDocument defaultTextDocument;
QTextCharFormat defaultCharFormat = defaultTextDocument.begin().charFormat();
format.setUnderlineStyle(defaultCharFormat.underlineStyle());
format.setUnderlineColor(defaultCharFormat.underlineColor());
format.setForeground(defaultCharFormat.foreground());
}
// Insert link text specified in dialog, otherwise write out url.
QString _linkText;
if (!linkText.isEmpty()) {
_linkText = linkText;
} else {
_linkText = linkUrl;
}
cursor.insertText(_linkText, format);
cursor.endEditBlock();
}
QColor ChatDocumentHandler::linkColor()
{
if (mLinkColor.isValid()) {
return mLinkColor;
}
regenerateColorScheme();
return mLinkColor;
}
void ChatDocumentHandler::regenerateColorScheme()
{
mLinkColor = KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::LinkText).color();
// TODO update existing link
}
int ChatDocumentHandler::currentHeadingLevel() const
{
return textCursor().blockFormat().headingLevel();
}
void ChatDocumentHandler::indentListMore()
{
m_nestedListHelper.handleOnIndentMore(textCursor());
}
void ChatDocumentHandler::indentListLess()
{
m_nestedListHelper.handleOnIndentLess(textCursor());
}
void ChatDocumentHandler::setListStyle(int styleIndex)
{
m_nestedListHelper.handleOnBulletType(-styleIndex, textCursor());
Q_EMIT currentListStyleChanged();
}
void ChatDocumentHandler::setHeadingLevel(int level)
{
const int boundedLevel = qBound(0, 6, level);
// Apparently, 5 is maximum for FontSizeAdjustment; otherwise level=1 and
// level=2 look the same
const int sizeAdjustment = boundedLevel > 0 ? 5 - boundedLevel : 0;
QTextCursor cursor = textCursor();
cursor.beginEditBlock();
QTextBlockFormat blkfmt;
blkfmt.setHeadingLevel(boundedLevel);
cursor.mergeBlockFormat(blkfmt);
QTextCharFormat chrfmt;
chrfmt.setFontWeight(boundedLevel > 0 ? QFont::Bold : QFont::Normal);
chrfmt.setProperty(QTextFormat::FontSizeAdjustment, sizeAdjustment);
// 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);
}
selectCursor.mergeCharFormat(chrfmt);
cursor.mergeBlockCharFormat(chrfmt);
cursor.endEditBlock();
// richTextComposer()->setTextCursor(cursor);
// richTextComposer()->setFocus();
// richTextComposer()->activateRichText();
}
bool ChatDocumentHandler::canIndentList() const
{
return m_nestedListHelper.canIndent(textCursor()) && textCursor().blockFormat().headingLevel() == 0;
}
bool ChatDocumentHandler::canDedentList() const
{
return m_nestedListHelper.canDedent(textCursor()) && textCursor().blockFormat().headingLevel() == 0;
}
int ChatDocumentHandler::currentListStyle() const
{
if (!textCursor().currentList()) {
return 0;
}
return -textCursor().currentList()->format().style();
}
int ChatDocumentHandler::fontSize() const
{
QTextCursor cursor = textCursor();
if (cursor.isNull())
return 0;
QTextCharFormat format = cursor.charFormat();
return format.font().pointSize();
}
QString ChatDocumentHandler::fileName() const
{
const QString filePath = QQmlFile::urlToLocalFileOrQrc(m_fileUrl);
const QString fileName = QFileInfo(filePath).fileName();
if (fileName.isEmpty())
return QStringLiteral("untitled.txt");
return fileName;
}
QString ChatDocumentHandler::fileType() const
{
return QFileInfo(fileName()).suffix();
}
QUrl ChatDocumentHandler::fileUrl() const
{
return m_fileUrl;
}
void ChatDocumentHandler::insertText(const QString &text)
{
textCursor().insertText(text);
}
QString ChatDocumentHandler::currentLinkUrl() const
{
return textCursor().charFormat().anchorHref();
}
void ChatDocumentHandler::dumpHtml()
{
qWarning() << htmlText();
}
QString ChatDocumentHandler::htmlText()
{
auto text = document()->toMarkdown();
while (text.startsWith(u"\n"_s)) {
text.remove(0, 1);
}
while (text.endsWith(u"\n"_s)) {
text.remove(text.size() - 1, text.size());
}
return text;
}
#include "moc_chatdocumenthandler.cpp"

View File

@@ -12,6 +12,7 @@
#include "enums/chatbartype.h"
#include "models/completionmodel.h"
#include "neochatroom.h"
#include "nestedlisthelper_p.h"
class QTextDocument;
@@ -88,6 +89,28 @@ class ChatDocumentHandler : public QObject
*/
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
Q_PROPERTY(QColor textColor READ textColor WRITE setTextColor NOTIFY textColorChanged)
Q_PROPERTY(QString fontFamily READ fontFamily WRITE setFontFamily NOTIFY fontFamilyChanged)
Q_PROPERTY(Qt::Alignment alignment READ alignment WRITE setAlignment NOTIFY alignmentChanged)
Q_PROPERTY(bool bold READ bold WRITE setBold NOTIFY boldChanged)
Q_PROPERTY(bool italic READ italic WRITE setItalic NOTIFY italicChanged)
Q_PROPERTY(bool underline READ underline WRITE setUnderline NOTIFY underlineChanged)
Q_PROPERTY(bool strikethrough READ strikethrough WRITE setStrikethrough NOTIFY strikethroughChanged)
Q_PROPERTY(bool canIndentList READ canIndentList NOTIFY cursorPositionChanged)
Q_PROPERTY(bool canDedentList READ canDedentList NOTIFY cursorPositionChanged)
Q_PROPERTY(int currentListStyle READ currentListStyle NOTIFY currentListStyleChanged)
Q_PROPERTY(int currentHeadingLevel READ currentHeadingLevel NOTIFY cursorPositionChanged)
// Q_PROPERTY(bool list READ list WRITE setList NOTIFY listChanged)
Q_PROPERTY(int fontSize READ fontSize WRITE setFontSize NOTIFY fontSizeChanged)
Q_PROPERTY(QString fileName READ fileName NOTIFY fileUrlChanged)
Q_PROPERTY(QString fileType READ fileType NOTIFY fileUrlChanged)
Q_PROPERTY(QUrl fileUrl READ fileUrl NOTIFY fileUrlChanged)
public:
explicit ChatDocumentHandler(QObject *parent = nullptr);
@@ -111,13 +134,76 @@ public:
*/
Q_INVOKABLE void updateMentions(const QString &editId);
QString fontFamily() const;
void setFontFamily(const QString &family);
QColor textColor() const;
void setTextColor(const QColor &color);
Qt::Alignment alignment() const;
void setAlignment(Qt::Alignment alignment);
bool bold() const;
void setBold(bool bold);
bool italic() const;
void setItalic(bool italic);
bool underline() const;
void setUnderline(bool underline);
bool strikethrough() const;
void setStrikethrough(bool strikethrough);
bool canIndentList() const;
bool canDedentList() const;
int currentListStyle() const;
int currentHeadingLevel() const;
// bool list() const;
// void setList(bool list);
int fontSize() const;
void setFontSize(int size);
QString fileName() const;
QString fileType() const;
QUrl fileUrl() const;
Q_INVOKABLE void insertText(const QString &text);
Q_INVOKABLE QString currentLinkUrl() const;
Q_INVOKABLE QString currentLinkText() const;
Q_INVOKABLE void updateLink(const QString &linkUrl, const QString &linkText);
Q_INVOKABLE void insertImage(const QUrl &imagePath);
Q_INVOKABLE void insertTable(int rows, int columns);
Q_INVOKABLE void indentListLess();
Q_INVOKABLE void indentListMore();
Q_INVOKABLE void setListStyle(int styleIndex);
Q_INVOKABLE void setHeadingLevel(int level);
Q_INVOKABLE void dumpHtml();
Q_INVOKABLE QString htmlText();
Q_SIGNALS:
void typeChanged();
void textItemChanged();
void roomChanged();
public Q_SLOTS:
void updateCompletion() const;
void fontFamilyChanged();
void textColorChanged();
void alignmentChanged();
void boldChanged();
void italicChanged();
void underlineChanged();
void checkableChanged();
void strikethroughChanged();
void currentListStyleChanged();
void fontSizeChanged();
void fileUrlChanged();
private:
ChatBarType::Type m_type = ChatBarType::None;
@@ -129,11 +215,22 @@ private:
QPointer<NeoChatRoom> m_room;
int cursorPosition() const;
int selectionStart() const;
int selectionEnd() const;
QString getText() const;
void pushMention(const Mention mention) const;
SyntaxHighlighter *m_highlighter = nullptr;
QQuickItem *m_textArea;
CompletionModel *m_completionModel = nullptr;
QTextCursor textCursor() const;
void mergeFormatOnWordOrSelection(const QTextCharFormat &format);
void selectLinkText(QTextCursor *cursor) const;
NestedListHelper m_nestedListHelper;
QColor linkColor();
QColor mLinkColor;
void regenerateColorScheme();
QUrl m_fileUrl;
};

View File

@@ -0,0 +1,249 @@
/**
* Nested list helper
*
* SPDX-FileCopyrightText: 2008 Stephen Kelly <steveire@gmail.com>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "nestedlisthelper_p.h"
#include <QKeyEvent>
#include <QTextBlock>
#include <QTextCursor>
#include <QTextList>
NestedListHelper::NestedListHelper()
{
listBottomMargin = 12;
listTopMargin = 12;
listNoMargin = 0;
}
bool NestedListHelper::handleBeforeKeyPressEvent(QKeyEvent *event, const QTextCursor &cursor)
{
// Only attempt to handle Backspace while on a list
if ((event->key() != Qt::Key_Backspace) || (!cursor.currentList())) {
return false;
}
bool handled = false;
if (!cursor.hasSelection() && cursor.currentList() && event->key() == Qt::Key_Backspace && cursor.atBlockStart()) {
handleOnIndentLess(cursor);
handled = true;
}
return handled;
}
bool NestedListHelper::canIndent(const QTextCursor &textCursor) const
{
if ((textCursor.block().isValid())
// && ( textEdit->textCursor().block().previous().isValid() )
) {
const QTextBlock block = textCursor.block();
const QTextBlock prevBlock = textCursor.block().previous();
if (block.textList()) {
if (prevBlock.textList()) {
return block.textList()->format().indent() <= prevBlock.textList()->format().indent();
}
} else {
return true;
}
}
return false;
}
bool NestedListHelper::canDedent(const QTextCursor &textCursor) const
{
QTextBlock thisBlock = textCursor.block();
QTextBlock nextBlock = thisBlock.next();
if (thisBlock.isValid()) {
int nextBlockIndent = 0;
if (nextBlock.isValid() && nextBlock.textList()) {
nextBlockIndent = nextBlock.textList()->format().indent();
}
if (thisBlock.textList()) {
const int thisBlockIndent = thisBlock.textList()->format().indent();
if (thisBlockIndent >= nextBlockIndent) {
return thisBlockIndent > 0;
}
}
}
return false;
}
bool NestedListHelper::handleAfterKeyPressEvent(QKeyEvent *event, const QTextCursor &cursor)
{
// Only attempt to handle Backspace and Return
if ((event->key() != Qt::Key_Backspace) && (event->key() != Qt::Key_Return)) {
return false;
}
bool handled = false;
if (!cursor.hasSelection() && cursor.currentList()) {
// Check if we're on the last list item.
// itemNumber is zero indexed
QTextBlock currentBlock = cursor.block();
if (cursor.currentList()->count() == cursor.currentList()->itemNumber(currentBlock) + 1) {
// Last block in this list, but may have just gained another list below.
if (currentBlock.next().textList()) {
reformatList(cursor.block());
}
reformatList(cursor.block());
// No need to reformatList in this case. reformatList is slow.
if ((event->key() == Qt::Key_Return) || (event->key() == Qt::Key_Backspace)) {
handled = true;
}
} else {
reformatList(cursor.block());
}
}
return handled;
}
void NestedListHelper::processList(QTextList *list)
{
QTextBlock block = list->item(0);
const int thisListIndent = list->format().indent();
QTextCursor cursor = QTextCursor(block);
list = cursor.createList(list->format());
bool processingSubList = false;
while (block.next().textList() != nullptr) {
block = block.next();
QTextList *nextList = block.textList();
const int nextItemIndent = nextList->format().indent();
if (nextItemIndent < thisListIndent) {
return;
} else if (nextItemIndent > thisListIndent) {
if (processingSubList) {
continue;
}
processingSubList = true;
processList(nextList);
} else {
processingSubList = false;
list->add(block);
}
}
// delete nextList;
// nextList = 0;
}
void NestedListHelper::reformatList(QTextBlock block)
{
if (block.textList()) {
const int minimumIndent = block.textList()->format().indent();
// Start at the top of the list
while (block.previous().textList() != nullptr) {
if (block.previous().textList()->format().indent() < minimumIndent) {
break;
}
block = block.previous();
}
processList(block.textList());
}
}
QTextCursor NestedListHelper::topOfSelection(QTextCursor cursor)
{
if (cursor.hasSelection()) {
cursor.setPosition(qMin(cursor.position(), cursor.anchor()));
}
return cursor;
}
QTextCursor NestedListHelper::bottomOfSelection(QTextCursor cursor)
{
if (cursor.hasSelection()) {
cursor.setPosition(qMax(cursor.position(), cursor.anchor()));
}
return cursor;
}
void NestedListHelper::handleOnIndentMore(const QTextCursor &textCursor)
{
QTextCursor cursor = textCursor;
QTextListFormat listFmt;
if (!cursor.currentList()) {
QTextListFormat::Style style;
cursor = topOfSelection(textCursor);
cursor.movePosition(QTextCursor::PreviousBlock);
if (cursor.currentList()) {
style = cursor.currentList()->format().style();
} else {
cursor = bottomOfSelection(textCursor);
cursor.movePosition(QTextCursor::NextBlock);
if (cursor.currentList()) {
style = cursor.currentList()->format().style();
} else {
style = QTextListFormat::ListDisc;
}
}
handleOnBulletType(style, textCursor);
} else {
listFmt = cursor.currentList()->format();
listFmt.setIndent(listFmt.indent() + 1);
cursor.createList(listFmt);
reformatList(textCursor.block());
}
}
void NestedListHelper::handleOnIndentLess(const QTextCursor &textCursor)
{
QTextCursor cursor = textCursor;
QTextList *currentList = cursor.currentList();
if (!currentList) {
return;
}
QTextListFormat listFmt = currentList->format();
if (listFmt.indent() > 1) {
listFmt.setIndent(listFmt.indent() - 1);
cursor.createList(listFmt);
reformatList(cursor.block());
} else {
QTextBlockFormat bfmt;
bfmt.setObjectIndex(-1);
cursor.setBlockFormat(bfmt);
reformatList(cursor.block().next());
}
}
void NestedListHelper::handleOnBulletType(int styleIndex, const QTextCursor &textCursor)
{
QTextCursor cursor = textCursor;
if (styleIndex != 0) {
auto style = static_cast<QTextListFormat::Style>(styleIndex);
QTextList *currentList = cursor.currentList();
QTextListFormat listFmt;
cursor.beginEditBlock();
if (currentList) {
listFmt = currentList->format();
listFmt.setStyle(style);
currentList->setFormat(listFmt);
} else {
listFmt.setStyle(style);
cursor.createList(listFmt);
}
cursor.endEditBlock();
} else {
QTextBlockFormat bfmt;
bfmt.setObjectIndex(-1);
cursor.setBlockFormat(bfmt);
}
reformatList(textCursor.block());
}

View File

@@ -0,0 +1,114 @@
/**
* Nested list helper
*
* SPDX-FileCopyrightText: 2008 Stephen Kelly <steveire@gmail.com>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#pragma once
class QKeyEvent;
class QTextCursor;
class QTextBlock;
class QTextList;
/**
*
* @short Helper class for automatic handling of nested lists in a text edit
*
*
* @author Stephen Kelly
* @since 4.1
* @internal
*/
class NestedListHelper
{
public:
/**
* Create a helper
*
* @param te The text edit object to handle lists in.
*/
NestedListHelper();
/**
*
* Handles a key press before it is processed by the text edit widget.
*
* Currently this causes a backspace at the beginning of a line or with a
* multi-line selection to decrease the nesting level of the list.
*
* @param event The event to be handled
* @return Whether the event was completely handled by this method.
*/
[[nodiscard]] bool handleBeforeKeyPressEvent(QKeyEvent *event, const QTextCursor &cursor);
/**
*
* Handles a key press after it is processed by the text edit widget.
*
* Currently this causes a Return at the end of the last list item, or
* a Backspace after the last list item to recalculate the spacing
* between the list items.
*
* @param event The event to be handled
* @return Whether the event was completely handled by this method.
*/
bool handleAfterKeyPressEvent(QKeyEvent *event, const QTextCursor &cursor);
/**
* Increases the indent (nesting level) on the current list item or selection.
*/
void handleOnIndentMore(const QTextCursor &textCursor);
/**
* Decreases the indent (nesting level) on the current list item or selection.
*/
void handleOnIndentLess(const QTextCursor &textCursor);
/**
* Changes the style of the current list or creates a new list with
* the specified style.
*
* @param styleIndex The QTextListStyle of the list.
*/
void handleOnBulletType(int styleIndex, const QTextCursor &textCursor);
/**
* @brief Check whether the current item in the list may be indented.
*
* An list item must have an item above it on the same or greater level
* if it can be indented.
*
* Also, a block which is currently part of a list can be indented.
*
* @sa canDedent
*
* @return Whether the item can be indented.
*/
[[nodiscard]] bool canIndent(const QTextCursor &textCursor) const;
/**
* \brief Check whether the current item in the list may be dedented.
*
* An item may be dedented if it is part of a list. Otherwise it can't be.
*
* @sa canIndent
*
* @return Whether the item can be dedented.
*/
[[nodiscard]] bool canDedent(const QTextCursor &textCursor) const;
private:
[[nodiscard]] QTextCursor topOfSelection(QTextCursor cursor);
[[nodiscard]] QTextCursor bottomOfSelection(QTextCursor cursor);
void processList(QTextList *list);
void reformatList(QTextBlock block);
int listBottomMargin;
int listTopMargin;
int listNoMargin;
};
//@endcond

View File

@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Dialogs
FileDialog {
id: root
signal chosen(string path)
title: i18nc("@title:dialog", "Select a File")
onAccepted: root.chosen(selectedFile)
}

View File

@@ -52,8 +52,8 @@ void TextHandler::setData(const QString &string)
QString TextHandler::handleSendText()
{
m_pos = 0;
m_dataBuffer = markdownToHTML(m_data);
m_dataBuffer = customMarkdownToHtml(m_dataBuffer);
m_dataBuffer = customMarkdownToHtml(m_data);
m_dataBuffer = markdownToHTML(m_dataBuffer);
m_nextTokenType = nextTokenType(m_dataBuffer, m_pos, m_nextToken, m_nextTokenType);
@@ -802,6 +802,9 @@ QString TextHandler::customMarkdownToHtml(const QString &stringIn)
// strikethrough
processSyntax(u"~~"_s, u"<del>"_s, u"</del>"_s);
// underline
processSyntax(u"_"_s, u"<u>"_s, u"</u>"_s);
return buffer;
}

View File

@@ -73,6 +73,7 @@ namespace TextRegex
static const QRegularExpression endTagType{u"[> /]"_s};
static const QRegularExpression endAttributeType{u"[> ]"_s};
static const QRegularExpression attributeData{u"['\"](.*?)['\"]"_s};
static const QRegularExpression htmlBodyContent{u"<body[^>]*>(.*?)</body>"_s, QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression removeReply{u"> <.*?>.*?\\n\\n"_s, QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression removeRichReply{u"<mx-reply>.*?</mx-reply>"_s, QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression codePill{u"<pre><code[^>]*>(.*?)</code></pre>"_s, QRegularExpression::DotMatchesEverythingOption};