1 /*
2 Gwenview: an image viewer
3 Copyright 2007 Aurélien Gâteau <agateau@kde.org>
4 
5 This program is free software; you can redistribute it and/or
6 modify it under the terms of the GNU General Public License
7 as published by the Free Software Foundation; either version 2
8 of the License, or (at your option) any later version.
9 
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU General Public License for more details.
14 
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18 
19 */
20 #include "documentfactory.h"
21 
22 // Qt
23 #include <QByteArray>
24 #include <QDateTime>
25 #include <QMap>
26 #include <QUndoGroup>
27 #include <QUrl>
28 
29 // KF
30 
31 // Local
32 #include "gwenview_lib_debug.h"
33 #include <gvdebug.h>
34 
35 namespace Gwenview
36 {
37 #undef ENABLE_LOG
38 #undef LOG
39 //#define ENABLE_LOG
40 #ifdef ENABLE_LOG
41 #define LOG(x) qCDebug(GWENVIEW_LIB_LOG) << x
42 #else
43 #define LOG(x) ;
44 #endif
45 
getMaxUnreferencedImages()46 inline int getMaxUnreferencedImages()
47 {
48     int defaultValue = 3;
49     QByteArray ba = qgetenv("GV_MAX_UNREFERENCED_IMAGES");
50     if (ba.isEmpty()) {
51         return defaultValue;
52     }
53     LOG("Custom value for max unreferenced images:" << ba);
54     bool ok;
55     int value = ba.toInt(&ok);
56     return ok ? value : defaultValue;
57 }
58 
59 static const int MAX_UNREFERENCED_IMAGES = getMaxUnreferencedImages();
60 
61 /**
62  * This internal structure holds the document and the last time it has been
63  * accessed. This access time is used to "garbage collect" the loaded
64  * documents.
65  */
66 struct DocumentInfo {
67     Document::Ptr mDocument;
68     QDateTime mLastAccess;
69 };
70 
71 /**
72  * Our collection of DocumentInfo instances. We keep them as pointers to avoid
73  * altering DocumentInfo::mDocument refcount, since we rely on it to garbage
74  * collect documents.
75  */
76 using DocumentMap = QMap<QUrl, DocumentInfo *>;
77 
78 struct DocumentFactoryPrivate {
79     DocumentMap mDocumentMap;
80     QUndoGroup mUndoGroup;
81 
82     /**
83      * Removes items in a map if they are no longer referenced elsewhere
84      */
garbageCollectGwenview::DocumentFactoryPrivate85     void garbageCollect(DocumentMap &map)
86     {
87         // Build a map of all unreferenced images. We use a MultiMap because in
88         // rare cases documents may get accessed at the same millisecond.
89         // See https://bugs.kde.org/show_bug.cgi?id=296401
90         using UnreferencedImages = QMultiMap<QDateTime, QUrl>;
91         UnreferencedImages unreferencedImages;
92 
93         DocumentMap::Iterator it = map.begin(), end = map.end();
94         for (; it != end; ++it) {
95             DocumentInfo *info = it.value();
96             if (info->mDocument->ref == 1 && !info->mDocument->isModified()) {
97                 unreferencedImages.insert(info->mLastAccess, it.key());
98             }
99         }
100 
101         // Remove oldest unreferenced images. Since the map is sorted by key,
102         // the oldest one is always unreferencedImages.begin().
103         for (UnreferencedImages::Iterator unreferencedIt = unreferencedImages.begin(); unreferencedImages.count() > MAX_UNREFERENCED_IMAGES;
104              unreferencedIt = unreferencedImages.erase(unreferencedIt)) {
105             QUrl url = unreferencedIt.value();
106             LOG("Collecting" << url);
107             it = map.find(url);
108             Q_ASSERT(it != map.end());
109             delete it.value();
110             map.erase(it);
111         }
112 
113 #ifdef ENABLE_LOG
114         logDocumentMap(map);
115 #endif
116     }
117 
logDocumentMapGwenview::DocumentFactoryPrivate118     void logDocumentMap(const DocumentMap &map)
119     {
120         LOG("map:");
121         DocumentMap::ConstIterator it = map.constBegin(), end = map.constEnd();
122         for (; it != end; ++it) {
123             LOG("-" << it.key() << "refCount=" << it.value()->mDocument.count() << "lastAccess=" << it.value()->mLastAccess);
124         }
125     }
126 
127     QList<QUrl> mModifiedDocumentList;
128 };
129 
DocumentFactory()130 DocumentFactory::DocumentFactory()
131     : d(new DocumentFactoryPrivate)
132 {
133 }
134 
~DocumentFactory()135 DocumentFactory::~DocumentFactory()
136 {
137     qDeleteAll(d->mDocumentMap);
138     delete d;
139 }
140 
instance()141 DocumentFactory *DocumentFactory::instance()
142 {
143     static DocumentFactory factory;
144     return &factory;
145 }
146 
getCachedDocument(const QUrl & url) const147 Document::Ptr DocumentFactory::getCachedDocument(const QUrl &url) const
148 {
149     const DocumentInfo *info = d->mDocumentMap.value(url);
150     return info ? info->mDocument : Document::Ptr();
151 }
152 
load(const QUrl & url)153 Document::Ptr DocumentFactory::load(const QUrl &url)
154 {
155     GV_RETURN_VALUE_IF_FAIL(!url.isEmpty(), Document::Ptr());
156     DocumentInfo *info = nullptr;
157 
158     DocumentMap::Iterator it = d->mDocumentMap.find(url);
159 
160     if (it != d->mDocumentMap.end()) {
161         LOG(url.fileName() << "url in mDocumentMap");
162         info = it.value();
163         info->mLastAccess = QDateTime::currentDateTime();
164         return info->mDocument;
165     }
166 
167     // At this point we couldn't find the document in the map
168 
169     // Start loading the document
170     LOG(url.fileName() << "loading");
171     auto *doc = new Document(url);
172     connect(doc, &Document::loaded, this, &DocumentFactory::slotLoaded);
173     connect(doc, &Document::saved, this, &DocumentFactory::slotSaved);
174     connect(doc, &Document::modified, this, &DocumentFactory::slotModified);
175     connect(doc, &Document::busyChanged, this, &DocumentFactory::slotBusyChanged);
176 
177     // Make sure that an url passed as command line argument is loaded
178     // and shown before a possibly long running dirlister on a slow
179     // network device is started. So start the dirlister after url is
180     // loaded or failed to load.
181     connect(doc, &Document::loaded, [this, url]() {
182         Q_EMIT readyForDirListerStart(url);
183     });
184     connect(doc, &Document::loadingFailed, [this, url]() {
185         Q_EMIT readyForDirListerStart(url);
186     });
187     connect(doc, &Document::downSampledImageReady, [this, url]() {
188         Q_EMIT readyForDirListerStart(url);
189     });
190 
191     doc->reload();
192 
193     // Create DocumentInfo instance
194     info = new DocumentInfo;
195     Document::Ptr docPtr(doc);
196     info->mDocument = docPtr;
197     info->mLastAccess = QDateTime::currentDateTime();
198 
199     // Place DocumentInfo in the map
200     d->mDocumentMap[url] = info;
201 
202     d->garbageCollect(d->mDocumentMap);
203 
204     return docPtr;
205 }
206 
modifiedDocumentList() const207 QList<QUrl> DocumentFactory::modifiedDocumentList() const
208 {
209     return d->mModifiedDocumentList;
210 }
211 
hasUrl(const QUrl & url) const212 bool DocumentFactory::hasUrl(const QUrl &url) const
213 {
214     return d->mDocumentMap.contains(url);
215 }
216 
clearCache()217 void DocumentFactory::clearCache()
218 {
219     qDeleteAll(d->mDocumentMap);
220     d->mDocumentMap.clear();
221     d->mModifiedDocumentList.clear();
222 }
223 
slotLoaded(const QUrl & url)224 void DocumentFactory::slotLoaded(const QUrl &url)
225 {
226     if (d->mModifiedDocumentList.contains(url)) {
227         d->mModifiedDocumentList.removeAll(url);
228         Q_EMIT modifiedDocumentListChanged();
229         Q_EMIT documentChanged(url);
230     }
231 }
232 
slotSaved(const QUrl & oldUrl,const QUrl & newUrl)233 void DocumentFactory::slotSaved(const QUrl &oldUrl, const QUrl &newUrl)
234 {
235     bool oldIsNew = oldUrl == newUrl;
236     bool oldUrlWasModified = d->mModifiedDocumentList.removeOne(oldUrl);
237     bool newUrlWasModified = false;
238     if (!oldIsNew) {
239         newUrlWasModified = d->mModifiedDocumentList.removeOne(newUrl);
240         DocumentInfo *info = d->mDocumentMap.take(oldUrl);
241         d->mDocumentMap.insert(newUrl, info);
242     }
243     d->garbageCollect(d->mDocumentMap);
244     if (oldUrlWasModified || newUrlWasModified) {
245         Q_EMIT modifiedDocumentListChanged();
246     }
247     if (oldUrlWasModified) {
248         Q_EMIT documentChanged(oldUrl);
249     }
250     if (!oldIsNew) {
251         Q_EMIT documentChanged(newUrl);
252     }
253 }
254 
slotModified(const QUrl & url)255 void DocumentFactory::slotModified(const QUrl &url)
256 {
257     if (!d->mModifiedDocumentList.contains(url)) {
258         d->mModifiedDocumentList << url;
259         Q_EMIT modifiedDocumentListChanged();
260     }
261     Q_EMIT documentChanged(url);
262 }
263 
slotBusyChanged(const QUrl & url,bool busy)264 void DocumentFactory::slotBusyChanged(const QUrl &url, bool busy)
265 {
266     Q_EMIT documentBusyStateChanged(url, busy);
267 }
268 
undoGroup()269 QUndoGroup *DocumentFactory::undoGroup()
270 {
271     return &d->mUndoGroup;
272 }
273 
forget(const QUrl & url)274 void DocumentFactory::forget(const QUrl &url)
275 {
276     DocumentInfo *info = d->mDocumentMap.take(url);
277     if (!info) {
278         return;
279     }
280     delete info;
281 
282     if (d->mModifiedDocumentList.contains(url)) {
283         d->mModifiedDocumentList.removeAll(url);
284         Q_EMIT modifiedDocumentListChanged();
285     }
286 }
287 
288 } // namespace
289