1 /*
2  * session.cpp
3  * Copyright 2019, Thorbjørn Lindeijer <bjorn@lindeijer.nl>
4  *
5  * This file is part of Tiled.
6  *
7  * This program is free software; you can redistribute it and/or modify it
8  * under the terms of the GNU General Public License as published by the Free
9  * Software Foundation; either version 2 of the License, or (at your option)
10  * any later version.
11  *
12  * This program is distributed in the hope that it will be useful, but WITHOUT
13  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
15  * more details.
16  *
17  * You should have received a copy of the GNU General Public License along with
18  * this program. If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 #include "session.h"
22 
23 #include "documentmanager.h"
24 #include "preferences.h"
25 #include "utils.h"
26 
27 #include <QFileInfo>
28 
29 #include "qtcompat_p.h"
30 
31 namespace Tiled {
32 
FileHelper(const QString & fileName)33 FileHelper::FileHelper(const QString &fileName)
34     : mDir { QFileInfo(fileName).dir() }
35 {}
36 
setFileName(const QString & fileName)37 void FileHelper::setFileName(const QString &fileName)
38 {
39     mDir = QFileInfo(fileName).dir();
40 }
41 
relative(const QStringList & fileNames) const42 QStringList FileHelper::relative(const QStringList &fileNames) const
43 {
44     QStringList result;
45     for (const QString &fileName : fileNames)
46         result.append(relative(fileName));
47     return result;
48 }
49 
resolve(const QStringList & fileNames) const50 QStringList FileHelper::resolve(const QStringList &fileNames) const
51 {
52     QStringList result;
53     for (const QString &fileName : fileNames)
54         result.append(resolve(fileName));
55     return result;
56 }
57 
58 QHash<const char*, Session::Callbacks> Session::mChangedCallbacks;
59 std::unique_ptr<Session> Session::mCurrent;
60 
Session(const QString & fileName)61 Session::Session(const QString &fileName)
62     : FileHelper            { fileName }
63     , settings              { Utils::jsonSettings(fileName) }
64     , project               { resolve(get<QString>("project")) }
65     , recentFiles           { resolve(get<QStringList>("recentFiles")) }
66     , openFiles             { resolve(get<QStringList>("openFiles")) }
67     , expandedProjectPaths  { resolve(get<QStringList>("expandedProjectPaths")) }
68     , activeFile            { resolve(get<QString>("activeFile")) }
69 {
70     const auto states = get<QVariantMap>("fileStates");
71     for (auto it = states.constBegin(); it != states.constEnd(); ++it)
72         fileStates.insert(resolve(it.key()), it.value().toMap());
73 
74     mSyncSettingsTimer.setInterval(1000);
75     mSyncSettingsTimer.setSingleShot(true);
__anon95f3e21c0102null76     QObject::connect(&mSyncSettingsTimer, &QTimer::timeout, [this] { sync(); });
77 }
78 
~Session()79 Session::~Session()
80 {
81     if (mSyncSettingsTimer.isActive())
82         sync();
83 }
84 
sync()85 void Session::sync()
86 {
87     mSyncSettingsTimer.stop();
88 
89     set("project",              relative(project));
90     set("recentFiles",          relative(recentFiles));
91     set("openFiles",            relative(openFiles));
92     set("expandedProjectPaths", relative(expandedProjectPaths));
93     set("activeFile",           relative(activeFile));
94 
95     QVariantMap states;
96     for (auto it = fileStates.constBegin(); it != fileStates.constEnd(); ++it)
97         states.insert(relative(it.key()), it.value());
98     set("fileStates", states);
99 }
100 
save()101 bool Session::save()
102 {
103     sync();
104     settings->sync();
105     return settings->status() == QSettings::NoError;
106 }
107 
108 /**
109  * This function "moves" the current session to a new location. It happens for
110  * example when saving a project for the first time or saving it under a
111  * different file name.
112  */
setFileName(const QString & fileName)113 void Session::setFileName(const QString &fileName)
114 {
115     // Make sure we have no pending changes to save to our old location
116     if (mSyncSettingsTimer.isActive())
117         sync();
118 
119     auto newSettings = Utils::jsonSettings(fileName);
120 
121     // Copy over all settings
122     const auto keys = settings->allKeys();
123     for (const auto &key : keys)
124         newSettings->setValue(key, settings->value(key));
125 
126     settings = std::move(newSettings);
127 
128     FileHelper::setFileName(fileName);
129 
130     scheduleSync();
131 }
132 
setProject(const QString & fileName)133 void Session::setProject(const QString &fileName)
134 {
135     project = fileName;
136     scheduleSync();
137 }
138 
addRecentFile(const QString & fileName)139 void Session::addRecentFile(const QString &fileName)
140 {
141     // Remember the file by its absolute file path (not the canonical one,
142     // which avoids unexpected paths when symlinks are involved).
143     const QString absoluteFilePath = QDir::cleanPath(QFileInfo(fileName).absoluteFilePath());
144     if (absoluteFilePath.isEmpty())
145         return;
146 
147     recentFiles.removeAll(absoluteFilePath);
148     recentFiles.prepend(absoluteFilePath);
149     while (recentFiles.size() > Preferences::MaxRecentFiles)
150         recentFiles.removeLast();
151 
152     scheduleSync();
153 }
154 
clearRecentFiles()155 void Session::clearRecentFiles()
156 {
157     recentFiles.clear();
158     scheduleSync();
159 }
160 
setOpenFiles(const QStringList & fileNames)161 void Session::setOpenFiles(const QStringList &fileNames)
162 {
163     openFiles = fileNames;
164     scheduleSync();
165 }
166 
setActiveFile(const QString & fileName)167 void Session::setActiveFile(const QString &fileName)
168 {
169     activeFile = fileName;
170     scheduleSync();
171 }
172 
fileState(const QString & fileName) const173 QVariantMap Session::fileState(const QString &fileName) const
174 {
175     return fileStates.value(fileName);
176 }
177 
setFileState(const QString & fileName,const QVariantMap & fileState)178 void Session::setFileState(const QString &fileName, const QVariantMap &fileState)
179 {
180     fileStates.insert(fileName, fileState);
181     scheduleSync();
182 }
183 
setFileStateValue(const QString & fileName,const QString & name,const QVariant & value)184 void Session::setFileStateValue(const QString &fileName, const QString &name, const QVariant &value)
185 {
186     auto &state = fileStates[fileName];
187     auto &v = state[name];
188     if (v != value) {
189         v = value;
190         scheduleSync();
191     }
192 }
193 
lastPathKey(Session::FileType fileType)194 static QString lastPathKey(Session::FileType fileType)
195 {
196     QString key = QLatin1String("last.");
197 
198     switch (fileType) {
199     case Session::ExecutablePath:
200         key.append(QLatin1String("executablePath"));
201         break;
202     case Session::ExportedFile:
203         key.append(QLatin1String("exportedFilePath"));
204         break;
205     case Session::ExternalTileset:
206         key.append(QLatin1String("externalTilesetPath"));
207         break;
208     case Session::ImageFile:
209         key.append(QLatin1String("imagePath"));
210         break;
211     case Session::ObjectTemplateFile:
212         key.append(QLatin1String("objectTemplatePath"));
213         break;
214     case Session::ObjectTypesFile:
215         key.append(QLatin1String("objectTypesPath"));
216         break;
217     case Session::WorkingDirectory:
218         key.append(QLatin1String("workingDirectory"));
219         break;
220     case Session::WorldFile:
221         key.append(QLatin1String("worldFilePath"));
222         break;
223     }
224 
225     return key;
226 }
227 
228 /**
229  * Returns the starting location for a file chooser for the given file type.
230  */
lastPath(FileType fileType,QStandardPaths::StandardLocation fallbackLocation) const231 QString Session::lastPath(FileType fileType, QStandardPaths::StandardLocation fallbackLocation) const
232 {
233     // First see if we can return the last used location for this file type
234     QString path = settings->value(lastPathKey(fileType)).toString();
235     if (!path.isEmpty())
236         return path;
237 
238     // The location of the current document could be helpful
239     if (fallbackLocation == QStandardPaths::DocumentsLocation) {
240         const DocumentManager *documentManager = DocumentManager::instance();
241         const Document *document = documentManager->currentDocument();
242         if (document) {
243             path = QFileInfo(document->fileName()).path();
244             if (!path.isEmpty())
245                 return path;
246         }
247     }
248 
249     // Try the location of the current project
250     if (!project.isEmpty()) {
251         path = QFileInfo(project).path();
252         if (!path.isEmpty())
253             return path;
254     }
255 
256     // Finally, we just open the fallback location
257     return QStandardPaths::writableLocation(fallbackLocation);
258 }
259 
260 /**
261  * \see lastPath()
262  */
setLastPath(FileType fileType,const QString & path)263 void Session::setLastPath(FileType fileType, const QString &path)
264 {
265     if (path.isEmpty())
266         return;
267 
268     settings->setValue(lastPathKey(fileType), path);
269 }
270 
defaultFileName()271 QString Session::defaultFileName()
272 {
273     return Preferences::instance()->dataLocation() + QLatin1String("/default.tiled-session");
274 }
275 
defaultFileNameForProject(const QString & projectFile)276 QString Session::defaultFileNameForProject(const QString &projectFile)
277 {
278     if (projectFile.isEmpty())
279         return defaultFileName();
280 
281     const QFileInfo fileInfo(projectFile);
282 
283     QString sessionFile = fileInfo.path();
284     sessionFile += QLatin1Char('/');
285     sessionFile += fileInfo.completeBaseName();
286     sessionFile += QStringLiteral(".tiled-session");
287 
288     return sessionFile;
289 }
290 
initialize()291 Session &Session::initialize()
292 {
293     Q_ASSERT(!mCurrent);
294     auto &session = switchCurrent(Preferences::instance()->startupSession());
295 
296     // Workaround for users facing issue #2852, bringing their default session
297     // to the right location.
298     if (session.project.isEmpty()) {
299         if (QFileInfo(session.fileName()).fileName() == QLatin1String("default.tiled-session")) {
300             const QString defaultName = defaultFileName();
301             if (session.fileName() != defaultName) {
302                 session.setFileName(defaultName);
303                 Preferences::instance()->setLastSession(defaultName);
304             }
305         }
306     }
307 
308     return session;
309 }
310 
current()311 Session &Session::current()
312 {
313     Q_ASSERT(mCurrent);
314     return *mCurrent;
315 }
316 
317 static void migratePreferences();
318 
switchCurrent(const QString & fileName)319 Session &Session::switchCurrent(const QString &fileName)
320 {
321     const bool initialSession = !mCurrent;
322 
323     // Do nothing if this session is already current
324     if (!initialSession && mCurrent->fileName() == fileName)
325         return *mCurrent;
326 
327     mCurrent = std::make_unique<Session>(fileName);
328     Preferences::instance()->setLastSession(mCurrent->fileName());
329 
330     if (initialSession)
331         migratePreferences();
332 
333     // Call all registered callbacks because any value may have changed
334     for (const auto &callbacks : qAsConst(mChangedCallbacks))
335         for (const auto &callback : callbacks)
336             callback();
337 
338     return *mCurrent;
339 }
340 
deinitialize()341 void Session::deinitialize()
342 {
343     mCurrent.reset();
344 }
345 
346 template<typename T>
migrateToSession(const char * preferencesKey,const char * sessionKey)347 static void migrateToSession(const char *preferencesKey, const char *sessionKey)
348 {
349     auto &session = Session::current();
350     if (session.isSet(sessionKey))
351         return;
352 
353     const auto value = Preferences::instance()->value(QLatin1String(preferencesKey));
354     if (!value.isValid())
355         return;
356 
357     session.set(sessionKey, value.value<T>());
358 }
359 
migratePreferences()360 static void migratePreferences()
361 {
362     // Migrate some preferences to the session for compatibility
363     migrateToSession<bool>("Automapping/WhileDrawing", "automapping.whileDrawing");
364 
365     migrateToSession<QStringList>("LoadedWorlds", "loadedWorlds");
366     migrateToSession<QString>("Storage/StampsDirectory", "stampsFolder");
367 
368     migrateToSession<int>("Map/Orientation", "map.orientation");
369     migrateToSession<int>("Storage/LayerDataFormat", "map.layerDataFormat");
370     migrateToSession<int>("Storage/MapRenderOrder", "map.renderOrder");
371     migrateToSession<bool>("Map/FixedSize", "map.fixedSize");
372     migrateToSession<int>("Map/Width", "map.width");
373     migrateToSession<int>("Map/Height", "map.height");
374     migrateToSession<int>("Map/TileWidth", "map.tileWidth");
375     migrateToSession<int>("Map/TileHeight", "map.tileHeight");
376 
377     migrateToSession<int>("Tileset/Type", "tileset.type");
378     migrateToSession<bool>("Tileset/EmbedInMap", "tileset.embedInMap");
379     migrateToSession<bool>("Tileset/UseTransparentColor", "tileset.useTransparentColor");
380     migrateToSession<QColor>("Tileset/TransparentColor", "tileset.transparentColor");
381     migrateToSession<QSize>("Tileset/TileSize", "tileset.tileSize");
382     migrateToSession<int>("Tileset/Spacing", "tileset.spacing");
383     migrateToSession<int>("Tileset/Margin", "tileset.margin");
384 
385     migrateToSession<QString>("AddPropertyDialog/PropertyType", "property.type");
386 
387     migrateToSession<QStringList>("Console/History", "console.history");
388 
389     migrateToSession<bool>("SaveAsImage/VisibleLayersOnly", "exportAsImage.visibleLayersOnly");
390     migrateToSession<bool>("SaveAsImage/CurrentScale", "exportAsImage.useCurrentScale");
391     migrateToSession<bool>("SaveAsImage/DrawGrid", "exportAsImage.drawTileGrid");
392     migrateToSession<bool>("SaveAsImage/IncludeBackgroundColor", "exportAsImage.includeBackgroundColor");
393 
394     migrateToSession<bool>("ResizeMap/RemoveObjects", "resizeMap.removeObjects");
395 
396     migrateToSession<int>("Animation/FrameDuration", "frame.defaultDuration");
397 
398     migrateToSession<QString>("lastUsedExportFilter", "map.lastUsedExportFilter");
399     migrateToSession<QString>("lastUsedMapFormat", "map.lastUsedFormat");
400     migrateToSession<QString>("lastUsedOpenFilter", "file.lastUsedOpenFilter");
401     migrateToSession<QString>("lastUsedTilesetExportFilter", "tileset.lastUsedExportFilter");
402     migrateToSession<QString>("lastUsedTilesetFilter", "tileset.lastUsedFilter");
403     migrateToSession<QString>("lastUsedTilesetFormat", "tileset.lastUsedFormat");
404 
405     auto &session = Session::current();
406     auto prefs = Preferences::instance();
407 
408     // Migrate some preferences that need manual handling
409     if (session.fileName() == Session::defaultFileName()) {
410         if (prefs->contains(QLatin1String("recentFiles"))) {
411             session.recentFiles = prefs->get<QStringList>("recentFiles/fileNames");
412             session.setOpenFiles(prefs->get<QStringList>("recentFiles/lastOpenFiles"));
413             session.setActiveFile(prefs->get<QString>("recentFiles/lastActive"));
414         }
415 
416         if (prefs->contains(QLatin1String("MapEditor/MapStates"))) {
417             const auto mapStates = prefs->get<QVariantMap>("MapEditor/MapStates");
418 
419             for (auto it = mapStates.begin(); it != mapStates.end(); ++it) {
420                 const QString &fileName = it.key();
421                 auto mapState = it.value().toMap();
422 
423                 const QPointF viewCenter = mapState.value(QLatin1String("viewCenter")).toPointF();
424 
425                 mapState.insert(QLatin1String("viewCenter"), toSettingsValue(viewCenter));
426 
427                 session.setFileState(fileName, mapState);
428             }
429         }
430 
431         if (session.save()) {
432             prefs->remove(QLatin1String("recentFiles"));
433             prefs->remove(QLatin1String("MapEditor/MapStates"));
434         }
435     }
436 }
437 
438 } // namespace Tiled
439