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"