File indexing completed on 2024-05-19 05:05:52

0001 /***************************************************************************
0002  *   SPDX-License-Identifier: GPL-2.0-or-later
0003  *                                                                         *
0004  *   SPDX-FileCopyrightText: 2004-2023 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 "openfileinfo.h"
0021 
0022 #include <QString>
0023 #include <QTimer>
0024 #include <QFileInfo>
0025 #include <QWidget>
0026 #include <QUrl>
0027 #include <QApplication>
0028 
0029 #include <KLocalizedString>
0030 #include <KConfig>
0031 #include <KConfigGroup>
0032 #include <KSharedConfig>
0033 #include <KParts/Part>
0034 #include <KParts/ReadOnlyPart>
0035 #include <KParts/ReadWritePart>
0036 #include <KParts/PartLoader>
0037 #include <kparts_version.h>
0038 
0039 #ifdef HAVE_POPPLERQT
0040 #include <FileImporterPDF>
0041 #endif // HAVE_POPPLERQT
0042 #include "logging_program.h"
0043 
0044 #if KPARTS_VERSION < QT_VERSION_CHECK(5, 100, 0)
0045 // Copied from kparts/partloader.h
0046 namespace KParts::PartLoader {
0047 template<typename T>
0048 static KPluginFactory::Result<T>
0049 instantiatePart(const KPluginMetaData &data, QWidget *parentWidget = nullptr, QObject *parent = nullptr, const QVariantList &args = {})
0050 {
0051     KPluginFactory::Result<T> result;
0052     KPluginFactory::Result<KPluginFactory> factoryResult = KPluginFactory::loadFactory(data);
0053     if (!factoryResult.plugin) {
0054         result.errorString = factoryResult.errorString;
0055         result.errorReason = factoryResult.errorReason;
0056         return result;
0057     }
0058     T *instance = factoryResult.plugin->create<T>(parentWidget, parent, args);
0059     if (!instance) {
0060         const QString fileName = data.fileName();
0061         result.errorString = QObject::tr("KPluginFactory could not load the plugin: %1").arg(fileName);
0062         result.errorText = QStringLiteral("KPluginFactory could not load the plugin: %1").arg(fileName);
0063         result.errorReason = KPluginFactory::INVALID_KPLUGINFACTORY_INSTANTIATION;
0064     } else {
0065         result.plugin = instance;
0066     }
0067     return result;
0068 }
0069 }
0070 #endif
0071 
0072 class OpenFileInfo::OpenFileInfoPrivate
0073 {
0074 private:
0075     static int globalCounter;
0076     int m_counter;
0077 
0078 public:
0079     static const QString keyLastAccess;
0080     static const QString keyURL;
0081     static const QString dateTimeFormat;
0082 
0083     OpenFileInfo *p;
0084 
0085     KParts::ReadOnlyPart *part;
0086     KPluginMetaData currentPart;
0087     QWidget *internalWidgetParent;
0088     QDateTime lastAccessDateTime;
0089     StatusFlags flags;
0090     OpenFileInfoManager *openFileInfoManager;
0091     QString mimeType;
0092     QUrl url;
0093 
0094     OpenFileInfoPrivate(OpenFileInfoManager *openFileInfoManager, const QUrl &url, const QString &mimeType, OpenFileInfo *p)
0095         :  m_counter(-1), p(p), part(nullptr), internalWidgetParent(nullptr) {
0096         this->openFileInfoManager = openFileInfoManager;
0097         this->url = url;
0098         if (this->url.isValid() && this->url.scheme().isEmpty())
0099             qCWarning(LOG_KBIBTEX_PROGRAM) << "No scheme specified for URL" << this->url.toDisplayString();
0100         this->mimeType = mimeType;
0101     }
0102 
0103     ~OpenFileInfoPrivate() {
0104         if (part != nullptr) {
0105             KParts::ReadWritePart *rwp = qobject_cast<KParts::ReadWritePart *>(part);
0106             if (rwp != nullptr)
0107                 rwp->closeUrl(true);
0108             delete part;
0109         }
0110     }
0111 
0112     KParts::ReadOnlyPart *createPart(QWidget *newWidgetParent, const KPluginMetaData &_partMetaData = {}) {
0113 
0114         KPluginMetaData partMetaData = _partMetaData;
0115 
0116         if (!p->flags().testFlag(OpenFileInfo::StatusFlag::Open)) {
0117             qCWarning(LOG_KBIBTEX_PROGRAM) << "Cannot create part for a file which is not open";
0118             return nullptr;
0119         }
0120 
0121         Q_ASSERT_X(internalWidgetParent == nullptr || internalWidgetParent == newWidgetParent, "KParts::ReadOnlyPart *OpenFileInfo::OpenFileInfoPrivate::createPart(QWidget *newWidgetParent, const KPluginMetaData & = {})", "internal widget should be either NULL or the same one as supplied as \"newWidgetParent\"");
0122 
0123         /** use cached part for this parent if possible */
0124         if (internalWidgetParent == newWidgetParent && (!partMetaData.isValid() || currentPart == partMetaData)) {
0125             Q_ASSERT_X(part != nullptr, "KParts::ReadOnlyPart *OpenFileInfo::OpenFileInfoPrivate::createPart(QWidget *newWidgetParent, const KPluginMetaData & = {})", "Part is NULL");
0126             return part;
0127         } else if (part != nullptr) {
0128             KParts::ReadWritePart *rwp = qobject_cast<KParts::ReadWritePart *>(part);
0129             if (rwp != nullptr)
0130                 rwp->closeUrl(true);
0131             part->deleteLater();
0132             part = nullptr;
0133         }
0134 
0135         /// reset to invalid values in case something goes wrong
0136         currentPart = {};
0137         internalWidgetParent = nullptr;
0138 
0139         if (!partMetaData.isValid()) {
0140             /// no valid KPartMetaData has been passed
0141             /// try to find a read-write part to open file
0142             partMetaData = p->defaultService();
0143         }
0144         if (!partMetaData.isValid()) {
0145             qCDebug(LOG_KBIBTEX_PROGRAM) << "PATH=" << getenv("PATH");
0146             qCDebug(LOG_KBIBTEX_PROGRAM) << "LD_LIBRARY_PATH=" << getenv("LD_LIBRARY_PATH");
0147             qCDebug(LOG_KBIBTEX_PROGRAM) << "XDG_DATA_DIRS=" << getenv("XDG_DATA_DIRS");
0148             qCDebug(LOG_KBIBTEX_PROGRAM) << "QT_PLUGIN_PATH=" << getenv("QT_PLUGIN_PATH");
0149             qCDebug(LOG_KBIBTEX_PROGRAM) << "KDEDIRS=" << getenv("KDEDIRS");
0150             qCCritical(LOG_KBIBTEX_PROGRAM) << "Cannot find part to handle mimetype " << mimeType;
0151             return nullptr;
0152         }
0153 
0154         QString errorString;
0155 
0156         const auto loadResult = KParts::PartLoader::instantiatePart<KParts::ReadWritePart>(partMetaData, newWidgetParent, qobject_cast<QObject *>(newWidgetParent));
0157 
0158         errorString = loadResult.errorString;
0159         part = loadResult.plugin;
0160 
0161         if (part == nullptr) {
0162             qCDebug(LOG_KBIBTEX_PROGRAM) << "name:" << partMetaData.name();
0163             qCDebug(LOG_KBIBTEX_PROGRAM) << "version:" << partMetaData.version();
0164             qCDebug(LOG_KBIBTEX_PROGRAM) << "category:" << partMetaData.category();
0165             qCDebug(LOG_KBIBTEX_PROGRAM) << "description:" << partMetaData.description();
0166             qCDebug(LOG_KBIBTEX_PROGRAM) << "fileName:" << partMetaData.fileName();
0167             qCDebug(LOG_KBIBTEX_PROGRAM) << "PATH=" << getenv("PATH");
0168             qCDebug(LOG_KBIBTEX_PROGRAM) << "LD_LIBRARY_PATH=" << getenv("LD_LIBRARY_PATH");
0169             qCDebug(LOG_KBIBTEX_PROGRAM) << "XDG_DATA_DIRS=" << getenv("XDG_DATA_DIRS");
0170             qCDebug(LOG_KBIBTEX_PROGRAM) << "QT_PLUGIN_PATH=" << getenv("QT_PLUGIN_PATH");
0171             qCDebug(LOG_KBIBTEX_PROGRAM) << "KDEDIRS=" << getenv("KDEDIRS");
0172             qCWarning(LOG_KBIBTEX_PROGRAM) << "Could not instantiate read-write part for" << partMetaData.name() << "(mimeType=" << mimeType << ", library=" << partMetaData.fileName() << ", error msg=" << errorString << ")";
0173             /// creating a read-write part failed, so maybe it is read-only (like Okular's PDF viewer)?
0174             const auto loadResult = KParts::PartLoader::instantiatePart<KParts::ReadOnlyPart>(partMetaData, newWidgetParent, qobject_cast<QObject *>(newWidgetParent));
0175             errorString = loadResult.errorString;
0176             part = loadResult.plugin;
0177         }
0178         if (part == nullptr) {
0179             /// still cannot create part, must be error
0180             qCCritical(LOG_KBIBTEX_PROGRAM) << "Could not instantiate part for" << partMetaData.name() << "(mimeType=" << mimeType << ", library=" << partMetaData.fileName() << ", error msg=" << errorString << ")";
0181             return nullptr;
0182         }
0183 
0184         if (url.isValid()) {
0185             /// open URL in part
0186             part->openUrl(url);
0187             /// update document list widget accordingly
0188             p->addFlags(OpenFileInfo::StatusFlag::RecentlyUsed);
0189             p->addFlags(OpenFileInfo::StatusFlag::HasName);
0190         } else {
0191             /// initialize part with empty document
0192             part->openUrl(QUrl());
0193         }
0194         p->addFlags(OpenFileInfo::StatusFlag::Open);
0195 
0196         currentPart = partMetaData;
0197         internalWidgetParent = newWidgetParent;
0198 
0199         Q_ASSERT_X(part != nullptr, "KParts::ReadOnlyPart *OpenFileInfo::OpenFileInfoPrivate::createPart(QWidget *newWidgetParent, const KPluginMetaData & = {})", "Creation of part failed, is NULL"); /// test should not be necessary, but just to be save ...
0200         return part;
0201     }
0202 
0203     int counter() {
0204         if (!url.isValid() && m_counter < 0)
0205             m_counter = ++globalCounter;
0206         else if (url.isValid())
0207             qCWarning(LOG_KBIBTEX_PROGRAM) << "This function should not be called if URL is valid";
0208         return m_counter;
0209     }
0210 
0211 };
0212 
0213 int OpenFileInfo::OpenFileInfoPrivate::globalCounter = 0;
0214 const QString OpenFileInfo::OpenFileInfoPrivate::dateTimeFormat = QStringLiteral("yyyy-MM-dd-hh-mm-ss-zzz");
0215 const QString OpenFileInfo::OpenFileInfoPrivate::keyLastAccess = QStringLiteral("LastAccess");
0216 const QString OpenFileInfo::OpenFileInfoPrivate::keyURL = QStringLiteral("URL");
0217 
0218 OpenFileInfo::OpenFileInfo(OpenFileInfoManager *openFileInfoManager, const QUrl &url)
0219         : d(new OpenFileInfoPrivate(openFileInfoManager, url, FileInfo::mimeTypeForUrl(url).name(), this))
0220 {
0221     /// nothing
0222 }
0223 
0224 OpenFileInfo::OpenFileInfo(OpenFileInfoManager *openFileInfoManager, const QString &mimeType)
0225         : d(new OpenFileInfoPrivate(openFileInfoManager, QUrl(), mimeType, this))
0226 {
0227     /// nothing
0228 }
0229 
0230 OpenFileInfo::~OpenFileInfo()
0231 {
0232     delete d;
0233 }
0234 
0235 void OpenFileInfo::setUrl(const QUrl &url)
0236 {
0237     Q_ASSERT_X(url.isValid(), "void OpenFileInfo::setUrl(const QUrl&)", "URL is not valid");
0238     d->url = url;
0239     if (d->url.scheme().isEmpty())
0240         qCWarning(LOG_KBIBTEX_PROGRAM) << "No scheme specified for URL" << d->url.toDisplayString();
0241     d->mimeType = FileInfo::mimeTypeForUrl(url).name();
0242     addFlags(OpenFileInfo::StatusFlag::HasName);
0243 }
0244 
0245 QUrl OpenFileInfo::url() const
0246 {
0247     return d->url;
0248 }
0249 
0250 bool OpenFileInfo::isModified() const
0251 {
0252     KParts::ReadWritePart *rwPart = qobject_cast< KParts::ReadWritePart *>(d->part);
0253     if (rwPart == nullptr)
0254         return false;
0255     else
0256         return rwPart->isModified();
0257 }
0258 
0259 bool OpenFileInfo::save()
0260 {
0261     KParts::ReadWritePart *rwPart = qobject_cast< KParts::ReadWritePart *>(d->part);
0262     if (rwPart == nullptr)
0263         return true;
0264     else
0265         return rwPart->save();
0266 }
0267 
0268 bool OpenFileInfo::close()
0269 {
0270     if (d->part == nullptr) {
0271         /// if there is no part, closing always "succeeds"
0272         return true;
0273     }
0274 
0275     KParts::ReadWritePart *rwp = qobject_cast<KParts::ReadWritePart *>(d->part);
0276     if (rwp == nullptr || rwp->closeUrl(true)) {
0277         d->part->deleteLater();
0278         d->part = nullptr;
0279         d->internalWidgetParent = nullptr;
0280         return true;
0281     }
0282     return false;
0283 }
0284 
0285 QString OpenFileInfo::mimeType() const
0286 {
0287     return d->mimeType;
0288 }
0289 
0290 QString OpenFileInfo::shortCaption() const
0291 {
0292     if (d->url.isValid())
0293         return d->url.fileName();
0294     else
0295         return i18n("Unnamed-%1", d->counter());
0296 }
0297 
0298 QString OpenFileInfo::fullCaption() const
0299 {
0300     if (d->url.isValid())
0301         return d->url.url(QUrl::PreferLocalFile);
0302     else
0303         return shortCaption();
0304 }
0305 
0306 KParts::ReadOnlyPart *OpenFileInfo::part(QWidget *parent, const KPluginMetaData &service)
0307 {
0308     return d->createPart(parent, service);
0309 }
0310 
0311 OpenFileInfo::StatusFlags OpenFileInfo::flags() const
0312 {
0313     return d->flags;
0314 }
0315 
0316 void OpenFileInfo::setFlags(StatusFlags statusFlags)
0317 {
0318     /// disallow files without name or valid url to become favorites
0319     if (!d->url.isValid() || !d->flags.testFlag(StatusFlag::HasName)) statusFlags &= ~static_cast<int>(StatusFlag::Favorite);
0320     /// files that got opened are by definition recently used files
0321     if (!d->url.isValid() && d->flags.testFlag(StatusFlag::Open)) statusFlags &= StatusFlag::RecentlyUsed;
0322 
0323     bool hasChanged = d->flags != statusFlags;
0324     d->flags = statusFlags;
0325     if (hasChanged)
0326         Q_EMIT flagsChanged(statusFlags);
0327 }
0328 
0329 void OpenFileInfo::addFlags(StatusFlags statusFlags)
0330 {
0331     /// disallow files without name or valid url to become favorites
0332     if (!d->url.isValid() || !d->flags.testFlag(StatusFlag::HasName)) statusFlags &= ~static_cast<int>(StatusFlag::Favorite);
0333 
0334     bool hasChanged = (~d->flags & statusFlags) > 0;
0335     d->flags |= statusFlags;
0336     if (hasChanged)
0337         Q_EMIT flagsChanged(statusFlags);
0338 }
0339 
0340 void OpenFileInfo::removeFlags(StatusFlags statusFlags)
0341 {
0342     bool hasChanged = (d->flags & statusFlags) > 0;
0343     d->flags &= ~statusFlags;
0344     if (hasChanged)
0345         Q_EMIT flagsChanged(statusFlags);
0346 }
0347 
0348 QDateTime OpenFileInfo::lastAccess() const
0349 {
0350     return d->lastAccessDateTime;
0351 }
0352 
0353 void OpenFileInfo::setLastAccess(const QDateTime &dateTime)
0354 {
0355     d->lastAccessDateTime = dateTime;
0356     Q_EMIT flagsChanged(OpenFileInfo::StatusFlag::RecentlyUsed);
0357 }
0358 
0359 QVector<KPluginMetaData> OpenFileInfo::listOfServices()
0360 {
0361     const QString mt = mimeType();
0362 
0363     QVector<KPluginMetaData> result = KParts::PartLoader::partsForMimeType(mt);
0364 
0365     // Always include KBibTeX's KPart in list of services:
0366     // First, check if KBibTeX's KPart is already in list
0367     auto it = std::find_if(result.cbegin(), result.cend(), [](const KPluginMetaData &md){
0368         return md.pluginId() == QStringLiteral("kbibtexpart");
0369     });
0370     // If not, insert it
0371     if (it == result.cend()) {
0372 #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
0373         auto kbibtexpart {KPluginMetaData(QStringLiteral("kbibtexpart"))};
0374 #else // (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
0375         auto kbibtexpart {KPluginMetaData::findPluginById(QStringLiteral("kf6/parts"), QStringLiteral("kbibtexpart"))};
0376 #endif // (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
0377         if (kbibtexpart.isValid())
0378             result << kbibtexpart;
0379     }
0380 
0381     return result;
0382 }
0383 
0384 KPluginMetaData OpenFileInfo::defaultService()
0385 {
0386     const QString mt = mimeType();
0387 
0388     KPluginMetaData result;
0389     static const QSet<QString> supportedMimeTypes{
0390         QStringLiteral("text/x-bibtex"),
0391 #ifdef HAVE_POPPLERQT5
0392         QStringLiteral("application/pdf"),
0393 #endif // HAVE_POPPLERQT5
0394         QStringLiteral("application/x-endnote-refer"), QStringLiteral("application/x-isi-export-format"), QStringLiteral("text/x-research-info-systems"),
0395         QStringLiteral("application/xml")
0396     };
0397     if (supportedMimeTypes.contains(mt)) {
0398         // If the mime type is natively supported by KBibTeX's part, enforce using this part
0399 #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
0400         result = KPluginMetaData(QStringLiteral("kbibtexpart"));
0401 #else // (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
0402         result = KPluginMetaData::findPluginById(QStringLiteral("kf6/parts"), QStringLiteral("kbibtexpart"));
0403 #endif // (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
0404     }
0405 
0406     if (!result.isValid()) {
0407         // If above test for supported mime types failed
0408         // or no valid KPluginMetaData could be instanciated for 'kbibtexpart',
0409         // pick any of the other available parts supporting the requested mime type
0410 
0411         const QVector<KPluginMetaData> parts = listOfServices();
0412         if (!parts.isEmpty())
0413             result = parts.first();
0414     }
0415 
0416     if (result.isValid())
0417         qCDebug(LOG_KBIBTEX_PROGRAM) << "Using" << result.name() << "(" << result.description() << ") for mime type" << mt << "through library" << result.fileName();
0418     else
0419         qCWarning(LOG_KBIBTEX_PROGRAM) << "Could not find part for mime type" << mt;
0420     return result;
0421 }
0422 
0423 KPluginMetaData OpenFileInfo::currentService()
0424 {
0425     return d->currentPart;
0426 }
0427 
0428 class OpenFileInfoManager::OpenFileInfoManagerPrivate
0429 {
0430 private:
0431     static const QString configGroupNameRecentlyUsed;
0432     static const QString configGroupNameFavorites;
0433     static const QString configGroupNameOpen;
0434     static const int maxNumRecentlyUsedFiles, maxNumFavoriteFiles, maxNumOpenFiles;
0435 
0436 public:
0437     OpenFileInfoManager *p;
0438 
0439     OpenFileInfoManager::OpenFileInfoList openFileInfoList;
0440     OpenFileInfo *currentFileInfo;
0441 
0442     OpenFileInfoManagerPrivate(OpenFileInfoManager *parent)
0443             : p(parent), currentFileInfo(nullptr) {
0444         /// nothing
0445     }
0446 
0447     ~OpenFileInfoManagerPrivate() {
0448         for (OpenFileInfoManager::OpenFileInfoList::Iterator it = openFileInfoList.begin(); it != openFileInfoList.end();) {
0449             OpenFileInfo *ofi = *it;
0450             delete ofi;
0451             it = openFileInfoList.erase(it);
0452         }
0453     }
0454 
0455     static bool byNameLessThan(const OpenFileInfo *left, const OpenFileInfo *right) {
0456         return left->shortCaption() < right->shortCaption();
0457     }
0458 
0459     static  bool byLRULessThan(const OpenFileInfo *left, const OpenFileInfo *right) {
0460         return left->lastAccess() > right->lastAccess(); /// reverse sorting!
0461     }
0462 
0463     void readConfig(QUrl &recentlyOpenURL) {
0464         readConfig(OpenFileInfo::StatusFlag::RecentlyUsed, configGroupNameRecentlyUsed, maxNumRecentlyUsedFiles);
0465         readConfig(OpenFileInfo::StatusFlag::Favorite, configGroupNameFavorites, maxNumFavoriteFiles);
0466         readConfig(OpenFileInfo::StatusFlag::Open, configGroupNameOpen, maxNumOpenFiles);
0467 
0468         KSharedConfigPtr config = KSharedConfig::openConfig(QStringLiteral("kbibtexrc"));
0469         KConfigGroup cg(config, configGroupNameOpen);
0470         static const QString recentlyOpenURLkey = OpenFileInfo::OpenFileInfoPrivate::keyURL + QStringLiteral("-current");
0471         recentlyOpenURL = cg.hasKey(recentlyOpenURLkey) ? QUrl(cg.readEntry(recentlyOpenURLkey)) : QUrl();
0472     }
0473 
0474     void writeConfig() {
0475         writeConfig(OpenFileInfo::StatusFlag::RecentlyUsed, configGroupNameRecentlyUsed, maxNumRecentlyUsedFiles);
0476         writeConfig(OpenFileInfo::StatusFlag::Favorite, configGroupNameFavorites, maxNumFavoriteFiles);
0477         writeConfig(OpenFileInfo::StatusFlag::Open, configGroupNameOpen, maxNumOpenFiles);
0478 
0479         KSharedConfigPtr config = KSharedConfig::openConfig(QStringLiteral("kbibtexrc"));
0480         KConfigGroup cg(config, configGroupNameOpen);
0481         static const QString recentlyOpenURLkey = OpenFileInfo::OpenFileInfoPrivate::keyURL + QStringLiteral("-current");
0482         if (currentFileInfo != nullptr && currentFileInfo->url().isValid())
0483             /// If there is a currently active and open file, remember that file's URL
0484             cg.writeEntry(recentlyOpenURLkey, currentFileInfo->url().url(QUrl::PreferLocalFile));
0485         else
0486             cg.deleteEntry(recentlyOpenURLkey);
0487     }
0488 
0489     void readConfig(OpenFileInfo::StatusFlag statusFlag, const QString &configGroupName, int maxNumFiles) {
0490         KSharedConfigPtr config = KSharedConfig::openConfig(QStringLiteral("kbibtexrc"));
0491 
0492         KConfigGroup cg(config, configGroupName);
0493         for (int i = 0; i < maxNumFiles; ++i) {
0494             QUrl fileUrl = QUrl(cg.readEntry(QString(QStringLiteral("%1-%2")).arg(OpenFileInfo::OpenFileInfoPrivate::keyURL).arg(i), ""));
0495             if (!fileUrl.isValid()) break;
0496             if (fileUrl.scheme().isEmpty())
0497                 fileUrl.setScheme(QStringLiteral("file"));
0498 
0499             /// For local files, test if they exist; ignore local files that do not exist
0500             if (fileUrl.isLocalFile()) {
0501                 if (!QFileInfo::exists(fileUrl.toLocalFile()))
0502                     continue;
0503             }
0504 
0505             OpenFileInfo *ofi = p->contains(fileUrl);
0506             if (ofi == nullptr) {
0507                 ofi = p->open(fileUrl);
0508             }
0509             ofi->addFlags(statusFlag);
0510             ofi->addFlags(OpenFileInfo::StatusFlag::HasName);
0511             ofi->setLastAccess(QDateTime::fromString(cg.readEntry(QString(QStringLiteral("%1-%2")).arg(OpenFileInfo::OpenFileInfoPrivate::keyLastAccess).arg(i), ""), OpenFileInfo::OpenFileInfoPrivate::dateTimeFormat));
0512         }
0513     }
0514 
0515     void writeConfig(OpenFileInfo::StatusFlag statusFlag, const QString &configGroupName, int maxNumFiles) {
0516         KSharedConfigPtr config = KSharedConfig::openConfig(QStringLiteral("kbibtexrc"));
0517         KConfigGroup cg(config, configGroupName);
0518         OpenFileInfoManager::OpenFileInfoList list = p->filteredItems(statusFlag);
0519 
0520         int i = 0;
0521         for (OpenFileInfoManager::OpenFileInfoList::ConstIterator it = list.constBegin(); i < maxNumFiles && it != list.constEnd(); ++it, ++i) {
0522             OpenFileInfo *ofi = *it;
0523 
0524             cg.writeEntry(QString(QStringLiteral("%1-%2")).arg(OpenFileInfo::OpenFileInfoPrivate::keyURL).arg(i), ofi->url().url(QUrl::PreferLocalFile));
0525             cg.writeEntry(QString(QStringLiteral("%1-%2")).arg(OpenFileInfo::OpenFileInfoPrivate::keyLastAccess).arg(i), ofi->lastAccess().toString(OpenFileInfo::OpenFileInfoPrivate::dateTimeFormat));
0526         }
0527         for (; i < maxNumFiles; ++i) {
0528             cg.deleteEntry(QString(QStringLiteral("%1-%2")).arg(OpenFileInfo::OpenFileInfoPrivate::keyURL).arg(i));
0529             cg.deleteEntry(QString(QStringLiteral("%1-%2")).arg(OpenFileInfo::OpenFileInfoPrivate::keyLastAccess).arg(i));
0530         }
0531         config->sync();
0532     }
0533 };
0534 
0535 const QString OpenFileInfoManager::OpenFileInfoManagerPrivate::configGroupNameRecentlyUsed = QStringLiteral("DocumentList-RecentlyUsed");
0536 const QString OpenFileInfoManager::OpenFileInfoManagerPrivate::configGroupNameFavorites = QStringLiteral("DocumentList-Favorites");
0537 const QString OpenFileInfoManager::OpenFileInfoManagerPrivate::configGroupNameOpen = QStringLiteral("DocumentList-Open");
0538 const int OpenFileInfoManager::OpenFileInfoManagerPrivate::maxNumFavoriteFiles = 256;
0539 const int OpenFileInfoManager::OpenFileInfoManagerPrivate::maxNumRecentlyUsedFiles = 8;
0540 const int OpenFileInfoManager::OpenFileInfoManagerPrivate::maxNumOpenFiles = 16;
0541 
0542 OpenFileInfoManager::OpenFileInfoManager(QObject *parent)
0543         : QObject(parent), d(new OpenFileInfoManagerPrivate(this))
0544 {
0545     QUrl recentlyOpenURL;
0546     d->readConfig(recentlyOpenURL);
0547 
0548     QTimer::singleShot(250, this, [this, recentlyOpenURL]() {
0549         /// In case there is at least one file marked as 'open' but it is not yet actually open,
0550         /// set it as current file now. The file to be opened (identified by URL) should be
0551         /// preferably the file that was actively open at the end of last KBibTeX session.
0552         /// Slightly delaying the actually opening of files is necessary to give precendence
0553         /// to bibliography files passed as command line arguments (see program.cpp) over files
0554         /// that where open when the previous KBibTeX session was quit.
0555         /// See KDE bug 417164.
0556         /// TODO still necessary to refactor the whole 'file opening' control flow to avoid hacks like this one
0557         if (d->currentFileInfo == nullptr) {
0558             OpenFileInfo *ofiToUse = nullptr;
0559             /// Go over the list of files that are flagged as 'open' ...
0560             for (auto *ofi : const_cast<const OpenFileInfoManager::OpenFileInfoList &>(d->openFileInfoList))
0561                 if (ofi->flags().testFlag(OpenFileInfo::StatusFlag::Open) && (ofiToUse == nullptr || (recentlyOpenURL.isValid() && ofi->url() == recentlyOpenURL)))
0562                     ofiToUse = ofi;
0563             if (ofiToUse != nullptr)
0564                 setCurrentFile(ofiToUse);
0565         }
0566     });
0567 }
0568 
0569 OpenFileInfoManager &OpenFileInfoManager::instance() {
0570     /// Allocate this singleton on heap not stack like most other singletons
0571     /// Supposedly, QCoreApplication will clean this singleton at application's end
0572     static OpenFileInfoManager *singleton = new OpenFileInfoManager(QCoreApplication::instance());
0573     return *singleton;
0574 }
0575 
0576 OpenFileInfoManager::~OpenFileInfoManager()
0577 {
0578     delete d;
0579 }
0580 
0581 OpenFileInfo *OpenFileInfoManager::createNew(const QString &mimeType)
0582 {
0583     OpenFileInfo *result = new OpenFileInfo(this, mimeType);
0584     connect(result, &OpenFileInfo::flagsChanged, this, &OpenFileInfoManager::flagsChanged);
0585     d->openFileInfoList << result;
0586     result->setLastAccess();
0587     return result;
0588 }
0589 
0590 OpenFileInfo *OpenFileInfoManager::open(const QUrl &url)
0591 {
0592     Q_ASSERT_X(url.isValid(), "OpenFileInfo *OpenFileInfoManager::open(const QUrl&)", "URL is not valid");
0593 
0594     OpenFileInfo *result = contains(url);
0595     if (result == nullptr) {
0596         /// file not yet open
0597         result = new OpenFileInfo(this, url);
0598         connect(result, &OpenFileInfo::flagsChanged, this, &OpenFileInfoManager::flagsChanged);
0599         d->openFileInfoList << result;
0600     } /// else: file was already open, re-use and return existing OpenFileInfo pointer
0601     result->setLastAccess();
0602     return result;
0603 }
0604 
0605 OpenFileInfo *OpenFileInfoManager::contains(const QUrl &url) const
0606 {
0607     if (!url.isValid()) return nullptr; /// can only be unnamed file
0608 
0609     for (auto *ofi : const_cast<const OpenFileInfoManager::OpenFileInfoList &>(d->openFileInfoList)) {
0610         if (ofi->url() == url)
0611             return ofi;
0612     }
0613     return nullptr;
0614 }
0615 
0616 bool OpenFileInfoManager::changeUrl(OpenFileInfo *openFileInfo, const QUrl &url)
0617 {
0618     OpenFileInfo *previouslyContained = contains(url);
0619 
0620     /// check if old url differs from new url and old url is valid
0621     if (previouslyContained != nullptr && previouslyContained->flags().testFlag(OpenFileInfo::StatusFlag::Open) && previouslyContained != openFileInfo) {
0622         qCWarning(LOG_KBIBTEX_PROGRAM) << "Open file with same URL already exists, forcefully closing it";
0623         close(previouslyContained);
0624     }
0625 
0626     QUrl oldUrl = openFileInfo->url();
0627     openFileInfo->setUrl(url);
0628 
0629     if (url != oldUrl && oldUrl.isValid()) {
0630         /// current document was most probabily renamed (e.g. due to "Save As")
0631         /// add old URL to recently used files, but exclude the open files list
0632         OpenFileInfo *ofi = open(oldUrl); // krazy:exclude=syscalls
0633         OpenFileInfo::StatusFlags statusFlags = (openFileInfo->flags() & ~static_cast<int>(OpenFileInfo::StatusFlag::Open)) | OpenFileInfo::StatusFlag::RecentlyUsed;
0634         ofi->setFlags(statusFlags);
0635     }
0636     if (previouslyContained != nullptr) {
0637         /// keep Favorite flag if set in file that have previously same URL
0638         if (previouslyContained->flags().testFlag(OpenFileInfo::StatusFlag::Favorite))
0639             openFileInfo->setFlags(openFileInfo->flags() | OpenFileInfo::StatusFlag::Favorite);
0640 
0641         /// remove the old entry with the same url has it will be replaced by the new one
0642         d->openFileInfoList.remove(d->openFileInfoList.indexOf(previouslyContained));
0643         previouslyContained->deleteLater();
0644         OpenFileInfo::StatusFlags statusFlags = OpenFileInfo::StatusFlag::Open;
0645         statusFlags |= OpenFileInfo::StatusFlag::RecentlyUsed;
0646         statusFlags |= OpenFileInfo::StatusFlag::Favorite;
0647         Q_EMIT flagsChanged(statusFlags);
0648     }
0649 
0650     if (openFileInfo == d->currentFileInfo)
0651         Q_EMIT currentChanged(openFileInfo, {});
0652     Q_EMIT flagsChanged(openFileInfo->flags());
0653 
0654     return true;
0655 }
0656 
0657 bool OpenFileInfoManager::close(OpenFileInfo *openFileInfo)
0658 {
0659     if (openFileInfo == nullptr) {
0660         qCWarning(LOG_KBIBTEX_PROGRAM) << "void OpenFileInfoManager::close(OpenFileInfo *openFileInfo): Cannot close openFileInfo which is NULL";
0661         return false;
0662     }
0663 
0664     bool isClosing = false;
0665     openFileInfo->setLastAccess();
0666 
0667     /// remove flag "open" from file to be closed and determine which file to show instead
0668     OpenFileInfo *nextCurrent = (d->currentFileInfo == openFileInfo) ? nullptr : d->currentFileInfo;
0669     for (OpenFileInfo *ofi : const_cast<const OpenFileInfoManager::OpenFileInfoList &>(d->openFileInfoList)) {
0670         if (!isClosing && ofi == openFileInfo && openFileInfo->close()) {
0671             isClosing = true;
0672             /// Mark file as closed (i.e. not open)
0673             openFileInfo->removeFlags(OpenFileInfo::StatusFlag::Open);
0674             /// If file has a filename, remember as recently used
0675             if (openFileInfo->flags().testFlag(OpenFileInfo::StatusFlag::HasName))
0676                 openFileInfo->addFlags(OpenFileInfo::StatusFlag::RecentlyUsed);
0677         } else if (nextCurrent == nullptr && ofi->flags().testFlag(OpenFileInfo::StatusFlag::Open))
0678             nextCurrent = ofi;
0679     }
0680 
0681     /// If the current document is to be closed,
0682     /// switch over to the next available one
0683     if (isClosing)
0684         setCurrentFile(nextCurrent);
0685 
0686     return isClosing;
0687 }
0688 
0689 bool OpenFileInfoManager::queryCloseAll()
0690 {
0691     /// Assume that all closing operations succeed
0692     bool isClosing = true;
0693     /// For keeping track of files that get closed here
0694     OpenFileInfoList restoreLaterList;
0695 
0696     /// For each file known ...
0697     for (OpenFileInfo *openFileInfo : const_cast<const OpenFileInfoManager::OpenFileInfoList &>(d->openFileInfoList)) {
0698         /// Check only open file (ignore recently used, favorites, ...)
0699         if (openFileInfo->flags().testFlag(OpenFileInfo::StatusFlag::Open)) {
0700             if (openFileInfo->close()) {
0701                 /// If file could be closed without user canceling the operation ...
0702                 /// Mark file as closed (i.e. not open)
0703                 openFileInfo->removeFlags(OpenFileInfo::StatusFlag::Open);
0704                 /// If file has a filename, remember as recently used
0705                 if (openFileInfo->flags().testFlag(OpenFileInfo::StatusFlag::HasName))
0706                     openFileInfo->addFlags(OpenFileInfo::StatusFlag::RecentlyUsed);
0707                 /// Remember file as to be marked as open later
0708                 restoreLaterList.append(openFileInfo);
0709             } else {
0710                 /// User chose to cancel closing operation,
0711                 /// stop everything here
0712                 isClosing = false;
0713                 break;
0714             }
0715         }
0716     }
0717 
0718     if (isClosing) {
0719         /// Closing operation was not cancelled, therefore mark
0720         /// all files that were open before as open now.
0721         /// This makes the files to be reopened when KBibTeX is
0722         /// restarted again (assuming that this function was
0723         /// called when KBibTeX is exiting).
0724         for (OpenFileInfo *openFileInfo : const_cast<const OpenFileInfoManager::OpenFileInfoList &>(restoreLaterList)) {
0725             openFileInfo->addFlags(OpenFileInfo::StatusFlag::Open);
0726         }
0727 
0728         d->writeConfig();
0729     }
0730 
0731     return isClosing;
0732 }
0733 
0734 OpenFileInfo *OpenFileInfoManager::currentFile() const
0735 {
0736     return d->currentFileInfo;
0737 }
0738 
0739 void OpenFileInfoManager::setCurrentFile(OpenFileInfo *openFileInfo, const KPluginMetaData &service)
0740 {
0741     bool hasChanged = d->currentFileInfo != openFileInfo;
0742     OpenFileInfo *previous = d->currentFileInfo;
0743     d->currentFileInfo = openFileInfo;
0744 
0745     if (d->currentFileInfo != nullptr) {
0746         d->currentFileInfo->addFlags(OpenFileInfo::StatusFlag::Open);
0747         d->currentFileInfo->setLastAccess();
0748     }
0749     if (hasChanged) {
0750         if (previous != nullptr) previous->setLastAccess();
0751         Q_EMIT currentChanged(openFileInfo, service);
0752     } else if (openFileInfo != nullptr && service.pluginId() != openFileInfo->currentService().pluginId())
0753         Q_EMIT currentChanged(openFileInfo, service);
0754 }
0755 
0756 OpenFileInfoManager::OpenFileInfoList OpenFileInfoManager::filteredItems(OpenFileInfo::StatusFlag required, OpenFileInfo::StatusFlags forbidden)
0757 {
0758     OpenFileInfoList result;
0759 
0760     for (OpenFileInfoList::Iterator it = d->openFileInfoList.begin(); it != d->openFileInfoList.end(); ++it) {
0761         OpenFileInfo *ofi = *it;
0762         if (ofi->flags().testFlag(required) && (ofi->flags() & forbidden) == 0)
0763             result << ofi;
0764     }
0765 
0766     if (required == OpenFileInfo::StatusFlag::RecentlyUsed)
0767         std::sort(result.begin(), result.end(), OpenFileInfoManagerPrivate::byLRULessThan);
0768     else if (required == OpenFileInfo::StatusFlag::Favorite || required == OpenFileInfo::StatusFlag::Open)
0769         std::sort(result.begin(), result.end(), OpenFileInfoManagerPrivate::byNameLessThan);
0770 
0771     return result;
0772 }
0773 
0774 void OpenFileInfoManager::deferredListsChanged()
0775 {
0776     OpenFileInfo::StatusFlags statusFlags = OpenFileInfo::StatusFlag::Open;
0777     statusFlags |= OpenFileInfo::StatusFlag::RecentlyUsed;
0778     statusFlags |= OpenFileInfo::StatusFlag::Favorite;
0779     Q_EMIT flagsChanged(statusFlags);
0780 }