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"