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