From 4db1e1c4373483e338eba202200f3f416ce7fee8 Mon Sep 17 00:00:00 2001 From: James Graham Date: Thu, 6 Nov 2025 20:46:51 +0000 Subject: [PATCH] Add automatic markdown formatting --- src/chatbar/RichEditBar.qml | 24 +- src/chatbar/StylePicker.qml | 14 +- src/chatbar/styledelegatehelper.cpp | 10 +- src/libneochat/CMakeLists.txt | 3 +- src/libneochat/chatdocumenthandler.cpp | 269 ++++++++---------- src/libneochat/chatdocumenthandler.h | 82 ++---- src/libneochat/chatmarkdownhelper.cpp | 235 +++++++++++++++ src/libneochat/chatmarkdownhelper.h | 43 +++ src/libneochat/enums/richformat.cpp | 180 ++++++++++++ src/libneochat/enums/richformat.h | 131 +++++++++ src/libneochat/enums/textstyle.h | 74 ----- src/libneochat/nestedlisthelper.cpp | 15 +- src/libneochat/nestedlisthelper_p.h | 5 +- src/messagecontent/TextComponent.qml | 14 + .../models/chatbarmessagecontentmodel.cpp | 11 +- .../models/chatbarmessagecontentmodel.h | 4 +- 16 files changed, 801 insertions(+), 313 deletions(-) create mode 100644 src/libneochat/chatmarkdownhelper.cpp create mode 100644 src/libneochat/chatmarkdownhelper.h create mode 100644 src/libneochat/enums/richformat.cpp create mode 100644 src/libneochat/enums/richformat.h delete mode 100644 src/libneochat/enums/textstyle.h diff --git a/src/chatbar/RichEditBar.qml b/src/chatbar/RichEditBar.qml index 6236fbd32..0ee1b937d 100644 --- a/src/chatbar/RichEditBar.qml +++ b/src/chatbar/RichEditBar.qml @@ -84,7 +84,7 @@ QQC2.ToolBar { checkable: true checked: root.focusedDocumentHandler.bold onClicked: { - root.focusedDocumentHandler.bold = checked; + root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Bold); root.clicked() } @@ -105,7 +105,7 @@ QQC2.ToolBar { checkable: true checked: root.focusedDocumentHandler.italic onClicked: { - root.focusedDocumentHandler.italic = checked; + root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Italic); root.clicked() } @@ -126,7 +126,7 @@ QQC2.ToolBar { checkable: true checked: root.focusedDocumentHandler.underline onClicked: { - root.focusedDocumentHandler.underline = checked; + root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Underline); root.clicked(); } @@ -142,7 +142,7 @@ QQC2.ToolBar { checkable: true checked: root.focusedDocumentHandler.strikethrough onClicked: { - root.focusedDocumentHandler.strikethrough = checked; + root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Strikethrough); root.clicked() } @@ -174,7 +174,7 @@ QQC2.ToolBar { checkable: true checked: root.focusedDocumentHandler.bold onTriggered: { - root.focusedDocumentHandler.bold = checked; + root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Bold); root.clicked(); } } @@ -184,7 +184,7 @@ QQC2.ToolBar { checkable: true checked: root.focusedDocumentHandler.italic onTriggered: { - root.focusedDocumentHandler.italic = checked; + root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Italic); root.clicked(); } } @@ -194,7 +194,7 @@ QQC2.ToolBar { checkable: true checked: root.focusedDocumentHandler.underline onTriggered: { - root.focusedDocumentHandler.underline = checked; + root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Underline); root.clicked(); } } @@ -204,7 +204,7 @@ QQC2.ToolBar { checkable: true checked: root.focusedDocumentHandler.strikethrough onTriggered: { - root.focusedDocumentHandler.strikethrough = checked; + root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.Strikethrough); root.clicked(); } } @@ -229,7 +229,7 @@ QQC2.ToolBar { checkable: true checked: root.focusedDocumentHandler.currentListStyle === 1 onClicked: { - root.focusedDocumentHandler.setListStyle(root.focusedDocumentHandler.currentListStyle === 1 ? 0 : 1); + root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.UnorderedList); root.clicked(); } @@ -245,7 +245,7 @@ QQC2.ToolBar { checkable: true checked: root.focusedDocumentHandler.currentListStyle === 4 onClicked: { - root.focusedDocumentHandler.setListStyle(root.focusedDocumentHandler.currentListStyle === 4 ? 0 : 4); + root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.OrderedList); root.clicked(); } @@ -303,7 +303,7 @@ QQC2.ToolBar { icon.name: "format-list-unordered" text: i18nc("@action:button", "Unordered List") onTriggered: { - root.focusedDocumentHandler.setListStyle(root.focusedDocumentHandler.currentListStyle === 1 ? 0 : 1); + root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.UnorderedList); root.clicked(); } } @@ -311,7 +311,7 @@ QQC2.ToolBar { icon.name: "format-list-ordered" text: i18nc("@action:button", "Ordered List") onTriggered: { - root.focusedDocumentHandler.setListStyle(root.focusedDocumentHandler.currentListStyle === 4 ? 0 : 4); + root.focusedDocumentHandler.setFormat(LibNeoChat.RichFormat.OrderedList); root.clicked(); } } diff --git a/src/chatbar/StylePicker.qml b/src/chatbar/StylePicker.qml index 8a7386443..a50cc2e1f 100644 --- a/src/chatbar/StylePicker.qml +++ b/src/chatbar/StylePicker.qml @@ -36,18 +36,18 @@ QQC2.Popup { leftPadding: lineRow.visible ? lineRow.width + lineRow.anchors.leftMargin + Kirigami.Units.smallSpacing : Kirigami.Units.largeSpacing verticalAlignment: Text.AlignVCenter - enabled: root.chatContentModel.focusType !== LibNeoChat.MessageComponentType.Code || styleDelegate.index === LibNeoChat.TextStyle.Paragraph || styleDelegate.index === LibNeoChat.TextStyle.Quote + enabled: root.chatContentModel.focusType !== LibNeoChat.MessageComponentType.Code || styleDelegate.index === LibNeoChat.RichFormat.Paragraph || styleDelegate.index === LibNeoChat.RichFormat.Quote readOnly: true selectByMouse: false onPressed: (event) => { - if (styleDelegate.index === LibNeoChat.TextStyle.Paragraph || - styleDelegate.index === LibNeoChat.TextStyle.Code || - styleDelegate.index === LibNeoChat.TextStyle.Quote + if (styleDelegate.index === LibNeoChat.RichFormat.Paragraph || + styleDelegate.index === LibNeoChat.RichFormat.Code || + styleDelegate.index === LibNeoChat.RichFormat.Quote ) { root.chatContentModel.insertStyleAtCursor(styleDelegate.index); } else { - root.focusedDocumentHandler.style = styleDelegate.index; + root.focusedDocumentHandler.setFormat(styleDelegate.index); } root.close(); } @@ -61,7 +61,7 @@ QQC2.Popup { leftMargin: Kirigami.Units.smallSpacing } - visible: styleDelegate.index === LibNeoChat.TextStyle.Code + visible: styleDelegate.index === LibNeoChat.RichFormat.Code QQC2.Label { horizontalAlignment: Text.AlignRight @@ -82,7 +82,7 @@ QQC2.Popup { background: Rectangle { color: Kirigami.Theme.backgroundColor - Kirigami.Theme.colorSet: styleDelegate.index === LibNeoChat.TextStyle.Quote ? Kirigami.Theme.Window : Kirigami.Theme.View + Kirigami.Theme.colorSet: styleDelegate.index === LibNeoChat.RichFormat.Quote ? Kirigami.Theme.Window : Kirigami.Theme.View Kirigami.Theme.inherit: false radius: Kirigami.Units.cornerRadius border { diff --git a/src/chatbar/styledelegatehelper.cpp b/src/chatbar/styledelegatehelper.cpp index 92a784c67..c794d0f0b 100644 --- a/src/chatbar/styledelegatehelper.cpp +++ b/src/chatbar/styledelegatehelper.cpp @@ -7,7 +7,7 @@ #include #include -#include "enums/textstyle.h" +#include "enums/richformat.h" StyleDelegateHelper::StyleDelegateHelper(QObject *parent) : QObject(parent) @@ -59,8 +59,8 @@ void StyleDelegateHelper::formatDocument() cursor.beginEditBlock(); cursor.select(QTextCursor::Document); cursor.removeSelectedText(); - const auto style = static_cast(m_textItem->property("index").toInt()); - const auto string = TextStyle::styleString(style); + const auto style = static_cast(m_textItem->property("index").toInt()); + const auto string = RichFormat::styleString(style); const int headingLevel = style <= 6 ? style : 0; // Apparently, 5 is maximum for FontSizeAdjustment; otherwise level=1 and @@ -74,9 +74,9 @@ void StyleDelegateHelper::formatDocument() QTextCharFormat chrfmt; chrfmt.setFontWeight(headingLevel > 0 ? QFont::Bold : QFont::Normal); chrfmt.setProperty(QTextFormat::FontSizeAdjustment, sizeAdjustment / 2); - if (style == TextStyle::Code) { + if (style == RichFormat::Code) { chrfmt.setFontFamilies({u"monospace"_s}); - } else if (style == TextStyle::Quote) { + } else if (style == RichFormat::Quote) { chrfmt.setFontItalic(true); } diff --git a/src/libneochat/CMakeLists.txt b/src/libneochat/CMakeLists.txt index 2107ee3b5..3275f7073 100644 --- a/src/libneochat/CMakeLists.txt +++ b/src/libneochat/CMakeLists.txt @@ -10,6 +10,7 @@ target_sources(LibNeoChat PRIVATE accountmanager.cpp chatbarcache.cpp chatdocumenthandler.cpp + chatmarkdownhelper.cpp clipboard.cpp delegatesizehelper.cpp emojitones.cpp @@ -31,9 +32,9 @@ target_sources(LibNeoChat PRIVATE enums/messagetype.h enums/powerlevel.cpp enums/pushrule.h + enums/richformat.cpp enums/roomsortparameter.cpp enums/roomsortorder.h - enums/textstyle.h enums/timelinemarkreadcondition.h events/imagepackevent.cpp events/pollevent.cpp diff --git a/src/libneochat/chatdocumenthandler.cpp b/src/libneochat/chatdocumenthandler.cpp index 94b6862ee..558aa44bb 100644 --- a/src/libneochat/chatdocumenthandler.cpp +++ b/src/libneochat/chatdocumenthandler.cpp @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -28,8 +29,8 @@ #include "chatbartype.h" #include "chatdocumenthandler_logging.h" +#include "chatmarkdownhelper.h" #include "eventhandler.h" -#include "textstyle.h" using namespace Qt::StringLiterals; @@ -130,6 +131,8 @@ ChatDocumentHandler::ChatDocumentHandler(QObject *parent) , m_highlighter(new SyntaxHighlighter(this)) , m_completionModel(new CompletionModel(this)) { + m_markdownHelper = new ChatMarkdownHelper(this); + connect(this, &ChatDocumentHandler::formatChanged, m_markdownHelper, &ChatMarkdownHelper::handleExternalFormatChange); } int ChatDocumentHandler::completionStartIndex() const @@ -162,6 +165,22 @@ void ChatDocumentHandler::setType(ChatBarType::Type type) Q_EMIT typeChanged(); } +NeoChatRoom *ChatDocumentHandler::room() const +{ + return m_room; +} + +void ChatDocumentHandler::setRoom(NeoChatRoom *room) +{ + if (m_room == room) { + return; + } + + m_room = room; + m_completionModel->setRoom(m_room); + Q_EMIT roomChanged(); +} + QQuickItem *ChatDocumentHandler::textItem() const { return m_textItem; @@ -187,9 +206,17 @@ void ChatDocumentHandler::setTextItem(QQuickItem *textItem) connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(updateCursor())); if (document()) { connect(document(), &QTextDocument::contentsChanged, this, &ChatDocumentHandler::contentsChanged); - connect(document(), &QTextDocument::contentsChanged, this, [this]() { - if (m_room) { - updateCursor(); + connect(document(), &QTextDocument::contentsChanged, this, &ChatDocumentHandler::updateCursor); + connect(document(), &QTextDocument::contentsChange, this, [this](int position) { + auto cursor = textCursor(); + if (cursor.isNull()) { + return; + } + cursor.setPosition(position); + cursor.select(QTextCursor::WordUnderCursor); + if (!cursor.selectedText().isEmpty()) { + cursor.mergeCharFormat(m_pendingFormat); + m_pendingFormat = {}; } }); initializeChars(); @@ -308,7 +335,6 @@ void ChatDocumentHandler::updateCursor() int start = completionStartIndex(); m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start)); - Q_EMIT formatChanged(); Q_EMIT atFirstLineChanged(); Q_EMIT atLastLineChanged(); } @@ -329,20 +355,20 @@ int ChatDocumentHandler::selectionEnd() const return m_textItem->property("selectionEnd").toInt(); } -NeoChatRoom *ChatDocumentHandler::room() const +QTextCursor ChatDocumentHandler::textCursor() const { - return m_room; -} - -void ChatDocumentHandler::setRoom(NeoChatRoom *room) -{ - if (m_room == room) { - return; + if (!document()) { + return QTextCursor(); } - m_room = room; - m_completionModel->setRoom(m_room); - Q_EMIT roomChanged(); + QTextCursor cursor = QTextCursor(document()); + if (selectionStart() != selectionEnd()) { + cursor.setPosition(selectionStart()); + cursor.setPosition(selectionEnd(), QTextCursor::KeepAnchor); + } else { + cursor.setPosition(cursorPosition()); + } + return cursor; } bool ChatDocumentHandler::isEmpty() const @@ -647,35 +673,6 @@ 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 formatChanged(); -} - void ChatDocumentHandler::setTextColor(const QColor &color) { QTextCharFormat format; @@ -684,23 +681,6 @@ void ChatDocumentHandler::setTextColor(const QColor &color) 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(); @@ -710,14 +690,6 @@ bool ChatDocumentHandler::bold() const return textCursor().charFormat().fontWeight() == QFont::Bold; } -void ChatDocumentHandler::setBold(bool bold) -{ - QTextCharFormat format; - format.setFontWeight(bold ? QFont::Bold : QFont::Normal); - mergeFormatOnWordOrSelection(format); - Q_EMIT formatChanged(); -} - bool ChatDocumentHandler::italic() const { QTextCursor cursor = textCursor(); @@ -726,14 +698,6 @@ bool ChatDocumentHandler::italic() const return textCursor().charFormat().fontItalic(); } -void ChatDocumentHandler::setItalic(bool italic) -{ - QTextCharFormat format; - format.setFontItalic(italic); - mergeFormatOnWordOrSelection(format); - Q_EMIT formatChanged(); -} - bool ChatDocumentHandler::underline() const { QTextCursor cursor = textCursor(); @@ -742,14 +706,6 @@ bool ChatDocumentHandler::underline() const return textCursor().charFormat().fontUnderline(); } -void ChatDocumentHandler::setUnderline(bool underline) -{ - QTextCharFormat format; - format.setFontUnderline(underline); - mergeFormatOnWordOrSelection(format); - Q_EMIT formatChanged(); -} - bool ChatDocumentHandler::strikethrough() const { QTextCursor cursor = textCursor(); @@ -758,23 +714,6 @@ bool ChatDocumentHandler::strikethrough() const 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(); @@ -784,22 +723,6 @@ QColor ChatDocumentHandler::textColor() const return format.foreground().color(); } -QTextCursor ChatDocumentHandler::textCursor() const -{ - if (!document()) { - return QTextCursor(); - } - - QTextCursor cursor = QTextCursor(document()); - if (selectionStart() != selectionEnd()) { - cursor.setPosition(selectionStart()); - cursor.setPosition(selectionEnd(), QTextCursor::KeepAnchor); - } else { - cursor.setPosition(cursorPosition()); - } - return cursor; -} - std::optional ChatDocumentHandler::textFormat() const { if (!m_textItem) { @@ -812,9 +735,14 @@ std::optional ChatDocumentHandler::textFormat() const void ChatDocumentHandler::mergeFormatOnWordOrSelection(const QTextCharFormat &format) { QTextCursor cursor = textCursor(); - if (!cursor.hasSelection()) + if (!cursor.hasSelection()) { cursor.select(QTextCursor::WordUnderCursor); - cursor.mergeCharFormat(format); + } + if (cursor.hasSelection()) { + cursor.mergeCharFormat(format); + } else { + m_pendingFormat = format.toCharFormat(); + } } QString ChatDocumentHandler::currentLinkText() const @@ -993,19 +921,38 @@ void ChatDocumentHandler::regenerateColorScheme() // TODO update existing link } +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: + return; + } +} + void ChatDocumentHandler::indentListMore() { m_nestedListHelper.handleOnIndentMore(textCursor()); + Q_EMIT currentListStyleChanged(); } void ChatDocumentHandler::indentListLess() { m_nestedListHelper.handleOnIndentLess(textCursor()); + Q_EMIT currentListStyleChanged(); } -void ChatDocumentHandler::setListStyle(int styleIndex) +void ChatDocumentHandler::setListFormat(RichFormat::Format format) { - m_nestedListHelper.handleOnBulletType(-styleIndex, textCursor()); + m_nestedListHelper.handleOnBulletType(RichFormat::listStyleForFormat(format), textCursor()); Q_EMIT currentListStyleChanged(); } @@ -1028,28 +975,38 @@ int ChatDocumentHandler::currentListStyle() const return -textCursor().currentList()->format().style(); } -TextStyle::Style ChatDocumentHandler::style() const +void ChatDocumentHandler::setTextFormat(RichFormat::Format format) { - return static_cast(textCursor().blockFormat().headingLevel()); + if (RichFormat::typeForFormat(format) != RichFormat::Text) { + return; + } + mergeFormatOnWordOrSelection(RichFormat::charFormatForFormat(format, RichFormat::hasFormat(textCursor(), format))); + Q_EMIT formatChanged(); } -void ChatDocumentHandler::setStyle(TextStyle::Style style) +RichFormat::Format ChatDocumentHandler::style() const { - const int headingLevel = style <= 6 ? style : 0; - // Apparently, 5 is maximum for FontSizeAdjustment; otherwise level=1 and - // level=2 look the same - const int sizeAdjustment = headingLevel > 0 ? 5 - headingLevel : 0; + return static_cast(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; + } + + qWarning() << format; QTextCursor cursor = textCursor(); + if (cursor.isNull()) { + return; + } cursor.beginEditBlock(); - QTextBlockFormat blkfmt; - blkfmt.setHeadingLevel(headingLevel); - cursor.mergeBlockFormat(blkfmt); + cursor.mergeBlockFormat(RichFormat::blockFormatForFormat(format)); - QTextCharFormat chrfmt; - chrfmt.setFontWeight(headingLevel > 0 ? QFont::Bold : QFont::Normal); - chrfmt.setProperty(QTextFormat::FontSizeAdjustment, sizeAdjustment); // Applying style to the current line or selection QTextCursor selectCursor = cursor; if (selectCursor.hasSelection()) { @@ -1066,23 +1023,16 @@ void ChatDocumentHandler::setStyle(TextStyle::Style style) } else { selectCursor.select(QTextCursor::BlockUnderCursor); } - selectCursor.mergeCharFormat(chrfmt); + const auto chrfmt = RichFormat::charFormatForFormat(format); + selectCursor.mergeCharFormat(chrfmt); cursor.mergeBlockCharFormat(chrfmt); cursor.endEditBlock(); + Q_EMIT formatChanged(); Q_EMIT styleChanged(); } -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); @@ -1102,6 +1052,19 @@ QUrl ChatDocumentHandler::fileUrl() const return m_fileUrl; } +void ChatDocumentHandler::tab() +{ + QTextCursor cursor = textCursor(); + if (cursor.isNull()) { + return; + } + if (cursor.currentList()) { + indentListMore(); + return; + } + insertText(u" "_s); +} + void ChatDocumentHandler::deleteChar() { QTextCursor cursor = textCursor(); @@ -1124,6 +1087,11 @@ void ChatDocumentHandler::backspace() return; } if (cursor.position() <= m_fixedStartChars.length()) { + qWarning() << "unhandled backspace"; + if (cursor.currentList()) { + indentListLess(); + return; + } if (const auto previousHandler = previousDocumentHandler()) { previousHandler->insertFragment(takeFirstBlock(), End, true); } else { @@ -1134,6 +1102,15 @@ void ChatDocumentHandler::backspace() cursor.deletePreviousChar(); } +void ChatDocumentHandler::insertReturn() +{ + QTextCursor cursor = textCursor(); + if (cursor.isNull()) { + return; + } + cursor.insertBlock(); +} + void ChatDocumentHandler::insertText(const QString &text) { textCursor().insertText(text); diff --git a/src/libneochat/chatdocumenthandler.h b/src/libneochat/chatdocumenthandler.h index f53f078ec..21ae7cd21 100644 --- a/src/libneochat/chatdocumenthandler.h +++ b/src/libneochat/chatdocumenthandler.h @@ -11,8 +11,9 @@ #include #include "chatbarcache.h" +#include "chatmarkdownhelper.h" #include "enums/chatbartype.h" -#include "enums/textstyle.h" +#include "enums/richformat.h" #include "models/completionmodel.h" #include "neochatroom.h" #include "nestedlisthelper_p.h" @@ -103,25 +104,19 @@ class ChatDocumentHandler : public QObject Q_PROPERTY(bool atLastLine READ atLastLine NOTIFY atLastLineChanged) 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 formatChanged) - Q_PROPERTY(bool italic READ italic WRITE setItalic NOTIFY formatChanged) - Q_PROPERTY(bool underline READ underline WRITE setUnderline NOTIFY formatChanged) - Q_PROPERTY(bool strikethrough READ strikethrough WRITE setStrikethrough NOTIFY formatChanged) + 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(TextStyle::Style style READ style WRITE setStyle NOTIFY styleChanged) + Q_PROPERTY(RichFormat::Format style READ style NOTIFY styleChanged) // 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) @@ -176,45 +171,33 @@ 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); + + Q_INVOKABLE void setFormat(RichFormat::Format format); + void setFormatOnCursor(RichFormat::Format format, const QTextCursor &cursor); bool canIndentList() const; bool canDedentList() const; int currentListStyle() const; + Q_INVOKABLE void indentListLess(); + Q_INVOKABLE void indentListMore(); - TextStyle::Style style() const; - void setStyle(TextStyle::Style style); - - // bool list() const; - // void setList(bool list); - - int fontSize() const; - void setFontSize(int size); + RichFormat::Format style() const; QString fileName() const; QString fileType() const; QUrl fileUrl() const; + Q_INVOKABLE void tab(); Q_INVOKABLE void deleteChar(); Q_INVOKABLE void backspace(); + Q_INVOKABLE void insertReturn(); Q_INVOKABLE void insertText(const QString &text); void insertFragment(const QTextDocumentFragment fragment, InsertPosition position = Cursor, bool keepPosition = false); Q_INVOKABLE QString currentLinkUrl() const; @@ -223,11 +206,6 @@ public: 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 dumpHtml(); Q_INVOKABLE QString htmlText() const; @@ -239,17 +217,10 @@ Q_SIGNALS: void atFirstLineChanged(); void atLastLineChanged(); - void fontFamilyChanged(); void textColorChanged(); - void alignmentChanged(); - void boldChanged(); - void italicChanged(); - void underlineChanged(); void checkableChanged(); - void strikethroughChanged(); void currentListStyleChanged(); - void fontSizeChanged(); void fileUrlChanged(); void formatChanged(); @@ -262,6 +233,7 @@ Q_SIGNALS: private: ChatBarType::Type m_type = ChatBarType::None; + QPointer m_room; QPointer m_textItem; QTextDocument *document() const; @@ -273,22 +245,26 @@ private: QString m_initialText = {}; void initializeChars(); - int completionStartIndex() const; - - QPointer m_room; - int cursorPosition() const; int selectionStart() const; int selectionEnd() const; + QTextCursor textCursor() const; + void setTextFormat(RichFormat::Format format); + void setStyleFormat(RichFormat::Format format); + void setListFormat(RichFormat::Format format); + + QPointer m_markdownHelper; + QTextCharFormat m_pendingFormat = {}; + + SyntaxHighlighter *m_highlighter = nullptr; + + int completionStartIndex() const; + + CompletionModel *m_completionModel = nullptr; QString getText() const; void pushMention(const Mention mention) const; - SyntaxHighlighter *m_highlighter = nullptr; - QQuickItem *m_textArea; - - CompletionModel *m_completionModel = nullptr; - QTextCursor textCursor() const; std::optional textFormat() const; void mergeFormatOnWordOrSelection(const QTextCharFormat &format); void selectLinkText(QTextCursor *cursor) const; diff --git a/src/libneochat/chatmarkdownhelper.cpp b/src/libneochat/chatmarkdownhelper.cpp new file mode 100644 index 000000000..1e56269bd --- /dev/null +++ b/src/libneochat/chatmarkdownhelper.cpp @@ -0,0 +1,235 @@ +// SPDX-FileCopyrightText: 2025 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#include "chatmarkdownhelper.h" + +#include +#include + +#include "chatdocumenthandler.h" + +struct MarkdownSyntax { + QLatin1String sequence; + bool closable = false; + bool lineStart = false; + RichFormat::Format format; +}; + +static const QList 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}, + MarkdownSyntax{.sequence = "## "_L1, .lineStart = true, .format = RichFormat::Heading2}, + MarkdownSyntax{.sequence = "### "_L1, .lineStart = true, .format = RichFormat::Heading3}, + MarkdownSyntax{.sequence = "#### "_L1, .lineStart = true, .format = RichFormat::Heading4}, + MarkdownSyntax{.sequence = "##### "_L1, .lineStart = true, .format = RichFormat::Heading5}, + MarkdownSyntax{.sequence = "###### "_L1, .lineStart = true, .format = RichFormat::Heading6}, + MarkdownSyntax{.sequence = "> "_L1, .lineStart = true, .format = RichFormat::Quote}, + MarkdownSyntax{.sequence = "* "_L1, .lineStart = true, .format = RichFormat::UnorderedList}, + MarkdownSyntax{.sequence = "- "_L1, .lineStart = true, .format = RichFormat::UnorderedList}, + MarkdownSyntax{.sequence = "1. "_L1, .lineStart = true, .format = RichFormat::OrderedList}, + MarkdownSyntax{.sequence = "1) "_L1, .lineStart = true, .format = RichFormat::OrderedList}, + 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}, +}; + +static std::optional checkSequence(const QString ¤tString, const QString &nextChar, bool lineStart = false) +{ + QList partialMatches; + std::optional fullMatch = std::nullopt; + auto it = syntax.cbegin(); + while ((it = std::find_if(it, + syntax.cend(), + [currentString, nextChar](const MarkdownSyntax &syntax) { + return syntax.sequence == currentString || syntax.sequence.startsWith(QString(currentString + nextChar)); + })) + != syntax.cend()) { + if (it->lineStart ? lineStart : true) { + if (it->sequence == currentString) { + fullMatch = *it; + } else { + partialMatches += *it; + } + } + ++it; + } + + if (partialMatches.length() > 0) { + return false; + } + if (fullMatch) { + return true; + } + return std::nullopt; +} + +static std::optional syntaxForSequence(const QString &sequence) +{ + const auto it = std::find_if(syntax.cbegin(), syntax.cend(), [sequence](const MarkdownSyntax &syntax) { + return syntax.sequence == sequence; + }); + if (it == syntax.cend()) { + return std::nullopt; + } + return *it; +} + +ChatMarkdownHelper::ChatMarkdownHelper(ChatDocumentHandler *parent) + : QObject(parent) +{ + Q_ASSERT(parent); + + connectDocument(); + connect(parent, &ChatDocumentHandler::textItemChanged, this, &ChatMarkdownHelper::connectDocument); +} + +QTextDocument *ChatMarkdownHelper::document() const +{ + const auto documentHandler = qobject_cast(parent()); + if (!documentHandler) { + return nullptr; + } + + if (!documentHandler->textItem()) { + return nullptr; + } + + const auto quickDocument = qvariant_cast(documentHandler->textItem()->property("textDocument")); + return quickDocument ? quickDocument->textDocument() : nullptr; +} + +void ChatMarkdownHelper::connectDocument() +{ + disconnect(); + + if (document()) { + m_startPos = qobject_cast(parent())->textItem()->property("cursorPosition").toInt(); + m_endPos = m_startPos; + if (m_startPos == 0) { + m_currentState = Pre; + } + + connect(document(), &QTextDocument::contentsChange, 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()); + if (cursor.isNull()) { + return; + } + + if (charsRemoved - charsAdded > 0) { + if (position < m_startPos) { + m_startPos = position; + } + m_endPos -= charsRemoved; + cursor.setPosition(m_endPos); + 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; + } + + for (auto i = 1; i <= charsAdded - charsRemoved; ++i) { + cursor.setPosition(m_startPos); + cursor.setPosition(m_endPos, QTextCursor::KeepAnchor); + const auto currentMarkdown = cursor.selectedText(); + cursor.setPosition(m_endPos); + cursor.setPosition(m_endPos + 1, QTextCursor::KeepAnchor); + const auto nextChar = cursor.selectedText(); + cursor.setPosition(m_startPos); + + const auto result = checkSequence(currentMarkdown, nextChar, cursor.atBlockStart()); + qWarning() << result; + + switch (m_currentState) { + case None: + if (nextChar == u' ' || cursor.atBlockEnd()) { + m_currentState = Pre; + } + ++m_startPos; + m_endPos = m_startPos; + break; + case Pre: + if (!result && RichFormat::formatsAtCursor(cursor).length() == 0) { + m_currentState = None; + } else if (result && !*result) { + m_currentState = Started; + ++m_endPos; + break; + } + ++m_startPos; + m_endPos = m_startPos; + break; + case Started: + if (!result) { + m_currentState = Pre; + ++m_startPos; + m_endPos = m_startPos; + break; + } else if (!*result) { + ++m_endPos; + break; + } + complete(); + break; + } + } + + qWarning() << "2" << m_currentState << m_startPos << m_endPos; +} + +void ChatMarkdownHelper::complete() +{ + auto cursor = QTextCursor(document()); + if (cursor.isNull()) { + return; + } + cursor.setPosition(m_startPos); + cursor.setPosition(m_endPos, QTextCursor::KeepAnchor); + const auto syntax = syntaxForSequence(cursor.selectedText()); + cursor.removeSelectedText(); + + if (m_currentFormats.contains(syntax->format)) { + m_currentFormats.remove(syntax->format); + } else if (syntax->closable) { + m_currentFormats.insert(syntax->format, m_startPos); + } + + ++m_startPos; + + const auto documentHandler = qobject_cast(parent()); + if (syntax) { + documentHandler->textItem()->setProperty("cursorPosition", m_startPos); + documentHandler->setFormat(syntax->format); + } + + m_currentState = Pre; + m_endPos = m_startPos; + + documentHandler->textItem()->setProperty("cursorPosition", m_startPos); +} + +void ChatMarkdownHelper::handleExternalFormatChange() +{ + auto cursor = QTextCursor(document()); + 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" diff --git a/src/libneochat/chatmarkdownhelper.h b/src/libneochat/chatmarkdownhelper.h new file mode 100644 index 000000000..4e98b83f4 --- /dev/null +++ b/src/libneochat/chatmarkdownhelper.h @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2025 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#pragma once + +#include +#include + +#include "enums/richformat.h" + +class QTextDocument; + +class ChatDocumentHandler; + +class ChatMarkdownHelper : public QObject +{ + Q_OBJECT + QML_ELEMENT + +public: + explicit ChatMarkdownHelper(ChatDocumentHandler *parent); + + void handleExternalFormatChange(); + +private: + enum State { + None, + Pre, + Started, + }; + + QTextDocument *document() const; + void connectDocument(); + + State m_currentState = None; + int m_startPos = 0; + int m_endPos = 0; + + QHash m_currentFormats; + + void checkMarkdown(int position, int charsRemoved, int charsAdded); + void complete(); +}; diff --git a/src/libneochat/enums/richformat.cpp b/src/libneochat/enums/richformat.cpp new file mode 100644 index 000000000..65fe766b8 --- /dev/null +++ b/src/libneochat/enums/richformat.cpp @@ -0,0 +1,180 @@ +// SPDX-FileCopyrightText: 2025 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#include "richformat.h" + +#include +#include +#include +#include + +QString RichFormat::styleString(Format format) +{ + switch (format) { + case Paragraph: + return u"Paragraph"_s; + case Heading1: + return u"Heading 1"_s; + case Heading2: + return u"Heading 2"_s; + case Heading3: + return u"Heading 3"_s; + case Heading4: + return u"Heading 4"_s; + case Heading5: + return u"Heading 5"_s; + case Heading6: + return u"Heading 6"_s; + case Code: + return u"Code"_s; + case Quote: + return u"\"Quote\""_s; + default: + return {}; + } +}; + +RichFormat::FormatType RichFormat::typeForFormat(Format format) +{ + switch (format) { + case Code: + case Quote: + return Block; + case Paragraph: + case Heading1: + case Heading2: + case Heading3: + case Heading4: + case Heading5: + case Heading6: + return Style; + case UnorderedList: + case OrderedList: + return List; + default: + return Text; + } +}; + +QTextListFormat::Style RichFormat::listStyleForFormat(Format format) +{ + switch (format) { + case UnorderedList: + return QTextListFormat::ListDisc; + case OrderedList: + return QTextListFormat::ListDecimal; + default: + return QTextListFormat::ListStyleUndefined; + } +} + +QTextCharFormat RichFormat::charFormatForFormat(Format format, bool invert) +{ + QTextCharFormat charFormat; + if (format == Bold || headingLevelForFormat(format) > 0) { + const auto weight = invert ? QFont::Normal : QFont::Bold; + charFormat.setFontWeight(weight); + } + if (format == Italic) { + charFormat.setFontItalic(!invert); + } + if (format == Underline) { + charFormat.setFontUnderline(!invert); + } + if (format == Strikethrough) { + charFormat.setFontStrikeOut(!invert); + } + if (headingLevelForFormat(format) > 0) { + // Apparently, 4 is maximum for FontSizeAdjustment; otherwise level=1 and + // level=2 look the same + charFormat.setProperty(QTextFormat::FontSizeAdjustment, 5 - headingLevelForFormat(format)); + } + if (format == Paragraph) { + charFormat.setFontWeight(QFont::Normal); + charFormat.setProperty(QTextFormat::FontSizeAdjustment, 0); + } + return charFormat; +} + +QTextBlockFormat RichFormat::blockFormatForFormat(Format format) +{ + QTextBlockFormat blockformat; + blockformat.setHeadingLevel(headingLevelForFormat(format)); + return blockformat; +} + +int RichFormat::headingLevelForFormat(Format format) +{ + const auto intFormat = int(format); + return intFormat <= 6 ? intFormat : 0; +} + +RichFormat::Format RichFormat::formatForHeadingLevel(int level) +{ + auto clampLevel = level > 6 ? 0 : level; + clampLevel = std::clamp(clampLevel, 0, 6); + return static_cast(clampLevel); +} + +bool RichFormat::hasFormat(QTextCursor cursor, Format format) +{ + switch (format) { + case Paragraph: + return cursor.blockFormat().headingLevel() == headingLevelForFormat(format); + case Heading1: + case Heading2: + case Heading3: + case Heading4: + case Heading5: + case Heading6: + return cursor.blockFormat().headingLevel() == headingLevelForFormat(format) && cursor.charFormat().fontWeight() == QFont::Bold; + case Quote: + return cursor.blockFormat().headingLevel() == headingLevelForFormat(format) && cursor.charFormat().fontItalic(); + case Code: + return cursor.blockFormat().headingLevel() == headingLevelForFormat(format); + case Bold: + return cursor.charFormat().fontWeight() == QFont::Bold; + case Italic: + return cursor.charFormat().fontItalic(); + case UnorderedList: + return cursor.currentList()->format().style() == QTextListFormat::ListDisc; + case OrderedList: + return cursor.currentList()->format().style() == QTextListFormat::ListDecimal; + case Strikethrough: + return cursor.charFormat().fontStrikeOut(); + case Underline: + return cursor.charFormat().fontUnderline(); + default: + return false; + } +} + +QList RichFormat::formatsAtCursor(const QTextCursor &cursor) +{ + QList formats; + if (cursor.isNull()) { + return formats; + } + if (cursor.charFormat().fontWeight() == QFont::Bold) { + formats += Bold; + } + if (cursor.charFormat().fontItalic()) { + formats += Italic; + } + if (cursor.charFormat().fontUnderline()) { + formats += Underline; + } + if (cursor.charFormat().fontStrikeOut()) { + formats += Strikethrough; + } + if (cursor.blockFormat().headingLevel() > 0 && cursor.blockFormat().headingLevel() <= 6) { + formats += formatForHeadingLevel(cursor.blockFormat().headingLevel()); + } + if (cursor.currentList() && cursor.currentList()->format().style() == QTextListFormat::ListDisc) { + formats += UnorderedList; + } + if (cursor.currentList() && cursor.currentList()->format().style() == QTextListFormat::ListDecimal) { + formats += OrderedList; + } + return formats; +} diff --git a/src/libneochat/enums/richformat.h b/src/libneochat/enums/richformat.h new file mode 100644 index 000000000..18671aacd --- /dev/null +++ b/src/libneochat/enums/richformat.h @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2025 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#pragma once + +#include +#include +#include + +class QTextBlockFormat; +class QTextCharFormat; +class QTextCursor; + +using namespace Qt::StringLiterals; + +/** + * @class RichFormat + */ +class RichFormat : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + +public: + /** + * @brief Enum to define available formats. + * + * @note The Paragraph and Heading values are intentially fixed to match heading + * level values returned by QTextBlockFormat::headingLevel(). + * + * @sa QTextBlockFormat::headingLevel() + */ + enum Format { + Paragraph = 0, + Heading1 = 1, + Heading2 = 2, + Heading3 = 3, + Heading4 = 4, + Heading5 = 5, + Heading6 = 6, + Quote = 7, + Code = 8, + InlineCode, + Bold, + Italic, + UnorderedList, + OrderedList, + Strikethrough, + Underline, + }; + Q_ENUM(Format); + + /** + * @brief Enum to define the type of format. + */ + enum FormatType { + Text, /**< The format is applied to the text chars. */ + List, /**< The format is list style. */ + Style, /**< The format is a paragraph style. */ + Block, /**< The format changes the block type. */ + }; + Q_ENUM(FormatType); + + /** + * @brief Translate the Format enum value to a human readable string. + * + * @sa Format + */ + static QString styleString(Format format); + + /** + * @brief Return the FormatType for the Format. + * + * @sa Format, FormatType + */ + static FormatType typeForFormat(Format format); + + /** + * @brief Return the QTextListFormat::Style for the Format. + * + * @sa Format, QTextListFormat::Style + */ + static QTextListFormat::Style listStyleForFormat(Format format); + + /** + * @brief Return the QTextCharFormat for the Format. + * + * Inverting returns a format which will remove the format when merged using + * QTextCursor::mergeCharFormat(). + * + * @sa Format, QTextCharFormat + */ + static QTextCharFormat charFormatForFormat(Format format, bool invert = false); + + /** + * @brief Return the QTextBlockFormat for the Format. + * + * @sa Format, QTextBlockFormat + */ + static QTextBlockFormat blockFormatForFormat(Format format); + + /** + * @brief Whether the given QTextCursor has the given Format. + * + * @sa Format, QTextCursor + */ + static bool hasFormat(QTextCursor cursor, Format format); + + /** + * @brief A lsit of Formats on the given QTextCursor. + * + * @sa Format, QTextCursor + */ + static QList formatsAtCursor(const QTextCursor &cursor); + +private: + /** + * @brief Return the heading level for the Format. + * + * @sa Format + */ + static int headingLevelForFormat(Format format); + + /** + * @brief Return the Format for the heading level. + * + * @sa Format + */ + static RichFormat::Format formatForHeadingLevel(int level); +}; diff --git a/src/libneochat/enums/textstyle.h b/src/libneochat/enums/textstyle.h deleted file mode 100644 index 40d08ab80..000000000 --- a/src/libneochat/enums/textstyle.h +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-FileCopyrightText: 2025 James Graham -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -#pragma once - -#include -#include - -using namespace Qt::StringLiterals; - -/** - * @class TextStyle - * - * A class with the Style enum for available text styles. - */ -class TextStyle : public QObject -{ - Q_OBJECT - QML_ELEMENT - QML_UNCREATABLE("") - -public: - /** - * @brief Enum to define available styles. - * - * @note The Paragraph and Heading values are intentially fixed to match heading - * level values returned by QTextBlockFormat::headingLevel(). - * - * @sa QTextBlockFormat::headingLevel() - */ - enum Style { - Paragraph = 0, - Heading1 = 1, - Heading2 = 2, - Heading3 = 3, - Heading4 = 4, - Heading5 = 5, - Heading6 = 6, - Code = 7, - Quote = 8, - }; - Q_ENUM(Style); - - /** - * @brief Translate the Kind enum value to a human readable string. - * - * @sa Kind - */ - static QString styleString(Style style) - { - switch (style) { - case Style::Paragraph: - return u"Paragraph"_s; - case Style::Heading1: - return u"Heading 1"_s; - case Style::Heading2: - return u"Heading 2"_s; - case Style::Heading3: - return u"Heading 3"_s; - case Style::Heading4: - return u"Heading 4"_s; - case Style::Heading5: - return u"Heading 5"_s; - case Style::Heading6: - return u"Heading 6"_s; - case Style::Code: - return u"Code"_s; - case Style::Quote: - return u"\"Quote\""_s; - default: - return {}; - } - }; -}; diff --git a/src/libneochat/nestedlisthelper.cpp b/src/libneochat/nestedlisthelper.cpp index dd153f501..a183335c1 100644 --- a/src/libneochat/nestedlisthelper.cpp +++ b/src/libneochat/nestedlisthelper.cpp @@ -219,11 +219,16 @@ void NestedListHelper::handleOnIndentLess(const QTextCursor &textCursor) } } -void NestedListHelper::handleOnBulletType(int styleIndex, const QTextCursor &textCursor) +void NestedListHelper::handleOnBulletType(QTextListFormat::Style style, QTextCursor cursor) { - QTextCursor cursor = textCursor; - if (styleIndex != 0) { - auto style = static_cast(styleIndex); + if (cursor.isNull()) { + return; + } + QTextListFormat::Style currentListStyle = QTextListFormat::ListStyleUndefined; + if (cursor.currentList()) { + currentListStyle = cursor.currentList()->format().style(); + } + if (style != currentListStyle && style != QTextListFormat::ListStyleUndefined) { QTextList *currentList = cursor.currentList(); QTextListFormat listFmt; @@ -245,5 +250,5 @@ void NestedListHelper::handleOnBulletType(int styleIndex, const QTextCursor &tex cursor.setBlockFormat(bfmt); } - reformatList(textCursor.block()); + reformatList(cursor.block()); } diff --git a/src/libneochat/nestedlisthelper_p.h b/src/libneochat/nestedlisthelper_p.h index af29bdf79..14e66ef24 100644 --- a/src/libneochat/nestedlisthelper_p.h +++ b/src/libneochat/nestedlisthelper_p.h @@ -8,10 +8,11 @@ #pragma once +#include + class QKeyEvent; class QTextCursor; class QTextBlock; -class QTextList; /** * @@ -73,7 +74,7 @@ public: * * @param styleIndex The QTextListStyle of the list. */ - void handleOnBulletType(int styleIndex, const QTextCursor &textCursor); + void handleOnBulletType(QTextListFormat::Style style, QTextCursor cursor); /** * @brief Check whether the current item in the list may be indented. diff --git a/src/messagecontent/TextComponent.qml b/src/messagecontent/TextComponent.qml index e2bc7a99e..6b007d4da 100644 --- a/src/messagecontent/TextComponent.qml +++ b/src/messagecontent/TextComponent.qml @@ -90,6 +90,11 @@ TextEdit { } } + Keys.onTabPressed: (event) => { + event.accepted = true; + chatDocumentHandler.tab(); + } + Keys.onDeletePressed: (event) => { event.accepted = true; chatDocumentHandler.deleteChar(); @@ -104,6 +109,15 @@ TextEdit { event.accepted = false; } + Keys.onEnterPressed: (event) => { + event.accepted = true; + chatDocumentHandler.insertReturn(); + } + Keys.onReturnPressed: (event) => { + event.accepted = true; + chatDocumentHandler.insertReturn(); + } + onFocusChanged: if (focus && !root.currentFocus) { Message.contentModel.setFocusRow(root.index, true) } diff --git a/src/messagecontent/models/chatbarmessagecontentmodel.cpp b/src/messagecontent/models/chatbarmessagecontentmodel.cpp index d406557df..ad0c3ee85 100644 --- a/src/messagecontent/models/chatbarmessagecontentmodel.cpp +++ b/src/messagecontent/models/chatbarmessagecontentmodel.cpp @@ -9,7 +9,6 @@ #include "chatdocumenthandler.h" #include "enums/chatbartype.h" #include "enums/messagecomponenttype.h" -#include "enums/textstyle.h" #include "messagecontentmodel.h" ChatBarMessageContentModel::ChatBarMessageContentModel(QObject *parent) @@ -278,16 +277,16 @@ ChatBarMessageContentModel::insertComponent(int row, MessageComponentType::Type return it; } -void ChatBarMessageContentModel::insertStyleAtCursor(TextStyle::Style style) +void ChatBarMessageContentModel::insertStyleAtCursor(RichFormat::Format style) { switch (style) { - case TextStyle::Paragraph: + case RichFormat::Paragraph: insertComponentAtCursor(MessageComponentType::Text); return; - case TextStyle::Code: + case RichFormat::Code: insertComponentAtCursor(MessageComponentType::Code); return; - case TextStyle::Quote: + case RichFormat::Quote: insertComponentAtCursor(MessageComponentType::Quote); return; default: @@ -299,7 +298,7 @@ void ChatBarMessageContentModel::insertComponentAtCursor(MessageComponentType::T { if (m_components[m_currentFocusComponent.row()].type == type) { if (type == MessageComponentType::Text && focusedDocumentHandler()) { - focusedDocumentHandler()->setStyle(TextStyle::Paragraph); + focusedDocumentHandler()->setFormat(RichFormat::Paragraph); } return; } diff --git a/src/messagecontent/models/chatbarmessagecontentmodel.h b/src/messagecontent/models/chatbarmessagecontentmodel.h index e1385cb3a..42a748ba5 100644 --- a/src/messagecontent/models/chatbarmessagecontentmodel.h +++ b/src/messagecontent/models/chatbarmessagecontentmodel.h @@ -9,7 +9,7 @@ #include "chatdocumenthandler.h" #include "enums/messagecomponenttype.h" -#include "enums/textstyle.h" +#include "enums/richformat.h" #include "messagecomponent.h" #include "models/messagecontentmodel.h" @@ -56,7 +56,7 @@ public: Q_INVOKABLE void refocusCurrentComponent() const; ChatDocumentHandler *focusedDocumentHandler() const; - Q_INVOKABLE void insertStyleAtCursor(TextStyle::Style style); + Q_INVOKABLE void insertStyleAtCursor(RichFormat::Format style); Q_INVOKABLE void insertComponentAtCursor(MessageComponentType::Type type);