File indexing completed on 2024-11-24 04:34:19

0001 /***************************************************************************
0002  *   SPDX-License-Identifier: GPL-2.0-or-later
0003  *                                                                         *
0004  *   SPDX-FileCopyrightText: 2004-2020 Thomas Fischer <fischer@unix-ag.uni-kl.de>
0005  *                                                                         *
0006  *   This program is free software; you can redistribute it and/or modify  *
0007  *   it under the terms of the GNU General Public License as published by  *
0008  *   the Free Software Foundation; either version 2 of the License, or     *
0009  *   (at your option) any later version.                                   *
0010  *                                                                         *
0011  *   This program is distributed in the hope that it will be useful,       *
0012  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0013  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0014  *   GNU General Public License for more details.                          *
0015  *                                                                         *
0016  *   You should have received a copy of the GNU General Public License     *
0017  *   along with this program; if not, see <https://www.gnu.org/licenses/>. *
0018  ***************************************************************************/
0019 
0020 #include "clipboard.h"
0021 
0022 #include <QApplication>
0023 #include <QClipboard>
0024 #include <QMimeData>
0025 #include <QBuffer>
0026 #include <QMouseEvent>
0027 #include <QDrag>
0028 #include <QMimeType>
0029 
0030 #include <Preferences>
0031 #include <models/FileModel>
0032 #include <FileImporterBibTeX>
0033 #include <FileExporterBibTeX>
0034 #include <File>
0035 #include <FileInfo>
0036 #include "fileview.h"
0037 #include "element/associatedfilesui.h"
0038 #include "logging_gui.h"
0039 
0040 template<class T>
0041 inline QPoint eventPos(T *event) {
0042 #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
0043     return event->pos();
0044 #else
0045     return event->position().toPoint();
0046 #endif
0047 }
0048 
0049 class Clipboard::ClipboardPrivate
0050 {
0051 public:
0052     FileView *fileView;
0053     QPoint previousPosition;
0054 
0055     ClipboardPrivate(FileView *fv, Clipboard *parent)
0056             : fileView(fv) {
0057         Q_UNUSED(parent)
0058     }
0059 
0060     QString selectionToText() {
0061         FileModel *model = fileView->fileModel();
0062         if (model == nullptr) return QString();
0063 
0064         const QModelIndexList mil = fileView->selectionModel()->selectedRows();
0065         QScopedPointer<File> file(new File());
0066         for (const QModelIndex &index : mil)
0067             file->append(model->element(fileView->sortFilterProxyModel()->mapToSource(index).row()));
0068 
0069         FileExporterBibTeX exporter(fileView);
0070         exporter.setEncoding(QStringLiteral("latex"));
0071         QBuffer buffer(fileView);
0072         buffer.open(QBuffer::WriteOnly);
0073         const bool success = exporter.save(&buffer, file.data());
0074         buffer.close();
0075         if (!success)
0076             return QString();
0077 
0078         buffer.open(QBuffer::ReadOnly);
0079         const QString text = QString::fromUtf8(buffer.readAll());
0080         buffer.close();
0081 
0082         return text;
0083     }
0084 
0085     bool insertUrl(const QString &text, QSharedPointer<Entry> entry) {
0086         const QUrl url = QUrl::fromUserInput(text);
0087         return insertUrl(url, entry);
0088     }
0089 
0090     /**
0091      * Makes an attempt to insert the passed text as an URL to the given
0092      * element. May fail for various reasons, such as the text not being
0093      * a valid URL or the element being invalid.
0094      */
0095     bool insertUrl(const QUrl &url, QSharedPointer<Entry> entry) {
0096         if (entry.isNull()) return false;
0097         if (!url.isValid()) return false;
0098         FileModel *model = fileView->fileModel();
0099         if (model == nullptr) return false;
0100 
0101         return !AssociatedFilesUI::associateUrl(url, entry, model->bibliographyFile(), true, fileView).isEmpty();
0102     }
0103 
0104     /**
0105      * Given a fragment of BibTeX code, insert the elements contained in
0106      * this code into the current file
0107      * @param code BibTeX code in text form
0108      * @return true if at least one element got inserted and no error occurred
0109      */
0110     bool insertBibTeX(const QString &code) {
0111         /// Use BibTeX importer to generate representation from plain text
0112         FileImporterBibTeX importer(fileView);
0113         QScopedPointer<File> file(importer.fromString(code));
0114         if (!file.isNull() && !file->isEmpty()) {
0115             FileModel *fileModel = fileView->fileModel();
0116             QSortFilterProxyModel *sfpModel = fileView->sortFilterProxyModel();
0117 
0118             /// Insert new elements one by one
0119             const int startRow = fileModel->rowCount(); ///< Memorize row where insertion started
0120             for (const auto &element : const_cast<const File &>(*file))
0121                 fileModel->insertRow(element, fileView->model()->rowCount());
0122             const int endRow = fileModel->rowCount() - 1; ///< Memorize row where insertion ended
0123 
0124             /// Select newly inserted elements
0125             QItemSelectionModel *ism = fileView->selectionModel();
0126             ism->clear();
0127             /// Keep track of the insert element which is most upwards in the list when inserted
0128             QModelIndex minRowTargetModelIndex;
0129             /// Highlight those rows in the editor which correspond to newly inserted elements
0130             for (int i = startRow; i <= endRow; ++i) {
0131                 QModelIndex targetModelIndex = sfpModel->mapFromSource(fileModel->index(i, 0));
0132                 ism->select(targetModelIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select);
0133 
0134                 /// Update the most upward inserted element
0135                 if (!minRowTargetModelIndex.isValid() || minRowTargetModelIndex.row() > targetModelIndex.row())
0136                     minRowTargetModelIndex = targetModelIndex;
0137             }
0138             /// Scroll tree view to show top-most inserted element
0139             fileView->scrollTo(minRowTargetModelIndex, QAbstractItemView::PositionAtTop);
0140 
0141             /// Return true if at least one element was inserted
0142             if (startRow <= endRow)
0143                 return true;
0144         }
0145 
0146         return false;
0147     }
0148 
0149     bool insertText(const QString &text, QSharedPointer<Element> element) {
0150         /// Cast current element into an entry which then may be used in case an URL needs to be inserted into it
0151         QSharedPointer<Entry> entry = element.dynamicCast<Entry>();
0152         const QRegularExpressionMatch urlRegExpMatch = KBibTeX::urlRegExp.match(text);
0153         /// Quick check if passed text's beginning looks like an URL;
0154         /// in this case try to add it to the current/selected entry
0155         if (urlRegExpMatch.hasMatch() && urlRegExpMatch.capturedStart() == 0 && !entry.isNull()) {
0156             if (insertUrl(urlRegExpMatch.captured(0), entry))
0157                 return true;
0158         }
0159 
0160         /// Assumption: user dropped a piece of BibTeX code,
0161         if (insertBibTeX(text))
0162             return true;
0163 
0164         qCInfo(LOG_KBIBTEX_GUI) << "This text cannot be in inserted, looks neither like a URL nor like BibTeX code: " << text;
0165 
0166         return false;
0167     }
0168 
0169     static QSet<QUrl> urlsInMimeData(const QMimeData *mimeData, const QStringList &acceptableMimeTypes) {
0170         QSet<QUrl> result;
0171         if (mimeData->hasUrls()) {
0172             const QList<QUrl> urls = mimeData->urls();
0173             for (const QUrl &url : urls) {
0174                 if (url.isValid()) {
0175                     if (acceptableMimeTypes.isEmpty())
0176                         /// No limitations regarding which mime types are allowed?
0177                         /// Then accept any URL
0178                         result.insert(url);
0179                     const QMimeType mimeType = FileInfo::mimeTypeForUrl(url);
0180                     if (acceptableMimeTypes.contains(mimeType.name())) {
0181                         /// So, there is a list of acceptable mime types.
0182                         /// Only if URL points to a file that matches a mime type
0183                         /// from list return URL
0184                         result.insert(url);
0185                     }
0186                 }
0187             }
0188         } else if (mimeData->hasText()) {
0189             const QString text = QString::fromUtf8(mimeData->data(QStringLiteral("text/plain")));
0190             QRegularExpressionMatchIterator urlRegExpMatchIt = KBibTeX::urlRegExp.globalMatch(text);
0191             while (urlRegExpMatchIt.hasNext()) {
0192                 const QRegularExpressionMatch urlRegExpMatch = urlRegExpMatchIt.next();
0193                 const QUrl url = QUrl::fromUserInput(urlRegExpMatch.captured());
0194                 if (url.isValid()) {
0195                     if (acceptableMimeTypes.isEmpty())
0196                         /// No limitations regarding which mime types are allowed?
0197                         /// Then accept any URL
0198                         result.insert(url);
0199                     const QMimeType mimeType = FileInfo::mimeTypeForUrl(url);
0200                     if (acceptableMimeTypes.contains(mimeType.name())) {
0201                         /// So, there is a list of acceptable mime types.
0202                         /// Only if URL points to a file that matches a mime type
0203                         /// from list return URL
0204                         result.insert(url);
0205                     }
0206                 }
0207             }
0208         }
0209 
0210         /// Return all found URLs (if any)
0211         return result;
0212     }
0213 
0214     static QSet<QUrl> urlsToOpen(const QMimeData *mimeData) {
0215         static const QStringList mimeTypesThatCanBeOpened{QStringLiteral("text/x-bibtex")}; // TODO share list of mime types with file-open dialogs?
0216         return urlsInMimeData(mimeData, mimeTypesThatCanBeOpened);
0217     }
0218 
0219     static QSet<QUrl> urlsToAssociate(const QMimeData *mimeData) {
0220         /// UNUSED static const QStringList mimeTypesThatCanBeAssociated{QStringLiteral("application/pdf")}; // TODO more mime types
0221         return urlsInMimeData(mimeData, QStringList() /** accepting any MIME type */);
0222     }
0223 
0224     QSharedPointer<Entry> dropTarget(const QPoint &pos) const {
0225         /// Locate element drop was performed on
0226         const QModelIndex dropIndex = fileView->indexAt(pos);
0227         if (dropIndex.isValid())
0228             return fileView->elementAt(dropIndex).dynamicCast<Entry>();
0229         return QSharedPointer<Entry>();
0230     }
0231 
0232     enum LooksLike {LooksLikeUnknown = 0, LooksLikeBibTeX, LooksLikeURL};
0233 
0234     LooksLike looksLikeWhat(const QMimeData *mimeData) const {
0235         if (mimeData->hasText()) {
0236             QString text = QString::fromUtf8(mimeData->data(QStringLiteral("text/plain")));
0237             const int p1 = text.indexOf(QLatin1Char('@'));
0238             const int p2 = text.lastIndexOf(QLatin1Char('}'));
0239             const int p3 = text.lastIndexOf(QLatin1Char(')'));
0240             if (p1 >= 0 && (p2 >= 0 || p3 >= 0)) {
0241                 text = text.mid(p1, qMax(p2, p3) - p1 + 1);
0242                 static const QRegularExpression bibTeXElement(QStringLiteral("^@([a-z]{5,})[{()]"), QRegularExpression::CaseInsensitiveOption);
0243                 const QRegularExpressionMatch bibTeXElementMatch = bibTeXElement.match(text.left(64));
0244                 if (bibTeXElementMatch.hasMatch() && bibTeXElementMatch.captured(1) != QStringLiteral("import") /** ignore '@import' */)
0245                     return LooksLikeBibTeX;
0246             }
0247             // TODO more tests for more bibliography formats
0248             else {
0249                 const QRegularExpressionMatch urlRegExpMatch = KBibTeX::urlRegExp.match(text.left(256));
0250                 if (urlRegExpMatch.hasMatch() && urlRegExpMatch.capturedStart() == 0)
0251                     return LooksLikeURL;
0252             }
0253         } else if (mimeData->hasUrls() && !mimeData->urls().isEmpty())
0254             return LooksLikeURL;
0255 
0256         return LooksLikeUnknown;
0257     }
0258 
0259     bool acceptableDropAction(const QMimeData *mimeData, const QPoint &pos) {
0260         if (!urlsToOpen(mimeData).isEmpty())
0261             /// Data to be dropped is an URL that should be opened,
0262             /// which is a job for the shell, but not for this part
0263             /// where this Clipboard belongs to. By ignoring this event,
0264             /// it will be delegated to the underlying shell, similarly
0265             /// as if the URL would be dropped on a KBibTeX program
0266             /// window with no file open.
0267             return false;
0268         else if (looksLikeWhat(mimeData) != LooksLikeUnknown || (!dropTarget(pos).isNull() && !urlsToAssociate(mimeData).isEmpty()))
0269             /// The dropped data is either text in a known bibliography
0270             /// format or the drop happens on an entry and the dropped
0271             /// data is an URL that can be associated with this entry
0272             /// (e.g. an URL to a PDF document).
0273             return true;
0274         else
0275             return false;
0276     }
0277 };
0278 
0279 Clipboard::Clipboard(FileView *fileView)
0280         : QObject(fileView), d(new ClipboardPrivate(fileView, this))
0281 {
0282     fileView->setClipboard(this);
0283     fileView->setAcceptDrops(!fileView->isReadOnly());
0284 }
0285 
0286 Clipboard::~Clipboard()
0287 {
0288     delete d;
0289 }
0290 
0291 void Clipboard::cut()
0292 {
0293     copy();
0294     d->fileView->selectionDelete();
0295 }
0296 
0297 void Clipboard::copy()
0298 {
0299     QString text = d->selectionToText();
0300     QClipboard *clipboard = QApplication::clipboard();
0301     clipboard->setText(text);
0302 }
0303 
0304 void Clipboard::copyReferences()
0305 {
0306     FileModel *model = d->fileView != nullptr ? d->fileView->fileModel() : nullptr;
0307     if (model == nullptr) return;
0308 
0309     QStringList references;
0310     const QModelIndexList mil = d->fileView->selectionModel()->selectedRows();
0311     references.reserve(mil.size());
0312     for (const QModelIndex &index : mil) {
0313         QSharedPointer<Entry> entry = model->element(d->fileView->sortFilterProxyModel()->mapToSource(index).row()).dynamicCast<Entry>();
0314         if (!entry.isNull())
0315             references << entry->id();
0316     }
0317 
0318     if (!references.isEmpty()) {
0319         QClipboard *clipboard = QApplication::clipboard();
0320         QString text = references.join(QStringLiteral(","));
0321 
0322         const QString copyReferenceCommand = Preferences::instance().copyReferenceCommand();
0323         if (!copyReferenceCommand.isEmpty())
0324             text = QString(QStringLiteral("\\%1{%2}")).arg(copyReferenceCommand, text);
0325 
0326         clipboard->setText(text);
0327     }
0328 }
0329 
0330 void Clipboard::paste()
0331 {
0332     QClipboard *clipboard = QApplication::clipboard();
0333     const bool modified = d->insertText(clipboard->text(), d->fileView->currentElement());
0334     if (modified)
0335         d->fileView->externalModification();
0336 }
0337 
0338 
0339 void Clipboard::editorMouseEvent(QMouseEvent *event)
0340 {
0341     if (!(event->buttons() & Qt::LeftButton))
0342         return;
0343 
0344     if (d->previousPosition.x() > -1 && (eventPos(event) - d->previousPosition).manhattanLength() >= QApplication::startDragDistance()) {
0345         QString text = d->selectionToText();
0346 
0347         QDrag *drag = new QDrag(d->fileView);
0348         QMimeData *mimeData = new QMimeData();
0349         QByteArray data = text.toUtf8();
0350         mimeData->setData(QStringLiteral("text/plain"), data);
0351         drag->setMimeData(mimeData);
0352 
0353         drag->exec(Qt::CopyAction);
0354     }
0355 
0356     d->previousPosition = eventPos(event);
0357 }
0358 
0359 void Clipboard::editorDragEnterEvent(QDragEnterEvent *event)
0360 {
0361     if (d->fileView->isReadOnly())
0362         event->ignore();
0363     else if (d->acceptableDropAction(event->mimeData(), eventPos(event)))
0364         event->acceptProposedAction();
0365     else
0366         event->ignore();
0367 }
0368 
0369 void Clipboard::editorDragMoveEvent(QDragMoveEvent *event)
0370 {
0371     if (d->fileView->isReadOnly())
0372         event->ignore();
0373     else if (d->acceptableDropAction(event->mimeData(), eventPos(event)))
0374         event->acceptProposedAction();
0375     else
0376         event->ignore();
0377 }
0378 
0379 void Clipboard::editorDropEvent(QDropEvent *event)
0380 {
0381     if (d->fileView->isReadOnly()) {
0382         event->ignore();
0383         return;
0384     }
0385 
0386     bool modified = false;
0387     const ClipboardPrivate::LooksLike looksLike = d->looksLikeWhat(event->mimeData());
0388     if (looksLike == ClipboardPrivate::LooksLikeBibTeX) {
0389         /// The dropped data looks like BibTeX code
0390         modified = d->insertBibTeX(event->mimeData()->text());
0391     } else if (looksLike == ClipboardPrivate::LooksLikeURL) {
0392         /// Dropped data does not look like a known bibliography
0393         /// format.
0394         /// Check if dropped data looks like an URL (e.g. pointing
0395         /// to a PDF document) and if the drop happens onto an
0396         /// bibliography entry (and not a comment or an empty area
0397         /// in the list view, for example)
0398         const QSharedPointer<Entry> dropTarget = d->dropTarget(eventPos(event));
0399         if (!dropTarget.isNull()) {
0400             const QSet<QUrl> urls = d->urlsToAssociate(event->mimeData());
0401             for (const QUrl &urlToAssociate : urls)
0402                 modified |= d->insertUrl(urlToAssociate, dropTarget);
0403         }
0404     }
0405 
0406     if (modified)
0407         d->fileView->externalModification();
0408 }
0409 
0410 QSet<QUrl> Clipboard::urlsToOpen(const QMimeData *mimeData)
0411 {
0412     return ClipboardPrivate::urlsToOpen(mimeData);
0413 }