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"