Use RichText text input to provide real mentions

This use internally now a QTextDocument and a new C++ class to
manipulate the document with QTextCursor.
This commit is contained in:
Carl Schwan
2020-11-26 14:25:30 +00:00
parent d6e1a6a45b
commit d6ab6bacdd
5 changed files with 380 additions and 85 deletions

View File

@@ -15,6 +15,7 @@ add_executable(neochat
main.cpp
notificationsmanager.cpp
sortfilterroomlistmodel.cpp
chatdocumenthandler.cpp
../res.qrc
)

231
src/chatdocumenthandler.cpp Normal file
View File

@@ -0,0 +1,231 @@
#include "chatdocumenthandler.h"
#include <QQmlFile>
#include <QQmlFileSelector>
#include <QQuickTextDocument>
#include <QTextBlock>
#include <QTextDocument>
#include <QStringBuilder>
#include "neochatroom.h"
ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
: QObject(parent)
, m_document(nullptr)
, m_cursorPosition(-1)
{
}
QQuickTextDocument *ChatDocumentHandler::document() const
{
return m_document;
}
void ChatDocumentHandler::setDocument(QQuickTextDocument *document)
{
if (document == m_document)
return;
if (m_document)
m_document->textDocument()->disconnect(this);
m_document = document;
Q_EMIT documentChanged();
}
int ChatDocumentHandler::cursorPosition() const
{
return m_cursorPosition;
}
void ChatDocumentHandler::setCursorPosition(int position)
{
if (position == m_cursorPosition)
return;
m_cursorPosition = position;
Q_EMIT cursorPositionChanged();
}
int ChatDocumentHandler::selectionStart() const
{
return m_selectionStart;
}
void ChatDocumentHandler::setSelectionStart(int position)
{
if (position == m_selectionStart)
return;
m_selectionStart = position;
Q_EMIT selectionStartChanged();
}
int ChatDocumentHandler::selectionEnd() const
{
return m_selectionEnd;
}
void ChatDocumentHandler::setSelectionEnd(int position)
{
if (position == m_selectionEnd)
return;
m_selectionEnd = position;
Q_EMIT selectionEndChanged();
}
QTextCursor ChatDocumentHandler::textCursor() const
{
QTextDocument *doc = textDocument();
if (!doc)
return QTextCursor();
QTextCursor cursor = QTextCursor(doc);
if (m_selectionStart != m_selectionEnd) {
cursor.setPosition(m_selectionStart);
cursor.setPosition(m_selectionEnd, QTextCursor::KeepAnchor);
} else {
cursor.setPosition(m_cursorPosition);
}
return cursor;
}
QTextDocument *ChatDocumentHandler::textDocument() const
{
if (!m_document)
return nullptr;
return m_document->textDocument();
}
NeoChatRoom *ChatDocumentHandler::room() const
{
return m_room;
}
void ChatDocumentHandler::setRoom(NeoChatRoom *room)
{
if (m_room == room)
return;
m_room = room;
Q_EMIT roomChanged();
}
QVariantMap ChatDocumentHandler::getAutocompletionInfo()
{
QTextCursor cursor = textCursor();
if (cursor.block().text() == m_lastState) {
// ignore change, it was caused by autocompletion
return QVariantMap {
{"type", AutoCompletionType::Ignore},
};
}
if (m_cursorPosition != m_autoCompleteBeginPosition
&& m_cursorPosition != m_autoCompleteEndPosition) {
// we moved our cursor, so cancel autocompletion
}
QString text = cursor.block().text();
QString textBeforeCursor = text;
textBeforeCursor.truncate(m_cursorPosition);
QString autoCompletePrefix = textBeforeCursor.section(" ", -1);
if (autoCompletePrefix.isEmpty()) {
return QVariantMap {
{"type", AutoCompletionType::None},
};
}
if (autoCompletePrefix.startsWith("@") || autoCompletePrefix.startsWith(":")) {
m_autoCompleteBeginPosition = textBeforeCursor.lastIndexOf(" ") + 1; // 1 == space
if (autoCompletePrefix.startsWith("@")) {
autoCompletePrefix.remove(0, 1);
return QVariantMap {
{"keyword", autoCompletePrefix},
{"type", AutoCompletionType::User},
};
} else {
return QVariantMap {
{"keyword", autoCompletePrefix},
{"type", AutoCompletionType::Emoji},
};
}
}
return QVariantMap {
{"type", AutoCompletionType::None},
};
}
void ChatDocumentHandler::postMessage(const QString &attachementPath, const QString &replyEventId) const
{
if (!m_room || !m_document)
return;
QString cleanedText = m_document->textDocument()->toMarkdown();
cleanedText = cleanedText.trimmed();
if (attachementPath.length() > 0) {
m_room->uploadFile(attachementPath, cleanedText);
}
if (cleanedText.length() == 0)
return;
auto messageEventType = RoomMessageEvent::MsgType::Text;
const QString rainbowPrefix = QStringLiteral("/rainbow ");
const QString mePrefix = QStringLiteral("/me ");
const QString noticePrefix = QStringLiteral("/notice ");
if (cleanedText.indexOf(rainbowPrefix) == 0) {
cleanedText = cleanedText.remove(0, rainbowPrefix.length());
QString rainbowText;
QStringList rainbowColors { "#ff2b00", "#ff5500", "#ff8000", "#ffaa00", "#ffd500", "#ffff00", "#d4ff00", "#aaff00", "#80ff00", "#55ff00", "#2bff00", "#00ff00", "#00ff2b", "#00ff55", "#00ff80", "#00ffaa", "#00ffd5", "#00ffff", "#00d4ff", "#00aaff", "#007fff", "#0055ff", "#002bff", "#0000ff", "#2a00ff", "#5500ff", "#7f00ff", "#aa00ff", "#d400ff", "#ff00ff", "#ff00d4", "#ff00aa", "#ff0080", "#ff0055", "#ff002b", "#ff0000" };
for (int i = 0; i < cleanedText.length(); i++) {
rainbowText = rainbowText % QStringLiteral("<font color='") % rainbowColors.at(i % rainbowColors.length()) % "'>" % cleanedText.at(i) % "</font>";
}
m_room->postHtmlMessage(cleanedText, rainbowText, messageEventType, replyEventId);
return;
}
if (cleanedText.indexOf(mePrefix) == 0) {
cleanedText = cleanedText.remove(0, mePrefix.length());
messageEventType = RoomMessageEvent::MsgType::Emote;
} else if (cleanedText.indexOf(noticePrefix) == 0) {
cleanedText = cleanedText.remove(0, noticePrefix.length());
messageEventType = RoomMessageEvent::MsgType::Notice;
}
m_room->postArbitaryMessage(cleanedText, messageEventType, replyEventId);
}
void ChatDocumentHandler::replaceAutoComplete(const QString &word)
{
QTextCursor cursor = textCursor();
if (cursor.block().text() == m_lastState) {
m_document->textDocument()->undo();
}
cursor.beginEditBlock();
cursor.select(QTextCursor::WordUnderCursor);
cursor.removeSelectedText();
cursor.deletePreviousChar();
while (!cursor.atBlockStart()) {
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
if (cursor.selectedText() == " ") {
cursor.movePosition(QTextCursor::NextCharacter);
break;
}
}
cursor.insertHtml(word);
m_lastState = cursor.block().text();
cursor.endEditBlock();
}

86
src/chatdocumenthandler.h Normal file
View File

@@ -0,0 +1,86 @@
/**
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <QFont>
#include <QObject>
#include <QTextCursor>
#include <QUrl>
class QTextDocument;
class QQuickTextDocument;
class NeoChatRoom;
class ChatDocumentHandler : public QObject
{
Q_OBJECT
Q_PROPERTY(QQuickTextDocument *document READ document WRITE setDocument NOTIFY documentChanged)
Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY cursorPositionChanged)
Q_PROPERTY(int selectionStart READ selectionStart WRITE setSelectionStart NOTIFY selectionStartChanged)
Q_PROPERTY(int selectionEnd READ selectionEnd WRITE setSelectionEnd NOTIFY selectionEndChanged)
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
public:
enum AutoCompletionType {
User,
Emoji,
None,
Ignore,
};
Q_ENUM(AutoCompletionType)
explicit ChatDocumentHandler(QObject *object = nullptr);
QQuickTextDocument *document() const;
void setDocument(QQuickTextDocument *document);
int cursorPosition() const;
void setCursorPosition(int position);
int selectionStart() const;
void setSelectionStart(int position);
int selectionEnd() const;
void setSelectionEnd(int position);
NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
Q_INVOKABLE void postMessage(const QString &attachmentPath, const QString &replyEventId) const;
/// This function will look at the current QTextCursor and determine if there
/// is the posibility to autocomplete it.
Q_INVOKABLE QVariantMap getAutocompletionInfo();
Q_INVOKABLE void replaceAutoComplete(const QString &word);
Q_SIGNALS:
void documentChanged();
void cursorPositionChanged();
void selectionStartChanged();
void selectionEndChanged();
void roomChanged();
private:
QTextCursor textCursor() const;
QTextDocument *textDocument() const;
QQuickTextDocument *m_document;
NeoChatRoom *m_room;
int m_cursorPosition;
int m_selectionStart;
int m_selectionEnd;
int m_autoCompleteBeginPosition = -1;
int m_autoCompleteEndPosition = -1;
QString m_lastState;
};
Q_DECLARE_METATYPE(ChatDocumentHandler::AutoCompletionType);

View File

@@ -40,6 +40,7 @@
#include "userdirectorylistmodel.h"
#include "userlistmodel.h"
#include "neochatconfig.h"
#include "chatdocumenthandler.h"
using namespace Quotient;
@@ -81,6 +82,7 @@ int main(int argc, char *argv[])
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Clipboard", &clipboard);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Config", config);
qmlRegisterType<AccountListModel>("org.kde.neochat", 1, 0, "AccountListModel");
qmlRegisterType<ChatDocumentHandler>("org.kde.neochat", 1, 0, "ChatDocumentHandler");
qmlRegisterType<RoomListModel>("org.kde.neochat", 1, 0, "RoomListModel");
qmlRegisterType<UserListModel>("org.kde.neochat", 1, 0, "UserListModel");
qmlRegisterType<MessageEventModel>("org.kde.neochat", 1, 0, "MessageEventModel");