Move the remaining functionality of ChatDocumentHandler to ChatTextItemHelper or split into own objects
This commit is contained in:
@@ -122,7 +122,7 @@ endmacro()
|
|||||||
|
|
||||||
add_executable(qmltest qmltest.cpp
|
add_executable(qmltest qmltest.cpp
|
||||||
chatmarkdownhelpertestwrapper.h
|
chatmarkdownhelpertestwrapper.h
|
||||||
qmltextitemwrappertestwrapper.h
|
chattextitemhelpertestwrapper.h
|
||||||
)
|
)
|
||||||
qt_add_qml_module(qmltest URI NeoChatTestUtils)
|
qt_add_qml_module(qmltest URI NeoChatTestUtils)
|
||||||
|
|
||||||
@@ -135,7 +135,6 @@ target_link_libraries(qmltest
|
|||||||
)
|
)
|
||||||
|
|
||||||
add_qml_tests(
|
add_qml_tests(
|
||||||
chatdocumenthelpertest.qml
|
chattextitemhelpertest.qml
|
||||||
chatmarkdownhelpertest.qml
|
chatmarkdownhelpertest.qml
|
||||||
qmltextitemwrappertest.qml
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 Carl Schwan <carl@carlschwan.eu>
|
|
||||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtTest
|
|
||||||
|
|
||||||
import org.kde.neochat.libneochat as LibNeoChat
|
|
||||||
|
|
||||||
TestCase {
|
|
||||||
name: "ChatDocumentHandlerTest"
|
|
||||||
|
|
||||||
LibNeoChat.ChatDocumentHandler {
|
|
||||||
id: documentHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
TextEdit {
|
|
||||||
id: textEdit
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_empty(): void {
|
|
||||||
compare(documentHandler.type, LibNeoChat.ChatBarType.None);
|
|
||||||
compare(documentHandler.room, null);
|
|
||||||
compare(documentHandler.textItem, null);
|
|
||||||
compare(documentHandler.atFirstLine, false);
|
|
||||||
compare(documentHandler.atLastLine, false);
|
|
||||||
compare(documentHandler.bold, false);
|
|
||||||
compare(documentHandler.italic, false);
|
|
||||||
compare(documentHandler.underline, false);
|
|
||||||
compare(documentHandler.strikethrough, false);
|
|
||||||
compare(documentHandler.style, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,8 +9,8 @@
|
|||||||
#include <qtextcursor.h>
|
#include <qtextcursor.h>
|
||||||
|
|
||||||
#include "chatmarkdownhelper.h"
|
#include "chatmarkdownhelper.h"
|
||||||
|
#include "chattextitemhelper.h"
|
||||||
#include "enums/richformat.h"
|
#include "enums/richformat.h"
|
||||||
#include "qmltextitemwrapper.h"
|
|
||||||
|
|
||||||
class ChatMarkdownHelperTestWrapper : public QObject
|
class ChatMarkdownHelperTestWrapper : public QObject
|
||||||
{
|
{
|
||||||
@@ -18,7 +18,7 @@ class ChatMarkdownHelperTestWrapper : public QObject
|
|||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The QML text Item the ChatDocumentHandler is handling.
|
* @brief The QML text Item the ChatMerkdownHelper is handling.
|
||||||
*/
|
*/
|
||||||
Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
|
Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ public:
|
|||||||
explicit ChatMarkdownHelperTestWrapper(QObject *parent = nullptr)
|
explicit ChatMarkdownHelperTestWrapper(QObject *parent = nullptr)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_chatMarkdownHelper(new ChatMarkdownHelper(this))
|
, m_chatMarkdownHelper(new ChatMarkdownHelper(this))
|
||||||
, m_textItem(new QmlTextItemWrapper(this))
|
, m_textItem(new ChatTextItemHelper(this))
|
||||||
{
|
{
|
||||||
connect(m_chatMarkdownHelper, &ChatMarkdownHelper::textItemChanged, this, &ChatMarkdownHelperTestWrapper::textItemChanged);
|
connect(m_chatMarkdownHelper, &ChatMarkdownHelper::textItemChanged, this, &ChatMarkdownHelperTestWrapper::textItemChanged);
|
||||||
connect(m_chatMarkdownHelper, &ChatMarkdownHelper::unhandledBlockFormat, this, &ChatMarkdownHelperTestWrapper::unhandledBlockFormat);
|
connect(m_chatMarkdownHelper, &ChatMarkdownHelper::unhandledBlockFormat, this, &ChatMarkdownHelperTestWrapper::unhandledBlockFormat);
|
||||||
@@ -38,7 +38,7 @@ public:
|
|||||||
}
|
}
|
||||||
void setTextItem(QQuickItem *textItem)
|
void setTextItem(QQuickItem *textItem)
|
||||||
{
|
{
|
||||||
auto textItemWrapper = new QmlTextItemWrapper(this);
|
auto textItemWrapper = new ChatTextItemHelper(this);
|
||||||
textItemWrapper->setTextItem(textItem);
|
textItemWrapper->setTextItem(textItem);
|
||||||
m_chatMarkdownHelper->setTextItem(textItemWrapper);
|
m_chatMarkdownHelper->setTextItem(textItemWrapper);
|
||||||
m_textItem->setTextItem(textItem);
|
m_textItem->setTextItem(textItem);
|
||||||
@@ -82,5 +82,5 @@ Q_SIGNALS:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
QPointer<ChatMarkdownHelper> m_chatMarkdownHelper;
|
QPointer<ChatMarkdownHelper> m_chatMarkdownHelper;
|
||||||
QPointer<QmlTextItemWrapper> m_textItem;
|
QPointer<ChatTextItemHelper> m_textItem;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import QtTest
|
|||||||
import NeoChatTestUtils
|
import NeoChatTestUtils
|
||||||
|
|
||||||
TestCase {
|
TestCase {
|
||||||
name: "QmlTextItemWrapperTest"
|
name: "ChatTextItemHelperTest"
|
||||||
|
|
||||||
TextEdit {
|
TextEdit {
|
||||||
id: textEdit
|
id: textEdit
|
||||||
@@ -17,51 +17,51 @@ TestCase {
|
|||||||
id: textEdit2
|
id: textEdit2
|
||||||
}
|
}
|
||||||
|
|
||||||
QmlTextItemWrapperTestWrapper {
|
ChatTextItemHelperTestWrapper {
|
||||||
id: qmlTextItemWrapper
|
id: chatTextItemHelper
|
||||||
|
|
||||||
textItem: textEdit
|
textItem: textEdit
|
||||||
}
|
}
|
||||||
|
|
||||||
SignalSpy {
|
SignalSpy {
|
||||||
id: spyItem
|
id: spyItem
|
||||||
target: qmlTextItemWrapper
|
target: chatTextItemHelper
|
||||||
signalName: "textItemChanged"
|
signalName: "textItemChanged"
|
||||||
}
|
}
|
||||||
|
|
||||||
SignalSpy {
|
SignalSpy {
|
||||||
id: spyContentsChanged
|
id: spyContentsChanged
|
||||||
target: qmlTextItemWrapper
|
target: chatTextItemHelper
|
||||||
signalName: "contentsChanged"
|
signalName: "contentsChanged"
|
||||||
}
|
}
|
||||||
|
|
||||||
SignalSpy {
|
SignalSpy {
|
||||||
id: spyContentsChange
|
id: spyContentsChange
|
||||||
target: qmlTextItemWrapper
|
target: chatTextItemHelper
|
||||||
signalName: "contentsChange"
|
signalName: "contentsChange"
|
||||||
}
|
}
|
||||||
|
|
||||||
SignalSpy {
|
SignalSpy {
|
||||||
id: spyCursor
|
id: spyCursor
|
||||||
target: qmlTextItemWrapper
|
target: chatTextItemHelper
|
||||||
signalName: "cursorPositionChanged"
|
signalName: "cursorPositionChanged"
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_item(): void {
|
function test_item(): void {
|
||||||
spyItem.clear();
|
spyItem.clear();
|
||||||
compare(qmlTextItemWrapper.textItem, textEdit);
|
compare(chatTextItemHelper.textItem, textEdit);
|
||||||
compare(spyItem.count, 0);
|
compare(spyItem.count, 0);
|
||||||
qmlTextItemWrapper.textItem = textEdit2;
|
chatTextItemHelper.textItem = textEdit2;
|
||||||
compare(qmlTextItemWrapper.textItem, textEdit2);
|
compare(chatTextItemHelper.textItem, textEdit2);
|
||||||
compare(spyItem.count, 1);
|
compare(spyItem.count, 1);
|
||||||
qmlTextItemWrapper.textItem = textEdit;
|
chatTextItemHelper.textItem = textEdit;
|
||||||
compare(qmlTextItemWrapper.textItem, textEdit);
|
compare(chatTextItemHelper.textItem, textEdit);
|
||||||
compare(spyItem.count, 2);
|
compare(spyItem.count, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_document(): void {
|
function test_document(): void {
|
||||||
// We can't get to the QTextDocument from QML so we have to use a helper function.
|
// We can't get to the QTextDocument from QML so we have to use a helper function.
|
||||||
compare(qmlTextItemWrapper.compareDocuments(textEdit.textDocument), true);
|
compare(chatTextItemHelper.compareDocuments(textEdit.textDocument), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_cursor(): void {
|
function test_cursor(): void {
|
||||||
@@ -69,8 +69,8 @@ TestCase {
|
|||||||
spyContentsChanged.clear();
|
spyContentsChanged.clear();
|
||||||
spyCursor.clear();
|
spyCursor.clear();
|
||||||
// We can't get to the QTextCursor from QML so we have to use a helper function.
|
// We can't get to the QTextCursor from QML so we have to use a helper function.
|
||||||
compare(qmlTextItemWrapper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
|
compare(chatTextItemHelper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
|
||||||
compare(textEdit.cursorPosition, qmlTextItemWrapper.cursorPosition());
|
compare(textEdit.cursorPosition, chatTextItemHelper.cursorPosition());
|
||||||
textEdit.insert(0, "test text")
|
textEdit.insert(0, "test text")
|
||||||
compare(spyContentsChange.count, 1);
|
compare(spyContentsChange.count, 1);
|
||||||
compare(spyContentsChange.signalArguments[0][0], 0);
|
compare(spyContentsChange.signalArguments[0][0], 0);
|
||||||
@@ -78,13 +78,13 @@ TestCase {
|
|||||||
compare(spyContentsChange.signalArguments[0][2], 9);
|
compare(spyContentsChange.signalArguments[0][2], 9);
|
||||||
compare(spyContentsChanged.count, 1);
|
compare(spyContentsChanged.count, 1);
|
||||||
compare(spyCursor.count, 1);
|
compare(spyCursor.count, 1);
|
||||||
compare(qmlTextItemWrapper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
|
compare(chatTextItemHelper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
|
||||||
compare(textEdit.cursorPosition, qmlTextItemWrapper.cursorPosition());
|
compare(textEdit.cursorPosition, chatTextItemHelper.cursorPosition());
|
||||||
textEdit.selectAll();
|
textEdit.selectAll();
|
||||||
compare(spyContentsChanged.count, 1);
|
compare(spyContentsChanged.count, 1);
|
||||||
compare(spyCursor.count, 1);
|
compare(spyCursor.count, 1);
|
||||||
compare(qmlTextItemWrapper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
|
compare(chatTextItemHelper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
|
||||||
compare(textEdit.cursorPosition, qmlTextItemWrapper.cursorPosition());
|
compare(textEdit.cursorPosition, chatTextItemHelper.cursorPosition());
|
||||||
textEdit.clear();
|
textEdit.clear();
|
||||||
compare(spyContentsChange.count, 2);
|
compare(spyContentsChange.count, 2);
|
||||||
compare(spyContentsChange.signalArguments[1][0], 0);
|
compare(spyContentsChange.signalArguments[1][0], 0);
|
||||||
@@ -100,18 +100,18 @@ TestCase {
|
|||||||
textEdit.insert(0, "test text");
|
textEdit.insert(0, "test text");
|
||||||
compare(textEdit.cursorPosition, 9);
|
compare(textEdit.cursorPosition, 9);
|
||||||
compare(spyCursor.count, 1);
|
compare(spyCursor.count, 1);
|
||||||
qmlTextItemWrapper.setCursorPosition(5);
|
chatTextItemHelper.setCursorPosition(5);
|
||||||
compare(textEdit.cursorPosition, 5);
|
compare(textEdit.cursorPosition, 5);
|
||||||
compare(spyCursor.count, 2);
|
compare(spyCursor.count, 2);
|
||||||
qmlTextItemWrapper.setCursorPosition(1);
|
chatTextItemHelper.setCursorPosition(1);
|
||||||
compare(textEdit.cursorPosition, 1);
|
compare(textEdit.cursorPosition, 1);
|
||||||
compare(spyCursor.count, 3);
|
compare(spyCursor.count, 3);
|
||||||
|
|
||||||
textEdit.cursorVisible = false;
|
textEdit.cursorVisible = false;
|
||||||
compare(textEdit.cursorVisible, false);
|
compare(textEdit.cursorVisible, false);
|
||||||
qmlTextItemWrapper.setCursorVisible(true);
|
chatTextItemHelper.setCursorVisible(true);
|
||||||
compare(textEdit.cursorVisible, true);
|
compare(textEdit.cursorVisible, true);
|
||||||
qmlTextItemWrapper.setCursorVisible(false);
|
chatTextItemHelper.setCursorVisible(false);
|
||||||
compare(textEdit.cursorVisible, false);
|
compare(textEdit.cursorVisible, false);
|
||||||
|
|
||||||
textEdit.clear();
|
textEdit.clear();
|
||||||
@@ -121,7 +121,7 @@ TestCase {
|
|||||||
function test_forceActiveFocus(): void {
|
function test_forceActiveFocus(): void {
|
||||||
textEdit2.forceActiveFocus();
|
textEdit2.forceActiveFocus();
|
||||||
compare(textEdit.activeFocus, false);
|
compare(textEdit.activeFocus, false);
|
||||||
qmlTextItemWrapper.forceActiveFocus();
|
chatTextItemHelper.forceActiveFocus();
|
||||||
compare(textEdit.activeFocus, true);
|
compare(textEdit.activeFocus, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,28 +8,28 @@
|
|||||||
#include <QQuickTextDocument>
|
#include <QQuickTextDocument>
|
||||||
#include <QTextCursor>
|
#include <QTextCursor>
|
||||||
|
|
||||||
#include "qmltextitemwrapper.h"
|
#include "chattextitemhelper.h"
|
||||||
|
|
||||||
class QmlTextItemWrapperTestWrapper : public QObject
|
class ChatTextItemHelperTestWrapper : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The QML text Item the ChatDocumentHandler is handling.
|
* @brief The QML text Item the TextItemHelper is handling.
|
||||||
*/
|
*/
|
||||||
Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
|
Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit QmlTextItemWrapperTestWrapper(QObject *parent = nullptr)
|
explicit ChatTextItemHelperTestWrapper(QObject *parent = nullptr)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_textItemWrapper(new QmlTextItemWrapper(this))
|
, m_textItemWrapper(new ChatTextItemHelper(this))
|
||||||
{
|
{
|
||||||
Q_ASSERT(m_textItemWrapper);
|
Q_ASSERT(m_textItemWrapper);
|
||||||
connect(m_textItemWrapper, &QmlTextItemWrapper::textItemChanged, this, &QmlTextItemWrapperTestWrapper::textItemChanged);
|
connect(m_textItemWrapper, &ChatTextItemHelper::textItemChanged, this, &ChatTextItemHelperTestWrapper::textItemChanged);
|
||||||
connect(m_textItemWrapper, &QmlTextItemWrapper::contentsChange, this, &QmlTextItemWrapperTestWrapper::contentsChange);
|
connect(m_textItemWrapper, &ChatTextItemHelper::contentsChange, this, &ChatTextItemHelperTestWrapper::contentsChange);
|
||||||
connect(m_textItemWrapper, &QmlTextItemWrapper::contentsChanged, this, &QmlTextItemWrapperTestWrapper::contentsChanged);
|
connect(m_textItemWrapper, &ChatTextItemHelper::contentsChanged, this, &ChatTextItemHelperTestWrapper::contentsChanged);
|
||||||
connect(m_textItemWrapper, &QmlTextItemWrapper::cursorPositionChanged, this, &QmlTextItemWrapperTestWrapper::cursorPositionChanged);
|
connect(m_textItemWrapper, &ChatTextItemHelper::cursorPositionChanged, this, &ChatTextItemHelperTestWrapper::cursorPositionChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
QQuickItem *textItem() const
|
QQuickItem *textItem() const
|
||||||
@@ -85,5 +85,5 @@ Q_SIGNALS:
|
|||||||
void cursorPositionChanged();
|
void cursorPositionChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QPointer<QmlTextItemWrapper> m_textItemWrapper;
|
QPointer<ChatTextItemHelper> m_textItemWrapper;
|
||||||
};
|
};
|
||||||
@@ -149,7 +149,9 @@ QQC2.Control {
|
|||||||
|
|
||||||
CompletionMenu {
|
CompletionMenu {
|
||||||
id: completionMenu
|
id: completionMenu
|
||||||
chatDocumentHandler: contentModel.focusedDocumentHandler
|
room: root.currentRoom
|
||||||
|
type: LibNeoChat.ChatBarType.Room
|
||||||
|
textItem: chatContentView.model.focusedTextItem
|
||||||
|
|
||||||
x: 1
|
x: 1
|
||||||
y: -height
|
y: -height
|
||||||
|
|||||||
@@ -12,11 +12,25 @@ import org.kde.kirigamiaddons.delegates as Delegates
|
|||||||
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
||||||
|
|
||||||
import org.kde.neochat
|
import org.kde.neochat
|
||||||
|
import org.kde.neochat.libneochat as LibNeoChat
|
||||||
|
|
||||||
QQC2.Popup {
|
QQC2.Popup {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property var chatDocumentHandler
|
/**
|
||||||
|
* @brief The current room that user is viewing.
|
||||||
|
*/
|
||||||
|
required property LibNeoChat.NeoChatRoom room
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The chatbar type
|
||||||
|
*/
|
||||||
|
required property int type
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The chatbar type
|
||||||
|
*/
|
||||||
|
required property LibNeoChat.ChatTextItemHelper textItem
|
||||||
|
|
||||||
visible: completions.count > 0
|
visible: completions.count > 0
|
||||||
|
|
||||||
@@ -33,7 +47,7 @@ QQC2.Popup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function complete() {
|
function complete() {
|
||||||
root.chatDocumentHandler.insertCompletion(completions.currentItem.replacedText, completions.currentItem.hRef)
|
completionModel.insertCompletion(completions.currentItem.replacedText, completions.currentItem.hRef)
|
||||||
}
|
}
|
||||||
|
|
||||||
leftPadding: 0
|
leftPadding: 0
|
||||||
@@ -60,8 +74,11 @@ QQC2.Popup {
|
|||||||
ListView {
|
ListView {
|
||||||
id: completions
|
id: completions
|
||||||
|
|
||||||
model: CompletionModel {
|
model: LibNeoChat.CompletionModel {
|
||||||
textItem: root.chatDocumentHandler.textItem
|
id: completionModel
|
||||||
|
room: root.room
|
||||||
|
type: root.type
|
||||||
|
textItem: root.textItem
|
||||||
roomListModel: RoomManager.roomListModel
|
roomListModel: RoomManager.roomListModel
|
||||||
userListModel: RoomManager.userListModel
|
userListModel: RoomManager.userListModel
|
||||||
}
|
}
|
||||||
@@ -97,7 +114,7 @@ QQC2.Popup {
|
|||||||
subtitleItem.textFormat: Text.PlainText
|
subtitleItem.textFormat: Text.PlainText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onClicked: root.chatDocumentHandler.insertCompletion(replacedText, hRef)
|
onClicked: completionModel.insertCompletion(replacedText, hRef)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ QQC2.ToolBar {
|
|||||||
property LibNeoChat.ChatBarCache chatBarCache
|
property LibNeoChat.ChatBarCache chatBarCache
|
||||||
|
|
||||||
required property MessageContent.ChatBarMessageContentModel contentModel
|
required property MessageContent.ChatBarMessageContentModel contentModel
|
||||||
readonly property LibNeoChat.ChatDocumentHandler focusedDocumentHandler: contentModel.focusedDocumentHandler
|
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: contentModel
|
target: contentModel
|
||||||
@@ -65,7 +64,7 @@ QQC2.ToolBar {
|
|||||||
3
|
3
|
||||||
|
|
||||||
readonly property ChatButtonHelper chatButtonHelper: ChatButtonHelper {
|
readonly property ChatButtonHelper chatButtonHelper: ChatButtonHelper {
|
||||||
textItem: contentModel.currentTextItem
|
textItem: contentModel.focusedTextItem
|
||||||
}
|
}
|
||||||
|
|
||||||
signal clicked
|
signal clicked
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ QQC2.Popup {
|
|||||||
|
|
||||||
required property MessageContent.ChatBarMessageContentModel chatContentModel
|
required property MessageContent.ChatBarMessageContentModel chatContentModel
|
||||||
required property ChatButtonHelper chatButtonHelper
|
required property ChatButtonHelper chatButtonHelper
|
||||||
readonly property LibNeoChat.ChatDocumentHandler focusedDocumentHandler: chatContentModel.focusedDocumentHandler
|
|
||||||
|
|
||||||
y: -implicitHeight
|
y: -implicitHeight
|
||||||
|
|
||||||
@@ -88,7 +87,7 @@ QQC2.Popup {
|
|||||||
radius: Kirigami.Units.cornerRadius
|
radius: Kirigami.Units.cornerRadius
|
||||||
border {
|
border {
|
||||||
width: 1
|
width: 1
|
||||||
color: styleDelegate.hovered || (root.focusedDocumentHandler?.style ?? false) === styleDelegate.index ?
|
color: styleDelegate.hovered || root.chatButtonHelper.currentStyle === styleDelegate.index ?
|
||||||
Kirigami.Theme.highlightColor :
|
Kirigami.Theme.highlightColor :
|
||||||
Kirigami.ColorUtils.linearInterpolation(
|
Kirigami.ColorUtils.linearInterpolation(
|
||||||
Kirigami.Theme.backgroundColor,
|
Kirigami.Theme.backgroundColor,
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ ChatButtonHelper::ChatButtonHelper(QObject *parent)
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
QmlTextItemWrapper *ChatButtonHelper::textItem() const
|
ChatTextItemHelper *ChatButtonHelper::textItem() const
|
||||||
{
|
{
|
||||||
return m_textItem;
|
return m_textItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatButtonHelper::setTextItem(QmlTextItemWrapper *textItem)
|
void ChatButtonHelper::setTextItem(ChatTextItemHelper *textItem)
|
||||||
{
|
{
|
||||||
if (textItem == m_textItem) {
|
if (textItem == m_textItem) {
|
||||||
return;
|
return;
|
||||||
@@ -30,11 +30,10 @@ void ChatButtonHelper::setTextItem(QmlTextItemWrapper *textItem)
|
|||||||
m_textItem = textItem;
|
m_textItem = textItem;
|
||||||
|
|
||||||
if (m_textItem) {
|
if (m_textItem) {
|
||||||
connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, &ChatButtonHelper::textItemChanged);
|
connect(m_textItem, &ChatTextItemHelper::formatChanged, this, &ChatButtonHelper::linkChanged);
|
||||||
connect(m_textItem, &QmlTextItemWrapper::formatChanged, this, &ChatButtonHelper::linkChanged);
|
connect(m_textItem, &ChatTextItemHelper::textFormatChanged, this, &ChatButtonHelper::textFormatChanged);
|
||||||
connect(m_textItem, &QmlTextItemWrapper::textFormatChanged, this, &ChatButtonHelper::textFormatChanged);
|
connect(m_textItem, &ChatTextItemHelper::styleChanged, this, &ChatButtonHelper::styleChanged);
|
||||||
connect(m_textItem, &QmlTextItemWrapper::styleChanged, this, &ChatButtonHelper::styleChanged);
|
connect(m_textItem, &ChatTextItemHelper::listChanged, this, &ChatButtonHelper::listChanged);
|
||||||
connect(m_textItem, &QmlTextItemWrapper::listChanged, this, &ChatButtonHelper::listChanged);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Q_EMIT textItemChanged();
|
Q_EMIT textItemChanged();
|
||||||
@@ -88,6 +87,14 @@ bool ChatButtonHelper::orderedList() const
|
|||||||
return m_textItem->formatsAtCursor().contains(RichFormat::OrderedList);
|
return m_textItem->formatsAtCursor().contains(RichFormat::OrderedList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RichFormat::Format ChatButtonHelper::currentStyle() const
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return RichFormat::Paragraph;
|
||||||
|
}
|
||||||
|
return static_cast<RichFormat::Format>(m_textItem->textCursor().blockFormat().headingLevel());
|
||||||
|
}
|
||||||
|
|
||||||
void ChatButtonHelper::setFormat(RichFormat::Format format)
|
void ChatButtonHelper::setFormat(RichFormat::Format format)
|
||||||
{
|
{
|
||||||
if (!m_textItem) {
|
if (!m_textItem) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
|
|
||||||
#include "qmltextitemwrapper.h"
|
#include "chattextitemhelper.h"
|
||||||
|
|
||||||
class ChatButtonHelper : public QObject
|
class ChatButtonHelper : public QObject
|
||||||
{
|
{
|
||||||
@@ -16,12 +16,12 @@ class ChatButtonHelper : public QObject
|
|||||||
/**
|
/**
|
||||||
* @brief The text item that the helper is interfacing with.
|
* @brief The text item that the helper is interfacing with.
|
||||||
*
|
*
|
||||||
* This is a QQuickItem that is a TextEdit (or inherited from) wrapped in a QmlTextItemWrapper
|
* This is a QQuickItem that is a TextEdit (or inherited from) wrapped in a ChatTextItemHelper
|
||||||
* to provide easy access to properties and basic QTextDocument manipulation.
|
* to provide easy access to properties and basic QTextDocument manipulation.
|
||||||
*
|
*
|
||||||
* @sa TextEdit, QTextDocument, QmlTextItemWrapper
|
* @sa TextEdit, QTextDocument, ChatTextItemHelper
|
||||||
*/
|
*/
|
||||||
Q_PROPERTY(QmlTextItemWrapper *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
|
Q_PROPERTY(ChatTextItemHelper *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Whether the text format at the current cursor is bold.
|
* @brief Whether the text format at the current cursor is bold.
|
||||||
@@ -53,6 +53,11 @@ class ChatButtonHelper : public QObject
|
|||||||
*/
|
*/
|
||||||
Q_PROPERTY(bool orderedList READ orderedList NOTIFY listChanged)
|
Q_PROPERTY(bool orderedList READ orderedList NOTIFY listChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The current style at the cursor.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(RichFormat::Format currentStyle READ currentStyle NOTIFY styleChanged)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Whether the list at the current cursor can be indented one level more.
|
* @brief Whether the list at the current cursor can be indented one level more.
|
||||||
*/
|
*/
|
||||||
@@ -76,8 +81,8 @@ class ChatButtonHelper : public QObject
|
|||||||
public:
|
public:
|
||||||
explicit ChatButtonHelper(QObject *parent = nullptr);
|
explicit ChatButtonHelper(QObject *parent = nullptr);
|
||||||
|
|
||||||
QmlTextItemWrapper *textItem() const;
|
ChatTextItemHelper *textItem() const;
|
||||||
void setTextItem(QmlTextItemWrapper *textItem);
|
void setTextItem(ChatTextItemHelper *textItem);
|
||||||
|
|
||||||
bool bold() const;
|
bool bold() const;
|
||||||
bool italic() const;
|
bool italic() const;
|
||||||
@@ -85,6 +90,7 @@ public:
|
|||||||
bool strikethrough() const;
|
bool strikethrough() const;
|
||||||
bool unorderedList() const;
|
bool unorderedList() const;
|
||||||
bool orderedList() const;
|
bool orderedList() const;
|
||||||
|
RichFormat::Format currentStyle() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Apply the given format at the current cursor position.
|
* @brief Apply the given format at the current cursor position.
|
||||||
@@ -129,7 +135,7 @@ Q_SIGNALS:
|
|||||||
void linkChanged();
|
void linkChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QPointer<QmlTextItemWrapper> m_textItem;
|
QPointer<ChatTextItemHelper> m_textItem;
|
||||||
|
|
||||||
void selectLinkText(QTextCursor &cursor) const;
|
void selectLinkText(QTextCursor &cursor) const;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class StyleDelegateHelper : public QObject
|
|||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The QML text Item the ChatDocumentHandler is handling.
|
* @brief The QML text Item the StyleDelegateHelper is handling.
|
||||||
*/
|
*/
|
||||||
Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
|
Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ target_sources(LibNeoChat PRIVATE
|
|||||||
neochatroommember.cpp
|
neochatroommember.cpp
|
||||||
accountmanager.cpp
|
accountmanager.cpp
|
||||||
chatbarcache.cpp
|
chatbarcache.cpp
|
||||||
chatdocumenthandler.cpp
|
chatbarsyntaxhighlighter.cpp
|
||||||
|
chatkeyhelper.cpp
|
||||||
chatmarkdownhelper.cpp
|
chatmarkdownhelper.cpp
|
||||||
|
chattextitemhelper.cpp
|
||||||
clipboard.cpp
|
clipboard.cpp
|
||||||
delegatesizehelper.cpp
|
delegatesizehelper.cpp
|
||||||
emojitones.cpp
|
emojitones.cpp
|
||||||
@@ -21,7 +23,6 @@ target_sources(LibNeoChat PRIVATE
|
|||||||
neochatdatetime.cpp
|
neochatdatetime.cpp
|
||||||
nestedlisthelper_p.h
|
nestedlisthelper_p.h
|
||||||
nestedlisthelper.cpp
|
nestedlisthelper.cpp
|
||||||
qmltextitemwrapper.cpp
|
|
||||||
roomlastmessageprovider.cpp
|
roomlastmessageprovider.cpp
|
||||||
spacehierarchycache.cpp
|
spacehierarchycache.cpp
|
||||||
texthandler.cpp
|
texthandler.cpp
|
||||||
@@ -92,13 +93,6 @@ ecm_qt_declare_logging_category(LibNeoChat
|
|||||||
DEFAULT_SEVERITY Info
|
DEFAULT_SEVERITY Info
|
||||||
)
|
)
|
||||||
|
|
||||||
ecm_qt_declare_logging_category(LibNeoChat
|
|
||||||
HEADER "chatdocumenthandler_logging.h"
|
|
||||||
IDENTIFIER "ChatDocumentHandling"
|
|
||||||
CATEGORY_NAME "org.kde.neochat.chatdocumenthandler"
|
|
||||||
DEFAULT_SEVERITY Info
|
|
||||||
)
|
|
||||||
|
|
||||||
generate_export_header(LibNeoChat BASE_NAME LibNeoChat)
|
generate_export_header(LibNeoChat BASE_NAME LibNeoChat)
|
||||||
target_include_directories(LibNeoChat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/enums ${CMAKE_CURRENT_SOURCE_DIR}/events ${CMAKE_CURRENT_SOURCE_DIR}/models)
|
target_include_directories(LibNeoChat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/enums ${CMAKE_CURRENT_SOURCE_DIR}/events ${CMAKE_CURRENT_SOURCE_DIR}/models)
|
||||||
target_link_libraries(LibNeoChat PUBLIC
|
target_link_libraries(LibNeoChat PUBLIC
|
||||||
|
|||||||
83
src/libneochat/chatbarsyntaxhighlighter.cpp
Normal file
83
src/libneochat/chatbarsyntaxhighlighter.cpp
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
|
||||||
|
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "chatbarsyntaxhighlighter.h"
|
||||||
|
|
||||||
|
#include "chatbarcache.h"
|
||||||
|
#include "chattextitemhelper.h"
|
||||||
|
#include "enums/chatbartype.h"
|
||||||
|
|
||||||
|
ChatBarSyntaxHighlighter::ChatBarSyntaxHighlighter(QObject *parent)
|
||||||
|
: QSyntaxHighlighter(parent)
|
||||||
|
{
|
||||||
|
m_theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
|
||||||
|
connect(m_theme, &Kirigami::Platform::PlatformTheme::colorsChanged, this, [this]() {
|
||||||
|
m_mentionFormat.setForeground(m_theme->linkColor());
|
||||||
|
m_errorFormat.setForeground(m_theme->negativeTextColor());
|
||||||
|
});
|
||||||
|
|
||||||
|
m_mentionFormat.setFontWeight(QFont::Bold);
|
||||||
|
m_mentionFormat.setForeground(m_theme->linkColor());
|
||||||
|
|
||||||
|
m_errorFormat.setForeground(m_theme->negativeTextColor());
|
||||||
|
m_errorFormat.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
|
||||||
|
|
||||||
|
connect(m_checker, &Sonnet::BackgroundChecker::misspelling, this, [this](const QString &word, int start) {
|
||||||
|
m_errors += {start, word};
|
||||||
|
m_checker->continueChecking();
|
||||||
|
});
|
||||||
|
connect(m_checker, &Sonnet::BackgroundChecker::done, this, [this]() {
|
||||||
|
m_rehighlightTimer.start();
|
||||||
|
});
|
||||||
|
m_rehighlightTimer.setInterval(100);
|
||||||
|
m_rehighlightTimer.setSingleShot(true);
|
||||||
|
m_rehighlightTimer.callOnTimeout(this, &QSyntaxHighlighter::rehighlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatBarSyntaxHighlighter::highlightBlock(const QString &text)
|
||||||
|
{
|
||||||
|
if (m_settings.checkerEnabledByDefault()) {
|
||||||
|
if (text != m_previousText) {
|
||||||
|
m_previousText = text;
|
||||||
|
m_checker->stop();
|
||||||
|
m_errors.clear();
|
||||||
|
m_checker->setText(text);
|
||||||
|
}
|
||||||
|
for (const auto &error : m_errors) {
|
||||||
|
setFormat(error.first, error.second.size(), m_errorFormat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!room || type == ChatBarType::None) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto mentions = room->cacheForType(type)->mentions();
|
||||||
|
mentions->erase(std::remove_if(mentions->begin(),
|
||||||
|
mentions->end(),
|
||||||
|
[this](auto &mention) {
|
||||||
|
if (document()->toPlainText().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mention.cursor.position() == 0 && mention.cursor.anchor() == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mention.cursor.position() - mention.cursor.anchor() != mention.text.size()) {
|
||||||
|
mention.cursor.setPosition(mention.start);
|
||||||
|
mention.cursor.setPosition(mention.cursor.anchor() + mention.text.size(), QTextCursor::KeepAnchor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mention.cursor.selectedText() != mention.text) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (currentBlock() == mention.cursor.block()) {
|
||||||
|
mention.start = mention.cursor.anchor();
|
||||||
|
mention.position = mention.cursor.position();
|
||||||
|
setFormat(mention.cursor.selectionStart(), mention.cursor.selectedText().size(), m_mentionFormat);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}),
|
||||||
|
mentions->end());
|
||||||
|
}
|
||||||
45
src/libneochat/chatbarsyntaxhighlighter.h
Normal file
45
src/libneochat/chatbarsyntaxhighlighter.h
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
|
||||||
|
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QQuickTextDocument>
|
||||||
|
#include <QSyntaxHighlighter>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
#include <Kirigami/Platform/PlatformTheme>
|
||||||
|
|
||||||
|
#include <Sonnet/BackgroundChecker>
|
||||||
|
#include <Sonnet/Settings>
|
||||||
|
|
||||||
|
#include "chattextitemhelper.h"
|
||||||
|
#include "neochatroom.h"
|
||||||
|
|
||||||
|
class ChatBarSyntaxHighlighter : public QSyntaxHighlighter
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ChatBarSyntaxHighlighter(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QPointer<NeoChatRoom> room;
|
||||||
|
ChatBarType::Type type = ChatBarType::None;
|
||||||
|
|
||||||
|
ChatTextItemHelper *textItem() const;
|
||||||
|
void setTextItem(ChatTextItemHelper *textItem);
|
||||||
|
|
||||||
|
void highlightBlock(const QString &text) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Kirigami::Platform::PlatformTheme *m_theme = nullptr;
|
||||||
|
QTextCharFormat m_mentionFormat;
|
||||||
|
QTextCharFormat m_errorFormat;
|
||||||
|
|
||||||
|
Sonnet::BackgroundChecker *m_checker = new Sonnet::BackgroundChecker;
|
||||||
|
Sonnet::Settings m_settings;
|
||||||
|
QString m_previousText;
|
||||||
|
|
||||||
|
QList<QPair<int, QString>> m_errors;
|
||||||
|
QTimer m_rehighlightTimer;
|
||||||
|
};
|
||||||
@@ -1,654 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
|
|
||||||
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
#include "chatdocumenthandler.h"
|
|
||||||
|
|
||||||
#include <QQmlFile>
|
|
||||||
#include <QQmlFileSelector>
|
|
||||||
#include <QQuickTextDocument>
|
|
||||||
#include <QStringBuilder>
|
|
||||||
#include <QSyntaxHighlighter>
|
|
||||||
#include <QTextBlock>
|
|
||||||
#include <QTextBoundaryFinder>
|
|
||||||
#include <QTextDocument>
|
|
||||||
#include <QTextDocumentFragment>
|
|
||||||
#include <QTextList>
|
|
||||||
#include <QTextTable>
|
|
||||||
#include <QTimer>
|
|
||||||
|
|
||||||
#include <Kirigami/Platform/PlatformTheme>
|
|
||||||
#include <KColorScheme>
|
|
||||||
|
|
||||||
#include <Sonnet/BackgroundChecker>
|
|
||||||
#include <Sonnet/Settings>
|
|
||||||
#include <qfont.h>
|
|
||||||
#include <qlogging.h>
|
|
||||||
#include <qnamespace.h>
|
|
||||||
#include <qtextcursor.h>
|
|
||||||
#include <sched.h>
|
|
||||||
|
|
||||||
#include "chatbartype.h"
|
|
||||||
#include "chatdocumenthandler_logging.h"
|
|
||||||
#include "chatmarkdownhelper.h"
|
|
||||||
#include "eventhandler.h"
|
|
||||||
#include "qmltextitemwrapper.h"
|
|
||||||
|
|
||||||
using namespace Qt::StringLiterals;
|
|
||||||
|
|
||||||
class SyntaxHighlighter : public QSyntaxHighlighter
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
QPointer<NeoChatRoom> room;
|
|
||||||
QTextCharFormat mentionFormat;
|
|
||||||
QTextCharFormat errorFormat;
|
|
||||||
Sonnet::BackgroundChecker checker;
|
|
||||||
Sonnet::Settings settings;
|
|
||||||
QList<QPair<int, QString>> errors;
|
|
||||||
QString previousText;
|
|
||||||
QTimer rehighlightTimer;
|
|
||||||
SyntaxHighlighter(QObject *parent)
|
|
||||||
: QSyntaxHighlighter(parent)
|
|
||||||
{
|
|
||||||
m_theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
|
|
||||||
connect(m_theme, &Kirigami::Platform::PlatformTheme::colorsChanged, this, [this]() {
|
|
||||||
mentionFormat.setForeground(m_theme->linkColor());
|
|
||||||
errorFormat.setForeground(m_theme->negativeTextColor());
|
|
||||||
});
|
|
||||||
|
|
||||||
mentionFormat.setFontWeight(QFont::Bold);
|
|
||||||
mentionFormat.setForeground(m_theme->linkColor());
|
|
||||||
|
|
||||||
errorFormat.setForeground(m_theme->negativeTextColor());
|
|
||||||
errorFormat.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
|
|
||||||
|
|
||||||
connect(&checker, &Sonnet::BackgroundChecker::misspelling, this, [this](const QString &word, int start) {
|
|
||||||
errors += {start, word};
|
|
||||||
checker.continueChecking();
|
|
||||||
});
|
|
||||||
connect(&checker, &Sonnet::BackgroundChecker::done, this, [this]() {
|
|
||||||
rehighlightTimer.start();
|
|
||||||
});
|
|
||||||
rehighlightTimer.setInterval(100);
|
|
||||||
rehighlightTimer.setSingleShot(true);
|
|
||||||
rehighlightTimer.callOnTimeout(this, &QSyntaxHighlighter::rehighlight);
|
|
||||||
}
|
|
||||||
void highlightBlock(const QString &text) override
|
|
||||||
{
|
|
||||||
if (settings.checkerEnabledByDefault()) {
|
|
||||||
if (text != previousText) {
|
|
||||||
previousText = text;
|
|
||||||
checker.stop();
|
|
||||||
errors.clear();
|
|
||||||
checker.setText(text);
|
|
||||||
}
|
|
||||||
for (const auto &error : errors) {
|
|
||||||
setFormat(error.first, error.second.size(), errorFormat);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto handler = dynamic_cast<ChatDocumentHandler *>(parent());
|
|
||||||
auto room = handler->room();
|
|
||||||
if (!room) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!room) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto mentions = room->cacheForType(handler->type())->mentions();
|
|
||||||
mentions->erase(std::remove_if(mentions->begin(),
|
|
||||||
mentions->end(),
|
|
||||||
[this](auto &mention) {
|
|
||||||
if (document()->toPlainText().isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mention.cursor.position() == 0 && mention.cursor.anchor() == 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mention.cursor.position() - mention.cursor.anchor() != mention.text.size()) {
|
|
||||||
mention.cursor.setPosition(mention.start);
|
|
||||||
mention.cursor.setPosition(mention.cursor.anchor() + mention.text.size(), QTextCursor::KeepAnchor);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mention.cursor.selectedText() != mention.text) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (currentBlock() == mention.cursor.block()) {
|
|
||||||
mention.start = mention.cursor.anchor();
|
|
||||||
mention.position = mention.cursor.position();
|
|
||||||
setFormat(mention.cursor.selectionStart(), mention.cursor.selectedText().size(), mentionFormat);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}),
|
|
||||||
mentions->end());
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
Kirigami::Platform::PlatformTheme *m_theme = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
, m_textItem(new QmlTextItemWrapper(this))
|
|
||||||
, m_highlighter(new SyntaxHighlighter(this))
|
|
||||||
{
|
|
||||||
connectTextItem();
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatBarType::Type ChatDocumentHandler::type() const
|
|
||||||
{
|
|
||||||
return m_type;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::setType(ChatBarType::Type type)
|
|
||||||
{
|
|
||||||
if (type == m_type) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_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;
|
|
||||||
Q_EMIT roomChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QQuickItem *ChatDocumentHandler::textItem() const
|
|
||||||
{
|
|
||||||
return m_textItem->textItem();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::setTextItem(QQuickItem *textItem)
|
|
||||||
{
|
|
||||||
m_textItem->setTextItem(textItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::connectTextItem()
|
|
||||||
{
|
|
||||||
Q_ASSERT(m_textItem);
|
|
||||||
connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, [this]() {
|
|
||||||
m_highlighter->setDocument(m_textItem->document());
|
|
||||||
initializeChars();
|
|
||||||
});
|
|
||||||
connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, &ChatDocumentHandler::textItemChanged);
|
|
||||||
connect(m_textItem, &QmlTextItemWrapper::contentsChanged, this, &ChatDocumentHandler::contentsChanged);
|
|
||||||
connect(m_textItem, &QmlTextItemWrapper::contentsChanged, this, &ChatDocumentHandler::atFirstLineChanged);
|
|
||||||
connect(m_textItem, &QmlTextItemWrapper::contentsChanged, this, &ChatDocumentHandler::atLastLineChanged);
|
|
||||||
connect(m_textItem, &QmlTextItemWrapper::cursorPositionChanged, this, &ChatDocumentHandler::atFirstLineChanged);
|
|
||||||
connect(m_textItem, &QmlTextItemWrapper::cursorPositionChanged, this, &ChatDocumentHandler::atLastLineChanged);
|
|
||||||
connect(m_textItem, &QmlTextItemWrapper::formatChanged, this, &ChatDocumentHandler::formatChanged);
|
|
||||||
connect(m_textItem, &QmlTextItemWrapper::textFormatChanged, this, &ChatDocumentHandler::textFormatChanged);
|
|
||||||
connect(m_textItem, &QmlTextItemWrapper::styleChanged, this, &ChatDocumentHandler::styleChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatDocumentHandler *ChatDocumentHandler::previousDocumentHandler() const
|
|
||||||
{
|
|
||||||
return m_previousDocumentHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::setPreviousDocumentHandler(ChatDocumentHandler *previousDocumentHandler)
|
|
||||||
{
|
|
||||||
m_previousDocumentHandler = previousDocumentHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatDocumentHandler *ChatDocumentHandler::nextDocumentHandler() const
|
|
||||||
{
|
|
||||||
return m_nextDocumentHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::setNextDocumentHandler(ChatDocumentHandler *nextDocumentHandler)
|
|
||||||
{
|
|
||||||
m_nextDocumentHandler = nextDocumentHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatDocumentHandler::fixedStartChars() const
|
|
||||||
{
|
|
||||||
return m_fixedStartChars;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::setFixedStartChars(const QString &chars)
|
|
||||||
{
|
|
||||||
if (chars == m_fixedStartChars) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_fixedStartChars = chars;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatDocumentHandler::fixedEndChars() const
|
|
||||||
{
|
|
||||||
return m_fixedEndChars;
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::setFixedEndChars(const QString &chars)
|
|
||||||
{
|
|
||||||
if (chars == m_fixedEndChars) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_fixedEndChars = chars;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatDocumentHandler::initialText() const
|
|
||||||
{
|
|
||||||
return m_initialText;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::setInitialText(const QString &text)
|
|
||||||
{
|
|
||||||
if (text == m_initialText) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_initialText = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::initializeChars()
|
|
||||||
{
|
|
||||||
const auto doc = m_textItem->document();
|
|
||||||
if (!doc) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTextCursor cursor = QTextCursor(doc);
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (doc->isEmpty() && !m_initialText.isEmpty()) {
|
|
||||||
cursor.insertText(m_initialText);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!m_fixedStartChars.isEmpty() && doc->characterAt(0) != m_fixedStartChars) {
|
|
||||||
cursor.movePosition(QTextCursor::Start);
|
|
||||||
cursor.insertText(m_fixedEndChars);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!m_fixedStartChars.isEmpty() && doc->characterAt(doc->characterCount()) != m_fixedStartChars) {
|
|
||||||
cursor.movePosition(QTextCursor::End);
|
|
||||||
cursor.insertText(m_fixedEndChars);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatDocumentHandler::isEmpty() const
|
|
||||||
{
|
|
||||||
return htmlText().length() == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatDocumentHandler::atFirstLine() const
|
|
||||||
{
|
|
||||||
const auto cursor = m_textItem->textCursor();
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return cursor.blockNumber() == 0 && cursor.block().layout()->lineForTextPosition(cursor.positionInBlock()).lineNumber() == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatDocumentHandler::atLastLine() const
|
|
||||||
{
|
|
||||||
const auto cursor = m_textItem->textCursor();
|
|
||||||
const auto doc = m_textItem->document();
|
|
||||||
if (cursor.isNull() || !doc) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return cursor.blockNumber() == doc->blockCount() - 1
|
|
||||||
&& cursor.block().layout()->lineForTextPosition(cursor.positionInBlock()).lineNumber() == (cursor.block().layout()->lineCount() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::setCursorFromDocumentHandler(ChatDocumentHandler *previousDocumentHandler, bool infront, int defaultPosition)
|
|
||||||
{
|
|
||||||
const auto doc = m_textItem->document();
|
|
||||||
if (!doc) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_textItem->forceActiveFocus();
|
|
||||||
|
|
||||||
if (!previousDocumentHandler) {
|
|
||||||
const auto docLastBlockLayout = doc->lastBlock().layout();
|
|
||||||
m_textItem->setCursorPosition(infront ? defaultPosition : docLastBlockLayout->lineAt(docLastBlockLayout->lineCount() - 1).textStart());
|
|
||||||
m_textItem->setCursorVisible(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto previousLinePosition = previousDocumentHandler->cursorPositionInLine();
|
|
||||||
const auto newMaxLineLength = lineLength(infront ? 0 : lineCount() - 1);
|
|
||||||
m_textItem->setCursorPosition(std::min(previousLinePosition, newMaxLineLength ? *newMaxLineLength : defaultPosition)
|
|
||||||
+ (infront ? 0 : doc->lastBlock().position()));
|
|
||||||
m_textItem->setCursorVisible(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChatDocumentHandler::lineCount() const
|
|
||||||
{
|
|
||||||
if (const auto doc = m_textItem->document()) {
|
|
||||||
return doc->lineCount();
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<int> ChatDocumentHandler::lineLength(int lineNumber) const
|
|
||||||
{
|
|
||||||
const auto doc = m_textItem->document();
|
|
||||||
if (!doc || lineNumber < 0 || lineNumber >= doc->lineCount()) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
const auto block = doc->findBlockByLineNumber(lineNumber);
|
|
||||||
const auto lineNumInBlock = lineNumber - block.firstLineNumber();
|
|
||||||
return block.layout()->lineAt(lineNumInBlock).textLength();
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChatDocumentHandler::cursorPositionInLine() const
|
|
||||||
{
|
|
||||||
const auto cursor = m_textItem->textCursor();
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return cursor.positionInBlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
QTextDocumentFragment ChatDocumentHandler::takeFirstBlock()
|
|
||||||
{
|
|
||||||
auto cursor = m_textItem->textCursor();
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
cursor.beginEditBlock();
|
|
||||||
cursor.movePosition(QTextCursor::Start);
|
|
||||||
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, m_fixedStartChars.length());
|
|
||||||
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
|
||||||
if (m_textItem->document()->blockCount() <= 1) {
|
|
||||||
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, m_fixedEndChars.length());
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto block = cursor.selection();
|
|
||||||
cursor.removeSelectedText();
|
|
||||||
cursor.endEditBlock();
|
|
||||||
if (m_textItem->document()->characterCount() - 1 <= (m_fixedStartChars.length() + m_fixedEndChars.length())) {
|
|
||||||
Q_EMIT removeMe(this);
|
|
||||||
}
|
|
||||||
return block;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::fillFragments(bool &hasBefore, QTextDocumentFragment &midFragment, std::optional<QTextDocumentFragment> &afterFragment)
|
|
||||||
{
|
|
||||||
auto cursor = m_textItem->textCursor();
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cursor.blockNumber() > 0) {
|
|
||||||
hasBefore = true;
|
|
||||||
}
|
|
||||||
auto afterBlock = cursor.blockNumber() < m_textItem->document()->blockCount() - 1;
|
|
||||||
|
|
||||||
cursor.beginEditBlock();
|
|
||||||
cursor.movePosition(QTextCursor::StartOfBlock);
|
|
||||||
if (!hasBefore) {
|
|
||||||
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, m_fixedStartChars.length());
|
|
||||||
}
|
|
||||||
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
|
||||||
if (!afterBlock) {
|
|
||||||
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, m_fixedEndChars.length());
|
|
||||||
}
|
|
||||||
cursor.endEditBlock();
|
|
||||||
|
|
||||||
midFragment = cursor.selection();
|
|
||||||
if (!midFragment.isEmpty()) {
|
|
||||||
cursor.removeSelectedText();
|
|
||||||
}
|
|
||||||
cursor.deletePreviousChar();
|
|
||||||
if (afterBlock) {
|
|
||||||
cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
|
|
||||||
afterFragment = cursor.selection();
|
|
||||||
cursor.removeSelectedText();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::insertFragment(const QTextDocumentFragment fragment, InsertPosition position, bool keepPosition)
|
|
||||||
{
|
|
||||||
auto cursor = m_textItem->textCursor();
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int currentPosition;
|
|
||||||
switch (position) {
|
|
||||||
case Start:
|
|
||||||
currentPosition = 0;
|
|
||||||
break;
|
|
||||||
case End:
|
|
||||||
currentPosition = m_textItem->document()->characterCount() - 1;
|
|
||||||
break;
|
|
||||||
case Cursor:
|
|
||||||
currentPosition = cursor.position();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
cursor.setPosition(currentPosition);
|
|
||||||
if (textFormat() && textFormat() == Qt::PlainText) {
|
|
||||||
const auto wasEmpty = isEmpty();
|
|
||||||
auto text = fragment.toPlainText();
|
|
||||||
while (text.startsWith(u"\n"_s)) {
|
|
||||||
text.removeFirst();
|
|
||||||
}
|
|
||||||
while (text.endsWith(u"\n"_s)) {
|
|
||||||
text.removeLast();
|
|
||||||
}
|
|
||||||
cursor.insertText(fragment.toPlainText());
|
|
||||||
if (wasEmpty) {
|
|
||||||
cursor.movePosition(QTextCursor::StartOfBlock);
|
|
||||||
cursor.deletePreviousChar();
|
|
||||||
cursor.movePosition(QTextCursor::EndOfBlock);
|
|
||||||
cursor.deleteChar();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cursor.insertMarkdown(trim(fragment.toMarkdown()));
|
|
||||||
}
|
|
||||||
if (keepPosition) {
|
|
||||||
cursor.setPosition(currentPosition);
|
|
||||||
}
|
|
||||||
m_textItem->setCursorPosition(cursor.position());
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatDocumentHandler::getText() const
|
|
||||||
{
|
|
||||||
if (!m_textItem->document()) {
|
|
||||||
qCWarning(ChatDocumentHandling) << "getText called with no QQuickTextDocument available.";
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return m_textItem->document()->toPlainText();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::pushMention(const Mention mention) const
|
|
||||||
{
|
|
||||||
if (!m_room || m_type == ChatBarType::None) {
|
|
||||||
qCWarning(ChatDocumentHandling) << "pushMention called with no ChatBarCache available. ChatBarType: " << m_type << " Room: " << m_room;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_room->cacheForType(m_type)->mentions()->push_back(mention);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::updateMentions(const QString &editId)
|
|
||||||
{
|
|
||||||
if (editId.isEmpty() || m_type == ChatBarType::None || !m_room) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auto event = m_room->findInTimeline(editId); event != m_room->historyEdge()) {
|
|
||||||
if (const auto &roomMessageEvent = &*event->viewAs<Quotient::RoomMessageEvent>()) {
|
|
||||||
// Replaces the mentions that are baked into the HTML but plaintext in the original markdown
|
|
||||||
const QRegularExpression re(uR"lit(<a\shref="https:\/\/matrix.to\/#\/([\S]*)"\s?>([\S]*)<\/a>)lit"_s);
|
|
||||||
|
|
||||||
m_room->cacheForType(m_type)->mentions()->clear();
|
|
||||||
|
|
||||||
int linkSize = 0;
|
|
||||||
auto matches = re.globalMatch(EventHandler::rawMessageBody(*roomMessageEvent));
|
|
||||||
while (matches.hasNext()) {
|
|
||||||
const QRegularExpressionMatch match = matches.next();
|
|
||||||
if (match.hasMatch()) {
|
|
||||||
const QString id = match.captured(1);
|
|
||||||
const QString name = match.captured(2);
|
|
||||||
|
|
||||||
const int position = match.capturedStart(0) - linkSize;
|
|
||||||
const int end = position + name.length();
|
|
||||||
linkSize += match.capturedLength(0) - name.length();
|
|
||||||
|
|
||||||
QTextCursor cursor(m_textItem->document());
|
|
||||||
cursor.setPosition(position);
|
|
||||||
cursor.setPosition(end, QTextCursor::KeepAnchor);
|
|
||||||
cursor.setKeepPositionOnInsert(true);
|
|
||||||
|
|
||||||
pushMention(Mention{.cursor = cursor, .text = name, .start = position, .position = end, .id = id});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<Qt::TextFormat> ChatDocumentHandler::textFormat() const
|
|
||||||
{
|
|
||||||
if (!m_textItem) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
return static_cast<Qt::TextFormat>(m_textItem->property("textFormat").toInt());
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::mergeFormatOnWordOrSelection(const QTextCharFormat &format)
|
|
||||||
{
|
|
||||||
QTextCursor cursor = m_textItem->textCursor();
|
|
||||||
if (!cursor.hasSelection()) {
|
|
||||||
cursor.select(QTextCursor::WordUnderCursor);
|
|
||||||
}
|
|
||||||
if (cursor.hasSelection()) {
|
|
||||||
cursor.mergeCharFormat(format);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::insertCompletion(const QString &text, const QUrl &link)
|
|
||||||
{
|
|
||||||
QTextCursor cursor = m_textItem->textCursor();
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor.beginEditBlock();
|
|
||||||
while (!cursor.selectedText().startsWith(u' ') && !cursor.atBlockStart()) {
|
|
||||||
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
|
|
||||||
}
|
|
||||||
if (cursor.selectedText().startsWith(u' ')) {
|
|
||||||
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
|
|
||||||
}
|
|
||||||
cursor.removeSelectedText();
|
|
||||||
|
|
||||||
const int start = cursor.position();
|
|
||||||
const auto insertString = u"%1 %2"_s.arg(text, link.isEmpty() ? QString() : u" "_s);
|
|
||||||
cursor.insertText(insertString);
|
|
||||||
cursor.setPosition(start);
|
|
||||||
cursor.setPosition(start + text.size(), QTextCursor::KeepAnchor);
|
|
||||||
cursor.setKeepPositionOnInsert(true);
|
|
||||||
cursor.endEditBlock();
|
|
||||||
if (!link.isEmpty()) {
|
|
||||||
pushMention({
|
|
||||||
.cursor = cursor,
|
|
||||||
.text = text,
|
|
||||||
.id = link.toString(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
m_highlighter->rehighlight();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::tab()
|
|
||||||
{
|
|
||||||
QTextCursor cursor = m_textItem->textCursor();
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (cursor.currentList()) {
|
|
||||||
m_textItem->indentListMoreAtCursor();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cursor.insertText(u" "_s);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::deleteChar()
|
|
||||||
{
|
|
||||||
QTextCursor cursor = m_textItem->textCursor();
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (cursor.position() >= m_textItem->document()->characterCount() - m_fixedEndChars.length() - 1) {
|
|
||||||
if (const auto nextHandler = nextDocumentHandler()) {
|
|
||||||
insertFragment(nextHandler->takeFirstBlock(), Cursor, true);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cursor.deleteChar();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::backspace()
|
|
||||||
{
|
|
||||||
QTextCursor cursor = m_textItem->textCursor();
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (cursor.position() <= m_fixedStartChars.length()) {
|
|
||||||
if (cursor.currentList()) {
|
|
||||||
m_textItem->indentListLessAtCursor();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (const auto previousHandler = previousDocumentHandler()) {
|
|
||||||
previousHandler->insertFragment(takeFirstBlock(), End, true);
|
|
||||||
} else {
|
|
||||||
Q_EMIT unhandledBackspaceAtBeginning(this);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cursor.deletePreviousChar();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::insertReturn()
|
|
||||||
{
|
|
||||||
QTextCursor cursor = m_textItem->textCursor();
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cursor.insertBlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::dumpHtml()
|
|
||||||
{
|
|
||||||
qWarning() << htmlText();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatDocumentHandler::htmlText() const
|
|
||||||
{
|
|
||||||
const auto doc = m_textItem->document();
|
|
||||||
if (!doc) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return trim(doc->toMarkdown());
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatDocumentHandler::trim(QString string) const
|
|
||||||
{
|
|
||||||
while (string.startsWith(u"\n"_s)) {
|
|
||||||
string.removeFirst();
|
|
||||||
}
|
|
||||||
while (string.endsWith(u"\n"_s)) {
|
|
||||||
string.removeLast();
|
|
||||||
}
|
|
||||||
return string;
|
|
||||||
}
|
|
||||||
|
|
||||||
#include "moc_chatdocumenthandler.cpp"
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
|
||||||
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QQmlEngine>
|
|
||||||
#include <QTextCursor>
|
|
||||||
#include <qnamespace.h>
|
|
||||||
#include <qtextdocumentfragment.h>
|
|
||||||
|
|
||||||
#include "chatbarcache.h"
|
|
||||||
#include "chatmarkdownhelper.h"
|
|
||||||
#include "enums/chatbartype.h"
|
|
||||||
#include "enums/richformat.h"
|
|
||||||
#include "neochatroom.h"
|
|
||||||
#include "nestedlisthelper_p.h"
|
|
||||||
|
|
||||||
class QTextDocument;
|
|
||||||
|
|
||||||
class QmlTextItemWrapper;
|
|
||||||
class NeoChatRoom;
|
|
||||||
class SyntaxHighlighter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @class ChatDocumentHandler
|
|
||||||
*
|
|
||||||
* Handle the QQuickTextDocument of a qml text item.
|
|
||||||
*
|
|
||||||
* The class provides functionality to highlight text in the text document as well
|
|
||||||
* as providing completion functionality via a CompletionModel.
|
|
||||||
*
|
|
||||||
* The ChatDocumentHandler is also linked to a NeoChatRoom to provide functionality
|
|
||||||
* to save the chat document text when switching between rooms.
|
|
||||||
*
|
|
||||||
* To get the full functionality the cursor position and text selection information
|
|
||||||
* need to be passed in. For example:
|
|
||||||
*
|
|
||||||
* @code{.qml}
|
|
||||||
* import QtQuick 2.0
|
|
||||||
* import QtQuick.Controls 2.15 as QQC2
|
|
||||||
*
|
|
||||||
* import org.kde.kirigami 2.12 as Kirigami
|
|
||||||
* import org.kde.neochat 1.0
|
|
||||||
*
|
|
||||||
* QQC2.TextArea {
|
|
||||||
* id: textField
|
|
||||||
*
|
|
||||||
* // Set this to a NeoChatRoom object.
|
|
||||||
* property var room
|
|
||||||
*
|
|
||||||
* ChatDocumentHandler {
|
|
||||||
* id: documentHandler
|
|
||||||
* document: textField.textDocument
|
|
||||||
* cursorPosition: textField.cursorPosition
|
|
||||||
* selectionStart: textField.selectionStart
|
|
||||||
* selectionEnd: textField.selectionEnd
|
|
||||||
* mentionColor: Kirigami.Theme.linkColor
|
|
||||||
* errorColor: Kirigami.Theme.negativeTextColor
|
|
||||||
* room: textField.room
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* @endcode
|
|
||||||
*
|
|
||||||
* @sa QQuickTextDocument, CompletionModel, NeoChatRoom
|
|
||||||
*/
|
|
||||||
class ChatDocumentHandler : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
QML_ELEMENT
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The QQuickTextDocument that is being handled.
|
|
||||||
*/
|
|
||||||
Q_PROPERTY(ChatBarType::Type type READ type WRITE setType NOTIFY typeChanged)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The current room that the text document is being handled for.
|
|
||||||
*/
|
|
||||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The QML text Item the ChatDocumentHandler is handling.
|
|
||||||
*/
|
|
||||||
Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether the cursor is currently on the first line.
|
|
||||||
*/
|
|
||||||
Q_PROPERTY(bool atFirstLine READ atFirstLine NOTIFY atFirstLineChanged)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether the cursor is cuurently on the last line.
|
|
||||||
*/
|
|
||||||
Q_PROPERTY(bool atLastLine READ atLastLine NOTIFY atLastLineChanged)
|
|
||||||
|
|
||||||
public:
|
|
||||||
enum InsertPosition {
|
|
||||||
Cursor,
|
|
||||||
Start,
|
|
||||||
End,
|
|
||||||
};
|
|
||||||
|
|
||||||
explicit ChatDocumentHandler(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
ChatBarType::Type type() const;
|
|
||||||
void setType(ChatBarType::Type type);
|
|
||||||
|
|
||||||
[[nodiscard]] NeoChatRoom *room() const;
|
|
||||||
void setRoom(NeoChatRoom *room);
|
|
||||||
|
|
||||||
QQuickItem *textItem() const;
|
|
||||||
void setTextItem(QQuickItem *textItem);
|
|
||||||
|
|
||||||
ChatDocumentHandler *previousDocumentHandler() const;
|
|
||||||
void setPreviousDocumentHandler(ChatDocumentHandler *previousDocumentHandler);
|
|
||||||
|
|
||||||
ChatDocumentHandler *nextDocumentHandler() const;
|
|
||||||
void setNextDocumentHandler(ChatDocumentHandler *nextDocumentHandler);
|
|
||||||
|
|
||||||
QString fixedStartChars() const;
|
|
||||||
void setFixedStartChars(const QString &chars);
|
|
||||||
QString fixedEndChars() const;
|
|
||||||
void setFixedEndChars(const QString &chars);
|
|
||||||
QString initialText() const;
|
|
||||||
void setInitialText(const QString &text);
|
|
||||||
|
|
||||||
bool isEmpty() const;
|
|
||||||
bool atFirstLine() const;
|
|
||||||
bool atLastLine() const;
|
|
||||||
void setCursorFromDocumentHandler(ChatDocumentHandler *previousDocumentHandler, bool infront, int defaultPosition = 0);
|
|
||||||
int lineCount() const;
|
|
||||||
std::optional<int> lineLength(int lineNumber) const;
|
|
||||||
int cursorPositionInLine() const;
|
|
||||||
QTextDocumentFragment takeFirstBlock();
|
|
||||||
void fillFragments(bool &hasBefore, QTextDocumentFragment &midFragment, std::optional<QTextDocumentFragment> &afterFragment);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update the mentions in @p document when editing a message.
|
|
||||||
*/
|
|
||||||
Q_INVOKABLE void updateMentions(const QString &editId);
|
|
||||||
|
|
||||||
Q_INVOKABLE void tab();
|
|
||||||
Q_INVOKABLE void deleteChar();
|
|
||||||
Q_INVOKABLE void backspace();
|
|
||||||
Q_INVOKABLE void insertReturn();
|
|
||||||
void insertFragment(const QTextDocumentFragment fragment, InsertPosition position = Cursor, bool keepPosition = false);
|
|
||||||
Q_INVOKABLE void insertCompletion(const QString &text, const QUrl &link);
|
|
||||||
|
|
||||||
Q_INVOKABLE void dumpHtml();
|
|
||||||
Q_INVOKABLE QString htmlText() const;
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void typeChanged();
|
|
||||||
void textItemChanged();
|
|
||||||
void roomChanged();
|
|
||||||
|
|
||||||
void atFirstLineChanged();
|
|
||||||
void atLastLineChanged();
|
|
||||||
|
|
||||||
void currentListStyleChanged();
|
|
||||||
|
|
||||||
void formatChanged();
|
|
||||||
void textFormatChanged();
|
|
||||||
void styleChanged();
|
|
||||||
|
|
||||||
void contentsChanged();
|
|
||||||
|
|
||||||
void unhandledBackspaceAtBeginning(ChatDocumentHandler *self);
|
|
||||||
void removeMe(ChatDocumentHandler *self);
|
|
||||||
|
|
||||||
private:
|
|
||||||
ChatBarType::Type m_type = ChatBarType::None;
|
|
||||||
QPointer<NeoChatRoom> m_room;
|
|
||||||
QPointer<QmlTextItemWrapper> m_textItem;
|
|
||||||
void connectTextItem();
|
|
||||||
|
|
||||||
QPointer<ChatDocumentHandler> m_previousDocumentHandler;
|
|
||||||
QPointer<ChatDocumentHandler> m_nextDocumentHandler;
|
|
||||||
|
|
||||||
QString m_fixedStartChars = {};
|
|
||||||
QString m_fixedEndChars = {};
|
|
||||||
QString m_initialText = {};
|
|
||||||
void initializeChars();
|
|
||||||
|
|
||||||
SyntaxHighlighter *m_highlighter = nullptr;
|
|
||||||
|
|
||||||
QString getText() const;
|
|
||||||
void pushMention(const Mention mention) const;
|
|
||||||
|
|
||||||
std::optional<Qt::TextFormat> textFormat() const;
|
|
||||||
void mergeFormatOnWordOrSelection(const QTextCharFormat &format);
|
|
||||||
|
|
||||||
QString trim(QString string) const;
|
|
||||||
};
|
|
||||||
122
src/libneochat/chatkeyhelper.cpp
Normal file
122
src/libneochat/chatkeyhelper.cpp
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
// 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 "chatkeyhelper.h"
|
||||||
|
|
||||||
|
ChatKeyHelper::ChatKeyHelper(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatTextItemHelper *ChatKeyHelper::textItem() const
|
||||||
|
{
|
||||||
|
return m_textItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatKeyHelper::setTextItem(ChatTextItemHelper *textItem)
|
||||||
|
{
|
||||||
|
if (textItem == m_textItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_textItem) {
|
||||||
|
m_textItem->disconnect(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_textItem = textItem;
|
||||||
|
|
||||||
|
if (m_textItem) {
|
||||||
|
connect(m_textItem, &ChatTextItemHelper::textItemChanged, this, &ChatKeyHelper::textItemChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_EMIT textItemChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatKeyHelper::up()
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QTextCursor cursor = m_textItem->textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cursor.blockNumber() == 0 && cursor.block().layout()->lineForTextPosition(cursor.positionInBlock()).lineNumber() == 0) {
|
||||||
|
Q_EMIT unhandledUp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cursor.movePosition(QTextCursor::Up);
|
||||||
|
m_textItem->setCursorPosition(cursor.position());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatKeyHelper::down()
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QTextCursor cursor = m_textItem->textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cursor.blockNumber() == cursor.document()->blockCount() - 1
|
||||||
|
&& cursor.block().layout()->lineForTextPosition(cursor.positionInBlock()).lineNumber() == (cursor.block().layout()->lineCount() - 1)) {
|
||||||
|
Q_EMIT unhandledDown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cursor.movePosition(QTextCursor::Down);
|
||||||
|
m_textItem->setCursorPosition(cursor.position());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatKeyHelper::tab()
|
||||||
|
{
|
||||||
|
QTextCursor cursor = m_textItem->textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cursor.currentList()) {
|
||||||
|
m_textItem->indentListMoreAtCursor();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cursor.insertText(u" "_s);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatKeyHelper::deleteChar()
|
||||||
|
{
|
||||||
|
QTextCursor cursor = m_textItem->textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cursor.position() >= m_textItem->document()->characterCount() - m_textItem->fixedEndChars().length() - 1) {
|
||||||
|
Q_EMIT unhandledDelete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cursor.deleteChar();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatKeyHelper::backspace()
|
||||||
|
{
|
||||||
|
QTextCursor cursor = m_textItem->textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cursor.position() <= m_textItem->fixedStartChars().length()) {
|
||||||
|
if (cursor.currentList()) {
|
||||||
|
m_textItem->indentListLessAtCursor();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Q_EMIT unhandledBackspace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cursor.deletePreviousChar();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatKeyHelper::insertReturn()
|
||||||
|
{
|
||||||
|
QTextCursor cursor = m_textItem->textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cursor.insertBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "moc_chatkeyhelper.cpp"
|
||||||
87
src/libneochat/chatkeyhelper.h
Normal file
87
src/libneochat/chatkeyhelper.h
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// 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 "chattextitemhelper.h"
|
||||||
|
|
||||||
|
class ChatKeyHelper : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ChatKeyHelper(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
ChatTextItemHelper *textItem() const;
|
||||||
|
void setTextItem(ChatTextItemHelper *textItem);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle up key at current cursor location.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void up();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle down key at current cursor location.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void down();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle tab key at current cursor location.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void tab();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle delete key at current cursor location.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void deleteChar();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle backspace key at current cursor location.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void backspace();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle return key at current cursor location.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void insertReturn();
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void textItemChanged();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief There is an unhandled up key press.
|
||||||
|
*
|
||||||
|
* i.e. up is pressed on the first line of the first block of the text item.
|
||||||
|
*/
|
||||||
|
void unhandledUp();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief There is an unhandled down key press.
|
||||||
|
*
|
||||||
|
* i.e. down is pressed on the last line of the last block of the text item.
|
||||||
|
*/
|
||||||
|
void unhandledDown();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief There is an unhandled delete key press.
|
||||||
|
*
|
||||||
|
* i.e. delete is pressed at the end of the last line of the last block of the
|
||||||
|
* text item.
|
||||||
|
*/
|
||||||
|
void unhandledDelete();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief There is an unhandled backspace key press.
|
||||||
|
*
|
||||||
|
* i.e. backspace is pressed at the beginning of the first line of the first
|
||||||
|
* block of the text item.
|
||||||
|
*/
|
||||||
|
void unhandledBackspace();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QPointer<ChatTextItemHelper> m_textItem;
|
||||||
|
};
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
#include <QTextDocument>
|
#include <QTextDocument>
|
||||||
#include <qtextcursor.h>
|
#include <qtextcursor.h>
|
||||||
|
|
||||||
#include "qmltextitemwrapper.h"
|
#include "chattextitemhelper.h"
|
||||||
#include "richformat.h"
|
#include "richformat.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
@@ -86,12 +86,12 @@ ChatMarkdownHelper::ChatMarkdownHelper(QObject *parent)
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
QmlTextItemWrapper *ChatMarkdownHelper::textItem() const
|
ChatTextItemHelper *ChatMarkdownHelper::textItem() const
|
||||||
{
|
{
|
||||||
return m_textItem;
|
return m_textItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatMarkdownHelper::setTextItem(QmlTextItemWrapper *textItem)
|
void ChatMarkdownHelper::setTextItem(ChatTextItemHelper *textItem)
|
||||||
{
|
{
|
||||||
if (textItem == m_textItem) {
|
if (textItem == m_textItem) {
|
||||||
return;
|
return;
|
||||||
@@ -104,15 +104,15 @@ void ChatMarkdownHelper::setTextItem(QmlTextItemWrapper *textItem)
|
|||||||
m_textItem = textItem;
|
m_textItem = textItem;
|
||||||
|
|
||||||
if (m_textItem) {
|
if (m_textItem) {
|
||||||
connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, &ChatMarkdownHelper::textItemChanged);
|
connect(m_textItem, &ChatTextItemHelper::textItemChanged, this, &ChatMarkdownHelper::textItemChanged);
|
||||||
connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, [this]() {
|
connect(m_textItem, &ChatTextItemHelper::textItemChanged, this, [this]() {
|
||||||
m_startPos = m_textItem->cursorPosition();
|
m_startPos = m_textItem->cursorPosition();
|
||||||
m_endPos = m_startPos;
|
m_endPos = m_startPos;
|
||||||
if (m_startPos == 0) {
|
if (m_startPos == 0) {
|
||||||
m_currentState = Pre;
|
m_currentState = Pre;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
connect(m_textItem, &QmlTextItemWrapper::contentsChange, this, &ChatMarkdownHelper::checkMarkdown);
|
connect(m_textItem, &ChatTextItemHelper::contentsChange, this, &ChatMarkdownHelper::checkMarkdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
Q_EMIT textItemChanged();
|
Q_EMIT textItemChanged();
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
class QQuickItem;
|
class QQuickItem;
|
||||||
class QTextDocument;
|
class QTextDocument;
|
||||||
|
|
||||||
class QmlTextItemWrapper;
|
class ChatTextItemHelper;
|
||||||
|
|
||||||
class ChatMarkdownHelper : public QObject
|
class ChatMarkdownHelper : public QObject
|
||||||
{
|
{
|
||||||
@@ -19,13 +19,20 @@ class ChatMarkdownHelper : public QObject
|
|||||||
public:
|
public:
|
||||||
explicit ChatMarkdownHelper(QObject *parent = nullptr);
|
explicit ChatMarkdownHelper(QObject *parent = nullptr);
|
||||||
|
|
||||||
QmlTextItemWrapper *textItem() const;
|
ChatTextItemHelper *textItem() const;
|
||||||
void setTextItem(QmlTextItemWrapper *textItem);
|
void setTextItem(ChatTextItemHelper *textItem);
|
||||||
|
|
||||||
void handleExternalFormatChange();
|
void handleExternalFormatChange();
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void textItemChanged();
|
void textItemChanged();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief There is an unhandled block format request.
|
||||||
|
*
|
||||||
|
* i.e. the markdown for as new block (e.g. code or quote) has been typed which
|
||||||
|
* ChatMarkdownHelper cannot resolve.
|
||||||
|
*/
|
||||||
void unhandledBlockFormat(RichFormat::Format format);
|
void unhandledBlockFormat(RichFormat::Format format);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -35,7 +42,7 @@ private:
|
|||||||
Started,
|
Started,
|
||||||
};
|
};
|
||||||
|
|
||||||
QPointer<QmlTextItemWrapper> m_textItem;
|
QPointer<ChatTextItemHelper> m_textItem;
|
||||||
|
|
||||||
State m_currentState = None;
|
State m_currentState = None;
|
||||||
int m_startPos = 0;
|
int m_startPos = 0;
|
||||||
|
|||||||
543
src/libneochat/chattextitemhelper.cpp
Normal file
543
src/libneochat/chattextitemhelper.cpp
Normal file
@@ -0,0 +1,543 @@
|
|||||||
|
// 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 "chattextitemhelper.h"
|
||||||
|
#include "richformat.h"
|
||||||
|
|
||||||
|
#include <QQuickTextDocument>
|
||||||
|
#include <QTextCursor>
|
||||||
|
#include <QTextDocumentFragment>
|
||||||
|
|
||||||
|
#include <Kirigami/Platform/PlatformTheme>
|
||||||
|
|
||||||
|
#include "chatbarsyntaxhighlighter.h"
|
||||||
|
#include "neochatroom.h"
|
||||||
|
|
||||||
|
ChatTextItemHelper::ChatTextItemHelper(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_highlighter(new ChatBarSyntaxHighlighter(this))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatTextItemHelper::setRoom(NeoChatRoom *room)
|
||||||
|
{
|
||||||
|
m_highlighter->room = room;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatTextItemHelper::setType(ChatBarType::Type type)
|
||||||
|
{
|
||||||
|
m_highlighter->type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
QQuickItem *ChatTextItemHelper::textItem() const
|
||||||
|
{
|
||||||
|
return m_textItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatTextItemHelper::setTextItem(QQuickItem *textItem)
|
||||||
|
{
|
||||||
|
if (textItem == m_textItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_textItem) {
|
||||||
|
m_textItem->disconnect(this);
|
||||||
|
if (const auto textDoc = document()) {
|
||||||
|
textDoc->disconnect(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_textItem = textItem;
|
||||||
|
|
||||||
|
if (m_textItem) {
|
||||||
|
connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(itemCursorPositionChanged()));
|
||||||
|
if (const auto doc = document()) {
|
||||||
|
connect(doc, &QTextDocument::contentsChanged, this, &ChatTextItemHelper::contentsChanged);
|
||||||
|
connect(doc, &QTextDocument::contentsChange, this, &ChatTextItemHelper::contentsChange);
|
||||||
|
m_highlighter->setDocument(doc);
|
||||||
|
}
|
||||||
|
initializeChars();
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_EMIT textItemChanged();
|
||||||
|
Q_EMIT formatChanged();
|
||||||
|
Q_EMIT textFormatChanged();
|
||||||
|
Q_EMIT styleChanged();
|
||||||
|
Q_EMIT listChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<Qt::TextFormat> ChatTextItemHelper::textFormat() const
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return static_cast<Qt::TextFormat>(m_textItem->property("textFormat").toInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatTextItemHelper::fixedStartChars() const
|
||||||
|
{
|
||||||
|
return m_fixedStartChars;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatTextItemHelper::fixedEndChars() const
|
||||||
|
{
|
||||||
|
return m_fixedEndChars;
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatTextItemHelper::setFixedChars(const QString &startChars, const QString &endChars)
|
||||||
|
{
|
||||||
|
if (startChars == m_fixedStartChars && endChars == m_fixedEndChars) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_fixedStartChars = startChars;
|
||||||
|
m_fixedEndChars = endChars;
|
||||||
|
initializeChars();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatTextItemHelper::initialText() const
|
||||||
|
{
|
||||||
|
return m_initialText;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatTextItemHelper::setInitialText(const QString &text)
|
||||||
|
{
|
||||||
|
if (text == m_initialText) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_initialText = text;
|
||||||
|
initializeChars();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatTextItemHelper::initializeChars()
|
||||||
|
{
|
||||||
|
const auto doc = document();
|
||||||
|
if (!doc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextCursor cursor = QTextCursor(doc);
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc->isEmpty() && !m_initialText.isEmpty()) {
|
||||||
|
cursor.insertText(m_initialText);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_fixedStartChars.isEmpty() && doc->characterAt(0) != m_fixedStartChars) {
|
||||||
|
cursor.movePosition(QTextCursor::Start);
|
||||||
|
cursor.insertText(m_fixedEndChars);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_fixedStartChars.isEmpty() && doc->characterAt(doc->characterCount()) != m_fixedStartChars) {
|
||||||
|
cursor.movePosition(QTextCursor::End);
|
||||||
|
cursor.insertText(m_fixedEndChars);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextDocument *ChatTextItemHelper::document() const
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
const auto quickDocument = qvariant_cast<QQuickTextDocument *>(textItem()->property("textDocument"));
|
||||||
|
return quickDocument ? quickDocument->textDocument() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatTextItemHelper::isEmpty() const
|
||||||
|
{
|
||||||
|
return markdownText().length() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChatTextItemHelper::lineCount() const
|
||||||
|
{
|
||||||
|
if (const auto doc = document()) {
|
||||||
|
return doc->lineCount();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<int> ChatTextItemHelper::lineLength(int lineNumber) const
|
||||||
|
{
|
||||||
|
const auto doc = document();
|
||||||
|
if (!doc || lineNumber < 0 || lineNumber >= doc->lineCount()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
const auto block = doc->findBlockByLineNumber(lineNumber);
|
||||||
|
const auto lineNumInBlock = lineNumber - block.firstLineNumber();
|
||||||
|
return block.layout()->lineAt(lineNumInBlock).textLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextDocumentFragment ChatTextItemHelper::takeFirstBlock()
|
||||||
|
{
|
||||||
|
auto cursor = textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
cursor.beginEditBlock();
|
||||||
|
cursor.movePosition(QTextCursor::Start);
|
||||||
|
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, m_fixedStartChars.length());
|
||||||
|
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||||
|
if (document()->blockCount() <= 1) {
|
||||||
|
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, m_fixedEndChars.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto block = cursor.selection();
|
||||||
|
cursor.removeSelectedText();
|
||||||
|
cursor.endEditBlock();
|
||||||
|
if (document()->characterCount() - 1 <= (m_fixedStartChars.length() + m_fixedEndChars.length())) {
|
||||||
|
Q_EMIT cleared(this);
|
||||||
|
}
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatTextItemHelper::fillFragments(bool &hasBefore, QTextDocumentFragment &midFragment, std::optional<QTextDocumentFragment> &afterFragment)
|
||||||
|
{
|
||||||
|
auto cursor = textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursor.blockNumber() > 0) {
|
||||||
|
hasBefore = true;
|
||||||
|
}
|
||||||
|
auto afterBlock = cursor.blockNumber() < document()->blockCount() - 1;
|
||||||
|
|
||||||
|
cursor.beginEditBlock();
|
||||||
|
cursor.movePosition(QTextCursor::StartOfBlock);
|
||||||
|
if (!hasBefore) {
|
||||||
|
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, m_fixedStartChars.length());
|
||||||
|
}
|
||||||
|
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||||
|
if (!afterBlock) {
|
||||||
|
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, m_fixedEndChars.length());
|
||||||
|
}
|
||||||
|
cursor.endEditBlock();
|
||||||
|
|
||||||
|
midFragment = cursor.selection();
|
||||||
|
if (!midFragment.isEmpty()) {
|
||||||
|
cursor.removeSelectedText();
|
||||||
|
}
|
||||||
|
cursor.deletePreviousChar();
|
||||||
|
if (afterBlock) {
|
||||||
|
cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
|
||||||
|
afterFragment = cursor.selection();
|
||||||
|
cursor.removeSelectedText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatTextItemHelper::insertFragment(const QTextDocumentFragment fragment, InsertPosition position, bool keepPosition)
|
||||||
|
{
|
||||||
|
auto cursor = textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int currentPosition;
|
||||||
|
switch (position) {
|
||||||
|
case Start:
|
||||||
|
currentPosition = 0;
|
||||||
|
break;
|
||||||
|
case End:
|
||||||
|
currentPosition = document()->characterCount() - 1;
|
||||||
|
break;
|
||||||
|
case Cursor:
|
||||||
|
currentPosition = cursor.position();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPosition < m_fixedStartChars.length()) {
|
||||||
|
currentPosition = m_fixedStartChars.length();
|
||||||
|
}
|
||||||
|
if (currentPosition >= document()->characterCount() - 1 - m_fixedEndChars.length()) {
|
||||||
|
currentPosition = document()->characterCount() - 1 - m_fixedEndChars.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.setPosition(currentPosition);
|
||||||
|
if (textFormat() && textFormat() == Qt::PlainText) {
|
||||||
|
const auto wasEmpty = isEmpty();
|
||||||
|
auto text = fragment.toPlainText();
|
||||||
|
text = trim(text);
|
||||||
|
cursor.insertText(text);
|
||||||
|
if (wasEmpty) {
|
||||||
|
cursor.movePosition(QTextCursor::StartOfBlock);
|
||||||
|
cursor.deletePreviousChar();
|
||||||
|
cursor.movePosition(QTextCursor::EndOfBlock);
|
||||||
|
cursor.deleteChar();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cursor.insertMarkdown(trim(fragment.toMarkdown()));
|
||||||
|
}
|
||||||
|
if (keepPosition) {
|
||||||
|
cursor.setPosition(currentPosition);
|
||||||
|
}
|
||||||
|
setCursorPosition(cursor.position());
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChatTextItemHelper::cursorPosition() const
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return m_textItem->property("cursorPosition").toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChatTextItemHelper::selectionStart() const
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return m_textItem->property("selectionStart").toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChatTextItemHelper::selectionEnd() const
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return m_textItem->property("selectionEnd").toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextCursor ChatTextItemHelper::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;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatTextItemHelper::setCursorPosition(int pos)
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_textItem->setProperty("cursorPosition", pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatTextItemHelper::setCursorVisible(bool visible)
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_textItem->setProperty("cursorVisible", visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatTextItemHelper::setCursorFromTextItem(ChatTextItemHelper *textItem, bool infront, int defaultPosition)
|
||||||
|
{
|
||||||
|
const auto doc = document();
|
||||||
|
if (!doc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_textItem->forceActiveFocus();
|
||||||
|
|
||||||
|
if (!textItem) {
|
||||||
|
const auto docLastBlockLayout = doc->lastBlock().layout();
|
||||||
|
setCursorPosition(infront ? defaultPosition : docLastBlockLayout->lineAt(docLastBlockLayout->lineCount() - 1).textStart());
|
||||||
|
setCursorVisible(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto previousLinePosition = textItem->textCursor().positionInBlock();
|
||||||
|
const auto newMaxLineLength = lineLength(infront ? 0 : lineCount() - 1);
|
||||||
|
setCursorPosition(std::min(previousLinePosition, newMaxLineLength ? *newMaxLineLength : defaultPosition) + (infront ? 0 : doc->lastBlock().position()));
|
||||||
|
setCursorVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatTextItemHelper::itemCursorPositionChanged()
|
||||||
|
{
|
||||||
|
Q_EMIT cursorPositionChanged();
|
||||||
|
Q_EMIT formatChanged();
|
||||||
|
Q_EMIT textFormatChanged();
|
||||||
|
Q_EMIT styleChanged();
|
||||||
|
Q_EMIT listChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<RichFormat::Format> ChatTextItemHelper::formatsAtCursor(QTextCursor cursor) const
|
||||||
|
{
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
cursor = textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return RichFormat::formatsAtCursor(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatTextItemHelper::mergeFormatOnCursor(RichFormat::Format format, QTextCursor cursor)
|
||||||
|
{
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
cursor = textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (RichFormat::typeForFormat(format)) {
|
||||||
|
case RichFormat::Text:
|
||||||
|
mergeTextFormatOnCursor(format, cursor);
|
||||||
|
return;
|
||||||
|
case RichFormat::List:
|
||||||
|
mergeListFormatOnCursor(format, cursor);
|
||||||
|
return;
|
||||||
|
case RichFormat::Block:
|
||||||
|
if (format != RichFormat::Paragraph) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case RichFormat::Style:
|
||||||
|
mergeStyleFormatOnCursor(format, cursor);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatTextItemHelper::mergeTextFormatOnCursor(RichFormat::Format format, QTextCursor cursor)
|
||||||
|
{
|
||||||
|
if (RichFormat::typeForFormat(format) != RichFormat::Text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
|
||||||
|
const auto charFormat = RichFormat::charFormatForFormat(format, RichFormat::hasFormat(cursor, format), theme->alternateBackgroundColor());
|
||||||
|
if (!cursor.hasSelection()) {
|
||||||
|
cursor.select(QTextCursor::WordUnderCursor);
|
||||||
|
}
|
||||||
|
cursor.mergeCharFormat(charFormat);
|
||||||
|
Q_EMIT formatChanged();
|
||||||
|
Q_EMIT textFormatChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatTextItemHelper::mergeStyleFormatOnCursor(RichFormat::Format format, QTextCursor cursor)
|
||||||
|
{
|
||||||
|
// Paragraph is special because it is normally a Block format but if we're already
|
||||||
|
// in a Paragraph it clears any existing style.
|
||||||
|
if (!(RichFormat::typeForFormat(format) == RichFormat::Style || format == RichFormat::Paragraph)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.beginEditBlock();
|
||||||
|
cursor.mergeBlockFormat(RichFormat::blockFormatForFormat(format));
|
||||||
|
|
||||||
|
// Applying style to the current line or selection
|
||||||
|
QTextCursor selectCursor = cursor;
|
||||||
|
if (selectCursor.hasSelection()) {
|
||||||
|
QTextCursor top = selectCursor;
|
||||||
|
top.setPosition(qMin(top.anchor(), top.position()));
|
||||||
|
top.movePosition(QTextCursor::StartOfBlock);
|
||||||
|
|
||||||
|
QTextCursor bottom = selectCursor;
|
||||||
|
bottom.setPosition(qMax(bottom.anchor(), bottom.position()));
|
||||||
|
bottom.movePosition(QTextCursor::EndOfBlock);
|
||||||
|
|
||||||
|
selectCursor.setPosition(top.position(), QTextCursor::MoveAnchor);
|
||||||
|
selectCursor.setPosition(bottom.position(), QTextCursor::KeepAnchor);
|
||||||
|
} else {
|
||||||
|
selectCursor.select(QTextCursor::BlockUnderCursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto chrfmt = RichFormat::charFormatForFormat(format);
|
||||||
|
selectCursor.mergeCharFormat(chrfmt);
|
||||||
|
cursor.mergeBlockCharFormat(chrfmt);
|
||||||
|
cursor.endEditBlock();
|
||||||
|
|
||||||
|
Q_EMIT formatChanged();
|
||||||
|
Q_EMIT styleChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatTextItemHelper::mergeListFormatOnCursor(RichFormat::Format format, const QTextCursor &cursor)
|
||||||
|
{
|
||||||
|
m_nestedListHelper.handleOnBulletType(RichFormat::listStyleForFormat(format), cursor);
|
||||||
|
Q_EMIT formatChanged();
|
||||||
|
Q_EMIT listChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatTextItemHelper::canIndentListMoreAtCursor(QTextCursor cursor) const
|
||||||
|
{
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
cursor = textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m_nestedListHelper.canIndent(cursor) && cursor.blockFormat().headingLevel() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatTextItemHelper::canIndentListLessAtCursor(QTextCursor cursor) const
|
||||||
|
{
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
cursor = textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m_nestedListHelper.canDedent(cursor) && cursor.blockFormat().headingLevel() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatTextItemHelper::indentListMoreAtCursor(QTextCursor cursor)
|
||||||
|
{
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
cursor = textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_nestedListHelper.handleOnIndentMore(cursor);
|
||||||
|
Q_EMIT listChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatTextItemHelper::indentListLessAtCursor(QTextCursor cursor)
|
||||||
|
{
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
cursor = textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_nestedListHelper.handleOnIndentLess(cursor);
|
||||||
|
Q_EMIT listChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatTextItemHelper::forceActiveFocus() const
|
||||||
|
{
|
||||||
|
if (!m_textItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_textItem->forceActiveFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatTextItemHelper::rehighlight() const
|
||||||
|
{
|
||||||
|
m_highlighter->rehighlight();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatTextItemHelper::markdownText() const
|
||||||
|
{
|
||||||
|
const auto doc = document();
|
||||||
|
if (!doc) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return trim(doc->toMarkdown());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatTextItemHelper::trim(QString string) const
|
||||||
|
{
|
||||||
|
while (string.startsWith(u"\n"_s)) {
|
||||||
|
string.removeFirst();
|
||||||
|
}
|
||||||
|
while (string.endsWith(u"\n"_s)) {
|
||||||
|
string.removeLast();
|
||||||
|
}
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "moc_chattextitemhelper.cpp"
|
||||||
@@ -5,14 +5,19 @@
|
|||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QQuickItem>
|
#include <QQuickItem>
|
||||||
|
#include <qcontainerfwd.h>
|
||||||
|
|
||||||
|
#include "enums/chatbartype.h"
|
||||||
#include "enums/richformat.h"
|
#include "enums/richformat.h"
|
||||||
#include "nestedlisthelper_p.h"
|
#include "nestedlisthelper_p.h"
|
||||||
|
|
||||||
class QTextDocument;
|
class QTextDocument;
|
||||||
|
|
||||||
|
class ChatBarSyntaxHighlighter;
|
||||||
|
class NeoChatRoom;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class QmlTextItemWrapper
|
* @class ChatTextItemHelper
|
||||||
*
|
*
|
||||||
* A class to wrap around a QQuickItem that is a QML TextEdit (or inherited from it).
|
* A class to wrap around a QQuickItem that is a QML TextEdit (or inherited from it).
|
||||||
*
|
*
|
||||||
@@ -21,22 +26,49 @@ class QTextDocument;
|
|||||||
*
|
*
|
||||||
* @sa QQuickItem, TextEdit
|
* @sa QQuickItem, TextEdit
|
||||||
*/
|
*/
|
||||||
class QmlTextItemWrapper : public QObject
|
class ChatTextItemHelper : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The QML text Item the ChatTextItemHelper is handling.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit QmlTextItemWrapper(QObject *parent);
|
enum InsertPosition {
|
||||||
|
Cursor,
|
||||||
|
Start,
|
||||||
|
End,
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit ChatTextItemHelper(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
void setRoom(NeoChatRoom *room);
|
||||||
|
|
||||||
|
void setType(ChatBarType::Type type);
|
||||||
|
|
||||||
QQuickItem *textItem() const;
|
QQuickItem *textItem() const;
|
||||||
void setTextItem(QQuickItem *textItem);
|
void setTextItem(QQuickItem *textItem);
|
||||||
|
|
||||||
|
QString fixedStartChars() const;
|
||||||
|
QString fixedEndChars() const;
|
||||||
|
void setFixedChars(const QString &startChars, const QString &endChars);
|
||||||
|
QString initialText() const;
|
||||||
|
void setInitialText(const QString &text);
|
||||||
|
|
||||||
QTextDocument *document() const;
|
QTextDocument *document() const;
|
||||||
|
int lineCount() const;
|
||||||
|
QTextDocumentFragment takeFirstBlock();
|
||||||
|
void fillFragments(bool &hasBefore, QTextDocumentFragment &midFragment, std::optional<QTextDocumentFragment> &afterFragment);
|
||||||
|
void insertFragment(const QTextDocumentFragment fragment, InsertPosition position = Cursor, bool keepPosition = false);
|
||||||
|
|
||||||
QTextCursor textCursor() const;
|
QTextCursor textCursor() const;
|
||||||
int cursorPosition() const;
|
int cursorPosition() const;
|
||||||
void setCursorPosition(int pos);
|
void setCursorPosition(int pos);
|
||||||
void setCursorVisible(bool visible);
|
void setCursorVisible(bool visible);
|
||||||
|
void setCursorFromTextItem(ChatTextItemHelper *textItem, bool infront, int defaultPosition = 0);
|
||||||
|
|
||||||
QList<RichFormat::Format> formatsAtCursor(QTextCursor cursor = {}) const;
|
QList<RichFormat::Format> formatsAtCursor(QTextCursor cursor = {}) const;
|
||||||
void mergeFormatOnCursor(RichFormat::Format format, QTextCursor cursor = {});
|
void mergeFormatOnCursor(RichFormat::Format format, QTextCursor cursor = {});
|
||||||
@@ -48,6 +80,10 @@ public:
|
|||||||
|
|
||||||
void forceActiveFocus() const;
|
void forceActiveFocus() const;
|
||||||
|
|
||||||
|
void rehighlight() const;
|
||||||
|
|
||||||
|
QString markdownText() const;
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void textItemChanged();
|
void textItemChanged();
|
||||||
|
|
||||||
@@ -55,6 +91,8 @@ Q_SIGNALS:
|
|||||||
|
|
||||||
void contentsChanged();
|
void contentsChanged();
|
||||||
|
|
||||||
|
void cleared(ChatTextItemHelper *self);
|
||||||
|
|
||||||
void cursorPositionChanged();
|
void cursorPositionChanged();
|
||||||
|
|
||||||
void formatChanged();
|
void formatChanged();
|
||||||
@@ -64,6 +102,17 @@ Q_SIGNALS:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
QPointer<QQuickItem> m_textItem;
|
QPointer<QQuickItem> m_textItem;
|
||||||
|
QPointer<ChatBarSyntaxHighlighter> m_highlighter;
|
||||||
|
|
||||||
|
std::optional<Qt::TextFormat> textFormat() const;
|
||||||
|
|
||||||
|
QString m_fixedStartChars = {};
|
||||||
|
QString m_fixedEndChars = {};
|
||||||
|
QString m_initialText = {};
|
||||||
|
void initializeChars();
|
||||||
|
|
||||||
|
bool isEmpty() const;
|
||||||
|
std::optional<int> lineLength(int lineNumber) const;
|
||||||
|
|
||||||
int selectionStart() const;
|
int selectionStart() const;
|
||||||
int selectionEnd() const;
|
int selectionEnd() const;
|
||||||
@@ -73,6 +122,8 @@ private:
|
|||||||
void mergeListFormatOnCursor(RichFormat::Format format, const QTextCursor &cursor);
|
void mergeListFormatOnCursor(RichFormat::Format format, const QTextCursor &cursor);
|
||||||
NestedListHelper m_nestedListHelper;
|
NestedListHelper m_nestedListHelper;
|
||||||
|
|
||||||
|
QString trim(QString string) const;
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void itemCursorPositionChanged();
|
void itemCursorPositionChanged();
|
||||||
};
|
};
|
||||||
@@ -6,35 +6,75 @@
|
|||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QTextCursor>
|
#include <QTextCursor>
|
||||||
|
|
||||||
|
#include "chattextitemhelper.h"
|
||||||
#include "completionproxymodel.h"
|
#include "completionproxymodel.h"
|
||||||
#include "models/actionsmodel.h"
|
#include "models/actionsmodel.h"
|
||||||
#include "models/customemojimodel.h"
|
#include "models/customemojimodel.h"
|
||||||
#include "models/emojimodel.h"
|
#include "models/emojimodel.h"
|
||||||
#include "qmltextitemwrapper.h"
|
#include "models/roomlistmodel.h"
|
||||||
#include "userlistmodel.h"
|
#include "userlistmodel.h"
|
||||||
|
|
||||||
CompletionModel::CompletionModel(QObject *parent)
|
CompletionModel::CompletionModel(QObject *parent)
|
||||||
: QAbstractListModel(parent)
|
: QAbstractListModel(parent)
|
||||||
, m_textItem(new QmlTextItemWrapper(this))
|
, m_textItem(new ChatTextItemHelper(this))
|
||||||
, m_filterModel(new CompletionProxyModel(this))
|
, m_filterModel(new CompletionProxyModel(this))
|
||||||
, m_emojiModel(new QConcatenateTablesProxyModel(this))
|
, m_emojiModel(new QConcatenateTablesProxyModel(this))
|
||||||
{
|
{
|
||||||
connect(m_textItem, &QmlTextItemWrapper::textItemChanged, this, &CompletionModel::textItemChanged);
|
|
||||||
connect(m_textItem, &QmlTextItemWrapper::cursorPositionChanged, this, &CompletionModel::updateTextStart);
|
|
||||||
connect(m_textItem, &QmlTextItemWrapper::contentsChanged, this, &CompletionModel::updateCompletion);
|
|
||||||
|
|
||||||
m_emojiModel->addSourceModel(&CustomEmojiModel::instance());
|
m_emojiModel->addSourceModel(&CustomEmojiModel::instance());
|
||||||
m_emojiModel->addSourceModel(&EmojiModel::instance());
|
m_emojiModel->addSourceModel(&EmojiModel::instance());
|
||||||
}
|
}
|
||||||
|
|
||||||
QQuickItem *CompletionModel::textItem() const
|
NeoChatRoom *CompletionModel::room() const
|
||||||
{
|
{
|
||||||
return m_textItem->textItem();
|
return m_room;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CompletionModel::setTextItem(QQuickItem *textItem)
|
void CompletionModel::setRoom(NeoChatRoom *room)
|
||||||
{
|
{
|
||||||
m_textItem->setTextItem(textItem);
|
if (m_room == room) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_room = room;
|
||||||
|
Q_EMIT roomChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatBarType::Type CompletionModel::type() const
|
||||||
|
{
|
||||||
|
return m_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompletionModel::setType(ChatBarType::Type type)
|
||||||
|
{
|
||||||
|
if (type == m_type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_type = type;
|
||||||
|
Q_EMIT typeChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatTextItemHelper *CompletionModel::textItem() const
|
||||||
|
{
|
||||||
|
return m_textItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompletionModel::setTextItem(ChatTextItemHelper *textItem)
|
||||||
|
{
|
||||||
|
if (textItem == m_textItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_textItem) {
|
||||||
|
m_textItem->disconnect(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_textItem = textItem;
|
||||||
|
|
||||||
|
if (m_textItem) {
|
||||||
|
connect(m_textItem, &ChatTextItemHelper::cursorPositionChanged, this, &CompletionModel::updateTextStart);
|
||||||
|
connect(m_textItem, &ChatTextItemHelper::contentsChanged, this, &CompletionModel::updateCompletion);
|
||||||
|
}
|
||||||
|
Q_EMIT textItemChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CompletionModel::updateTextStart()
|
void CompletionModel::updateTextStart()
|
||||||
@@ -239,4 +279,45 @@ void CompletionModel::setUserListModel(UserListModel *userListModel)
|
|||||||
Q_EMIT userListModelChanged();
|
Q_EMIT userListModelChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CompletionModel::insertCompletion(const QString &text, const QUrl &link)
|
||||||
|
{
|
||||||
|
QTextCursor cursor = m_textItem->textCursor();
|
||||||
|
if (cursor.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.beginEditBlock();
|
||||||
|
while (!cursor.selectedText().startsWith(u' ') && !cursor.atBlockStart()) {
|
||||||
|
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
|
||||||
|
}
|
||||||
|
if (cursor.selectedText().startsWith(u' ')) {
|
||||||
|
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
|
||||||
|
}
|
||||||
|
cursor.removeSelectedText();
|
||||||
|
|
||||||
|
const int start = cursor.position();
|
||||||
|
const auto insertString = u"%1 %2"_s.arg(text, link.isEmpty() ? QString() : u" "_s);
|
||||||
|
cursor.insertText(insertString);
|
||||||
|
cursor.setPosition(start);
|
||||||
|
cursor.setPosition(start + text.size(), QTextCursor::KeepAnchor);
|
||||||
|
cursor.setKeepPositionOnInsert(true);
|
||||||
|
cursor.endEditBlock();
|
||||||
|
if (!link.isEmpty()) {
|
||||||
|
pushMention({
|
||||||
|
.cursor = cursor,
|
||||||
|
.text = text,
|
||||||
|
.id = link.toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
m_textItem->rehighlight();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompletionModel::pushMention(const Mention mention) const
|
||||||
|
{
|
||||||
|
if (!m_room || m_type == ChatBarType::None) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_room->cacheForType(m_type)->mentions()->push_back(mention);
|
||||||
|
}
|
||||||
|
|
||||||
#include "moc_completionmodel.cpp"
|
#include "moc_completionmodel.cpp"
|
||||||
|
|||||||
@@ -8,11 +8,13 @@
|
|||||||
#include <QQuickItem>
|
#include <QQuickItem>
|
||||||
#include <QSortFilterProxyModel>
|
#include <QSortFilterProxyModel>
|
||||||
|
|
||||||
#include "roomlistmodel.h"
|
#include "chatbarcache.h"
|
||||||
|
#include "chattextitemhelper.h"
|
||||||
|
#include "enums/chatbartype.h"
|
||||||
|
#include "neochatroom.h"
|
||||||
|
|
||||||
class CompletionProxyModel;
|
class CompletionProxyModel;
|
||||||
class UserListModel;
|
class UserListModel;
|
||||||
class QmlTextItemWrapper;
|
|
||||||
class RoomListModel;
|
class RoomListModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,10 +30,20 @@ class CompletionModel : public QAbstractListModel
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The current room that the text document is being handled for.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The QQuickTextDocument that is being handled.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(ChatBarType::Type type READ type WRITE setType NOTIFY typeChanged)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The QML text Item that completions are being provided for.
|
* @brief The QML text Item that completions are being provided for.
|
||||||
*/
|
*/
|
||||||
Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
|
Q_PROPERTY(ChatTextItemHelper *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The current type of completion being done on the entered text.
|
* @brief The current type of completion being done on the entered text.
|
||||||
@@ -77,8 +89,14 @@ public:
|
|||||||
|
|
||||||
explicit CompletionModel(QObject *parent = nullptr);
|
explicit CompletionModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
QQuickItem *textItem() const;
|
NeoChatRoom *room() const;
|
||||||
void setTextItem(QQuickItem *textItem);
|
void setRoom(NeoChatRoom *room);
|
||||||
|
|
||||||
|
ChatBarType::Type type() const;
|
||||||
|
void setType(ChatBarType::Type type);
|
||||||
|
|
||||||
|
ChatTextItemHelper *textItem() const;
|
||||||
|
void setTextItem(ChatTextItemHelper *textItem);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get the given role value at the given index.
|
* @brief Get the given role value at the given index.
|
||||||
@@ -110,16 +128,20 @@ public:
|
|||||||
AutoCompletionType autoCompletionType() const;
|
AutoCompletionType autoCompletionType() const;
|
||||||
void setAutoCompletionType(AutoCompletionType autoCompletionType);
|
void setAutoCompletionType(AutoCompletionType autoCompletionType);
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_INVOKABLE void insertCompletion(const QString &text, const QUrl &link);
|
||||||
void textItemChanged();
|
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
void roomChanged();
|
void roomChanged();
|
||||||
|
void typeChanged();
|
||||||
|
void textItemChanged();
|
||||||
void autoCompletionTypeChanged();
|
void autoCompletionTypeChanged();
|
||||||
void roomListModelChanged();
|
void roomListModelChanged();
|
||||||
void userListModelChanged();
|
void userListModelChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QPointer<QmlTextItemWrapper> m_textItem;
|
QPointer<NeoChatRoom> m_room;
|
||||||
|
ChatBarType::Type m_type = ChatBarType::None;
|
||||||
|
QPointer<ChatTextItemHelper> m_textItem;
|
||||||
|
|
||||||
int m_textStart = 0;
|
int m_textStart = 0;
|
||||||
void updateTextStart();
|
void updateTextStart();
|
||||||
@@ -132,5 +154,6 @@ private:
|
|||||||
UserListModel *m_userListModel;
|
UserListModel *m_userListModel;
|
||||||
RoomListModel *m_roomListModel;
|
RoomListModel *m_roomListModel;
|
||||||
QConcatenateTablesProxyModel *m_emojiModel;
|
QConcatenateTablesProxyModel *m_emojiModel;
|
||||||
|
|
||||||
|
void pushMention(const Mention mention) const;
|
||||||
};
|
};
|
||||||
Q_DECLARE_METATYPE(CompletionModel::AutoCompletionType);
|
|
||||||
|
|||||||
@@ -1,278 +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
|
|
||||||
|
|
||||||
#include "qmltextitemwrapper.h"
|
|
||||||
#include "richformat.h"
|
|
||||||
|
|
||||||
#include <QQuickTextDocument>
|
|
||||||
#include <QTextCursor>
|
|
||||||
|
|
||||||
#include <Kirigami/Platform/PlatformTheme>
|
|
||||||
|
|
||||||
QmlTextItemWrapper::QmlTextItemWrapper(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
QQuickItem *QmlTextItemWrapper::textItem() const
|
|
||||||
{
|
|
||||||
return m_textItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
void QmlTextItemWrapper::setTextItem(QQuickItem *textItem)
|
|
||||||
{
|
|
||||||
if (textItem == m_textItem) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_textItem) {
|
|
||||||
m_textItem->disconnect(this);
|
|
||||||
if (const auto textDoc = document()) {
|
|
||||||
textDoc->disconnect(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_textItem = textItem;
|
|
||||||
|
|
||||||
if (m_textItem) {
|
|
||||||
connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(itemCursorPositionChanged()));
|
|
||||||
if (document()) {
|
|
||||||
connect(document(), &QTextDocument::contentsChanged, this, &QmlTextItemWrapper::contentsChanged);
|
|
||||||
connect(document(), &QTextDocument::contentsChange, this, &QmlTextItemWrapper::contentsChange);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Q_EMIT textItemChanged();
|
|
||||||
Q_EMIT formatChanged();
|
|
||||||
Q_EMIT textFormatChanged();
|
|
||||||
Q_EMIT styleChanged();
|
|
||||||
Q_EMIT listChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QTextDocument *QmlTextItemWrapper::document() const
|
|
||||||
{
|
|
||||||
if (!m_textItem) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
const auto quickDocument = qvariant_cast<QQuickTextDocument *>(textItem()->property("textDocument"));
|
|
||||||
return quickDocument ? quickDocument->textDocument() : nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
int QmlTextItemWrapper::cursorPosition() const
|
|
||||||
{
|
|
||||||
if (!m_textItem) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return m_textItem->property("cursorPosition").toInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
int QmlTextItemWrapper::selectionStart() const
|
|
||||||
{
|
|
||||||
if (!m_textItem) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return m_textItem->property("selectionStart").toInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
int QmlTextItemWrapper::selectionEnd() const
|
|
||||||
{
|
|
||||||
if (!m_textItem) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return m_textItem->property("selectionEnd").toInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
QTextCursor QmlTextItemWrapper::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;
|
|
||||||
}
|
|
||||||
|
|
||||||
void QmlTextItemWrapper::setCursorPosition(int pos)
|
|
||||||
{
|
|
||||||
if (!m_textItem) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_textItem->setProperty("cursorPosition", pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
void QmlTextItemWrapper::setCursorVisible(bool visible)
|
|
||||||
{
|
|
||||||
if (!m_textItem) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_textItem->setProperty("cursorVisible", visible);
|
|
||||||
}
|
|
||||||
|
|
||||||
void QmlTextItemWrapper::itemCursorPositionChanged()
|
|
||||||
{
|
|
||||||
Q_EMIT cursorPositionChanged();
|
|
||||||
Q_EMIT formatChanged();
|
|
||||||
Q_EMIT textFormatChanged();
|
|
||||||
Q_EMIT styleChanged();
|
|
||||||
Q_EMIT listChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<RichFormat::Format> QmlTextItemWrapper::formatsAtCursor(QTextCursor cursor) const
|
|
||||||
{
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
cursor = textCursor();
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return RichFormat::formatsAtCursor(cursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
void QmlTextItemWrapper::mergeFormatOnCursor(RichFormat::Format format, QTextCursor cursor)
|
|
||||||
{
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
cursor = textCursor();
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch (RichFormat::typeForFormat(format)) {
|
|
||||||
case RichFormat::Text:
|
|
||||||
mergeTextFormatOnCursor(format, cursor);
|
|
||||||
return;
|
|
||||||
case RichFormat::List:
|
|
||||||
mergeListFormatOnCursor(format, cursor);
|
|
||||||
return;
|
|
||||||
case RichFormat::Block:
|
|
||||||
if (format != RichFormat::Paragraph) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case RichFormat::Style:
|
|
||||||
mergeStyleFormatOnCursor(format, cursor);
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void QmlTextItemWrapper::mergeTextFormatOnCursor(RichFormat::Format format, QTextCursor cursor)
|
|
||||||
{
|
|
||||||
if (RichFormat::typeForFormat(format) != RichFormat::Text) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
|
|
||||||
const auto charFormat = RichFormat::charFormatForFormat(format, RichFormat::hasFormat(cursor, format), theme->alternateBackgroundColor());
|
|
||||||
if (!cursor.hasSelection()) {
|
|
||||||
cursor.select(QTextCursor::WordUnderCursor);
|
|
||||||
}
|
|
||||||
cursor.mergeCharFormat(charFormat);
|
|
||||||
Q_EMIT formatChanged();
|
|
||||||
Q_EMIT textFormatChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void QmlTextItemWrapper::mergeStyleFormatOnCursor(RichFormat::Format format, QTextCursor cursor)
|
|
||||||
{
|
|
||||||
// Paragraph is special because it is normally a Block format but if we're already
|
|
||||||
// in a Paragraph it clears any existing style.
|
|
||||||
if (!(RichFormat::typeForFormat(format) == RichFormat::Style || format == RichFormat::Paragraph)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor.beginEditBlock();
|
|
||||||
cursor.mergeBlockFormat(RichFormat::blockFormatForFormat(format));
|
|
||||||
|
|
||||||
// Applying style to the current line or selection
|
|
||||||
QTextCursor selectCursor = cursor;
|
|
||||||
if (selectCursor.hasSelection()) {
|
|
||||||
QTextCursor top = selectCursor;
|
|
||||||
top.setPosition(qMin(top.anchor(), top.position()));
|
|
||||||
top.movePosition(QTextCursor::StartOfBlock);
|
|
||||||
|
|
||||||
QTextCursor bottom = selectCursor;
|
|
||||||
bottom.setPosition(qMax(bottom.anchor(), bottom.position()));
|
|
||||||
bottom.movePosition(QTextCursor::EndOfBlock);
|
|
||||||
|
|
||||||
selectCursor.setPosition(top.position(), QTextCursor::MoveAnchor);
|
|
||||||
selectCursor.setPosition(bottom.position(), QTextCursor::KeepAnchor);
|
|
||||||
} else {
|
|
||||||
selectCursor.select(QTextCursor::BlockUnderCursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto chrfmt = RichFormat::charFormatForFormat(format);
|
|
||||||
selectCursor.mergeCharFormat(chrfmt);
|
|
||||||
cursor.mergeBlockCharFormat(chrfmt);
|
|
||||||
cursor.endEditBlock();
|
|
||||||
|
|
||||||
Q_EMIT formatChanged();
|
|
||||||
Q_EMIT styleChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void QmlTextItemWrapper::mergeListFormatOnCursor(RichFormat::Format format, const QTextCursor &cursor)
|
|
||||||
{
|
|
||||||
m_nestedListHelper.handleOnBulletType(RichFormat::listStyleForFormat(format), cursor);
|
|
||||||
Q_EMIT formatChanged();
|
|
||||||
Q_EMIT listChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool QmlTextItemWrapper::canIndentListMoreAtCursor(QTextCursor cursor) const
|
|
||||||
{
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
cursor = textCursor();
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m_nestedListHelper.canIndent(cursor) && cursor.blockFormat().headingLevel() == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool QmlTextItemWrapper::canIndentListLessAtCursor(QTextCursor cursor) const
|
|
||||||
{
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
cursor = textCursor();
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m_nestedListHelper.canDedent(cursor) && cursor.blockFormat().headingLevel() == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void QmlTextItemWrapper::indentListMoreAtCursor(QTextCursor cursor)
|
|
||||||
{
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
cursor = textCursor();
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m_nestedListHelper.handleOnIndentMore(cursor);
|
|
||||||
Q_EMIT listChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void QmlTextItemWrapper::indentListLessAtCursor(QTextCursor cursor)
|
|
||||||
{
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
cursor = textCursor();
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m_nestedListHelper.handleOnIndentLess(cursor);
|
|
||||||
Q_EMIT listChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void QmlTextItemWrapper::forceActiveFocus() const
|
|
||||||
{
|
|
||||||
if (!m_textItem) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_textItem->forceActiveFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
#include "moc_qmltextitemwrapper.cpp"
|
|
||||||
@@ -110,8 +110,14 @@ QQC2.Control {
|
|||||||
height: implicitHeight
|
height: implicitHeight
|
||||||
y: -height - 5
|
y: -height - 5
|
||||||
z: 10
|
z: 10
|
||||||
|
<<<<<<< HEAD
|
||||||
|
|
||||||
chatDocumentHandler: documentHandler
|
chatDocumentHandler: documentHandler
|
||||||
|
=======
|
||||||
|
room: root.Message.room
|
||||||
|
type: root.chatBarCache.isEditing ? ChatBarType.Edit : ChatBarType.Thread
|
||||||
|
// textItem: textArea
|
||||||
|
>>>>>>> c7858a151 (Move the remaining functionality of ChatDocumentHandler to ChatTextItemHelper or split into own objects)
|
||||||
margins: 0
|
margins: 0
|
||||||
Behavior on height {
|
Behavior on height {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
@@ -125,13 +131,6 @@ QQC2.Control {
|
|||||||
// opt-out of whatever spell checker a styled TextArea might come with
|
// opt-out of whatever spell checker a styled TextArea might come with
|
||||||
Kirigami.SpellCheck.enabled: false
|
Kirigami.SpellCheck.enabled: false
|
||||||
|
|
||||||
ChatDocumentHandler {
|
|
||||||
id: documentHandler
|
|
||||||
type: root.chatBarCache.isEditing ? ChatBarType.Edit : ChatBarType.Thread
|
|
||||||
textItem: textArea
|
|
||||||
room: root.Message.room
|
|
||||||
}
|
|
||||||
|
|
||||||
TextMetrics {
|
TextMetrics {
|
||||||
id: textMetrics
|
id: textMetrics
|
||||||
text: textArea.text
|
text: textArea.text
|
||||||
|
|||||||
@@ -51,11 +51,9 @@ QQC2.Control {
|
|||||||
* @brief The attributes of the component.
|
* @brief The attributes of the component.
|
||||||
*/
|
*/
|
||||||
required property var componentAttributes
|
required property var componentAttributes
|
||||||
readonly property ChatDocumentHandler chatDocumentHandler: componentAttributes?.chatDocumentHandler ?? null
|
readonly property ChatTextItemHelper chatTextItemHelper: componentAttributes?.chatTextItemHelper ?? null
|
||||||
onChatDocumentHandlerChanged: if (chatDocumentHandler) {
|
onChatTextItemHelperChanged: if (chatTextItemHelper) {
|
||||||
chatDocumentHandler.type = ChatBarType.Room;
|
chatTextItemHelper.textItem = codeText;
|
||||||
chatDocumentHandler.room = root.Message.room;
|
|
||||||
chatDocumentHandler.textItem = codeText;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,27 +92,23 @@ QQC2.Control {
|
|||||||
id: codeText
|
id: codeText
|
||||||
|
|
||||||
Keys.onUpPressed: (event) => {
|
Keys.onUpPressed: (event) => {
|
||||||
event.accepted = false;
|
event.accepted = true;
|
||||||
if (root.chatDocumentHandler.atFirstLine) {
|
Message.contentModel.keyHelper.up();
|
||||||
Message.contentModel.focusRow = root.index - 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Keys.onDownPressed: (event) => {
|
Keys.onDownPressed: (event) => {
|
||||||
event.accepted = false;
|
event.accepted = true;
|
||||||
if (root.chatDocumentHandler.atLastLine) {
|
Message.contentModel.keyHelper.down();
|
||||||
Message.contentModel.focusRow = root.index + 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onDeletePressed: (event) => {
|
Keys.onDeletePressed: (event) => {
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
root.chatDocumentHandler.deleteChar();
|
root.Message.contentModel.keyHelper.deleteChar();
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onPressed: (event) => {
|
Keys.onPressed: (event) => {
|
||||||
if (event.key == Qt.Key_Backspace && cursorPosition == 0) {
|
if (event.key == Qt.Key_Backspace && cursorPosition == 0) {
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
root.chatDocumentHandler.backspace();
|
root.Message.contentModel.keyHelper.backspace();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.accepted = false;
|
event.accepted = false;
|
||||||
|
|||||||
@@ -45,11 +45,9 @@ QQC2.TextArea {
|
|||||||
* @brief The attributes of the component.
|
* @brief The attributes of the component.
|
||||||
*/
|
*/
|
||||||
required property var componentAttributes
|
required property var componentAttributes
|
||||||
readonly property ChatDocumentHandler chatDocumentHandler: componentAttributes?.chatDocumentHandler ?? null
|
readonly property ChatTextItemHelper chatTextItemHelper: componentAttributes?.chatTextItemHelper ?? null
|
||||||
onChatDocumentHandlerChanged: if (chatDocumentHandler) {
|
onChatTextItemHelperChanged: if (chatTextItemHelper) {
|
||||||
chatDocumentHandler.type = ChatBarType.Room;
|
chatTextItemHelper.textItem = root;
|
||||||
chatDocumentHandler.room = root.Message.room;
|
|
||||||
chatDocumentHandler.textItem = root;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,16 +64,12 @@ QQC2.TextArea {
|
|||||||
signal selectedTextChanged(string selectedText)
|
signal selectedTextChanged(string selectedText)
|
||||||
|
|
||||||
Keys.onUpPressed: (event) => {
|
Keys.onUpPressed: (event) => {
|
||||||
event.accepted = false;
|
event.accepted = true;
|
||||||
if (root.chatDocumentHandler.atFirstLine) {
|
Message.contentModel.keyHelper.up();
|
||||||
Message.contentModel.focusRow = root.index - 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Keys.onDownPressed: (event) => {
|
Keys.onDownPressed: (event) => {
|
||||||
event.accepted = false;
|
event.accepted = true;
|
||||||
if (root.chatDocumentHandler.atLastLine) {
|
Message.contentModel.keyHelper.down();
|
||||||
Message.contentModel.focusRow = root.index + 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Keys.onLeftPressed: (event) => {
|
Keys.onLeftPressed: (event) => {
|
||||||
if (cursorPosition == 1) {
|
if (cursorPosition == 1) {
|
||||||
@@ -94,12 +88,12 @@ QQC2.TextArea {
|
|||||||
|
|
||||||
Keys.onDeletePressed: (event) => {
|
Keys.onDeletePressed: (event) => {
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
chatDocumentHandler.deleteChar();
|
Message.contentModel.keyHelper.deleteChar();
|
||||||
}
|
}
|
||||||
Keys.onPressed: (event) => {
|
Keys.onPressed: (event) => {
|
||||||
if (event.key == Qt.Key_Backspace) {
|
if (event.key == Qt.Key_Backspace) {
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
chatDocumentHandler.backspace();
|
Message.contentModel.keyHelper.backspace();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.accepted = false;
|
event.accepted = false;
|
||||||
|
|||||||
@@ -49,11 +49,9 @@ TextEdit {
|
|||||||
* @brief The attributes of the component.
|
* @brief The attributes of the component.
|
||||||
*/
|
*/
|
||||||
required property var componentAttributes
|
required property var componentAttributes
|
||||||
readonly property ChatDocumentHandler chatDocumentHandler: componentAttributes?.chatDocumentHandler ?? null
|
readonly property ChatTextItemHelper chatTextItemHelper: componentAttributes?.chatTextItemHelper ?? null
|
||||||
onChatDocumentHandlerChanged: if (chatDocumentHandler) {
|
onChatTextItemHelperChanged: if (chatTextItemHelper) {
|
||||||
chatDocumentHandler.type = ChatBarType.Room;
|
chatTextItemHelper.textItem = root;
|
||||||
chatDocumentHandler.room = root.Message.room;
|
|
||||||
chatDocumentHandler.textItem = root;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,32 +76,28 @@ TextEdit {
|
|||||||
Layout.maximumWidth: Message.maxContentWidth
|
Layout.maximumWidth: Message.maxContentWidth
|
||||||
|
|
||||||
Keys.onUpPressed: (event) => {
|
Keys.onUpPressed: (event) => {
|
||||||
event.accepted = false;
|
event.accepted = true;
|
||||||
if (chatDocumentHandler.atFirstLine) {
|
Message.contentModel.keyHelper.up();
|
||||||
Message.contentModel.focusRow = root.index - 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Keys.onDownPressed: (event) => {
|
Keys.onDownPressed: (event) => {
|
||||||
event.accepted = false;
|
event.accepted = true;
|
||||||
if (chatDocumentHandler.atLastLine) {
|
Message.contentModel.keyHelper.down();
|
||||||
Message.contentModel.focusRow = root.index + 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onTabPressed: (event) => {
|
Keys.onTabPressed: (event) => {
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
chatDocumentHandler.tab();
|
Message.contentModel.keyHelper.tab();
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onDeletePressed: (event) => {
|
Keys.onDeletePressed: (event) => {
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
chatDocumentHandler.deleteChar();
|
Message.contentModel.keyHelper.deleteChar();
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onPressed: (event) => {
|
Keys.onPressed: (event) => {
|
||||||
if (event.key == Qt.Key_Backspace && cursorPosition == 0) {
|
if (event.key == Qt.Key_Backspace && cursorPosition == 0) {
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
chatDocumentHandler.backspace();
|
Message.contentModel.keyHelper.backspace();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.accepted = false;
|
event.accepted = false;
|
||||||
@@ -111,11 +105,11 @@ TextEdit {
|
|||||||
|
|
||||||
Keys.onEnterPressed: (event) => {
|
Keys.onEnterPressed: (event) => {
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
chatDocumentHandler.insertReturn();
|
Message.contentModel.keyHelper.insertReturn();
|
||||||
}
|
}
|
||||||
Keys.onReturnPressed: (event) => {
|
Keys.onReturnPressed: (event) => {
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
chatDocumentHandler.insertReturn();
|
Message.contentModel.keyHelper.insertReturn();
|
||||||
}
|
}
|
||||||
|
|
||||||
onFocusChanged: if (focus && !root.currentFocus) {
|
onFocusChanged: if (focus && !root.currentFocus) {
|
||||||
|
|||||||
@@ -6,20 +6,26 @@
|
|||||||
#include <QTextDocumentFragment>
|
#include <QTextDocumentFragment>
|
||||||
|
|
||||||
#include "chatbarcache.h"
|
#include "chatbarcache.h"
|
||||||
#include "chatdocumenthandler.h"
|
#include "chatkeyhelper.h"
|
||||||
|
#include "chatmarkdownhelper.h"
|
||||||
|
#include "chattextitemhelper.h"
|
||||||
#include "enums/chatbartype.h"
|
#include "enums/chatbartype.h"
|
||||||
#include "enums/messagecomponenttype.h"
|
#include "enums/messagecomponenttype.h"
|
||||||
#include "enums/richformat.h"
|
#include "enums/richformat.h"
|
||||||
#include "messagecontentmodel.h"
|
#include "messagecontentmodel.h"
|
||||||
#include "qmltextitemwrapper.h"
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr auto TextItemKey = "chatTextItemHelper"_L1;
|
||||||
|
}
|
||||||
|
|
||||||
ChatBarMessageContentModel::ChatBarMessageContentModel(QObject *parent)
|
ChatBarMessageContentModel::ChatBarMessageContentModel(QObject *parent)
|
||||||
: MessageContentModel(parent)
|
: MessageContentModel(parent)
|
||||||
, m_currentTextItem(new QmlTextItemWrapper(this))
|
|
||||||
, m_markdownHelper(new ChatMarkdownHelper(this))
|
, m_markdownHelper(new ChatMarkdownHelper(this))
|
||||||
|
, m_keyHelper(new ChatKeyHelper(this))
|
||||||
{
|
{
|
||||||
m_editableActive = true;
|
m_editableActive = true;
|
||||||
connectCurentTextItem();
|
connectKeyHelper();
|
||||||
initializeModel();
|
initializeModel();
|
||||||
|
|
||||||
connect(this, &ChatBarMessageContentModel::roomChanged, this, [this]() {
|
connect(this, &ChatBarMessageContentModel::roomChanged, this, [this]() {
|
||||||
@@ -53,17 +59,38 @@ ChatBarMessageContentModel::ChatBarMessageContentModel(QObject *parent)
|
|||||||
|
|
||||||
Q_EMIT focusRowChanged();
|
Q_EMIT focusRowChanged();
|
||||||
});
|
});
|
||||||
|
connect(this, &ChatBarMessageContentModel::focusRowChanged, this, [this]() {
|
||||||
|
m_markdownHelper->setTextItem(focusedTextItem());
|
||||||
|
m_keyHelper->setTextItem(focusedTextItem());
|
||||||
|
});
|
||||||
|
connect(this, &ChatBarMessageContentModel::roomChanged, this, [this]() {
|
||||||
|
for (const auto &component : m_components) {
|
||||||
|
if (const auto textItem = textItemForComponent(component)) {
|
||||||
|
textItem->setRoom(m_room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(this, &ChatBarMessageContentModel::typeChanged, this, [this]() {
|
||||||
|
for (const auto &component : m_components) {
|
||||||
|
if (const auto textItem = textItemForComponent(component)) {
|
||||||
|
textItem->setType(m_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(m_markdownHelper, &ChatMarkdownHelper::unhandledBlockFormat, this, &ChatBarMessageContentModel::insertStyleAtCursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatBarMessageContentModel::initializeModel()
|
void ChatBarMessageContentModel::initializeModel()
|
||||||
{
|
{
|
||||||
beginInsertRows({}, rowCount(), rowCount());
|
beginInsertRows({}, rowCount(), rowCount());
|
||||||
const auto documentHandler = new ChatDocumentHandler();
|
const auto textItem = new ChatTextItemHelper(this);
|
||||||
connectHandler(documentHandler);
|
textItem->setRoom(m_room);
|
||||||
|
textItem->setType(m_type);
|
||||||
|
connectTextItem(textItem);
|
||||||
m_components += MessageComponent{
|
m_components += MessageComponent{
|
||||||
.type = MessageComponentType::Text,
|
.type = MessageComponentType::Text,
|
||||||
.display = {},
|
.display = {},
|
||||||
.attributes = {{"chatDocumentHandler"_L1, QVariant::fromValue<ChatDocumentHandler *>(documentHandler)}},
|
.attributes = {{TextItemKey, QVariant::fromValue<ChatTextItemHelper *>(textItem)}},
|
||||||
};
|
};
|
||||||
m_currentFocusComponent = QPersistentModelIndex(index(0));
|
m_currentFocusComponent = QPersistentModelIndex(index(0));
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
@@ -71,86 +98,6 @@ void ChatBarMessageContentModel::initializeModel()
|
|||||||
Q_EMIT focusRowChanged();
|
Q_EMIT focusRowChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatBarMessageContentModel::connectCurentTextItem()
|
|
||||||
{
|
|
||||||
if (const auto docHandler = focusedDocumentHandler()) {
|
|
||||||
m_currentTextItem->setTextItem(docHandler->textItem());
|
|
||||||
}
|
|
||||||
connect(this, &ChatBarMessageContentModel::focusRowChanged, this, [this]() {
|
|
||||||
if (const auto docHandler = focusedDocumentHandler()) {
|
|
||||||
m_currentTextItem->setTextItem(docHandler->textItem());
|
|
||||||
m_markdownHelper->setTextItem(m_currentTextItem);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatBarMessageContentModel::connectHandler(ChatDocumentHandler *handler)
|
|
||||||
{
|
|
||||||
connect(handler, &ChatDocumentHandler::contentsChanged, this, &ChatBarMessageContentModel::updateCache);
|
|
||||||
connect(handler, &ChatDocumentHandler::unhandledBackspaceAtBeginning, this, [this](ChatDocumentHandler *handler) {
|
|
||||||
const auto index = indexForDocumentHandler(handler);
|
|
||||||
if (index.isValid()) {
|
|
||||||
if (index.row() > 0 && MessageComponentType::isFileType(m_components[index.row() - 1].type)) {
|
|
||||||
removeAttachment();
|
|
||||||
} else if (m_components[index.row()].type == MessageComponentType::Code || m_components[index.row()].type == MessageComponentType::Quote) {
|
|
||||||
insertComponentAtCursor(MessageComponentType::Text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
connect(handler, &ChatDocumentHandler::removeMe, this, [this](ChatDocumentHandler *handler) {
|
|
||||||
removeComponent(handler);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatDocumentHandler *ChatBarMessageContentModel::documentHandlerForComponent(const MessageComponent &component) const
|
|
||||||
{
|
|
||||||
if (const auto chatDocumentHandler = qvariant_cast<ChatDocumentHandler *>(component.attributes["chatDocumentHandler"_L1])) {
|
|
||||||
return chatDocumentHandler;
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatDocumentHandler *ChatBarMessageContentModel::documentHandlerForIndex(const QModelIndex &index) const
|
|
||||||
{
|
|
||||||
return documentHandlerForComponent(m_components[index.row()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
QModelIndex ChatBarMessageContentModel::indexForDocumentHandler(ChatDocumentHandler *handler) const
|
|
||||||
{
|
|
||||||
for (auto it = m_components.begin(); it != m_components.end(); ++it) {
|
|
||||||
const auto currentIndex = index(it - m_components.begin());
|
|
||||||
if (documentHandlerForIndex(currentIndex) == handler) {
|
|
||||||
return currentIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatBarMessageContentModel::updateDocumentHandlerRefs(const ComponentIt &it)
|
|
||||||
{
|
|
||||||
if (it == m_components.end()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto handler = documentHandlerForComponent(*it);
|
|
||||||
if (!handler) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (it != m_components.begin()) {
|
|
||||||
if (const auto beforeHandler = documentHandlerForComponent(*(it - 1))) {
|
|
||||||
beforeHandler->setNextDocumentHandler(handler);
|
|
||||||
handler->setPreviousDocumentHandler(beforeHandler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (it + 1 != m_components.end()) {
|
|
||||||
if (const auto afterHandler = documentHandlerForComponent(*(it + 1))) {
|
|
||||||
afterHandler->setPreviousDocumentHandler(handler);
|
|
||||||
handler->setNextDocumentHandler(afterHandler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatBarType::Type ChatBarMessageContentModel::type() const
|
ChatBarType::Type ChatBarMessageContentModel::type() const
|
||||||
{
|
{
|
||||||
return m_type;
|
return m_type;
|
||||||
@@ -165,6 +112,46 @@ void ChatBarMessageContentModel::setType(ChatBarType::Type type)
|
|||||||
Q_EMIT typeChanged();
|
Q_EMIT typeChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ChatKeyHelper *ChatBarMessageContentModel::keyHelper() const
|
||||||
|
{
|
||||||
|
return m_keyHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatBarMessageContentModel::connectKeyHelper()
|
||||||
|
{
|
||||||
|
connect(m_keyHelper, &ChatKeyHelper::unhandledUp, this, [this]() {
|
||||||
|
setFocusRow(m_currentFocusComponent.row() - 1);
|
||||||
|
});
|
||||||
|
connect(m_keyHelper, &ChatKeyHelper::unhandledDown, this, [this]() {
|
||||||
|
setFocusRow(m_currentFocusComponent.row() + 1);
|
||||||
|
});
|
||||||
|
connect(m_keyHelper, &ChatKeyHelper::unhandledDelete, this, [this]() {
|
||||||
|
const auto currentRow = m_currentFocusComponent.row();
|
||||||
|
if (currentRow < m_components.size() - 1) {
|
||||||
|
if (const auto nextTextItem = textItemForComponent(m_components[currentRow + 1])) {
|
||||||
|
focusedTextItem()->insertFragment(nextTextItem->takeFirstBlock(), ChatTextItemHelper::Cursor, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(m_keyHelper, &ChatKeyHelper::unhandledBackspace, this, [this]() {
|
||||||
|
const auto currentRow = m_currentFocusComponent.row();
|
||||||
|
if (currentRow > 0) {
|
||||||
|
const auto previousRow = currentRow - 1;
|
||||||
|
if (MessageComponentType::isFileType(m_components[previousRow].type)) {
|
||||||
|
removeAttachment();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (const auto previousTextItem = textItemForComponent(m_components[previousRow])) {
|
||||||
|
previousTextItem->insertFragment(focusedTextItem()->takeFirstBlock(), ChatTextItemHelper::End, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m_components[currentRow].type == MessageComponentType::Code || m_components[currentRow].type == MessageComponentType::Quote) {
|
||||||
|
insertComponentAtCursor(MessageComponentType::Text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
int ChatBarMessageContentModel::focusRow() const
|
int ChatBarMessageContentModel::focusRow() const
|
||||||
{
|
{
|
||||||
return m_currentFocusComponent.row();
|
return m_currentFocusComponent.row();
|
||||||
@@ -199,39 +186,22 @@ void ChatBarMessageContentModel::setFocusIndex(const QModelIndex &index, bool mo
|
|||||||
|
|
||||||
void ChatBarMessageContentModel::focusCurrentComponent(const QModelIndex &previousIndex, bool down)
|
void ChatBarMessageContentModel::focusCurrentComponent(const QModelIndex &previousIndex, bool down)
|
||||||
{
|
{
|
||||||
const auto chatDocumentHandler = focusedDocumentHandler();
|
const auto textItem = focusedTextItem();
|
||||||
if (!chatDocumentHandler) {
|
if (!textItem) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
chatDocumentHandler->setCursorFromDocumentHandler(documentHandlerForIndex(previousIndex), down, MessageComponentType::Quote ? 1 : 0);
|
textItem->setCursorFromTextItem(textItemForIndex(previousIndex), down, MessageComponentType::Quote ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatBarMessageContentModel::refocusCurrentComponent() const
|
void ChatBarMessageContentModel::refocusCurrentComponent() const
|
||||||
{
|
{
|
||||||
const auto chatDocumentHandler = focusedDocumentHandler();
|
const auto textItem = focusedTextItem();
|
||||||
if (!chatDocumentHandler) {
|
if (!textItem) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
chatDocumentHandler->textItem()->forceActiveFocus();
|
textItem->forceActiveFocus();
|
||||||
}
|
|
||||||
|
|
||||||
QmlTextItemWrapper *ChatBarMessageContentModel::currentTextItem() const
|
|
||||||
{
|
|
||||||
return m_currentTextItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatDocumentHandler *ChatBarMessageContentModel::focusedDocumentHandler() const
|
|
||||||
{
|
|
||||||
if (!m_currentFocusComponent.isValid()) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (const auto chatDocumentHandler = documentHandlerForIndex(m_currentFocusComponent)) {
|
|
||||||
return chatDocumentHandler;
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatBarMessageContentModel::emitFocusChangeSignals()
|
void ChatBarMessageContentModel::emitFocusChangeSignals()
|
||||||
@@ -240,6 +210,53 @@ void ChatBarMessageContentModel::emitFocusChangeSignals()
|
|||||||
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {CurrentFocusRole});
|
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {CurrentFocusRole});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ChatTextItemHelper *ChatBarMessageContentModel::focusedTextItem() const
|
||||||
|
{
|
||||||
|
if (!m_currentFocusComponent.isValid()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (const auto textItem = textItemForIndex(m_currentFocusComponent)) {
|
||||||
|
return textItem;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatBarMessageContentModel::connectTextItem(ChatTextItemHelper *chattextitemhelper)
|
||||||
|
{
|
||||||
|
connect(chattextitemhelper, &ChatTextItemHelper::contentsChanged, this, &ChatBarMessageContentModel::updateCache);
|
||||||
|
connect(chattextitemhelper, &ChatTextItemHelper::cleared, this, [this](ChatTextItemHelper *helper) {
|
||||||
|
removeComponent(helper);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatTextItemHelper *ChatBarMessageContentModel::textItemForComponent(const MessageComponent &component) const
|
||||||
|
{
|
||||||
|
if (const auto textItem = qvariant_cast<ChatTextItemHelper *>(component.attributes[TextItemKey])) {
|
||||||
|
return textItem;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatTextItemHelper *ChatBarMessageContentModel::textItemForIndex(const QModelIndex &index) const
|
||||||
|
{
|
||||||
|
return textItemForComponent(m_components[index.row()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
QModelIndex ChatBarMessageContentModel::indexForTextItem(ChatTextItemHelper *textItem) const
|
||||||
|
{
|
||||||
|
if (!textItem) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto it = m_components.begin(); it != m_components.end(); ++it) {
|
||||||
|
const auto currentIndex = index(it - m_components.begin());
|
||||||
|
if (textItemForIndex(currentIndex) == textItem) {
|
||||||
|
return currentIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
void ChatBarMessageContentModel::addAttachment(const QUrl &path)
|
void ChatBarMessageContentModel::addAttachment(const QUrl &path)
|
||||||
{
|
{
|
||||||
if (m_type == ChatBarType::None || !m_room) {
|
if (m_type == ChatBarType::None || !m_room) {
|
||||||
@@ -278,15 +295,16 @@ ChatBarMessageContentModel::insertComponent(int row, MessageComponentType::Type
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (MessageComponentType::isTextType(type)) {
|
if (MessageComponentType::isTextType(type)) {
|
||||||
const auto documentHandler = new ChatDocumentHandler();
|
const auto textItemWrapper = new ChatTextItemHelper(this);
|
||||||
documentHandler->setInitialText(intialText);
|
textItemWrapper->setInitialText(intialText);
|
||||||
|
textItemWrapper->setRoom(m_room);
|
||||||
|
textItemWrapper->setType(m_type);
|
||||||
if (type == MessageComponentType::Quote) {
|
if (type == MessageComponentType::Quote) {
|
||||||
documentHandler->setFixedStartChars(u"\""_s);
|
textItemWrapper->setFixedChars(u"\""_s, u"\""_s);
|
||||||
documentHandler->setFixedEndChars(u"\""_s);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
attributes.insert("chatDocumentHandler"_L1, QVariant::fromValue<ChatDocumentHandler *>(documentHandler));
|
attributes.insert(TextItemKey, QVariant::fromValue<ChatTextItemHelper *>(textItemWrapper));
|
||||||
connectHandler(documentHandler);
|
connectTextItem(textItemWrapper);
|
||||||
}
|
}
|
||||||
beginInsertRows({}, row, row);
|
beginInsertRows({}, row, row);
|
||||||
const auto it = m_components.insert(row,
|
const auto it = m_components.insert(row,
|
||||||
@@ -295,7 +313,6 @@ ChatBarMessageContentModel::insertComponent(int row, MessageComponentType::Type
|
|||||||
.display = {},
|
.display = {},
|
||||||
.attributes = attributes,
|
.attributes = attributes,
|
||||||
});
|
});
|
||||||
updateDocumentHandlerRefs(it);
|
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
return it;
|
return it;
|
||||||
}
|
}
|
||||||
@@ -320,8 +337,8 @@ void ChatBarMessageContentModel::insertStyleAtCursor(RichFormat::Format style)
|
|||||||
void ChatBarMessageContentModel::insertComponentAtCursor(MessageComponentType::Type type)
|
void ChatBarMessageContentModel::insertComponentAtCursor(MessageComponentType::Type type)
|
||||||
{
|
{
|
||||||
if (m_components[m_currentFocusComponent.row()].type == type) {
|
if (m_components[m_currentFocusComponent.row()].type == type) {
|
||||||
if (type == MessageComponentType::Text && focusedDocumentHandler()) {
|
if (type == MessageComponentType::Text && focusedTextItem()) {
|
||||||
currentTextItem()->mergeFormatOnCursor(RichFormat::Paragraph);
|
focusedTextItem()->mergeFormatOnCursor(RichFormat::Paragraph);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -330,8 +347,8 @@ void ChatBarMessageContentModel::insertComponentAtCursor(MessageComponentType::T
|
|||||||
QTextDocumentFragment midFragment;
|
QTextDocumentFragment midFragment;
|
||||||
std::optional<QTextDocumentFragment> afterFragment = std::nullopt;
|
std::optional<QTextDocumentFragment> afterFragment = std::nullopt;
|
||||||
|
|
||||||
if (const auto currentChatDocumentHandler = focusedDocumentHandler()) {
|
if (const auto currentTextItem = focusedTextItem()) {
|
||||||
currentChatDocumentHandler->fillFragments(hasBefore, midFragment, afterFragment);
|
currentTextItem->fillFragments(hasBefore, midFragment, afterFragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto currentType = m_components[m_currentFocusComponent.row()].type;
|
const auto currentType = m_components[m_currentFocusComponent.row()].type;
|
||||||
@@ -343,8 +360,8 @@ void ChatBarMessageContentModel::insertComponentAtCursor(MessageComponentType::T
|
|||||||
|
|
||||||
const auto insertIt = insertComponent(insertRow, type);
|
const auto insertIt = insertComponent(insertRow, type);
|
||||||
if (insertIt != m_components.end()) {
|
if (insertIt != m_components.end()) {
|
||||||
if (const auto insertChatDocumentHandler = documentHandlerForComponent(*insertIt)) {
|
if (const auto insertTextItem = textItemForComponent(*insertIt)) {
|
||||||
insertChatDocumentHandler->insertFragment(midFragment);
|
insertTextItem->insertFragment(midFragment);
|
||||||
}
|
}
|
||||||
m_currentFocusComponent = QPersistentModelIndex(index(insertIt - m_components.begin()));
|
m_currentFocusComponent = QPersistentModelIndex(index(insertIt - m_components.begin()));
|
||||||
emitFocusChangeSignals();
|
emitFocusChangeSignals();
|
||||||
@@ -353,8 +370,8 @@ void ChatBarMessageContentModel::insertComponentAtCursor(MessageComponentType::T
|
|||||||
if (afterFragment) {
|
if (afterFragment) {
|
||||||
const auto afterIt = insertComponent(insertRow + 1, currentType);
|
const auto afterIt = insertComponent(insertRow + 1, currentType);
|
||||||
if (afterIt != m_components.end()) {
|
if (afterIt != m_components.end()) {
|
||||||
if (const auto afterChatDocumentHandler = documentHandlerForComponent(*afterIt)) {
|
if (const auto afterTextItem = textItemForComponent(*afterIt)) {
|
||||||
afterChatDocumentHandler->insertFragment(*afterFragment);
|
afterTextItem->insertFragment(*afterFragment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -405,21 +422,10 @@ ChatBarMessageContentModel::ComponentIt ChatBarMessageContentModel::removeCompon
|
|||||||
setFocusRow(newFocusRow);
|
setFocusRow(newFocusRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (const auto chatDocumentHandler = documentHandlerForIndex(index(row))) {
|
if (const auto textItem = textItemForIndex(index(row))) {
|
||||||
const auto beforeHandler = chatDocumentHandler->previousDocumentHandler();
|
m_components[row].attributes.remove(TextItemKey);
|
||||||
const auto afterHandler = chatDocumentHandler->nextDocumentHandler();
|
textItem->disconnect(this);
|
||||||
if (beforeHandler && afterHandler) {
|
textItem->deleteLater();
|
||||||
beforeHandler->setNextDocumentHandler(afterHandler);
|
|
||||||
afterHandler->setPreviousDocumentHandler(beforeHandler);
|
|
||||||
} else if (beforeHandler) {
|
|
||||||
beforeHandler->setNextDocumentHandler(nullptr);
|
|
||||||
} else if (afterHandler) {
|
|
||||||
afterHandler->setPreviousDocumentHandler(nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_components[row].attributes.remove("chatDocumentHandler"_L1);
|
|
||||||
chatDocumentHandler->disconnect(this);
|
|
||||||
chatDocumentHandler->deleteLater();
|
|
||||||
}
|
}
|
||||||
it = m_components.erase(it);
|
it = m_components.erase(it);
|
||||||
endRemoveRows();
|
endRemoveRows();
|
||||||
@@ -427,9 +433,9 @@ ChatBarMessageContentModel::ComponentIt ChatBarMessageContentModel::removeCompon
|
|||||||
return it;
|
return it;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatBarMessageContentModel::removeComponent(ChatDocumentHandler *handler)
|
void ChatBarMessageContentModel::removeComponent(ChatTextItemHelper *textItem)
|
||||||
{
|
{
|
||||||
const auto index = indexForDocumentHandler(handler);
|
const auto index = indexForTextItem(textItem);
|
||||||
if (index.isValid()) {
|
if (index.isValid()) {
|
||||||
removeComponent(index.row());
|
removeComponent(index.row());
|
||||||
}
|
}
|
||||||
@@ -479,8 +485,8 @@ QString ChatBarMessageContentModel::messageText() const
|
|||||||
QString text;
|
QString text;
|
||||||
for (const auto &component : m_components) {
|
for (const auto &component : m_components) {
|
||||||
if (MessageComponentType::isTextType(component.type)) {
|
if (MessageComponentType::isTextType(component.type)) {
|
||||||
if (const auto chatDocumentHandler = documentHandlerForComponent(component)) {
|
if (const auto textItem = textItemForComponent(component)) {
|
||||||
auto newText = chatDocumentHandler->htmlText();
|
auto newText = textItem->markdownText();
|
||||||
if (component.type == MessageComponentType::Quote) {
|
if (component.type == MessageComponentType::Quote) {
|
||||||
newText = formatQuote(newText);
|
newText = formatQuote(newText);
|
||||||
} else if (component.type == MessageComponentType::Code) {
|
} else if (component.type == MessageComponentType::Code) {
|
||||||
@@ -517,9 +523,9 @@ void ChatBarMessageContentModel::clearModel()
|
|||||||
{
|
{
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
for (const auto &component : m_components) {
|
for (const auto &component : m_components) {
|
||||||
if (const auto chatDocumentHandler = documentHandlerForComponent(component)) {
|
if (const auto textItem = textItemForComponent(component)) {
|
||||||
chatDocumentHandler->disconnect(this);
|
textItem->disconnect(this);
|
||||||
chatDocumentHandler->deleteLater();
|
textItem->deleteLater();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_components.clear();
|
m_components.clear();
|
||||||
|
|||||||
@@ -5,14 +5,14 @@
|
|||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
#include <qabstractitemmodel.h>
|
|
||||||
|
|
||||||
#include "chatdocumenthandler.h"
|
#include "chatkeyhelper.h"
|
||||||
|
#include "chatmarkdownhelper.h"
|
||||||
|
#include "chattextitemhelper.h"
|
||||||
#include "enums/messagecomponenttype.h"
|
#include "enums/messagecomponenttype.h"
|
||||||
#include "enums/richformat.h"
|
#include "enums/richformat.h"
|
||||||
#include "messagecomponent.h"
|
#include "messagecomponent.h"
|
||||||
#include "models/messagecontentmodel.h"
|
#include "models/messagecontentmodel.h"
|
||||||
#include "qmltextitemwrapper.h"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class ChatBarMessageContentModel
|
* @class ChatBarMessageContentModel
|
||||||
@@ -42,31 +42,34 @@ class ChatBarMessageContentModel : public MessageContentModel
|
|||||||
/**
|
/**
|
||||||
* @brief The text item that the helper is interfacing with.
|
* @brief The text item that the helper is interfacing with.
|
||||||
*
|
*
|
||||||
* This is a QQuickItem that is a TextEdit (or inherited from) wrapped in a QmlTextItemWrapper
|
* This is a QQuickItem that is a TextEdit (or inherited from) wrapped in a ChatTextItemHelper
|
||||||
* to provide easy access to properties and basic QTextDocument manipulation.
|
* to provide easy access to properties and basic QTextDocument manipulation.
|
||||||
*
|
*
|
||||||
* @sa TextEdit, QTextDocument, QmlTextItemWrapper
|
* @sa TextEdit, QTextDocument, ChatTextItemHelper
|
||||||
*/
|
*/
|
||||||
Q_PROPERTY(QmlTextItemWrapper *currentTextItem READ currentTextItem NOTIFY focusRowChanged)
|
Q_PROPERTY(ChatKeyHelper *keyHelper READ keyHelper CONSTANT)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The ChatDocumentHandler of the model component that currently has focus.
|
* @brief The text item that the helper is interfacing with.
|
||||||
|
*
|
||||||
|
* This is a QQuickItem that is a TextEdit (or inherited from) wrapped in a ChatTextItemHelper
|
||||||
|
* to provide easy access to properties and basic QTextDocument manipulation.
|
||||||
|
*
|
||||||
|
* @sa TextEdit, QTextDocument, ChatTextItemHelper
|
||||||
*/
|
*/
|
||||||
Q_PROPERTY(ChatDocumentHandler *focusedDocumentHandler READ focusedDocumentHandler NOTIFY focusRowChanged)
|
Q_PROPERTY(ChatTextItemHelper *focusedTextItem READ focusedTextItem NOTIFY focusRowChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ChatBarMessageContentModel(QObject *parent = nullptr);
|
explicit ChatBarMessageContentModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
ChatBarType::Type type() const;
|
ChatBarType::Type type() const;
|
||||||
void setType(ChatBarType::Type type);
|
void setType(ChatBarType::Type type);
|
||||||
|
ChatKeyHelper *keyHelper() const;
|
||||||
int focusRow() const;
|
int focusRow() const;
|
||||||
MessageComponentType::Type focusType() const;
|
MessageComponentType::Type focusType() const;
|
||||||
Q_INVOKABLE void setFocusRow(int focusRow, bool mouse = false);
|
Q_INVOKABLE void setFocusRow(int focusRow, bool mouse = false);
|
||||||
void setFocusIndex(const QModelIndex &index, bool mouse = false);
|
|
||||||
Q_INVOKABLE void refocusCurrentComponent() const;
|
Q_INVOKABLE void refocusCurrentComponent() const;
|
||||||
QmlTextItemWrapper *currentTextItem() const;
|
ChatTextItemHelper *focusedTextItem() const;
|
||||||
ChatDocumentHandler *focusedDocumentHandler() const;
|
|
||||||
|
|
||||||
Q_INVOKABLE void insertStyleAtCursor(RichFormat::Format style);
|
Q_INVOKABLE void insertStyleAtCursor(RichFormat::Format style);
|
||||||
|
|
||||||
@@ -91,22 +94,22 @@ private:
|
|||||||
|
|
||||||
std::optional<QString> getReplyEventId() override;
|
std::optional<QString> getReplyEventId() override;
|
||||||
|
|
||||||
QPointer<QmlTextItemWrapper> m_currentTextItem;
|
void setFocusIndex(const QModelIndex &index, bool mouse = false);
|
||||||
void connectCurentTextItem();
|
void focusCurrentComponent(const QModelIndex &previousIndex, bool down);
|
||||||
QPointer<ChatMarkdownHelper> m_markdownHelper;
|
void emitFocusChangeSignals();
|
||||||
|
|
||||||
void connectHandler(ChatDocumentHandler *handler);
|
void connectTextItem(ChatTextItemHelper *chattextitemhelper);
|
||||||
ChatDocumentHandler *documentHandlerForComponent(const MessageComponent &component) const;
|
ChatTextItemHelper *textItemForComponent(const MessageComponent &component) const;
|
||||||
ChatDocumentHandler *documentHandlerForIndex(const QModelIndex &index) const;
|
ChatTextItemHelper *textItemForIndex(const QModelIndex &index) const;
|
||||||
QModelIndex indexForDocumentHandler(ChatDocumentHandler *handler) const;
|
QModelIndex indexForTextItem(ChatTextItemHelper *textItem) const;
|
||||||
void updateDocumentHandlerRefs(const ComponentIt &it);
|
|
||||||
|
QPointer<ChatMarkdownHelper> m_markdownHelper;
|
||||||
|
QPointer<ChatKeyHelper> m_keyHelper;
|
||||||
|
void connectKeyHelper();
|
||||||
|
|
||||||
ComponentIt insertComponent(int row, MessageComponentType::Type type, QVariantMap attributes = {}, const QString &intialText = {});
|
ComponentIt insertComponent(int row, MessageComponentType::Type type, QVariantMap attributes = {}, const QString &intialText = {});
|
||||||
ComponentIt removeComponent(ComponentIt it);
|
ComponentIt removeComponent(ComponentIt it);
|
||||||
void removeComponent(ChatDocumentHandler *handler);
|
void removeComponent(ChatTextItemHelper *textItem);
|
||||||
|
|
||||||
void focusCurrentComponent(const QModelIndex &previousIndex, bool down);
|
|
||||||
void emitFocusChangeSignals();
|
|
||||||
|
|
||||||
void updateCache() const;
|
void updateCache() const;
|
||||||
QString messageText() const;
|
QString messageText() const;
|
||||||
|
|||||||
Reference in New Issue
Block a user