Add automatic markdown formatting

This commit is contained in:
James Graham
2025-11-06 20:46:51 +00:00
parent 11bf741554
commit 4db1e1c437
16 changed files with 801 additions and 313 deletions

View File

@@ -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();
}
}

View File

@@ -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 {

View File

@@ -7,7 +7,7 @@
#include <QTextCursor>
#include <QTextDocument>
#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<TextStyle::Style>(m_textItem->property("index").toInt());
const auto string = TextStyle::styleString(style);
const auto style = static_cast<RichFormat::Format>(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);
}

View File

@@ -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

View File

@@ -21,6 +21,7 @@
#include <Sonnet/BackgroundChecker>
#include <Sonnet/Settings>
#include <qfont.h>
#include <qlogging.h>
#include <qnamespace.h>
#include <qtextcursor.h>
@@ -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<Qt::TextFormat> ChatDocumentHandler::textFormat() const
{
if (!m_textItem) {
@@ -812,9 +735,14 @@ std::optional<Qt::TextFormat> 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<TextStyle::Style>(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<RichFormat::Format>(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);

View File

@@ -11,8 +11,9 @@
#include <qtextdocumentfragment.h>
#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<NeoChatRoom> m_room;
QPointer<QQuickItem> m_textItem;
QTextDocument *document() const;
@@ -273,22 +245,26 @@ private:
QString m_initialText = {};
void initializeChars();
int completionStartIndex() const;
QPointer<NeoChatRoom> 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<ChatMarkdownHelper> 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<Qt::TextFormat> textFormat() const;
void mergeFormatOnWordOrSelection(const QTextCharFormat &format);
void selectLinkText(QTextCursor *cursor) const;

View File

@@ -0,0 +1,235 @@
// 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
#include "chatmarkdownhelper.h"
#include <QTextDocument>
#include <qtextcursor.h>
#include "chatdocumenthandler.h"
struct MarkdownSyntax {
QLatin1String sequence;
bool closable = false;
bool lineStart = false;
RichFormat::Format format;
};
static 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},
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<bool> checkSequence(const QString &currentString, const QString &nextChar, bool lineStart = false)
{
QList<MarkdownSyntax> partialMatches;
std::optional<MarkdownSyntax> 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<MarkdownSyntax> 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<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;
}
void ChatMarkdownHelper::connectDocument()
{
disconnect();
if (document()) {
m_startPos = qobject_cast<ChatDocumentHandler *>(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<ChatDocumentHandler *>(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"

View File

@@ -0,0 +1,43 @@
// 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 <QQmlEngine>
#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<RichFormat::Format, int> m_currentFormats;
void checkMarkdown(int position, int charsRemoved, int charsAdded);
void complete();
};

View File

@@ -0,0 +1,180 @@
// 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
#include "richformat.h"
#include <QTextBlockFormat>
#include <QTextCharFormat>
#include <QTextCursor>
#include <qtextformat.h>
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<Format>(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::Format> RichFormat::formatsAtCursor(const QTextCursor &cursor)
{
QList<Format> 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;
}

View File

@@ -0,0 +1,131 @@
// 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 <QQmlEngine>
#include <QTextList>
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<Format> 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);
};

View File

@@ -1,74 +0,0 @@
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QObject>
#include <QQmlEngine>
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 {};
}
};
};

View File

@@ -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<QTextListFormat::Style>(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());
}

View File

@@ -8,10 +8,11 @@
#pragma once
#include <QTextList>
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.

View File

@@ -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)
}

View File

@@ -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;
}

View File

@@ -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);