File indexing completed on 2024-05-12 04:19:37

0001 /*
0002 Gwenview: an image viewer
0003 Copyright 2007 Aurélien Gâteau <agateau@kde.org>
0004 
0005 This program is free software; you can redistribute it and/or
0006 modify it under the terms of the GNU General Public License
0007 as published by the Free Software Foundation; either version 2
0008 of the License, or (at your option) any later version.
0009 
0010 This program is distributed in the hope that it will be useful,
0011 but WITHOUT ANY WARRANTY; without even the implied warranty of
0012 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0013 GNU General Public License for more details.
0014 
0015 You should have received a copy of the GNU General Public License
0016 along with this program; if not, write to the Free Software
0017 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
0018 
0019 */
0020 #include "documentfactory.h"
0021 
0022 // Qt
0023 #include <QByteArray>
0024 #include <QDateTime>
0025 #include <QMap>
0026 #include <QUndoGroup>
0027 #include <QUrl>
0028 
0029 // KF
0030 
0031 // Local
0032 #include "gwenview_lib_debug.h"
0033 #include <gvdebug.h>
0034 
0035 namespace Gwenview
0036 {
0037 #undef ENABLE_LOG
0038 #undef LOG
0039 // #define ENABLE_LOG
0040 #ifdef ENABLE_LOG
0041 #define LOG(x) qCDebug(GWENVIEW_LIB_LOG) << x
0042 #else
0043 #define LOG(x) ;
0044 #endif
0045 
0046 inline int getMaxUnreferencedImages()
0047 {
0048     int defaultValue = 3;
0049     QByteArray ba = qgetenv("GV_MAX_UNREFERENCED_IMAGES");
0050     if (ba.isEmpty()) {
0051         return defaultValue;
0052     }
0053     LOG("Custom value for max unreferenced images:" << ba);
0054     bool ok;
0055     const int value = ba.toInt(&ok);
0056     return ok ? value : defaultValue;
0057 }
0058 
0059 static const int MAX_UNREFERENCED_IMAGES = getMaxUnreferencedImages();
0060 
0061 /**
0062  * This internal structure holds the document and the last time it has been
0063  * accessed. This access time is used to "garbage collect" the loaded
0064  * documents.
0065  */
0066 struct DocumentInfo {
0067     Document::Ptr mDocument;
0068     QDateTime mLastAccess;
0069 };
0070 
0071 /**
0072  * Our collection of DocumentInfo instances. We keep them as pointers to avoid
0073  * altering DocumentInfo::mDocument refcount, since we rely on it to garbage
0074  * collect documents.
0075  */
0076 using DocumentMap = QMap<QUrl, DocumentInfo *>;
0077 
0078 struct DocumentFactoryPrivate {
0079     DocumentMap mDocumentMap;
0080     QUndoGroup mUndoGroup;
0081 
0082     /**
0083      * Removes items in a map if they are no longer referenced elsewhere
0084      */
0085     void garbageCollect(DocumentMap &map)
0086     {
0087         // Build a map of all unreferenced images. We use a MultiMap because in
0088         // rare cases documents may get accessed at the same millisecond.
0089         // See https://bugs.kde.org/show_bug.cgi?id=296401
0090         using UnreferencedImages = QMultiMap<QDateTime, QUrl>;
0091         UnreferencedImages unreferencedImages;
0092 
0093         DocumentMap::Iterator it = map.begin(), end = map.end();
0094         for (; it != end; ++it) {
0095             DocumentInfo *info = it.value();
0096             if (info->mDocument->ref == 1 && !info->mDocument->isModified()) {
0097                 unreferencedImages.insert(info->mLastAccess, it.key());
0098             }
0099         }
0100 
0101         // Remove oldest unreferenced images. Since the map is sorted by key,
0102         // the oldest one is always unreferencedImages.begin().
0103         for (UnreferencedImages::Iterator unreferencedIt = unreferencedImages.begin(); unreferencedImages.count() > MAX_UNREFERENCED_IMAGES;
0104              unreferencedIt = unreferencedImages.erase(unreferencedIt)) {
0105             const QUrl url = unreferencedIt.value();
0106             LOG("Collecting" << url);
0107             it = map.find(url);
0108             Q_ASSERT(it != map.end());
0109             delete it.value();
0110             map.erase(it);
0111         }
0112 
0113 #ifdef ENABLE_LOG
0114         logDocumentMap(map);
0115 #endif
0116     }
0117 
0118     void logDocumentMap(const DocumentMap &map)
0119     {
0120         LOG("map:");
0121         DocumentMap::ConstIterator it = map.constBegin(), end = map.constEnd();
0122         for (; it != end; ++it) {
0123             LOG("-" << it.key() << "refCount=" << it.value()->mDocument.count() << "lastAccess=" << it.value()->mLastAccess);
0124         }
0125     }
0126 
0127     QList<QUrl> mModifiedDocumentList;
0128 };
0129 
0130 DocumentFactory::DocumentFactory()
0131     : d(new DocumentFactoryPrivate)
0132 {
0133 }
0134 
0135 DocumentFactory::~DocumentFactory()
0136 {
0137     qDeleteAll(d->mDocumentMap);
0138     delete d;
0139 }
0140 
0141 DocumentFactory *DocumentFactory::instance()
0142 {
0143     static DocumentFactory factory;
0144     return &factory;
0145 }
0146 
0147 Document::Ptr DocumentFactory::getCachedDocument(const QUrl &url) const
0148 {
0149     const DocumentInfo *info = d->mDocumentMap.value(url);
0150     return info ? info->mDocument : Document::Ptr();
0151 }
0152 
0153 Document::Ptr DocumentFactory::load(const QUrl &url)
0154 {
0155     GV_RETURN_VALUE_IF_FAIL(!url.isEmpty(), Document::Ptr());
0156     DocumentInfo *info = nullptr;
0157 
0158     DocumentMap::Iterator it = d->mDocumentMap.find(url);
0159 
0160     if (it != d->mDocumentMap.end()) {
0161         LOG(url.fileName() << "url in mDocumentMap");
0162         info = it.value();
0163         info->mLastAccess = QDateTime::currentDateTime();
0164         return info->mDocument;
0165     }
0166 
0167     // At this point we couldn't find the document in the map
0168 
0169     // Start loading the document
0170     LOG(url.fileName() << "loading");
0171     auto doc = new Document(url);
0172     connect(doc, &Document::loaded, this, &DocumentFactory::slotLoaded);
0173     connect(doc, &Document::saved, this, &DocumentFactory::slotSaved);
0174     connect(doc, &Document::modified, this, &DocumentFactory::slotModified);
0175     connect(doc, &Document::busyChanged, this, &DocumentFactory::slotBusyChanged);
0176 
0177     // Make sure that an url passed as command line argument is loaded
0178     // and shown before a possibly long running dirlister on a slow
0179     // network device is started. So start the dirlister after url is
0180     // loaded or failed to load.
0181     connect(doc, &Document::loaded, [this, url]() {
0182         Q_EMIT readyForDirListerStart(url);
0183     });
0184     connect(doc, &Document::loadingFailed, [this, url]() {
0185         Q_EMIT readyForDirListerStart(url);
0186     });
0187     connect(doc, &Document::downSampledImageReady, [this, url]() {
0188         Q_EMIT readyForDirListerStart(url);
0189     });
0190 
0191     doc->reload();
0192 
0193     // Create DocumentInfo instance
0194     info = new DocumentInfo;
0195     Document::Ptr docPtr(doc);
0196     info->mDocument = docPtr;
0197     info->mLastAccess = QDateTime::currentDateTime();
0198 
0199     // Place DocumentInfo in the map
0200     d->mDocumentMap[url] = info;
0201 
0202     d->garbageCollect(d->mDocumentMap);
0203 
0204     return docPtr;
0205 }
0206 
0207 QList<QUrl> DocumentFactory::modifiedDocumentList() const
0208 {
0209     return d->mModifiedDocumentList;
0210 }
0211 
0212 bool DocumentFactory::hasUrl(const QUrl &url) const
0213 {
0214     return d->mDocumentMap.contains(url);
0215 }
0216 
0217 void DocumentFactory::clearCache()
0218 {
0219     qDeleteAll(d->mDocumentMap);
0220     d->mDocumentMap.clear();
0221     d->mModifiedDocumentList.clear();
0222 }
0223 
0224 void DocumentFactory::slotLoaded(const QUrl &url)
0225 {
0226     if (d->mModifiedDocumentList.contains(url)) {
0227         d->mModifiedDocumentList.removeAll(url);
0228         Q_EMIT modifiedDocumentListChanged();
0229         Q_EMIT documentChanged(url);
0230     }
0231 }
0232 
0233 void DocumentFactory::slotSaved(const QUrl &oldUrl, const QUrl &newUrl)
0234 {
0235     const bool oldIsNew = oldUrl == newUrl;
0236     const bool oldUrlWasModified = d->mModifiedDocumentList.removeOne(oldUrl);
0237     bool newUrlWasModified = false;
0238     if (!oldIsNew) {
0239         newUrlWasModified = d->mModifiedDocumentList.removeOne(newUrl);
0240         DocumentInfo *info = d->mDocumentMap.take(oldUrl);
0241         d->mDocumentMap.insert(newUrl, info);
0242     }
0243     d->garbageCollect(d->mDocumentMap);
0244     if (oldUrlWasModified || newUrlWasModified) {
0245         Q_EMIT modifiedDocumentListChanged();
0246     }
0247     if (oldUrlWasModified) {
0248         Q_EMIT documentChanged(oldUrl);
0249     }
0250     if (!oldIsNew) {
0251         Q_EMIT documentChanged(newUrl);
0252     }
0253 }
0254 
0255 void DocumentFactory::slotModified(const QUrl &url)
0256 {
0257     if (!d->mModifiedDocumentList.contains(url)) {
0258         d->mModifiedDocumentList << url;
0259         Q_EMIT modifiedDocumentListChanged();
0260     }
0261     Q_EMIT documentChanged(url);
0262 }
0263 
0264 void DocumentFactory::slotBusyChanged(const QUrl &url, bool busy)
0265 {
0266     Q_EMIT documentBusyStateChanged(url, busy);
0267 }
0268 
0269 QUndoGroup *DocumentFactory::undoGroup()
0270 {
0271     return &d->mUndoGroup;
0272 }
0273 
0274 void DocumentFactory::forget(const QUrl &url)
0275 {
0276     DocumentInfo *info = d->mDocumentMap.take(url);
0277     if (!info) {
0278         return;
0279     }
0280     delete info;
0281 
0282     if (d->mModifiedDocumentList.contains(url)) {
0283         d->mModifiedDocumentList.removeAll(url);
0284         Q_EMIT modifiedDocumentListChanged();
0285     }
0286 }
0287 
0288 } // namespace
0289 
0290 #include "moc_documentfactory.cpp"