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 }