File indexing completed on 2024-12-22 04:46:06

0001 /*
0002    SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org>
0003 
0004    SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "messagedelegatehelpertext.h"
0008 #include "colorsandmessageviewstyle.h"
0009 #include "delegateutils/messagedelegateutils.h"
0010 #include "messagecache.h"
0011 #include "model/messagesmodel.h"
0012 #include "model/threadmessagemodel.h"
0013 #include "rocketchataccount.h"
0014 #include "ruqolawidgets_selection_debug.h"
0015 #include "textconverter.h"
0016 #include "utils.h"
0017 
0018 #include <KStringHandler>
0019 
0020 #include <QAbstractItemView>
0021 #include <QAbstractTextDocumentLayout>
0022 #include <QDrag>
0023 #include <QListView>
0024 #include <QMimeData>
0025 #include <QPainter>
0026 #include <QPixmap>
0027 #include <QStyleOptionViewItem>
0028 #include <QToolTip>
0029 
0030 MessageDelegateHelperText::MessageDelegateHelperText(RocketChatAccount *account, QListView *view, TextSelectionImpl *textSelectionImpl)
0031     : MessageDelegateHelperBase(account, view, textSelectionImpl)
0032 {
0033 }
0034 
0035 MessageDelegateHelperText::~MessageDelegateHelperText() = default;
0036 
0037 QString MessageDelegateHelperText::makeMessageText(const QPersistentModelIndex &index, bool connectToUpdates) const
0038 {
0039     const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>();
0040     Q_ASSERT(message);
0041     QString text = index.data(MessagesModel::MessageConvertedText).toString();
0042     const QString threadMessageId = message->threadMessageId();
0043 
0044     if (mShowThreadContext && !threadMessageId.isEmpty()) {
0045         const auto sameAsPreviousMessageThread = [&] {
0046             if (index.row() < 1) {
0047                 return false;
0048             }
0049             const auto previousIndex = index.sibling(index.row() - 1, index.column());
0050             const auto *previousMessage = previousIndex.data(MessagesModel::MessagePointer).value<Message *>();
0051             Q_ASSERT(previousMessage);
0052             return threadMessageId == previousMessage->threadMessageId();
0053         }();
0054         if (mRocketChatAccount) {
0055             if (!sameAsPreviousMessageThread) {
0056                 const MessagesModel *model = mRocketChatAccount->messageModelForRoom(message->roomId());
0057                 if (model) {
0058                     auto that = const_cast<MessageDelegateHelperText *>(this);
0059                     // Find the previous message in the same thread, to use it as context
0060                     auto hasSameThread = [&](const Message &msg) {
0061                         return msg.threadMessageId() == threadMessageId || msg.messageId() == threadMessageId;
0062                     };
0063                     Message contextMessage = model->findLastMessageBefore(message->messageId(), hasSameThread);
0064                     auto messageCache = mRocketChatAccount->messageCache();
0065                     if (contextMessage.messageId().isEmpty()) {
0066                         ThreadMessageModel *cachedModel = messageCache->threadMessageModel(threadMessageId);
0067                         if (cachedModel) {
0068                             contextMessage = cachedModel->findLastMessageBefore(message->messageId(), hasSameThread);
0069                             if (contextMessage.messageId().isEmpty()) {
0070                                 Message *msg = messageCache->messageForId(threadMessageId);
0071                                 if (msg) {
0072                                     contextMessage = *msg;
0073                                 } else if (connectToUpdates) {
0074                                     connect(messageCache, &MessageCache::messageLoaded, this, [=](const QString &msgId) {
0075                                         if (msgId == threadMessageId) {
0076                                             that->updateView(index);
0077                                         }
0078                                     });
0079                                 }
0080                             } else {
0081                                 // qDebug() << "using cache, found" << contextMessage.messageId() << contextMessage.text();
0082                             }
0083                         } else if (connectToUpdates) {
0084                             connect(messageCache, &MessageCache::modelLoaded, this, [=]() {
0085                                 that->updateView(index);
0086                             });
0087                         }
0088                     }
0089                     // Use TextConverter in case it starts with a [](URL) reply marker
0090                     const QString contextText =
0091                         KStringHandler::rsqueeze(QLatin1Char('@') + contextMessage.username() + QLatin1String(": ") + contextMessage.text(), 200);
0092 
0093                     QString needUpdateMessageId;
0094                     const int maximumRecursiveQuotedText = mRocketChatAccount->ruqolaServerConfig()->messageQuoteChainLimit();
0095                     const TextConverter::ConvertMessageTextSettings settings(contextText,
0096                                                                              mRocketChatAccount->userName(),
0097                                                                              {},
0098                                                                              mRocketChatAccount->highlightWords(),
0099                                                                              mRocketChatAccount->emojiManager(),
0100                                                                              mRocketChatAccount->messageCache(),
0101                                                                              contextMessage.mentions(),
0102                                                                              contextMessage.channels(),
0103                                                                              mSearchText,
0104                                                                              maximumRecursiveQuotedText);
0105 
0106                     int recursiveIndex = 0;
0107                     const QString contextString = TextConverter::convertMessageText(settings, needUpdateMessageId, recursiveIndex);
0108                     if (!needUpdateMessageId.isEmpty() && connectToUpdates) {
0109                         connect(messageCache, &MessageCache::messageLoaded, this, [=](const QString &msgId) {
0110                             if (msgId == needUpdateMessageId) {
0111                                 that->updateView(index);
0112                             }
0113                         });
0114                     }
0115                     // TODO add url ?
0116                     Utils::QuotedRichTextInfo info;
0117                     info.richText = contextString;
0118                     text.prepend(Utils::formatQuotedRichText(std::move(info)));
0119                 }
0120             }
0121         }
0122     }
0123 
0124     return text;
0125 }
0126 
0127 QString MessageDelegateHelperText::urlAt(const QModelIndex &index, QPoint relativePos) const
0128 {
0129     auto document = documentForIndex(index);
0130     if (!document) {
0131         return {};
0132     }
0133 
0134     return document->documentLayout()->anchorAt(relativePos);
0135 }
0136 
0137 void MessageDelegateHelperText::draw(QPainter *painter, QRect rect, const QModelIndex &index, const QStyleOptionViewItem &option)
0138 {
0139     auto *doc = documentForIndex(index, rect.width(), true);
0140     if (!doc) {
0141         return;
0142     }
0143     MessageDelegateUtils::drawSelection(doc, rect, rect.top(), painter, index, option, mTextSelectionImpl->textSelection(), {}, {});
0144 }
0145 
0146 QSize MessageDelegateHelperText::sizeHint(const QModelIndex &index, int maxWidth, const QStyleOptionViewItem &option, qreal *pBaseLine) const
0147 {
0148     Q_UNUSED(option)
0149     auto *doc = documentForIndex(index, maxWidth, true);
0150     return MessageDelegateUtils::textSizeHint(doc, pBaseLine);
0151 }
0152 
0153 bool MessageDelegateHelperText::handleMouseEvent(QMouseEvent *mouseEvent, QRect messageRect, const QStyleOptionViewItem &option, const QModelIndex &index)
0154 {
0155     Q_UNUSED(option)
0156     if (!messageRect.contains(mouseEvent->pos())) {
0157         return false;
0158     }
0159 
0160     const QPoint pos = mouseEvent->pos() - messageRect.topLeft();
0161     const QEvent::Type eventType = mouseEvent->type();
0162     // Text selection
0163     switch (eventType) {
0164     case QEvent::MouseButtonPress:
0165         mTextSelectionImpl->setMightStartDrag(false);
0166         if (const auto *doc = documentForIndex(index, messageRect.width(), true)) {
0167             const int charPos = doc->documentLayout()->hitTest(pos, Qt::FuzzyHit);
0168             qCDebug(RUQOLAWIDGETS_SELECTION_LOG) << "pressed at pos" << charPos;
0169             if (charPos == -1) {
0170                 return false;
0171             }
0172             if (mTextSelectionImpl->textSelection()->contains(index, charPos) && doc->documentLayout()->hitTest(pos, Qt::ExactHit) != -1) {
0173                 mTextSelectionImpl->setMightStartDrag(true);
0174                 return true;
0175             }
0176 
0177             // QWidgetTextControl also has code to support selectBlockOnTripleClick, shift to extend selection
0178             // (look there if you want to add these things)
0179 
0180             mTextSelectionImpl->textSelection()->setStart(index, charPos);
0181             return true;
0182         } else {
0183             mTextSelectionImpl->textSelection()->clear();
0184         }
0185         break;
0186     case QEvent::MouseMove:
0187         if (!mTextSelectionImpl->mightStartDrag()) {
0188             if (const auto *doc = documentForIndex(index, messageRect.width(), true)) {
0189                 const int charPos = doc->documentLayout()->hitTest(pos, Qt::FuzzyHit);
0190                 if (charPos != -1) {
0191                     // QWidgetTextControl also has code to support isPreediting()/commitPreedit(), selectBlockOnTripleClick
0192                     mTextSelectionImpl->textSelection()->setEnd(index, charPos);
0193                     return true;
0194                 }
0195             }
0196         }
0197         break;
0198     case QEvent::MouseButtonRelease:
0199         qCDebug(RUQOLAWIDGETS_SELECTION_LOG) << "released";
0200         MessageDelegateUtils::setClipboardSelection(mTextSelectionImpl->textSelection());
0201         // Clicks on links
0202         if (!mTextSelectionImpl->textSelection()->hasSelection()) {
0203             if (const auto *doc = documentForIndex(index, messageRect.width(), true)) {
0204                 const QString link = doc->documentLayout()->anchorAt(pos);
0205                 if (!link.isEmpty()) {
0206                     Q_EMIT mRocketChatAccount->openLinkRequested(link);
0207                     return true;
0208                 }
0209             }
0210         } else if (mTextSelectionImpl->mightStartDrag()) {
0211             // clicked into selection, didn't start drag, clear it (like kwrite and QTextEdit)
0212             mTextSelectionImpl->textSelection()->clear();
0213         }
0214         // don't return true here, we need to send mouse release events to other helpers (ex: click on image)
0215         break;
0216     case QEvent::MouseButtonDblClick:
0217         if (!mTextSelectionImpl->textSelection()->hasSelection()) {
0218             if (const auto *doc = documentForIndex(index, messageRect.width(), true)) {
0219                 const int charPos = doc->documentLayout()->hitTest(pos, Qt::FuzzyHit);
0220                 qCDebug(RUQOLAWIDGETS_SELECTION_LOG) << "double-clicked at pos" << charPos;
0221                 if (charPos == -1) {
0222                     return false;
0223                 }
0224                 mTextSelectionImpl->textSelection()->selectWordUnderCursor(index, charPos, this);
0225                 return true;
0226             }
0227         }
0228         break;
0229     default:
0230         break;
0231     }
0232     return false;
0233 }
0234 
0235 bool MessageDelegateHelperText::handleHelpEvent(QHelpEvent *helpEvent, QRect messageRect, const QModelIndex &index)
0236 {
0237     if (helpEvent->type() != QEvent::ToolTip) {
0238         return false;
0239     }
0240 
0241     const auto *doc = documentForIndex(index, messageRect.width(), true);
0242     if (!doc) {
0243         return false;
0244     }
0245 
0246     const QPoint pos = helpEvent->pos() - messageRect.topLeft();
0247     QString formattedTooltip;
0248     if (MessageDelegateUtils::generateToolTip(doc, pos, formattedTooltip)) {
0249         QToolTip::showText(helpEvent->globalPos(), formattedTooltip, mListView);
0250         return true;
0251     }
0252     return true;
0253 }
0254 
0255 bool MessageDelegateHelperText::maybeStartDrag(QMouseEvent *mouseEvent, QRect messageRect, const QStyleOptionViewItem &option, const QModelIndex &index)
0256 {
0257     if (!mTextSelectionImpl->mightStartDrag()) {
0258         return false;
0259     }
0260     if (mTextSelectionImpl->textSelection()->hasSelection()) {
0261         const QPoint pos = mouseEvent->pos() - messageRect.topLeft();
0262         const auto *doc = documentForIndex(index, messageRect.width(), false);
0263         const int charPos = doc->documentLayout()->hitTest(pos, Qt::FuzzyHit);
0264         if (charPos != -1 && mTextSelectionImpl->textSelection()->contains(index, charPos)) {
0265             auto mimeData = new QMimeData;
0266             mimeData->setHtml(mTextSelectionImpl->textSelection()->selectedText(TextSelection::Html));
0267             mimeData->setText(mTextSelectionImpl->textSelection()->selectedText(TextSelection::Text));
0268             auto drag = new QDrag(const_cast<QWidget *>(option.widget));
0269             drag->setMimeData(mimeData);
0270             drag->exec(Qt::CopyAction);
0271             mTextSelectionImpl->setMightStartDrag(false); // don't clear selection on release
0272             return true;
0273         }
0274     }
0275     return false;
0276 }
0277 
0278 void MessageDelegateHelperText::setShowThreadContext(bool b)
0279 {
0280     mShowThreadContext = b;
0281 }
0282 
0283 bool MessageDelegateHelperText::showThreadContext() const
0284 {
0285     return mShowThreadContext;
0286 }
0287 
0288 QTextDocument *MessageDelegateHelperText::documentForIndex(const QModelIndex &index) const
0289 {
0290     return documentForIndex(index, -1, false);
0291 }
0292 
0293 QTextDocument *MessageDelegateHelperText::documentForIndex(const QModelIndex &index, int width, bool connectToUpdates) const
0294 {
0295     Q_ASSERT(index.isValid());
0296     const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>();
0297     Q_ASSERT(message);
0298     const auto messageId = message->messageId();
0299     Q_ASSERT(!messageId.isEmpty());
0300 
0301     auto it = mDocumentCache.find(messageId);
0302     if (it != mDocumentCache.end()) {
0303         auto ret = it->value.get();
0304         if (width != -1 && !qFuzzyCompare(ret->textWidth(), width)) {
0305             ret->setTextWidth(width);
0306         }
0307         return ret;
0308     }
0309 
0310     const auto persistentIndex = QPersistentModelIndex(index);
0311     const QString text = makeMessageText(persistentIndex, connectToUpdates);
0312     if (text.isEmpty()) {
0313         return nullptr;
0314     }
0315     auto doc = MessageDelegateUtils::createTextDocument(MessageDelegateUtils::useItalicsForMessage(index), text, width);
0316     auto ret = doc.get();
0317     connect(&ColorsAndMessageViewStyle::self(), &ColorsAndMessageViewStyle::needToUpdateColors, ret, [this, persistentIndex, ret]() {
0318         ret->setHtml(makeMessageText(persistentIndex, false));
0319         auto that = const_cast<MessageDelegateHelperText *>(this);
0320         that->updateView(persistentIndex);
0321     });
0322     mDocumentCache.insert(messageId, std::move(doc));
0323     return ret;
0324 }
0325 
0326 #include "moc_messagedelegatehelpertext.cpp"