File indexing completed on 2024-12-01 04:36:39
0001 /* 0002 SPDX-FileCopyrightText: 2021 David Faure <faure@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "textselection.h" 0008 #include "messages/message.h" 0009 #include "model/messagesmodel.h" 0010 #include "ruqolawidgets_selection_debug.h" 0011 0012 #include <QTextCursor> 0013 #include <QTextDocument> 0014 #include <QTextDocumentFragment> 0015 0016 TextSelection::TextSelection() = default; 0017 0018 DocumentFactoryInterface::~DocumentFactoryInterface() = default; 0019 0020 bool TextSelection::hasSelection() const 0021 { 0022 return mStartIndex.isValid() && mEndIndex.isValid() 0023 && ((mStartPos > -1 && mEndPos > -1 && mStartPos != mEndPos) || !mAttachmentSelection.isEmpty() || !mMessageUrlSelection.isEmpty()); 0024 } 0025 0026 TextSelection::OrderedPositions TextSelection::orderedPositions() const 0027 { 0028 Q_ASSERT(!mStartIndex.isValid() || !mEndIndex.isValid() || mStartIndex.model() == mEndIndex.model()); 0029 TextSelection::OrderedPositions ret{mStartIndex.row(), mStartPos, mEndIndex.row(), mEndPos}; 0030 if (ret.fromRow > ret.toRow) { 0031 std::swap(ret.fromRow, ret.toRow); 0032 std::swap(ret.fromCharPos, ret.toCharPos); 0033 } 0034 return ret; 0035 } 0036 0037 void TextSelection::selectionText(const OrderedPositions ordered, 0038 Format format, 0039 int row, 0040 const QModelIndex &index, 0041 QTextDocument *doc, 0042 QString &str, 0043 const MessageAttachment &att, 0044 const MessageUrl &messageUrl) const 0045 { 0046 const QTextCursor cursor = selectionForIndex(index, doc, att, messageUrl); 0047 const QTextDocumentFragment fragment(cursor); 0048 str += format == Text ? fragment.toPlainText() : fragment.toHtml(); 0049 if (row < ordered.toRow) { 0050 str += QLatin1Char('\n'); 0051 } 0052 } 0053 0054 DocumentFactoryInterface *TextSelection::messageUrlHelperFactory() const 0055 { 0056 return mMessageUrlHelperFactory; 0057 } 0058 0059 void TextSelection::setMessageUrlHelperFactory(DocumentFactoryInterface *newMessageUrlHelperFactory) 0060 { 0061 mMessageUrlHelperFactory = newMessageUrlHelperFactory; 0062 } 0063 0064 DocumentFactoryInterface *TextSelection::textHelperFactory() const 0065 { 0066 return mTextHelperFactory; 0067 } 0068 0069 const QVector<DocumentFactoryInterface *> &TextSelection::attachmentFactories() const 0070 { 0071 return mAttachmentFactories; 0072 } 0073 0074 void TextSelection::setAttachmentFactories(const QVector<DocumentFactoryInterface *> &newAttachmentFactories) 0075 { 0076 mAttachmentFactories = newAttachmentFactories; 0077 } 0078 0079 void TextSelection::setTextHelperFactory(DocumentFactoryInterface *newTextHelperFactory) 0080 { 0081 mTextHelperFactory = newTextHelperFactory; 0082 } 0083 0084 QString TextSelection::selectedText(Format format) const 0085 { 0086 if (!hasSelection()) { 0087 return {}; 0088 } 0089 const OrderedPositions ordered = orderedPositions(); 0090 QString str; 0091 for (int row = ordered.fromRow; row <= ordered.toRow; ++row) { 0092 const QModelIndex index = QModelIndex(mStartIndex).siblingAtRow(row); 0093 QTextDocument *doc = mTextHelperFactory ? mTextHelperFactory->documentForIndex(index) : nullptr; 0094 if (doc) { 0095 selectionText(ordered, format, row, index, doc, str); 0096 } 0097 const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>(); 0098 if (message) { 0099 const auto attachments = message->attachments(); 0100 for (const auto &att : attachments) { 0101 for (auto factory : std::as_const(mAttachmentFactories)) { 0102 doc = factory->documentForAttachement(att); 0103 if (doc) { 0104 if (!str.endsWith(QLatin1Char('\n'))) { 0105 str += QLatin1Char('\n'); 0106 } 0107 selectionText(ordered, format, row, index, doc, str, att); 0108 break; 0109 } 0110 } 0111 } 0112 0113 const auto messageUrls = message->urls(); 0114 for (const auto &url : messageUrls) { 0115 doc = mMessageUrlHelperFactory->documentForUrlPreview(url); 0116 if (doc) { 0117 if (!str.endsWith(QLatin1Char('\n'))) { 0118 str += QLatin1Char('\n'); 0119 } 0120 selectionText(ordered, format, row, index, doc, str, {}, url); 0121 break; 0122 } 0123 } 0124 } 0125 } 0126 return str; 0127 } 0128 0129 bool TextSelection::contains(const QModelIndex &index, int charPos, const MessageAttachment &att) const 0130 { 0131 Q_UNUSED(att); 0132 if (!hasSelection()) 0133 return false; 0134 Q_ASSERT(index.model() == mStartIndex.model()); 0135 // TODO implement check attachment 0136 const int row = index.row(); 0137 const OrderedPositions ordered = orderedPositions(); 0138 if (row == ordered.fromRow) { 0139 if (row == ordered.toRow) // single line selection 0140 return ordered.fromCharPos <= charPos && charPos <= ordered.toCharPos; 0141 return ordered.fromCharPos <= charPos; 0142 } else if (row == ordered.toRow) { 0143 return charPos <= ordered.toCharPos; 0144 } else { 0145 return row > ordered.fromRow && row < ordered.toRow; 0146 } 0147 } 0148 0149 QTextCursor TextSelection::selectionForIndex(const QModelIndex &index, QTextDocument *doc, const MessageAttachment &att, const MessageUrl &msgUrl) const 0150 { 0151 if (!hasSelection()) 0152 return {}; 0153 Q_ASSERT(index.model() == mStartIndex.model()); 0154 Q_ASSERT(index.model() == mEndIndex.model()); 0155 0156 if (att.isValid() && mAttachmentSelection.isEmpty() && mMessageUrlSelection.isEmpty() && msgUrl.hasHtmlDescription()) { 0157 return {}; 0158 } 0159 const OrderedPositions ordered = orderedPositions(); 0160 int fromCharPos = ordered.fromCharPos; 0161 int toCharPos = ordered.toCharPos; 0162 // qDebug() << "BEFORE toCharPos" << toCharPos << " fromCharPos " << fromCharPos; 0163 QTextCursor cursor(doc); 0164 0165 if (att.isValid()) { 0166 for (const AttachmentSelection &attSelection : std::as_const(mAttachmentSelection)) { 0167 if (attSelection.attachment == att) { 0168 fromCharPos = attSelection.fromCharPos; 0169 toCharPos = attSelection.toCharPos; 0170 // qDebug() << "ATTACHMENT toCharPos" << toCharPos << " fromCharPos " << fromCharPos; 0171 break; 0172 } 0173 } 0174 } 0175 // TODO add block 0176 0177 if (msgUrl.hasHtmlDescription()) { 0178 for (const MessageUrlSelection &messageUrlSelection : std::as_const(mMessageUrlSelection)) { 0179 if (messageUrlSelection.messageUrl == msgUrl) { 0180 fromCharPos = messageUrlSelection.fromCharPos; 0181 toCharPos = messageUrlSelection.toCharPos; 0182 // qDebug() << "MessageUrl toCharPos" << toCharPos << " fromCharPos " << fromCharPos; 0183 break; 0184 } 0185 } 0186 } 0187 0188 // qDebug() << "AFTER toCharPos" << toCharPos << " fromCharPos " << fromCharPos; 0189 const int row = index.row(); 0190 if (row == ordered.fromRow) 0191 cursor.setPosition(qMax(fromCharPos, 0)); 0192 else if (row > ordered.fromRow) 0193 cursor.setPosition(0); 0194 else 0195 return {}; 0196 if (row == ordered.toRow) 0197 cursor.setPosition(qMax(toCharPos, 0), QTextCursor::KeepAnchor); 0198 else if (row < ordered.toRow) 0199 cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); 0200 else 0201 return {}; 0202 return cursor; 0203 } 0204 0205 void TextSelection::clear() 0206 { 0207 const QModelIndex index = mStartIndex; 0208 const OrderedPositions ordered = orderedPositions(); 0209 0210 mStartIndex = QPersistentModelIndex{}; 0211 mEndIndex = QPersistentModelIndex{}; 0212 mStartPos = -1; 0213 mEndPos = -1; 0214 mAttachmentSelection.clear(); 0215 mMessageUrlSelection.clear(); 0216 0217 // Repaint indexes that are no longer selected 0218 if (ordered.fromRow > -1) { 0219 if (ordered.toRow > -1) { 0220 for (int row = ordered.fromRow; row <= ordered.toRow; ++row) { 0221 Q_EMIT repaintNeeded(index.siblingAtRow(row)); 0222 } 0223 } else { 0224 Q_EMIT repaintNeeded(index); 0225 } 0226 } 0227 } 0228 0229 void TextSelection::setStart(const QModelIndex &index, int charPos, const MessageAttachment &msgAttach) 0230 { 0231 clear(); 0232 Q_ASSERT(index.isValid()); 0233 mStartIndex = index; 0234 if (msgAttach.isValid()) { 0235 AttachmentSelection selection; 0236 selection.fromCharPos = charPos; 0237 selection.attachment = msgAttach; 0238 mAttachmentSelection.append(std::move(selection)); 0239 // qDebug() << " start selection is in attachment "; 0240 } else { 0241 mStartPos = charPos; 0242 } 0243 } 0244 0245 void TextSelection::setEnd(const QModelIndex &index, int charPos, const MessageAttachment &msgAttach) 0246 { 0247 int from = mEndIndex.row(); 0248 int to = index.row(); 0249 if (from != -1 && from != to) { 0250 mEndIndex = index; 0251 0252 if (from > to) { // reducing (moving the end up) 0253 std::swap(from, to); 0254 ++from; // 'from' is @p index, it's under the mouse anyway 0255 } else { // extending (moving the down) 0256 --to; // 'to' is @p index, it's under the mouse anyway 0257 } 0258 0259 // Repaint indexes that are no longer selected 0260 // or that got selected when moving the mouse down fast 0261 for (int row = from; row <= to; ++row) { 0262 Q_EMIT repaintNeeded(index.siblingAtRow(row)); 0263 } 0264 } 0265 0266 Q_ASSERT(index.isValid()); 0267 mEndIndex = index; 0268 if (msgAttach.isValid()) { 0269 const auto countAtt{mAttachmentSelection.count()}; 0270 for (int i = 0; i < countAtt; ++i) { 0271 if (mAttachmentSelection.at(i).attachment == msgAttach) { 0272 AttachmentSelection attachmentSelectFound = mAttachmentSelection.takeAt(i); 0273 attachmentSelectFound.toCharPos = charPos; 0274 mAttachmentSelection.append(std::move(attachmentSelectFound)); 0275 return; 0276 } 0277 } 0278 0279 AttachmentSelection selection; 0280 selection.toCharPos = charPos; 0281 selection.attachment = msgAttach; 0282 mAttachmentSelection.append(std::move(selection)); 0283 } else { 0284 mEndPos = charPos; 0285 } 0286 } 0287 0288 void TextSelection::selectWord(const QModelIndex &index, int charPos, QTextDocument *doc) 0289 { 0290 QTextCursor cursor(doc); 0291 cursor.setPosition(charPos); 0292 clear(); 0293 cursor.select(QTextCursor::WordUnderCursor); 0294 mStartIndex = index; 0295 mEndIndex = index; 0296 mStartPos = cursor.selectionStart(); 0297 mEndPos = cursor.selectionEnd(); 0298 } 0299 0300 void TextSelection::selectWordUnderCursor(const QModelIndex &index, int charPos, DocumentFactoryInterface *factory) 0301 { 0302 if (!factory) { 0303 qCWarning(RUQOLAWIDGETS_SELECTION_LOG) << " Factory is null. It's a bug"; 0304 return; 0305 } 0306 QTextDocument *doc = factory->documentForIndex(index); 0307 selectWord(index, charPos, doc); 0308 } 0309 0310 void TextSelection::selectWordUnderCursor(const QModelIndex &index, int charPos, DocumentFactoryInterface *factory, const MessageAttachment &msgAttach) 0311 { 0312 if (!factory) { 0313 qCWarning(RUQOLAWIDGETS_SELECTION_LOG) << " Factory is null. It's a bug"; 0314 return; 0315 } 0316 if (msgAttach.isValid()) { 0317 QTextDocument *doc = factory->documentForAttachement(msgAttach); 0318 selectWord(index, charPos, doc); 0319 0320 AttachmentSelection selection; 0321 selection.fromCharPos = mStartPos; 0322 selection.toCharPos = mEndPos; 0323 selection.attachment = msgAttach; 0324 mAttachmentSelection.append(std::move(selection)); 0325 } 0326 } 0327 0328 void TextSelection::selectWordUnderCursor(const QModelIndex &index, int charPos, DocumentFactoryInterface *factory, const MessageUrl &msgUrl) 0329 { 0330 if (!factory) { 0331 qCWarning(RUQOLAWIDGETS_SELECTION_LOG) << " Factory is null. It's a bug"; 0332 return; 0333 } 0334 if (msgUrl.hasHtmlDescription()) { 0335 QTextDocument *doc = mMessageUrlHelperFactory->documentForUrlPreview(msgUrl); 0336 selectWord(index, charPos, doc); 0337 0338 MessageUrlSelection selection; 0339 selection.fromCharPos = mStartPos; 0340 selection.toCharPos = mEndPos; 0341 selection.messageUrl = msgUrl; 0342 mMessageUrlSelection.append(std::move(selection)); 0343 } 0344 } 0345 0346 void TextSelection::selectMessage(const QModelIndex &index) 0347 { 0348 Q_ASSERT(index.isValid()); 0349 clear(); 0350 mStartIndex = index; 0351 mEndIndex = index; 0352 mStartPos = 0; 0353 QTextDocument *doc = mTextHelperFactory ? mTextHelperFactory->documentForIndex(index) : nullptr; 0354 if (doc) { 0355 mEndPos = doc->characterCount() - 1; 0356 } 0357 const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>(); 0358 if (message) { 0359 const auto attachments = message->attachments(); 0360 for (const auto &att : attachments) { 0361 for (auto factory : std::as_const(mAttachmentFactories)) { 0362 doc = factory->documentForAttachement(att); 0363 if (doc) { 0364 AttachmentSelection selection; 0365 selection.attachment = att; 0366 selection.fromCharPos = 0; 0367 selection.toCharPos = doc->characterCount() - 1; 0368 mAttachmentSelection.append(std::move(selection)); 0369 } 0370 } 0371 } 0372 } 0373 } 0374 0375 #include "moc_textselection.cpp"