File indexing completed on 2024-12-08 04:34:23

0001 /*
0002    SPDX-FileCopyrightText: 2022-2024 Laurent Montel <montel@kde.org>
0003 
0004    SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "messagelistdelegatebase.h"
0008 #include "delegateutils/messagedelegateutils.h"
0009 #include "delegateutils/textselectionimpl.h"
0010 
0011 #include "rocketchataccount.h"
0012 #include "ruqolawidgets_debug.h"
0013 #include "ruqolawidgets_selection_debug.h"
0014 #include "textconverter.h"
0015 
0016 #include <QAbstractTextDocumentLayout>
0017 #include <QDrag>
0018 #include <QListView>
0019 #include <QMimeData>
0020 #include <QMouseEvent>
0021 
0022 MessageListDelegateBase::MessageListDelegateBase(QListView *view, QObject *parent)
0023     : QItemDelegate{parent}
0024     , MessageListTextUi(new TextSelectionImpl, view)
0025 {
0026     TextUiBase::setCacheMaxEntries(32); // Enough?
0027     auto textSelection = mTextSelectionImpl->textSelection();
0028     textSelection->setTextHelperFactory(this);
0029     connect(textSelection, &TextSelection::repaintNeeded, this, &MessageListDelegateBase::updateView);
0030 }
0031 
0032 MessageListDelegateBase::~MessageListDelegateBase()
0033 {
0034     delete mTextSelectionImpl;
0035 }
0036 
0037 void MessageListDelegateBase::clearSizeHintCache()
0038 {
0039     MessageListTextUi::clearSizeHintCache();
0040 }
0041 
0042 void MessageListDelegateBase::clearCache()
0043 {
0044     TextUiBase::clearCache();
0045 }
0046 
0047 bool MessageListDelegateBase::maybeStartDrag(QMouseEvent *mouseEvent, QRect messageRect, const QStyleOptionViewItem &option, const QModelIndex &index)
0048 {
0049     if (!mTextSelectionImpl->mightStartDrag()) {
0050         return false;
0051     }
0052     const QPoint pos = mouseEvent->pos() - messageRect.topLeft();
0053     if (mTextSelectionImpl->textSelection()->hasSelection()) {
0054         const auto *doc = documentForModelIndex(index, messageRect.width());
0055         const int charPos = doc->documentLayout()->hitTest(pos, Qt::FuzzyHit);
0056         if (charPos != -1 && mTextSelectionImpl->textSelection()->contains(index, charPos)) {
0057             auto mimeData = new QMimeData;
0058             mimeData->setHtml(mTextSelectionImpl->textSelection()->selectedText(TextSelection::Html));
0059             mimeData->setText(mTextSelectionImpl->textSelection()->selectedText(TextSelection::Text));
0060             auto drag = new QDrag(const_cast<QWidget *>(option.widget));
0061             drag->setMimeData(mimeData);
0062             drag->exec(Qt::CopyAction);
0063             mTextSelectionImpl->setMightStartDrag(false); // don't clear selection on release
0064             return true;
0065         }
0066     }
0067     return false;
0068 }
0069 
0070 bool MessageListDelegateBase::handleMouseEvent(QMouseEvent *mouseEvent, QRect messageRect, const QStyleOptionViewItem &option, const QModelIndex &index)
0071 {
0072     Q_UNUSED(option)
0073     if (!messageRect.contains(mouseEvent->pos())) {
0074         return false;
0075     }
0076 
0077     const QPoint pos = mouseEvent->pos() - messageRect.topLeft();
0078     const QEvent::Type eventType = mouseEvent->type();
0079     // Text selection
0080     switch (eventType) {
0081     case QEvent::MouseButtonPress:
0082         mTextSelectionImpl->setMightStartDrag(false);
0083         if (const auto *doc = documentForModelIndex(index, messageRect.width())) {
0084             const int charPos = doc->documentLayout()->hitTest(pos, Qt::FuzzyHit);
0085             qCDebug(RUQOLAWIDGETS_SELECTION_LOG) << "pressed at pos" << charPos;
0086             if (charPos == -1) {
0087                 return false;
0088             }
0089             if (mTextSelectionImpl->textSelection()->contains(index, charPos) && doc->documentLayout()->hitTest(pos, Qt::ExactHit) != -1) {
0090                 mTextSelectionImpl->setMightStartDrag(true);
0091                 return true;
0092             }
0093 
0094             // TODO add triple click
0095 
0096             // QWidgetTextControl also has code to support selectBlockOnTripleClick, shift to extend selection
0097             // (look there if you want to add these things)
0098 
0099             mTextSelectionImpl->textSelection()->setStart(index, charPos);
0100             return true;
0101         } else {
0102             mTextSelectionImpl->textSelection()->clear();
0103         }
0104         break;
0105     case QEvent::MouseMove:
0106         if (!mTextSelectionImpl->mightStartDrag()) {
0107             if (const auto *doc = documentForModelIndex(index, messageRect.width())) {
0108                 const int charPos = doc->documentLayout()->hitTest(pos, Qt::FuzzyHit);
0109                 if (charPos != -1) {
0110                     // QWidgetTextControl also has code to support isPreediting()/commitPreedit(), selectBlockOnTripleClick
0111                     mTextSelectionImpl->textSelection()->setEnd(index, charPos);
0112                     return true;
0113                 }
0114             }
0115         }
0116         break;
0117     case QEvent::MouseButtonRelease:
0118         qCDebug(RUQOLAWIDGETS_SELECTION_LOG) << "released";
0119         MessageDelegateUtils::setClipboardSelection(mTextSelectionImpl->textSelection());
0120         // Clicks on links
0121         if (!mTextSelectionImpl->textSelection()->hasSelection()) {
0122             if (const auto *doc = documentForModelIndex(index, messageRect.width())) {
0123                 const QString link = doc->documentLayout()->anchorAt(pos);
0124                 auto rocketAccount = rocketChatAccount(index);
0125                 if (!rocketAccount) {
0126                     qCWarning(RUQOLAWIDGETS_LOG) << "rochetchat account is null. Verify if it's ok";
0127                     return true;
0128                 }
0129                 if (!link.isEmpty()) {
0130                     Q_EMIT rocketAccount->openLinkRequested(link);
0131                     return true;
0132                 }
0133             }
0134         } else if (mTextSelectionImpl->mightStartDrag()) {
0135             // clicked into selection, didn't start drag, clear it (like kwrite and QTextEdit)
0136             mTextSelectionImpl->textSelection()->clear();
0137         }
0138         // don't return true here, we need to send mouse release events to other helpers (ex: click on image)
0139         break;
0140     case QEvent::MouseButtonDblClick:
0141         if (!mTextSelectionImpl->textSelection()->hasSelection()) {
0142             if (const auto *doc = documentForModelIndex(index, messageRect.width())) {
0143                 const int charPos = doc->documentLayout()->hitTest(pos, Qt::FuzzyHit);
0144                 qCDebug(RUQOLAWIDGETS_SELECTION_LOG) << "double-clicked at pos" << charPos;
0145                 if (charPos == -1) {
0146                     return false;
0147                 }
0148                 mTextSelectionImpl->textSelection()->selectWordUnderCursor(index, charPos, this);
0149                 return true;
0150             }
0151         }
0152         break;
0153     default:
0154         break;
0155     }
0156     return false;
0157 }
0158 
0159 QTextDocument *MessageListDelegateBase::documentForIndex(const QModelIndex &index) const
0160 {
0161     return documentForModelIndex(index, -1);
0162 }
0163 
0164 QSize MessageListDelegateBase::textSizeHint(const QModelIndex &index, int maxWidth, const QStyleOptionViewItem &option, qreal *pBaseLine) const
0165 {
0166     Q_UNUSED(option)
0167     auto *doc = documentForModelIndex(index, maxWidth);
0168     return MessageDelegateUtils::textSizeHint(doc, pBaseLine);
0169 }
0170 
0171 void MessageListDelegateBase::selectAll(const QStyleOptionViewItem &option, const QModelIndex &index)
0172 {
0173     Q_UNUSED(option);
0174     mTextSelectionImpl->textSelection()->selectMessage(index);
0175     mListView->update(index);
0176     MessageDelegateUtils::setClipboardSelection(mTextSelectionImpl->textSelection());
0177 }
0178 
0179 QString MessageListDelegateBase::selectedText() const
0180 {
0181     return mTextSelectionImpl->textSelection()->selectedText(TextSelection::Format::Text);
0182 }
0183 
0184 bool MessageListDelegateBase::hasSelection() const
0185 {
0186     return mTextSelectionImpl->textSelection()->hasSelection();
0187 }
0188 
0189 const QString &MessageListDelegateBase::searchText() const
0190 {
0191     return mSearchText;
0192 }
0193 
0194 void MessageListDelegateBase::setSearchText(const QString &newSearchText)
0195 {
0196     if (mSearchText != newSearchText) {
0197         mSearchText = newSearchText;
0198         clearCache();
0199     }
0200 }
0201 
0202 QTextDocument *MessageListDelegateBase::documentForDelegate(RocketChatAccount *rcAccount, const QString &messageId, const QString &messageStr, int width) const
0203 {
0204     auto it = mDocumentCache.find(messageId);
0205     if (it != mDocumentCache.end()) {
0206         auto ret = it->value.get();
0207         if (width != -1 && !qFuzzyCompare(ret->textWidth(), width)) {
0208             ret->setTextWidth(width);
0209         }
0210         return ret;
0211     }
0212     if (messageStr.isEmpty()) {
0213         return nullptr;
0214     }
0215     // Use TextConverter in case it starts with a [](URL) reply marker
0216     QString needUpdateMessageId; // TODO use it ?
0217     int maximumRecursiveQuotedText = -1;
0218     if (rcAccount) {
0219         maximumRecursiveQuotedText = rcAccount->ruqolaServerConfig()->messageQuoteChainLimit();
0220     }
0221     const TextConverter::ConvertMessageTextSettings settings(messageStr,
0222                                                              rcAccount ? rcAccount->userName() : QString(),
0223                                                              {},
0224                                                              rcAccount ? rcAccount->highlightWords() : QStringList(),
0225                                                              rcAccount ? rcAccount->emojiManager() : nullptr,
0226                                                              rcAccount ? rcAccount->messageCache() : nullptr,
0227                                                              {},
0228                                                              {},
0229                                                              mSearchText,
0230                                                              maximumRecursiveQuotedText);
0231 
0232     int recursiveIndex = 0;
0233     const QString contextString = TextConverter::convertMessageText(settings, needUpdateMessageId, recursiveIndex);
0234     auto doc = MessageDelegateUtils::createTextDocument(false, contextString, width);
0235     auto ret = doc.get();
0236     mDocumentCache.insert(messageId, std::move(doc));
0237     return ret;
0238 }
0239 
0240 #include "moc_messagelistdelegatebase.cpp"