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